From 232aaa386e3de06218a3070bfd39829b29a83954 Mon Sep 17 00:00:00 2001 From: Chris Knowles Date: Wed, 5 Jun 2019 01:15:59 -0400 Subject: [PATCH 0001/2244] Fix dependencies Some missing or misspelled dependencies. Checked on Ubuntu 19.04. --- BUILD.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/BUILD.md b/BUILD.md index a9a2e4a2..d8a05ad6 100644 --- a/BUILD.md +++ b/BUILD.md @@ -12,7 +12,7 @@ case, use the [prebuilt server] (so you will not need Java or the Android SDK). ## Requirements You need [adb]. It is available in the [Android SDK platform -tools][platform-tools], or packaged in your distribution (`android-adb-tools`). +tools][platform-tools], or packaged in your distribution (`android-tools-adb`). On Windows, download the [platform-tools][platform-tools-windows] and extract the following files to a directory accessible from your `PATH`: @@ -40,10 +40,10 @@ Install the required packages from your package manager. ```bash # runtime dependencies -sudo apt install ffmpeg libsdl2-2.0.0 +sudo apt install ffmpeg libsdl2-2.0-0 # client build dependencies -sudo apt install make gcc pkg-config meson ninja-build \ +sudo apt install make gcc git pkg-config meson ninja-build \ libavcodec-dev libavformat-dev libavutil-dev \ libsdl2-dev From a13524e7f9d696635983776c548adaa1b9c8508f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 5 Jun 2019 09:52:25 +0200 Subject: [PATCH 0002/2244] Replace android-tools-adb by adb Here is the description of the adb package in Debian: > Description: Android Debug Bridge > > A versatile command line tool that lets you communicate with an > emulator instance or connected Android-powered device. > > This package recommends "android-sdk-platform-tools-common" which > contains the udev rules for Android devices. Without this package, adb > and fastboot need to be running with root permission. And android-tools-adb: > Description: transitional package > > This is a transitional package. It can safely be removed. --- BUILD.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BUILD.md b/BUILD.md index d8a05ad6..6d961d53 100644 --- a/BUILD.md +++ b/BUILD.md @@ -12,7 +12,7 @@ case, use the [prebuilt server] (so you will not need Java or the Android SDK). ## Requirements You need [adb]. It is available in the [Android SDK platform -tools][platform-tools], or packaged in your distribution (`android-tools-adb`). +tools][platform-tools], or packaged in your distribution (`adb`). On Windows, download the [platform-tools][platform-tools-windows] and extract the following files to a directory accessible from your `PATH`: From e2996e85c0e2e75b012b259ba4346b0b6d7b1e9a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 11 Jun 2019 23:00:09 +0200 Subject: [PATCH 0003/2244] Update links to v1.9 in README and BUILD --- BUILD.md | 6 +++--- README.md | 14 +++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/BUILD.md b/BUILD.md index 6d961d53..bfe9173b 100644 --- a/BUILD.md +++ b/BUILD.md @@ -234,10 +234,10 @@ You can then [run](README.md#run) _scrcpy_. ## Prebuilt server - - [`scrcpy-server-v1.8.jar`][direct-scrcpy-server] - _(SHA-256: 839055ef905903bf98ead1b9b8a127fe402b39ad657a81f9a914b2dbcb2ce5c0)_ + - [`scrcpy-server-v1.9.jar`][direct-scrcpy-server] + _(SHA-256: ad7e539f100e48259b646f26982bc63e0a60a81ac87ae135e242855bef69bd1a)_ -[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.8/scrcpy-server-v1.8.jar +[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.9/scrcpy-server-v1.9.jar Download the prebuilt server somewhere, and specify its path during the Meson configuration: diff --git a/README.md b/README.md index 5026ebe2..3d8fcd8d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# scrcpy (v1.8) +# scrcpy (v1.9) 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. @@ -45,13 +45,13 @@ For Gentoo, an [Ebuild] is available: [`scrcpy/`][ebuild-link]. For Windows, for simplicity, prebuilt archives with all the dependencies (including `adb`) are available: - - [`scrcpy-win32-v1.8.zip`][direct-win32] - _(SHA-256: c0c29ed1c66deaa73bdadacd09e598aafb3a117929cf7a314cce1cc45e34de53)_ - - [`scrcpy-win64-v1.8.zip`][direct-win64] - _(SHA-256: 9cc980d07bd8f036ae4e91d0bc6fc3281d7fa8f9752d4913b643c0fb72a19fb7)_ + - [`scrcpy-win32-v1.9.zip`][direct-win32] + _(SHA-256: 3234f7fbcc26b9e399f50b5ca9ed085708954c87fda1b0dd32719d6e7dd861ef)_ + - [`scrcpy-win64-v1.9.zip`][direct-win64] + _(SHA-256: 0088eca1811ea7c7ac350d636c8465b266e6c830bb268770ff88fddbb493077e)_ -[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v1.8/scrcpy-win32-v1.8.zip -[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.8/scrcpy-win64-v1.8.zip +[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v1.9/scrcpy-win32-v1.9.zip +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.9/scrcpy-win64-v1.9.zip You can also [build the app manually][BUILD]. From 19ca6a0d66b5bf0bb997e64c6e5e1c278d79d6fd Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 11 Jun 2019 23:01:23 +0200 Subject: [PATCH 0004/2244] Fix typo in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3d8fcd8d..8fa5fe76 100644 --- a/README.md +++ b/README.md @@ -317,7 +317,7 @@ you are interested, see [issue 14]. | 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 | `Ctrl`+`Shift+`v` | + | copy computer clipboard to device | `Ctrl`+`Shift`+`v` | | enable/disable FPS counter (on stdout) | `Ctrl`+`i` | _¹Double-click on black borders to remove them._ From de2016a48e3016e6f24858afa9c52a9a54e8ab47 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 11 Jun 2019 23:41:00 +0200 Subject: [PATCH 0005/2244] Add link to Snap package in README --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 8fa5fe76..273d380e 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,12 @@ control it using keyboard and mouse. On Linux, you typically need to [build the app manually][BUILD]. Don't worry, it's not that hard. +A [Snap] package is available: [`scrcpy`][snap-link]. + +[snap-link]: https://snapstats.org/snaps/scrcpy + +[snap]: https://en.wikipedia.org/wiki/Snappy_(package_manager) + For Arch Linux, an [AUR] package is available: [`scrcpy`][aur-link]. [AUR]: https://wiki.archlinux.org/index.php/Arch_User_Repository From bcd0a876f7bf52642330410812a6b7100cdeda91 Mon Sep 17 00:00:00 2001 From: zzndb Date: Wed, 12 Jun 2019 17:14:08 +0800 Subject: [PATCH 0006/2244] Fix a spell mistake After commented default portable option in `app/meson.build` get some error and then find this. :) Signed-off-by: Romain Vimont --- 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 d0599bef..d59394f0 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -15,7 +15,7 @@ #define SOCKET_NAME "scrcpy" #define SERVER_FILENAME "scrcpy-server.jar" -#define DEFAULT_SERVER_PATH PREFIX "/share/scrcpy/" SERVER_FLENAME +#define DEFAULT_SERVER_PATH PREFIX "/share/scrcpy/" SERVER_FILENAME #define DEVICE_SERVER_PATH "/data/local/tmp/" SERVER_FILENAME static const char * From 53310a925a495f61d42dd90faa0a0748074d63c5 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 12 Jun 2019 11:26:23 +0200 Subject: [PATCH 0007/2244] Disable portable build by default The default value of a boolean meson option is true. We want non-portable build by default. --- meson_options.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson_options.txt b/meson_options.txt index a443ccb2..354dfe69 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -3,6 +3,6 @@ option('build_server', type: 'boolean', value: true, description: 'Build the ser option('crossbuild_windows', type: 'boolean', value: false, description: 'Build for Windows from Linux') option('windows_noconsole', type: 'boolean', value: false, description: 'Disable console on Windows (pass -mwindows flag)') option('prebuilt_server', type: 'string', description: 'Path of the prebuilt server') -option('portable', type: 'boolean', description: 'Use scrcpy-server.jar from the same directory as the scrcpy executable') +option('portable', type: 'boolean', value: false, description: 'Use scrcpy-server.jar from the same directory as the scrcpy executable') option('skip_frames', type: 'boolean', value: true, description: 'Always display the most recent frame') option('hidpi_support', type: 'boolean', value: true, description: 'Enable High DPI support') From 8ca36406b9893ba7a9af270eec2f81136062d4fb Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 12 Jun 2019 11:43:18 +0200 Subject: [PATCH 0008/2244] Remove compilation flag "skip_frames" It is unused since ebccb9f6cc111e8acfbe10d656cac5c1f1b744a0. --- meson_options.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/meson_options.txt b/meson_options.txt index 354dfe69..e832fafd 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -4,5 +4,4 @@ option('crossbuild_windows', type: 'boolean', value: false, description: 'Build option('windows_noconsole', type: 'boolean', value: false, description: 'Disable console on Windows (pass -mwindows flag)') option('prebuilt_server', type: 'string', description: 'Path of the prebuilt server') option('portable', type: 'boolean', value: false, description: 'Use scrcpy-server.jar from the same directory as the scrcpy executable') -option('skip_frames', type: 'boolean', value: true, description: 'Always display the most recent frame') option('hidpi_support', type: 'boolean', value: true, description: 'Enable High DPI support') From b769083a5bcebebefa5ec50cbacba584421add21 Mon Sep 17 00:00:00 2001 From: Arne Schwabe Date: Wed, 12 Jun 2019 18:36:25 +0200 Subject: [PATCH 0009/2244] Use getPhysicalDisplayToken on Anroid Q+ instead of getBuiltInDisplay This makes the -S (screen off) parameter work on Android Q beta 4 Closes #586 --- .../com/genymobile/scrcpy/wrappers/SurfaceControl.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) 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 bed21b3c..e028dcd8 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java @@ -2,6 +2,7 @@ package com.genymobile.scrcpy.wrappers; import android.annotation.SuppressLint; import android.graphics.Rect; +import android.os.Build; import android.os.IBinder; import android.view.Surface; @@ -77,7 +78,13 @@ public final class SurfaceControl { public static IBinder getBuiltInDisplay(int builtInDisplayId) { try { - return (IBinder) CLASS.getMethod("getBuiltInDisplay", int.class).invoke(null, builtInDisplayId); + // Android Q does not have this method anymore but has a + // replacement. + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + return (IBinder) CLASS.getMethod("getBuiltInDisplay", int.class).invoke(null, builtInDisplayId); + } else { + return (IBinder) CLASS.getMethod("getPhysicalDisplayToken", long.class).invoke(null, builtInDisplayId); + } } catch (Exception e) { throw new AssertionError(e); } From fe758e6e1545db7a4a95af44fd081c75e9a12aba Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 14 Jun 2019 10:11:15 +0200 Subject: [PATCH 0010/2244] Improve comment Rephrase to simplify and add a link to the issue. --- .../java/com/genymobile/scrcpy/wrappers/SurfaceControl.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 e028dcd8..9f479f03 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java @@ -78,8 +78,8 @@ public final class SurfaceControl { public static IBinder getBuiltInDisplay(int builtInDisplayId) { try { - // Android Q does not have this method anymore but has a - // replacement. + // the method signature has changed in Android Q + // if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { return (IBinder) CLASS.getMethod("getBuiltInDisplay", int.class).invoke(null, builtInDisplayId); } else { From 4940746bcba1ef50dec9add25cf231e8596407af Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 14 Jun 2019 10:15:10 +0200 Subject: [PATCH 0011/2244] Remove useless else The if-block ends with a return. --- .../java/com/genymobile/scrcpy/wrappers/SurfaceControl.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 9f479f03..5b5586ff 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java @@ -82,9 +82,8 @@ public final class SurfaceControl { // if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { return (IBinder) CLASS.getMethod("getBuiltInDisplay", int.class).invoke(null, builtInDisplayId); - } else { - return (IBinder) CLASS.getMethod("getPhysicalDisplayToken", long.class).invoke(null, builtInDisplayId); } + return (IBinder) CLASS.getMethod("getPhysicalDisplayToken", long.class).invoke(null, builtInDisplayId); } catch (Exception e) { throw new AssertionError(e); } From 0a233fd27fc9c44dc942d921f3daa70206712129 Mon Sep 17 00:00:00 2001 From: taaem Date: Fri, 14 Jun 2019 18:38:35 +0200 Subject: [PATCH 0012/2244] Fix required java package for Fedora The Java JDK is needed to build the server. The relevant Fedora package is java-devel, not java. Signed-off-by: Romain Vimont --- BUILD.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BUILD.md b/BUILD.md index bfe9173b..68739be3 100644 --- a/BUILD.md +++ b/BUILD.md @@ -70,7 +70,7 @@ sudo dnf install https://download1.rpmfusion.org/free/fedora/rpmfusion-free-rele sudo dnf install SDL2-devel ffms2-devel meson gcc make # server build dependencies -sudo dnf install java +sudo dnf install java-devel ``` From b91ecf52256da73f5c8dca04fb82c13ec826cbd7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 18 Jun 2019 17:13:53 +0200 Subject: [PATCH 0013/2244] Fix --serial help Make explicit that --serial excepts a parameter. --- 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 bf3b7a50..7fd54ed6 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -86,7 +86,7 @@ static void usage(const char *arg0) { " This flag forces to render all frames, at a cost of a\n" " possible increased latency.\n" "\n" - " -s, --serial\n" + " -s, --serial serial\n" " The device serial number. Mandatory only if several devices\n" " are connected to adb.\n" "\n" From 87d7a157a9da80a010bc4f7654d123f9ea3caba7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 20 Jun 2019 10:45:52 +0200 Subject: [PATCH 0014/2244] Reference USBaudio from README --- README.md | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 273d380e..37a83e62 100644 --- a/README.md +++ b/README.md @@ -292,15 +292,12 @@ scrcpy --render-expired-frames ### Forward audio -Audio is not forwarded by _scrcpy_. +Audio is not forwarded by _scrcpy_. Use [USBaudio] (Linux-only). -There is a limited solution using [AOA], implemented in the [`audio`] branch. If -you are interested, see [issue 14]. +Also see [issue #14]. - -[AOA]: https://source.android.com/devices/accessories/aoa2 -[`audio`]: https://github.com/Genymobile/scrcpy/commits/audio -[issue 14]: https://github.com/Genymobile/scrcpy/issues/14 +[USBaudio]: https://github.com/rom1v/usbaudio +[issue #14]: https://github.com/Genymobile/scrcpy/issues/14 ## Shortcuts From bfb3f0842f523b315ec92668bb15818fb3883bab Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 20 Jun 2019 10:59:19 +0200 Subject: [PATCH 0015/2244] Prevent to turn screen off if no control If --no-control is set, then the controller is not initialized (both in the client and the server), so it is not possible to control the device to turn its screen off. See . --- app/src/main.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main.c b/app/src/main.c index 7fd54ed6..ffa2f02d 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -414,6 +414,11 @@ parse_args(struct args *args, int argc, char *argv[]) { } } + if (args->no_control && args->turn_screen_off) { + LOGE("Cannot request to turn screen off if control is disabled"); + return false; + } + return true; } From 91ecb4f218f9a0f348ed8aee47919d6c2f4779a9 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 20 Jun 2019 12:15:45 +0200 Subject: [PATCH 0016/2244] Close socket on error Suggested-by: barry-ran --- app/src/server.c | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/server.c b/app/src/server.c index d59394f0..14b8d41d 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -155,6 +155,7 @@ connect_and_read_byte(uint16_t port) { // is not listening, so read one byte to detect a working connection if (net_recv(socket, &byte, 1) != 1) { // the server is not listening yet behind the adb tunnel + net_close(socket); return INVALID_SOCKET; } return socket; From 439b009a794983a738f3d48e1d07cd721dca112d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 23 Jun 2019 20:47:21 +0200 Subject: [PATCH 0017/2244] Fix expected parameters count in error message --- 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 1e4d10d6..25da0f4e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -68,7 +68,7 @@ public final class Server { @SuppressWarnings("checkstyle:MagicNumber") private static Options createOptions(String... args) { if (args.length != 6) { - throw new IllegalArgumentException("Expecting 5 parameters"); + throw new IllegalArgumentException("Expecting 6 parameters"); } Options options = new Options(); From 056e47e752be3f4555d4af9057f1ad25809524c9 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 23 Jun 2019 20:49:38 +0200 Subject: [PATCH 0018/2244] Replace "cannot" by "could not" --- app/src/controller.c | 2 +- app/src/file_handler.c | 4 +-- app/src/input_manager.c | 34 +++++++++---------- app/src/main.c | 2 +- app/src/recorder.c | 4 +-- app/src/scrcpy.c | 4 +-- app/src/screen.c | 2 +- app/src/server.c | 8 ++--- app/src/stream.c | 2 +- app/src/sys/unix/command.c | 2 +- app/src/sys/win/command.c | 4 +-- .../java/com/genymobile/scrcpy/Server.java | 2 +- .../scrcpy/wrappers/StatusBarManager.java | 4 +-- 13 files changed, 37 insertions(+), 37 deletions(-) diff --git a/app/src/controller.c b/app/src/controller.c index 4b1f4c8b..7f90d787 100644 --- a/app/src/controller.c +++ b/app/src/controller.c @@ -91,7 +91,7 @@ run_controller(void *data) { bool ok = process_msg(controller, &msg); control_msg_destroy(&msg); if (!ok) { - LOGD("Cannot write msg to socket"); + LOGD("Could not write msg to socket"); break; } } diff --git a/app/src/file_handler.c b/app/src/file_handler.c index 051db897..ec53faae 100644 --- a/app/src/file_handler.c +++ b/app/src/file_handler.c @@ -31,7 +31,7 @@ file_handler_init(struct file_handler *file_handler, const char *serial) { if (serial) { file_handler->serial = SDL_strdup(serial); if (!file_handler->serial) { - LOGW("Cannot strdup serial"); + LOGW("Could not strdup serial"); SDL_DestroyCond(file_handler->event_cond); SDL_DestroyMutex(file_handler->mutex); return false; @@ -169,7 +169,7 @@ file_handler_stop(struct file_handler *file_handler) { cond_signal(file_handler->event_cond); if (file_handler->current_process != PROCESS_NONE) { if (!cmd_terminate(file_handler->current_process)) { - LOGW("Cannot terminate install process"); + LOGW("Could not terminate install process"); } cmd_simple_wait(file_handler->current_process, NULL); file_handler->current_process = PROCESS_NONE; diff --git a/app/src/input_manager.c b/app/src/input_manager.c index fb8ef8f0..37e41ca9 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -47,7 +47,7 @@ send_keycode(struct controller *controller, enum android_keycode keycode, if (actions & ACTION_DOWN) { msg.inject_keycode.action = AKEY_EVENT_ACTION_DOWN; if (!controller_push_msg(controller, &msg)) { - LOGW("Cannot request 'inject %s (DOWN)'", name); + LOGW("Could not request 'inject %s (DOWN)'", name); return; } } @@ -55,7 +55,7 @@ send_keycode(struct controller *controller, enum android_keycode keycode, if (actions & ACTION_UP) { msg.inject_keycode.action = AKEY_EVENT_ACTION_UP; if (!controller_push_msg(controller, &msg)) { - LOGW("Cannot request 'inject %s (UP)'", name); + LOGW("Could not request 'inject %s (UP)'", name); } } } @@ -102,7 +102,7 @@ press_back_or_turn_screen_on(struct controller *controller) { msg.type = CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON; if (!controller_push_msg(controller, &msg)) { - LOGW("Cannot request 'turn screen on'"); + LOGW("Could not request 'turn screen on'"); } } @@ -112,7 +112,7 @@ expand_notification_panel(struct controller *controller) { msg.type = CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL; if (!controller_push_msg(controller, &msg)) { - LOGW("Cannot request 'expand notification panel'"); + LOGW("Could not request 'expand notification panel'"); } } @@ -122,7 +122,7 @@ collapse_notification_panel(struct controller *controller) { msg.type = CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL; if (!controller_push_msg(controller, &msg)) { - LOGW("Cannot request 'collapse notification panel'"); + LOGW("Could not request 'collapse notification panel'"); } } @@ -132,7 +132,7 @@ request_device_clipboard(struct controller *controller) { msg.type = CONTROL_MSG_TYPE_GET_CLIPBOARD; if (!controller_push_msg(controller, &msg)) { - LOGW("Cannot request device clipboard"); + LOGW("Could not request device clipboard"); } } @@ -140,7 +140,7 @@ static void set_device_clipboard(struct controller *controller) { char *text = SDL_GetClipboardText(); if (!text) { - LOGW("Cannot get clipboard text: %s", SDL_GetError()); + LOGW("Could not get clipboard text: %s", SDL_GetError()); return; } if (!*text) { @@ -155,7 +155,7 @@ set_device_clipboard(struct controller *controller) { if (!controller_push_msg(controller, &msg)) { SDL_free(text); - LOGW("Cannot request 'set device clipboard'"); + LOGW("Could not request 'set device clipboard'"); } } @@ -167,7 +167,7 @@ set_screen_power_mode(struct controller *controller, msg.set_screen_power_mode.mode = mode; if (!controller_push_msg(controller, &msg)) { - LOGW("Cannot request 'set screen power mode'"); + LOGW("Could not request 'set screen power mode'"); } } @@ -191,7 +191,7 @@ static void clipboard_paste(struct controller *controller) { char *text = SDL_GetClipboardText(); if (!text) { - LOGW("Cannot get clipboard text: %s", SDL_GetError()); + LOGW("Could not get clipboard text: %s", SDL_GetError()); return; } if (!*text) { @@ -205,7 +205,7 @@ clipboard_paste(struct controller *controller) { msg.inject_text.text = text; if (!controller_push_msg(controller, &msg)) { SDL_free(text); - LOGW("Cannot request 'paste clipboard'"); + LOGW("Could not request 'paste clipboard'"); } } @@ -222,12 +222,12 @@ input_manager_process_text_input(struct input_manager *input_manager, msg.type = CONTROL_MSG_TYPE_INJECT_TEXT; msg.inject_text.text = SDL_strdup(event->text); if (!msg.inject_text.text) { - LOGW("Cannot strdup input text"); + LOGW("Could not strdup input text"); return; } if (!controller_push_msg(input_manager->controller, &msg)) { SDL_free(msg.inject_text.text); - LOGW("Cannot request 'inject text'"); + LOGW("Could not request 'inject text'"); } } @@ -368,7 +368,7 @@ input_manager_process_key(struct input_manager *input_manager, struct control_msg msg; if (input_key_from_sdl_to_android(event, &msg)) { if (!controller_push_msg(controller, &msg)) { - LOGW("Cannot request 'inject keycode'"); + LOGW("Could not request 'inject keycode'"); } } } @@ -385,7 +385,7 @@ input_manager_process_mouse_motion(struct input_manager *input_manager, input_manager->screen->frame_size, &msg)) { if (!controller_push_msg(input_manager->controller, &msg)) { - LOGW("Cannot request 'inject mouse motion event'"); + LOGW("Could not request 'inject mouse motion event'"); } } } @@ -431,7 +431,7 @@ input_manager_process_mouse_button(struct input_manager *input_manager, input_manager->screen->frame_size, &msg)) { if (!controller_push_msg(input_manager->controller, &msg)) { - LOGW("Cannot request 'inject mouse button event'"); + LOGW("Could not request 'inject mouse button event'"); } } } @@ -446,7 +446,7 @@ input_manager_process_mouse_wheel(struct input_manager *input_manager, struct control_msg msg; if (mouse_wheel_from_sdl_to_android(event, position, &msg)) { if (!controller_push_msg(input_manager->controller, &msg)) { - LOGW("Cannot request 'inject mouse wheel event'"); + LOGW("Could not request 'inject mouse wheel event'"); } } } diff --git a/app/src/main.c b/app/src/main.c index ffa2f02d..b4f8953d 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -415,7 +415,7 @@ parse_args(struct args *args, int argc, char *argv[]) { } if (args->no_control && args->turn_screen_off) { - LOGE("Cannot request to turn screen off if control is disabled"); + LOGE("Could not request to turn screen off if control is disabled"); return false; } diff --git a/app/src/recorder.c b/app/src/recorder.c index 321a17ee..f0f64a5f 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -33,7 +33,7 @@ recorder_init(struct recorder *recorder, struct size declared_frame_size) { recorder->filename = SDL_strdup(filename); if (!recorder->filename) { - LOGE("Cannot strdup filename"); + LOGE("Could not strdup filename"); return false; } @@ -133,7 +133,7 @@ recorder_write_header(struct recorder *recorder, const AVPacket *packet) { uint8_t *extradata = av_malloc(packet->size * sizeof(uint8_t)); if (!extradata) { - LOGC("Cannot allocate extradata"); + LOGC("Could not allocate extradata"); return false; } diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 761edb69..ede34dd7 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -259,7 +259,7 @@ av_log_callback(void *avcl, int level, const char *fmt, va_list vl) { } char *local_fmt = SDL_malloc(strlen(fmt) + 10); if (!local_fmt) { - LOGC("Cannot allocate string"); + LOGC("Could not allocate string"); return; } // strcpy is safe here, the destination is large enough @@ -391,7 +391,7 @@ scrcpy(const struct scrcpy_options *options) { msg.set_screen_power_mode.mode = SCREEN_POWER_MODE_OFF; if (!controller_push_msg(&controller, &msg)) { - LOGW("Cannot request 'set screen power mode'"); + LOGW("Could not request 'set screen power mode'"); } } diff --git a/app/src/screen.c b/app/src/screen.c index 67b268c5..159d6a47 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -85,7 +85,7 @@ get_optimal_size(struct size current_size, struct size frame_size) { uint32_t h; if (!get_preferred_display_bounds(&display_size)) { - // cannot get display bounds, do not constraint the size + // could not get display bounds, do not constraint the size w = current_size.width; h = current_size.height; } else { diff --git a/app/src/server.c b/app/src/server.c index 14b8d41d..5b593c47 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -35,7 +35,7 @@ get_server_path(void) { // use scrcpy-server.jar in the same directory as the executable char *executable_path = get_executable_path(); if (!executable_path) { - LOGE("Cannot get executable path, " + LOGE("Could not get executable path, " "using " SERVER_FILENAME " from current directory"); // not found, use current directory return SERVER_FILENAME; @@ -47,7 +47,7 @@ get_server_path(void) { size_t len = dirlen + 1 + sizeof(SERVER_FILENAME); char *server_path = SDL_malloc(len); if (!server_path) { - LOGE("Cannot alloc server path string, " + LOGE("Could not alloc server path string, " "using " SERVER_FILENAME " from current directory"); SDL_free(executable_path); return SERVER_FILENAME; @@ -182,7 +182,7 @@ close_socket(socket_t *socket) { SDL_assert(*socket != INVALID_SOCKET); net_shutdown(*socket, SHUT_RDWR); if (!net_close(*socket)) { - LOGW("Cannot close socket"); + LOGW("Could not close socket"); return; } *socket = INVALID_SOCKET; @@ -306,7 +306,7 @@ server_stop(struct server *server) { SDL_assert(server->process != PROCESS_NONE); if (!cmd_terminate(server->process)) { - LOGW("Cannot terminate server"); + LOGW("Could not terminate server"); } cmd_simple_wait(server->process, NULL); // ignore exit code diff --git a/app/src/stream.c b/app/src/stream.c index 4f38cecf..30151859 100644 --- a/app/src/stream.c +++ b/app/src/stream.c @@ -100,7 +100,7 @@ read_packet_with_meta(void *opaque, uint8_t *buf, int buf_size) { if (pts != NO_PTS && !receiver_state_push_meta(state, pts)) { LOGE("Could not store PTS for recording"); - // we cannot save the PTS, the recording would be broken + // we could not save the PTS, the recording would be broken return AVERROR(ENOMEM); } } diff --git a/app/src/sys/unix/command.c b/app/src/sys/unix/command.c index 55aea5e8..9e00ccf8 100644 --- a/app/src/sys/unix/command.c +++ b/app/src/sys/unix/command.c @@ -94,7 +94,7 @@ cmd_simple_wait(pid_t pid, int *exit_code) { int status; int code; if (waitpid(pid, &status, 0) == -1 || !WIFEXITED(status)) { - // cannot wait, or exited unexpectedly, probably by a signal + // could not wait, or exited unexpectedly, probably by a signal code = -1; } else { code = WEXITSTATUS(status); diff --git a/app/src/sys/win/command.c b/app/src/sys/win/command.c index 484ce9f0..f23730a0 100644 --- a/app/src/sys/win/command.c +++ b/app/src/sys/win/command.c @@ -33,7 +33,7 @@ cmd_execute(const char *path, const char *const argv[], HANDLE *handle) { wchar_t *wide = utf8_to_wide_char(cmd); if (!wide) { - LOGC("Cannot allocate wide char string"); + LOGC("Could not allocate wide char string"); return PROCESS_ERROR_GENERIC; } @@ -67,7 +67,7 @@ cmd_simple_wait(HANDLE handle, DWORD *exit_code) { DWORD code; if (WaitForSingleObject(handle, INFINITE) != WAIT_OBJECT_0 || !GetExitCodeProcess(handle, &code)) { - // cannot wait or retrieve the exit code + // could not wait or retrieve the exit code code = -1; // max value, it's unsigned } if (exit_code) { diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 25da0f4e..3bd2fcdc 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -116,7 +116,7 @@ public final class Server { try { new File(SERVER_PATH).delete(); } catch (Exception e) { - Ln.e("Cannot unlink server", e); + Ln.e("Could not unlink server", e); } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java index 74003b64..7cd28da6 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java @@ -29,7 +29,7 @@ public class StatusBarManager { try { expandNotificationsPanelMethod.invoke(manager); } catch (InvocationTargetException | IllegalAccessException e) { - Ln.e("Cannot invoke ServiceBarManager.expandNotificationsPanel()", e); + Ln.e("Could not invoke ServiceBarManager.expandNotificationsPanel()", e); } } @@ -45,7 +45,7 @@ public class StatusBarManager { try { collapsePanelsMethod.invoke(manager); } catch (InvocationTargetException | IllegalAccessException e) { - Ln.e("Cannot invoke ServiceBarManager.collapsePanels()", e); + Ln.e("Could not invoke ServiceBarManager.collapsePanels()", e); } } } From 8e65c10720b5713d642241508c38d831012d9f06 Mon Sep 17 00:00:00 2001 From: beango1 Date: Sun, 23 Jun 2019 13:02:34 -0400 Subject: [PATCH 0019/2244] Add option --window-title Add an option to set a custom window title. Signed-off-by: Romain Vimont --- app/src/main.c | 12 ++++++++++++ app/src/scrcpy.c | 5 ++++- app/src/scrcpy.h | 1 + app/src/screen.c | 4 ++-- app/src/screen.h | 2 +- 5 files changed, 20 insertions(+), 4 deletions(-) diff --git a/app/src/main.c b/app/src/main.c index b4f8953d..e322cf7a 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -17,6 +17,7 @@ struct args { const char *serial; const char *crop; const char *record_filename; + const char *window_title; enum recorder_format record_format; bool fullscreen; bool no_control; @@ -103,6 +104,9 @@ static void usage(const char *arg0) { " -v, --version\n" " Print the version of scrcpy.\n" "\n" + " --window-title text\n" + " Set a custom window title.\n" + "\n" "Shortcuts:\n" "\n" " Ctrl+f\n" @@ -295,6 +299,7 @@ guess_record_format(const char *filename) { } #define OPT_RENDER_EXPIRED_FRAMES 1000 +#define OPT_WINDOW_TITLE 1001 static bool parse_args(struct args *args, int argc, char *argv[]) { @@ -316,6 +321,8 @@ parse_args(struct args *args, int argc, char *argv[]) { {"show-touches", no_argument, NULL, 't'}, {"turn-screen-off", no_argument, NULL, 'S'}, {"version", no_argument, NULL, 'v'}, + {"window-title", required_argument, NULL, + OPT_WINDOW_TITLE}, {NULL, 0, NULL, 0 }, }; int c; @@ -378,6 +385,9 @@ parse_args(struct args *args, int argc, char *argv[]) { case OPT_RENDER_EXPIRED_FRAMES: args->render_expired_frames = true; break; + case OPT_WINDOW_TITLE: + args->window_title = optarg; + break; default: // getopt prints the error message on stderr return false; @@ -434,6 +444,7 @@ main(int argc, char *argv[]) { .serial = NULL, .crop = NULL, .record_filename = NULL, + .window_title = NULL, .record_format = 0, .help = false, .version = false, @@ -478,6 +489,7 @@ main(int argc, char *argv[]) { .crop = args.crop, .port = args.port, .record_filename = args.record_filename, + .window_title = args.window_title, .record_format = args.record_format, .max_size = args.max_size, .bit_rate = args.bit_rate, diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index ede34dd7..d9088155 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -380,7 +380,10 @@ scrcpy(const struct scrcpy_options *options) { controller_started = true; } - if (!screen_init_rendering(&screen, device_name, frame_size, + const char *window_title = + options->window_title ? options->window_title : device_name; + + if (!screen_init_rendering(&screen, window_title, frame_size, options->always_on_top)) { goto end; } diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index d705d2db..fd86bad1 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -9,6 +9,7 @@ struct scrcpy_options { const char *serial; const char *crop; const char *record_filename; + const char *window_title; enum recorder_format record_format; uint16_t port; uint16_t max_size; diff --git a/app/src/screen.c b/app/src/screen.c index 159d6a47..18d24dda 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -134,7 +134,7 @@ create_texture(SDL_Renderer *renderer, struct size frame_size) { } bool -screen_init_rendering(struct screen *screen, const char *device_name, +screen_init_rendering(struct screen *screen, const char *window_title, struct size frame_size, bool always_on_top) { screen->frame_size = frame_size; @@ -152,7 +152,7 @@ screen_init_rendering(struct screen *screen, const char *device_name, #endif } - screen->window = SDL_CreateWindow(device_name, SDL_WINDOWPOS_UNDEFINED, + screen->window = SDL_CreateWindow(window_title, SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, window_size.width, window_size.height, window_flags); diff --git a/app/src/screen.h b/app/src/screen.h index 5734fdc2..63da6aa5 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -44,7 +44,7 @@ screen_init(struct screen *screen); // initialize screen, create window, renderer and texture (window is hidden) bool -screen_init_rendering(struct screen *screen, const char *device_name, +screen_init_rendering(struct screen *screen, const char *window_title, struct size frame_size, bool always_on_top); // show the window From e4ac943d86e6328f53eb63f86164da534b293524 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 24 Jun 2019 21:36:54 +0200 Subject: [PATCH 0020/2244] Document --window-title in README --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 37a83e62..c0a4c510 100644 --- a/README.md +++ b/README.md @@ -289,6 +289,14 @@ latency), use: scrcpy --render-expired-frames ``` +### Custom window title + +By default, the window title is the device model. It can be changed: + +```bash +scrcpy --window-title 'My device' +``` + ### Forward audio From 49612561233b3cde41339ee08c71892e42649706 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 26 Jun 2019 23:50:39 +0200 Subject: [PATCH 0021/2244] Close decoder on stream ended Add missing call to decoder_close(). --- app/src/stream.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/stream.c b/app/src/stream.c index 30151859..e85834c1 100644 --- a/app/src/stream.c +++ b/app/src/stream.c @@ -198,7 +198,7 @@ run_stream(void *data) { if (stream->recorder && !recorder_open(stream->recorder, codec)) { LOGE("Could not open recorder"); - goto finally_close_input; + goto finally_close_decoder; } AVPacket packet; @@ -248,6 +248,10 @@ quit: if (stream->recorder) { recorder_close(stream->recorder); } +finally_close_decoder: + if (stream->decoder) { + decoder_close(stream->decoder); + } finally_close_input: avformat_close_input(&format_ctx); finally_free_avio_ctx: From 3c55d0c69b089a1a282b8803dcd076404226901e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 12 Jul 2019 21:05:45 +0200 Subject: [PATCH 0022/2244] Fix double-free on error If writing the recording header fails, do not clean the resources immediately to avoid double-free. --- app/src/recorder.c | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/src/recorder.c b/app/src/recorder.c index f0f64a5f..3de8257a 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -151,9 +151,6 @@ recorder_write_header(struct recorder *recorder, const AVPacket *packet) { int ret = avformat_write_header(recorder->ctx, NULL); if (ret < 0) { LOGE("Failed to write header to %s", recorder->filename); - SDL_free(extradata); - avio_closep(&recorder->ctx->pb); - avformat_free_context(recorder->ctx); return false; } From 9dea6d23848fe68450b003067cc231f914332c6b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 29 Jul 2019 14:58:38 +0200 Subject: [PATCH 0023/2244] Add me as copyright owner --- LICENSE | 1 + README.md | 1 + 2 files changed, 2 insertions(+) diff --git a/LICENSE b/LICENSE index cea43741..3d6840b1 100644 --- a/LICENSE +++ b/LICENSE @@ -188,6 +188,7 @@ identification within third-party archives. Copyright (C) 2018 Genymobile + Copyright (C) 2018-2019 Romain Vimont Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index 37a83e62..515b2a40 100644 --- a/README.md +++ b/README.md @@ -372,6 +372,7 @@ Read the [developers page]. ## Licence Copyright (C) 2018 Genymobile + Copyright (C) 2018-2019 Romain Vimont Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From 3b69463e61becd3e36e27abb3bb43eb77389164c Mon Sep 17 00:00:00 2001 From: Jonathan Date: Sun, 21 Jul 2019 10:08:27 +0200 Subject: [PATCH 0024/2244] Update README.md Signed-off-by: Romain Vimont --- README.md | 52 ++++++++++++++++++++++++++-------------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 515b2a40..997ae896 100644 --- a/README.md +++ b/README.md @@ -2,14 +2,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. -It works on _GNU/Linux_, _Windows_ and _MacOS_. +It works on _GNU/Linux_, _Windows_ and _macOS_. ![screenshot](assets/screenshot-debian-600.jpg) ## Requirements -The Android part requires at least API 21 (Android 5.0). +The Android device requires at least API 21 (Android 5.0). Make sure you [enabled adb debugging][enable-adb] on your device(s). @@ -62,7 +62,7 @@ For Windows, for simplicity, prebuilt archives with all the dependencies You can also [build the app manually][BUILD]. -### Mac OS +### macOS The application is available in [Homebrew]. Just install it: @@ -101,22 +101,22 @@ scrcpy --help ### Reduce size Sometimes, it is useful to mirror an Android device at a lower definition to -increase performances. +increase performance. -To limit both width and height to some value (e.g. 1024): +To limit both the width and height to some value (e.g. 1024): ```bash scrcpy --max-size 1024 scrcpy -m 1024 # short version ``` -The other dimension is computed to that the device aspect-ratio is preserved. +The other dimension is computed to that the device aspect ratio is preserved. That way, a device in 1920×1080 will be mirrored at 1024×576. ### Change bit-rate -The default bit-rate is 8Mbps. To change the video bitrate (e.g. to 2Mbps): +The default bit-rate is 8 Mbps. To change the video bitrate (e.g. to 2 Mbps): ```bash scrcpy --bit-rate 2M @@ -128,7 +128,7 @@ scrcpy -b 2M # short version The device screen may be cropped to mirror only part of the screen. -This is useful for example to mirror only 1 eye of the Oculus Go: +This is useful for example to mirror only one eye of the Oculus Go: ```bash scrcpy --crop 1224:1440:0:0 # 1224x1440 at offset (0,0) @@ -304,24 +304,24 @@ Also see [issue #14]. | Action | Shortcut | | -------------------------------------- |:---------------------------- | - | switch fullscreen mode | `Ctrl`+`f` | - | 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)_ (`Cmd`+`↑` on MacOS) | - | click on `VOLUME_DOWN` | `Ctrl`+`↓` _(down)_ (`Cmd`+`↓` on MacOS) | - | click on `POWER` | `Ctrl`+`p` | - | power on | _Right-click²_ | - | turn device screen off (keep mirroring)| `Ctrl`+`o` | - | 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 | `Ctrl`+`Shift`+`v` | - | enable/disable FPS counter (on stdout) | `Ctrl`+`i` | + | Switch fullscreen mode | `Ctrl`+`f` | + | 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)_ (`Cmd`+`↑` on macOS) | + | Click on `VOLUME_DOWN` | `Ctrl`+`↓` _(down)_ (`Cmd`+`↓` on macOS) | + | Click on `POWER` | `Ctrl`+`p` | + | Power on | _Right-click²_ | + | Turn device screen off (keep mirroring)| `Ctrl`+`o` | + | 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 | `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._ From 02692ffa42c66c1c3dc93752c2fe2bec74e0240b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 29 Jul 2019 15:19:07 +0200 Subject: [PATCH 0025/2244] Rename "build_" to "compile_" Recent versions of meson complain about an option having name starting with "build_": > DEPRECATION: Option uses prefix "build_", which is reserved for Meson. > This will become an error in the future. Use "compile_" instead. --- Makefile.CrossWindows | 10 +++++----- meson.build | 4 ++-- meson_options.txt | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Makefile.CrossWindows b/Makefile.CrossWindows index 7955c544..49adcfb7 100644 --- a/Makefile.CrossWindows +++ b/Makefile.CrossWindows @@ -44,7 +44,7 @@ clean: build-server: [ -d "$(SERVER_BUILD_DIR)" ] || ( mkdir "$(SERVER_BUILD_DIR)" && \ meson "$(SERVER_BUILD_DIR)" \ - --buildtype release -Dbuild_app=false ) + --buildtype release -Dcompile_app=false ) ninja -C "$(SERVER_BUILD_DIR)" prepare-deps-win32: @@ -56,7 +56,7 @@ build-win32: prepare-deps-win32 --cross-file cross_win32.txt \ --buildtype release --strip -Db_lto=true \ -Dcrossbuild_windows=true \ - -Dbuild_server=false \ + -Dcompile_server=false \ -Dportable=true ) ninja -C "$(WIN32_BUILD_DIR)" @@ -66,7 +66,7 @@ build-win32-noconsole: prepare-deps-win32 --cross-file cross_win32.txt \ --buildtype release --strip -Db_lto=true \ -Dcrossbuild_windows=true \ - -Dbuild_server=false \ + -Dcompile_server=false \ -Dwindows_noconsole=true \ -Dportable=true ) ninja -C "$(WIN32_NOCONSOLE_BUILD_DIR)" @@ -80,7 +80,7 @@ build-win64: prepare-deps-win64 --cross-file cross_win64.txt \ --buildtype release --strip -Db_lto=true \ -Dcrossbuild_windows=true \ - -Dbuild_server=false \ + -Dcompile_server=false \ -Dportable=true ) ninja -C "$(WIN64_BUILD_DIR)" @@ -90,7 +90,7 @@ build-win64-noconsole: prepare-deps-win64 --cross-file cross_win64.txt \ --buildtype release --strip -Db_lto=true \ -Dcrossbuild_windows=true \ - -Dbuild_server=false \ + -Dcompile_server=false \ -Dwindows_noconsole=true \ -Dportable=true ) ninja -C "$(WIN64_NOCONSOLE_BUILD_DIR)" diff --git a/meson.build b/meson.build index 053d8c94..bdd4a879 100644 --- a/meson.build +++ b/meson.build @@ -3,11 +3,11 @@ project('scrcpy', 'c', meson_version: '>= 0.37', default_options: 'c_std=c11') -if get_option('build_app') +if get_option('compile_app') subdir('app') endif -if get_option('build_server') +if get_option('compile_server') subdir('server') endif diff --git a/meson_options.txt b/meson_options.txt index e832fafd..d93161e3 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -1,5 +1,5 @@ -option('build_app', type: 'boolean', value: true, description: 'Build the client') -option('build_server', type: 'boolean', value: true, description: 'Build the server') +option('compile_app', type: 'boolean', value: true, description: 'Build the client') +option('compile_server', type: 'boolean', value: true, description: 'Build the server') option('crossbuild_windows', type: 'boolean', value: false, description: 'Build for Windows from Linux') option('windows_noconsole', type: 'boolean', value: false, description: 'Disable console on Windows (pass -mwindows flag)') option('prebuilt_server', type: 'string', description: 'Path of the prebuilt server') From 6b3d9e3eab1d9ba4250300eccd04528dbee9023a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 30 Jul 2019 12:17:33 +0200 Subject: [PATCH 0026/2244] Add unit test for device message serialization There was a test for the deserialization, but not for the serialization. --- .../scrcpy/DeviceMessageWriterTest.java | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 server/src/test/java/com/genymobile/scrcpy/DeviceMessageWriterTest.java diff --git a/server/src/test/java/com/genymobile/scrcpy/DeviceMessageWriterTest.java b/server/src/test/java/com/genymobile/scrcpy/DeviceMessageWriterTest.java new file mode 100644 index 00000000..df12f647 --- /dev/null +++ b/server/src/test/java/com/genymobile/scrcpy/DeviceMessageWriterTest.java @@ -0,0 +1,35 @@ +package com.genymobile.scrcpy; + +import org.junit.Assert; +import org.junit.Test; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +public class DeviceMessageWriterTest { + + @Test + public void testSerializeClipboard() throws IOException { + DeviceMessageWriter writer = new DeviceMessageWriter(); + + String text = "aéûoç"; + byte[] data = text.getBytes(StandardCharsets.UTF_8); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(DeviceMessage.TYPE_CLIPBOARD); + dos.writeShort(data.length); + dos.write(data); + + byte[] expected = bos.toByteArray(); + + DeviceMessage msg = DeviceMessage.createClipboard(text); + bos = new ByteArrayOutputStream(); + writer.writeTo(msg, bos); + + byte[] actual = bos.toByteArray(); + + Assert.assertArrayEquals(expected, actual); + } +} From a90ccbdf3b37694aac8c34cd6d0837d511c81843 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 31 Jul 2019 01:48:32 +0200 Subject: [PATCH 0027/2244] Add option to change the push target A drag & drop always pushed the file to /sdcard/. Add an option to customize the target directory. Fixes --- README.md | 5 +++++ app/src/device.h | 1 - app/src/file_handler.c | 21 ++++++++++++++------- app/src/file_handler.h | 4 +++- app/src/main.c | 14 ++++++++++++++ app/src/scrcpy.c | 3 ++- app/src/scrcpy.h | 1 + 7 files changed, 39 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index dbc92e8a..25d600c3 100644 --- a/README.md +++ b/README.md @@ -251,6 +251,11 @@ _scrcpy_ window. There is no visual feedback, a log is printed to the console. +The target directory can be changed on start: + +```bash +scrcpy --push-target /sdcard/foo/bar/ +``` ### Read-only diff --git a/app/src/device.h b/app/src/device.h index f3449e5e..828443d7 100644 --- a/app/src/device.h +++ b/app/src/device.h @@ -7,7 +7,6 @@ #include "net.h" #define DEVICE_NAME_FIELD_LENGTH 64 -#define DEVICE_SDCARD_PATH "/sdcard/" // name must be at least DEVICE_NAME_FIELD_LENGTH bytes bool diff --git a/app/src/file_handler.c b/app/src/file_handler.c index ec53faae..e02ca2a9 100644 --- a/app/src/file_handler.c +++ b/app/src/file_handler.c @@ -5,17 +5,19 @@ #include "config.h" #include "command.h" -#include "device.h" #include "lock_util.h" #include "log.h" +#define DEFAULT_PUSH_TARGET "/sdcard/" + static void file_handler_request_destroy(struct file_handler_request *req) { SDL_free(req->file); } bool -file_handler_init(struct file_handler *file_handler, const char *serial) { +file_handler_init(struct file_handler *file_handler, const char *serial, + const char *push_target) { cbuf_init(&file_handler->queue); @@ -46,6 +48,8 @@ file_handler_init(struct file_handler *file_handler, const char *serial) { file_handler->stopped = false; file_handler->current_process = PROCESS_NONE; + file_handler->push_target = push_target ? push_target : DEFAULT_PUSH_TARGET; + return true; } @@ -67,8 +71,8 @@ install_apk(const char *serial, const char *file) { } static process_t -push_file(const char *serial, const char *file) { - return adb_push(serial, file, DEVICE_SDCARD_PATH); +push_file(const char *serial, const char *file, const char *push_target) { + return adb_push(serial, file, push_target); } bool @@ -124,7 +128,8 @@ run_file_handler(void *data) { process = install_apk(file_handler->serial, req.file); } else { LOGI("Pushing %s...", req.file); - process = push_file(file_handler->serial, req.file); + process = push_file(file_handler->serial, req.file, + file_handler->push_target); } file_handler->current_process = process; mutex_unlock(file_handler->mutex); @@ -137,9 +142,11 @@ run_file_handler(void *data) { } } else { if (process_check_success(process, "adb push")) { - LOGI("%s successfully pushed to /sdcard/", req.file); + LOGI("%s successfully pushed to %s", req.file, + file_handler->push_target); } else { - LOGE("Failed to push %s to /sdcard/", req.file); + LOGE("Failed to push %s to %s", req.file, + file_handler->push_target); } } diff --git a/app/src/file_handler.h b/app/src/file_handler.h index 22245105..3418c532 100644 --- a/app/src/file_handler.h +++ b/app/src/file_handler.h @@ -22,6 +22,7 @@ struct file_handler_request_queue CBUF(struct file_handler_request, 16); struct file_handler { char *serial; + const char *push_target; SDL_Thread *thread; SDL_mutex *mutex; SDL_cond *event_cond; @@ -32,7 +33,8 @@ struct file_handler { }; bool -file_handler_init(struct file_handler *file_handler, const char *serial); +file_handler_init(struct file_handler *file_handler, const char *serial, + const char *push_target); void file_handler_destroy(struct file_handler *file_handler); diff --git a/app/src/main.c b/app/src/main.c index e322cf7a..7a1cb9db 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -18,6 +18,7 @@ struct args { const char *crop; const char *record_filename; const char *window_title; + const char *push_target; enum recorder_format record_format; bool fullscreen; bool no_control; @@ -76,6 +77,11 @@ static void usage(const char *arg0) { " Set the TCP port the client listens on.\n" " Default is %d.\n" "\n" + " --push-target path\n" + " Set the target directory for pushing files to the device by\n" + " drag & drop. It is passed as-is to \"adb push\".\n" + " Default is \"/sdcard/\".\n" + "\n" " -r, --record file.mp4\n" " Record screen to file.\n" " The format is determined by the -F/--record-format option if\n" @@ -300,6 +306,7 @@ guess_record_format(const char *filename) { #define OPT_RENDER_EXPIRED_FRAMES 1000 #define OPT_WINDOW_TITLE 1001 +#define OPT_PUSH_TARGET 1002 static bool parse_args(struct args *args, int argc, char *argv[]) { @@ -313,6 +320,8 @@ parse_args(struct args *args, int argc, char *argv[]) { {"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, 'f'}, {"render-expired-frames", no_argument, NULL, @@ -388,6 +397,9 @@ parse_args(struct args *args, int argc, char *argv[]) { case OPT_WINDOW_TITLE: args->window_title = optarg; break; + case OPT_PUSH_TARGET: + args->push_target = optarg; + break; default: // getopt prints the error message on stderr return false; @@ -445,6 +457,7 @@ main(int argc, char *argv[]) { .crop = NULL, .record_filename = NULL, .window_title = NULL, + .push_target = NULL, .record_format = 0, .help = false, .version = false, @@ -490,6 +503,7 @@ main(int argc, char *argv[]) { .port = args.port, .record_filename = args.record_filename, .window_title = args.window_title, + .push_target = args.push_target, .record_format = args.record_format, .max_size = args.max_size, .bit_rate = args.bit_rate, diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index d9088155..01dc52e4 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -334,7 +334,8 @@ scrcpy(const struct scrcpy_options *options) { video_buffer_initialized = true; if (options->control) { - if (!file_handler_init(&file_handler, server.serial)) { + if (!file_handler_init(&file_handler, server.serial, + options->push_target)) { goto end; } file_handler_initialized = true; diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index fd86bad1..faeb246f 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -10,6 +10,7 @@ struct scrcpy_options { const char *crop; const char *record_filename; const char *window_title; + const char *push_target; enum recorder_format record_format; uint16_t port; uint16_t max_size; From 63af7fbafe3740b17b5495a8230298eff9d45581 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 31 Jul 2019 01:55:32 +0200 Subject: [PATCH 0028/2244] Reduce latency by 1 frame To packetize the H.264 raw stream, av_parser_parse2() (called by av_read_frame()) knows that it has received a full frame only after it has received some data for the next frame. As a consequence, the client always waited until the next frame before sending the current frame to the decoder! On the device side, we know packets boundaries. To reduce latency, make the device always transmit the "frame meta" to packetize the stream manually (it was already implemented to send PTS, but only enabled on recording). On the client side, replace av_read_frame() by manual packetizing and parsing. --- app/src/recorder.c | 10 ++ app/src/scrcpy.c | 1 - app/src/server.c | 2 +- app/src/server.h | 1 - app/src/stream.c | 335 ++++++++++++++++++++++----------------------- app/src/stream.h | 18 +-- 6 files changed, 180 insertions(+), 187 deletions(-) diff --git a/app/src/recorder.c b/app/src/recorder.c index 3de8257a..c14394a3 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -166,11 +166,21 @@ recorder_rescale_packet(struct recorder *recorder, AVPacket *packet) { bool recorder_write(struct recorder *recorder, AVPacket *packet) { if (!recorder->header_written) { + if (packet->pts != AV_NOPTS_VALUE) { + LOGE("The first packet is not a config packet"); + return false; + } bool ok = recorder_write_header(recorder, packet); if (!ok) { return false; } recorder->header_written = true; + return true; + } + + if (packet->pts == AV_NOPTS_VALUE) { + // ignore config packets + return true; } recorder_rescale_packet(recorder, packet); diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 01dc52e4..ed988778 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -277,7 +277,6 @@ scrcpy(const struct scrcpy_options *options) { .local_port = options->port, .max_size = options->max_size, .bit_rate = options->bit_rate, - .send_frame_meta = record, .control = options->control, }; if (!server_start(&server, options->serial, ¶ms)) { diff --git a/app/src/server.c b/app/src/server.c index 5b593c47..85b1b6b8 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -130,7 +130,7 @@ execute_server(struct server *server, const struct server_params *params) { bit_rate_string, server->tunnel_forward ? "true" : "false", params->crop ? params->crop : "-", - params->send_frame_meta ? "true" : "false", + "true", // always send frame meta (packet boundaries + timestamp) params->control ? "true" : "false", }; return adb_execute(server->serial, cmd, sizeof(cmd) / sizeof(cmd[0])); diff --git a/app/src/server.h b/app/src/server.h index 74a6cac8..4970d64e 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -34,7 +34,6 @@ struct server_params { uint16_t local_port; uint16_t max_size; uint32_t bit_rate; - bool send_frame_meta; bool control; }; diff --git a/app/src/stream.c b/app/src/stream.c index e85834c1..0396bf60 100644 --- a/app/src/stream.c +++ b/app/src/stream.c @@ -22,54 +22,8 @@ #define HEADER_SIZE 12 #define NO_PTS UINT64_C(-1) -static struct frame_meta * -frame_meta_new(uint64_t pts) { - struct frame_meta *meta = SDL_malloc(sizeof(*meta)); - if (!meta) { - return meta; - } - meta->pts = pts; - meta->next = NULL; - return meta; -} - -static void -frame_meta_delete(struct frame_meta *frame_meta) { - SDL_free(frame_meta); -} - static bool -receiver_state_push_meta(struct receiver_state *state, uint64_t pts) { - struct frame_meta *frame_meta = frame_meta_new(pts); - if (!frame_meta) { - return false; - } - - // append to the list - // (iterate to find the last item, in practice the list should be tiny) - struct frame_meta **p = &state->frame_meta_queue; - while (*p) { - p = &(*p)->next; - } - *p = frame_meta; - return true; -} - -static uint64_t -receiver_state_take_meta(struct receiver_state *state) { - struct frame_meta *frame_meta = state->frame_meta_queue; // first item - SDL_assert(frame_meta); // must not be empty - uint64_t pts = frame_meta->pts; - state->frame_meta_queue = frame_meta->next; // remove the item - frame_meta_delete(frame_meta); - return pts; -} - -static int -read_packet_with_meta(void *opaque, uint8_t *buf, int buf_size) { - struct stream *stream = opaque; - struct receiver_state *state = &stream->receiver_state; - +stream_recv_packet(struct stream *stream, AVPacket *packet) { // The video stream contains raw packets, without time information. When we // record, we retrieve the timestamps separately, from a "meta" header // added by the server before each raw packet. @@ -82,60 +36,30 @@ read_packet_with_meta(void *opaque, uint8_t *buf, int buf_size) { // // It is followed by bytes containing the packet/frame. - if (!state->remaining) { -#define HEADER_SIZE 12 - uint8_t header[HEADER_SIZE]; - ssize_t r = net_recv_all(stream->socket, header, HEADER_SIZE); - if (r == -1) { - return AVERROR(errno); - } - if (r == 0) { - return AVERROR_EOF; - } - // no partial read (net_recv_all()) - SDL_assert_release(r == HEADER_SIZE); - - uint64_t pts = buffer_read64be(header); - state->remaining = buffer_read32be(&header[8]); - - if (pts != NO_PTS && !receiver_state_push_meta(state, pts)) { - LOGE("Could not store PTS for recording"); - // we could not save the PTS, the recording would be broken - return AVERROR(ENOMEM); - } + uint8_t header[HEADER_SIZE]; + ssize_t r = net_recv_all(stream->socket, header, HEADER_SIZE); + if (r < HEADER_SIZE) { + return false; } - SDL_assert(state->remaining); + uint64_t pts = buffer_read64be(header); + uint32_t len = buffer_read32be(&header[8]); + SDL_assert(len); - if (buf_size > state->remaining) { - buf_size = state->remaining; + if (av_new_packet(packet, len)) { + LOGE("Could not allocate packet"); + return false; } - ssize_t r = net_recv(stream->socket, buf, buf_size); - if (r == -1) { - return errno ? AVERROR(errno) : AVERROR_EOF; - } - if (r == 0) { - return AVERROR_EOF; + r = net_recv_all(stream->socket, packet->data, len); + if (r < len) { + av_packet_unref(packet); + return false; } - SDL_assert(state->remaining >= r); - state->remaining -= r; + packet->pts = pts != NO_PTS ? pts : AV_NOPTS_VALUE; - return r; -} - -static int -read_raw_packet(void *opaque, uint8_t *buf, int buf_size) { - struct stream *stream = opaque; - ssize_t r = net_recv(stream->socket, buf, buf_size); - if (r == -1) { - return errno ? AVERROR(errno) : AVERROR_EOF; - } - if (r == 0) { - return AVERROR_EOF; - } - return r; + return true; } static void @@ -145,55 +69,136 @@ notify_stopped(void) { SDL_PushEvent(&stop_event); } +static bool +process_config_packet(struct stream *stream, AVPacket *packet) { + if (stream->recorder && !recorder_write(stream->recorder, packet)) { + LOGE("Could not send config packet to recorder"); + return false; + } + return true; +} + +static bool +process_frame(struct stream *stream, AVPacket *packet) { + if (stream->decoder && !decoder_push(stream->decoder, packet)) { + return false; + } + + if (stream->recorder) { + packet->dts = packet->pts; + + if (!recorder_write(stream->recorder, packet)) { + LOGE("Could not write frame to output file"); + return false; + } + } + + return true; +} + +static bool +stream_parse(struct stream *stream, AVPacket *packet) { + uint8_t *in_data = packet->data; + int in_len = packet->size; + uint8_t *out_data = NULL; + int out_len = 0; + int r = av_parser_parse2(stream->parser, stream->codec_ctx, + &out_data, &out_len, in_data, in_len, + AV_NOPTS_VALUE, AV_NOPTS_VALUE, -1); + + // PARSER_FLAG_COMPLETE_FRAMES is set + SDL_assert(r == in_len); + SDL_assert(out_len == in_len); + + if (stream->parser->key_frame == 1) { + packet->flags |= AV_PKT_FLAG_KEY; + } + + bool ok = process_frame(stream, packet); + if (!ok) { + LOGE("Could not process frame"); + return false; + } + + return true; +} + +static bool +stream_push_packet(struct stream *stream, AVPacket *packet) { + bool is_config = packet->pts == AV_NOPTS_VALUE; + + // A config packet must not be decoded immetiately (it contains no + // frame); instead, it must be concatenated with the future data packet. + if (stream->has_pending || is_config) { + size_t offset; + if (stream->has_pending) { + offset = stream->pending.size; + if (av_grow_packet(&stream->pending, packet->size)) { + LOGE("Could not grow packet"); + return false; + } + } else { + offset = 0; + if (av_new_packet(&stream->pending, packet->size)) { + LOGE("Could not create packet"); + return false; + } + stream->has_pending = true; + } + + memcpy(stream->pending.data + offset, packet->data, packet->size); + + if (!is_config) { + // prepare the concat packet to send to the decoder + stream->pending.pts = packet->pts; + stream->pending.dts = packet->dts; + stream->pending.flags = packet->flags; + packet = &stream->pending; + } + } + + if (is_config) { + // config packet + bool ok = process_config_packet(stream, packet); + if (!ok) { + return false; + } + } else { + // data packet + bool ok = stream_parse(stream, packet); + + if (stream->has_pending) { + // the pending packet must be discarded (consumed or error) + stream->has_pending = false; + av_packet_unref(&stream->pending); + } + + if (!ok) { + return false; + } + } + return true; +} + static int run_stream(void *data) { struct stream *stream = data; - AVFormatContext *format_ctx = avformat_alloc_context(); - if (!format_ctx) { - LOGC("Could not allocate format context"); - goto end; - } - - unsigned char *buffer = av_malloc(BUFSIZE); - if (!buffer) { - LOGC("Could not allocate buffer"); - goto finally_free_format_ctx; - } - - // initialize the receiver state - stream->receiver_state.frame_meta_queue = NULL; - stream->receiver_state.remaining = 0; - - // if recording is enabled, a "header" is sent between raw packets - int (*read_packet)(void *, uint8_t *, int) = - stream->recorder ? read_packet_with_meta : read_raw_packet; - AVIOContext *avio_ctx = avio_alloc_context(buffer, BUFSIZE, 0, stream, - read_packet, NULL, NULL); - if (!avio_ctx) { - LOGC("Could not allocate avio context"); - // avformat_open_input takes ownership of 'buffer' - // so only free the buffer before avformat_open_input() - av_free(buffer); - goto finally_free_format_ctx; - } - - format_ctx->pb = avio_ctx; - - if (avformat_open_input(&format_ctx, NULL, NULL, NULL) < 0) { - LOGE("Could not open video stream"); - goto finally_free_avio_ctx; - } - AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264); if (!codec) { LOGE("H.264 decoder not found"); goto end; } + stream->codec_ctx = avcodec_alloc_context3(codec); + if (!stream->codec_ctx) { + LOGC("Could not allocate codec context"); + goto end; + } + if (stream->decoder && !decoder_open(stream->decoder, codec)) { LOGE("Could not open decoder"); - goto finally_close_input; + goto finally_free_codec_ctx; } if (stream->recorder && !recorder_open(stream->recorder, codec)) { @@ -201,50 +206,40 @@ run_stream(void *data) { goto finally_close_decoder; } - AVPacket packet; - av_init_packet(&packet); - packet.data = NULL; - packet.size = 0; + stream->parser = av_parser_init(AV_CODEC_ID_H264); + if (!stream->parser) { + LOGE("Could not initialize parser"); + goto finally_close_recorder; + } - while (!av_read_frame(format_ctx, &packet)) { - if (SDL_AtomicGet(&stream->stopped)) { - // if the stream is stopped, the socket had been shutdown, so the - // last packet is probably corrupted (but not detected as such by - // FFmpeg) and will not be decoded correctly - av_packet_unref(&packet); - goto quit; - } - if (stream->decoder && !decoder_push(stream->decoder, &packet)) { - av_packet_unref(&packet); - goto quit; - } - - if (stream->recorder) { - // we retrieve the PTS in order they were received, so they will - // be assigned to the correct frame - uint64_t pts = receiver_state_take_meta(&stream->receiver_state); - packet.pts = pts; - packet.dts = pts; - - // no need to rescale with av_packet_rescale_ts(), the timestamps - // are in microseconds both in input and output - if (!recorder_write(stream->recorder, &packet)) { - LOGE("Could not write frame to output file"); - av_packet_unref(&packet); - goto quit; - } + // We must only pass complete frames to av_parser_parse2()! + // It's more complicated, but this allows to reduce the latency by 1 frame! + stream->parser->flags |= PARSER_FLAG_COMPLETE_FRAMES; + + for (;;) { + AVPacket packet; + bool ok = stream_recv_packet(stream, &packet); + if (!ok) { + // end of stream + break; } + ok = stream_push_packet(stream, &packet); av_packet_unref(&packet); - - if (avio_ctx->eof_reached) { + if (!ok) { + // cannot process packet (error already logged) break; } } LOGD("End of frames"); -quit: + if (stream->has_pending) { + av_packet_unref(&stream->pending); + } + + av_parser_close(stream->parser); +finally_close_recorder: if (stream->recorder) { recorder_close(stream->recorder); } @@ -252,13 +247,8 @@ finally_close_decoder: if (stream->decoder) { decoder_close(stream->decoder); } -finally_close_input: - avformat_close_input(&format_ctx); -finally_free_avio_ctx: - av_free(avio_ctx->buffer); - av_free(avio_ctx); -finally_free_format_ctx: - avformat_free_context(format_ctx); +finally_free_codec_ctx: + avcodec_free_context(&stream->codec_ctx); end: notify_stopped(); return 0; @@ -270,7 +260,7 @@ stream_init(struct stream *stream, socket_t socket, stream->socket = socket; stream->decoder = decoder, stream->recorder = recorder; - SDL_AtomicSet(&stream->stopped, 0); + stream->has_pending = false; } bool @@ -287,7 +277,6 @@ stream_start(struct stream *stream) { void stream_stop(struct stream *stream) { - SDL_AtomicSet(&stream->stopped, 1); if (stream->decoder) { decoder_interrupt(stream->decoder); } diff --git a/app/src/stream.h b/app/src/stream.h index 1ebff1a0..160ed7f5 100644 --- a/app/src/stream.h +++ b/app/src/stream.h @@ -3,6 +3,7 @@ #include #include +#include #include #include @@ -10,23 +11,18 @@ struct video_buffer; -struct frame_meta { - uint64_t pts; - struct frame_meta *next; -}; - struct stream { socket_t socket; struct video_buffer *video_buffer; SDL_Thread *thread; - SDL_atomic_t stopped; struct decoder *decoder; struct recorder *recorder; - struct receiver_state { - // meta (in order) for frames not consumed yet - struct frame_meta *frame_meta_queue; - size_t remaining; // remaining bytes to receive for the current frame - } receiver_state; + AVCodecContext *codec_ctx; + AVCodecParserContext *parser; + // successive packets may need to be concatenated, until a non-config + // packet is available + bool has_pending; + AVPacket pending; }; void From 35d9185f6c3dc1c34ea7d46859ee89827e2fe74a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 31 Jul 2019 01:55:40 +0200 Subject: [PATCH 0029/2244] Record asynchronously The record file was written from the stream thread. As a consequence, any blocking I/O to write the file delayed the decoder. For maximum performance even when recording is enabled, send (refcounted) packets to a separate recording thread. --- app/src/recorder.c | 184 +++++++++++++++++++++++++++++++++++++++++++++ app/src/recorder.h | 30 +++++++- app/src/stream.c | 27 +++++-- 3 files changed, 233 insertions(+), 8 deletions(-) diff --git a/app/src/recorder.c b/app/src/recorder.c index c14394a3..c36e3f91 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -5,6 +5,7 @@ #include "compat.h" #include "config.h" +#include "lock_util.h" #include "log.h" static const AVRational SCRCPY_TIME_BASE = {1, 1000000}; // timestamps in us @@ -26,6 +27,82 @@ find_muxer(const char *name) { return oformat; } +static struct record_packet * +record_packet_new(const AVPacket *packet) { + struct record_packet *rec = SDL_malloc(sizeof(*rec)); + if (!rec) { + return NULL; + } + if (av_packet_ref(&rec->packet, packet)) { + SDL_free(rec); + return NULL; + } + rec->next = NULL; + return rec; +} + +static void +record_packet_delete(struct record_packet *rec) { + av_packet_unref(&rec->packet); + SDL_free(rec); +} + +static void +recorder_queue_init(struct recorder_queue *queue) { + queue->first = NULL; + // queue->last is undefined if queue->first == NULL +} + +static inline bool +recorder_queue_is_empty(struct recorder_queue *queue) { + return !queue->first; +} + +static bool +recorder_queue_push(struct recorder_queue *queue, const AVPacket *packet) { + struct record_packet *rec = record_packet_new(packet); + if (!rec) { + LOGC("Could not allocate record packet"); + return false; + } + rec->next = NULL; + + if (recorder_queue_is_empty(queue)) { + queue->first = queue->last = rec; + } else { + // chain rec after the (current) last packet + queue->last->next = rec; + // the last packet is now rec + queue->last = rec; + } + return true; +} + +static inline struct record_packet * +recorder_queue_take(struct recorder_queue *queue) { + SDL_assert(!recorder_queue_is_empty(queue)); + + struct record_packet *rec = queue->first; + SDL_assert(rec); + + queue->first = rec->next; + // no need to update queue->last if the queue is left empty: + // queue->last is undefined if queue->first == NULL + + return rec; +} + +static void +recorder_queue_clear(struct recorder_queue *queue) { + struct record_packet *rec = queue->first; + while (rec) { + struct record_packet *current = rec; + rec = rec->next; + record_packet_delete(current); + } + queue->first = NULL; +} + bool recorder_init(struct recorder *recorder, const char *filename, @@ -37,6 +114,24 @@ recorder_init(struct recorder *recorder, return false; } + recorder->mutex = SDL_CreateMutex(); + if (!recorder->mutex) { + LOGC("Could not create mutex"); + SDL_free(recorder->filename); + return false; + } + + recorder->queue_cond = SDL_CreateCond(); + if (!recorder->queue_cond) { + LOGC("Could not create cond"); + SDL_DestroyMutex(recorder->mutex); + SDL_free(recorder->filename); + return false; + } + + recorder_queue_init(&recorder->queue); + recorder->stopped = false; + recorder->failed = false; recorder->format = format; recorder->declared_frame_size = declared_frame_size; recorder->header_written = false; @@ -46,6 +141,8 @@ recorder_init(struct recorder *recorder, void recorder_destroy(struct recorder *recorder) { + SDL_DestroyCond(recorder->queue_cond); + SDL_DestroyMutex(recorder->mutex); SDL_free(recorder->filename); } @@ -186,3 +283,90 @@ recorder_write(struct recorder *recorder, AVPacket *packet) { recorder_rescale_packet(recorder, packet); return av_write_frame(recorder->ctx, packet) >= 0; } + +static int +run_recorder(void *data) { + struct recorder *recorder = data; + + for (;;) { + mutex_lock(recorder->mutex); + + while (!recorder->stopped && + recorder_queue_is_empty(&recorder->queue)) { + cond_wait(recorder->queue_cond, recorder->mutex); + } + + // if stopped is set, continue to process the remaining events (to + // finish the recording) before actually stopping + + if (recorder->stopped && recorder_queue_is_empty(&recorder->queue)) { + mutex_unlock(recorder->mutex); + break; + } + + struct record_packet *rec = recorder_queue_take(&recorder->queue); + + mutex_unlock(recorder->mutex); + + bool ok = recorder_write(recorder, &rec->packet); + record_packet_delete(rec); + if (!ok) { + LOGE("Could not record packet"); + + mutex_lock(recorder->mutex); + recorder->failed = true; + // discard pending packets + recorder_queue_clear(&recorder->queue); + mutex_unlock(recorder->mutex); + break; + } + + } + + LOGD("Recorder thread ended"); + + return 0; +} + +bool +recorder_start(struct recorder *recorder) { + LOGD("Starting recorder thread"); + + recorder->thread = SDL_CreateThread(run_recorder, "recorder", recorder); + if (!recorder->thread) { + LOGC("Could not start recorder thread"); + return false; + } + + return true; +} + +void +recorder_stop(struct recorder *recorder) { + mutex_lock(recorder->mutex); + recorder->stopped = true; + cond_signal(recorder->queue_cond); + mutex_unlock(recorder->mutex); +} + +void +recorder_join(struct recorder *recorder) { + SDL_WaitThread(recorder->thread, NULL); +} + +bool +recorder_push(struct recorder *recorder, const AVPacket *packet) { + mutex_lock(recorder->mutex); + SDL_assert(!recorder->stopped); + + if (recorder->failed) { + // reject any new packet (this will stop the stream) + return false; + } + + bool ok = recorder_queue_push(&recorder->queue, packet); + cond_signal(recorder->queue_cond); + + mutex_unlock(recorder->mutex); + return ok; +} diff --git a/app/src/recorder.h b/app/src/recorder.h index 8a8e3310..8d1f575d 100644 --- a/app/src/recorder.h +++ b/app/src/recorder.h @@ -3,6 +3,8 @@ #include #include +#include +#include #include "common.h" @@ -11,12 +13,29 @@ enum recorder_format { RECORDER_FORMAT_MKV, }; +struct record_packet { + AVPacket packet; + struct record_packet *next; +}; + +struct recorder_queue { + struct record_packet *first; + struct record_packet *last; // undefined if first is NULL +}; + struct recorder { char *filename; enum recorder_format format; AVFormatContext *ctx; struct size declared_frame_size; bool header_written; + + SDL_Thread *thread; + SDL_mutex *mutex; + SDL_cond *queue_cond; + bool stopped; // set on recorder_stop() by the stream reader + bool failed; // set on packet write failure + struct recorder_queue queue; }; bool @@ -33,6 +52,15 @@ void recorder_close(struct recorder *recorder); bool -recorder_write(struct recorder *recorder, AVPacket *packet); +recorder_start(struct recorder *recorder); + +void +recorder_stop(struct recorder *recorder); + +void +recorder_join(struct recorder *recorder); + +bool +recorder_push(struct recorder *recorder, const AVPacket *packet); #endif diff --git a/app/src/stream.c b/app/src/stream.c index 0396bf60..bca89f71 100644 --- a/app/src/stream.c +++ b/app/src/stream.c @@ -71,7 +71,7 @@ notify_stopped(void) { static bool process_config_packet(struct stream *stream, AVPacket *packet) { - if (stream->recorder && !recorder_write(stream->recorder, packet)) { + if (stream->recorder && !recorder_push(stream->recorder, packet)) { LOGE("Could not send config packet to recorder"); return false; } @@ -87,8 +87,8 @@ process_frame(struct stream *stream, AVPacket *packet) { if (stream->recorder) { packet->dts = packet->pts; - if (!recorder_write(stream->recorder, packet)) { - LOGE("Could not write frame to output file"); + if (!recorder_push(stream->recorder, packet)) { + LOGE("Could not send packet to recorder"); return false; } } @@ -201,15 +201,22 @@ run_stream(void *data) { goto finally_free_codec_ctx; } - if (stream->recorder && !recorder_open(stream->recorder, codec)) { - LOGE("Could not open recorder"); - goto finally_close_decoder; + if (stream->recorder) { + if (!recorder_open(stream->recorder, codec)) { + LOGE("Could not open recorder"); + goto finally_close_decoder; + } + + if (!recorder_start(stream->recorder)) { + LOGE("Could not start recorder"); + goto finally_close_recorder; + } } stream->parser = av_parser_init(AV_CODEC_ID_H264); if (!stream->parser) { LOGE("Could not initialize parser"); - goto finally_close_recorder; + goto finally_stop_and_join_recorder; } // We must only pass complete frames to av_parser_parse2()! @@ -239,6 +246,12 @@ run_stream(void *data) { } av_parser_close(stream->parser); +finally_stop_and_join_recorder: + if (stream->recorder) { + recorder_stop(stream->recorder); + LOGI("Finishing recording..."); + recorder_join(stream->recorder); + } finally_close_recorder: if (stream->recorder) { recorder_close(stream->recorder); From d4ed8b6f26fcf0f7bd8c211daf905ee47f4d7ea8 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 31 Jul 2019 01:55:43 +0200 Subject: [PATCH 0030/2244] Log scrcpy version and URL on start Keep --version which also print the version of dependencies. --- app/src/main.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main.c b/app/src/main.c index 7a1cb9db..53dd04c0 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -485,6 +485,8 @@ main(int argc, char *argv[]) { return 0; } + LOGI("scrcpy " SCRCPY_VERSION " "); + #ifdef SCRCPY_LAVF_REQUIRES_REGISTER_ALL av_register_all(); #endif From 6abb4902c60169132bfa0c9bd6cc56fa8580e7c0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 31 Jul 2019 11:04:38 +0200 Subject: [PATCH 0031/2244] Log recording failure If recording fails, log "recording failed" instead of "recording complete". --- app/src/recorder.c | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/src/recorder.c b/app/src/recorder.c index c36e3f91..63a1400e 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -216,12 +216,17 @@ recorder_close(struct recorder *recorder) { int ret = av_write_trailer(recorder->ctx); if (ret < 0) { LOGE("Failed to write trailer to %s", recorder->filename); + recorder->failed = true; } avio_close(recorder->ctx->pb); avformat_free_context(recorder->ctx); - const char *format_name = recorder_get_format_name(recorder->format); - LOGI("Recording complete to %s file: %s", format_name, recorder->filename); + if (recorder->failed) { + LOGE("Recording failed to %s", recorder->filename); + } else { + const char *format_name = recorder_get_format_name(recorder->format); + LOGI("Recording complete to %s file: %s", format_name, recorder->filename); + } } static bool From 421e4be399ce57c6376d0bbd00f238c1723c1c56 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 31 Jul 2019 16:33:30 +0200 Subject: [PATCH 0032/2244] Remove root directory from Windows zip releases Put the scrcpy files at the root of the zip archive. This avoids an unnecessary level of directories when extracting. --- Makefile.CrossWindows | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Makefile.CrossWindows b/Makefile.CrossWindows index 49adcfb7..6c68c387 100644 --- a/Makefile.CrossWindows +++ b/Makefile.CrossWindows @@ -125,12 +125,12 @@ dist-win64: build-server build-win64 build-win64-noconsole cp prebuilt-deps/SDL2-2.0.8/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/" zip-win32: dist-win32 - cd "$(DIST)"; \ - zip -r "$(WIN32_TARGET)" "$(WIN32_TARGET_DIR)" + cd "$(DIST)/$(WIN32_TARGET_DIR)"; \ + zip -r "../$(WIN32_TARGET)" . zip-win64: dist-win64 - cd "$(DIST)"; \ - zip -r "$(WIN64_TARGET)" "$(WIN64_TARGET_DIR)" + cd "$(DIST)/$(WIN64_TARGET_DIR)"; \ + zip -r "../$(WIN64_TARGET)" . sums: cd "$(DIST)"; \ From 96b5067cbfe9621ea8cfa977b79b73264f29aa3b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 1 Aug 2019 22:08:16 +0200 Subject: [PATCH 0033/2244] Remove unnecessary backslash in cbuf --- app/src/cbuf.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/cbuf.h b/app/src/cbuf.h index 5d9fe4ae..9777d9f4 100644 --- a/app/src/cbuf.h +++ b/app/src/cbuf.h @@ -35,7 +35,7 @@ (PCBUF)->head = ((PCBUF)->head + 1) % cbuf_size_(PCBUF); \ } \ ok; \ - }) \ + }) #define cbuf_take(PCBUF, PITEM) \ ({ \ From 26213f1031e364bd8496e5317fba8140adaba357 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 1 Aug 2019 22:14:09 +0200 Subject: [PATCH 0034/2244] Fix cbuf documentation --- app/src/cbuf.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/cbuf.h b/app/src/cbuf.h index 9777d9f4..35b39b7b 100644 --- a/app/src/cbuf.h +++ b/app/src/cbuf.h @@ -6,7 +6,7 @@ #include // To define a circular buffer type of 20 ints: -// typedef CBUF(int, 20) my_cbuf_t; +// struct cbuf_int CBUF(int, 20); // // data has length CAP + 1 to distinguish empty vs full. #define CBUF(TYPE, CAP) { \ From 53b6ee2cf493fc8574d41290df7462ffb6e7eb7c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 1 Aug 2019 22:53:02 +0200 Subject: [PATCH 0035/2244] Add generic intrusive FIFO queue We need several FIFO queues (a queue of packets, a queue of messages, etc.). Some of them are implemented using cbuf, a generic circular buffer. But for recording, we need to store the packets in an unbounded queue until they are written, so the queue was implemented manually. Create a generic implementation (using macros) to avoid reimplementing it every time. --- app/meson.build | 3 ++ app/src/queue.h | 74 ++++++++++++++++++++++++++++++++++++++++++ app/tests/test_queue.c | 38 ++++++++++++++++++++++ 3 files changed, 115 insertions(+) create mode 100644 app/src/queue.h create mode 100644 app/tests/test_queue.c diff --git a/app/meson.build b/app/meson.build index 02d24a34..3410c2d3 100644 --- a/app/meson.build +++ b/app/meson.build @@ -150,6 +150,9 @@ tests = [ 'tests/test_device_msg_deserialize.c', 'src/device_msg.c' ]], + ['test_queue', [ + 'tests/test_queue.c', + ]], ['test_strutil', [ 'tests/test_strutil.c', 'src/str_util.c' diff --git a/app/src/queue.h b/app/src/queue.h new file mode 100644 index 00000000..294823da --- /dev/null +++ b/app/src/queue.h @@ -0,0 +1,74 @@ +// generic intrusive FIFO queue +#ifndef QUEUE_H +#define QUEUE_H + +#include +#include + +// To define a queue type of "struct foo": +// struct queue_foo QUEUE(struct foo); +#define QUEUE(TYPE) { \ + TYPE *first; \ + TYPE *last; \ +} + +#define queue_init(PQ) \ + (void) ((PQ)->first = NULL) + +#define queue_is_empty(PQ) \ + !(PQ)->first + +// NEXTFIELD is the field in the ITEM type used for intrusive linked-list +// +// For example: +// struct foo { +// int value; +// struct foo *next; +// }; +// +// // define the type "struct my_queue" +// struct my_queue QUEUE(struct foo); +// +// struct my_queue queue; +// queue_init(&queue); +// +// struct foo v1 = { .value = 42 }; +// struct foo v2 = { .value = 27 }; +// +// queue_push(&queue, next, v1); +// queue_push(&queue, next, v2); +// +// struct foo *foo; +// queue_take(&queue, next, &foo); +// assert(foo->value == 42); +// queue_take(&queue, next, &foo); +// assert(foo->value == 27); +// assert(queue_is_empty(&queue)); +// + +// push a new item into the queue +#define queue_push(PQ, NEXTFIELD, ITEM) \ + (void) ({ \ + (ITEM)->NEXTFIELD = NULL; \ + if (queue_is_empty(PQ)) { \ + (PQ)->first = (PQ)->last = (ITEM); \ + } else { \ + (PQ)->last->NEXTFIELD = (ITEM); \ + (PQ)->last = (ITEM); \ + } \ + }) + +// take the next item and remove it from the queue (the queue must not be empty) +// the result is stored in *(PITEM) +// (without typeof(), we could not store a local variable having the correct +// type so that we can "return" it) +#define queue_take(PQ, NEXTFIELD, PITEM) \ + (void) ({ \ + SDL_assert(!queue_is_empty(PQ)); \ + *(PITEM) = (PQ)->first; \ + (PQ)->first = (PQ)->first->NEXTFIELD; \ + }) + // no need to update (PQ)->last if the queue is left empty: + // (PQ)->last is undefined if !(PQ)->first anyway + +#endif diff --git a/app/tests/test_queue.c b/app/tests/test_queue.c new file mode 100644 index 00000000..bcbced2b --- /dev/null +++ b/app/tests/test_queue.c @@ -0,0 +1,38 @@ +#include + +#include + +struct foo { + int value; + struct foo *next; +}; + +static void test_queue(void) { + struct my_queue QUEUE(struct foo) queue; + queue_init(&queue); + + assert(queue_is_empty(&queue)); + + struct foo v1 = { .value = 42 }; + struct foo v2 = { .value = 27 }; + + queue_push(&queue, next, &v1); + queue_push(&queue, next, &v2); + + struct foo *foo; + + assert(!queue_is_empty(&queue)); + queue_take(&queue, next, &foo); + assert(foo->value == 42); + + assert(!queue_is_empty(&queue)); + queue_take(&queue, next, &foo); + assert(foo->value == 27); + + assert(queue_is_empty(&queue)); +} + +int main(void) { + test_queue(); + return 0; +} From 5e4ccfd83298d3fb2e1c638b6235b811d034afdd Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 1 Aug 2019 23:15:47 +0200 Subject: [PATCH 0036/2244] Use generic FIFO queue for recording Replace the specific recording queue by the new generic FIFO queue implementation. --- app/src/recorder.c | 76 +++++++++++----------------------------------- app/src/recorder.h | 6 ++-- 2 files changed, 19 insertions(+), 63 deletions(-) diff --git a/app/src/recorder.c b/app/src/recorder.c index 63a1400e..402f2530 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -37,7 +37,6 @@ record_packet_new(const AVPacket *packet) { SDL_free(rec); return NULL; } - rec->next = NULL; return rec; } @@ -47,60 +46,13 @@ record_packet_delete(struct record_packet *rec) { SDL_free(rec); } -static void -recorder_queue_init(struct recorder_queue *queue) { - queue->first = NULL; - // queue->last is undefined if queue->first == NULL -} - -static inline bool -recorder_queue_is_empty(struct recorder_queue *queue) { - return !queue->first; -} - -static bool -recorder_queue_push(struct recorder_queue *queue, const AVPacket *packet) { - struct record_packet *rec = record_packet_new(packet); - if (!rec) { - LOGC("Could not allocate record packet"); - return false; - } - rec->next = NULL; - - if (recorder_queue_is_empty(queue)) { - queue->first = queue->last = rec; - } else { - // chain rec after the (current) last packet - queue->last->next = rec; - // the last packet is now rec - queue->last = rec; - } - return true; -} - -static inline struct record_packet * -recorder_queue_take(struct recorder_queue *queue) { - SDL_assert(!recorder_queue_is_empty(queue)); - - struct record_packet *rec = queue->first; - SDL_assert(rec); - - queue->first = rec->next; - // no need to update queue->last if the queue is left empty: - // queue->last is undefined if queue->first == NULL - - return rec; -} - static void recorder_queue_clear(struct recorder_queue *queue) { - struct record_packet *rec = queue->first; - while (rec) { - struct record_packet *current = rec; - rec = rec->next; - record_packet_delete(current); + while (!queue_is_empty(queue)) { + struct record_packet *rec; + queue_take(queue, next, &rec); + record_packet_delete(rec); } - queue->first = NULL; } bool @@ -129,7 +81,7 @@ recorder_init(struct recorder *recorder, return false; } - recorder_queue_init(&recorder->queue); + queue_init(&recorder->queue); recorder->stopped = false; recorder->failed = false; recorder->format = format; @@ -296,20 +248,20 @@ run_recorder(void *data) { for (;;) { mutex_lock(recorder->mutex); - while (!recorder->stopped && - recorder_queue_is_empty(&recorder->queue)) { + while (!recorder->stopped && queue_is_empty(&recorder->queue)) { cond_wait(recorder->queue_cond, recorder->mutex); } // if stopped is set, continue to process the remaining events (to // finish the recording) before actually stopping - if (recorder->stopped && recorder_queue_is_empty(&recorder->queue)) { + if (recorder->stopped && queue_is_empty(&recorder->queue)) { mutex_unlock(recorder->mutex); break; } - struct record_packet *rec = recorder_queue_take(&recorder->queue); + struct record_packet *rec; + queue_take(&recorder->queue, next, &rec); mutex_unlock(recorder->mutex); @@ -369,9 +321,15 @@ recorder_push(struct recorder *recorder, const AVPacket *packet) { return false; } - bool ok = recorder_queue_push(&recorder->queue, packet); + struct record_packet *rec = record_packet_new(packet); + if (!rec) { + LOGC("Could not allocate record packet"); + return false; + } + + queue_push(&recorder->queue, next, rec); cond_signal(recorder->queue_cond); mutex_unlock(recorder->mutex); - return ok; + return true; } diff --git a/app/src/recorder.h b/app/src/recorder.h index 8d1f575d..5dbfcc3b 100644 --- a/app/src/recorder.h +++ b/app/src/recorder.h @@ -7,6 +7,7 @@ #include #include "common.h" +#include "queue.h" enum recorder_format { RECORDER_FORMAT_MP4 = 1, @@ -18,10 +19,7 @@ struct record_packet { struct record_packet *next; }; -struct recorder_queue { - struct record_packet *first; - struct record_packet *last; // undefined if first is NULL -}; +struct recorder_queue QUEUE(struct record_packet); struct recorder { char *filename; From e2ac996183c661a1887ae225c81b624551cc0299 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 31 Jul 2019 16:11:08 +0200 Subject: [PATCH 0037/2244] Use Cmd instead of Ctrl on macOS when possible Fixes --- README.md | 40 +++++++++++++++--------------- app/src/input_manager.c | 55 +++++++++++++++++++++++------------------ app/src/main.c | 37 +++++++++++++++------------ 3 files changed, 72 insertions(+), 60 deletions(-) diff --git a/README.md b/README.md index 25d600c3..c1967343 100644 --- a/README.md +++ b/README.md @@ -315,26 +315,26 @@ Also see [issue #14]. ## Shortcuts - | Action | Shortcut | - | -------------------------------------- |:---------------------------- | - | Switch fullscreen mode | `Ctrl`+`f` | - | 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)_ (`Cmd`+`↑` on macOS) | - | Click on `VOLUME_DOWN` | `Ctrl`+`↓` _(down)_ (`Cmd`+`↓` on macOS) | - | Click on `POWER` | `Ctrl`+`p` | - | Power on | _Right-click²_ | - | Turn device screen off (keep mirroring)| `Ctrl`+`o` | - | 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 | `Ctrl`+`Shift`+`v` | - | Enable/disable FPS counter (on stdout) | `Ctrl`+`i` | + | Action | Shortcut | Shortcut (macOS) + | -------------------------------------- |:----------------------------- |:----------------------------- + | Switch fullscreen mode | `Ctrl`+`f` | `Cmd`+`f` + | 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` + | 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` _¹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/input_manager.c b/app/src/input_manager.c index 37e41ca9..5258beda 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -242,16 +242,27 @@ input_manager_process_key(struct input_manager *input_manager, 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 shortcut involves Alt or Meta, and they should not be forwarded - // to the device + // no shortcuts involve Alt, and it must not be forwarded to the device return; } struct controller *controller = input_manager->controller; // capture all Ctrl events - if (ctrl | meta) { + if (ctrl || cmd) { SDL_Keycode keycode = event->keysym.sym; bool down = event->type == SDL_KEYDOWN; int action = down ? ACTION_DOWN : ACTION_UP; @@ -259,63 +270,59 @@ input_manager_process_key(struct input_manager *input_manager, 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) { action_home(controller, action); } return; case SDLK_b: // fall-through case SDLK_BACKSPACE: - if (control && ctrl && !meta && !shift && !repeat) { + if (control && cmd && !shift && !repeat) { action_back(controller, action); } return; case SDLK_s: - if (control && ctrl && !meta && !shift && !repeat) { + if (control && cmd && !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) { action_menu(controller, action); } return; case SDLK_p: - if (control && ctrl && !meta && !shift && !repeat) { + if (control && cmd && !shift && !repeat) { action_power(controller, action); } return; case SDLK_o: - if (control && ctrl && !shift && !meta && down) { + if (control && cmd && !shift && down) { set_screen_power_mode(controller, SCREEN_POWER_MODE_OFF); } return; case SDLK_DOWN: -#ifdef __APPLE__ - if (control && !ctrl && meta && !shift) { -#else - if (control && ctrl && !meta && !shift) { -#endif + if (control && cmd && !shift) { // forward repeated events action_volume_down(controller, action); } return; case SDLK_UP: -#ifdef __APPLE__ - if (control && !ctrl && meta && !shift) { -#else - if (control && ctrl && !meta && !shift) { -#endif + if (control && cmd && !shift) { // forward repeated events action_volume_up(controller, action); } return; case SDLK_c: - if (control && ctrl && !meta && !shift && !repeat && down) { + if (control && cmd && !shift && !repeat && down) { request_device_clipboard(controller); } return; case SDLK_v: - if (control && ctrl && !meta && !repeat && down) { + if (control && cmd && !repeat && down) { if (shift) { // store the text in the device clipboard set_device_clipboard(controller); @@ -326,29 +333,29 @@ input_manager_process_key(struct input_manager *input_manager, } return; case SDLK_f: - if (ctrl && !meta && !shift && !repeat && down) { + if (!shift && cmd && !repeat && down) { screen_switch_fullscreen(input_manager->screen); } return; case SDLK_x: - if (ctrl && !meta && !shift && !repeat && down) { + if (!shift && cmd && !repeat && down) { screen_resize_to_fit(input_manager->screen); } return; case SDLK_g: - if (ctrl && !meta && !shift && !repeat && down) { + if (!shift && cmd && !repeat && down) { screen_resize_to_pixel_perfect(input_manager->screen); } return; case SDLK_i: - if (ctrl && !meta && !shift && !repeat && down) { + if (!shift && cmd && !repeat && down) { struct fps_counter *fps_counter = input_manager->video_buffer->fps_counter; switch_fps_counter_state(fps_counter); } return; case SDLK_n: - if (control && ctrl && !meta && !repeat && down) { + if (control && cmd && !repeat && down) { if (shift) { collapse_notification_panel(controller); } else { diff --git a/app/src/main.c b/app/src/main.c index 53dd04c0..dfeca7cb 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -35,6 +35,11 @@ struct args { }; static void 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" @@ -115,13 +120,13 @@ static void usage(const char *arg0) { "\n" "Shortcuts:\n" "\n" - " Ctrl+f\n" + " " CTRL_OR_CMD "+f\n" " switch fullscreen mode\n" "\n" - " Ctrl+g\n" + " " CTRL_OR_CMD "+g\n" " resize window to 1:1 (pixel-perfect)\n" "\n" - " Ctrl+x\n" + " " CTRL_OR_CMD "+x\n" " Double-click on black borders\n" " resize window to remove black borders\n" "\n" @@ -129,48 +134,48 @@ static void usage(const char *arg0) { " Middle-click\n" " click on HOME\n" "\n" - " Ctrl+b\n" - " Ctrl+Backspace\n" + " " CTRL_OR_CMD "+b\n" + " " CTRL_OR_CMD "+Backspace\n" " Right-click (when screen is on)\n" " click on BACK\n" "\n" - " Ctrl+s\n" + " " CTRL_OR_CMD "+s\n" " click on APP_SWITCH\n" "\n" " Ctrl+m\n" " click on MENU\n" "\n" - " Ctrl+Up\n" + " " CTRL_OR_CMD "+Up\n" " click on VOLUME_UP\n" "\n" - " Ctrl+Down\n" + " " CTRL_OR_CMD "+Down\n" " click on VOLUME_DOWN\n" "\n" - " Ctrl+p\n" + " " CTRL_OR_CMD "+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" + " " CTRL_OR_CMD "+o\n" " turn device screen off (keep mirroring)\n" "\n" - " Ctrl+n\n" + " " CTRL_OR_CMD "+n\n" " expand notification panel\n" "\n" - " Ctrl+Shift+n\n" + " " CTRL_OR_CMD "+Shift+n\n" " collapse notification panel\n" "\n" - " Ctrl+c\n" + " " CTRL_OR_CMD "+c\n" " copy device clipboard to computer\n" "\n" - " Ctrl+v\n" + " " CTRL_OR_CMD "+v\n" " paste computer clipboard to device\n" "\n" - " Ctrl+Shift+v\n" + " " CTRL_OR_CMD "+Shift+v\n" " copy computer clipboard to device\n" "\n" - " Ctrl+i\n" + " " CTRL_OR_CMD "+i\n" " enable/disable FPS counter (print frames/second in logs)\n" "\n" " Drag & drop APK file\n" From b0184f2869d259ae83eebe981010e39485fe459f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 4 Aug 2019 16:21:50 +0200 Subject: [PATCH 0038/2244] Initialize queue "last" field The compiler is not always able to see that "last" is always initialized before being used, so always initialize it. --- app/src/queue.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/queue.h b/app/src/queue.h index 294823da..229ddb87 100644 --- a/app/src/queue.h +++ b/app/src/queue.h @@ -13,7 +13,7 @@ } #define queue_init(PQ) \ - (void) ((PQ)->first = NULL) + (void) ((PQ)->first = (PQ)->last = NULL) #define queue_is_empty(PQ) \ !(PQ)->first From c3a58ad10fdcc3be7d38263037f6d148573cc3c1 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 3 Aug 2019 23:30:30 +0200 Subject: [PATCH 0039/2244] Upgrade FFmpeg (4.1.4) for Windows Include the latest version of FFmpeg in Windows releases. --- Makefile.CrossWindows | 19 ++++++++++--------- cross_win32.txt | 4 ++-- cross_win64.txt | 4 ++-- prebuilt-deps/Makefile | 24 ++++++++++++------------ 4 files changed, 26 insertions(+), 25 deletions(-) diff --git a/Makefile.CrossWindows b/Makefile.CrossWindows index 6c68c387..6ac98d9f 100644 --- a/Makefile.CrossWindows +++ b/Makefile.CrossWindows @@ -100,10 +100,11 @@ dist-win32: build-server build-win32 build-win32-noconsole cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server.jar "$(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.1.3-win32-shared/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.1.3-win32-shared/bin/avcodec-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.1.3-win32-shared/bin/avformat-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.1.3-win32-shared/bin/swresample-3.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.1.4-win32-shared/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.1.4-win32-shared/bin/avcodec-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.1.4-win32-shared/bin/avformat-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.1.4-win32-shared/bin/swresample-3.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.1.4-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)/" @@ -114,11 +115,11 @@ dist-win64: build-server build-win64 build-win64-noconsole cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server.jar "$(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.1.3-win64-shared/bin/avutil-56.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.1.3-win64-shared/bin/avcodec-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.1.3-win64-shared/bin/avformat-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.1.3-win64-shared/bin/swresample-3.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.1.3-win64-shared/bin/swscale-5.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.1.4-win64-shared/bin/avutil-56.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.1.4-win64-shared/bin/avcodec-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.1.4-win64-shared/bin/avformat-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.1.4-win64-shared/bin/swresample-3.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.1.4-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 2db35fe0..0156adfe 100644 --- a/cross_win32.txt +++ b/cross_win32.txt @@ -15,6 +15,6 @@ cpu = 'i686' endian = 'little' [properties] -prebuilt_ffmpeg_shared = 'ffmpeg-4.1.3-win32-shared' -prebuilt_ffmpeg_dev = 'ffmpeg-4.1.3-win32-dev' +prebuilt_ffmpeg_shared = 'ffmpeg-4.1.4-win32-shared' +prebuilt_ffmpeg_dev = 'ffmpeg-4.1.4-win32-dev' prebuilt_sdl2 = 'SDL2-2.0.8/i686-w64-mingw32' diff --git a/cross_win64.txt b/cross_win64.txt index 79181653..5cd03fc6 100644 --- a/cross_win64.txt +++ b/cross_win64.txt @@ -15,6 +15,6 @@ cpu = 'x86_64' endian = 'little' [properties] -prebuilt_ffmpeg_shared = 'ffmpeg-4.1.3-win64-shared' -prebuilt_ffmpeg_dev = 'ffmpeg-4.1.3-win64-dev' +prebuilt_ffmpeg_shared = 'ffmpeg-4.1.4-win64-shared' +prebuilt_ffmpeg_dev = 'ffmpeg-4.1.4-win64-dev' prebuilt_sdl2 = 'SDL2-2.0.8/x86_64-w64-mingw32' diff --git a/prebuilt-deps/Makefile b/prebuilt-deps/Makefile index 04f8b779..82a5d09b 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.1.3-win32-shared.zip \ - 8ea472d673370d5e87517a75587abfa6f189ee4f82e8da21fdbc49d0db0c1a89 \ - ffmpeg-4.1.3-win32-shared + @./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/shared/ffmpeg-4.1.4-win32-shared.zip \ + 596608277f6b937c3dea7c46e854665d75b3de56790bae07f655ca331440f003 \ + ffmpeg-4.1.4-win32-shared prepare-ffmpeg-dev-win32: - @./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/dev/ffmpeg-4.1.3-win32-dev.zip \ - e16d3150b6ccf0b71908f5b964cb8c051d79053c8f5cd6d777d617ab4f03613a \ - ffmpeg-4.1.3-win32-dev + @./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/dev/ffmpeg-4.1.4-win32-dev.zip \ + a80c86e263cfad26e202edfa5e6e939a2c88843ae26f031d3e0d981a39fd03fb \ + ffmpeg-4.1.4-win32-dev prepare-ffmpeg-shared-win64: - @./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/shared/ffmpeg-4.1.3-win64-shared.zip \ - 0b974578e07d974c4bafb36c7ed0b46e46b001d38b149455089c13b57ddefe5d \ - ffmpeg-4.1.3-win64-shared + @./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/shared/ffmpeg-4.1.4-win64-shared.zip \ + a90889871de2cab8a79b392591313a188189a353f69dde1db98aebe20b280989 \ + ffmpeg-4.1.4-win64-shared prepare-ffmpeg-dev-win64: - @./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/dev/ffmpeg-4.1.3-win64-dev.zip \ - 334b473467db096a5b74242743592a73e120a137232794508e4fc55593696a5b \ - ffmpeg-4.1.3-win64-dev + @./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/dev/ffmpeg-4.1.4-win64-dev.zip \ + 6c9d53f9e94ce1821e975ec668e5b9d6e9deb4a45d0d7e30264685d3dfbbb068 \ + ffmpeg-4.1.4-win64-dev prepare-sdl2: @./prepare-dep https://libsdl.org/release/SDL2-devel-2.0.8-mingw.tar.gz \ From 0aec1e502ef3d909595f6c7008b0960149c5039b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 3 Aug 2019 23:35:11 +0200 Subject: [PATCH 0040/2244] Update platform-tools (29.0.2) 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 82a5d09b..072aa4e3 100644 --- a/prebuilt-deps/Makefile +++ b/prebuilt-deps/Makefile @@ -35,6 +35,6 @@ prepare-sdl2: SDL2-2.0.8 prepare-adb: - @./prepare-dep https://dl.google.com/android/repository/platform-tools_r29.0.1-windows.zip \ - 2334f92cf571fd2d9bf6ff7c637765bee5d8323e0bd8e051e15927d87b54b4e8 \ + @./prepare-dep https://dl.google.com/android/repository/platform-tools_r29.0.2-windows.zip \ + d78f02e5e2c9c4c1d046dcd4e6fbdf586e5f57ef66eb0da5c2b49d745d85d5ee \ platform-tools From b54f0bfe487274f2fb02456c6f3e0458f9245786 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 3 Aug 2019 23:40:34 +0200 Subject: [PATCH 0041/2244] Upgrade SDL (2.0.10) 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 6ac98d9f..c07cb24f 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.8/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/SDL2-2.0.10/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.8/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/SDL2-2.0.10/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 0156adfe..3ad45c79 100644 --- a/cross_win32.txt +++ b/cross_win32.txt @@ -17,4 +17,4 @@ endian = 'little' [properties] prebuilt_ffmpeg_shared = 'ffmpeg-4.1.4-win32-shared' prebuilt_ffmpeg_dev = 'ffmpeg-4.1.4-win32-dev' -prebuilt_sdl2 = 'SDL2-2.0.8/i686-w64-mingw32' +prebuilt_sdl2 = 'SDL2-2.0.10/i686-w64-mingw32' diff --git a/cross_win64.txt b/cross_win64.txt index 5cd03fc6..3f222ba5 100644 --- a/cross_win64.txt +++ b/cross_win64.txt @@ -17,4 +17,4 @@ endian = 'little' [properties] prebuilt_ffmpeg_shared = 'ffmpeg-4.1.4-win64-shared' prebuilt_ffmpeg_dev = 'ffmpeg-4.1.4-win64-dev' -prebuilt_sdl2 = 'SDL2-2.0.8/x86_64-w64-mingw32' +prebuilt_sdl2 = 'SDL2-2.0.10/x86_64-w64-mingw32' diff --git a/prebuilt-deps/Makefile b/prebuilt-deps/Makefile index 072aa4e3..6cfb4100 100644 --- a/prebuilt-deps/Makefile +++ b/prebuilt-deps/Makefile @@ -30,9 +30,9 @@ prepare-ffmpeg-dev-win64: ffmpeg-4.1.4-win64-dev prepare-sdl2: - @./prepare-dep https://libsdl.org/release/SDL2-devel-2.0.8-mingw.tar.gz \ - ffff7305d634aff5e1df5b7bb935435c3a02c8b03ad94a1a2be9169a558a7961 \ - SDL2-2.0.8 + @./prepare-dep https://libsdl.org/release/SDL2-devel-2.0.10-mingw.tar.gz \ + a90a7cddaec4996f4d7be6d80c57ec69b062e132bffc513965f99217f603274a \ + SDL2-2.0.10 prepare-adb: @./prepare-dep https://dl.google.com/android/repository/platform-tools_r29.0.2-windows.zip \ From 8969444ff2b37132a72ce92512590028b0dbc0bf Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 4 Aug 2019 14:43:08 +0200 Subject: [PATCH 0042/2244] List scrcpy characteristics in README They were listed in the blog post introducing scrcpy: --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index c1967343..f050798d 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,17 @@ It works on _GNU/Linux_, _Windows_ and _macOS_. ![screenshot](assets/screenshot-debian-600.jpg) +It focuses on: + + - **lightness** (native, displays only the device screen) + - **performance** (30~60fps) + - **quality** (1920×1080 or above) + - **low latency** ([35~70ms][lowlatency]) + - **low startup time** (~1 second to display the first image) + - **non-intrusiveness** (nothing is left installed on the device) + +[lowlatency]: https://github.com/Genymobile/scrcpy/pull/646 + ## Requirements From c28619e4e8de8c195132d12ec44e92dc27e04da0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 4 Aug 2019 16:41:04 +0200 Subject: [PATCH 0043/2244] Bump version to 1.10 --- meson.build | 2 +- server/build.gradle | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/meson.build b/meson.build index bdd4a879..57b66db6 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('scrcpy', 'c', - version: '1.9', + version: '1.10', meson_version: '>= 0.37', default_options: 'c_std=c11') diff --git a/server/build.gradle b/server/build.gradle index d5c1fb00..f1b48a28 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -6,8 +6,8 @@ android { applicationId "com.genymobile.scrcpy" minSdkVersion 21 targetSdkVersion 29 - versionCode 10 - versionName "1.9" + versionCode 11 + versionName "1.10" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { From 9bcee4ea428472c884fc869a42e0d667c5958b4b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 4 Aug 2019 22:03:45 +0200 Subject: [PATCH 0044/2244] Update links to v1.10 in README and BUILD --- BUILD.md | 6 +++--- README.md | 14 +++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/BUILD.md b/BUILD.md index 68739be3..bb9a7585 100644 --- a/BUILD.md +++ b/BUILD.md @@ -234,10 +234,10 @@ You can then [run](README.md#run) _scrcpy_. ## Prebuilt server - - [`scrcpy-server-v1.9.jar`][direct-scrcpy-server] - _(SHA-256: ad7e539f100e48259b646f26982bc63e0a60a81ac87ae135e242855bef69bd1a)_ + - [`scrcpy-server-v1.10.jar`][direct-scrcpy-server] + _(SHA-256: cbeb1a4e046f1392c1dc73c3ccffd7f86dec4636b505556ea20929687a119390)_ -[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.9/scrcpy-server-v1.9.jar +[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.10/scrcpy-server-v1.10.jar Download the prebuilt server somewhere, and specify its path during the Meson configuration: diff --git a/README.md b/README.md index f050798d..f698cb4c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# scrcpy (v1.9) +# scrcpy (v1.10) 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. @@ -62,13 +62,13 @@ For Gentoo, an [Ebuild] is available: [`scrcpy/`][ebuild-link]. For Windows, for simplicity, prebuilt archives with all the dependencies (including `adb`) are available: - - [`scrcpy-win32-v1.9.zip`][direct-win32] - _(SHA-256: 3234f7fbcc26b9e399f50b5ca9ed085708954c87fda1b0dd32719d6e7dd861ef)_ - - [`scrcpy-win64-v1.9.zip`][direct-win64] - _(SHA-256: 0088eca1811ea7c7ac350d636c8465b266e6c830bb268770ff88fddbb493077e)_ + - [`scrcpy-win32-v1.10.zip`][direct-win32] + _(SHA-256: f98b400b3764404b33b212e9762dd6f1593ddb766c1480fc2609c94768e4a8e1)_ + - [`scrcpy-win64-v1.10.zip`][direct-win64] + _(SHA-256: 95de34575d873c7e95dfcfb5e74d0f6af4f70b2a5bc6fde0f48d1a05480e3a44)_ -[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v1.9/scrcpy-win32-v1.9.zip -[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.9/scrcpy-win64-v1.9.zip +[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v1.10/scrcpy-win32-v1.10.zip +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.10/scrcpy-win64-v1.10.zip You can also [build the app manually][BUILD]. From c05056343b56be65ae887f8b7ead61a8072622b9 Mon Sep 17 00:00:00 2001 From: Arne Schwabe Date: Mon, 5 Aug 2019 15:02:05 +0200 Subject: [PATCH 0045/2244] Fix building on OS X (missing NULL in queue.h) Headers seem to be a bit different in Apple land and you need to include stddef.h explicitly to the NULL declaration. This also makes the code a bit more correct, as stddef.h is the header in the C standard that defines NULL (https://en.cppreference.com/w/cpp/header/cstddef). --- app/src/queue.h | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/queue.h b/app/src/queue.h index 229ddb87..ed3d4c21 100644 --- a/app/src/queue.h +++ b/app/src/queue.h @@ -3,6 +3,7 @@ #define QUEUE_H #include +#include #include // To define a queue type of "struct foo": From 3f77c29c956ef3f4cb7779fcdf90f61b22e0093e Mon Sep 17 00:00:00 2001 From: Harsh Shandilya Date: Wed, 24 Jul 2019 21:42:30 +0530 Subject: [PATCH 0046/2244] Uprev AGP to latest stable Signed-off-by: Harsh Shandilya --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 1b6f5aef..b6ec625d 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.3.0' + classpath 'com.android.tools.build:gradle:3.4.2' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files From 44fb692a64adffa16fbbb84068949d3121f1a7ae Mon Sep 17 00:00:00 2001 From: Harsh Shandilya Date: Wed, 24 Jul 2019 21:42:44 +0530 Subject: [PATCH 0047/2244] Uprev Gradle wrapper to latest stable Signed-off-by: Harsh Shandilya Signed-off-by: Romain Vimont --- gradle/wrapper/gradle-wrapper.jar | Bin 53636 -> 55616 bytes gradle/wrapper/gradle-wrapper.properties | 3 +- gradlew | 116 ++++++++++++++--------- gradlew.bat | 30 ++++-- 4 files changed, 93 insertions(+), 56 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 13372aef5e24af05341d49695ee84e5f9b594659..5c2d1cf016b3885f6930543d57b744ea8c220a1a 100644 GIT binary patch delta 50527 zcmZo!%zR)IGhcu=GYc030|y5K1A`;OL_Sqs5S_cx&enl}fx&*FgRhKMi@2(o$P%%S zMIVb88PGJWP+$RRn0Q7VLVwp61o2nhOuNC%%E0i7lYv2Z@U+bY!-c~| z{`JjFNswVpc4+QuHW$+C+;Jm{b=zfrpLxMfkz$iuksBjj0>YYFo54@thN|Opj=@J}BU&A}+ba zbGO5#bv=n6Z)RoAwEp~Sljo$2W9ME!Tl7xi(O$hNcS08*nsDZZpJ8mtm!^}Kzsv58 zc~lEbHV?Rs34K1VS|FuEI(ux3;DlmroNgvC4SMA$m5fCz1iCx6Jm3~`Bd}=pN{AiK_3ljju6{fyX;YM&B90CHLe+d zm$)@AaSL|d-XV1A|HQ8KA9=4<8^4LUTGT6aUqj&VQP163VkW1{OebsAzb`r)`*aq+ z4|h#g`1$jhZK56AY*!CNtmERp_2A>F1h1ppfA}r=W_$R-|2?rhzqh}3-sL#ko%dv! z?p8Ce8(YPnE$g`Dvgu=$Pp`Rn^{RCkHkGaM_ zVtr`kd+Yl1qaTl4OWewuAR84h&;0fco!pI{`!X)L@VuT^JAK!$oW=V8<4k_8>Yfzs z8ggaN4nrRIXf`it7rCsgm_of}DM~w+&P;K#Sl#{d&Y7>rSl;JNo#^XX{<3Sr@p)x7 z_f+Z^Xm4}&(sj5!;TqcuwXUo;d)Fi$e$zT)D(h{1{P0EIrFW-V7u~%n^{u|g^^u@kQ-+?2&-?29BTM%S zRO_6nbLMaM@cT5$y3sG{x5f{~Lxlo|zg)EMt&2I7sN--r_fXUV;fa4l?GILcN;2;e z;8RhVmbCL;@t=9(sOaFKdij`z6S-enMm?a{^H($W`?ZtyO0lNjO+Gg}Zu43`)f7?i;U=DKi>$!897OpaybuHULvrr*SUoo#QE{R5|% zil!TFH-sil3aK_1mpkCxsrl%dKtUzFx_IUt@hG8V(YGaX zUH9o6jTKO{o}BrbdvV{UeYfT)DxO&r9Ah~#`=qq`Dle;lr**?xE*_M4X%yNQ zdWV?X$IDBnMEcJ7m+4{rT%qK{+`I@D@rB}bEp-MxY&YxX#BPfS&vkv`>~LpMS$F)!D>EiY2~E!O2oO4a z`GVgq)6;&&bEAIs`&@8|Z^?MX|G{1I{gftN6W8yF>Cf&JpR+B`dv^W(dwa$^$CKHG zXOuojx^^sv>oZ?ol49#ok!feXO>0iQQ#PR?Kl$h}*|v}Ai88OA^KjPN?>{<0=k%xD1#?EFM~_{B4Q zCLOb!C!r=CWzOw;`Tgx_0cP)ZYA7u5{xjR+nb852ZnLo1S?`#GpDgTk=V|@F+o$bK z=f>d9nG=1DQ>u=19RHfI`Szux>4}HVxOzIh(mT8oiwPYOJ+4>De! zoKo*}x~O@z=~9cC9>0FRHFD1L=zP0r=7%pGW`_RjkG1|3+Hgj}QNO{Dajgk+&sv!S zzr3WmC4cpQZ;~(MOtZ6Pu!&h}+No1i`$@ZNR?Itgq4#^9C?4BWc;3$Cm9(DBfuBW6 z&jopJsNeK2`R60_oB7gCVJz7FC-DWfU03QRx99aeiMQB3etDhUmSrcwC3`|g>e#1mqi=h8IeYOgRc%>pa`xg9 zhuaRC54Lhwrf}`?_4=k42h&&I*u8Mp^^ni6SN-HQ{cS0{U-C%)!|l8^b{z_J>K*rw zzGwZ&Qx|Vqzo6n@fN8zR5xzeg8}%iAOjelo*uRmX@S{|G>^rKzD%pNCl=f}w_~oCrKaPG^S*KOEGK+Zo7*BTy>;&r z?G@5*Ebm>eUiJLWg@ykz!t?oW?mB;4{Oz_Kb7y39SLNv#ZuAehJkw{1jrBv`n;*9% zdu^^?uxOK6$#($0fGcwl~bRW`EbGnsl$u(fzQHl#W?_tzqiz2?wsvlvzBf^zO!% z%ai9x72aX2R(YrKT-E*Uj|p#&U9oWgQaRc1SN-82t>B|iCe5lff3`2u_*%c3@Z*i0 zTVCt<@V@D)zaURc>e)Im6XQ%3IzT=PjrX}1+ zPWCcg=B=r{IpvIJ3uDcr( zHrvngT_dOZ^p!k+JgRD9Qhkojnd2ObTw&I!R z$xoE(@15lf|GA^<);dkW)3-b<7JG$i1xBQ*z~OcV{F#W{S^`e&KJWl6FFNitqgL8!K3R zExqoj+lm9M?%c_3F_v4k4d$#zs{(YTuXJ9PjcJnDG*Ay`X**~nf>H3cI_-vm` z)3dvO#H8E!W#-75!SBpfYjCOxzS-i=6LyxQNtiIdxj3YiAS^JbZC~Mod7$@afmS=tSRb!)s zwQl;QuTF4vzA4+(W%fknm`GH3I=f{=jG2At0@g_@bsEA{gO46zey1_nM#6X5-9Y!L z62@{2brHA6}K0q^edJ}ZVJDpdsDqUx~Tun_78e{4j=M=oUXGo{GGDH)7ML!#Wxy< z#rWzRPCBD8FX*>?fN#6%+@cGI+CL<6>nHZ<1$1rCsGlEQ1 zRF5ycJwbTd7mxL-wO+T?pQ4r;+U{y zYUGL|s&{W3www^PcLST%rHwn*U2q7qeLB~Z+rGa)LMS+F-gNG1N9tRD2CTofEhuNt zR_DW0R5=q@vOPc8?y!HsIo+Al+KYCE>?tcwbj=m3VVe}$T2$1fctYU%q$wM^{x&nW zN6)yPxijL>^!QzSc=R}xx~g`5{S>mNRArh(wrxiBHgn^*0#jbr)=KHle#RCTwv|tA z_B$TIlDf@xa#0&KmuH;+{dwoI`aQ8~1}jf2ongMyrFw{ zyd;(2wUHvco5G#8nQmBnds5czNnIv6o^LPz&((ev^=X5Rla6Na!&r9i@|nEeiZ}S` z!=8Psx%>1(TSx4>p5;HE_Fql%@etW4E?!*j*~H_s>!kNh@9VoBb@iW9vH4vTxFUR- z?Jlp+`EBLSUaQkI1^P4RnlC=TLG|pl@W#q&u|2X)n#ZkQ^xS@FAzUAOu`uG$^6x7G zlX9mYS@ZhU=k~jQTJ;S|6Lp>~`?B@33HM3wtonKpi|6kvPD_c!n;hnSnje$3ZrU@o zF6ND82j{7Z?+{q~s$9C%9Sw4M#W$TtLD*IM<`{?d_m-^mi?$Yf6x9%N{ zUbcGuihpg!vRM)FlZ1BO&b}3T?@Yh9K7Zt@s%=MuVn4V0d_20@f1#)25D=>WqSR}u$dWjUG12PmDNj)HHJ6r zXZcj#jFWdXxV|}bh0iy?WwNb@SFPaN{q4wAO+d4ZAZJ75;qq z7=87I%30mb6U4uo^=BO8+BQ4kQP`fTdynZT@8i$EZ2vkm({WL?S4itS&BUouM;2QO z*1vz!^GmTH`LT|c)a-Q&H*I_>Rnwbz-}Hy{?DdKVIFsYvxk>TnEZd@c=Y83gB<-*T zjeR%Tc&|9U+@!rb*>QR=Yphw8v3AM%%ez+0yL8H3OFqzYZPnIk2f8Ly%7&T;Shqje zaAwgjE1jj@ORp^rTc5v>KWd}Q?X5Z5yY4#jwbmln_hizzSV7iW?%mljoW%ZX!ucU8SV{6cU;e*UH{@@Q?k!#*?vKvKQeV3iX^*e3jEWP;4c`daI z3vQf9$h|TB;I6X&^_$pgPUVu;P%c>Lkrs;=jTrLx;o1S+|- z?c018MjP+Q<(u8^9GzD9`}FFpu&~&T->{zIJ-=|}N8uGbxu+L= zoT6q`y~>mGj)O<-tT49qYgYYKnZ=aBIeV)1YNL$0htGC@oi&Xo>hM45dV|CNcy2}I zEtojvt^R{;-*$ca+%}8<`rbc_{C1sxT)OK`X-#PTX2n|l%Um~3|Eztoqy63e^DH~3 zU-^BZhxtY)-?@@awjFvhZt{H>9=aPIxhT=M@mHVm++8#8+46Yn&r>*l*`kwwk-WU> z^W%mM;e}nB{2y~$H0wXq+1JDVL3GaIda>mnOE29!5GiP*P&a3?){Ue@m*ofggJwtV zX)1s0E^%dUg;>9FA+OiILp#-H=<#p5R^L+}I^~zyAB$?oQ#CC1jq_bhOZW7A-w^Jc z9e3FOV%U7Io*J)>)4esnZPE-?yjmgj-g)VYE$7yK=nc6jwoKeeK+5@efg7Ajegs$*&5o- zwf=`MdDfUu-eFmCUgjJB`A7FI)Jn=^sECw*nC+;(x>&A%(~TKRj@L-x#6R>@BQWg%|b5T zf@g)h9w)H1`Xjd$6C=f0<0xc!d* z_n|t!Z#>~t%_~S&6KY7iIcWpTC?}a>B%~)>%^|8c)tu3yPT|}@^t;?`Y#8hivBR2 z-ahHhcEc?BrPALf{qp{i>Q*qx?%XB0oZ0jZSo_VkqyJyU3ztVXjbDQm1 z^8)UPd9ix&6-Q)Ns^YYg8N1vgYNI1-PiS?Yzxwa$r}QHibu_55 z(Pz;@1_p*x3=9m03=9nUMd|vJXRz>3UKh?$A5fH^UX)r~?2}nslA4!Vq?eqNSX^A% zdyuQiLB#dF_T?;5m($-|FF4$u<~o7HHR!je{IU*ZH-QRl>y*si9v7W2Pl-=81PaX28<>QU@0 zR(qGb)oHZ}%^7M{uS99H$=w~M92CI8DtUb?lqv?g;^*jZn{ zC@t%aQ`x+_a-3wY`#trZbF43V`h*hQxi_TOmmSR#J$@&6mQ%FG_H7pe5-Jw*^BBMN zS*=ph_GRT6gTvB1^^EWTJSh>YyFw53O{#|_$vi|?lR|B9G-sV-JUk?vlVfW4X{8Gs}CjW)2l0~MM>OAz~dA0oQ zk&i6XK8o_jU;SX(`0eA#g&Vn*e|~((9h~m&_USxVNngB?{_M#5+a=F72k%R_{QQ?` zt@FCzkG{v}8@^p7lJRlvq>|PDTwEQVu$E1Uy?xGl-xs~v+mw#6>m2{o&w0e?XU+E> zhmsSzK^t=K6g^p)mFZV?`Pm%ZskYk_ZW%92Us*g?;;Xn$*2%uz_iD=S8Fz0xqgR=> zQ)lm+4^x|B*G5c#+L3ztpp#>Ln3nLeYG(e7xb#nn$ns{X_3>+RS=g zGsM>4s;=zBmfX~h`>SKGOfH_DaxnbVlc}4Y>KY#j+?BGs)wS`i;$r8$t*n;|m&9`N z7<9Q#)!2E@S}kp{xR%joC)3~m`t6y{o{C$sWbww@OP^lMTpg43-DmlWX@`C|Z)>l= zmNWNeiF;|(-P@*@qm?AM_@DUZ>6|Ru891%|{oAhxzLfkooZ6y!Uq{Dp##0BM!`Dq~ zoDZ9u)_6Ol|4g~Ly+i#_#;IB5KKow)Z~D z$U0TBxl2~HxYCdBW}d5E?A9gu##2vPn>BR{)C(NR@m^RcvLjzw&dvB~*j}f-*M7h2 zJ7cGsvw%OGSLTQ!^Ii*;gf;wU4{-n3c7S>I?)NTNH+5e!{-Cr@vV!xz;ETU5v5sfH z-fVScz8}i<>%*qT@DHmR->-c7yJG5roynhlt_H5%8>Z5D*g5a2-ZI(i{^B;7zl>td zLN8rD$|iBSUOZNRt-`E*9=9`@*SEISB_s$fXO-I{t;4ExZ&IWDBX_pCp7|0LQt2jp z@*eQ+(VQ@YFWu+X-Wg$y|EGLb@a5;?-ZUf5NO%58PM`H#W2|!iZF$c&?}^4Y<2|Vl ztkz}y*x9(fFzT>A&mW=i$P3nv>$|tr8Z)teyCYoG`+N7+VCDMVxA}T_<9zfh-UY3g z6fu`mu*T{{>5Mhse7--+Tr~0Nl*mU<&-r{WYqnl@;h{+9%rnm;7#9<30IjXK0S*={Hl)tk|^Adh-78vPEFSGctKH?zrJaSnU24%eM!g?IbKWaaXp4<)X7Tt2$%6S3T{Q2-&ja+ppUe^}Cv$IH?8aHgi}X@KT64Q zNreBO)ph^Kx{x03P?Ol*op;}_sap~9Wu5uncC)FSQs2tnJ^uHA&7AM9Qm|`mT$is-+o8?x^BiR z8e5wjqhyaAx9F_@aQNf=KUROlk8S8G6g;@(g2h^1p3Ac*D|PO<(Hgi>JoypBd;3<- zDpO6q$ry97vX&v?t3@nwPAY|Bd>-E4h3UN0{dd{b}NpM2zE-K&30nZ74F z%BoBcc=-P5-8WZZRo4&hwht=yN$r|>Ydre`KB;I1U-;MjSZdLqyPLb_W?Wpretfyp zqP8V%Hu8%P>@Qq&{*uAAQ+k)SJ!Ni|J6y51Un1p##_}J>!^8J3?_()<`0V@Uekb$N z`)f^uZ5GVuos`&O^YuXK*I2#Pr{_O67W(qUJ>%`pR(F{HJpAPUP&}w8uia!sN3(YV|hZO^Z$?69YpV3v2@X z`XQsZPQrTtS3bwBmAM7I4)txrxZG~YTo-dA65dC#A`V;;}1 zZ!_az(=APqo4qU7O4asp^|at$2MkJPPrvV%pDX=xZcN?X6NV)^JN=HD%i3q(3ibQ@ z{zLECoL;GU+htD2CuH=hZ{o@I&YNgfvQ(!%Kdk@Tj_t|sZpOSQ;9fS@)Yf~=>x9|+ z^Ft+sG_PyFR6Sp>b1UZmo0(6IzJIxzurvDMy;;>qg4b0?=uZA}E{OS@K(Bh1m~MFd z#vH#neLeYNeCB704BQTBvsLa(xZbKIx&4*sjt53ZX7ze@)oE2-JZ@0vpQ*Csm{%C5 z|GUfbLUF4rnl0JBE_1l}L-%6-{FPnhGlD-dNIBns+y8B4)=k%UhV?G%Zmq81H`0E6 z@>YWA=N}%m>$m5l1zjw-SgL^4v^DZ3jxFntB>LVi^YFHAiW_MMW@m_JV zuHc~~=}T|;clgZ=CFwRUyV^DT5@Z&KH>>&xsnzhZACCUF zDEd$b+srlbvu3DXePmw1s9N4u=BsMC{KW+;%{hx+vnw5v+MT#|`lF15?=R~Eyv2~$ zddy5VGhfchz`!dud84ys{oF0fuPQARew$&J?4TalqRG~srpW18py0ryRM7BriwAr8 z_N4s&Nfx$#sz;aH>bxv$EgR&wNmOC4#oq6)zT9&bI)2iA_wvhASzn(1zHsMU^Zb9| z&-Y5oRsMOX%@Eef$~A9clJUHiPDeLC`H*9IXYB(^EiILuOy@&C*sz}e{+PACZcl*F zxz!bdac_=$|EXBnR2_1~>sVCGgl_%dk41Z&4r+&*?qaQ2-L!pWgbw4oE<)?Sg5BTU=vx;l}v2+m|B};p4MbCdScKYr5D}E^LTk`1ikFKWmt8VnI z=REW3#_daDnf9C3FRy(+@!Y$)Z{2s5e=jS2FT~;E_hR|iWjp4{WW3tAHliq@`M{Mh zqu@k=bq9k z{f+xa)Y&?()tyHs9a~rOXh!1KSH~Z*QvP+_#;obe zX=b_IM|szU@kgqOznw90k_h8bBj4S+Zuhzr?|Mo8iI}lI-9#=s%T$=t_NIRC-M1eE zRv2Ze`3rmGMMUpe_&ny6nb7XyMV}a=j&BomHf(<*@%XvorHPm4Z|{F|ImmTG$;9mV zTYX1oAA5PpPdrRXd~;Vqk$^7wc>QlD43Dn8OFiuav%sqR|GvfRmky7&Cx7b)`G zzI}eL)H+M9ediv$`1|*0p1=;x7>NmoX7;Ro!4~ z-%8tUa<)5z<%^2q4w|!swW{}Jb*_q5+xpT?F{Jv;7E#kXU*<&??F;j0^kL@jF#GId zl=ted)VD?MhlM9zOHf|=P~*k*3ogr@4x4|}{k8u`fKj_z@drt+O@75Ur!D8)Q8OWO z!Nc1>`kLNn{gB(|KQEZ+>GDg4hs7fP*4N8e^V%QaH~qt5ANE0*xBt=dGmAbtUs<`F zP11$e{_uIxKTe1BKWg(D+PEGT@XNGlT(0$FYuo(?+nI`O&b*rR)I#q0!!q%60Z*r1 zTDV{P%L9r3d3WVm2Kk%xBs1$7Ye| zyK|zCpVs>w`WJaH-sF#H{pAnkl6UGK%xbHDBz5}m+%w$S6CFYiPOE=-zvzB4*T2ON z3;%>KR8Tx3;yNYskoqanz=fQyKW6%yTR48XQsMW;SN(0op%c17@gZ4GQ=@+L1y}lp zaQY?`ub$^Kt8B&n1rs^j3`C|r&+GWdC2PJm;F`MPwVxLX>reZ%EXwgb{$+~bs`xpp z;sYnN-#zXp5g08T?7CGXd~rwStQDe+u2(XXtS+rFF}Hg#Yer*s*Nl~&kx6F8K`f$% z?!v{Yh4aqvJF%><)>wMxr%vZ%qfar5pIZA}J6mzbX0v{!(aFO%Y_G|SwMwg9o}zU| z=Y`ezYn99$?_yTY?r5*qG+MDdYE5IwA&*Gu?jo;k$GLOnx`du{PGr4#A;3g@#yeHFd)AlA zy4OfF78tVBZYX&4&Re=dfls8|X}iE0&CG?9v!1uIiGQ7ODWT&2<*e@1$@OJF+786A zmafsRTqicg-@PHpBJ9E!=eW$rnupfjoR=M^dVOMX_w&$YH&}b+CLGOOHCa+*diZ9m zWzUY3FS~T4C@5-sh0NU<;m@A=?ArK6Z;z^W%FU&xt*50)Iv2*?6JI0p^Lo3wXsrI3 zUng^yhG!jH>9@AjclkxL($&$z<( zt~IZwEa+P=l>hDPwTPb`A}3>2i^YAnSGb#rh<)bZo{?+iy0;@Z&N}_M_3H8qM(0md zFMcd5xo_T!1rK9x_XcO$`mvuq@x1)^&9=7Ylm{36U+~;{D|gTS$;RW_MaDMYynOHU zO-*@LfAZKH8M9SEW(gLh`!6gpQgu7@8n@n6JiOI+M@DJPf6iNi^S?c_@2(vgM z@U8et44cdW;UCeRb}}mB=H1mRLPRse-2=oTt(fln@jcz`7I@K|XX#DxFR2gg1-?v; zcT0_rT(D~SdGQNXMl)tPezn-?Hfg!%r^di5Md=H6+C=Dmzs6eIE9sNJPU2usn(B%P zQHEVJQ)W&*_=cy+v3}i|%x~;}PrDn0`#m!a*ONZ@C(U$)SznT4{DvZtrsN0995N)# z`;!vmFWC1)w{6T2>E_&`()_D%^LFI}8|TIGUB18;lB}7QDETbe@>&9`)y7Hp_Dt=I zIQ#r;l*A6+^o?@<&c^SKxy?!M(M#Z7x8Q2w-3hPdWDd>sd*k-g#>%I@JWQ!s`OhpT z&gmT`5~))@E0oA6Uzbx6bhVGIn-as;XDq`u*RH`)V@fIe`o4C_}=W73F@wzqDqHC&orgFXGD%BKOb;>>c+5A{^09%T0AU{ z7pw1{x}ouQ#pRzlay1O{UT;Fu6jb6rJwN}sw4Nbi!TW`;6%~GTuA6ejM$u>4>^bq4 z!HZJbV@2vTFXznl6g=10l2F?18kiO}*Y=Cf`>#ijEV?JmcJWiX>3V+7NnpXD#hbkI@uiBMF5?sna)Tazzu-pl--7uC51 z4iCR3vokPo3QX=}k(&JO2;1b-eq8lG>t#}9>+ZcVynME0StHx@Z30U#oSl|Zec{?N zc3n^HiM~;j)D|w(n0)tT+N_gjtgLS{^V@s~6Vc~buI}opc_SxyRl}kc9bBi@|C6n= zV|w-Z+?zbJ*||A?6Yqbnwt4@%cHjN-Z(Hm4e^6@ht;m*?b~bZQojm#Mk{q9#GTn*B z%j!iU3|GtuOE@lYc*-1$qAB4Y1#j*yTB;!JZ{~VareCpH*l^AfP0{bCruLtj+Q0J0 zj*y=&Jl`KqE_xb#_J@g`GV8nPm(QC$Hn6+iQ&gR^0^(%ku>S7PAaXxCE{Nl*=C&#ug-TMAWMMJxYP5qh#w}Z+|Ymb*^uYH)vwBsEbX-=-iuqpJ zOCpYK?4PxB(Q0qY#&cU9KMS%9KOg1xs%7=_&uz1%xPRS?3Ve0^^<5tSSF^X}$m;oV zZ(%*BbNAfRHqC_RE9wQls+~P_iYUQ*Td;+vZb| z`ac7@1IzokTB_&jQq%`@+JW{^EaQJ=5@0{nHIcmX(&D zDq#L7cH3Z6tKIXKT+6CyU5}TzJKAS`xq8|8^PHMn7I_g%1U_zKyS6ib>7^-CIbx50 zWVt0S_bX&y(T6^(sdKscH_cXyYO}R9HeK+%R+P10tHS5tW0qK3N5_N0OyQyH4)z~y z>`ZoZbBZv_d6simdsoT!@{JC>44>+)f=gl#e+$~XC*tKg#(6h31eX}Q6@*7w%G?d= zd3Y~9U1zz25Rb&YC6hxxENOfHs9LJ*u^4akhS0-iH}}ZjHvN*&-ZuBf(nS7!{?S|4 znfzh+%M^Zn^6P7hGE;fO5A%Oi=JkJQa`^UJpUjG=bKf5vm#S-;pH<;zWC9lP$xsRCbDVeTCZ9!!_2YZW)$(=y%k zkJSE)AGqpPKPWy#L6~T7Tt()W0PUOaDY3j{gwO zWv-ww^YUwvwK?r?D`Z`N#~I&ud{$DyXMIt30iR^m!TOEW8DDuT7ny%F=dGXqFMo3R zma3IH3*Y{lz`Z8Y=-E_{DcX#yA2{_HwLcgCW!qeOpio==^WQ6T-puN5R-S6KZeev@ z9A{2`#15%xb6j7_d7YSbd6My-=3O@44t}kl9cyo`@bCTkNGtcY&&PEx&05SEFn{F#&b`P>?~jOAj0*$#fs|5dn9Zp8*;7r zEgkXm^7Pn?(lW{a)`giGn8m*liG1M}Gx@6d#t)7!*3@$^jyzeNEce!}b6cT=c82u6 zshR52Kd&!!lsNS@bEUx%{j8qy9Lt__r`#H4=eFw&+xxDH^u7Dddqg(_Wq~TXMM+x{kAu}-$q+XxeJSYGgrHQ-bp2YmTT~)Y_WwYHa6V_MshP< zRV4e*Db2EBe`@Jvws3mxlwSwfzHH2WE~T3wZ^!UM`DN$&88s_+xZC^YD=Yo&*skYb zTR+F9dzsL*Scf+sEKS&r*S1XCrhQ||hCed)t9D3fB_8Fuwnl$pF5BA}fg-~jJBs+i z=W?3dmTMHU>n+F-fBw*NjZBPf^GemfTf+Z-X?grmXr0{TYtKK-+msPKhbdMy#(AMo z80+lSOMh1Uu({SEz|Vedo$TrZvksTq{?nWPex8R-eUaRI(HRHzEUV@pI2};EsMK%W z^2rrb(m5|okv+P&IKsN@m%aW}U3ae?3y=J`AYXqcCj4Fo*Bv##$MfcS%zdfi%f_+v zB{O+x0~+j1bWH)^_zOZ(l>#)>6P%16#hSH5msUy;J6IqR;0SpS?xfxSr{Js;jj z-bion4gEL&JWFK#uDLqClV`i1IIUx8=^19{;b-R(HcvG;PBr|Vg8M(mv*j1muO-Zn zX#cC!dD}%lPu$tU<8-EB8{EtLEoG4M@V8aeT}tc$eLr26_Kj>8<6k8{Z^ zieXW{a!Et}P4A_{65)9bzxXb3U72S4D7M1s;N15~OPdaJ9sA#UFPY2gzS@S}8dB5F zMSV9?ea9EXq-pPZ;Ez_~!SFhTuZ{BnA!wn`yp4S~Lo7!bLJ6cpcOgKFv zdPE+kK2w#Psd{dYBInjD-LUMfYom3euCK~i8X6#^(3O2P>dvaQw|rM`yS8fnw!dP3 zyO#a`eD6%kBp#+u%HQ{vPcJ_I$^P`-&v(Sx`2{b^{5#=$yK;w#@)AkSvl^Lh>5EU+ zPfY8n=_?IZe6D44&h5{uw{@|G>b^3D3 zzmr`4<%RoZ%RP|fI$v^`MX%tpoZggqN|{}vdWDxm&XpExPT#q5(&?wg(@ZumooU>| z_bI0J`Ob=chqzOYIhmh*61=2-Vz>5_oe?);m6Ge1@6zm_zUzlcrQYdhUUBc9X{{@J zyw>fZXkJvuUW2k-CfZ9^igKlMv>M08sM~(!GS}F1>gej&nrS6#3NIVwO-WljJMM8Q z*ZS~^<~{S?E{n=5y6k1={;)T1mG~>|pV@l%ROFv~8M{9`o0s)Omn%CjFKxa_o%!0y za(8`w=e&L-R$sZz%lP=&T$zH8m3sHIa@aRp9!V6`d-SNRa-P18t7zZOm7yZHP8~D& zHRJK6oZuaYeI3_LT^6iXrnx8f;riV#Yi8a)Cw`(SzhjU1u9w35JnZx*ZSOv6BCh$z zS8c1A%(ca#j=Ex(lYQpBnk72*^{Z=n>(=GQ>|R#(bJy0btEJ4=*RPA&JxO!n9emDX(2yH6=4CR^?XIJ*lgI9QW=~ zUbS`m>x-YPiUqV||9QAuNL>8m6Kv$WX02b(X}!0zE{1W6IiGc0Xrp_%>FUek4H-@M zUUDBxZp{7FYod;za(}Za;UKZk;h{w%yW=zO#+%9g}U2m{@5n3SZ8v^+&hlI`6x*S>-;u z{_>l+thF~4mnzIkS{0vm_0FF;4!didryuCNr*}Gu>wQC{!HaJ8h4)O_mW%Q%37T=T zIw!YEU6}ntIJft<8BaDl6?C4w_GQMqZg*z4`tVBI4IBz#>``B5C~8L^d9ib)ZPwwp zh0G*-b?w;G58IyUMZ_DMEX&EsDm-9P#}R(|Y-rL8H|Ni}Yp>t( zougT^^3eUT7j_D3L(R1g|NB~Ha^cd{bw^AmavU(tp76l=sa(^_Jtd37UmM1oT*$k5 z=^ej|;&YAqY}RAVPfHnt86UI#atg1$B)_NmnV#g1+n0`V#kW1Zt@BNIUgXtfzqcJb zlJ}Xr&8Wtw@xd0a6PpCStp3@vL(C*_f^7GZ#ddd({4&_PIrh8cuKO~DthzfKt@6`W zeDPlW;ida+>V>eG)K z7r1ubo_UOCu7dI}hL-hSJMTW4GWD$J*2Emi-tEp?Uk2X^0P1|GY+j5`N#J~x+9mPrGDZRg&k`be2_UI(P^5o$>R2U_EOGy7kN${a#0mCpUfI@ zZ2IZzY&REKJ+Mwoc3aQBLbR&;q#%Djzr&xElCf%cW%z7!Ty_VE8E)-e`n1E-)JVyZ z>zT(S+qvQw zekd2+C!}~!JoCL5x0_EYt7LOX+BcmSj>TreG20eoTwFWz)?B;nYdXtz_3YH1`NGfQ zOG@dvNom_mihpgNr5l~b z&f{2H{7d}&^rPvr#~%svE{dF6{JT6-VRyZbU1IU_-M*8q91@-uCT2csvu1>= zT@I<5H81d`Y*qNuxT=Vy_bBMUMYZnSR_L$nkRJjI&#wmIU+VSJ^(8J5zr4%dhTV zW|+$u?$NO4)fM=b^-#~JTWO)2%B-oC*+Mt3X1nas<#-aeSGc~@;>Odtb7x%-Z3|jd z|3CEX^M%n*=YRgYXgPbxy~E!Pa;>z4JNG8vPQ8S$rICCCd|F>=GM>LcxT?r+8f$MT-Ssgc^C}0d+U7P=Ax{){+a2bk27AJ)|zz6#UQ`R@ulnCkcVgc zpSDlFctJaKK2PLb&EDTaU!z+yH70xvo8@|A0^`)`BfZ~N*EH~J-C6zVaMBO=AN2y8 zO6#|*>2cet$2n8W-Yb8_9|`+)pM*bsQ^|2#&pqkx*~WrHA6e}D6|Lpho{?nOo$~hW zhpnwj8m>9q&(DYd6#pO)P#N_!{YT`~dQA_R={x@%GPHGe+j=aduCHFTh0pu*ABKfN zPiLOe|FHGM`$bNT|GQ6SGN0C&8b2vBc;B8&^+7*Ur`Dfczv9m-mfd?6PE=gjv}l!w z%?tCn5?eLqC`a~G-Tu8gFyC7x>fzKCAEjICSAWX>!yxqW)q-_LS^VNG-*mk`xUFD~ zLpSGEKHZ#-%SR2}6Q&jPly3b{t}3L>y|^p*XK9`D!}}}$RR3!}RsY03am`_^A0c&< z?+5)n|Hq80{#=i0pV#}()>`|uIeu{eH_e=-SUTg>d(S1mXFo{3l<<13RKS8>fBqdW zos)KaKiXFx{&M~o{i*g3R)_qXknjG%rfQ9Q=j=Cv^W7@VpC9Qz=qWSfnRoYT;r&kx zX7$bevMc|{fm_T=A09h+^!f|Me?|`fGfg!w-|+eRh@tNKHdkfY`fHK`Uw4a~W!<{< z)11mV<{}A#?zbC%NrVPi{OC&hv%oX6{oh7b$^M`Bk|UckKX=BQtXcZIcaCGu?Bz43 zRQz=-OpbfCD*1T0-hQ^Mw>w%-$B0{PJ@Rvv(PPh(F5HuhA9N;94~%1d#=^m@7rQ2h zpRwK1A-HJ+ztFjJ1^4uAjyF35WE=gL#xlNXnsGP#@T zGO0$RF~WDn-2;)AZzetC{w!nCa^u*-S!Ykw<*B(n=`$9)q1VS1%`?$9RdacrUHzk) z9D%7%%8Hz}^F5pPILz$0(!m`IyR~_IcEqkQdN9}Q$+|Dntydo``yXSY=4^iDqF%h? z&aO+=%*z~gnIE6-*>tn0Upc}=KZvXS$d1Iuh|*0zn7L=kZ?pb+BRu6sl1R#`ONS;J zoI12X^~#PW9&xU;70h!MFH^fLdb57&qLK{8dgh<{rYsBBO6fK(D zHZ@MlTfMpcrAh0jk7mv5j(ALToxE<*d%>CchlFS69}1oJQ?2Iah2OQm7^_-l|9zsy zdVcBAyC$3D&aP-%)SkgoALkZ+!}eR=sg(sMl6_i*{~oqI7bkT)e_eZB zZbY<;{gSy8a+IFznV5HIO{Pg_U5~l?-JYvuYxb~a7yQ}!fR}W38-n2V&)?YKJXMt{KUoD$# zULo>w*1p8{?((mD9|Z2wjK7j(e05Wk+v57WEZfd6ow!Bv!VMcZlDnn*9izRa$l~gahxnu%e`(%j->P?X-R_UijudPD?WyB4zRPd5 z^vR3ECiTmmZi}7@e!uKsOv(G13?0** z1$`c7XxKU!OQgx(_coqg#Py~>AaU~AC583}6}#k*W^f)lxwiA+rGtmJF^5cZs-%Z?a^{<%Fcpv)gL}lElL5*Dc9=)mZdxak#0dx-I(! z<*XCS)+n3AE^H2F>$u^~ukKRAX(ghTf;O8!ITPsP z*(E*u&5IegZ#>b+%_*vSqZHYitbbP6<=po7%UP>~HfS1ctl>1f<9ImX%Ek^2xg%2~ zKiuD0F^}nfB-{R#ZS(76GOH#F*J{}^t(1w_*unSEcgcEB&v@tlSr@(U9hto8%=Y6x z{k!k?a%rTMZQ|jVZo|^=73^jO`A0dG>e9zS(9T$8YjB#3&|hzU1w@ zpu+FmvAkci6nOgcZe8(xek$Qy+MYL!Ejt@7KE0A*w|;3U=YqN3l^O4wG~cy7dgpj* zm$>n}dgc>l!K{xC7P++W20RhF)qG-$c;HjTHSbxM=4GsAFL=Yc^)JUA_JqD>gWHD} zeB&r6=6LsiM&^++h9YK}&yl}WOLnuKd~IoLvgKlm70-{I8*WQg&5LW;sQtpH>f)DG z+6LcKf0_PT*B%+aEZoOJ-jDHtfq^E2MDyJG zW;(6c+!C~~YLT)DYi;eM>6g_7b;DPDE6%pe(f#dt+xzU@ zC#$v3eJr0muTIq+U(U~uFX4a`W%VS!vI>F8)c(`D4>`ihIM+?t)FzC$!I7Zh2we1`Gb%Pm{07zw{SKVmY+x ziKO(kGYiaL>IOHoYFg_ov7cQgz}PnVZL!qC3%6~48Vj79G~Z0<=g-s}z52XTU;V9K zX?dFyWGAdCn3vMZ5Vy@pL3oRd#+J#STy_Zkp6n|5nRKD*4j zz?-#Pa@F?JL0mZuH#nvhI-4$Dz{NkM-f-@{womsK1}nZas^Xt=k9{rwgkAP5OZg{E zwR3U%#qq_?P3J$$6}z@ac8*^^^9SF|UNHS-iP_)Qe_Xw}PKh7n{_={+x9<67_gB{1 zIbAa~p1CWGZ8uv9>zDAyr)NEH3$And&zgBeL`rl1WsO$5uoYi9rfQ44S$uQJl7}&F z7uMD{NTz(NuW`E>S-WAEvMH=gP>O(s!?Za~yc%wBpva`5)KB{JWp=pa0{(;E?M7Zb!SHc+X$% zz`01%_et_Q+huB>Ue+%#Joz;E?4o0AC!SsWKdbfJ-(~gvitWn(-rG9uymS7GhIrX8 zlY=F^VV%pSE=qb*5MCeo+GP8Tj7<-=F1wJD`DcXNBS%D4<|h@)IC?(TGU^+H+Byl%Y}lIcV7N2)+_N6nNYYNPE8*4TjU$*Nq(&%nKSrZnx(yQ-;lfbrrcMnW{>3-xt+JNJS z{Z`-dnHU&MSQ!}P!Q1>}#rW#8^DiZc9RJThJv1-5=b^(5wPG#tSo1yIhK4HM9v>!c zNZ3?<{?z1ko42eH>5V_?ztAoH5x0GV{Hcn0L6Uu@-j^>wFFNxMdeH5*Sk^ti=*=P2gmS-I}?)^h!y7u5AU z3Rj=sV7&Ue)g!;~IX$gQJJU*^J!*Zf>zrDzbn8WnqP)oQ3mjK+wljw=R-SIXX?Cts z#Y$1FwGR2qF5XV+D1WX&R({SvNAPCNHwJ%4|z8|S*1DMC+UCMM7L_`XhNXOe^Xw;79yCVTNj*E%Z~W*Ri{ zubA+Lwf?xt*@bsziZxf=ta&CHG_yIcIVz8Y;Tn zzh*Z-c}nL-PsM2Co-

E|l>~@7Z!Z;baDSe{6}W0i*msSs zo;RjU`l?y9wU1Z7U1!~8tv~&9KAw2`;8oZ1fM+|k3d=Q?2nO6$%li|5cbfU_+32I- zy!m;Og4-Aw7!sHn7%af4@`D8LziepWnQ?DXMw)`a@-+Cxr@)z7)E+4RFh_0!)aB`Zu0p5C3c;pems_fC|k zzstDjSD#X|&~?9l=^xi8cYh@)oS)TsrRTHVijb%BoL_If`IdR==()x1McaPW3>S$hnGhq7@3y$R;_1}2(960-#SXQvjI3B|w zUu<`c^Y{Pt6R$>Sc6wNUe4ggNt;lI%T3F;vQ{K>jl5+oNGC3`7+gLV>q^58{gN9b7awu}Z}MmEkLq(;flOd`yldb@H!C zw>VMNc6H;ti-{58A5**zNqx@U)pe1VE4)6_X7-h`5`(-MJ!&?!0&niI$J;TKeK>RL z`5Ngp6Lx%>b>9X3Oc7bH8;``;85pAZ7#P%`(JES>nJgVHYMbmW6)AVTFG+oCviMQM zwK_M&I87YxuJ<+*U z^)_pyNT`i$y(5d|&5f>Ftq)Jc^f?^$y}3{I@UpJ89DNUk1#5!-bw8eIU?+NT`Nx?Y zB{gA7(nD+fo}G6u5Q?Aha9h{&5F7bp;>sNly%TF9r|efc9Dek|b(deey7F5-`U>tB zJ;t3_W8L)r!S1I2NfP(DAO9};;VP=HX<-!`aryJ)w38dpUfoomET+R6b!!t(wbJ_O zUT&TTJG+aMCtnWM?*1HHtUW!=I9U5Lck#=a-CT<arA?JHAWEdJLib9-;^A>ZVz%Z6(v zNq=j2>gJtTGQ}kMMR=3=;kx_6rwf9*i(fj|J1IPj-ST!_n1Jb zexEOyeklIKt~Tox87sPWm$T@7D|@H)!s3Rp>7AqTk@9uMPh#tJH~;TV@r^zzEmfxC zxcGx}PhxWKQ~{a-~tAS~?Q2kTwm>N-7zi@A2&&9S>MSz03M(>sCNGmc-3uaG$= zRrdPt-2H-w?wLE>mX=~w|6lK~{pps=t=do5PCPyHaGw0F(}ykz>?srL;P-Oop4uIG zcj3;2Kd*KeaHZC639xzlw&dt#UgOCbSFXRd=6+rFaGHt3^^0M4pH$|Xnx(JrJjr(F zWxe;xJ-cqZ_8-68Te$T3@6ZIB*lVAcGJo5%Nmh2xru{|Rz4|sjWc|1)HeuDAwa)b& zIuAwmPdfLhdyVAX3Wnzk9yG_8CQf+G|29(nXR`1TnP+mRw53zjgYwV@ljsZYqpaR# z{+{L{6y=n}GgPUZ!++-&M^kHh zeyr^|FY~POo5@pFon7~OuH5KPIGOri;;F!t(5V)_pO*Yyv;OmvDRJpWRlAxR+pFT; z40g`VdAIh#-rMhT1^oiAZ+4%#HFHsl#NWv2-e;_%onIf>)V(a|G1oewlgIyx-}>c)g7t}o-c`nOpwQ^nZ ze&^lErjI6{xF+PP=f3O5{)b{OO_#--n(|(4y~yng|Itq7YNJKJr^c(YWGlbr_dCUP&+io1KfgEo@9+Ee|5!dK z?c-h`9LOrnctt_#fW*B31IY$|sboftieSHclWx^dY06|Z=1ODEaJbTMX}M4L=pK=e z3?VumKdiq-90{`3d(A= z^Xkg_l&l1IJ3YQKtIU~uxv8;apY^sxHDxB=$J1t(np}>&a>+7w!MCPFsdFLmeVbK{KDqRvplz-73xzdU4BWsz$irH`i4WoZc+VTM3z||_h?_f zmFMxFwv)LpZnZgV;E^^qwp{)|$!EicX_M^Dr1#FeVBD8D^Gjv@wXeErm)@l0Ih)Td z>Prj1_NB%7%0i!&dNb#>eY(W*s=GrsOzq|>9n%Rnw{N~y`);qS`;x^ym)D+2j%z<9 z9A|OkMDsMmn|F3BR=p#f4`_EO4JEm?Hu4-J; zr(RMM(6*|AaXIU(Z`WpC*`!&&ZmCk|lmi#DL^!wDE<9^AcVQCmYpqL0A17-470bB4 zy2>uy>R8*p^EbnGF7>ul-n;5HQ*7YvZEwz7i*a%cqOhp z@jy9p!dfM6cSpaYyg93ObWZ8gt}bnWO*JX$|ojxPVm#L}ghe(BgU*p+}ZO)1Qh^wXw~H@D4wGwI=neOp2vPt0BA`#LR%>Bd$G{aUl_lXQ=5Z%GQ}{HT)p zG5ufri=b4wvWN#YqCbpP4}9kM6wmu$=fb+KWbt&E#z4zASupxYESPHCXN}^Y?-$b@g)Y z`=7sCu;1k6iN`-W^j|FgRxcd&tI6(qY12K~ zIbRYoe_!;wA6s$4-JC7*hr+pC$pVKZ9BsENo7?v6rHVXy`D_VL>H29v^-BdMuX8i%nKTcQHOB8j^c@lm~bBloK2`Qx)3h#8b z-hOk+vZ|)1t~K6Peu`?Wlh?Et|dm+P7iyHb?f1#dG8;dgj@k8jw+{YVIyv1d_2-++_&h1S*msK8=9p+%Mg7aU z^ECU^&sEQyUS_S&Dk%y7)T3do^tCMZN5}omOBTO+e6#pP-FKx`E{9!RbzZ&g+n&F_ zO2UkT zGo0#oJ^aO(zg1$%IioLAf3l?{-e|Cy$uv{ZLwdqu&EWq(Llur6Rn9T%d-In;?Zf&> zhyI7l{K`;rt8EecwdRB6!_SAro#r#SJ$JWmS{-}F_>1}3`3KJA{MS5kCg+dH76ZSQ z!cWy2@~+V>j9V%@5|=!lz?0T(&}Q$jpg-e=$m(o^NN3oeBl$3FnV-c%iPBC^k&AmT9234#D5|2??4#xQ?vSmh zhYy?P;$v?Zo%o%l{k0rTBvafti{g0~om41ZvGNPUNv;W+swWok7_Dc~TGgd z&R2$$oD(()sVI6h+k`jG$ePf?v#D#=Ryl=AMx(VXo~IN-RZlRRdIYiEbT~U(PJxxt zNH{C)KiWu}`=1s3qMQs2G9nBN8sN^aA0PYVxkvfxr-py87k)1Jf1d8oCyyhH*m8IT zk2Orl;bGy>UUFl`qifDvb2=2)T6(AdcDSawT=;>=tc#aQmT2eBn((gV!ps?BIa1~e zg>RL--}^iL$>ndA_pFypw$S+N_`Yi1_up@x+wT4JUOb*{1MB}uA3J*7ohy7CMek`n z&gE*dSnhD>_RlBvg_7?%9?mtoxAXzey|oL({558@7aQGMoS=B_s?hW5$_jb@_s_fK zA3XG2R%g({|M8E={F)iz{_P)l&OfiLFl*`m@F!5Mz9(Fs|Dmu={iJsP)({wkhq;ZGRo2Zr>0f_h$BG2&eW#wR z*YtQS)EpOP&>YDc{^6@Y+>{F&_FG$8T)1m!a^voOb0d=r#*-vo_?PhV@Og85m$~kj z^;S~X`rf=Py2{r3q7VJ?GoHPD>zZ1R(;_9Dy{QbczbD1YwG<^K-GAW7Usx$=SXprY z#I)So?vtGN3Qd!J>DQye{+oSQy_TTu6fO(Z^qH1-H}Tr)C$3yvbs zTLZ6|-br4z<&5H4ql))(o9?cQ`K0;sA(NUG&)oT2wqEIs(|vw#_08lI-s<%&Uqz#S z?an)_$SD-N`n!tj^_NrT7267IQ0FPCIX!pg4wLkkmS-=0m#*|$bCIc;C$g=@^&)Ry z!nlI~qpfFp${wG0^4D|MS1pmAJB2mQ$>!Jpl+HEW{1Y_Y7rW={>De#I z)7$2AZ2GmtS!OSnKdmy|=Dj)Ru23OY#x;w`=;zAKQw(H0IDNMZYb2gr!T(S9)S+-u zt_g?BF3eGxUH^RI?>h+rxof)bt+~TyUmLnTY56Y`x0?@(-}_Cx^lL`gi90W1tlx9I z?9dN6aYQgnh8SZC1W}`wNf7)^Dm@Fkx%2=iZ1c zp{UroN~#}DNN5U2?l;?SH_0q>w&~lBx>>8zm8bf0uCAYAJ+UT#y{@A}8_sQgED(fVgT62}82>jOWA|M5L`KlBgt|0x^RpRe!f zaOt08^rrI!XYHxZ3cKaYdkmfZ-Fw!o*EBC`o|M=czVJ!NwQGLWe}bN0o_xB-!Xk0z z@;TpEAG!VBF(M>#jnK6p`|?xoHdUpSPB|_$aZBaKrMuQGd|nc?`EPaW_T5&UZ||4A z`fDY7GHS;9nglWT)A1J1R;{jRv-rHCYw^BCuj)V5otY{yXR-NbtL3M)E4sHjEcv7B z$#vH%>4{9C*S+0^8hksv3(Yp(-hMx6Nvn7kr`pU}7k;wNIRBob);cA3IkT9(q}Tm| zudd&gOrEW|%OR2H*0L4NN2kiC{mCd&oisJ@K0os_j%7w_x35&ZPM@6PBe-AJF3088 zotM`S`kMVHt#{j>KJCk?+lO|E-S6+>a^5)WpW>Ot^-fze4#eGY-+leUI+we9cFE%)EwG{3y>^yc_(gYDT-?K_jZXI5F9{p?xBU)8y}zijS`g`yV> z*fT5mLJw~&_^QKH%)xhlQJUDNEe)@iUrdZ?XX#y4%$YB6VXK5jJyV~v*p_QIc%05B z{glaSKkW5D^^EkoKGpRObC3R6t)mmBw>auVgoCkwa*xHV3(qEAeKl*M$eti!-!BPP zPD_8kbZYQhmb^K_;Q%*h#l=*ezO8X?7GnE*c3ET=%lRLRUZ3FkMkp{X(7t(Tl4RVm z%RJN0FG^(VJ$IAq`5MnTYfI|Sg;}PTPi$8*j(96$X5F66lH6Uq?V4q@`nhZI4$1Qi z4znHSEi7}st&4S9sqBSDSCc%Y>}wbPA3Yo4^na%G3;tA=+ZxMmcNXy< zt!00A+|MUTT<&PzuKm*mzCEycca&S?e@j7P!#D3jj`mjX#PaaNGRd5Me5zviOuBX3pBL1B-w=^E_vp4$XI`m_{t9sCnqri_b#>G1 z$WpnPT=g$@7|mBxir!Sjcf0*0^Y<^uqSH4t*IDa3ynMXVmu1Vel&j)T*Dbp8BxLQ< z038L-#7Uw4#mZ-WpTt~KRw{{pnP#!x!k}jh%VLj8=aBP?kA9r+DDL<&YtgRcFN^ir zxASekaHjrR>zt4~p^?Xr+sJY3l$&6{czEyqclY=|{XKR1@x0RqA3c3+@$i?HGaIYr zx3zs=?(SyF{m6Ez$5y=C!_UEQLcei@&(1R)(Q~7Z#_$T~26=EQa|fsRNLboG*#r6;eH9_lvz zfB2sXwSRCWwVPX>fq_AG@|$=qM(xQS2^zkjTTd7m7#Ns91Oo$uCIbUQdQoCZPO5Hs zQDQ+sYLRY1Vo_plYDsF5ZeC(;YO!8HQGP*cQAuWMF*DjQ%jAkgZ4Qu$SMS)bo4h*F zqyF_RrEOd5ob9Ld2Wj~)6A-$TVWPNJ@Y0dviILZH6Q=VxFRnjyf04e)#Ut_$`i<_% zP6(K`t4OhthqzA|4&?h_1uOxC%&J{Rm=@cys_iV*RB8Oudfkal(jl^rQqzm zUct7*Igfkhe-4^n?e|i)m1WIT$u~=C)1o&`;&xur%H6(Yjqaia*^|E9ns(4u=S1)K zof0>i60_qDoHkb+kblD z0&VMvS-mTYy52c&EtLohlEVixpUaN?GUe8i=yTF&ctfYz=uZ0fu#?5s<){x_g&a_fupWSw*KmbyKlZ43)Ey9 zni})07vsy#o9B{RmbT)>x%+Kit&6A4s?&9@Z)Ne|_-n0{veI(PtSzf2r=}$TUMW)X`b(SUdpldfF26^NazS|oeOn&gm@_9?=6{CN z{~3?wPgpPbN#y*LFDACh`pvqj+tE7#*PFL)j$mP6c*8aM?sAv%XRI+y0uQ&fc zI!@nS>!&9mlzGZ1((?J6+Hd!sTmLTm^!mPkJ>v(neSro&-&1QAByfDL*|9i*v0PN= zk(|2ylhwbuF8%O0c>Khd1kU=?wlj`$$%pO!(8(lk_tt3}dvnEs&htMus3p&RU9Xqy zd`z6@;FQdn_hg#wg<_K@h`rf!@WOOM-Q=Bnwr5)KZ#k<}=p-3^y(@cGa?!41Q}*Ou zPTetc<6Ngj4>Eo(ns}i*EvfGER~g^$VY0hZEaqkI;Qn@RUfR>0H(77rQeXB^MY5ia z%Psar!O|x)w;eT*=u9iyQmY$yWsQ^4Ys+P!3Ku7eUEJNae5R}Nd4p?d=da3cPTLs0 zeXZ{1fHICxAx}(h2ww;@J+2yaNATdJ2xFgp5%ZS3y%m`589s61mNntA2MU(6_8QEz zUpRG*)#5iZj~wv<4MDqa0jlfpO?k3zS-t@ zr>ntH(^To?uG06{ODl5YC)>a zq6KMZo0e88t+>B$i?p?ikgfW^-?Gd?v$SVhx4lzcyjG{}PrYdP?nz#Ax%J)Gq`cj< zSo*t<+_s4HdRCjmxhpF~m3jUyf4i!J_pz3mf!B<$J(O(j#86r(uH^R*xX;p zkiSwRA-Zj468mngt{cIL2bb;Hw{}s>ddUTc8do23p2`1vimLDQP{E@nOYiKg61@9U z^^T8gX-&y2iCO8}4GW!0?`cl?&T~3j#qjWt?q6J{*G?S1R^eK2ooSdZvHp8xX>{@H z4J%fycd;_M;~nLDw$$vCy<^TwfwmC!ZP8z()MMWIOseACE59gzlDyNz$*%JaW8+%( zo|S3S+bsBXp;~K|*#|lAbx~$~HXpZL3f7Kn%BZ_9RjM~T?4Cu$%_NTPyGv(%|7o;x zzjWr+cN?ab>`t9`C8d9{U;Xmi`!+xHu-<3lSTEIf(B4QwzfwwHXzGHQS;CDk=O3tB z(#pTsgpJj>$MC~0QKlU&ryEp0?o)WydCTTx`lL$P@GLiPmAA`(X6bz3*~k08g!SK> zHkZj$)+%feS+B4|L_KKTg6zLPt_Y||xpTgdsMSyG@_WK@Z1uHO97nRc>Uy5k|61|a z#bfrzXO2OCzTAKDMd9pD3H$9$@3^KYe=*RBw{AN1K~^lYu<@Z~&GsdNnpdJ7@8>AU z{E%ZVRQla-@Zk6_ehJl6@_ZozQWFm`zAR*2e`VXhpO*hx{3fuxI{hSQg88x&!p2Wp z%TMrsZpm++V5=1KhwVD&WuBxjy_d7PUS!w1e7^lmYWt}`gL@+T&OOxqCHzv^yYzkW zxza~>1Og}iJJQf|bN_@150m#N|KPv1OOqk6>7nhz3(2L`64r3rAYv~U~J;8k#M>tSG-i5 zm+AQ&?s?xhUbX+Ttz8o5;`-ui@KM(rwo*bOdQWEvhScia$QA3KA$1{e$<$f;%u>87 zAF0jnZY?<^+$0>o>H@c@tos@6dZl-!=wEB62?UI--y+-NHDhn4b z{a568A@0%J*+w%oex^K&K76zxtyAsl^e}-bzl?$zcRxFp|5`tJIa`36U>~Pw(>eP% z*A2f-|LX~B_TPLYr2FKn{*0;2hQcK!rPnT3`n~ttZOmG^@w>Lwv^BS`Otp@HLZ_qEENIVEOL&S$(_%WTb2-`laj zSx^IZ^ZiG^CUEj^MmTh_YvA z_kC}hIsg9pe1`89BXQ!zpw0w|BZUucdRM$_oheqo??TR`B74uJ z|Bro{&LD#Dfu9g-1yKARj-8$ncvU7AbsTTs*fAWyaK1a zZm~akK3F>XP&fh1r>%T6C`Rz{rs3*qj&ZGtBGA(Spnz@a`cT3K; z^v%OEVP7`WU}k~f z`|>~Rdwwk2^FcwcF*Ne@)5-k%ev1E*)SJHJe2Ue~J4;Tt&-pyJc;DvzxBvM)`(9u7 zkM+RmA8I|h7V3{{BpBypyUI_CDrC&(ez{2EUgrY=J=YKQTzcJ(Q#Z;uN=Zu2>M1{* zp?~)jSEj2+6-W4?IqSPx1rM#VIN5nn#5wNHJ~cLPH^(+zN3+k{@}6-CSuI<);`-H) zi`ROu%t`C_%9wk3v*lI2s97y{r!G%kx^&^QO)D*LUn;(N@$jC7E5f$zSoQSH&*;jy zTTd=anO|{g)SA8e`!S*VM`^=y{rAzHeJU#J*oB}>XaC;Wt5;{~883alCGvR3<9v(7Pjz+I-imT_Iz40F<+q20 zk_zYl^o-toCDmE_ZTtiQ8FS-V9-@h9U8_&eGSxrVayTu@R4l|>JiC1J(USVZoVT>C zjQ700=@Unb=qmwmJQ} z=4Q=%@y@gMUajVx)466vYqDOn3eWNE6_@r;%#mHSQ~kx3g{!L{Ne2bj)EPY~J7)iI z`KxDNE3{AEO3G|qbnrw~wqL(^sq*5;MGEy$G&BfOXr!J%;rs+#(-Obq3Riz}W{PBO|(+gGSbfPlfF42oxwc9N_V1d?+erc83n};&D zZgA4(s^6B}-ZPskFL8TQY|@M4e5~&uZfp8(xcC8cjmE;wyLgQ3f<749C0;zWAh@b= zOQ)$qQ`6oG$Ac<%p%Yx@yxbE|cUVu$qV+P@x2b%m-mCK49d2nl|FLz)@1;%aKWuHX zKJ?{W`qCALKHhw)9X0EpYxez~ufFPB>6guP+YPv1Gg|$s=U(O)#`669d|O4uCDmPb zMSUXJ6IP}gRk`il9TItNu$xsopsn#+AsqU&(0l56**)rW%Yo_o{zAXI{iQ z@zD-$#lG7+yB0FrzBm^4YcKQSFE3S=dS>nM=$-o|QABW$%m3Le`hUcvnCfLRt@$q8 zPdvr-{L(p&rOi`z3sjt%Fe~NDNeh+F-5m9TYeYV*4v{)KLprTXyKryhk6+D;Clqbc zGW%{7H|4|Z*xRZTuJ0(9aF3Z_ZozJGx^sy~Z$oBL!l83z_WJ5FE8b0-CEoUC+A5j0 z8#f<|K3|diOu{`et;O0&eM*PhwYMxa^<``?%T7HnI?D_W4;we$5KUaz1t{Tnh36_;du*?IiNn~vCvd0i*nJEjJ2Zf!fT{+ht* z%Lf;IF<8D^@{K~X-tw#yrt(Ve{u88^{%TYB+bcIyeih@#zMXk@pLrQ<+x}iJ$>iE+ z-9snq&&}<>uzQA#K%VV~p7PB~2{w*RZXD989{idrJnCKwalKWshn;4rtY+#fIkxbL zWBaAX4CC|h$EUnLA!sjal^tZ```DQGg@RS6K+!BI)eXXV2d!ov7yEXvd_wbgft+3M zS%V+Fn!RsP>8=iT*?Vla`ngU-@)sRyeIqd8;yc%UDP zyi%0#N1^vI9~PdJTlwv3`-@eLJ6t+WmnxpCD*Rb(S+#%FbmP*h$fW1jcHarDtz3F8 zb>gS0^&K&PyCityCt5x)oKfy*GTqFO@sN8d_k3SPwQC!uCe7UY_FVD|_2Tstou>7B z8J^!h6*ZQRl3=B^hK(izlOH?K&rwG+=t{G{#jb_ zy|6CM&1Xz{uN>WXsb2Y4}JvX&*sviHHIA1{}wKKiCu9>L|Bc7}hvVVBA z%iw0_S+n3vmSrtVUJHA@3X;(`eXgr$_BVX;5``}x=I)EH5Knke->~rdiTF2?uNqW- zp$!_$yl`x-A2S2PQ%(j3>&YPJ_n&2*%yUt+{%F{1KjA=ue}2&xsnH8U9QP=PwQRe< zt)$Q)HlO=+4YVSMd+YOU((m#^mJh=mq+Y-u-* z-RvfzW_9=K|En8rYNu8?$9D7VGfZ|(%@FCidG>4S)fzp0@ebj0Qe80-Q+$2rc>ByR znEo*7+0>=89?y#0B4#$-djiiv&Dij>8=GQuRxP>u{#EjmdJUN_;{`Gqaua8r@{oAN zTYGSlulDxD%(ioe$1Xg%(5ERB)$(7$!&I)p<+?%j#$6EZEdCGuxZhnvnQm#Eq_pPXhWG^=mWXzQ|m;|-(C_C zl;mmbvTEmX1zm~R%NF(3XD_OW(kiKPS=Yqz*hxUqq|k;pNk&*^vZK#KN$1(oSrTlq zvXu+hzdSar&c$|%oI2Mdt#zEo(^gD~<@J$gzF#zF!R}K`#ataxtN&=+nI-DoxA47> zZJ4i5-_bMEa#pn`Tlr0}tGyiXc6a#2PS*z-fpbrGEfVXV*r*bknJ?OVvHt0nm&=xl z3S9|ZbePX@#oW)Uj$R0~Gb}p2dR5ZfySnLxXD;5c61lZ|aoSnSEy+JGtlU<)eD$pA ztBd|x9e!ZB^N@d#e(!<*Q+NHy47ljF|BqXh%!U9D9+O3TK6LzK`;~hsbJDrETg?}5 zs8=l7_=9hc{EIh^#%p<i^ZONZ-~Ma-p!XxFN9CW!ZO6Oc^p>bD+}?d4NXKtY!uNR_>TZ`s z><|)b$YJx%5j(5X&M|xY-A=2vZ+A~V5W97f*YU2}DQ8D@!Gp4gUMj4Ue`BsBvsJP2 zW@M3tdwx*k;kP^+nt85pX_&>Ob-kbc=!H^2T5zB>dsCNU-t!%|kG(jUddvM}4zumC zsLtZ%Z?o+<%UNAc_?rj4Xs>8pYT)vT;Vr-7y?o7{Up6zA*}k6qj|sJ@xm40+@ggP$ z1~qmD24iU66RKZ3HP>I*QQ-LhYTdm>x?5H#*oCt+Y8NzgDIIh9pr|71JHf3YanqJ` z%f0VrzFqi3@Q>cU2`;J^kMKWeH#!%0zUa~<=Hyv#&Yd~;FD` z1u|#$lukBTw>U$g(~~)PLy6Dp8Hp!C_EgR7&N?Y*b~xbVWYg7Z^_5q*uI+jq;lDHI z#?_^@^I6-j$huqni*1{@vEutjBj=)%tA%Wq^Jw4x>KkqMc(z`o-;_OE<)>q;y!d-P z>rZDIq`cqzCHLo(-uD$wD^EM!C_4GHx6OA|sX>uS+JqLFzIV&ld@lW0cRB9s&M&jw z+Wu|4r2TJNw4S(W_SNZqD|CD6^9@vXtXcWwmY8IfjiBR;8L2O_Pn}({BwCoAl zq@&^&kA*#d-}*D%X5*ty`>QGcP9ExYOvYhE)& zm*?23?Yi3EoZY`3b(npA$|{3yo=sUh=DC{l$eK$$OMaRCXzPc2oqXZu6*nez87{AX zoF=y2;JiWQ8n%=f7UymnpOM>d`r>o1eQ4XoZPK=H_Hh-~@yJ`5{;JQIks<#tdslAZ z%g;}*zG{|@|729Y{jGCcgAv!4m}M_{m%2NA74eL{5Hf$_()KOxOEOK;eYUZm)v0q# znjXTt*W{a$qxJm7-D?jgNS~OPyWnM?jUQ9vlKKrjb;~cvJ3Y?`i;H@c+&xuE`bIa0 zbGD($ra62P%@*QDa+mqK`tFpgTk~6id7KYsryL6^=N!FGVZjBp=1fP){|ai=(d~4ziVP^l)I1Yn9;Ir37;ur&A~g95@xHg&WbK9h*IC8 zw)dn{p=ge!@@e0=h5Z+{d!*k|)V#dpzt!dKFYfPiwzk!_Oh{_>Q8WI#_^$2mmvi#% z=h|A;|NH%#o1wTuA(^9F{l&zIf(Mn{{n}boB_m__9&xQJs!;=t_m-R&da5=_s&Ek{Mp@`=i1$Po(Y~ODzwNbKdW6j-FOSGr$3cY$c>Z-^3YuTn( z@0^leR_DG|YS&cdFOw3bzKOO;Prmozx?o@BYToIyZzW6!dmU|cSTivq|JtctrQy2a zCxy;kH{H5xVN_eh_MS-#P76d{l)s`||94lS>6@g@cRrn5aL35^Zq(XUy={IQT>U3) zT-WzviDRz(E!R2cHXHC=4q|Wr%Tp1ewX3j<{6rC)Iw`^ zOpic)Pek_Z?N{B;9e=7}l(T>8o=FW1^wTraAZ%zf&y%4^4p*$o#vKOQ?` z;+$b0$^Eo_S6Xq+Y6sUymn#x_m2#PHCouDt-xsJ`84**n`OyV#Me`@0lCNIk7qZGp ze)nVhT)mm)yX0nC=yfEnUEX!&_0mb9zL%wYZ#?3OUH4P&-RCLNt23pqnusz7O7Kan zSQT0_`(cf>uX%9X-ul~lx6&sF@*Jy9+;Gydij9B8Z)fw+drmJi^>6IWlB$}^Iknj9 z>@2TMDZ4)GsoxS*z0~^B(^$dx@69z1%l+qkFMsI2YThS{IKidww48h;xm#T-m(P^9 z3w(Ruv5HbtD!6NhfQGnw{MRo_i)rO^j2#U-?SL>w+z}Ux~4c zX?NhX!xGU>lVaWaVwXNEJi2iI1EKoci`OSJO?HVsvn=D8v%u^QWluYI&v_!tR+jo& zvw!{DEYp5OP=c$`WlpZ_c87>gw|6g&fBX7cyzk~5VT*T8-SWx`3pOoviJhG*G$tflsc|@(KmhKf@PI%JGfXCcD)Skh_mdz-O_#G$I2&% zmBLNxbWftsBy7=I9p%f!!0?`hfx!)2+Z{inTE8?Ha>aA`?~MW5gHE@1DRqQqIEqym zE^Arg>oe<0|B{yP3uRwDD4TvJue1K>{{!9nGkzr0e3O^_`1ssAwr1DtN6ydB-Ld?B zr`j*wzW)CQ!2@b$+#3Ql6VB*Z-Z4FGEA6eeH)nBL^3h{jiu;$=h~^3#oxU3Dv`w%? zHF43>`kJ3L7w%84a(`1fM{|2q--oi+uxE4BHeb{h6?!JEG{x6`O5F;jQkB~i!|K)x ze&KHw3tYhIclrC$yb2{ARdJh4)?LrPZFy03>21ZS)SUPqv6sIty|D1gRKY2wO?Nx} zZH`?kdhH;?d0n{fyzhjs4}G?tG(DKOgsJ}U3rVIqvH6?oH?6yMUtQq3$nA+Q_SC8I z1z&u=Ywr!~33FH2%~@%dd#C@P^3uF3y`QhmE_Heuf1%~$wpTxuq$EB@*Uw55&^-V5 z>5Xd}9^@|5+S1(k?!@v_96v0x`1=({gpC4NvGI;|9VmX|7P)< zPK85PT)Q~B7vEL#S@u4qUOnU69|!Nt^9^bb&NkFf`NQKIA>n#H@5Z0jLecu-$cSU= zc9u0omO{s4-rBAy-?K5`iD~|%FRarm->WeAEI2cv78Rx#+ zQMs>w2WBxeicQJNWX%=|S`ga5b!~h7;^$w!_`i6#;{N+HZ}KL^-Z&!v_vyLsb??8I z?mxf#_toq5f2AAzeA+LtZ0(u$SFuHKvH-s#Lw&Dd|HnxWQai-wPG|P14{$Q-NaS?m z*nWt|@fe45KYQZT!)=FMRtiWNoVsnu+3pn4s@`0o)gopid&szAn*TiRMDw~+E%w}x zp7&Jlli`0LzGsiZLsz?hs`CF=$?QM&Stu^)+H}=DN)JOBZ^{&wTz*lmbJxS8Vy=O8 zdSb@yY>E0=S0{($MsH@lrMzrvhJQqs&C0FECskGVTiUFV`q+_|u{-4SwKcnL{we34 zQYFQbx07%6apx;fH|iMicdu5m>z)|lmE&G&!kI6yJ>AJ(ndKJOZL98izOtc{HTR{u zK54L8wQaeK?M=Chx$Wmp{rcLn&`PtL<@#CcdvD%OUNte}d;R;nM<2~myu0r8j(=xu zxo1>u&U@iE{lN6z)|29rt%v66zj<-ga&xAG!iD5xC!``S*Eg_gHNtg9zKI<}1c?rFj2 z9zn_aQ+H_kOii$Ip0)L|zHQ|Z-t?;3lM^g2e%tJHXtvP3NguWy_`i=oGIB=M-q|}{ zAIt7p^L**#>AZV#o;)?v`_J03aL1R~n%c{9uUh`QYH5-?$JA)o2WH9Hh5XL@1?wXw zzSX%qZK>R&Z-?C5Q}1;88qK<>x0v~Ls+5g%O3o2kqhA7|8T+>RzHGT^BzO4_&y5H2 z+tpsie6-qfDInimqcC{0=;0C(ciR=3s}?R1-rIXC$?RhJJNpGkPi(sXNY*Uh>6oC5 z=#4wM8n-@l6l)w_*0XY(QPRxCRWaS#{WGTt)$eY5WyX0tWl#5GqcSC>$1IDC{FJmF zmNRV^FR7fLcY8``+_MW)|85JMDt_MV|GXt`XWu8S=n1yaeP}EB{&7!_{eu-f{2vzf z*cYkn`W58qzP##8$F`}4XD^BA{L%TR^7wqs@BV_kzUzArO)Q)z`_J&7(nDd!r2&#B zXB?SwK=%H|`g*Psy$$Q08|a>I&W;Uf7RZS7Ddk=h9TB~F+T699;@mff9N&`RCB81J zvF%Xqh4+eA=49$Fjo@E>WwrU!nO4=xpSio(0_Pj&%=qKAYj5l8@4GUe-VIK8&orlgqIIA6s{=)*VwZDv z=lwm#_v)A}_qC~#FDq><{|8x0I`P{UMOpdFoqYV`P_TCThWP9~mlhwtHa}smNEhS( zw~;q*O!@t5&DkSbcVp6y>a0jF+EK+Q##cjwEFsewMN9(yR62+qUdyTX13hT!X)}v|LsDEt<3{ zPwkACDtR|OZSL=-mK=?{IrI+Q6&1Cyi9M$@Z)wRV=UcLFhKUW+3O=iJ9ZHfq`mmdW z$8T|p(9(y~JUt#>^JF<}+UPl@HJd})>b=*SWu?3-4l<4&dz!ThMYcYYxYku)9;n~9 zIC9Q`y)K&tRdwWTZ!ql;xE=e%DW`HH_r|x3;z_T)PwZN|dbaKdVluv4K}qgGpl_HGC%W#i6ruKMpgEyi0$6KB9*@* zc*2p)P5)}w2&Z3AwR2ei~r-@t@*}m8jJ<-ZArPm|#Kof#o|)Z{4wK-DQ0L z&voHod*_KKFWsH6EW2Zqqsy&|XS4qcNSw0TIysN8kW>3e>Z#-YnycnN4E$($?1=c6 zkpJvzN2Xh-{cH7pbpMA_O>2FZ_PYIto=o1Z$z2{er@OGe)24j?{FUJ~@?{T0|496; z6I-+F%&sn3p|W__nJw)9`fshC5c^;mm)gC@?WY%Q@Q|5MQ0$%UyI@(0)T2LpChUDW zYs1;ar&Z?m%n$g5u$bi#YC;DxC6oM(Tw*y61~rWVTDR#sB!^truG#ysqkwv1HG3rx}~Fim#i- zw(XpyEPb@?N$G8!yjx)#UzyB~xspB2fO-E(*j@c z?K)|nH)DyX{toxROPa09^`CS*C~vOfsZ{5u~giXKARx-!Ppx=;LR7YY*hd0~1gnFAk*S4Sk zrgbF#Qu&)Y!?u!HQmtxxbK)zB!<^1v!Kda`M3?LrUAs)vrp-q` zJ>|vQsY|{~*?MM4sbqGn;JkR!Ak*KY`$E0e`ELRz#p{%IIcK$;aeNl|>CPdBYW}&N za~5vCct9rIvq@i5XYqZ@p4JOT1y((+=ic2>oBi2j7yq`q2QS`|;eB3s@cD&lEpyUE z<{KSI_{Y9zw}ghzd#B%Z9N$kKPh~o6mS=E8(^Fm3=~HS?`-AGlCQsI7GLPFA&bKb| z*|zXxS)XJKZ=h&Af8dnIum8WjJk|Pl6U*;ck9!Un99ZfYyhF@MZc?gA#tpd}=N6{S zk`;9=Gt0S@zh~0A$8GT!r@WuJbXFthkqiq}3B#|f+KDW$euo|sy`(OpF!u*L>cB(@ zi?`h>CI*HhY>)<^)?}&kV)bi7Z|7Zh5U`!Sr0~%Wl__R1r&O0{`rh@v<)FE@KvUFJ zbfE&Pt5&}BahA(R)?MRk^?%4+@sLr4tHY~?LB4f{@7+mSj(Vc{_vX&r{JbYU?d|;i z`~EWo9NB9jbYkTN1FiDwAyqvqPb9~*_HnXK))Mj2)>zSz9Of8!u(Yk^p~>v}JL{(E z+)g<$O(szP3hR2)GffvaaQW{nID6}r{23wVE7h4hcg~c1!58y;!Gss7r?>5~=HdT* z@I%7Sk6g|vS62pZ@_czC!*unsiesyJKk-bFX*So2iMaG_uHZ~P=U*qcrCWYG#V5Wm z>gHp)qgzXFn;p$+-o4IqN9zHd;=MCp#0LA;|J#-|Np(*x&*6Z5U%K``Yh~O1x^$F^mFoTCu?5H6FTYvdyYcIc{NJKMr~SMop3PG2 zExGTX*73@f?kb$eGdMlIL0%=q@q|3+}z$@-ORn|AXgA1t0XJNJCynIisOb2z1H zUP-OLD!1n&%lZ#)r~jNz3Ej1F*OiqI?-=-8KYRA<1zkDYV|%9v@BSM9?tNkQ=6g%a ziq_>A=uKL9eyN-MB>}dlHOwog%&GX)BDhddis7*2Ca)Hb%d;-ktCt-t`6jAZTO|I_ z|6k;V$ga&gA!bb~rxQDBGP^EHMxAGVJNF8Q;B!`<-)Fj0yhB3f|7beV?z~Uq=)q62 zHW!`eY3Mx?Ix4BB{P^>m1?zjYUdZ-F4LY?jpDK)l@?KJJO$OvUy3i=WW6IHv1O+I!5KrhnxQ&TM^&;d>gNynWkRs`O~%M zir?8j|5Eq&>rd_m*D0zg2FeTP@Bd|>o#fZV5PFEoOG(UC$J3+Ox>sE(%%UdrNU&hG z(lyB{GoSj~U(6*Zv8MJrYFM+g9bUUCQ`&m%vwaWuUHq>5i6?CCuDrJ|*PVQOLi5La zpMJ&N)lX*k#g@%--?jR8-)V*LoZAJbr@gCWQ=GP{?{xZ4T zoj;%Po%P&&;g2!L#I|LASXck8DN)-jHS&8;*IUVL>A#+Byg0L3iA^z?M_)qi@v38m zk9n5fV62+wRpqFj-n`w7$mn;_|C61GP#vroPy<*t<*5fsxa$D1~>@(n%>Q^})vJ z9yKbejfz#@3)FwNC0=?e&>(n6@nl8!q6hpg)0B1^Rd+qHY}x*XXQls(oeevJ-xRFn ze(q7cyFAawqP3)G=304ArGI^j-@sbP+Sz9;xM3rk zGs#48x9{$1iMM%E!#c!g&b-O}Mf7Do*UzXYvy{^gr9}RFqBO~LQs)Z0HPy9l&Ig|Q z1L6jWmL;SiJmldfCa~BQ`fPC!Ln~&x=}GKS^_*v7C{C zVKMj^or@(}lUJV>te-mRzL&J4$g%yKBRB6_CiUOs>b7eu7#E9N_saKL62QE)bwR6# z-k#YihpjfvoO-kK2Uo4kdKUd(47DpR8P9I$Xf@d_et+iX`#)vR&H4NH`9tvrcWXGy5bE?XA~V%qnCt6azJvM8YCR)zG(+XGwWlzSH3l<7|rww}kv&t<^46UgYw^ zA}e}v$(|RUk6?IQz51n747XeE{!01K-WJ_lUhT2sZ~yGwTP`nr zd2;=!+$EpR6hAnooyc~S!MbJf%kK^v_Hh>@zk7YFIQ0E;j9_B*Cyka@DwE@%{=eQ{ z-Sy#)j{-|gS43xN+JCuwvg~qc#lq?@)t_1D_406hWcpB<9&xHvQhVpRN2%*Xr|Vlk zza`=Nuk)((;g|P6oH}99^!%b#-1F4@qW{r({O3e|)V5dJA5qGl`E}~%>FMZG@eLYq31W| zoZqVDxcA)ORURK_wNDg&=65WZ@#QRG`{KoI$L;Iaf3SMGbB2D2)|PtRXW!bx_b9&1 z*l~VBSM#%tZR|Yy=R?ZN>r!t@KfSz0AXV|1L{WNY@{7mY-ns;?v$6MnIQ5U^?u#1> zYISdL=04x4-DYs7%|O|SC&uBJgIKeZjfp`6kD>ll9-V}ViBIb9EVo`4Uh2f$zaiy; z)iDVP%lLyc>f4U6;jV&HdP-`78UVOfxV=5$frWu}_B%?rA{Zn0%@ zMzNo9Ryp&sc12pDe2#w`-$?_}gfCSyn&PH8%vvd-_PfHEM|np*&xyGnhoAVV1ZDVN z&RE*HgimMI{RL^0*0RNkq!j#$3Q9w|XPT34E-g}lsjf1#WvEEMG!cZ$jF zX^n`~SJ{{*KjVe*YG1`s3#oL+!c9|{7#J#8VNJ?_lVX!|GdSzZZnvg#i!RiPa#(XA zC`D7DWr~1@@FcZRpDd3rW>b74W6DmxoAzV^eJsAJZ-C)w?fTxRqY%uZvEe0NS{RmrjLRZ~@d>m7LdO{r5rrlQWn;#>VE zcZZuHwj#aZ{znRJ-cCJcUELn0tW%!O70TN!xa0YTrJJVA*&mm1;xG5Ch5Pf&V+FS@ z;d@=5beSvn_#e%CRo~x8hj^{8*4>|&d%I|hPx$W2$`cKZQyNuuvMbJPxH&o5l_l&d zua5Z!=if(O?GqHX71;E6Nn>D&#HIb8(<1(!ujejV8sV4~%~Lta9Q z&g4%=+U=be+H455X6oB4|P8>UXB zg_Co&7P<7+AAh--aozz1f2Gr=KA-KL?CcYgKOp4q+A_`BH`(dZ?$+CaCw1>A_6zS+ z?Rlto^^8(`^^=uP8vWFJeZPsay9sMAJ<+VpC%EGnO&$cryP$t^*lB;jHeZ^8#&|E_L>R!nnH6dT06KPF158u~xP?UqN!=v8bm! zeH(;Mh|F$PTR7)Oc**=TnMTXsJ}OMx5$UWrp}ldB!0)R1dp6$hH797Ekha{k!TUz0 z&a=vE7u|Df>bzF`=bx4Ey}8)5?@c%Jt2{oW%JX zwx7wdxwZeT4DY!+i7plqb#Gg4J$ks##JjI4eMUz{#j5@HUMaC{KD9rL@7Jb;7orUX z&C2b%8_&M`*YL4dmbu={zSzoBRpf@g8pF2L4JW+<`Z~38geUQZOU~LAda|bd=U1t- zQ9hd|Itb0WvAfk&KzQ9_Kc5=$*>lwI><+o!mAr4g_H8fC#VwnbXukL7p1;|2S@Kzt z=|NSSrdMsMXlO5tG(Bd2$amZ96CY;hu9;yLp7Yr3wl1&!Q)p!^#K6G8!2sGdR&X|g z_bw9yL-yqN<=*vy0=DIoJP#Q-a0q;C?a=V#XqnE>vT?$tOIloOogz2vRtYXFF>>}* zJEi|aUvEj5`&4fG15>5rxy|$LywHy*^IzuF5}>Ger{wzJ7l#dji*X z(<2kEUTE8B7S+4?l@GW9go@^ek;G6R?k~Lm6_=m^KG*` zs@~I1yG16{+$`+gxk#IP+Qie*X?Z6#r!I+&Fw0%sCB3fsJZH<2Xb}$``^ow#=O&dI zZhy6NS?rR<-~KR$CA=`$d_H)^V|9;|ds}?-9S?ZOrsnuvi2D4}wDH5F;04ciS)?uv zx@BE90fo-cW`|A~zw_wmM;zvj9g z-mQPSJ8xrd(N}?4f8E)-YdB?4b`%1T>Ng% zo_5ebC+xx1`WbPb-+0y^I(3a@wx?Ne&N9|Ef2L*GYpq1h^Y0i}emkjb`MAbi*7ikx zYH_f;_#>-b6E7@W7<^oSZPAR&hW*ij7iQ1<`+T{g!Bvsl3|H^Sv3oj?52PnyPxhX@{JO`_1&~;HSdbPDMu7)S#Q7amUL=jcUsNTf8x7|>L)=B z8@m9}TT?T{)$UIh{2gGlcsEDtrrjLr$I2GwU9N9zQn{%T;!x9~W#=e7|Dx)?1>rSE zHLXsEa9-#)ZrJ~*=}>&3=h65=W5up{M|UnLnP!u&`SAKyE5W{we7p5=w!9Ur=aLm1 zo-r+%b<_FDS;Y_Etu{n4$19|+dJy^PRN(`8JBvu)6?Z+3y8d@-nqTg)*}*bKY2Ho6 ztz`nYoTtpGuR5^8u=_`6*~iuEmR3xis&=XQVMF#@5tUlO(>-a&zbS6xFa3J(`1z3H ztk|OkmKQABv>VSE_WZE=tkr7rQOiU=P1=?Hr()L@;mLB(_MeuVvrJFlFmlfPvpnnC zx7wqYiz!jFuZRgTFyv@5FsLywFyt4d>rdux=AK;8FIk_N5OZ|mKd)Ic&8w=mrQY0R zn~?L6`SmozZ5y5j+c8Lbn@!s|k^f9Y)g(8$^Bpqe$zv2g3VBHcl^GCAr?BUTuZi0+u;T zp3GY_tJBNB`~8#}8~w>Q9;{HCTzbW0quTOUGqi(EX0AGEBKtXYS6`H+Z{WELOI(g8 zRXKWX}?a$e(}sr^%3sItfz%!o_f5hU^%rWO>+Iop#3_*WxCfl zhK8-U7+Gf-UHM!l_EqXqz2~R2_L~F<&uce%wJGY_kEb?UcZ5w=YYdd{oUt+WVS-KG zG1kXn<*H5>J%W|Qy0++dzk9)Ttf@1@XL4!N$l?n-VHzs{0eXnbu7hnkP?wc_K&{u{PA8r(|##pD?1tReUO+PAFOHFu*H z>6Yel8mFBvPQLHk z^rxru>>Y!<(M^V{udi7he|N{$jq4JRUNbFyeQj;T?RpK4HLrN$gx0+CYdLh)aP!?e zp?lS4EC`$Uvnenz`sPwwPszoXR!BF+E;*Uawbi|;^PPll%*36$4*ZkkbGx-Vm+^DB zYx3KxZ#O1n-&m8!*}h8lc37*@vbAsixK6ROlHhYMH)|=IxwdaN%c)<6*J_Sc7N(vt z5tTK3ef?8_%?{n1`b(YrSQmO|Z1k8A9dxSri%$2pSzh(;uH4w0JXP5N- zKm1D2SyE>`d7g`V$E4*ot-H2uUb$wwcP~r&v>d6N{LF1E-D^#K`Ba~5=lQe5QZK2) zQ@d>IjFT5`D$R(VC$iKc(pi_@A@Q)l%#6EFH*{5dnLAba%rgZ?CF^|cARN=@zRT} zb@uUCrx?}`99-Eq&xve^Fx?rjA?wDQ!&XO5GHW_@6fkMTiA#FS$W(1++1a{2k2C7* zpKR$op52|2cPGs~89&#~Z`<-kP4!&jd_scfS>~u2JGL1crHk>f2bsLNn6kQyCu)j? zyOV+LKX3SL=e0ZoM&Efftj5Sw#REv+z3q4hO^o-eSj!DL>nWxrr9i6&w z=k2p{i;k!sEHF3!I%}ef&)>Mj@2)c^Se~5~7IX1kOP6an@63Wd3S|)%b2T5Wc>-!4 zrD^y+-IMsMrS@Il1HFS+a_o1xecGba?B`&1|16LBpYo^kjKA!Ccxd@a*I6v4H{JHU z>uHlH;P$%2qTAheIIUZhAN^uoSJ&oXbk@au#CPv9D3yim38a#6)dvw&9ike$< zW*W0z_HJ`)42rvGxjT)o;9TgObe{I&fXIm=hW?!&^c7~;&VKEC@m&OWwsPx<$84Uf zf{J+VbVc*rX^`fHdwyt(u9^0Zmsq^ zhI>DRNXI(eSi9y<$-_gsPut#1*`c_5$?*x^PmP@QrfYM^s&+qK%6Ih8DkJB8is4#| z@}-KV#`sojxps9^)Y04>t)V@};X9YE>M5Mh7IX4dj;oGXuVj5uZ&1~r2U|E=qeG8z zyxkSH&iF}m(OyM!4fi`5;SxoYYi>`N{WQxt@8H|EJ6hW%Kgp_U{l3F?bd%!t)8&y7 zI}a@B>J>LSR~mc9q;g%;<@QK1UK{l(+3y3+&TYG+TCUz(vcY)M-JDXU{zyxohd28r zocF1JPni)U-pyJqVq~mRuhqr7r@7uBWqXW%gJSiQIOl!(-!qElH<~|XZ@V-9=sc!u zXSslf&t}Tampj>ECC$kDb74%*iRMoN-)H8@UhJ1}{rD*9Xu^rZT1SuVOrEP&E&A6k zj3rZ5VB;EFi8Riedz;IXR_#g6Or9NQlU!BfR;Upt!*l#hLbiXQ_WN5MH|wL8ZuzmX z=(BR$AD5+0Z#N~V2>FVzM@M%r&fMep$!(Icu9>fmV2^57d7F^zQDd$25Xm26>#jdp z-e#lKpZUpnDqrBiWrOi_8BF-J$(I1q4PIwSG8$5aWy~dpRGIn>d=(xqsy+WD0)3@Q;_q#3FIvv+Q)l}J5xfq(Nt`Ps{(cZ>2} zYfAOgc>1|e@|o&+C)?>(O&{Hg!w>yhcd+WT@W#3xUCmPMDDETN5h7wAl^nIbxM z$CPI2tJVJ=y-To|=Q_cLQ|JnR(S`WDeaCKZU+O$JIj(+Bb@ZLitBWR8T^A~A;d*W& zY_@C0hv_!avHP=oO3Z@&w(ZM5BXi8!>a}o_Sl5orvzJ9bNM5=y zVdd?MHFx)Zo_kxPZe3N`ucojzA@i8j`3pD}OVlgPs`@i+$11x`cYQX@lMIO8p1p9x z%7RnkX3B3n`LumrHq`&gSYjyUDVNabI@2V%^^*SfSjW?uc^jXL-PpNjrlif=jTzU1X2bmKO^EOn*L4$76UBeU)D!o>ONSvMKjmPk#g%IrVq5&wI4>!<5Q z#`lD{Rqg9aSE~Pu5ue=tlI4Ec=HpIJCNg*PIzHuD-(|k*Zhmdxd%mEo%hkvhBeyob)dCEhNSmiwL{A#GYySvQZ}34@cA68Vy^PE*K6{! ze>WXZWSBl)IOFE5D%+JT@1HeYJM^M{%e?*Z6EEw{|6HhLwNb1=Tz%!L>5FB)y0tw} z%l?!5@8iy#-u%$ogDhV+J$^5^_tgM`hJl&CSgF?9QB6xciu9q+PnZxAmT^v&9}>SFZd@F~kor?n~wg zMP9AQo%hE2-y=JnWqOm=t=qhh|8n~!Z^4(>CZ5f?Q=XHSs_(b{M!suD*K+m1gWqg? z5`=tQgE-@-{YF=O{bQv)-lx8A z;GMsi_4q|C<%CleTTW_bOiwED(6oB$lQUhdB~`f0Lw(VuR=u4};-`H(PR1R18u+h{ z&HAb3mlLOVgxot~{=_*(v;KhTC&_h_?H^ZJwn-Nk&6uY3P5x-ctEY$g3+!+2Hjw0h z&@N-!6L@Wo+gYCKsk?5JNR`iSV*gk#P|}%fSm)AbleP71)YszsHt8W<;hHZMi)vnc z_?3Bc`Hj<-@~@smzIl9N^(M`kwK8vy_0KF6exhvCG~d8{`KjMOG)>PQO+Hz&eMQ}| zi;w3-HZ&z`T%WM~VHWT2{)E0Z!=szid+eKp`uY~lTiX`vzT#q{j@6lrKSIl&$+Yz! z-cql#z3~-i-~1!Nd@^Sr_Be5^J@c8T==8Vrg%{R_{M%+Ca!JOxMSGo6%7hMUjYods zg5ic{^ESG6Oy8D0=k}Tp&lu87rokBX&u$>*{%eJ%7-~^&u?G~DN%^Q%Ct_3hy&jZYUH zJbnB-+n?5S`6HHhTD7idZ`4=5KWQ&})I%@73Z_cNz5LIgY5khn!%=#;eoc^>_S+gQ zDV?`!D<>|V8}=mhKv=B+uW8(|_M}XiGnb|&MXud*q1{w$_tu{$MO&X&&O3hVStx9y^WrDr?oa&ATW@~JnJHLES(s-7x5 z?%r|fyb*hO(C+z<#e44Y8}RBEnf6!DZJMsL%XG=j(=s0l)x?&P^8oXkIZlQeeXJkJ_J}Cr!K4Z60A&x#HvJipi1Bw{CKKI(zNO^})0L z=o+5A_wc<%`so+`Cx28|1*cx;m8g{2>Q}aRY2v!63HuYH8Y}iN{d&O4YkKWRZubig zF(GIE$0Q z*QMIek~ET*w&^|<-?+Zu>8ee3IbQ2$8{AzvJ8SRqH;!?vhciF7ag~ba2QI$j`nzxc z;U7B-7!I!zvA9rYfA7(}{NRh_9Qym8>i+dVvbnr7eg9#hd)vG}>Tmh~S7cu32B|;p zmnx^mEvdckv~giTw_Gf`gqzUx>|oA7zs7weOY>gVtM^n<0MFKJ6#nkCcOBBr_9 zzIne&yl?d1tLA=peenm|Bb$TgS%lmRD9i}V>s@}L_*Ct5vz4qp-dZJeT)~}0d`<*XO^etZ7UVgN<{_l#+u&bXK zTcosfZg}svQVU(?a3uNV)0G}c=c~g^r({>|Z*#T%pm^+&@;x>A%>n;yZNGD7FYv$E zX8)*YU2Lzt;c2_{zIw+eKPOdITaPogi)*$YoU_IH z=INa0=TiRp|C!ER?Dy^Pp}9Zz$Cgh^d~@c+@A+4RGds63v2a;eZ=5UMm@aR#(?2mQ z@rOd04Qug-=#>rbpLVF~9gk3&^XG!nPmVzDY#*PmW*`CI!&ee-MOoAD?A&3?1~(~}QdtP6Qxu1~F( zeHkXK8Taw&U$h}6sqUwCC)pVo7=$J>HtW@AmP@CL*16q2@##qSE7h{eA->7c)1FF9 zV)z)ey>Ipei*3P=jvdp~^;y%oCghsN;)%lBr|1P_++3KGdDEPkbArgEgrbOj2OnDB z|8Z>of;n?$Jg-ex@xHdh(Ea(DJKxXM&i{Vzxt{T_<9qG@NgiPQqt{e6L+g{^kA!QA zo!we%IGXCuEmF|i)|JFk+LyLdPIyO1|KuGydfHm^!aq8(_!=l@98rJ$QAuy*j}Y6q zL#Gq>EP8a6V|%StlUR-0(QLtzLp+Z+Kl-=RAWr!tLwwlB zsRsL`9L4vA^{;>IoA{Zx@aM7}dsaS*=8`(QXRcv!tFpH7w)&|jm#&<-XMz4}W>@RU zvNJAyKFswh&FJNvJaJv;gC}1tS3YO7`uQbhz5^eYWG-dDG;Q7~DQ51QXAVr>6>1Rl z_}QgN^Spi2ijHYkKKh_BQTo%gK!c)fi?u@J?UHYpR4$2nT)2Q`Q;%x#O^>OKQaz`) zE$xqd`AF#F%8XS9W}XpY`BJ~nca`o2XXTkO$K*mU$gMZ9NR*ydrwFj5OOuesctJt_;!CI>;Q)@Ku9G3g0{Jq>iV1M$lYvS{g=hk~_ zh~5wTD9d^{uJVT1q7$Jv%)FQ06MFx3$^WE_TY5LfWF0!g@oVqpcUz4^3MPiSYjpI8 zysfzyb|G-{Ll48rzLL`v%!4haZRFY#bgrwWy7hVFY~|bAzJA%B7pOmF)f&^d?2F#{ zv&2fb3;A2yx;5>{)!80&>!#rPj3tG}vrWZR>hC9&CtvgNmpbjX?as{Hd&jth=J9Xi z*mM84&-)AK^ORPJ-2L`@m1XV?cXQt<87r%_(!I)ii@F)gUaXwrGUw`M*JSQrk?Rho zKChI0>)m4iIJ+6@6LX4{NF5gnV(Yk`kAHp9yT~}v}h`iQPNxrHt?;ifQO=y?a zf#BzQ8(K`~9GWau*B81kZ`%xUu9}lG+TtHfml3y>T4t~|k*EH-$sexp(3r~x{hOug zdhZu(3HM?D<`H#%+8(Q_%7X=!H(ve_dHquB%aNjGvfh%hwc8} zQ;>cyVmsfwXl3ujaN9%M>Q=UhpP92Z-Q~+#IYSPn=sjr~%>LX~yY`w@w%!b|{jg*H zW-p6*UcITCMdn;Oe)x}3?(_CyhQN!j0}KvK<$wQpmTp{}{F7f7mYGKH+4E1}Rehy> zkK-xl1|vTEkgR(iciJZHV7~t+_eZ{xgQ16EiSVm7iDeP)hDE|(YqKwi_^4jr;CxVY z&XNlY9<0xpv#(k0mI*)WuYAQ_ZR{dEtkzBIVtqU=ZAjIa^23%%c46NEsi?m*qGSJG zfA!1SJY~Y+Pxej6vem=a{@Itm_{zESALsJa|8)E(e4|zJO|sH+GX;iy>I|B)M-4B1PCO;`zVDgEQsF7z6*u^u z`=d2UOD*Kz`vbNmt206_9=Te7;cvXq%K+<>wsX|?Op2KE@Fvq*{aa$cgl|3n%jvfI z-m!Bte>T($yk06Hk`&QV?{K`7Q`xC}TiMA1ndQG?+G~?eSnfQ$dry0XeJ%Ukls^Yo z9#}DX>cWNxA)jhJJoN4#yqm8$sps9U*1N?gU&vLy<_KH!XYuOS_5Sw`IbT2Rc|-s8 zQt?C;fra9Q*Agd*R!nf@Hxx}?v+wbeil$kbLGfM=GSwdi)#?>A{%B9Gon`M>HshOd zMb|&~9lv?fW=`p4;aF{H&bao&ediF@yG8ku5+BbKKS{tt$#kTEV=5?*|lm8 z62W)ccS_H`G%KAwW{t<{u(I%EiN&9Kc=UF3Semb`UKKG*?C6Xsk2g$r|C0aXwt}X> z<5ednZqqsMX~FTd_}huVWgeb92J&lXt2idrzc22&X8F6T`ddPL=7(vkwc-LpOP1F2 za>`%jT{LUwi_e`Of1j6r+GDBu%|`tVcgb_teNJ(YydOU;x>i=qXa8#2jf9&Q9bd+| zI=V&#a0Ui&O-nhk6Meg5<$czYTTBcLuB;3UI?!1q$kCqqlb5eiu8;M<;viyscl|r} z#?+qyEQz8mse!9j{9sh-$nXd=jBGx*J+VB9ZTH({Z-pK%|B&;jn}5a%7SAi`^;16j zMJGh6d|Y^W=I!e9w$Japd~biho?*tJ$tIgLR?YBGZc5^k-7#<3)3&aYDql>5cFa7e z>!Vs2z3=^s9oqLV7wugBXkQn5J(p1QnVf=f$8B3qZkuuML(9tPEw9gNcuZ|gG+h6% zfK^#{etPrflRD1&)qNX!W?E`_p5gMYDCSz!`Z1Jsqw8+3qq3@rAE)=;vMP_;Tsm24 z=cL%gXIjNAHth#zY**~?T$p0rmS?dht7vZ$*I&`%n7G(2*`{fss#m=33U70LJE@-S zd)QJ2*1N4AH%}4SGSxP$@_W%(*3J=48?I)8)iy!2~j_x8(7-gI$8 zSG!c2!RiI}KI+eYMjig*a`%bjGVhiB4wuhw)4KXW;P%EF8PrQd5#nflc?PLcn% ze}CJ%9_=!w_tP?$K0FnuV;=&eBK);YWSpPYXreuQs=(n9$|1-~?8Y#r2^;xBw#Vf3o} zjcVV5IIXWQRQ@==3OEoD{Qc#I-wL*dE>=rtDL(03`DEc`&$OfUs{ih7oX{dKlh@SQ zZqR5Nu2D91AA8cgAQ!IdDkqLG-cqW#oOMdV$Yss>Fr4f{o+3=9my3=9nN43m8? zDrRYGoI3C2tEai`sg~AR?UQGGw?1V;JJJSpmM-i-T~!A7fx5{#nYsmu_zu%$WD;TK zg0F{|oOi&O7vzfEjdr#U3=9nRlh++Etp|x9+rR?ifUy9>TgTZ@?*(|H8U|7Ta}zg+ z1I8d7`=L6ZWdlBqpkRP%2I+h?c_NFL4!VX_H`8t~vobKe;$&dZ1=|84mNd#TPHsFZ zI@$WDngYlZ8G-+57cnp}Tw-8gP==@hlS>-2*}w|dg>y_kb5I1V`1sRzE2Wtj7<^b5 z7)-#*AjFbJKX#a6MwQ8Whg87IO}E+1KETJoFh`bwK@p-7OfG5M%?VY`H<{x&4_I+! zqtBv+3=9mX7$65df>kmwFf3`@B>`2;!52`JpI($&Tr>W!FpFog7sQ(C`^tx0PTZq0dCFNyc~G#z502qxV6cHI0#QpES59s`tup!Ad3CVOD5pn& z6hQHkMwZse8+$w^zc~Z(3(Agvs3H)xq;YHeAe>?{<4Zb0Vje)@&ssKbSX_S~c`Qtgw$-hek;O;}&;s8o z0t>)C*WD#J+29l;Ea6K6*%=t@L8_p5Nn^^Q$p=sQf=xtUJ_S|7u%vO`(#acplqOq2 z-G~?jLr;=Fwojh5MrktF8Hi`lo6ZOmSr1H}wMGw~ZqXYS2!)!5C*SR|f#(KL;{wzI z`NPD(UkbxonIEV)TJA+H^ delta 48549 zcmX@GiMeGlGhcu=GYc030|y6#)~2cM6ZuqmLG<;hGlJ|H7#Kn)I`~Q_oj9~r>ZK&p z*H1-^3<&jNAoVL1SitmIbqM`KUl7F4Xi7<2%)r2Km4SgldGZ8hvB~x9oRgQb3Drkw zm+3cgUvHatP`*G>&&DywIfv8JbJgD2-24v~9uX;gZS%}l?o7@4?~geZoDUt4NOG## zWS2%%1(3J4|9)wA0(*opBnolV^yt z@ksk^{MT6jBu`nBT z-!RL1#x>{WFgTYve5t%p)Ltub>DRoryT5p%{c3ewJ@qp}yXxB|=J|U5anC9qYoLGcAId7drrEJ|7v zQy0;9fG4l#l&!k7bY|k^txwuGm341LA3V#LTRNehJN)FWCrbm5h|bvICwB9SK5FK9 z{wAV$6$1mqZ3YGgC2$&>dzydp5?$f?k9$^gF*^o)iQ8^u=4Jh=#BMgb{DF2M>#33) zC#V1U`MKtK9cU9CeGdRZaalCcjjB9)50$#?3_W*7#E<(dj`go0SzK?j{9J znHiSOZ+p7B)^OvB?=6m7{zRUA?R+R=_LQY<>OmV$eOM&WW-4{)L$dx(ubbbdbnj5< z`DeWNS#Y~UK>+*C7O|Mqx!$F3Z+yvlf1sWj)%`3htM0}yF)#$OFfiy&o}er<`NJuZ z`rhDZeZ=pYXjZ zSy`8k+kJ62wLKi9eWH2y$(?7*_Z2@obLPFheH~N8p;b+RXHHfudZ0PavqO_3A28D+^nZo(YzPopwGPv8Tox15|&%CqZ-qlqT6EiMvl%AJZ&3ZVi=+XYx>)*NU zOm?K*TK0Y7Li>3d1}tWCxm0_W_{@H9p51=utaQ7Wc&$b1VJ(Sunt#d+9>~t*ZF4zQ zxH{^psg~GvyZYU3d(Xw@E`KU#vSat_D;1{-{brSB)$X_@m@d(){zrFK-?rSSIR>Ai zZf2D)eKKcR^s}T!#arFRnv+6Z3>;(ZDmQs{JP~wM*%P#}N0jYr8ZTqL?}v9AS5!2M zihdVJoAF}aZ@0@^wC<#G#9m%q(OrIJ{j@LKt1J5Xc|MoRu$O+Q-|zA3!;I;B`vTsr zdH7N4aKhAT$?J+z?|T%*_SdQk?aez`p|3EvaKeinwO(x*XV)3%-0R$A%TVYr?{T}t zyW`bYj_KU}6<8(vx1%d_lIT&1%tMp!y4Q<5HvglbIMc#;(qxf~9>$lLxy2LWqbF#b zOwhLIV^;~4Z_u<+kv(K0x3zw!+FmVOjzLw_c6ReyM{a}ln zalKW^g-0o@ql~BD^PEE4&ew;8#5B~tsa}4yf8BJ==$hbg4pIFDE$l1*Szh@d&{@n= z->&oRP~IC=r=n${W#7>9kj$gl+ecX$7=CdvFc>p1Fyv33qAWD|wxnWxn0JA2s?7h~ zS!$WPauppE19BKzw*)k4h+S@)b|WbxY60(3R?+Pz&0=@m?VauGU8`XC>UZgf7jqXI zDeFeCACq)Li+tGo>Sk0&c^ zTez7?aLJKNPu8d~NS@?=`LHL_YM@!Xd`xN{Z-|uf^IL8vG++k`V zeZ;uJ%p>?bYa!qH-wRm`G(?OUQ(9+9x#+fOO*~v6AayQ0iDj4k;yQ`C8A8{V^r>5& zFI5SC`@G7(v_$2%YD>`bsfMb{wB7WsWk=7Nm_0FIszGnlT8Xmxi^M}YMYox)n&n!* zd$oe2m)DAOvvOT;lj;YgSvD{Imc$Up47pDiy+M)4EJG&j#%7UfjHDY8YE- z=nrG#u6xTPX6=~X5mz0tcxuF{EkzsWEKMmjHOtfdxhL!BE&Y@)NsiLN)+VvJr&F#t zE%Y{=GAB!Lg?}N3(5*#zGP5Lwi*(FkyN z{0f|m56_!!Iu_Kk#_|>0>q%zkZ@!H@B7DI6#Irj(ju>g@xSv=4zFB{Y+|<6 z-J%)49IrXopS>+5-BNpT^;4dsdQn&2XZaqVp7!-a?bV~Dmkxv;x)qq36yW(l2=*KBQB_Ip+#-{D8|CIyIznyx>!@*Ah?7yrzxMZDeZ z=lv(4psc1_tEvN*)SaP792lT_YBY&$A0AtY;MHR9(+3Q)_XO?T6Fc8|Tyf4>mQfOPv>s3r=x^sP(%z6H{zEO1T zRJk3M4wnzV5q!34&w|xjuV44d2~WK0I(c>Ksqtn3ZLeV2PS97-x` zbDvL;D06p8dTm~i`y{u)DD_UZ)N)1Byq#S2{>gVkr^FUcDYwpwTX%a$o;xdV_~j3B zK}zeSW^E8J@H^D|WKN4R|9QhVi$(5My!x?lI^#)W=JyBQX-+wRz}$e>zQ0j3q@^il zS%4+)&fT4NG(+Z2dlaV`SI2wxtevJD2m3yEcKxmUj_E&~8DjUB=dAGx$qcTwGmqWc zVg6>WlSX}<+_G0z?!VrhPt;n^U%fM6Y0f7pvzUF>%5O~0E+|{TqOfJ+yw;PNIb5xk zYKzWpPS|$mPO)sM&?C{rMR#6FsHh!Rdw924VBHTTxtKV$3gb9S{|%qijDK1#E9{=R ztM&feTKT#|vd{l*-#p!N=KhYVb-QPL*C@Vmz{fmj6DRwOkM+$qOMjTJmH+)S|CJ!m zM60Ib6WopOA8Fpv66W(rz;^4!aHjahviBD--+!c0ci{4u>z(l#55+3CpZwjt$NuE+ z?mgB|{yNWFQN+)*rKWk_x6MuLhR*T*la6_v3!Ar``HPO_^0ZSG9Y4(E)YtYH?N{s* zJGO^^ztCmHDUS`NKC%3%o~7|$MKdnwRcF|jyMo(v_j4>2vps$H@s=0mE3)?Vv8%+i znU}Vv=(Da~@oZa`ZZEUC=Udgi{c}uiZ@tgCWSY^{JGUiQ++N}?sb#3}a)-?~dDN!I z8QwUziA)R(KiMEn53$J~&PvtyPR{igb`&`N|Mv51&2~)n&-b*|!uQ_3n>jc4^5&nBD#8aF-x^<$ zT6f*-*1VnfH};&LS6P!&dCc!gcfVhfVC&{&;Kq8-dMG< zee<-Xffp}pEq|=HZsnWRv)Whv|F+7_efOkvmz#nsx2Hc;uDc)97b5Rx?|$yRt;^Jm z9VMC9tv7wOJNoReXYW?`nwRo6pD^Z`Q>XY( z`=hDIgzEv1merr>ZDZthy=U=te*DyRR`HMAUSGJyZNT`iogt{>W2eZ`A8!}xbHt16 zQ>n51!kjQ?{u_h2Wg6#C^uM-0@FnOCOX1!r7LGbWcuyR2U;%|48*!-^aY#Uo10= zr^L*dt8n&g!J&dHs%jrRxbL4TkTn-kD3nYI;xux6EUHi_xx`IsFF(hBgFla=y(TQ- zFmyoBE)u)#;uG05mdv8qQX z@1#kN(>ym{Vqc=-kmWUD3y)z?c+-ol30rtJ`OUgs>u`!G#p=qPzuc%PH?USV^C=4h zgCrLNg8{hFKR-;q{&lf%sL21^*(rz1PA?Ad@@nN^%KX*QAezIab-`;Xze<2k(=Olq z$*n2oIc8f#Md$x;`+JC|?{K)CW%FT)1LsbZ|JV3hZocqr_pP+5cYB|gp8q+w`23su z|Ns0HZ}|Ho@~~ddiVA0EVK$HT-%XZ|- z6kXPJnv2PKS?Vr#Q4{xF%SC?l-QMhUu+8wYkZ|jpo`Q2y#*#(1j=fnjai)<>@VwJL z^JklV=BhfLqb}-gc`V26$TL~x`su=cIvZz{yMEh}{Q1EwrTR-7G|C=vnJsUL)(+iZ zY*co0N7@k|YqO*@mp!{nbatevndLpn5I0%d@|r_=wL-{J>#V0-d7dt&t&LmtPrIwP zi%(K|K5Lt618pR)%NFMe&nvoWz*>z>jL$%9?BFo<0ebX@|;=$i}9>)?aRyx&( zMdWRresbn%rI$hXQuZxK@NR7m47X^@_P+gf=hKqZD=8uAYZfeBv++brs$iDjg(A_0 z+_eJh88nSgO6uirl}atzrPAg7c~z6%vw32r3x3^NSheAmrTfc**h4;%ZP5iUk}JL@ zt-HB=+6HdFpeYYtRk?Y8^$wQZ?fAlc?VaLFC+e41PVdsTGD^6dli4!8sV{xYq%RpS zH%z=-82anpodwH9O4hN|Elb#$78b%`oweC2%)of_rjup-)_reVo?kx1m7bG4ZPN#@ z(!<3)PdoV7D?MXmSz{(Mp4GlFBfjlnQvDM9TWgaXk1N?-D3q07^~2-%@f`~v3CqrJ zimYinUn+B;UR-P6mBQeneTS}V?YsCE8v}(<5?bdhAs)UBs)j;=vqW>u-B%eB%Ddp08-GS>b6t`}2qTNY$ln{pJTOrwOcU z*%H*N!xg<+q9|`#x`2G`)T*O7S*xEL?b|YOz5sX0!_9k^O7%TmSiZsN*W=8?o*k=M z6IETCWy0JtW%VBWvv0^hH>LF3a+%!+EN{q3=0>#kZnb$dNigh`Z#A#E*Q`lD-~XNe zOaFNw;~evb**815_{ga4K77@0*Yat%`aN%{4>N9W z><`h`RA|au_gcV}e{04Xn+ATzPyZsEE`M=0>38k^@RzCn{9n}{Cpmib9p^54{=g+a zUhwsg^>>@?UQC`n;r&V57WHWh{Ph=3z4iLGnaIY8dS;>*uYDJoJ%3AXWy`nDd-Y1& z?A+R$9OF-Xm$)r*=OE`!?uk#|bN}pqp_BAGK|w@g(QCeMElm5A=1GS*X9-%rTeMew z$v3aHb3On)mbMmt-yIBp40a-890{gQ9l4q<75V`{J3 zJMQRteHZMLW4d(yt3XwzsOy|LpP$4it-AiQ{-c`xw!FeScaJ&VIqJQ`|17)3n?@`4 zpNUuePTSoRa~FS`bmy*{y6*SSmtU*d$}LE3*vFP(dq8MOA(#H;ZvQ4f|MdmDkIe7M zrB8JFA?j>#=uyn0@7DtNc-60;${lg3YVPjjxQYG6<%e$b{$~$>k2r!x3YS(I>5RaIxevt)=qb(7AQDQypYh*znraH@&@POTgHct?mXw&_{#Hb{fxNW>I0IS zVh=xBzx=RW`Ms63ll{zgZ(BC&_^M2pQ*k<0&^^wgf#E4lh0O#cIky?cC+J_`aq|+2*#T zoS$bUWp8xkM$o?;J{KP;NttC6ouZtKd#iWoKXjDh5A;~v;qGK3_K5Q-=Uk)1$NvAx z|CJuIm}&bt?ar)qT^sd(e7EzBv0K7j?G?znU-)p?y-Qp%Q7qx@S3LgBNxpT@BVyUn z1@$po=8Hahc=U37r{+2Ly(O(&wSPG;EO>cSe*Uu6d@Nv~p^xa|rs!sW!b% z;nUPL9A+Ntl)XL~CEZZ-60|)3X;0@gb{pS-%(*w56chIG%!vGHtu%k)pSFzJwi`w} zSU6KJt=DVVTwE~CZ<}p_pf%%-)!Phnn$)U);;NG+IKTHdYjheZ|r-`-)G)CV{UwZ_V)k( z&i^%MoO5jdBa5y>a&wLb$*|=e(cG)x9N6|$!{_F$?k&1Wu5RR#9gQe-Jp8E`)reIvWY=lE5h@-XdbC9J>I)Q-=W zk(+bRYU}!h?4OJ59>4gOnjL9UW85~i_K?Oa6T!+m2E5-FH6082zNYEIg8He;Z~YTI zeC4>;Y3^ONYjs6RKk7e7H@!Wfbc1Bkbe=`;WH!EWx>i1QZ)&!-_O7{eFWgyL=eE_y zcXl7&lCoW5`M~lh@ z&n`@tTlaYByN7#E?|PiUT6;p{Mc?WLhr^4$9G#TABm3sXxAnpHdp11dvX)-Qrx76W zcwuf>d${=5sS5Uj$BPdhy7g;QX|Q};Y3b#Umt>6gzR@_-HYxdB)2ZKzOZJ{c56)hyJ-EP`Jrnbhl*nezd8nxpc;Y1#5R%M;gT@N8bOI{(C{F*0agWo~ljvxU(d2-IO~^{O8(jj7@aY zxO4b^$erVDUHiS=-gWP;e|C#)0s4=c9k~nr>zGUKX>D#*Gg?|9bVK z)G(6W`tz>inz}jP9v)s9Q@j6D7R#GU@jCUHuFH?zOKV#HX3gbUk6)dx6H?FV7Ad%1 z#%R?2_|HA7Q~A5ywmkYmLmqaKh6Go`v;!eetD*mlr~52vT7}k{-xE%qAurJ%>?F7-k&7;=B7i< zx+b-CSB$=?eJjl1E!0S^XFdD;(Cl68WiQmqUQ`A7e+@`sW`BNZ?Zayxth%qGA8t`R zWOy`S*GBnFnZ#-A7n?)V!ku=%X}-tx)=aOkKjxtKiH&0Qyf&r6{y|w!A}9ObO7=Oc z@YQ`rlfm0JM>UpmnEt%OdUD6XdZ#)4OFK7g>~R*Ak@J?}+~2skxOe8todNH!*lt;O zeabr%@3LdgQ@{D=FSL557+~Z1!8E|Qv5-Hsc~5%d{oOX|>c53@zkj%(G5MO^wad|N z$JzSMwst(V6;+w-Kc&8~rnZDRwzyMx&Bc@K^PWD|DSO_x=4-KxgRr5=WUIM4W#XrG zf1ESg$8pQBQ*&NMQra>@XPww7=^uNX?VMHL+VEU@%gsLkt(^|R+IHtNs5 zm9)I~=L@;Fg3spcy5ZPv``T7^;q%iT;kI&VDG4EoZcB`or5&5B@GmR$*0w#{66MZc zc9&>hP#F_gS$kxENb7xz#{bhR{fvyJcnUb4mpyW6{lY}s$uGW!opt)|a^E&ZyW;(> zDXI@@9+|wHE4Xg{%_l#V`Lib2c`viGb6#^7Cfu}|1MsnAPJl$%qzWe4)ox|D! zhfZa?C{?zbO9}9LPPid3;e&y~tCaFdJ#UoXotxycHmcj}>b`*Ji(zZ}lXOD^Juhh7 z$g17;ZQ<6fTd#&?=f8ftcklOa_iq2z`d*(_Wjf6n+nt+!OubjBH-1(VlL=$yv6IWC78uE`|G>ikarSvujRV4|Ig?B#Gsm0= zUNTeafzmv&1=c$vX1(NMXEvFde&WweuJfNhzhO9^==<<8bBWY>-Vc&C{#9N}EQ25Z zWSn*QF>}nRRK``0i+-+|uq1U>j)xi~|4bz&%kG)!&Ibm6##u`Dc=f5l0elgLk zX*@LlQ}O2c*FD|68_qrcdMStFL+hTvhu0X^KK)pCPv!jCOChWk-1(tDlv;16T-wI4 z_W4`JtcTsA_XHP&J-mHr`j4OWK|go+%+hN8%CNO?_o0OhxzEe&+yCbL*y;S_t~7OpJ4d~Dg(+2*Ee^LLgw-(6O} z)u;9B^w+z;%<7PPy(V+3P}-Ve*2_PBRp?~CG|cXn^K|@_J+o$FMf&oI39~NB_?KKO zQTtMGWW`I*7kBTvEn0R{?ub{t#XaHfBVP?J=WOrv<$YQB@Y34o*wT|r1atd2p6<-! zHhXgJ;G)Yt_X1R=nv_dspXmq`=aM}6#L9Nr?M?MpcLZkGJY<^id96wEvaMY&Ctd2l z`1-M_<<1+a(KGgZ{kY+v$;O*KlDaE1c2B&qBssuFfdA!0-Ks-V-j^)D{>o^wk8!MJ zcfOLKhdhTW&s`&_GfR$^Y+KD1Jy%`kR$|#QpRYGdaPr770uLds4p zSr91kMmVk7YhS&&x{{Bu%qPRct8VVu$Qj&Kw0uk1%kF7<*}kqb)!bdaw#{*DUGH4} z`9z-DR!uSH_Z@tT4;gs-Wqqk!+~FYL%93dy94XoEv*6aI9OK?((#=+LL$!1p4T}|#1UfZs~w$PflEBp8y>+XHK%CYa8U4!HHl4VQ=r78)t z?7UW8{ibzc#?8!#(v2U)loQLoJiV2bY{TcFE3xo?##cARl@`~p^12yKG1x83Uv)T* z>6K4WSb|N^<(ykpe$sM`we@#rYFuzT6Kgr=wpQfs(-UoFd%niHWuynyY~E6`WsCY% zw!&2|vib+ws!n*l6uf-U+EKZw2d*G>U{=zVmRx>Q&+XD$f+t zw)JnBZJu<*U_$yL%?wlP!QsLH`>qmqG?{0d-d5P5+RW!vWxrOrxv zC(BAt^;k^Vu<+uPi^3^KB-%cIz4;(|B5UK`MVf5WIz!BR=3A>iP-)uwQR78Vx!tSB z7gxwRPgxlu=)dN4)m5K7)8re=7HnO$`ooGzMNR-U&d#Y0zq^R`VpuKGRadvx_n{hq9u%PxKX(&V*&VR@*P z&tI*-bLtjeQm|cosq~BF)O8E9L#tZkLSMS=lj}JiQgCX~`BhtH=LNj1`nBfJ?$CXW z`5`~m?k)cC{K@0Asre71L;u;_?|S?5h}T)>HJ&eO9iQrdn!dnw-6W0|*S401t_T+S zx9L>%PcxRXcP(=YSOu#b>mS|@-PgQ-(WmgAepA1Hj6I?EF17hk`RYHW|8|}F{-JK_ z`=|LH#q&>n?-t@;@kik=kMzL{k9t?mNOkMYIh3~RO-;1A?)ImuWtilWS*8J5VP*T_$X;=`!>9vt8M;9#3Xn?5?%Cv|RVhx!0@RW*Q!;udn_6 z^hMau*)8|LCuewV#H|ta2fpM^RrUdoK8N_)qTCcZxG!DAc<@bx#m|w@F*q zIAwXP?!AROPhMDf*zw_I?SZ>BE+7)yr8^Jyegz`SFJ8c4h87x~y%}sxWQOX-AA^22Sn@ zXq)qH%~9u!Y03{j(aE` z^3TqGO6RS>R+-=nsjJJ5h15-q7g`xN?b*~su~TAITKi8`2L#u5*sDy7JMAMKw_)WU z=9y-{A7`5_`}C_qOK6+3ZKV+F#_0SGiK?K2WhZw=BnkPsADJm@##a4hhU&!`hhz@M zTP2y9D++3SOey7gI`@?Qlk1}YCZ4i?Hh;yR;5z1HTR+tmpJwyS-+$EJUGi1bzZG8; z|2sY09{O+Tg2&;1{El5(e5&4LYCW&~=X@4xV|T0MzZ-t#R^@q}lNGYxqn%gjztsMs zJJ@M~?J>=N<_ONr<-ui_} zCJmK0Pq*HkzvGi+-qzr-b9+@jzkK=FXYT8pN8HT59SY!*v`@Qw#o40sLgICqbI-U~ z(`(MevYk5t#2&t$ep3&+}(9AN#U> z1yL;dnKNT1`N++i`KJ1CVOY)E3$>nsnUl5e)z?^TEQyLOli|?cxLL*^eaf}|dGq%s zt+d&=F7NW+FdOCLEe}QQrWj4`y_m=C?h@ZVciP+HGa9dMyYD_8I$z2BioCsTZ+x3O+1~;AI?sZ*JA#0u~}E?J68hp z5=pCbeuXdV+bnlI)m94kc3YoWedD9WzBgCG6~c{~GJZc>oO5OFX$#|<6EtrqG|9CY zpSSoqRXoC{$?t2Gj-0cmc;S>;VXulf6P|l7uuF_Kx<0|VTz;~9>8WE&W{Yi$`<+tk z_2J~5jV%5ZMN^f_V-ix^ll*u@pQo&T7~XUF?)mSFu1;CF@Jqe##4o>8WoEy9yvz8VI)L z)$tqcc3d7TKUFZ$j`gM2uC5vDj)lwB&f+B4VFJbB5Tg`V3ltabS|`^1rNvpuG!@9&I}`%*V=UxVDlXB?~^ z2a7{p?R{g~(}KKzPBvlGpPaqdG2dSF+OyNiUTgXPK5pLUIO|rJ+!xl*+S;qwR;-x6 z%+~#AfA56NJXX`1i&RtAE|Lu4Jt=;pXkC5jj?||yuJKFs9T(MD&EDrz?WpSD{le_V z_2#WlORVp5o=tN&Ut1!cc6RB^67zdCUrg70OL}^JBir*(clkW0cm`#25kGsUgQd+S zQPD>k&Sren`Rq5(<@}HA&)s_}e`ltJ8tk8+ruSK_^1bE1>8BF*E$8q$XI1d0`BdGk zSZ>|t^;>u+gsrZa_Otts$>I$mpL3fz2R4!#L9+xy#-6Z@b2pWv#8|S8uJT( z4TO}~R?975S3K%+%k};eW`k@+)t8EEJ~X+t>~rNmyk_-*ueU2cKit{Hcd7irvKVH2 zr~YM(pLyI^vUHzr$gcCg8eF1w*!(hkW?gjR%I}d{*`GtU$tf<_{Yb)1d!pC350`2+>Y6`_d3w#;vcCVOMRorlI($y_T+1PI9_a9WMjMzDi@8aVH z-&szk3NNpo=i#`|`<>|&Oxv?mG z%eKmk8+UBg-M?zT{V&;XluB=KJ3+oV5Cig3J?* zufjH}*Iuz@T5hPB#`68oiN{=+qB(r?d^xN>#bKz+1Iq;pBBr$=5wtPGa2+*Z@u5%T%r|v zV3+&GI;(=ZGj4Zzetfaq^R&AllJ)hax-ZSMD)jYV+b)0Z`K9K%+1u|Qs^`TOT+-an ztjGG|zd(mox_l)+Z%|>h>%&!yR^_tWc`jAv?n_^}?riW5sdd_G+8;mI-*$~LtK`#d zsVC<3JKp`-{enluB6Y&GLlM5M;$JQ=KXS3+OK`FMT5E@Fwv|hBCtTZp@((X_$wP+R zDLxmwSZ8O68s2}lE$V-Fh)f9Ac8dW1U(vraUxz&r_2dxWDzhMy?{;p5?DIWwcbINW zeQI>yaMDg5vACw`&KLG_eEcI&)L}EytyZrU9?{jzGZTEd%SA8tWrB?S* zo$0kL%7>~2o?lnom+$EF-u;M8heid*w>pU}a)*BlZ}^h0hCTNmd)M6N zE%>G&wclU=(5vf-zbiZA*pAzpbjLA@{CzNOLHJA4ik;KSF7dx;|F9`#>h+iIFRXL+ z|Gm-ntv&L5J;#c!=k?1kNPh{IzUBI&@^D+(yqL75=I3&M2;G<;R`F7B*PFsizgOk#kO5-r?0n*OVS)H|*Kem=OI*=FZ!R5eKid);l{{eEhG( z^F=6PZ%;x6t6H_F<{ML$KhrY>uZS$1))32?w%2!m%MD=xxk(pnBZ}4ja`6dFoSuGm z>zumW+1mbjzc25;=lVd!aYOk8tCan(OD0@ZeR*@rmz+mWi`o>+tP1+yEKa((B*<>j zg{AY899~K{i?AQreY>rf_kt+%6bHt7fiGU+MlYllydTW7a(FEJV0qK2jm*dM#h2|~ zayfrL>)+#ij*DeWEE61mM1C`Fh^Y{sXMWJjy!VFE{X;Kqn{ZaKFZ^lqB7#%*{KD@O z4DYiqz2AIIUc>J{gIB#n-e2X6+7_$V@{TW+Lj>y2sV%I$k#&FZ{X^TVH=O;)w_5e+ z`?K}83iWFz+_U)BwCX^f(5uD#j#rr_zcn0|SFBdszAf>O|APllu0>x^7toyf#?|*? zP+)7+qTHF^^JWD^riKgIviJR$OnBicvfNFqu-@@^e!#c&8ma$;pWeBz@TxwwkzrA! zp!b4njL}DgG`{WIyZ!9O@*MtzAfNv!&o8g(nEdC<`T7O#I&O4W%Gpf)#FdlrZ(-1p zbm2LP##iQOe$mftaU)9{vub&zuEte_x62%Ao22#{@?gpXQUHk zJx%LoNPgY$e1f*>@}!K9>6$+sPu@KHcf;n*vwy#Qf4IE(gXOs=ug=+&o5{aFx4Y;L z=Ng0Jd9w{~%>BmM<5;O<^Z7=%mBXhzxw{8lS8x}s+4TJ4e`eI#tGfq$I*+h0Fihoy zErRl6l&O!5z8xlDduS8uDxm{8hAah4BD0VB1$#*^FyU<7ATcRnn$?o-4f<{_(^lvR zznG?%`CIzet?B#B^xM|g_}>4m|LaQq#jR$tX7Dc;>PZA1ZstKO8@aAPEn}r|R z7z&U17&7)6o=wo+dB$w>wJW)ERsPMG8S<6)>ZKq16hz*}>S`;<7_Tic$z7>A*L8l> ztABQz7nS>7K6h(NWSD8aa^C7`XV-W#-qVTadcmK2^xG9?(^ae8tfaQp2VFobrBBDt7AiSo#vbH&X&Dpyld9kV?L%wJtPYJ zCC>D%TYtd%O|`ZEWL5DC&cHh{-0?AuvNVkTHIdaZOj$ahJoOf=)h z5eLYOg{Ejn(zKPawc{_@gs8_wHmvX(c%JmD=;}f>&=QnDcc-U~^^A+K>U2ili zx||t(pFjG2yE=rE)1Axi@N?IcR+9^@*SF^OJ>q7Kf27TN{)6eqqQm`Shx^^y)EfNS|!06F;#cP)lO?~ z4cO2V8hPx*rnO4fk49}@HdEIA)GeLhe;c~wwEQjBPrAt;(k&VCzF;G}h;nQ}mwD5q zUF+UP`7BK@*POX|$Kpr(w(6|We)?eZnRzQe{;_sm&ryGU!R1{a{;@kYa6kX{%rP{0QxRe9F}->(;Eo1iOXSHLFc}s_&mZB;xWgYhuH`F0-KKC6hUJ&5JoN zov0CW)muer@1jp^G4uXyR=XRbx!73p>aunRx!YMwzX=N7G1N6$r4XUqZ^|*xb=}Jv z=Iby1U*rGx+2Mh1Z~d{?Te>glTzDgE)%KxDtn|g=i_bZ%FZeHhB$dFh{<2TMu;Qji z?f=!RJD;6hI5GLk+Bt_N3)HvI_|Wt0;n~CD_Kj2A{~AiE9T7gkXxM+9X;J*i$Cp~n zAAF5hdwr~BIjiqSn_22LjPo6Y>JJ(F{GN7JoG*%R`EgF+1qE}rygy{Hu0Dz9?w7L{ zoaGtz@|~#^_hM9wZ#8`4a9`!OVAGq!&OMj9e#AH4g!~VAtZ!1f5`uEKF*3+Gv^!@w`*-YX5 zqE(eKAK5>>_`|uc;rsRHi$0v*Uq5$Ivi8IMQ7Sb}?~=d#DGX~Y7*@Hpk^d)BUnXcYgAt z7Iz-kwyRv`Vqj2~m^?AgpuRFeCRFmj@1~P^W|v#?CT-Ky)KSg7bk_Cs-D%36!BQu# z=3JV!=-SD5CXqMZwA{QY$HAqcp|waxNyNomII7u~0c&{q)Q4l#2ZuY7`py zPkndZ^eJ&>txALB&wKiZ-I;&PsF9!W_OQ`uHUsVZ?5PzR4ed{@)ej&4TcOh6ezfa& z%U@x~Ieq?JbNf%f2{!s&!Z2S@zVFA!mJjtZ6X*U;`4(w!Cw}Cng~Wl$pZ?Y`#B)68 zW~|X{&}aWJm0_ROY5$)ew<_#vdHed)-!>lq&_}a>O!N4;P5wUTBJum8s`t4c{r=H7 z{*wPIUKjpTtxGKv3X#Lj0*&A)rmV8rD6It`x z_)KRXXK=Ur*S^|iZfcv8mw$e{t&nre>-|J65!K2jw!lFg?C*W&u(o~Tv)p9 zz(S@ur_9xZIA7PWJ{R3uzg6I6a>WMcMyJHmd2i?aHLShmdOY1%O=9n{qn5jJZe3Y! z8hn3}hq3i>W2KZUJ{lGaE9Wj&SQ;(6`0!q#HCrYcCUbv`yTSR4$K&_M;$!!(Y~7)I zNy2pUnTIx2-F{0A&t7_LZOXQ;slhjM+S1vU%X3t=ozo9?bgglW2)%USk@LKIm)t0k zSxwOj6O#mA-3sv&KL2IejK$|ZsoB>ZT;cy>nmB*c!Kr(6LM~kT;}mpl{p|_DyS-=Y z+wM{onS4cCS81lsuIs51Vs9@m`)&Hs!y7jT^ z6Zyq%-Lnq$6x`S*8T@7Eqz^kMChWOxCZAh>XIIj1)V)7=T z;Hq^SM7Qj!H)|DdY5#slJ?d}C&Ga!2p9FPlE>Q#vr~*c9HJ-3Pwri%$FTOJ49#yv_1Q zmFGkbBy8_`zV+P_i9f%7))Yo>>FU{_r+?=8Iv*v;>6gBxpLt@S9T_UJv}NYqK!zE? zjb75WpY+~eIJ@QTMWgpw2t>q2=l6mxb_$ijXMWWle{+-_Y_0`TK3BRN3>$Mn{ z9$L>5x-^LEgQ&ruu2ovzQ?&L6t<_T3@D?rMlStgX^MXvaQO(Y}1A(*WT}w{No5Ln@ zc24D+h-USw$qH2s0q?#qN%VckCi7*@ys9_lS8BSSx(46UN!}UzCUf?kso|gav@PeH z-dl10>%AbqevNzkUU%J<2#KGow`aw4v*-5pzAdYlJ+PYoDAhSfZ`CTb*q)sMEmMA8 z+;i?#7t_5M>xW^#W<)#AyES!2*%j8yaffaew(Pd%6SVwiYG#xv^}ERb7*|*4F}2c_ zk5!B0KVO`_Y0hbt@7JO$4qktHW3k`9qZJK_I-7YOo!XK0tvkf=_M*Jv?H5n{&6NDS zbK_6erAGqm^AFuhI-ktCymD!oe^%NQ@on?EURI_({mUI0HR01YZ!arj=W8)v=4Lz( zoyf~1;w>e#ZR+v5M8h6Kqv&soZ?Ee)YNyiA6}-`V%Z=`yvZcD#4ZW`S=d-?hEb{O7 zS{4Bp+il4+LZ*apUYjiU@q)HTf7J`NOV&1DPA)00dJ*io|3tm>dRLFUvUAQwJ8#F| zd;e|vb-VqcD;nAg=XFe!a4Z928_lE)Xv6`LbI&3Mr( z5*pqb)VEgTlADB;P1fz0`s*prRYd)`$ESj?7Sf$@LDb9#& z`M{iNrmtnsY#L_i+}e5RAkV2sTV_sctxssn$&Ft5S82tT5>vg8EL$#aUDWf_wpTl& z)$`Qx{8_)l{Jxh}^9tN>UidXK$D(OzvBliT-F0H=XC9rAj4BOF;Zb9ize32^3?t^Azx>W|GUrs=yTIE&2{ zx#(Iic=zwxg}SMJ--K7an4!PLZkFqLzo4yaBXepw#HS`aR}A@f`(4g$&3Ripg14-1 znU+?hxp7)6n^+wu^VP!wUpMb~tdl=Ex$WuR>2Wc>`A6?_{C%Ci=*>~PPw!JpJwC90 zlaAr>`jGWz`|J};QjR~}A1wOlm8AGE+O++iAyZ9ZJ$L=vy)FCqIdkxx*xp+i?X-AR zfcr74V_WsaTc)xE?{Z|l8Wxz?aWmw2KJ!B{=EG)JRu{SMJ$26hrRvrS%XOP#%g^kZ zWxwz3oqwAe<2h3gtX%u=kl)mgE!Gcqt-3HX`_5oqxFL&cp1&x!s6 z`lp-;Y4<$>YAhL4{{1n3WVPX@?oo#&u8Hq0mR@LoZL2DFB9rFDl1}frIkl2|rP1!d4G}j*&2qk6{oS`<&pq?YRrM^XYmHaG zb8&6?xi{A>^1}96n;dpGO^NW0RNOOD?#`j^k7jCT-rm`NxH&ob+_YnJbbHt78dVGX zbN6X$Cf;y+eNCpy`Q6tm3FeO$8f_Qs|M})s$@=9?GVi`A+?sIt?t@oh8H+#M|7VZd z1a6U9vU(F21A~(U!{k0?i+aCXm$O1GIF}rr!pwbT!nuatjRyoGg%lm-^?xgR z@9Ygb`nq7d{P!Opt61{`0@XI2`ruYsvA)av;aRSGixypC+#hWsRRQKYmiX|H;Gp-fsRsKZ>4wvAV9t_u%!Vy5%RIO4r@dtKa|f z`1X(*>HC2nV(s^-ckW;Ri2cXHn7Sp8j{oTFn*S)Bt8U#Q{`~?gZdESWx_9;Cg8Ka9 zjfZzv?Xu>p|NiFn>o+&G1zuP!n}6Z3p{yA{f``TP4?$@u_S!!~h87!Sz>iYQD zy(>2al`k%r@01MkG+uO^?TbsuyVwQirFzv^ua(X0ocO`RNxo0A{^4ZK+L}8aE#>;Bl}l=-pvM%B{xPrlohzU~zKwEBaESU~K#-CtWARyGTo9=2X&*t~q{W6sI# zVOKo7IvOS!_^!6~x^t+|cB09TjaJd;J#?h9VeSBqyVXj}Roz0xr z%#Bk^H!n75 z+d8aSJJ_ygNgULb$~)#O)%Q>>Y%zDN3h(~W zun*F_`;WfwD2`e1ph7F_Lb~+-e(e58X{U6$svz|GAKdc@B?-f-7OvVZ0;gY1Ul zqM*Z{3uo7Vd1So#u9@#f|LtzklRcArL`@Yx{yW{O=dhOjQ|OvZ&u(eS`ElOcdqtge z5>}-yXFpr2F+2U#qLkeMf_AF7-rC()ym{E~Oh z@b3+G=~VM_ovC!nbfX80NZ`UwuZsi}9ew8+I3ou!L9uLn-ZjS!yxz|3{c z^zVJpSc;jN;WvH!Q@`5m97zWA8;&f4|sr^!91%U8b_o&T7?S1)FJxwNEj z)0M|Y`f^t%ukD<8WrpB(y(Lj?>*sLxxw!{%#;q}}Of|nRaq|<`o0ZA?u2uB0My8Zq zOxBh%T=QmKgzepppV(QC&P`@nH_LtTiARS{?O1A;uHU|5%2ln^aSQasrypCl?}LV8 zYNa@PvGwlYwPn?Q0^44$J<`vAy7Kbw`q@wYbvC~}`?Bii#t(9?;!pWBTVl=Clh=nC z-sQ9LPY zG3VA#etTr&#A}f`0&BS6F8e$y(zcd;a!;&M)?B{lj{@Fw?KG6JKe2tf@uZJ8ryVbS zHs#N%^X135-2Gx_t~56`x7OXBefrCi2k#2&DzbX@u*HCdwo&8L3aG1zYEvYYHGIBTr1eyH^Jb^ z4qg!x9|xO+#WK&LGMK|3RIQuU%pmLjBf5p7tfn=f4-> zYEuii@re7^r2w|(eNFBUic_>+*%P1FAwXS_hkhHI!59E4>{zI9$@> z`XzOVV~dF2qD8KpS}kIJ3nyuGUyhjM7CJ%ws_wr9`!j_ccSha{+M*X>(%Bt#Wx*|j z)>P+%i!^nfOGOxVP8H_xO}rX#!M^dT@Qw;D(v2M^^RJ-aWejPIX67ncJF3Ew)%z3H*juGGR|}7j%!+L(X9PZA}aTNueF@gy+G@l z{!f;cUj1SZ6W*;w3Q`&BPk4RBtN$Mkjbg67rr zF#*Y98OIcthVW##vs%5FB)cupTPAGBM6Uz?tkR^Ho)X`r{lUn5bJxjr7k`$UsqeSi z{Au-#Z`>yyJ>%&P6JLKS%%ZpSPd1kc`=QJjqsFg0-|X6aBQ*8K$FJ`;?Vi4Fa@G5F z2ii=`mKA17O<%e(^L(ww%UOq>Ej-2- zTyf1ZeC9RIn9^(0LjCT?yvy3uWYiFMh>hc|v+A#zR?R+(R#(>Zr|k(pIpJ>C^9lRr zcz%wt=`Yw{qq}>T-B|;JNJ*lu0QxPSQri&svqm5gecL1}rJY0%`@-o|q#W##K< zAG@J!cs(#hO!`Wxn+ynpun-EZrY z&$Im!{-d?%nW_$BsLztAK1ZdN6)V?DFPpwT;mUS{+;Zc@?*@@edwHi^@bJ!G{&ths z)<>)riVw9pYXv7q2yC7qSFurcvC6xGxA)SQ>;1hUpy8E#|L%&CNs&`$|IL({U^n08 zz0Z@mZ?<5r zdMWyB*3EN*6${uH7`E`iPD85K6RgiJm%b``_qtSMmyM?kBV%AYi-Cgn4KE%}6+b6o z>jpzMLoU zam$%{HMcG2-9E&ys4G{*Ev!AY!8+GG`^Mt1hxLA^%|yde)Os|Pb9ZJ+e%IX6>3dG= z7xT2`1(CT=B9m3`7tLj!XKQ}a@b8z#+U7Z1yv}S|_j$oo{mQl*nkA_*`t#3KyPY$; z>~6a_z+mqej|<DAUg37C=)wmb$?&PO)&wzf{aWzYpea|RTg~*k&c+$%dSkY`rR#)LZ(6-^pJDwb zgYdrreqDExO0{C-B#eHSRb4RO$#q@$v*%&!q^icBMaNBwkJdAOUy;1q&Cjt{Xy$t{ zqi0hxC+oXhi+iqixQR>bd}(d#RqpVLJps{AceQEW7IV6%s8J?=dKT-+Y{MpTj&0l4 zdDXp_o^7yMDD&oqspUNTpIm%>?kHPn;fbp`CqMff_y2Aj%(Nu(M&kA-sb>>DR+;X% zO*$s@u58n`=#<3oGyd&3U0?iQxm8`8cxleTFj?shf$r-&q6=2{`QBLb*zC@_#r*mg z!)rBU^^>yX$4$eHru*^_z0FE;gXt<&&y zU-Y)TQrY~2qgHHl?dkt*cMU2(F5H*2ux;boS4$60w!3-tk5Q-owW8`C>7$CDC1NxD zmaaUwCp4ifp37(5$7S7j_AuUMe>G30boa_1M zOP{))cwW-+zxKwlo40S??iO>oac@SHJ@eFEm2A2z-$}Lvaptl`X$j7Aw3wPP*D?DQ zkCObwO{~@$(U$T(HS9~{>R;aP$X_J?$>Nc(lXg}9N5N&PSE8Q_*iQK+p_{5>-F{=C zaR#?rmE`Hi+8U=KD%<)+vz$}c%sMvfYwD5Z8)tE;o_cUNb@PrtB?iX-q?B%HW-NRw zdwE90rT?PE|Jrw*j<8j-OD{~lYEUO?AG~zx#08;};q3SKEAD!?`31*9>w<0dX-nGD z#IEyvyd2fKHp)CH&)B~$bNn6QM9+C5R2lLRx$Y+4WCJhe!Fh+ zPWJ8o?{zTX!$SK5@}V}yZI@OE)ZCa^{9CX1#_9dy^$dHCuC_dru$F`0-}lQIne(-K zz67fM$TdCDA2Q$I{)?{pN@0s@wyEftglO6GR{O5p&&eiq z$nBcY$%PkAJ$bw+yGKYYcm267i?rXDsurg&X)ND+Uh|3Rw9gDYb0zf(zmB3~r*>*uvX0o#=H8xZ$;ylKw#X%> zt`YV4>|1l*<+;Y3f6Cu;0;3N-s7mnhsHjoDv6s_sC-a*JowngOb$`4)GFSYM&@QF` zZ`hHjpaTjfC3{IraxgG>^D{6gLN>Iqb3iw=wT5@ri=<1|rET7AY|UdSCKRDDVHt1E zJ0-43Tdt@~D7$TPP=_-%pm3A*O`W@Ea?;Z{Lvr`7`kJ>@E_6wpXmtOyZ8;hj9)*V3 zM6C^r{v-ZJ{L1s$ci#wxozef<_x<<1$JO(nS3f^JwSIm*>w&UA!Kulu8<;IG-qDw; zk4}B8Q4sp1(C649F4p6ra}F?T*|qR@KT}RR->fBf$UU{j=h*R3lgGkZvp#r=@;?qv z`gxg4F7TsdO_<62;E$Cxjz`5ms$R5_TJ-j~NKNF?@DDqB)@%Kk)phnq6~n#rF1wrZ zS5z?F5BaD&CuBpg&pzFrIYN_yx#op@kY&9e`k~&I^?vvVW>)``KNfaf{xPvhtCHAM49ygTXl>I2&m#y<=``S5)yCn}leDS06;U4S!8F^|ecTdw@Sor_S z6WQS3k3K3dF8A}lqwCn^8DX0>vv2k6cRlmYO7kjtajte-cKp~kU)j#%d~X+LL~m|M zxaPH-H|nNEUj5CNja$|(-*oK2qP5Ro6eiq>mYH%)H});f9FPTiQTxyLrhmX#D{A3L+7TBXNj{o}_`#n-)drK$XIUn_Fgyf@t^S!usP z_6_zgH`j2X6ymn~iD-GMJ5Yece+NlBbe;nnwv^k15Dc;WkFx7NBSrds43)47?q z=IYc1@2uyU6eV4p_)(bi?CHGZW!X2D%}ceZwBCK_gN3Q|=e|V#<-I~L&iu)#%kyh^ zZEbUC!kIsZrYIiDKXo%K%EdhEX?<&8&@;WLt)JGlY%8*vTxmYNd}ft%{DM;Z;v*H; zuUDv^_EFYsdde5y_IhpV-x-l><{ox~8}6>f(uB4X3^Y9&M96?8CWk zy_TPsx9-%XT(@C9y|_o`u_*8K55>HiA6~4oaG&4a z)DxfaL$z+{!|N5H$5MIsAIwkv<9Ar}PuyX7mwE&F1M)L}_dMvDdw!>z*RlB~d)lt6 zuF*Vf8>zOgYt*}yMuS? z*Rbpgn>F=7j+UReme9?jU%vb$Qx{8Ze0Ol)#>_Y?~=3Lroa5$=jf@sR%LYYBw1Z*i+*&GlQVj8saEpd z>??uW?}aIS;0lfYX|?{xniYKy-YLARURd?>c(v-GtGVwq&&{5m zU%B)ItN1(b)STnmYul&sPVrjO(vZDzTZm>@R*d(TTT#-T*Vac)t-s*)YtKoaMUk3O zLS}0U0)sEydbNJT3`6taV}C2fK5^;Ye)S;ezx1^eETN(9oUd0Ig+!ZOzaqF<>~@?i zzYBkV$JeTYbMDovz4I?#<6P|J^WV~NahRUl}Gh_xjr%^&V?xnq+y{zIiJqU+T7bo7^3q^o!?qJehrF%aJ6d z2DW{98vbdz$5-r)jGNGtbZ(P)iCkj&TxA)#_(+LYca86y`2M(T-`#ti3}y-DY?gA5 zc3)_Zw%orqYae^!$J&P$I(CL}_OHG&zPs67n{(Wvc<1%=b+zAxmb{y{`9taVd9Kd& z<@Y0hXmZW@xaRJ`GUlcn^TO|GEgM*SGmcBSZQ+yMFO7wwMOk0}o+V1s^*n264|7`u&qC3xLX6vt-!hfN~^Ue1dlZSg} zEJ~Y`Y_H00F)yx6?Qgec?AfWSFSVFRywF@wXi&lE+sU2bcOm<<&>`W835?lnu5Q=) zP6^iUFjW%2AofvwL!QBfO>2KHGFs76wsN9_i)KP?Qu>MSDr)BygATN*U-)HX)|Ri4 zaG5p7uVuSV{ldRNvu{aNMciMyZDn@hW2YA%gEQXN3GVyg%X+8gg0R%?EBiVwU69gR z@cB#qyl-LK#Wv^)?ch-r(oncnE_t_>wPfxw-{z>lrIBmgw|J*lTw(D0)XMNL_RFs% zC3ywuBlSLZM#c^0iK1mm%4HLU->Z4;-f48=qQK4q>52Cbx7@4mW2sy?S@`=zUcQ|_ z6mI_D2>z*&XyKV=i5xqzkI$kOU0bs{Kj1ZPuoA(oeE_PlYF*1b;kN>3Ev*-+Bx@4;FGQ3luubx zH}9?Q8Ksl^X0g;fm=dAmQNLir^_d?3D|XBd*{6QzaGBDb(`6Ur-wDSm+|l2aP~lKg z^kZ7u)6^V)lYrHV^FDk(BHZYqFptBn>0^t`#K)XV&F5wCecPy*`{?JS$pjztw=KPV?`TdU)3d$ZZkp@z z+m!9s_B_z|&$PwVWrf>}dv;>;)=ZC+7u#duc>10`b6V%*ne&f>zMVZET;S;&W?&RzV6@4|WV4ZB zU_lvgpn;Kr$y2S#Vi_9sT$`r4tK6KHdX9mCL57imL4tvSA*nPoCq=g?H90l2phT|# zeonb*|K7aA20Sh0`@94lr_>u~oD5rV;?kmQNAE3*4@~0mH2d{q>+F`>wNE~tNG#pF zcU|3sCC)rNhZ*pGuF5NJly)VTzq|<>IR`tS5{p<(m2_8!;*^h;4jLIlPc@jvY&macQD+-FW&NY z;}fTuTZJ^@AH1}<067a1a%ep$6ef3PXmfxV6Ha`YHhEu$hqU&#j@-5PFYrHITyooP zrjrN9#SEuaf|rgQPnDdkm?=^pIU!)$mM@9V?^#yA-&y?Z-d+3p`hRQ%9m>p{Z(iIm z6*$<(D0_bDcc)5A*-BYfS=Y9TqhDhpQ#+o0TFw)7_kBUz)wb6kCnv5y>v}q?oYT|1 z=Ju<3QHMf*bFMgf{$0buD<4{utq#2^*_l}$yZM@&N8-&*@4i<}yB?TQdf)oFQ&D|j z+KnA&zHa?L|NRR!m#JZ^V^q!V$|>_4-+63iY2M7()5T}Di!6#bZG2<*H_7d(mhx>L z-KsK|Hf`%@TIYEwTkoo^&WYadx`q#$60_qDoH=-(-B4ujiz!nj*J!rBwrt#{k(pu; zSG%z?e%C(p3mcCYtbS=VPqn>i-J66=vqh)aOX}sd);`|Ql_jyu$!7((+huvK+55{v zYwYImaF0H4}xLj>9epBf0 zH`|GKq2Qkg}GD<>&zHgQD$>m?YzM<{M?aDBj9&HxDeWz4#~CAByWZUO_L!x!_}tpSd5eC# zg(>g;$vv@Oyknk-mvTmHP+`lBdmJC%U*^(IajfKAbGc4=MTo?6KP}Dtx%W04*zND% zvM9Ksp?%8ga}zl&%+#Mt9Jil!%zoJq^(WaaHb*x*RJG09I9>5*Si*MXv!FnU$0$Cv ze;ESsJTvUY(+t*0OZt)D>%VM7y1?K6+j`RnVcMJZkgVsU>}~iZ3TRdB%1=Uixs6vdNPF z9-gl(nQPKtu-<3tsbQ0-p8u=&5ARpqnKeiUZ-Cq%{d0$sG+1YEAlvekKPKn!@?l%rw0ld`axS{+^G= zr2i_!c`Q=?c!>W&yV1Pmy2aAo$EK;hwe&y#bMEym;M3F=w1|otJ_$V%*fH5e_3COZ zp&8TCT{X|T7)l>CWjlJSl-JwE@VLR38%1mUsGYtLASG9Czc4cRqf>cR1$e zrqxGRoz-3zcgL{z(X7zbg@KyVPT&8k^*Kz_Jk_Ph^Ihz)(>8gByvd52(n2-nJ7(_K z7k>NN+vPR4mrQ=RY3t#$zIoC?{CvvqxSKD@R;F0=DXy~nv@Mo*Rn(h#uI~O?s-chb zx0hbZxn5uI9=Pf0^gp8dvtmNJrPul9o!+V2X0Uy=(SBup$w|{P9CJz?N*cah|J$N^ zO}9`&>!Q*9tA0Dn`7cW;q;{%J-o40aYkKkQz?gJ~!=LxH-MhQ|iLTp=vpsvJN~rZc zKC~b&qWW83-T4XKMpZ}BqP{sFRtWlJw%+k5Coi|e}PadKPn4(C2C#G$()$e(=HEI1a?&*qm&q`+g zYB;JGo*CiStiq~jsc0ps^wus@u}baJ8-|5757iI-Ueb0#CPhx|e7Quz`-PkMEj-f~ z@3Zuq$l^aK!tHuy59@{cr3|Z{aF@F5Gp*oy5>%h_NQ1Yp$=Kp`VTL!;Ip;2aNrjpJ z8w+HaTUT05d7rz(Urz6>v{}JP26fe`O9W1e8^|@}vvD3%ol(@Y&Dq}ksYJfR<6fD| zJ46~6{|>5^fgnj!d8SUZvBlX%!6*GtA#~^N;T&b}yAyxL zT0L&N+SOGnzF=~kqTnCr>)ag2@74dkw2(Q# zX|I3(JUVGt%bouWl5O8o4+}a5cW(Nszv`^Rt!MJ8+jq6zSQd0_-|a71*SJ^anclsc z=PB!@(3ryHzj0l1^TW#czSM`&yZiDj_whVgdDT|#a;?Cs$BS9iCZu`9K0mvm@Nnj? zp!zksVuvgAw@S!;+s`>m;&zeL!NebJ8B%G#<{mV%b!v8dBxW1Q8^h_nJMq@cl*}lB z_#G_&efW-ZtYy|Z5$rK>eg<=a^y7eH*H13(_KI$W3Cjx_XMAJa^-VA;r~2djySt{@ zHXAOSt9&l{X7ST`itD!SbE-bAC#Ww_wcu)I&HR7$&IjBL&m5od^zn?R!Y4j^iu@D_ zt5j3jaxG6vFzBAjGN(z_JDlazi&md_pm*`KQhW82%$2fA^Ljh>FY)QgDO&hJ_LtIM zJ%bysettF1O;{c|NyD%G?4&8P!wgJK10VWsvY9mb>onuLY$aUBRwnd)GPL3QxOoP5 zu3U18v534A<6`+IwhG->6z(Q4`cL@DiWnvWCD<3ryq_lj$`7f(KDDW%$zaEZrN2w0 zmQ9@LVr2X(d#Bcp4~#V$hMwM{x=oRHp3J-R`>yb(e;WSj#C5|c@wV$-*X9G3 z&zqLpHvL*y?=QbnJW#OeZ%CnFS$`W#tG8tuOXz&=ve`$Km$8+IWG;Pny1~tP)sB3LF6LOuS-Q)mhL+fa$w!gkGFrFzOj;VN?=Q}Q|p4y{o#Ud z>dx*z?Q$x4+YT4*%D>!cPaV#%$i~k7S~BnQ?#u6d9OK^n?dnrqZC3h}FYN0|L*Lbx ze$85Hp^+SG%(L1k{q76N%Dubap8F`WbN&X~yIb-T**9-uJu~<4nU!6<*@c(OuG=9E zoPq+D zn}f7Z&z4(>@s)f+i(RW*7EIqkU;w_(xOYm5B%>Q(9drFP;{lr7>+dkSJyA$8|{xSQWKLLmOfBdkJw^w*9 zAvfhivSjIoLq9m{)hjDCdgg!no+#P>Al>Kp=X$O`Oy9kKsIIOIIkaA7jzGYgCD{?r z8nUIIb+k-wxjDUcVeb0OS7rWA#WHsVGv01~Gx3^WbhrAKYY~Ss-fld0%*6O!`ix(7 zXUj@mk2)c7k zr^>7}#(T++`e{>Z+qV|3HGL}8dCl%{;me$(4Tecsd8gD4nsWD*|N7mM*0+W2`@5vV zcPk!V*%JHj**;dEy_XGmw|hTusxADK>b7vl^KR>^%5ymqA{<{M3O`*F^|>@(%6iFd z_ASnwhg^QGnK4b`N>1~4Mx!J5gd)l(XZp_A7ixL&PTZY_Lu;PYN8dEs{dVG&8F{;y zcE9?3`09o`A)MKgl@n_UnO5KUKI`qb*X}Vvx);Ls=g6*hD=F`Pb|yM5>VCoMa>++N zY9-&iW=`M4r0Ujt;MSTwZ%Rv}i*?Vf;W{mQ^=+-Aq?V`tmY2D4o1VJboe<#c=XQ8- z|3ClB$T_?2dOw_`oV#bs=Sv6cPm9G_Ro*;Ro4}c*k-!{ws_&(_#GGfZKV?K&WN>|3 z5Ppz%$Hax|EZ5GoZ@Rs0>*71jaOU%`)@2-TN4Y@3N-|26m-Q(}FR%_~y zpV)N&fv4GfO;-QMl8tlkE?d>FdUvs;S*cU;l_j~o(uKC`?j_#L$`QP(E99qb(R^QP z2dlYug_twfxg?K6Qx0rfq<25tf-iOIe9Mxj3+h8ek8D1BS!L_}UkYtnGY(pEZst0d z?02xZ!tKy!uDAo;Ty@6}zPL5pYX{raEVo^Y{pw$A+n4fF|KWF`e+wS;ziGCA=+yWA z@29{IM!Wx>uW&u+x<_k6o#N4TQzmx2Hv4*mdGW4Q4^02GPQT7-w&bd0pValq+vJqz zAGw;QRK>jBqayfVw9cR0KcR>2vs_76aYQimFvyd-HW{7S2|F zw_^F~>2cHVNVZ*wvf0;Qy|yiGE{Cn#-Od)%iIx2?HcsP7Hy8hzDtvhV@&gA|EtLO% zj9bO5y(Pk@e9o+I=ghKh+InA`D#^fAQS@->r#M+D?vt4>mw(yDRNtrnZ_CqO)^6Ja zA^Z9Y#b4gpW5QxvK7A1s)dwkw(O?;=r4fBCYtC7UCI4~P`SOe*J;yL3X* z{8pHjWlF)(b>}|*Z#Z`^dSC*ZrVb(Qyhu0jZ zwG}eGk2t>w`l_Fbu=|!I|G+V)ypw(7)1`7pwzb=ItkqY3{q1qEq_k@PK6|U)uJ#R? ze5VRhQeRZAk<#m(*J@J07k*~Jr=!O**Z9f1*MI8g)XSf9be0g$#06WI8*jgHLQcTn zQOJMt*X0`{=QvKU5Zn7`O?h+azHeuEe|^m4nUKrGUQnFySRm5>v00XI;R##*q=Ffx4OJ3K#7=pmSHadUo1J!LVr816P~U)@T2cp1nM<_|@O~pGjHz3z`>9D!yUU zZsm3Sm&aj~MLM}sy%W{s)0~=?IquQ@x35$zH_Mritnc@^yc1>Xnh{Cj7) zr~6L*oT9QBn-+Nx4I9P2~lxVHbzT1Ssh{M>w?>2VOx4irK(kOD}1A(jCKP-p{NaT9t*s6Jqn z2SQdQ}`49OYcE=y^Nc!=nUuoWLv*Hb251p&e+dj8Ezw`T^E%oc_{;~xe z=<6|gmh$COIR7(Mof85ZHIg|`3&nXwOq}@Ww@934#N!X8oUWYGLUG0=xf0VJ*5?|j z6#cAa+n6H%ilM3WyUD`yDbkCKg%+9H)#)!s9uyjUky>iv0^&El#5 zWw*YRT=r`{kMzgLTPj&Qv&`qt+L!!c+ljK4`OmL#8NCSqz!ojEO)~8L4I#&Aw`Xfh z=RWuoEObw+UgM!`>C>}LhB-&ubTZe)ZHu{E#CzW2qIGirQ3a8n2eyYRVy;=_O<7p* ze&>#|jSGr+oCB9G7VFRF){2e&-lSl%;_uFyXz7sBdy}@`QB6pc$m`FaRkEx>&#~Dt z^L*%{Hy3%s&$#y<-FlJh`MTiA`pJ#^^7oq9)P5-yQT#A%@!ficw;@rP>n6;xYyZ&9 z!r1)f-6OBER>etE85$C8Smrxu1-|$*_r#(0zkEM)x1ZSDR^eV3l%+jC_rt{vrG z@?&3ho{LR$&8@C$-7MmU8W-lW9PHmEW6&PB^9x7+-^ptpaC+YPY&kEfr)K$!q{k|q zGjpx(ZvIdrf6{4>hyVGQMLFry<{4`&pBOH~->^0z(?`Ja^HR@AEj&LYT90IJ;YvIg zQF3k*;{w+iDu-ll3ZMA>BlFCU*E(_+tQ;$Sf;BfyxhcLyX|htUm;I)|Uydsyj=qRT z?JYb>x~l$=iGd-Hoq@p~+`XNBO|E`#=;TRSvP3UEAJsMMjf zbZMwaRO_aM>ng#IU!=6n@|hCr^yKKlDNUJYTnbIQvU!SbKAtCc zv|h&fk6`)fUAxtocwRBzzIHdYd;00BnTbh93w!50{IqE4bFOJKqszQh^Tfk_qSrri zd-dQmkK#Kio|9A4|FE8qykl0BJumwfo9t2U_K3EJi_h5kZ|s||Fp>4L*wn64|Z!Xx79NhML?W^1OCQYa|nwz=Z zzbje((!Iv;Rud0{w&nrgrr*@!E$D;H?y&YEG zaVee;uWzkZQaZHn=bo&uCJ$0A`VTV9tZAAELYsOaIT4$mdrnCB*Y{I=>2J@&q*#c;lEx8JA3 zBAv-%SrLDd&pk2;t?aZBO3Fzqv|aNDt-GgGbuwxaD+9w!UWUnnJ47a36tABf7F~Qf zOyJ)XiPIZhg?NrUlv8L(7QQ3KG-XKu$94ygh9#cP3tTsbx`#}RiF|hH_ujwUd%moF zx|DxQ@S!DVN`4)2PXDF;%lTHFdsxcNP2S5Ucb=Pfb>7|l^8L1-#Y*Y?Y<%;UYP^K6++w0ZYo^-8C}w@Im<#&ch?tIFLH*dS$krD&$w zY^l#+ZP0!N)gA znm*Eb_2$q#@#Hn@7xsHQ`PUX~JHUKFr(aC=%(|OlR}|x}%sCm#l( zq(I&I-*zY`YRD>`I}r7%$fm?^Lbk8_@5|Xn!Aur2i_|~cORKzFcD3g9O6jdzi;gCw zMLXr64=Gu^fRnA{QhoW{&5K`dSZ}(}N9bDS;SKLtH*Q4*-}-sif@=R0uO@t0 z^*|x#(iX9Yi=yIEL|Cmidb>q7Ht*&7*5U4!p~dy?(Y&7DTyhCM2Y>%?sCOz)_D`u< z@j$ue4Bxew{@1D7tgkM*&GjvDf0O@*l}*e}zj)?%zZL&t7@zHQKS5r9Yc$s-iG+vG zv&&^ml&iD-`t`sDiK|TulC5&IIb=h5>A*|@vu;F^OoTHe0A_% zm3{uayZQbvjY&yLX{IfDQ4BV zTh}Ya=%>zeI%VU0`%dr{dq2wuukk&-`mM z^V#h1f*C(LxZDj`c3kq4QT>!WMRZ;0*+TpE;hJ+7)KAX}vwYWb){61%#_S2j-5OJV z-&qi<{(28XsJil%y={}{P0o&C<*=MAIbJ6mv%r?g)hHv zoL6*s&$;Cs#XVAqmnYurIFhwso5KQUn|a&`?>;bjTHL*I@>iN4-|io?EG?S@&!3zh zcj%UJ$)=aFkJrR~uP@e!+^Cm)Sm*ka;w76;-}oW>=;ph9sX4RGs+o6}AJTZqbyDuU z*;j$Rz1#M#J887-3d1}F)q`QjZXD5jS9P;JR_8)pS<(baxx{(v1)oL!i#=u;qh9%} zVB&r!eb!U!D>;uAbeBDkVVZNCRex*n!ML*aRmKP7{I8>OF7^`&Kpw9Cwk zdy4$_waZktc>M0?b(zg|W%up%-c`C7SI>(+Kbf@f$+MYv&WfFs{V>V< zw~y2(hm$i;2rqFumv?!l)B)w+6~}7jSeNVHw5W}m{E~T{iEQl7j~nh4m8jpTPPtUP zs8ZCEQ+>~lZJ)A)lG0djYg}8m!f)LH6Cr*1$yYZrg?jzG5p?fpv~>EylfNF-^{b#p`<$GZ^UE0-7?w@8pW#p+6zeVRC}O)ga&wul@0MAQ%6l~3H1kB_Rg^Lu zM4GrJ+Wau{YpI&`0(4w6A^a{=W^UNx^rRuovlGm=1TKanlz4|jc9jVQK>KFXgX9MQ~zPZVTQK5uTtI~-gURdc#UDm zzjoi=sE-ciN0)B#*4+14<_2f+499EEtq(JPg&K&&#qN4*)1}Azvs7xj#Hq>di5k92 zJ5{R=>AjiWnw+i_Zo7-MCB-o9Tm8YGo{h~nr=-;WZ{!zw?#A-t2gi;rW;#oDboguP zC0ZU`bXHuaI9127&gn(Z(VP2^b}Jw9u;~rkIBnY6<$rjy_)n$2$Voi+b)veCK%JUP z;y1LR>*$KS8B^F97>)~07TBRzf3sdXT=bvY?Gu+=HijPWyQ>y@*BskB{f1^Y?rR z7uaWQEaiP|hoQdl^Lv)xt)JJV@4RIH@Aq+g<{t+4{1*tf@fbG>^oTtPoK~pWxT1dd zh9irW4*M-QdUvJT#+K8bF$XtQ*eE}al$`IqK-7)t_Qx2Z`NvZw&#QlA+9zsPVShrz@9#@<(6G+q9e4d>0p`R9@RwQos1b+?Cz0CRs&n{d(!amMFQ27mQYm z^IlySHLGQEZ@l~6R8P~cX1xO^reun;cTbDb$ZYZ*)Z&f>yAQ?B4$3+EQ; z^z{f@hHl%^m3HG@*wTwX)}#eLJ$P&`Lo?rmrMk1cqYkorWrpW?%Wm(w>iVd7@m9Wc zuk{=+_DXN7H#^wZ`>HLk-}A+pYnpR5n0njBUvm3(&ZEO;2CHwy`44HUx}TfA=KSdE zRCQ`jxOaMpw|I8`=A%#NsI@pv_T^NpUElJ}>b*?zw#<*SgUWuCFi)LuSL<%i;V*N*ibTC$RRQS)T^5~Hw>6H>!haJ*YxwlmB7MV3{;l2y~WW_he# z6W;eStTpPA<;0r{J}_yQl$_qx_-~ueim6eyZ%>$If4MmO8tc{7J1^%gpQs=wZ9Xkq zefOqPR_nxRUbprf-fppXZ%Vz`@-3=spL|bu->|7A=0UqJlRU>ZNts_ed0nn9x?RP- zRn+wNi?*GU)`j~2O}Tk}%T7;*+gF?pI@&Crw(6GJm(4MYz2AKodv%?o-pb+oyQlx_ zTUPII-kP?4dFC~i1(&ZMSrfVS+1BJ;86W!8e*8U@$=d$Wj5AjK<&LPx2d#?|7=G8! zwk!Ccy(gbv!=i;*=w0N0tGvipn-n4rx_=0i3>V$gT%KAJe(?K;T2}i5{HiJMo|zkO zGYu2@!(l(|qp;Z#w|A+3LJpq)P|V4`x6oqF%au=zd&MX2zFraCxtHxg@N@l56GYEE zn9?Nrrz!lrT5xl@tKH%U(|?GzyRC21^wFNdQ2+Ca$PG z4Am9qh?Y*wn-@6e@UHT$8$+aKEqiy$+x&e|MA5G^6QbGYu9Y>M)cY3}Ru z^R}@$zdPTPThA-an;oXwYjOE^!8XY~mFgFoZ?ImD3h6Wyxvl=Izh0a+rVbX{2ocTL@!(MIS zb!W>{+S^vG!sEVs(Y!b}kG`be1v=mDlwvooJ0VrpQdsu%-2Sk4d&=jYYcV|ZoKd5u z%=n4ypSAZLS5CG+TW(bUq}s6aw?TL8hKQ9iO54$f8dmf6@=(iYaAshmOx(ujt6w_ekP_N57!!A5Y_xIdWFPvp8RbIGG6P=#ERyO6rVU^lBrDbzO{EE_NnX1`{ zv7~Qg?+@3(BP7z--eJr>R&2^VKZgq!wpK$J(?-CDH=KN;3QDN{#CDH#L z>o-rEr%zw`?qg-HWw{-EUGeT#zRdY2HyX?B>B>kceR^*H;nj6qRvwF1EYAzt*sk(s z>H582>LX^H6WlVJkF$AEs&U=jZ$ICCc^JIr`>$Q1*7Lt*{^e1PD~tOhx~QeKFT>et zx4FynsO6vQ51+gJVy5@|Zi}mT^trx?RsEk7zy8)1;lxJYWr6z~)Wv1gjr9`dI_ueM zZd;SFSK#UA8Q*6}mCRsXw8+zJo6E{|s*B_nNV_)r_qyG!*O2?VX$6OX+r-qbG504N zF!KH#6w4%f-;vezxT68XU1yU$EbsCjq`FOsy*D$X*2HkbyZlp}Q9g6DmS37QUsvfz zS()`^kD?`<$DfNt$$A~R+5KZp`fuLz-va7$%YJ>GymqzxE~5=?F&(!x1GmR~X+Og9 z_j%QeJVV>Via&MkR`wZhKk#|ZZ~Y(d-agW(y`i~`rFwsukMq9M&V7rOoK@M2Kh~oT zC3QQR=1jIe9K|Fhym|WJ0!BelyUZvje47^&0|N^y1A`lQeCfj_)%vBuvH3!dBDTgg zIuDX2Zu=2=v}>CF?xnwy6nedqoK&)$E`Miao0E1kw`}^c{g3J&9GyPvM?%dvcggoU z(mvd5!p<|QpP$?L{G4t1x998g?=zZwuwiEQH}ZI?+Gx9X3U5fsj;sZF4^}qTwxxJm zYFQ+8Ox0HB`MAw`3XgQ`(-YCc_E?|GeSi9t54&DhC1=0sT&TOYajniFzZT;w zq4pf-r^jXTrcOR6(PF*pSlrHPt#hrZ-03-IC!T%kvDDe}UF3q8NN@?8wr5vBV^SAP%Q(x|^G@p<^MJ_LK<(++gMiUR33bSX-uwc=gx!Wr>NjREq zp5dN9_YLEd=9?ecuyvJk+ug5OhB!c(Z( z&ivFOegnRBrHkfzT6gR4%6GmB|DgY*ASKiHm4g2xtDnzg8hD>9oX_bgq1w+Ot3G@E zReb^LTR(e_ob&np@n)0edO@336@TT!#s)hjH-$Z)DY*LEwm=T;r-hSk{K^dqxwi|R z-n(d4`J!3*X@ZO8C#=0zrx3XOKkKB;Q^obqHFgU{-Z{E=gNvB;laDeUok=c#nsoZ7 zB&?pQafXrm5R0JQlFt%l&VQ7rIQ^C|KV)ZdGS0d0UEO@0qLueilVR)9E#m1y3=GG# zCJXEko$N44aPpm{9QE84Ay>D?X@9nuJegT#B15x{6938MNeoU6AB=-bu#)wip% zqhId-Hgo0~A3n8jj~~CWtzN(P+O5|g-)}9y_vgn>VHcBb|7Vx@e%1f{Q}1dgfAaZy zslU3@|M|(kezGvw>TSZMm7S4~W#*P-##%|fxpwn#+{|kx-!kXSK5q5=Wypaxzn?Zv zk0)A*eq3R5?s=1q-V@^|#!-)z=JI`zkng{mY14oC#`?=Da_=7-KmE?~(?_Js(<=7x z%J(ofE6QN%3(p5>{3TJM4`ib*I7uU}au4b)= z*1>k0UHrG^mC4ONT>K<>!N-`nV#QmJeJ+VQ_1QFTL1C}o>57GZTlaW$h-L3_S^oJb zmv`x|fHbksJEg9_D%ofdbF8=~Mm@~ZKW*CYjO8Ad@pj@bY82N`xaTSrKga!b#nS%Q zrg`-f-f0Q$ymdU=<-K>!9gY3hvualQg>yVEjZxV9^-)*D7bdCrXOGoxUT2#dRk`?! z+0Tm25zmX}ZvP-^HG9w1<7XFf{<*kl^U<|-d6Q3;^)~w)Nm;|uzBVKFM9nb?Ib-cdm;{!kK*&UkqbIF(xu$=A)2_0&x1LTnG58stXIwrjH1=b{3azZIk-Jxi%`VIk zjXPnYm889UjrGao5UJ9=H#ukBIhGK8qo>?;H+R38uV{QHEY&z-hmlb3>a6Zr zx{9k;S&FRjdXt-KkaP2tUJTPb)-(-~o1yY|a<{6SIeJ8T1GDM8hx%vgMYcuE4P1RQ zWM6}b2EX*JP0bUvOt;uc9WjWQZ|Li!xoX-x@t&E{&SE<6Vnuk^AA8S;QC<3a#+}lM zV%}!lTcvhcZHv0N)rlieTls8S##~9>Q}2%~u)ZhPA{A_xn7=LErN+~1eT$cpzJ2wC zlb1EUXFQvlBa?IVQchr3z4t7Kg8CELaz8i^^5rnhJzZ`deX`+*+_ULR0@pUEZ94q0 z(|F1u&U(YMl9!||v!(Lys90OTZN&C2uJJ;b%HwdI_Y3wlw4@22J#@>%Y1?TLKFfod zCpK*92>I=JP2Yz%Jx{ps`pmtC?Axt!mrR;c zA8DC$E3vZplLH%5qtHZ=)x7*q9&efC*fWPIF1#m9ac^|H^claLJ833d<-452Bzzr< z&2nmreyJ`{71d#qYK}N!baCy)uH=K3GnKpB+T*L1C!AYqxS7}Q%kt`F+cpae=B0}Y z-S0?I`0~PI%h4{i$tUDQw=(3fJtcZ_;vbW(VfAdui&kduzRG*!MrPYCg)7Oej{|@1 zy_45%G_i7))U?ov>vpg#Slo8PVUDxxD~l5;g>^*|Cd$TB+KT20tZBS+AbSD-cj}L!tu$Et-f#b5D?t9LM690Rz+mftf|N6I^AMiV`HIq zLMSD}~0Z~Kk&=VEg%eHRo@;}Baw8iIa$Br;fd86~+8QZ!~ zr6haLEmxU*_|BE(=U#+py-F+IEZXfm+qzio@|Q^q9T$kOI-GkqlUe)u=~KmRn~MT2 zPDSf9Cuw_ zpnoBk?XF|FQLq1}xT~V?-*g>bHtl9&_XUf*3;a7yFF3qMSiUO2?mL43yII9T$?W55 zg^#RXXmFQj?z#OSZ{MW_?Jo_NJzCh;?D%HlF{${}4|4akdF++vub#Q8-R7Ij>!`@u z-ebRc_Dx%Vxnlpp`A=W-{*$?1&oJk7pKf#h6XuTQF0N+#^X7)3JenqZUx`JUKbBar z;zq^KM`xe1-9N<6vv2DDgb%rYT$}TsTQg*+dM7XC>^eF%mF@mX_SP4A&F`N%Gvu1; zUpDe?KayG5xv}f))KrhtCKkS7vrjL+=yB!2;YD9=9E_Ca+3MkB{H>I~{^Wz|pLX@l z@5P;M4wOs$QLs<>aJc5g1O3akZ1#uxvuch!SkJLNhn??~S+o4(^=iyY!uwV0_RW3x zU~Ay%rskPxyqg=OcldRd?pK?CoVTuN|GYctOji!yUOO$M;GDGD=QrMS4*GuzmW*E6 z>vlHJ(#WJvs!>qG?8Wpi+Rgo4jPoC+^V%Krug|=be8GR`^#_k5PX{mWI9MxG`0nM# zmk)amY&N|0;Q0@>=HEXnIcBcg^M#2?Gb~1jo5xNu)8d$9uW4Q2i~WsCs%zbtZ*7~Y zD6%_N)?!U?t3{T6_3NX?%Z{gsuqPi)fnq5qElyb(ga~etXndD=kE-g!}HQkEhx8A6lRFXI9FQ zuRhZ4CpTIM@s*03yB{hvmS|fv_idWQ?M}AY-Lr2$+?du9!ShjbQ^@92x2NiCYu?uM z#+(1(4NP%5xol~*S!rCTG}HaP(!qPXbZ4LT;;ox3xF@)+r(QLN@0{$Uj1$kBS)Dgo z-1R}FVAQDF7rWYUhGbN&__6!~E?ed)sowa*s2|9tsU zHc0>Po|k<`*Y-K`bABt_Q?Ovod8-wnk@i&=bnBaUJxgCGcFK0Wr0|I!y zI!Eqyyr*7PGIL>snB%2bg$TnFS^xL=n#{R)_U)rPcOtG|i%?qB?GTQQk-T;MDD$QM&?+t!?n%rVm#X;c0Y|h(5t&vKz@C}y; z8>wHfiMm^IpoRyO#9RmvuWM+b?eMlo;g?6WxM=tlc;5lLxpRt2lTVU%%AX6rN*Lt#HF;?ZR~uTcqBm_{uCf+;jKH?*mQ# zOS97gtQRTAtXuucD6Ks#>f`*6<*g#?T(7>ZZ3vXrI_CCZwP@Zpqns4|_1ndDOCH3` ze`2oHJNwUBjv1Gdjy46ZRxRZhW_fX_-E#I0k=5+NYs@CQU-Z*?y)tXURMvgJWrv1Qhn}excZL!=3-~bi?%QxrJ@)$h zw+ki$Zx|1jxCXrZ{zU)X!p*DqnxzVAq;af$cKy$fr~91N>K?edQYi(Z*deSK2k#&u97&aEiJg13@TQZ^?)GyN%d&dUzkj{ZvVFyc_uhHpt1d7~&b$3! zxPI`u$8`ifh<6k;m z;N_{*&r*FgHuakqwLfE5nHQrquP=zTN*~O3}6}qv0;upoDyu&AS*5A(5E)6!0 z_0=sAj`Y|V<`OAw>;3kN5bMj`TYBr*R(;oSkq=^@yU3t_0Z0E*zj+?kJ7z43@hY6> z9(%{bx>EV=queWJ=a#6xU%L7S!`p}Mnfms9_m5}D+xuVtxOz+d{QJwKe=zTwSnqHC zS^j0%Kkd86E^7??+{h<9Nrn0mcb*?elArWfbvEnng*)3e z`Zh`L-Lhcco`sKGw%!hXtNBS}r@WJwSY_Jv3g(k{R-HUpCjRWgufK9f-s>~fZmI8B zx$qF1uvGNK1Cx$688pg86+OA~JC)hclJ%OJ!c6{cGg)?@af#;DU(+am?eKZ&2j&;c zh5wjx7A91?x~tR+uZ}tJN$8hY#+?v{%Ma(PZ#Znapkij+!5c{q>wcVBYN4{RSa|QM z4VT+b)brju?^$_DuF_fZqwlY6xq{cVJ}}SvR3H6!%a)aUrk`W2xyBSW-IS5_b=Au~ zZCxV!zFjPuv1tCe_Lq7q3d1$BuP_`I`evcIx5aUf*=+9Cvs@bjJvw^!Yn*(NWDwbT z=30*4&VP4Urk7swcG#k)5cIaLVcCybKZ(okUlYn&)cAe;C2VS61@2$J;i1LaCXp2z zqHgT-s_gn+pLmJudcuLX=fA$LE$Q@rk*=ihWI@Ih+wQVBPPV0YRbHxYx!h&boZ%vsSv9SNz>Ze{mW;Px=eMdhacrsJQm0i8ITLh(}1U%lQ3vUZz zxo|tCV&`5>f40iW)(0=fRlHJbc{pu(uwsQOledGKIOiL8uIcqoyKj0txXQsToGI-# z>z&gquW5Jr*ZL`Z`Nzumt+?glZ;t8r6x_}j3GdylEd{<-^{ z^ObGxFVAGyt#0_+y5-yVEA?GhFMQQFbl2N7#d!+*CSKM$DgU6%LY@mZ*mnH4+uOa- z^udZ_Dk~;uHs0fZ(a!NRrfz1Pi@lrBkDrmhI86#J=Imp?b%Caz{;8#v=ig__*5I}GRkJp0OM`6g<~bEZvyIOc6-JFqn9QrMS9HKk&eC1MxW zGs^wvTTm(Q@Oi!l>woU4xAQYBYZvU<$a0G%An&jCU+Hhp-0c~ES!mVwmM*D!e|Q^{ z_~{q+2b9_Gw4R^)a>4o+_08AXe(^JGa(}tKo^wKOduqyc^;55z)u-)Pb=UP>C;!ED z4il@(dfgRQs|R-f_`P!L$_=^`8Ri{(eC3|EV~EA1C$}ekzPp3;v4z2rp7nzu)fj^Zk*R5v^f1%3@{q z^Fq_Zm(T5%`z4>)arD3U{_==7r}Qq(75{%Qu;|Vj`f{M z;*0L~T1T7T(!QwYTrGObHsgEK@$?IEYya$M{{3EtTvD0f&r~Co_RGaOq z7jK%_WawLU-tzmM`3qL-yM&3LKq8E&{*vA2>}O)O+esE*N- zS9P0D+OB?7VRpFUs5tLt#T`#?rPbdpJ)<78B`3S|etN57@#&9q9+(||QnKl{&s4>| zucBG>Jr4gW+nmY%(8Q_~ANFX!ZpHfNsR`Z3^O=r+U$X02<+g~@ z%GIfpv*dbYx+T>V)gHape4R9T|N5KKjN*>BPZ$`*Rvx`ybopuA;X52Hk4?kU%p`+i z>P=FLVkh%3?-O@kvEz`zm1(yFGmREVnN?4yKH{afs9X2@{r*qqf8N{m{_je)@8N9a z)mNv<=*xK?4P`S}Bl~d1#4DR_Zj8( zJvFhX7t7D;Qf+(c^2_-B*5~^_SIxP1BQeRC% z9nCE&!zGvP^82#kvzF>l!@kui<&3dZ){93ZJv$>6H@<+cpb67F@gFg+`70 zubm6)-MrFne1FNXL6XTk_d4GO0mh{zeJZU_KJSoN_}3x5uueW#t3EklF3;qG zS?u+}Z~Z_ENH6nmo3b_b)O3wmO`*4RQ#Bu{cN~)vR%R6DVf)YfHey;%`Ruz#Uo8H? z@^7k(!o?%(9~SnMFVl9q&GMu8`9166cec;Zoq2C>U&pxO(M^SQ3#O za%o2U(nLjG-qg%hmvojDgwKn;R1kldxy|DHY@VH4>uvu{jOHt!eJ$<9?Y!cdi3hUV zw>_Vf@wWzqx$#tA5wT?%DG6!trXYGu|D2i4s;t5)+?I?9<^%id&gc zu_jqkn@9NW()sUi`#JxxnwJoIoY%*2Ls#GH8J9jxSy-g`is#t>U()r~)r;he&djkl zt~%{%nYVbh>`OkUckKtQt?#5tye{6k^!t}nmAiHCuw8sxzCc|xbJ@yg=O4G({N2Ix z_nL(7lw+Q8ett8rEjiVDVYc+rjMP-z{5ME*qX$ zp>aB4&ZnPzJi#X|& z-bwC<7S4RSGx-SLt_QQ4X3tetH@;vXd)DD_&#cBTc?%nMOgxl(=VhYXqg^vI(`Wdu z*1KeQMl1Trl3kM|?!L2)ag{R9Y@PUv8+E3jLhVsV0t*9!6!&ERY?u0SsZf!Bd^|oz zGoLjrG*w&c$s@|dyLMZqu8G&>D+woES9WGu~ws{C?fj>iE~(xiQ}1 z^Z&~J<13xtsF1Q@Y2=|tG2d>czt6A#J*W1^;~olA{+<29QWpMgY#VG_3Tuw4TkokXmMqKuCe&;$b2CX`TJNG$29qw9lXcK2v_q91EH~(8xKk6+@J(Cc1Bw6ykv*5(( zfewMkbm9XyYA-qx{>>%B=-WpZmq<6(T^!DCqW4c399(}uSJEttXF_4a`;;q%CuYBw zJXdm5Te+#ys&Tniw8%GUgSXe-UGSQ@G|Khr+i#s|GwNcuE-aF+uX}OMZ^`^apSd0% zzZ!locujJI%gdM@`iBp1S-tv+UfX~5j&-jOO?Y#BTXy{I!$<#_tlV&6^`nsBM~VXP z)-Xt(eH5OzBl?iRf~~7GrtV$GD|^-S!L*>0s{gKC-SyVyF#n6ht8+cS2jAHDYSA+B ztG|xvId2db&HSfwCF6JRhU)V9N9)rrtorEe`>jIbWyyJUONn=?;%P7bAGQ3@&v7i@ zcI*2KI)1iyeSbWg-~88!nO|+?SL;Ix%MQF>ySU-=4h9Y;eO^EN%dgTa{jJ_7u1nqG zbt~?bzFY17gPR}E%|9^vuHXvcO}sy@CGq~ao@62_Epg-D^=t;Ft&>?wy7oO6RO**G zSYL8EO1ROO>tg@JdmNz)4Bn>gshA-4{hxZ}_u%>6Mf-Vq%oJo3I}-n15=?2Ezrm^5 zm`DHNPE8+<#)oWfVzrL<&r5V)EcwEb9G7E%RORd*$9U#OPQ@3(a^@exvR}Rz--4!l zir)Sx`jY>dx&5n5<{o46%zL2RzJK;&%}-Mo2tH4ks#7%kQjN(s zp~t)|^S_?SogBIHZKTttr;&$ro$kIoe)-cEL%YIXSCFpJ0L_k1p1VhM4-*4}0xJW< zWcdR|_0j&qjw1iIZP$IPv@}M7nvEMaTTceajf1J#{_jsVEqS!BtDc=psQOG!VYuV6O{c%jxc8}L zWq8YLUF{buxeh1AoqoubwySP>)8>;p&g-lCHuO9-TB_o8NGLAS(Ioyxh(Pq#*Hi9< z3yRfW%*rW#E_&NcbJE%yrfGRb?k9>RdMd4(raaN!mi(ga@GG_J*~>1(8y$c8HFWpI zgs|A4Toc>G=n%d7%!2%2sQ`l+`g5kc$oUDl%&~1OnKj2&e9wWXSyx|c_g%~?I2~*- z#lC-1-{dvCC;xf-7q#zPn3Q!yG2=lc21C$*nQwtQ_3ZSmPA(O`Uz*(WbFFKRrg zcd5L=Pmo8+uR6J^IbqqW({Eipt||9%_3M&J{~t~IIz{Ip`*Q0^j}wb7OprOMW+)mJ zRpBO>89rI%v-^wQ2c_lO&nBo@D(?%Bm;Q9}u1Q*rWJ$X7u^$ks+65qc2*}H4~?jJe-Jm&Xx=~c07;7^?s zJuT|eEZ0Z#&z(Cnvv^tgvv>dgeErMbP`O5Z!Vz9xt4*IWofglZEv06-RFU2J=oU6n zW34n(6@%qTajp{;Ba3JI*1x;rJuTU&XL`&<4HK62x6>A;MYkT0sYt8+%%3(@@tOVh zJ#S*kci**;Sflrpj!+Htnj2f5p{hCjw5cE!=&m zRrRmJiW6m$M*VTWroGPD^Gqo~`}dvYd-lDxSaxH}&gU~*qWRt$-Jak1FMHR?`ukzN zpIYiW5+!D8z5N)zC!u(GpvLb*MN+GGF6Rl*3x*xWR(Z)GM;G;}>a32qxUtK$tu^3jZ~8vX zZ8{HsJ`FSVFy{Hr?Y-5S$5{Qbjg7q2jC=KOz4&fzJsNy&g5rPu^p%g~m?hK>hg)0U zkGs3$sVr}qPR?~nw#vQ#9`x#0wBFYJtbaP><B+XO!y@|CCEc=;?#4Wp>$goG?{K)Lukci*HvXKppw(%ab(){F zCp*s*;M~OeXs@f-%OClujYM@851z>r3O(!B`)YawiZF3;-rN3iSB|Iorn^)AneqQ) zsGqXKcOlDUC-G@>D>uL2oA~Vc_t(+~Y|qJ-$j%X(qY})$j(6dG?&UcL|4h+*xL;!3 zO^c_FoYk`al}SwNxdg5HPq)-d&0M*&*|?L>ngI?}8w0%A~&}_D$Dd*B#J2$_UU1GLx-P~jCBCfZ?MUy&%GF}|0 zR{z3QxIfEg&D@|VPpuyM8*Tk1uDbJL6=%%-brCJ6-=3SjzU5s#_o1_8kDA4P_GoXu z88X`;BcXJbu9L9t?6)`9q<%D$kv82BA981w*YPmEf`}v8a!)vpAFckxjv6}77sZ~v zVrF2dnS7wZwLUjkI9#MIZ&N|x<_r(ssoQRNG&{s`IIeaO;1--9)R{ZQ?8bKAh|5<~ zuHVdARlm@F;r~PDwlq&$`7JvA*V6xs?H64xE>GrA+i?SiDDT^Ng=URod;12-aF2H?3|%fyoTdOJ+GuunB_IL4d)7j zkFnWJHSF;1o6{q(&Ek{HcFA9F-+!$tX-m{ekD3~CG^l)a)T)@P!I7K8f_v}I$;;Rt zx_Vutq?7)uwmf~qHU3qHHEyg*eJ+1hHNLv5t2ka-{kgc^Sk|WV3Ev&oSn(M}Dq$JtY)mzf?)o>FsIGO&KDs`O#^9XG!GGH1Ks zzIPk{N(uiBwqJ6O)-}GD{3U$;VC@fPX6{QJU(_$H`+3Em&L&hxM4Zak~FI;qEqRhlN#ru`F7FEm5d6CxninnJ|XwODdl^a$@InDRF zE!+R4PLJr`*T(51lxsL^QT;@I#y1LWEV8vzo_Hon1qrA`?yB5jv)1EEvAb^Bg0TGs zhsEX^7>ZxwI&A3u$~e8`zbNhaM-9M%RhU0Ehx6O=h zoLiLjSh7s#imnOA%Y6g!mvV7Ezt{cJ_+?k^bvHrbkMKg9 zT>m-sX(i8ETkdQOSh%`sV#ke{{}q<5w%W(xJe@!Ku0!vfi9*izOWF24FKPN1abn@R z2~R?oU%K6LD5B=&?wpk`UIag3owOnD{k(4Phd-S9=EN~iaIbm(N<$_8> zz-s+AtBlvV((~sFulg_SyP^7EtJIG7wtIi{&YUqVPlKO3vMID)*z8-hFL!n2o5dLm zU2jA$vwipOena1{=kFAh_C(ZuYFT^q>|Kp%InC7)-5CYT^b1l2n<5|X*Jl13Veo~c zg0uWaQN$;=3HSaoKCYM6X3P1Pwq^3E6C3CGGexXpQZsfmWYgN{<8W(EYQ~E+&9Ac0 zu16cpHaz1dH0i*RTT<^DB?@HulOD8dILG(7Uw<=ePMr08>w_)_2>kPl=2*h$#d^d#DRqJF7FNLx6`G-XK^hUR z7vmQmk<#Ft%=S^VhT%SQdP%^{!avV@`X4!4yz}G|@t%Fi;PL#;)n(sn(#tpRw%q@( z<~N^0o2%g24wFklq8FPwJT^J%w-tCF*wAo0##qbgz>EfKrem%Zh5juaznSDXJn9Xu z%+9~!q;TrO+)KSND(NRySE^1ryL#f1eH#_CS8Ij_i=`}HAW^tUF5L9$p5q2rU+4T= z->Mp?yRrMbu3^QbU#kvXIK=unT3Wm7V@TY|$w%g2+FVrUX%@0ipUd`6Y}>XY*};Zt zp^FpJwuN;~;JMVZOw-LsU!038ve)95gau!F{i82w{JQ;~zLhFGzPqLwrXKdmpS5Zo zr-#v&TYG9nHGL0N-JBgY`TnVid)7QvlDr(SHD*uDrRQ%dCCz?5nAR)fA0sJOYk7B* z`f@KPZ}HTt`}XDvi+`BF$n!e=)5E!kyCaW2tP+3f-J{>k(-?KmutT!;^a-uWTI*-@ z8O>&#C&yV|H)rw#=hY4FLTZMp2~mf5;?Bi1$(p9Inc<(qP zJA3i1(me^=m}=ZrUF&;IQ?K)s2EUxP+w}Z&lfxfY-dZ(n#v5tj+nW~3^sbzg^x;XZ z)6;h`uA$ur%obTq-W<6!fTPE4rB{1aMvvWY(^*T7XQy#>2ZLmca`$Oz4m!~<~UkUJ2x|GcKNntf#KS>-Usu{dnFooc8|r^f8nnV zsXMxg|9u+$!uCLoo!y0w;=@OuOP_shcFXtjJ>L+nHoL>3Jgwyq|9Mo4-^$E=X0@%u z>KjYkG)MN6Rvzx(Ec&|}>W^MbQIb3?y6ddl-v+jd4z;HF4}LNE|ImIF^P}0k5~7@&ECrFc=WvCjb+qL?k$^BS#D^bp1Yv- zVxu)@jJx^HgFB^W)ysmm~(!O0+7!6ieWW$rfV8uu8wO$ipVqhp>VPJ4UQOv*uQ!Ef%SzMBu8vsAC zcyiN86%LS9&)-B8PcAqu!m_2Yj0LQG?rCYTxhyNI?#3`NFa)wNFzBL~o4`7GqO$bl z52qBs3S}O}-ag98!0?NUfx#F>Av*`y#M_bzlUJUCc2B7XyO7GmnsN`qZtpwMS!V30>qtRn_i-0Q^y zcfz@%*IlCQ3=DaE3=C>0iuX%^72BPa2Rp&y-nP5a!VC;=G#MBaP!viifECv3u}^k6 zBLr4__kd655f%o9shsE@D>Mcx_G6Tq+;s!uUXN?rRW5TeFepnfFleBd++YD#{NXJ7 zV3UzAK27-J^smmpo-xas4z1ySfD5_ z4h1W=JIg!S%~xXb-!l-?Q7-C0(NG%>)=qnhg^JLoh1?gCSTWgxJzJGjVd^d3yzrB9z05A@X2yOQTB4ovSh&AgjR#T`FDx zWypV7lP|W*OjalmpPX}n2W$+=>5X6mAjFo&DLIoJFMER($-s_!Vq;)1hscA;Esfhs zCO2Mj1uH^1A_puDA+|K$ClUo{(E}vX@UUG6yw#sB)Xj~%?qw9c` zL5MAlb!#Ro9?*j)3X~B^h#D}trLko_INoVLn0f#dE?20z?BXV|C* zH+ovWz8UQ87Y))FKAOy6Jrfi~f0!5;EWx%yh%JrMJ0|/dev/null APP_HOME="`pwd -P`" cd "$SAVED" >/dev/null +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -85,7 +105,7 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then @@ -150,11 +170,19 @@ if $cygwin ; then esac fi -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " } -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" +APP_ARGS=$(save "$@") -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" +# 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 aec99730..24467a14 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,3 +1,19 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @@ -8,14 +24,14 @@ @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal -@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= - set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@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" + @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome @@ -46,10 +62,9 @@ echo location of your Java installation. goto fail :init -@rem Get command-line arguments, handling Windowz variants +@rem Get command-line arguments, handling Windows variants if not "%OS%" == "Windows_NT" goto win9xME_args -if "%@eval[2+2]" == "4" goto 4NT_args :win9xME_args @rem Slurp the command line arguments. @@ -60,11 +75,6 @@ set _SKIP=2 if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -set CMD_LINE_ARGS=%$ :execute @rem Setup the command line From 8507fea2718141075de519a3f1f53c99951cd368 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 8 Aug 2019 18:57:20 +0200 Subject: [PATCH 0048/2244] Record a packet with its duration Record a packet only once the following has been received, so that we can set its duration before muxing it. Fixes --- app/src/recorder.c | 30 ++++++++++++++++++++++++++++-- app/src/recorder.h | 6 ++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/app/src/recorder.c b/app/src/recorder.c index 402f2530..0de05d81 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -87,6 +87,7 @@ recorder_init(struct recorder *recorder, recorder->format = format; recorder->declared_frame_size = declared_frame_size; recorder->header_written = false; + recorder->previous = NULL; return true; } @@ -257,6 +258,19 @@ run_recorder(void *data) { if (recorder->stopped && queue_is_empty(&recorder->queue)) { mutex_unlock(recorder->mutex); + struct record_packet *last = recorder->previous; + if (last) { + // assign an arbitrary duration to the last packet + last->packet.duration = 100000; + bool ok = recorder_write(recorder, &last->packet); + if (!ok) { + // failing to write the last frame is not very serious, no + // future frame may depend on it, so the resulting file + // will still be valid + LOGW("Could not record last packet"); + } + record_packet_delete(last); + } break; } @@ -265,8 +279,20 @@ run_recorder(void *data) { mutex_unlock(recorder->mutex); - bool ok = recorder_write(recorder, &rec->packet); - record_packet_delete(rec); + // recorder->previous is only written from this thread, no need to lock + struct record_packet *previous = recorder->previous; + recorder->previous = rec; + + if (!previous) { + // we just received the first packet + continue; + } + + // we now know the duration of the previous packet + previous->packet.duration = rec->packet.pts - previous->packet.pts; + + bool ok = recorder_write(recorder, &previous->packet); + record_packet_delete(previous); if (!ok) { LOGE("Could not record packet"); diff --git a/app/src/recorder.h b/app/src/recorder.h index 5dbfcc3b..af541882 100644 --- a/app/src/recorder.h +++ b/app/src/recorder.h @@ -34,6 +34,12 @@ struct recorder { bool stopped; // set on recorder_stop() by the stream reader bool failed; // set on packet write failure struct recorder_queue queue; + + // we can write a packet only once we received the next one so that we can + // set its duration (next_pts - current_pts) + // "previous" is only accessed from the recorder thread, so it does not + // need to be protected by the mutex + struct record_packet *previous; }; bool From 27eacc3c11d0d40d4a125629a2ade9f459fc6d77 Mon Sep 17 00:00:00 2001 From: toddsierens <42444866+toddsierens@users.noreply.github.com> Date: Thu, 8 Aug 2019 17:17:48 -0400 Subject: [PATCH 0049/2244] Update WindowManager.java --- .../main/java/com/genymobile/scrcpy/wrappers/WindowManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 56330f9d..52096461 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java @@ -14,7 +14,7 @@ public final class WindowManager { try { Class cls = manager.getClass(); try { - return (Integer) manager.getClass().getMethod("getRotation").invoke(manager); + return (Integer) cls.getMethod("getRotation").invoke(manager); } catch (NoSuchMethodException e) { // method changed since this commit: // https://android.googlesource.com/platform/frameworks/base/+/8ee7285128c3843401d4c4d0412cd66e86ba49e3%5E%21/#F2 From 20b3f101a40cd7455cc5b41e381291504deec5ba Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 9 Aug 2019 15:15:28 +0200 Subject: [PATCH 0050/2244] Print gradle output on compiling Enable the attribute "console" of custom_target() introduced in meson 0.48. This allows to get a feedback of what gradle does (which can takes a very long time). This produces warnings because we declare to support meson >= 0.37, but we don't want to stop supporting older versions for that. Older versions just ignore the option: > WARNING: Unknown keyword arguments in target scrcpy-server: console Newer meson versions use it, but warn because we declare supporting older versions: > WARNING: Project targetting '>= 0.37' but tried to use feature > introduced in '0.48.0': console arg in custom_target Meson does not support conditional branches to suppress such warnings, so just keep the warnings. --- server/meson.build | 1 + 1 file changed, 1 insertion(+) diff --git a/server/meson.build b/server/meson.build index d96373a0..43901246 100644 --- a/server/meson.build +++ b/server/meson.build @@ -6,6 +6,7 @@ if prebuilt_server == '' build_always: true, # gradle is responsible for tracking source changes output: 'scrcpy-server.jar', command: [find_program('./scripts/build-wrapper.sh'), meson.current_source_dir(), '@OUTPUT@', get_option('buildtype')], + console: true, install: true, install_dir: 'share/scrcpy') else From a9c8fa305d2313bed70b2d1d756083f40e065352 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 13 Aug 2019 18:49:25 +0200 Subject: [PATCH 0051/2244] Fix segfault on recording with old FFmpeg The AVPacket fields side_data and side_data_elems were not initialized by av_packet_ref() in old FFmpeg versions (prior to [1]). As a consequence, on av_packet_unref(), side_data was freed, causing a segfault. Fixes [1]: --- app/src/recorder.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/recorder.c b/app/src/recorder.c index 0de05d81..0d4e4e68 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -33,6 +33,11 @@ record_packet_new(const AVPacket *packet) { if (!rec) { return NULL; } + + // av_packet_ref() does not initialize all fields in old FFmpeg versions + // See + av_init_packet(&rec->packet); + if (av_packet_ref(&rec->packet, packet)) { SDL_free(rec); return NULL; From da5b0ec0d59a9da591507cbb7b5d19f55b76c35c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 14 Aug 2019 22:24:26 +0200 Subject: [PATCH 0052/2244] Improve FAQ --- FAQ.md | 44 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/FAQ.md b/FAQ.md index 4b04d228..49382471 100644 --- a/FAQ.md +++ b/FAQ.md @@ -1,10 +1,5 @@ # Frequently Asked Questions -## Common issues - -The application is very young, it is not unlikely that you encounter problems -with it. - Here are the common reported problems and their status. @@ -20,9 +15,13 @@ Windows may need some [drivers] to detect your device. [drivers]: https://developer.android.com/studio/run/oem-usb.html -### Mouse clicks do not work +### I can only mirror, I cannot interact with the device On some devices, you may need to enable an option to allow [simulating input]. +In developer options, enable: + +> **USB debugging (Security settings)** +> _Allow granting permissions and simulating input via USB debugging_ [simulating input]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323 @@ -43,6 +42,16 @@ meson x --buildtype release -Dhidpi_support=false However, the video will be displayed at lower resolution. +### The quality is low on HiDPI display + +On Windows, you may 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 + + ### KWin compositor crashes On Plasma Desktop, compositor is disabled while _scrcpy_ is running. @@ -50,3 +59,26 @@ On Plasma Desktop, compositor is disabled while _scrcpy_ is running. 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" + +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 +``` + +Just try with a lower definition: + +``` +scrcpy -m 1920 +scrcpy -m 1024 +scrcpy -m 800 +``` From 7040e8abc45ae95d901341c81f7d4cfe52da171c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 15 Sep 2019 17:17:43 +0200 Subject: [PATCH 0053/2244] Fix control message reader test The mouse event test actually tested a key event control message. --- .../scrcpy/ControlMessageReaderTest.java | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java index df1db1a6..3d2574fc 100644 --- a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java @@ -82,19 +82,26 @@ public class ControlMessageReaderTest { ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); - dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE); + dos.writeByte(ControlMessage.TYPE_INJECT_MOUSE_EVENT); dos.writeByte(MotionEvent.ACTION_DOWN); dos.writeInt(MotionEvent.BUTTON_PRIMARY); - dos.writeInt(KeyEvent.META_CTRL_ON); + dos.writeInt(100); + dos.writeInt(200); + dos.writeShort(1080); + dos.writeShort(1920); + byte[] packet = bos.toByteArray(); reader.readFrom(new ByteArrayInputStream(packet)); ControlMessage event = reader.next(); - Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType()); + Assert.assertEquals(ControlMessage.TYPE_INJECT_MOUSE_EVENT, event.getType()); Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction()); - Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getKeycode()); - Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); + Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getButtons()); + Assert.assertEquals(100, event.getPosition().getPoint().getX()); + Assert.assertEquals(200, event.getPosition().getPoint().getY()); + Assert.assertEquals(1080, event.getPosition().getScreenSize().getWidth()); + Assert.assertEquals(1920, event.getPosition().getScreenSize().getHeight()); } @Test From 6e38e0cbfee9cb5258f9d1bf33c8608f58b97844 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 15 Sep 2019 17:03:38 +0200 Subject: [PATCH 0054/2244] Rename variable names "event" to "msg" Some variable names had not been renamed when "event" was renamed to "message" (28980bbc90ab93cf61bdc4b36d2448b8cebbb7df). --- .../com/genymobile/scrcpy/ControlMessage.java | 66 +++++++++---------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java index 0de4bc3c..a1cd873a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java @@ -30,60 +30,60 @@ public final class ControlMessage { } public static ControlMessage createInjectKeycode(int action, int keycode, int metaState) { - ControlMessage event = new ControlMessage(); - event.type = TYPE_INJECT_KEYCODE; - event.action = action; - event.keycode = keycode; - event.metaState = metaState; - return event; + ControlMessage msg = new ControlMessage(); + msg.type = TYPE_INJECT_KEYCODE; + msg.action = action; + msg.keycode = keycode; + msg.metaState = metaState; + return msg; } public static ControlMessage createInjectText(String text) { - ControlMessage event = new ControlMessage(); - event.type = TYPE_INJECT_TEXT; - event.text = text; - return event; + ControlMessage msg = new ControlMessage(); + msg.type = TYPE_INJECT_TEXT; + msg.text = text; + return msg; } public static ControlMessage createInjectMouseEvent(int action, int buttons, Position position) { - ControlMessage event = new ControlMessage(); - event.type = TYPE_INJECT_MOUSE_EVENT; - event.action = action; - event.buttons = buttons; - event.position = position; - return event; + ControlMessage msg = new ControlMessage(); + msg.type = TYPE_INJECT_MOUSE_EVENT; + msg.action = action; + msg.buttons = buttons; + msg.position = position; + return msg; } public static ControlMessage createInjectScrollEvent(Position position, int hScroll, int vScroll) { - ControlMessage event = new ControlMessage(); - event.type = TYPE_INJECT_SCROLL_EVENT; - event.position = position; - event.hScroll = hScroll; - event.vScroll = vScroll; - return event; + ControlMessage msg = new ControlMessage(); + msg.type = TYPE_INJECT_SCROLL_EVENT; + msg.position = position; + msg.hScroll = hScroll; + msg.vScroll = vScroll; + return msg; } public static ControlMessage createSetClipboard(String text) { - ControlMessage event = new ControlMessage(); - event.type = TYPE_SET_CLIPBOARD; - event.text = text; - return event; + ControlMessage msg = new ControlMessage(); + msg.type = TYPE_SET_CLIPBOARD; + msg.text = text; + return msg; } /** * @param mode one of the {@code Device.SCREEN_POWER_MODE_*} constants */ public static ControlMessage createSetScreenPowerMode(int mode) { - ControlMessage event = new ControlMessage(); - event.type = TYPE_SET_SCREEN_POWER_MODE; - event.action = mode; - return event; + ControlMessage msg = new ControlMessage(); + msg.type = TYPE_SET_SCREEN_POWER_MODE; + msg.action = mode; + return msg; } public static ControlMessage createEmpty(int type) { - ControlMessage event = new ControlMessage(); - event.type = type; - return event; + ControlMessage msg = new ControlMessage(); + msg.type = type; + return msg; } public int getType() { From 9463850c2471dac67b571f5343cd5307cc0d6d54 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 15 Sep 2019 16:27:37 +0200 Subject: [PATCH 0055/2244] Rename "convert.h" to "event_converter.h" The filename gave no hint about what was converted. --- app/meson.build | 2 +- app/src/{convert.c => event_converter.c} | 2 +- app/src/{convert.h => event_converter.h} | 0 app/src/input_manager.c | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename app/src/{convert.c => event_converter.c} (99%) rename app/src/{convert.h => event_converter.h} (100%) diff --git a/app/meson.build b/app/meson.build index 3410c2d3..ccd05fee 100644 --- a/app/meson.build +++ b/app/meson.build @@ -3,10 +3,10 @@ src = [ 'src/command.c', 'src/control_msg.c', 'src/controller.c', - 'src/convert.c', 'src/decoder.c', 'src/device.c', 'src/device_msg.c', + 'src/event_converter.c', 'src/file_handler.c', 'src/fps_counter.c', 'src/input_manager.c', diff --git a/app/src/convert.c b/app/src/event_converter.c similarity index 99% rename from app/src/convert.c rename to app/src/event_converter.c index adf6d400..17351645 100644 --- a/app/src/convert.c +++ b/app/src/event_converter.c @@ -1,4 +1,4 @@ -#include "convert.h" +#include "event_converter.h" #define MAP(FROM, TO) case FROM: *to = TO; return true #define FAIL default: return false diff --git a/app/src/convert.h b/app/src/event_converter.h similarity index 100% rename from app/src/convert.h rename to app/src/event_converter.h diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 5258beda..506c5798 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -1,7 +1,7 @@ #include "input_manager.h" #include -#include "convert.h" +#include "event_converter.h" #include "lock_util.h" #include "log.h" From ffdbf5990ba82fa206248b4a411442a6885dbcca Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 15 Sep 2019 16:32:46 +0200 Subject: [PATCH 0056/2244] Rename event converter functions Rename "XXX_from_sdl_to_android" to "convert_XXX", to avoid huge function names. --- app/src/event_converter.c | 19 +++++++------------ app/src/event_converter.h | 18 +++++++----------- app/src/input_manager.c | 12 ++++-------- 3 files changed, 18 insertions(+), 31 deletions(-) diff --git a/app/src/event_converter.c b/app/src/event_converter.c index 17351645..dea61eee 100644 --- a/app/src/event_converter.c +++ b/app/src/event_converter.c @@ -31,7 +31,6 @@ autocomplete_metastate(enum android_metastate metastate) { return metastate; } - static enum android_metastate convert_meta_state(SDL_Keymod mod) { enum android_metastate metastate = 0; @@ -158,8 +157,7 @@ convert_mouse_buttons(uint32_t state) { } bool -input_key_from_sdl_to_android(const SDL_KeyboardEvent *from, - struct control_msg *to) { +convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to) { to->type = CONTROL_MSG_TYPE_INJECT_KEYCODE; if (!convert_keycode_action(from->type, &to->inject_keycode.action)) { @@ -177,9 +175,8 @@ input_key_from_sdl_to_android(const SDL_KeyboardEvent *from, } bool -mouse_button_from_sdl_to_android(const SDL_MouseButtonEvent *from, - struct size screen_size, - struct control_msg *to) { +convert_mouse_button(const SDL_MouseButtonEvent *from, struct size screen_size, + struct control_msg *to) { to->type = CONTROL_MSG_TYPE_INJECT_MOUSE_EVENT; if (!convert_mouse_action(from->type, &to->inject_mouse_event.action)) { @@ -196,9 +193,8 @@ mouse_button_from_sdl_to_android(const SDL_MouseButtonEvent *from, } bool -mouse_motion_from_sdl_to_android(const SDL_MouseMotionEvent *from, - struct size screen_size, - struct control_msg *to) { +convert_mouse_motion(const SDL_MouseMotionEvent *from, struct size screen_size, + struct control_msg *to) { to->type = CONTROL_MSG_TYPE_INJECT_MOUSE_EVENT; to->inject_mouse_event.action = AMOTION_EVENT_ACTION_MOVE; to->inject_mouse_event.buttons = convert_mouse_buttons(from->state); @@ -210,9 +206,8 @@ mouse_motion_from_sdl_to_android(const SDL_MouseMotionEvent *from, } bool -mouse_wheel_from_sdl_to_android(const SDL_MouseWheelEvent *from, - struct position position, - struct control_msg *to) { +convert_mouse_wheel(const SDL_MouseWheelEvent *from, struct position position, + struct control_msg *to) { to->type = CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT; to->inject_scroll_event.position = position; diff --git a/app/src/event_converter.h b/app/src/event_converter.h index 5989e163..2d7aa1ae 100644 --- a/app/src/event_converter.h +++ b/app/src/event_converter.h @@ -17,25 +17,21 @@ struct complete_mouse_wheel_event { }; bool -input_key_from_sdl_to_android(const SDL_KeyboardEvent *from, - struct control_msg *to); +convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to); bool -mouse_button_from_sdl_to_android(const SDL_MouseButtonEvent *from, - struct size screen_size, - struct control_msg *to); +convert_mouse_button(const SDL_MouseButtonEvent *from, struct size screen_size, + struct control_msg *to); // the video size may be different from the real device size, so we need the // size to which the absolute position apply, to scale it accordingly bool -mouse_motion_from_sdl_to_android(const SDL_MouseMotionEvent *from, - struct size screen_size, - struct control_msg *to); +convert_mouse_motion(const SDL_MouseMotionEvent *from, struct size screen_size, + struct control_msg *to); // on Android, a scroll event requires the current mouse position bool -mouse_wheel_from_sdl_to_android(const SDL_MouseWheelEvent *from, - struct position position, - struct control_msg *to); +convert_mouse_wheel(const SDL_MouseWheelEvent *from, struct position position, + struct control_msg *to); #endif diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 506c5798..faacd29e 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -373,7 +373,7 @@ input_manager_process_key(struct input_manager *input_manager, } struct control_msg msg; - if (input_key_from_sdl_to_android(event, &msg)) { + if (convert_input_key(event, &msg)) { if (!controller_push_msg(controller, &msg)) { LOGW("Could not request 'inject keycode'"); } @@ -388,9 +388,7 @@ input_manager_process_mouse_motion(struct input_manager *input_manager, return; } struct control_msg msg; - if (mouse_motion_from_sdl_to_android(event, - input_manager->screen->frame_size, - &msg)) { + if (convert_mouse_motion(event, input_manager->screen->frame_size, &msg)) { if (!controller_push_msg(input_manager->controller, &msg)) { LOGW("Could not request 'inject mouse motion event'"); } @@ -434,9 +432,7 @@ input_manager_process_mouse_button(struct input_manager *input_manager, } struct control_msg msg; - if (mouse_button_from_sdl_to_android(event, - input_manager->screen->frame_size, - &msg)) { + if (convert_mouse_button(event, input_manager->screen->frame_size, &msg)) { if (!controller_push_msg(input_manager->controller, &msg)) { LOGW("Could not request 'inject mouse button event'"); } @@ -451,7 +447,7 @@ input_manager_process_mouse_wheel(struct input_manager *input_manager, .point = get_mouse_point(input_manager->screen), }; struct control_msg msg; - if (mouse_wheel_from_sdl_to_android(event, position, &msg)) { + if (convert_mouse_wheel(event, position, &msg)) { if (!controller_push_msg(input_manager->controller, &msg)) { LOGW("Could not request 'inject mouse wheel event'"); } From 513d1ac96d24d6cf07fb1850232b5c0e8f555615 Mon Sep 17 00:00:00 2001 From: Ta-da <53489032+clang-clang-clang@users.noreply.github.com> Date: Fri, 27 Sep 2019 10:04:41 +0800 Subject: [PATCH 0057/2244] Fix option "record-format" related short opt --- 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 dfeca7cb..3be74d6b 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -328,7 +328,7 @@ parse_args(struct args *args, int argc, char *argv[]) { {"push-target", required_argument, NULL, OPT_PUSH_TARGET}, {"record", required_argument, NULL, 'r'}, - {"record-format", required_argument, NULL, 'f'}, + {"record-format", required_argument, NULL, 'F'}, {"render-expired-frames", no_argument, NULL, OPT_RENDER_EXPIRED_FRAMES}, {"serial", required_argument, NULL, 's'}, From 795d1030327701373bd81bafc01d26422eda28e5 Mon Sep 17 00:00:00 2001 From: Yu-Chen Lin Date: Sat, 28 Sep 2019 10:37:25 +0800 Subject: [PATCH 0058/2244] input_manager.c: Correct log Signed-off-by: Yu-Chen Lin --- app/src/input_manager.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index faacd29e..e6b3738b 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -102,7 +102,7 @@ press_back_or_turn_screen_on(struct controller *controller) { msg.type = CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON; if (!controller_push_msg(controller, &msg)) { - LOGW("Could not request 'turn screen on'"); + LOGW("Could not request 'press back or turn screen on'"); } } From 7d1932b9076b6ff3a3fd77d3b6d94a886a91ceaa Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 28 Sep 2019 12:23:54 +0200 Subject: [PATCH 0059/2244] Fix gradle warnings in tests --- .../java/com/genymobile/scrcpy/ControlMessageReaderTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java index 3d2574fc..f0c643d4 100644 --- a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java @@ -77,6 +77,7 @@ public class ControlMessageReaderTest { } @Test + @SuppressWarnings("checkstyle:MagicNumber") public void testParseMouseEvent() throws IOException { ControlMessageReader reader = new ControlMessageReader(); From f510f1de1c28dba662a60c7b2b642163cfbf4242 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 28 Sep 2019 14:46:44 +0200 Subject: [PATCH 0060/2244] Remove "make" from build dependencies The project is built with meson+ninja. --- BUILD.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BUILD.md b/BUILD.md index bb9a7585..475580f8 100644 --- a/BUILD.md +++ b/BUILD.md @@ -43,7 +43,7 @@ Install the required packages from your package manager. sudo apt install ffmpeg libsdl2-2.0-0 # client build dependencies -sudo apt install make gcc git pkg-config meson ninja-build \ +sudo apt install gcc git pkg-config meson ninja-build \ libavcodec-dev libavformat-dev libavutil-dev \ libsdl2-dev From 129dabcfa466d0d76d2f2b2715e48d649d80a4ba Mon Sep 17 00:00:00 2001 From: Louis Kruger Date: Sat, 28 Sep 2019 18:54:08 -0400 Subject: [PATCH 0061/2244] Include config.h to fix HIDPI support Ref: Signed-off-by: Romain Vimont --- app/src/screen.c | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/screen.c b/app/src/screen.c index 18d24dda..e34bcf46 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -3,6 +3,7 @@ #include #include +#include "config.h" #include "common.h" #include "compat.h" #include "icon.xpm" From 1f8ba1ca79f4915f3a7b3c5199258904e3192445 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 29 Sep 2019 22:36:56 +0200 Subject: [PATCH 0062/2244] Include config.h everywhere Ref: Suggested-by: Louis Kruger --- app/src/buffer_util.h | 2 ++ app/src/cbuf.h | 2 ++ app/src/command.c | 1 + app/src/command.h | 2 ++ app/src/common.h | 2 ++ app/src/control_msg.c | 1 + app/src/control_msg.h | 1 + app/src/controller.h | 1 + app/src/decoder.c | 2 +- app/src/decoder.h | 2 ++ app/src/device.c | 2 ++ app/src/device.h | 1 + app/src/device_msg.c | 1 + app/src/device_msg.h | 2 ++ app/src/event_converter.c | 2 ++ app/src/event_converter.h | 1 + app/src/file_handler.h | 1 + app/src/fps_counter.c | 1 + app/src/fps_counter.h | 2 ++ app/src/input_manager.c | 2 ++ app/src/input_manager.h | 1 + app/src/lock_util.h | 1 + app/src/main.c | 2 +- app/src/net.c | 1 + app/src/net.h | 2 ++ app/src/queue.h | 2 ++ app/src/receiver.h | 1 + app/src/recorder.c | 2 +- app/src/recorder.h | 1 + app/src/scrcpy.c | 1 + app/src/scrcpy.h | 2 ++ app/src/screen.h | 1 + app/src/server.h | 1 + app/src/str_util.c | 2 ++ app/src/str_util.h | 2 ++ app/src/stream.c | 2 +- app/src/stream.h | 1 + app/src/sys/unix/command.c | 2 ++ app/src/sys/unix/net.c | 2 ++ app/src/sys/win/net.c | 1 + app/src/tiny_xpm.c | 1 + app/src/tiny_xpm.h | 2 ++ app/src/video_buffer.h | 1 + 43 files changed, 61 insertions(+), 4 deletions(-) diff --git a/app/src/buffer_util.h b/app/src/buffer_util.h index a79014b1..681421f3 100644 --- a/app/src/buffer_util.h +++ b/app/src/buffer_util.h @@ -4,6 +4,8 @@ #include #include +#include "config.h" + static inline void buffer_write16be(uint8_t *buf, uint16_t value) { buf[0] = value >> 8; diff --git a/app/src/cbuf.h b/app/src/cbuf.h index 35b39b7b..c18e4680 100644 --- a/app/src/cbuf.h +++ b/app/src/cbuf.h @@ -5,6 +5,8 @@ #include #include +#include "config.h" + // To define a circular buffer type of 20 ints: // struct cbuf_int CBUF(int, 20); // diff --git a/app/src/command.c b/app/src/command.c index 4cb2e408..d914e6ab 100644 --- a/app/src/command.c +++ b/app/src/command.c @@ -5,6 +5,7 @@ #include #include +#include "config.h" #include "common.h" #include "log.h" #include "str_util.h" diff --git a/app/src/command.h b/app/src/command.h index db6358da..d119c9bb 100644 --- a/app/src/command.h +++ b/app/src/command.h @@ -33,6 +33,8 @@ #endif +#include "config.h" + # define NO_EXIT_CODE -1 enum process_result { diff --git a/app/src/common.h b/app/src/common.h index 8963f058..e5cbe953 100644 --- a/app/src/common.h +++ b/app/src/common.h @@ -3,6 +3,8 @@ #include +#include "config.h" + #define ARRAY_LEN(a) (sizeof(a) / sizeof(a[0])) #define MIN(X,Y) (X) < (Y) ? (X) : (Y) #define MAX(X,Y) (X) > (Y) ? (X) : (Y) diff --git a/app/src/control_msg.c b/app/src/control_msg.c index 9c3d9849..fff93592 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -2,6 +2,7 @@ #include +#include "config.h" #include "buffer_util.h" #include "log.h" #include "str_util.h" diff --git a/app/src/control_msg.h b/app/src/control_msg.h index e7fdfc4c..308d54a3 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -5,6 +5,7 @@ #include #include +#include "config.h" #include "android/input.h" #include "android/keycodes.h" #include "common.h" diff --git a/app/src/controller.h b/app/src/controller.h index ae13e39f..1b0d005b 100644 --- a/app/src/controller.h +++ b/app/src/controller.h @@ -5,6 +5,7 @@ #include #include +#include "config.h" #include "cbuf.h" #include "control_msg.h" #include "net.h" diff --git a/app/src/decoder.c b/app/src/decoder.c index 8fa218f4..cad19913 100644 --- a/app/src/decoder.c +++ b/app/src/decoder.c @@ -8,8 +8,8 @@ #include #include -#include "compat.h" #include "config.h" +#include "compat.h" #include "buffer_util.h" #include "events.h" #include "lock_util.h" diff --git a/app/src/decoder.h b/app/src/decoder.h index 76fee80e..f243812c 100644 --- a/app/src/decoder.h +++ b/app/src/decoder.h @@ -4,6 +4,8 @@ #include #include +#include "config.h" + struct video_buffer; struct decoder { diff --git a/app/src/device.c b/app/src/device.c index 8027ccbb..4f50ab48 100644 --- a/app/src/device.c +++ b/app/src/device.c @@ -1,4 +1,6 @@ #include "device.h" + +#include "config.h" #include "log.h" bool diff --git a/app/src/device.h b/app/src/device.h index 828443d7..34a5f17f 100644 --- a/app/src/device.h +++ b/app/src/device.h @@ -3,6 +3,7 @@ #include +#include "config.h" #include "common.h" #include "net.h" diff --git a/app/src/device_msg.c b/app/src/device_msg.c index a90d78dd..2fc90ae4 100644 --- a/app/src/device_msg.c +++ b/app/src/device_msg.c @@ -3,6 +3,7 @@ #include #include +#include "config.h" #include "buffer_util.h" #include "log.h" diff --git a/app/src/device_msg.h b/app/src/device_msg.h index fd4a7eb1..04723597 100644 --- a/app/src/device_msg.h +++ b/app/src/device_msg.h @@ -5,6 +5,8 @@ #include #include +#include "config.h" + #define DEVICE_MSG_TEXT_MAX_LENGTH 4093 #define DEVICE_MSG_SERIALIZED_MAX_SIZE (3 + DEVICE_MSG_TEXT_MAX_LENGTH) diff --git a/app/src/event_converter.c b/app/src/event_converter.c index dea61eee..da4b2e30 100644 --- a/app/src/event_converter.c +++ b/app/src/event_converter.c @@ -1,5 +1,7 @@ #include "event_converter.h" +#include "config.h" + #define MAP(FROM, TO) case FROM: *to = TO; return true #define FAIL default: return false diff --git a/app/src/event_converter.h b/app/src/event_converter.h index 2d7aa1ae..e0b24c15 100644 --- a/app/src/event_converter.h +++ b/app/src/event_converter.h @@ -4,6 +4,7 @@ #include #include +#include "config.h" #include "control_msg.h" struct complete_mouse_motion_event { diff --git a/app/src/file_handler.h b/app/src/file_handler.h index 3418c532..4c158296 100644 --- a/app/src/file_handler.h +++ b/app/src/file_handler.h @@ -5,6 +5,7 @@ #include #include +#include "config.h" #include "cbuf.h" #include "command.h" diff --git a/app/src/fps_counter.c b/app/src/fps_counter.c index daece470..2a9478f6 100644 --- a/app/src/fps_counter.c +++ b/app/src/fps_counter.c @@ -3,6 +3,7 @@ #include #include +#include "config.h" #include "lock_util.h" #include "log.h" diff --git a/app/src/fps_counter.h b/app/src/fps_counter.h index 6b560a35..1c56bb01 100644 --- a/app/src/fps_counter.h +++ b/app/src/fps_counter.h @@ -7,6 +7,8 @@ #include #include +#include "config.h" + struct fps_counter { SDL_Thread *thread; SDL_mutex *mutex; diff --git a/app/src/input_manager.c b/app/src/input_manager.c index e6b3738b..cf2a7519 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -1,6 +1,8 @@ #include "input_manager.h" #include + +#include "config.h" #include "event_converter.h" #include "lock_util.h" #include "log.h" diff --git a/app/src/input_manager.h b/app/src/input_manager.h index 83cb7405..61a0447f 100644 --- a/app/src/input_manager.h +++ b/app/src/input_manager.h @@ -3,6 +3,7 @@ #include +#include "config.h" #include "common.h" #include "controller.h" #include "fps_counter.h" diff --git a/app/src/lock_util.h b/app/src/lock_util.h index d1ca7336..260d2c12 100644 --- a/app/src/lock_util.h +++ b/app/src/lock_util.h @@ -4,6 +4,7 @@ #include #include +#include "config.h" #include "log.h" static inline void diff --git a/app/src/main.c b/app/src/main.c index 3be74d6b..c00bf419 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -8,8 +8,8 @@ #define SDL_MAIN_HANDLED // avoid link error on Linux Windows Subsystem #include -#include "compat.h" #include "config.h" +#include "compat.h" #include "log.h" #include "recorder.h" diff --git a/app/src/net.c b/app/src/net.c index a0bc38f2..bf4389dd 100644 --- a/app/src/net.c +++ b/app/src/net.c @@ -2,6 +2,7 @@ #include +#include "config.h" #include "log.h" #ifdef __WINDOWS__ diff --git a/app/src/net.h b/app/src/net.h index dd82c083..ffd5dd89 100644 --- a/app/src/net.h +++ b/app/src/net.h @@ -17,6 +17,8 @@ typedef int socket_t; #endif +#include "config.h" + bool net_init(void); diff --git a/app/src/queue.h b/app/src/queue.h index ed3d4c21..6cf7aba6 100644 --- a/app/src/queue.h +++ b/app/src/queue.h @@ -6,6 +6,8 @@ #include #include +#include "config.h" + // To define a queue type of "struct foo": // struct queue_foo QUEUE(struct foo); #define QUEUE(TYPE) { \ diff --git a/app/src/receiver.h b/app/src/receiver.h index c119b827..6108e545 100644 --- a/app/src/receiver.h +++ b/app/src/receiver.h @@ -5,6 +5,7 @@ #include #include +#include "config.h" #include "net.h" // receive events from the device diff --git a/app/src/recorder.c b/app/src/recorder.c index 0d4e4e68..77186350 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -3,8 +3,8 @@ #include #include -#include "compat.h" #include "config.h" +#include "compat.h" #include "lock_util.h" #include "log.h" diff --git a/app/src/recorder.h b/app/src/recorder.h index af541882..b1953fcb 100644 --- a/app/src/recorder.h +++ b/app/src/recorder.h @@ -6,6 +6,7 @@ #include #include +#include "config.h" #include "common.h" #include "queue.h" diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index ed988778..defcb751 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -7,6 +7,7 @@ #include #include +#include "config.h" #include "command.h" #include "common.h" #include "compat.h" diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index faeb246f..1593fb1e 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -5,6 +5,8 @@ #include #include +#include "config.h" + struct scrcpy_options { const char *serial; const char *crop; diff --git a/app/src/screen.h b/app/src/screen.h index 63da6aa5..bc189189 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -5,6 +5,7 @@ #include #include +#include "config.h" #include "common.h" struct video_buffer; diff --git a/app/src/server.h b/app/src/server.h index 4970d64e..2140d8ab 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -4,6 +4,7 @@ #include #include +#include "config.h" #include "command.h" #include "net.h" diff --git a/app/src/str_util.c b/app/src/str_util.c index 7d46a1a0..15378d8a 100644 --- a/app/src/str_util.c +++ b/app/src/str_util.c @@ -10,6 +10,8 @@ #include +#include "config.h" + size_t xstrncpy(char *dest, const char *src, size_t n) { size_t i; diff --git a/app/src/str_util.h b/app/src/str_util.h index 0b7a571a..56490190 100644 --- a/app/src/str_util.h +++ b/app/src/str_util.h @@ -3,6 +3,8 @@ #include +#include "config.h" + // like strncpy, except: // - it copies at most n-1 chars // - the dest string is nul-terminated diff --git a/app/src/stream.c b/app/src/stream.c index bca89f71..6c3192f2 100644 --- a/app/src/stream.c +++ b/app/src/stream.c @@ -8,8 +8,8 @@ #include #include -#include "compat.h" #include "config.h" +#include "compat.h" #include "buffer_util.h" #include "decoder.h" #include "events.h" diff --git a/app/src/stream.h b/app/src/stream.h index 160ed7f5..cb50468e 100644 --- a/app/src/stream.h +++ b/app/src/stream.h @@ -7,6 +7,7 @@ #include #include +#include "config.h" #include "net.h" struct video_buffer; diff --git a/app/src/sys/unix/command.c b/app/src/sys/unix/command.c index 9e00ccf8..6a3a5a47 100644 --- a/app/src/sys/unix/command.c +++ b/app/src/sys/unix/command.c @@ -7,6 +7,8 @@ #include "command.h" +#include "config.h" + #include #include #include diff --git a/app/src/sys/unix/net.c b/app/src/sys/unix/net.c index 199cd7c2..d940f3bb 100644 --- a/app/src/sys/unix/net.c +++ b/app/src/sys/unix/net.c @@ -2,6 +2,8 @@ #include +#include "config.h" + bool net_init(void) { // do nothing diff --git a/app/src/sys/win/net.c b/app/src/sys/win/net.c index dc483682..55519782 100644 --- a/app/src/sys/win/net.c +++ b/app/src/sys/win/net.c @@ -1,5 +1,6 @@ #include "net.h" +#include "config.h" #include "log.h" bool diff --git a/app/src/tiny_xpm.c b/app/src/tiny_xpm.c index 0fb410f3..5ea89078 100644 --- a/app/src/tiny_xpm.c +++ b/app/src/tiny_xpm.c @@ -5,6 +5,7 @@ #include #include +#include "config.h" #include "log.h" struct index { diff --git a/app/src/tiny_xpm.h b/app/src/tiny_xpm.h index 85dea5c2..6e6f8035 100644 --- a/app/src/tiny_xpm.h +++ b/app/src/tiny_xpm.h @@ -3,6 +3,8 @@ #include +#include "config.h" + SDL_Surface * read_xpm(char *xpm[]); diff --git a/app/src/video_buffer.h b/app/src/video_buffer.h index 26a6fa1f..303b3fc2 100644 --- a/app/src/video_buffer.h +++ b/app/src/video_buffer.h @@ -4,6 +4,7 @@ #include #include +#include "config.h" #include "fps_counter.h" // forward declarations From 810ff80ba7c425f021bcfda972345595a85b803c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 15 Sep 2019 16:58:14 +0200 Subject: [PATCH 0063/2244] Add buffer_write64be() Add a function to write 64 bits in big-endian from a uint64_t. --- app/src/buffer_util.h | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/buffer_util.h b/app/src/buffer_util.h index 681421f3..262df1dc 100644 --- a/app/src/buffer_util.h +++ b/app/src/buffer_util.h @@ -20,6 +20,12 @@ buffer_write32be(uint8_t *buf, uint32_t value) { buf[3] = value; } +static inline void +buffer_write64be(uint8_t *buf, uint64_t value) { + buffer_write32be(buf, value >> 32); + buffer_write32be(&buf[4], (uint32_t) value); +} + static inline uint16_t buffer_read16be(const uint8_t *buf) { return (buf[0] << 8) | buf[1]; From d90549d1e67e018285fedb8f1d820c04b7d5b3c7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 22 Sep 2019 14:45:56 +0200 Subject: [PATCH 0064/2244] Rename "pointer" to "mouse pointer" This will help to distinguish them from "touch pointers". --- .../com/genymobile/scrcpy/Controller.java | 37 +++++++++---------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index 263fc2fc..cbc4aec4 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -20,35 +20,35 @@ public class Controller { private final KeyCharacterMap charMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD); private long lastMouseDown; - private final MotionEvent.PointerProperties[] pointerProperties = {new MotionEvent.PointerProperties()}; - private final MotionEvent.PointerCoords[] pointerCoords = {new MotionEvent.PointerCoords()}; + private final MotionEvent.PointerProperties[] mousePointerProperties = {new MotionEvent.PointerProperties()}; + private final MotionEvent.PointerCoords[] mousePointerCoords = {new MotionEvent.PointerCoords()}; public Controller(Device device, DesktopConnection connection) { this.device = device; this.connection = connection; - initPointer(); + initMousePointer(); sender = new DeviceMessageSender(connection); } - private void initPointer() { - MotionEvent.PointerProperties props = pointerProperties[0]; + private void initMousePointer() { + MotionEvent.PointerProperties props = mousePointerProperties[0]; props.id = 0; props.toolType = MotionEvent.TOOL_TYPE_FINGER; - MotionEvent.PointerCoords coords = pointerCoords[0]; + MotionEvent.PointerCoords coords = mousePointerCoords[0]; coords.orientation = 0; coords.pressure = 1; coords.size = 1; } - private void setPointerCoords(Point point) { - MotionEvent.PointerCoords coords = pointerCoords[0]; + private void setMousePointerCoords(Point point) { + MotionEvent.PointerCoords coords = mousePointerCoords[0]; coords.x = point.getX(); coords.y = point.getY(); } private void setScroll(int hScroll, int vScroll) { - MotionEvent.PointerCoords coords = pointerCoords[0]; + MotionEvent.PointerCoords coords = mousePointerCoords[0]; coords.setAxisValue(MotionEvent.AXIS_HSCROLL, hScroll); coords.setAxisValue(MotionEvent.AXIS_VSCROLL, vScroll); } @@ -158,9 +158,9 @@ public class Controller { // ignore event return false; } - setPointerCoords(point); - MotionEvent event = MotionEvent.obtain(lastMouseDown, now, action, 1, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, 0, 0, - InputDevice.SOURCE_TOUCHSCREEN, 0); + setMousePointerCoords(point); + MotionEvent event = MotionEvent.obtain(lastMouseDown, now, action, 1, mousePointerProperties, + mousePointerCoords, 0, buttons, 1f, 1f, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0); return injectEvent(event); } @@ -171,23 +171,22 @@ public class Controller { // ignore event return false; } - setPointerCoords(point); + setMousePointerCoords(point); setScroll(hScroll, vScroll); - MotionEvent event = MotionEvent.obtain(lastMouseDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, 0, 1f, 1f, 0, - 0, InputDevice.SOURCE_MOUSE, 0); + MotionEvent event = MotionEvent.obtain(lastMouseDown, now, MotionEvent.ACTION_SCROLL, 1, + mousePointerProperties, mousePointerCoords, 0, 0, 1f, 1f, 0, 0, InputDevice.SOURCE_MOUSE, 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); + 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); + return injectKeyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0) && injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0); } private boolean injectEvent(InputEvent event) { From 77f876e29cdd6f78dd9de4be6511b6228d58a1e7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 15 Sep 2019 16:16:17 +0200 Subject: [PATCH 0065/2244] Add "inject touch" control message Add a control message type in the protocol to forward touch events to the device. --- app/src/control_msg.c | 19 ++++++++++ app/src/control_msg.h | 7 ++++ app/tests/test_control_msg_serialize.c | 36 +++++++++++++++++++ .../com/genymobile/scrcpy/ControlMessage.java | 35 ++++++++++++++---- .../scrcpy/ControlMessageReader.java | 19 ++++++++++ .../scrcpy/ControlMessageReaderTest.java | 31 ++++++++++++++++ 6 files changed, 140 insertions(+), 7 deletions(-) diff --git a/app/src/control_msg.c b/app/src/control_msg.c index fff93592..11e87e40 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -1,6 +1,7 @@ #include "control_msg.h" #include +#include #include "config.h" #include "buffer_util.h" @@ -24,6 +25,16 @@ write_string(const char *utf8, size_t max_len, unsigned char *buf) { return 2 + len; } +static uint16_t +to_fixed_point_16(float f) { + SDL_assert(f >= 0.0f && f <= 1.0f); + uint32_t u = f * 0x1p16f; // 2^16 + if (u >= 0xffff) { + u = 0xffff; + } + return (uint16_t) u; +} + size_t control_msg_serialize(const struct control_msg *msg, unsigned char *buf) { buf[0] = msg->type; @@ -43,6 +54,14 @@ control_msg_serialize(const struct control_msg *msg, unsigned char *buf) { buffer_write32be(&buf[2], msg->inject_mouse_event.buttons); write_position(&buf[6], &msg->inject_mouse_event.position); return 18; + case CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT: + buf[1] = msg->inject_touch_event.action; + buffer_write64be(&buf[2], msg->inject_touch_event.pointer_id); + write_position(&buf[10], &msg->inject_touch_event.position); + uint16_t pressure = + to_fixed_point_16(msg->inject_touch_event.pressure); + buffer_write16be(&buf[22], pressure); + return 24; case CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT: write_position(&buf[1], &msg->inject_scroll_event.position); buffer_write32be(&buf[13], diff --git a/app/src/control_msg.h b/app/src/control_msg.h index 308d54a3..546564cf 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -19,6 +19,7 @@ enum control_msg_type { CONTROL_MSG_TYPE_INJECT_KEYCODE, CONTROL_MSG_TYPE_INJECT_TEXT, CONTROL_MSG_TYPE_INJECT_MOUSE_EVENT, + CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT, CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON, CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL, @@ -50,6 +51,12 @@ struct control_msg { enum android_motionevent_buttons buttons; struct position position; } inject_mouse_event; + struct { + enum android_motionevent_action action; + uint64_t pointer_id; + struct position position; + float pressure; + } inject_touch_event; struct { struct position position; int32_t hscroll; diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c index c0c501f2..ea06211a 100644 --- a/app/tests/test_control_msg_serialize.c +++ b/app/tests/test_control_msg_serialize.c @@ -100,6 +100,41 @@ static void test_serialize_inject_mouse_event(void) { assert(!memcmp(buf, expected, sizeof(expected))); } +static void test_serialize_inject_touch_event(void) { + struct control_msg msg = { + .type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, + .inject_touch_event = { + .action = AMOTION_EVENT_ACTION_DOWN, + .pointer_id = 0x1234567887654321L, + .position = { + .point = { + .x = 100, + .y = 200, + }, + .screen_size = { + .width = 1080, + .height = 1920, + }, + }, + .pressure = 1.0f, + }, + }; + + unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; + int size = control_msg_serialize(&msg, buf); + assert(size == 24); + + const unsigned char expected[] = { + CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, + 0x00, // AKEY_EVENT_ACTION_DOWN + 0x12, 0x34, 0x56, 0x78, 0x87, 0x65, 0x43, 0x21, // pointer id + 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0xc8, // 100 200 + 0x04, 0x38, 0x07, 0x80, // 1080 1920 + 0xff, 0xff, // pressure + }; + assert(!memcmp(buf, expected, sizeof(expected))); +} + static void test_serialize_inject_scroll_event(void) { struct control_msg msg = { .type = CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT, @@ -237,6 +272,7 @@ int main(void) { test_serialize_inject_text(); test_serialize_inject_text_long(); test_serialize_inject_mouse_event(); + test_serialize_inject_touch_event(); test_serialize_inject_scroll_event(); test_serialize_back_or_screen_on(); test_serialize_expand_notification_panel(); diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java index a1cd873a..34da7741 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java @@ -8,13 +8,14 @@ public final class ControlMessage { public static final int TYPE_INJECT_KEYCODE = 0; public static final int TYPE_INJECT_TEXT = 1; public static final int TYPE_INJECT_MOUSE_EVENT = 2; - public static final int TYPE_INJECT_SCROLL_EVENT = 3; - public static final int TYPE_BACK_OR_SCREEN_ON = 4; - public static final int TYPE_EXPAND_NOTIFICATION_PANEL = 5; - public static final int TYPE_COLLAPSE_NOTIFICATION_PANEL = 6; - public static final int TYPE_GET_CLIPBOARD = 7; - public static final int TYPE_SET_CLIPBOARD = 8; - public static final int TYPE_SET_SCREEN_POWER_MODE = 9; + public static final int TYPE_INJECT_TOUCH_EVENT = 3; + public static final int TYPE_INJECT_SCROLL_EVENT = 4; + public static final int TYPE_BACK_OR_SCREEN_ON = 5; + public static final int TYPE_EXPAND_NOTIFICATION_PANEL = 6; + public static final int TYPE_COLLAPSE_NOTIFICATION_PANEL = 7; + public static final int TYPE_GET_CLIPBOARD = 8; + public static final int TYPE_SET_CLIPBOARD = 9; + public static final int TYPE_SET_SCREEN_POWER_MODE = 10; private int type; private String text; @@ -22,6 +23,8 @@ public final class ControlMessage { private int action; // KeyEvent.ACTION_* or MotionEvent.ACTION_* or POWER_MODE_* private int keycode; // KeyEvent.KEYCODE_* private int buttons; // MotionEvent.BUTTON_* + private long pointerId; + private float pressure; private Position position; private int hScroll; private int vScroll; @@ -54,6 +57,16 @@ public final class ControlMessage { return msg; } + public static ControlMessage createInjectTouchEvent(int action, long pointerId, Position position, float pressure) { + ControlMessage msg = new ControlMessage(); + msg.type = TYPE_INJECT_TOUCH_EVENT; + msg.action = action; + msg.pointerId = pointerId; + msg.pressure = pressure; + msg.position = position; + return msg; + } + public static ControlMessage createInjectScrollEvent(Position position, int hScroll, int vScroll) { ControlMessage msg = new ControlMessage(); msg.type = TYPE_INJECT_SCROLL_EVENT; @@ -110,6 +123,14 @@ public final class ControlMessage { return buttons; } + public long getPointerId() { + return pointerId; + } + + public float getPressure() { + return pressure; + } + public Position getPosition() { return position; } diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java index 8ced049d..e6a6c905 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java @@ -10,6 +10,7 @@ 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; @@ -62,6 +63,9 @@ public class ControlMessageReader { case ControlMessage.TYPE_INJECT_MOUSE_EVENT: msg = parseInjectMouseEvent(); break; + case ControlMessage.TYPE_INJECT_TOUCH_EVENT: + msg = parseInjectTouchEvent(); + break; case ControlMessage.TYPE_INJECT_SCROLL_EVENT: msg = parseInjectScrollEvent(); break; @@ -130,6 +134,21 @@ public class ControlMessageReader { return ControlMessage.createInjectMouseEvent(action, buttons, position); } + @SuppressWarnings("checkstyle:MagicNumber") + private ControlMessage parseInjectTouchEvent() { + if (buffer.remaining() < INJECT_TOUCH_EVENT_PAYLOAD_LENGTH) { + return null; + } + int action = toUnsigned(buffer.get()); + long pointerId = buffer.getLong(); + Position position = readPosition(buffer); + // 16 bits fixed-point + int pressureInt = toUnsigned(buffer.getShort()); + // convert it to a float between 0 and 1 (0x1p16f is 2^16 as float) + float pressure = pressureInt == 0xffff ? 1f : (pressureInt / 0x1p16f); + return ControlMessage.createInjectTouchEvent(action, pointerId, position, pressure); + } + private ControlMessage parseInjectScrollEvent() { if (buffer.remaining() < INJECT_SCROLL_EVENT_PAYLOAD_LENGTH) { return null; diff --git a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java index f0c643d4..33380295 100644 --- a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java @@ -105,6 +105,37 @@ public class ControlMessageReaderTest { Assert.assertEquals(1920, event.getPosition().getScreenSize().getHeight()); } + @Test + @SuppressWarnings("checkstyle:MagicNumber") + public void testParseTouchEvent() throws IOException { + ControlMessageReader reader = new ControlMessageReader(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(ControlMessage.TYPE_INJECT_TOUCH_EVENT); + dos.writeByte(MotionEvent.ACTION_DOWN); + dos.writeLong(-42); // pointerId + dos.writeInt(100); + dos.writeInt(200); + dos.writeShort(1080); + dos.writeShort(1920); + dos.writeShort(0xffff); // pressure + + byte[] packet = bos.toByteArray(); + + reader.readFrom(new ByteArrayInputStream(packet)); + ControlMessage event = reader.next(); + + Assert.assertEquals(ControlMessage.TYPE_INJECT_TOUCH_EVENT, event.getType()); + Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction()); + Assert.assertEquals(-42, event.getPointerId()); + Assert.assertEquals(100, event.getPosition().getPoint().getX()); + Assert.assertEquals(200, event.getPosition().getPoint().getY()); + Assert.assertEquals(1080, event.getPosition().getScreenSize().getWidth()); + Assert.assertEquals(1920, event.getPosition().getScreenSize().getHeight()); + Assert.assertEquals(1f, event.getPressure(), 0f); // must be exact + } + @Test @SuppressWarnings("checkstyle:MagicNumber") public void testParseScrollEvent() throws IOException { From f765aae352ecc0404ab483ec3255e7e1608ee128 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 15 Sep 2019 22:28:59 +0200 Subject: [PATCH 0066/2244] Inject touch events on the server On receiving an "inject touch" control message, update the local pointers state and inject touches. --- .../com/genymobile/scrcpy/Controller.java | 64 +++++++++++ .../java/com/genymobile/scrcpy/Pointer.java | 55 ++++++++++ .../com/genymobile/scrcpy/PointersState.java | 103 ++++++++++++++++++ 3 files changed, 222 insertions(+) create mode 100644 server/src/main/java/com/genymobile/scrcpy/Pointer.java create mode 100644 server/src/main/java/com/genymobile/scrcpy/PointersState.java diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index cbc4aec4..5ea712d4 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -23,10 +23,18 @@ public class Controller { private final MotionEvent.PointerProperties[] mousePointerProperties = {new MotionEvent.PointerProperties()}; private final MotionEvent.PointerCoords[] mousePointerCoords = {new MotionEvent.PointerCoords()}; + private long lastTouchDown; + private final PointersState pointersState = new PointersState(); + private final MotionEvent.PointerProperties[] touchPointerProperties = + new MotionEvent.PointerProperties[PointersState.MAX_POINTERS]; + private final MotionEvent.PointerCoords[] touchPointerCoords = + new MotionEvent.PointerCoords[PointersState.MAX_POINTERS]; + public Controller(Device device, DesktopConnection connection) { this.device = device; this.connection = connection; initMousePointer(); + initTouchPointers(); sender = new DeviceMessageSender(connection); } @@ -41,6 +49,20 @@ public class Controller { coords.size = 1; } + private void initTouchPointers() { + for (int i = 0; i < PointersState.MAX_POINTERS; ++i) { + MotionEvent.PointerProperties props = new MotionEvent.PointerProperties(); + props.toolType = MotionEvent.TOOL_TYPE_FINGER; + + MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords(); + coords.orientation = 0; + coords.size = 1; + + touchPointerProperties[i] = props; + touchPointerCoords[i] = coords; + } + } + private void setMousePointerCoords(Point point) { MotionEvent.PointerCoords coords = mousePointerCoords[0]; coords.x = point.getX(); @@ -90,6 +112,9 @@ public class Controller { case ControlMessage.TYPE_INJECT_MOUSE_EVENT: injectMouse(msg.getAction(), msg.getButtons(), msg.getPosition()); break; + case ControlMessage.TYPE_INJECT_TOUCH_EVENT: + injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure()); + break; case ControlMessage.TYPE_INJECT_SCROLL_EVENT: injectScroll(msg.getPosition(), msg.getHScroll(), msg.getVScroll()); break; @@ -164,6 +189,45 @@ public class Controller { return injectEvent(event); } + private boolean injectTouch(int action, long pointerId, Position position, float pressure) { + long now = SystemClock.uptimeMillis(); + + Point point = device.getPhysicalPoint(position); + if (point == null) { + // ignore event + return false; + } + + int pointerIndex = pointersState.getPointerIndex(pointerId); + if (pointerIndex == -1) { + Ln.w("Too many pointers for touch event"); + return false; + } + Pointer pointer = pointersState.get(pointerIndex); + pointer.setPoint(point); + pointer.setPressure(pressure); + pointer.setUp(action == MotionEvent.ACTION_UP); + + int pointerCount = pointersState.update(touchPointerProperties, touchPointerCoords); + + if (pointerCount == 1) { + if (action == MotionEvent.ACTION_DOWN) { + lastTouchDown = now; + } + } else { + // secondary pointers must use ACTION_POINTER_* ORed with the pointerIndex + if (action == MotionEvent.ACTION_UP) { + action = MotionEvent.ACTION_POINTER_UP | (pointerIndex << MotionEvent.ACTION_POINTER_INDEX_SHIFT); + } else if (action == MotionEvent.ACTION_DOWN) { + action = MotionEvent.ACTION_POINTER_DOWN | (pointerIndex << MotionEvent.ACTION_POINTER_INDEX_SHIFT); + } + } + + MotionEvent event = MotionEvent.obtain(lastTouchDown, now, action, pointerCount, touchPointerProperties, + touchPointerCoords, 0, 0, 1f, 1f, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0); + return injectEvent(event); + } + private boolean injectScroll(Position position, int hScroll, int vScroll) { long now = SystemClock.uptimeMillis(); Point point = device.getPhysicalPoint(position); diff --git a/server/src/main/java/com/genymobile/scrcpy/Pointer.java b/server/src/main/java/com/genymobile/scrcpy/Pointer.java new file mode 100644 index 00000000..b89cc256 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/Pointer.java @@ -0,0 +1,55 @@ +package com.genymobile.scrcpy; + +public class Pointer { + + /** + * Pointer id as received from the client. + */ + private final long id; + + /** + * Local pointer id, using the lowest possible values to fill the {@link android.view.MotionEvent.PointerProperties PointerProperties}. + */ + private final int localId; + + private Point point; + private float pressure; + private boolean up; + + public Pointer(long id, int localId) { + this.id = id; + this.localId = localId; + } + + public long getId() { + return id; + } + + public int getLocalId() { + return localId; + } + + public Point getPoint() { + return point; + } + + public void setPoint(Point point) { + this.point = point; + } + + public float getPressure() { + return pressure; + } + + public void setPressure(float pressure) { + this.pressure = pressure; + } + + public boolean isUp() { + return up; + } + + public void setUp(boolean up) { + this.up = up; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/PointersState.java b/server/src/main/java/com/genymobile/scrcpy/PointersState.java new file mode 100644 index 00000000..d8daaff2 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/PointersState.java @@ -0,0 +1,103 @@ +package com.genymobile.scrcpy; + +import android.view.MotionEvent; + +import java.util.ArrayList; +import java.util.List; + +public class PointersState { + + public static final int MAX_POINTERS = 10; + + private final List pointers = new ArrayList<>(); + + private int indexOf(long id) { + for (int i = 0; i < pointers.size(); ++i) { + Pointer pointer = pointers.get(i); + if (pointer.getId() == id) { + return i; + } + } + return -1; + } + + private boolean isLocalIdAvailable(int localId) { + for (int i = 0; i < pointers.size(); ++i) { + Pointer pointer = pointers.get(i); + if (pointer.getLocalId() == localId) { + return false; + } + } + return true; + } + + private int nextUnusedLocalId() { + for (int localId = 0; localId < MAX_POINTERS; ++localId) { + if (isLocalIdAvailable(localId)) { + return localId; + } + } + return -1; + } + + public Pointer get(int index) { + return pointers.get(index); + } + + public int getPointerIndex(long id) { + int index = indexOf(id); + if (index != -1) { + // already exists, return it + return index; + } + if (pointers.size() >= MAX_POINTERS) { + // it's full + return -1; + } + // id 0 is reserved for mouse events + int localId = nextUnusedLocalId(); + if (localId == -1) { + throw new AssertionError("pointers.size() < maxFingers implies that a local id is available"); + } + Pointer pointer = new Pointer(id, localId); + pointers.add(pointer); + // return the index of the pointer + return pointers.size() - 1; + } + + /** + * Initialize the motion event parameters. + * + * @param props the pointer properties + * @param coords the pointer coordinates + * @return The number of items initialized (the number of pointers). + */ + public int update(MotionEvent.PointerProperties[] props, MotionEvent.PointerCoords[] coords) { + int count = pointers.size(); + for (int i = 0; i < count; ++i) { + Pointer pointer = pointers.get(i); + + // id 0 is reserved for mouse events + props[i].id = pointer.getLocalId(); + + Point point = pointer.getPoint(); + coords[i].x = point.getX(); + coords[i].y = point.getY(); + coords[i].pressure = pointer.getPressure(); + } + cleanUp(); + return count; + } + + /** + * Remove all pointers which are UP. + */ + private void cleanUp() { + for (int i = pointers.size() - 1; i >= 0; --i) { + Pointer pointer = pointers.get(i); + if (pointer.isUp()) { + pointers.remove(i); + } + } + } +} From b5a2d99bc24b6ae1cd040bef7086e4847cae4d07 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 22 Sep 2019 21:30:05 +0200 Subject: [PATCH 0067/2244] Send touch events from the client On SDL touch events, send control messages to the server. --- app/src/event_converter.c | 28 ++++++++++++++++++++++++++++ app/src/event_converter.h | 4 ++++ app/src/input_manager.c | 11 +++++++++++ app/src/input_manager.h | 4 ++++ app/src/scrcpy.c | 5 +++++ 5 files changed, 52 insertions(+) diff --git a/app/src/event_converter.c b/app/src/event_converter.c index da4b2e30..e9fbe13b 100644 --- a/app/src/event_converter.c +++ b/app/src/event_converter.c @@ -207,6 +207,34 @@ convert_mouse_motion(const SDL_MouseMotionEvent *from, struct size screen_size, return true; } +static bool +convert_touch_action(SDL_EventType from, enum android_motionevent_action *to) { + switch (from) { + MAP(SDL_FINGERMOTION, AMOTION_EVENT_ACTION_MOVE); + MAP(SDL_FINGERDOWN, AMOTION_EVENT_ACTION_DOWN); + MAP(SDL_FINGERUP, AMOTION_EVENT_ACTION_UP); + FAIL; + } +} + +bool +convert_touch(const SDL_TouchFingerEvent *from, struct size screen_size, + struct control_msg *to) { + to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT; + + if (!convert_touch_action(from->type, &to->inject_touch_event.action)) { + return false; + } + + to->inject_touch_event.pointer_id = from->fingerId; + to->inject_touch_event.position.screen_size = screen_size; + // SDL touch event coordinates are normalized in the range [0; 1] + to->inject_touch_event.position.point.x = from->x * screen_size.width; + to->inject_touch_event.position.point.y = from->y * screen_size.height; + to->inject_touch_event.pressure = from->pressure; + return true; +} + bool convert_mouse_wheel(const SDL_MouseWheelEvent *from, struct position position, struct control_msg *to) { diff --git a/app/src/event_converter.h b/app/src/event_converter.h index e0b24c15..f6f136a3 100644 --- a/app/src/event_converter.h +++ b/app/src/event_converter.h @@ -30,6 +30,10 @@ bool convert_mouse_motion(const SDL_MouseMotionEvent *from, struct size screen_size, struct control_msg *to); +bool +convert_touch(const SDL_TouchFingerEvent *from, struct size screen_size, + struct control_msg *to); + // on Android, a scroll event requires the current mouse position bool convert_mouse_wheel(const SDL_MouseWheelEvent *from, struct position position, diff --git a/app/src/input_manager.c b/app/src/input_manager.c index cf2a7519..2123f241 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -397,6 +397,17 @@ input_manager_process_mouse_motion(struct input_manager *input_manager, } } +void +input_manager_process_touch(struct input_manager *input_manager, + const SDL_TouchFingerEvent *event) { + struct control_msg msg; + if (convert_touch(event, input_manager->screen->frame_size, &msg)) { + if (!controller_push_msg(input_manager->controller, &msg)) { + LOGW("Could not request 'inject touch event'"); + } + } +} + static bool is_outside_device_screen(struct input_manager *input_manager, int x, int y) { diff --git a/app/src/input_manager.h b/app/src/input_manager.h index 61a0447f..0009cb81 100644 --- a/app/src/input_manager.h +++ b/app/src/input_manager.h @@ -29,6 +29,10 @@ void input_manager_process_mouse_motion(struct input_manager *input_manager, const SDL_MouseMotionEvent *event); +void +input_manager_process_touch(struct input_manager *input_manager, + const SDL_TouchFingerEvent *event); + void input_manager_process_mouse_button(struct input_manager *input_manager, const SDL_MouseButtonEvent *event, diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index defcb751..c219c9e5 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -181,6 +181,11 @@ handle_event(SDL_Event *event, bool control) { input_manager_process_mouse_button(&input_manager, &event->button, control); break; + case SDL_FINGERMOTION: + case SDL_FINGERDOWN: + case SDL_FINGERUP: + input_manager_process_touch(&input_manager, &event->tfinger); + break; case SDL_DROPFILE: { if (!control) { break; From 30168f042890f01071d15a7d7c88050b1825aa73 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 22 Sep 2019 21:33:16 +0200 Subject: [PATCH 0068/2244] Ignore duplicate mouse events In SDL, a touch event may simulate an identical mouse event. Since we already handle touch event, ignore these duplicates. --- app/src/input_manager.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 2123f241..db15da75 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -389,6 +389,10 @@ input_manager_process_mouse_motion(struct input_manager *input_manager, // do not send motion events when no button is pressed return; } + if (event->which == SDL_TOUCH_MOUSEID) { + // simulated from touch events, so it's a duplicate + return; + } struct control_msg msg; if (convert_mouse_motion(event, input_manager->screen->frame_size, &msg)) { if (!controller_push_msg(input_manager->controller, &msg)) { @@ -419,6 +423,10 @@ void input_manager_process_mouse_button(struct input_manager *input_manager, const SDL_MouseButtonEvent *event, bool control) { + if (event->which == SDL_TOUCH_MOUSEID) { + // simulated from touch events, so it's a duplicate + return; + } if (event->type == SDL_MOUSEBUTTONDOWN) { if (control && event->button == SDL_BUTTON_RIGHT) { press_back_or_turn_screen_on(input_manager->controller); From 280d5b718cb0416adb164d363ffe077060ea87b1 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 3 Oct 2019 19:59:01 +0200 Subject: [PATCH 0069/2244] Use common pointers for mouse and touch The mouse is a pointer like any other. --- .../com/genymobile/scrcpy/Controller.java | 66 +++++-------------- .../com/genymobile/scrcpy/PointersState.java | 1 + 2 files changed, 16 insertions(+), 51 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index 5ea712d4..479f82cd 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -19,10 +19,6 @@ public class Controller { private final KeyCharacterMap charMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD); - private long lastMouseDown; - private final MotionEvent.PointerProperties[] mousePointerProperties = {new MotionEvent.PointerProperties()}; - private final MotionEvent.PointerCoords[] mousePointerCoords = {new MotionEvent.PointerCoords()}; - private long lastTouchDown; private final PointersState pointersState = new PointersState(); private final MotionEvent.PointerProperties[] touchPointerProperties = @@ -33,22 +29,10 @@ public class Controller { public Controller(Device device, DesktopConnection connection) { this.device = device; this.connection = connection; - initMousePointer(); initTouchPointers(); sender = new DeviceMessageSender(connection); } - private void initMousePointer() { - MotionEvent.PointerProperties props = mousePointerProperties[0]; - props.id = 0; - props.toolType = MotionEvent.TOOL_TYPE_FINGER; - - MotionEvent.PointerCoords coords = mousePointerCoords[0]; - coords.orientation = 0; - coords.pressure = 1; - coords.size = 1; - } - private void initTouchPointers() { for (int i = 0; i < PointersState.MAX_POINTERS; ++i) { MotionEvent.PointerProperties props = new MotionEvent.PointerProperties(); @@ -63,18 +47,6 @@ public class Controller { } } - private void setMousePointerCoords(Point point) { - MotionEvent.PointerCoords coords = mousePointerCoords[0]; - coords.x = point.getX(); - coords.y = point.getY(); - } - - private void setScroll(int hScroll, int vScroll) { - MotionEvent.PointerCoords coords = mousePointerCoords[0]; - coords.setAxisValue(MotionEvent.AXIS_HSCROLL, hScroll); - coords.setAxisValue(MotionEvent.AXIS_VSCROLL, vScroll); - } - @SuppressWarnings("checkstyle:MagicNumber") public void control() throws IOException { // on start, power on the device @@ -110,10 +82,10 @@ public class Controller { injectText(msg.getText()); break; case ControlMessage.TYPE_INJECT_MOUSE_EVENT: - injectMouse(msg.getAction(), msg.getButtons(), msg.getPosition()); + injectTouch(msg.getAction(), PointersState.POINTER_ID_MOUSE, msg.getPosition(), 1, msg.getButtons()); break; case ControlMessage.TYPE_INJECT_TOUCH_EVENT: - injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure()); + injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), 0); break; case ControlMessage.TYPE_INJECT_SCROLL_EVENT: injectScroll(msg.getPosition(), msg.getHScroll(), msg.getVScroll()); @@ -173,23 +145,7 @@ public class Controller { return successCount; } - private boolean injectMouse(int action, int buttons, Position position) { - long now = SystemClock.uptimeMillis(); - if (action == MotionEvent.ACTION_DOWN) { - lastMouseDown = now; - } - Point point = device.getPhysicalPoint(position); - if (point == null) { - // ignore event - return false; - } - setMousePointerCoords(point); - MotionEvent event = MotionEvent.obtain(lastMouseDown, now, action, 1, mousePointerProperties, - mousePointerCoords, 0, buttons, 1f, 1f, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0); - return injectEvent(event); - } - - private boolean injectTouch(int action, long pointerId, Position position, float pressure) { + private boolean injectTouch(int action, long pointerId, Position position, float pressure, int buttons) { long now = SystemClock.uptimeMillis(); Point point = device.getPhysicalPoint(position); @@ -235,10 +191,18 @@ public class Controller { // ignore event return false; } - setMousePointerCoords(point); - setScroll(hScroll, vScroll); - MotionEvent event = MotionEvent.obtain(lastMouseDown, now, MotionEvent.ACTION_SCROLL, 1, - mousePointerProperties, mousePointerCoords, 0, 0, 1f, 1f, 0, 0, InputDevice.SOURCE_MOUSE, 0); + + MotionEvent.PointerProperties props = touchPointerProperties[0]; + props.id = 0; + + MotionEvent.PointerCoords coords = touchPointerCoords[0]; + coords.x = point.getX(); + coords.y = point.getY(); + coords.setAxisValue(MotionEvent.AXIS_HSCROLL, hScroll); + coords.setAxisValue(MotionEvent.AXIS_VSCROLL, vScroll); + + MotionEvent event = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, + touchPointerProperties, touchPointerCoords, 0, 0, 1f, 1f, 0, 0, InputDevice.SOURCE_MOUSE, 0); return injectEvent(event); } diff --git a/server/src/main/java/com/genymobile/scrcpy/PointersState.java b/server/src/main/java/com/genymobile/scrcpy/PointersState.java index d8daaff2..eab258b1 100644 --- a/server/src/main/java/com/genymobile/scrcpy/PointersState.java +++ b/server/src/main/java/com/genymobile/scrcpy/PointersState.java @@ -8,6 +8,7 @@ import java.util.List; public class PointersState { public static final int MAX_POINTERS = 10; + public static final long POINTER_ID_MOUSE = -1; private final List pointers = new ArrayList<>(); From 7e1d52c1194ae262bd34489419a26e148d899138 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 3 Oct 2019 20:00:50 +0200 Subject: [PATCH 0070/2244] Rename "touch pointer" to "pointer" There are only touch pointers now, mouse pointers have been removed. --- .../com/genymobile/scrcpy/Controller.java | 26 +++++++++---------- 1 file changed, 13 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 479f82cd..8c5e645a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -21,19 +21,19 @@ public class Controller { private long lastTouchDown; private final PointersState pointersState = new PointersState(); - private final MotionEvent.PointerProperties[] touchPointerProperties = + private final MotionEvent.PointerProperties[] pointerProperties = new MotionEvent.PointerProperties[PointersState.MAX_POINTERS]; - private final MotionEvent.PointerCoords[] touchPointerCoords = + private final MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[PointersState.MAX_POINTERS]; public Controller(Device device, DesktopConnection connection) { this.device = device; this.connection = connection; - initTouchPointers(); + initPointers(); sender = new DeviceMessageSender(connection); } - private void initTouchPointers() { + private void initPointers() { for (int i = 0; i < PointersState.MAX_POINTERS; ++i) { MotionEvent.PointerProperties props = new MotionEvent.PointerProperties(); props.toolType = MotionEvent.TOOL_TYPE_FINGER; @@ -42,8 +42,8 @@ public class Controller { coords.orientation = 0; coords.size = 1; - touchPointerProperties[i] = props; - touchPointerCoords[i] = coords; + pointerProperties[i] = props; + pointerCoords[i] = coords; } } @@ -164,7 +164,7 @@ public class Controller { pointer.setPressure(pressure); pointer.setUp(action == MotionEvent.ACTION_UP); - int pointerCount = pointersState.update(touchPointerProperties, touchPointerCoords); + int pointerCount = pointersState.update(pointerProperties, pointerCoords); if (pointerCount == 1) { if (action == MotionEvent.ACTION_DOWN) { @@ -179,8 +179,8 @@ public class Controller { } } - MotionEvent event = MotionEvent.obtain(lastTouchDown, now, action, pointerCount, touchPointerProperties, - touchPointerCoords, 0, 0, 1f, 1f, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0); + MotionEvent event = MotionEvent.obtain(lastTouchDown, now, action, pointerCount, pointerProperties, + pointerCoords, 0, 0, 1f, 1f, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0); return injectEvent(event); } @@ -192,17 +192,17 @@ public class Controller { return false; } - MotionEvent.PointerProperties props = touchPointerProperties[0]; + MotionEvent.PointerProperties props = pointerProperties[0]; props.id = 0; - MotionEvent.PointerCoords coords = touchPointerCoords[0]; + MotionEvent.PointerCoords coords = pointerCoords[0]; coords.x = point.getX(); coords.y = point.getY(); coords.setAxisValue(MotionEvent.AXIS_HSCROLL, hScroll); coords.setAxisValue(MotionEvent.AXIS_VSCROLL, vScroll); - MotionEvent event = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, - touchPointerProperties, touchPointerCoords, 0, 0, 1f, 1f, 0, 0, InputDevice.SOURCE_MOUSE, 0); + MotionEvent event = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, + pointerCoords, 0, 0, 1f, 1f, 0, 0, InputDevice.SOURCE_MOUSE, 0); return injectEvent(event); } From 6220456def65e00696a268ac654756a8b22a96a7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 3 Oct 2019 20:14:12 +0200 Subject: [PATCH 0071/2244] Merge mouse and touch events Both are handled the very same way on the device. --- app/src/control_msg.c | 8 +--- app/src/control_msg.h | 7 +-- app/src/event_converter.c | 47 ++++++++++--------- app/tests/test_control_msg_serialize.c | 38 ++------------- .../com/genymobile/scrcpy/ControlMessage.java | 30 +++++------- .../scrcpy/ControlMessageReader.java | 16 +------ .../com/genymobile/scrcpy/Controller.java | 3 -- .../com/genymobile/scrcpy/PointersState.java | 1 - .../scrcpy/ControlMessageReaderTest.java | 31 +----------- 9 files changed, 48 insertions(+), 133 deletions(-) diff --git a/app/src/control_msg.c b/app/src/control_msg.c index 11e87e40..e042dc5a 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -49,11 +49,6 @@ control_msg_serialize(const struct control_msg *msg, unsigned char *buf) { CONTROL_MSG_TEXT_MAX_LENGTH, &buf[1]); return 1 + len; } - case CONTROL_MSG_TYPE_INJECT_MOUSE_EVENT: - buf[1] = msg->inject_mouse_event.action; - buffer_write32be(&buf[2], msg->inject_mouse_event.buttons); - write_position(&buf[6], &msg->inject_mouse_event.position); - return 18; case CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT: buf[1] = msg->inject_touch_event.action; buffer_write64be(&buf[2], msg->inject_touch_event.pointer_id); @@ -61,7 +56,8 @@ control_msg_serialize(const struct control_msg *msg, unsigned char *buf) { uint16_t pressure = to_fixed_point_16(msg->inject_touch_event.pressure); buffer_write16be(&buf[22], pressure); - return 24; + buffer_write32be(&buf[24], msg->inject_touch_event.buttons); + return 28; case CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT: write_position(&buf[1], &msg->inject_scroll_event.position); buffer_write32be(&buf[13], diff --git a/app/src/control_msg.h b/app/src/control_msg.h index 546564cf..2f319d9d 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -15,10 +15,11 @@ #define CONTROL_MSG_SERIALIZED_MAX_SIZE \ (3 + CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH) +#define POINTER_ID_MOUSE UINT64_C(-1); + enum control_msg_type { CONTROL_MSG_TYPE_INJECT_KEYCODE, CONTROL_MSG_TYPE_INJECT_TEXT, - CONTROL_MSG_TYPE_INJECT_MOUSE_EVENT, CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT, CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON, @@ -49,10 +50,6 @@ struct control_msg { struct { enum android_motionevent_action action; enum android_motionevent_buttons buttons; - struct position position; - } inject_mouse_event; - struct { - enum android_motionevent_action action; uint64_t pointer_id; struct position position; float pressure; diff --git a/app/src/event_converter.c b/app/src/event_converter.c index e9fbe13b..13abfab2 100644 --- a/app/src/event_converter.c +++ b/app/src/event_converter.c @@ -128,15 +128,6 @@ convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod) { } } -static bool -convert_mouse_action(SDL_EventType from, enum android_motionevent_action *to) { - switch (from) { - MAP(SDL_MOUSEBUTTONDOWN, AMOTION_EVENT_ACTION_DOWN); - MAP(SDL_MOUSEBUTTONUP, AMOTION_EVENT_ACTION_UP); - FAIL; - } -} - static enum android_motionevent_buttons convert_mouse_buttons(uint32_t state) { enum android_motionevent_buttons buttons = 0; @@ -176,20 +167,31 @@ convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to) { return true; } +static bool +convert_mouse_action(SDL_EventType from, enum android_motionevent_action *to) { + switch (from) { + MAP(SDL_MOUSEBUTTONDOWN, AMOTION_EVENT_ACTION_DOWN); + MAP(SDL_MOUSEBUTTONUP, AMOTION_EVENT_ACTION_UP); + FAIL; + } +} + bool convert_mouse_button(const SDL_MouseButtonEvent *from, struct size screen_size, struct control_msg *to) { - to->type = CONTROL_MSG_TYPE_INJECT_MOUSE_EVENT; + to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT; - if (!convert_mouse_action(from->type, &to->inject_mouse_event.action)) { + if (!convert_mouse_action(from->type, &to->inject_touch_event.action)) { return false; } - to->inject_mouse_event.buttons = + to->inject_touch_event.pointer_id = POINTER_ID_MOUSE; + to->inject_touch_event.position.screen_size = screen_size; + to->inject_touch_event.position.point.x = from->x; + to->inject_touch_event.position.point.y = from->y; + to->inject_touch_event.pressure = 1.f; + to->inject_touch_event.buttons = convert_mouse_buttons(SDL_BUTTON(from->button)); - to->inject_mouse_event.position.screen_size = screen_size; - to->inject_mouse_event.position.point.x = from->x; - to->inject_mouse_event.position.point.y = from->y; return true; } @@ -197,12 +199,14 @@ convert_mouse_button(const SDL_MouseButtonEvent *from, struct size screen_size, bool convert_mouse_motion(const SDL_MouseMotionEvent *from, struct size screen_size, struct control_msg *to) { - to->type = CONTROL_MSG_TYPE_INJECT_MOUSE_EVENT; - to->inject_mouse_event.action = AMOTION_EVENT_ACTION_MOVE; - to->inject_mouse_event.buttons = convert_mouse_buttons(from->state); - to->inject_mouse_event.position.screen_size = screen_size; - to->inject_mouse_event.position.point.x = from->x; - to->inject_mouse_event.position.point.y = from->y; + to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT; + 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_size; + to->inject_touch_event.position.point.x = from->x; + to->inject_touch_event.position.point.y = from->y; + to->inject_touch_event.pressure = 1.f; + to->inject_touch_event.buttons = convert_mouse_buttons(from->state); return true; } @@ -232,6 +236,7 @@ convert_touch(const SDL_TouchFingerEvent *from, struct size screen_size, to->inject_touch_event.position.point.x = from->x * screen_size.width; to->inject_touch_event.position.point.y = from->y * screen_size.height; to->inject_touch_event.pressure = from->pressure; + to->inject_touch_event.buttons = 0; return true; } diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c index ea06211a..83ab011f 100644 --- a/app/tests/test_control_msg_serialize.c +++ b/app/tests/test_control_msg_serialize.c @@ -67,39 +67,6 @@ static void test_serialize_inject_text_long(void) { assert(!memcmp(buf, expected, sizeof(expected))); } -static void test_serialize_inject_mouse_event(void) { - struct control_msg msg = { - .type = CONTROL_MSG_TYPE_INJECT_MOUSE_EVENT, - .inject_mouse_event = { - .action = AMOTION_EVENT_ACTION_DOWN, - .buttons = AMOTION_EVENT_BUTTON_PRIMARY, - .position = { - .point = { - .x = 260, - .y = 1026, - }, - .screen_size = { - .width = 1080, - .height = 1920, - }, - }, - }, - }; - - unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; - int size = control_msg_serialize(&msg, buf); - assert(size == 18); - - const unsigned char expected[] = { - CONTROL_MSG_TYPE_INJECT_MOUSE_EVENT, - 0x00, // AKEY_EVENT_ACTION_DOWN - 0x00, 0x00, 0x00, 0x01, // AMOTION_EVENT_BUTTON_PRIMARY - 0x00, 0x00, 0x01, 0x04, 0x00, 0x00, 0x04, 0x02, // 260 1026 - 0x04, 0x38, 0x07, 0x80, // 1080 1920 - }; - assert(!memcmp(buf, expected, sizeof(expected))); -} - static void test_serialize_inject_touch_event(void) { struct control_msg msg = { .type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, @@ -117,12 +84,13 @@ static void test_serialize_inject_touch_event(void) { }, }, .pressure = 1.0f, + .buttons = AMOTION_EVENT_BUTTON_PRIMARY, }, }; unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; int size = control_msg_serialize(&msg, buf); - assert(size == 24); + assert(size == 28); const unsigned char expected[] = { CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, @@ -131,6 +99,7 @@ static void test_serialize_inject_touch_event(void) { 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0xc8, // 100 200 0x04, 0x38, 0x07, 0x80, // 1080 1920 0xff, 0xff, // pressure + 0x00, 0x00, 0x00, 0x01 // AMOTION_EVENT_BUTTON_PRIMARY }; assert(!memcmp(buf, expected, sizeof(expected))); } @@ -271,7 +240,6 @@ int main(void) { test_serialize_inject_keycode(); test_serialize_inject_text(); test_serialize_inject_text_long(); - test_serialize_inject_mouse_event(); test_serialize_inject_touch_event(); test_serialize_inject_scroll_event(); test_serialize_back_or_screen_on(); diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java index 34da7741..30c05a3b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java @@ -7,15 +7,14 @@ public final class ControlMessage { public static final int TYPE_INJECT_KEYCODE = 0; public static final int TYPE_INJECT_TEXT = 1; - public static final int TYPE_INJECT_MOUSE_EVENT = 2; - public static final int TYPE_INJECT_TOUCH_EVENT = 3; - public static final int TYPE_INJECT_SCROLL_EVENT = 4; - public static final int TYPE_BACK_OR_SCREEN_ON = 5; - public static final int TYPE_EXPAND_NOTIFICATION_PANEL = 6; - public static final int TYPE_COLLAPSE_NOTIFICATION_PANEL = 7; - public static final int TYPE_GET_CLIPBOARD = 8; - public static final int TYPE_SET_CLIPBOARD = 9; - public static final int TYPE_SET_SCREEN_POWER_MODE = 10; + public static final int TYPE_INJECT_TOUCH_EVENT = 2; + public static final int TYPE_INJECT_SCROLL_EVENT = 3; + public static final int TYPE_BACK_OR_SCREEN_ON = 4; + public static final int TYPE_EXPAND_NOTIFICATION_PANEL = 5; + public static final int TYPE_COLLAPSE_NOTIFICATION_PANEL = 6; + public static final int TYPE_GET_CLIPBOARD = 7; + public static final int TYPE_SET_CLIPBOARD = 8; + public static final int TYPE_SET_SCREEN_POWER_MODE = 9; private int type; private String text; @@ -48,22 +47,15 @@ public final class ControlMessage { return msg; } - public static ControlMessage createInjectMouseEvent(int action, int buttons, Position position) { - ControlMessage msg = new ControlMessage(); - msg.type = TYPE_INJECT_MOUSE_EVENT; - msg.action = action; - msg.buttons = buttons; - msg.position = position; - return msg; - } - - public static ControlMessage createInjectTouchEvent(int action, long pointerId, Position position, float pressure) { + public static ControlMessage createInjectTouchEvent(int action, long pointerId, Position position, float pressure, + int buttons) { ControlMessage msg = new ControlMessage(); msg.type = TYPE_INJECT_TOUCH_EVENT; msg.action = action; msg.pointerId = pointerId; msg.pressure = pressure; msg.position = position; + msg.buttons = buttons; return msg; } diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java index e6a6c905..2f8b5177 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java @@ -60,9 +60,6 @@ public class ControlMessageReader { case ControlMessage.TYPE_INJECT_TEXT: msg = parseInjectText(); break; - case ControlMessage.TYPE_INJECT_MOUSE_EVENT: - msg = parseInjectMouseEvent(); - break; case ControlMessage.TYPE_INJECT_TOUCH_EVENT: msg = parseInjectTouchEvent(); break; @@ -124,16 +121,6 @@ public class ControlMessageReader { return ControlMessage.createInjectText(text); } - private ControlMessage parseInjectMouseEvent() { - if (buffer.remaining() < INJECT_MOUSE_EVENT_PAYLOAD_LENGTH) { - return null; - } - int action = toUnsigned(buffer.get()); - int buttons = buffer.getInt(); - Position position = readPosition(buffer); - return ControlMessage.createInjectMouseEvent(action, buttons, position); - } - @SuppressWarnings("checkstyle:MagicNumber") private ControlMessage parseInjectTouchEvent() { if (buffer.remaining() < INJECT_TOUCH_EVENT_PAYLOAD_LENGTH) { @@ -146,7 +133,8 @@ public class ControlMessageReader { int pressureInt = toUnsigned(buffer.getShort()); // convert it to a float between 0 and 1 (0x1p16f is 2^16 as float) float pressure = pressureInt == 0xffff ? 1f : (pressureInt / 0x1p16f); - return ControlMessage.createInjectTouchEvent(action, pointerId, position, pressure); + int buttons = buffer.getInt(); + return ControlMessage.createInjectTouchEvent(action, pointerId, position, pressure, buttons); } private ControlMessage parseInjectScrollEvent() { diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index 8c5e645a..19bf9d94 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -81,9 +81,6 @@ public class Controller { case ControlMessage.TYPE_INJECT_TEXT: injectText(msg.getText()); break; - case ControlMessage.TYPE_INJECT_MOUSE_EVENT: - injectTouch(msg.getAction(), PointersState.POINTER_ID_MOUSE, msg.getPosition(), 1, msg.getButtons()); - break; case ControlMessage.TYPE_INJECT_TOUCH_EVENT: injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), 0); break; diff --git a/server/src/main/java/com/genymobile/scrcpy/PointersState.java b/server/src/main/java/com/genymobile/scrcpy/PointersState.java index eab258b1..d8daaff2 100644 --- a/server/src/main/java/com/genymobile/scrcpy/PointersState.java +++ b/server/src/main/java/com/genymobile/scrcpy/PointersState.java @@ -8,7 +8,6 @@ import java.util.List; public class PointersState { public static final int MAX_POINTERS = 10; - public static final long POINTER_ID_MOUSE = -1; private final List pointers = new ArrayList<>(); diff --git a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java index 33380295..ede759dc 100644 --- a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java @@ -76,35 +76,6 @@ public class ControlMessageReaderTest { Assert.assertEquals(new String(text, StandardCharsets.US_ASCII), event.getText()); } - @Test - @SuppressWarnings("checkstyle:MagicNumber") - public void testParseMouseEvent() throws IOException { - ControlMessageReader reader = new ControlMessageReader(); - - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - DataOutputStream dos = new DataOutputStream(bos); - dos.writeByte(ControlMessage.TYPE_INJECT_MOUSE_EVENT); - dos.writeByte(MotionEvent.ACTION_DOWN); - dos.writeInt(MotionEvent.BUTTON_PRIMARY); - dos.writeInt(100); - dos.writeInt(200); - dos.writeShort(1080); - dos.writeShort(1920); - - byte[] packet = bos.toByteArray(); - - reader.readFrom(new ByteArrayInputStream(packet)); - ControlMessage event = reader.next(); - - Assert.assertEquals(ControlMessage.TYPE_INJECT_MOUSE_EVENT, event.getType()); - Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction()); - Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getButtons()); - Assert.assertEquals(100, event.getPosition().getPoint().getX()); - Assert.assertEquals(200, event.getPosition().getPoint().getY()); - Assert.assertEquals(1080, event.getPosition().getScreenSize().getWidth()); - Assert.assertEquals(1920, event.getPosition().getScreenSize().getHeight()); - } - @Test @SuppressWarnings("checkstyle:MagicNumber") public void testParseTouchEvent() throws IOException { @@ -120,6 +91,7 @@ public class ControlMessageReaderTest { dos.writeShort(1080); dos.writeShort(1920); dos.writeShort(0xffff); // pressure + dos.writeInt(MotionEvent.BUTTON_PRIMARY); byte[] packet = bos.toByteArray(); @@ -134,6 +106,7 @@ public class ControlMessageReaderTest { Assert.assertEquals(1080, event.getPosition().getScreenSize().getWidth()); Assert.assertEquals(1920, event.getPosition().getScreenSize().getHeight()); Assert.assertEquals(1f, event.getPressure(), 0f); // must be exact + Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getButtons()); } @Test From bab936194858bdf21f8505e30c38c25c26d72434 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 17 Oct 2019 21:45:52 +0200 Subject: [PATCH 0072/2244] Do not crash on control error Some devices do not have some methods that we invoke via reflection, or their call do not return the expected value. In that case, do not crash the whole controller. --- .../java/com/genymobile/scrcpy/Device.java | 4 ++ .../scrcpy/wrappers/ClipboardManager.java | 49 ++++++++++---- .../scrcpy/wrappers/InputManager.java | 27 ++++++-- .../scrcpy/wrappers/PowerManager.java | 31 ++++++--- .../scrcpy/wrappers/StatusBarManager.java | 40 ++++++++---- .../scrcpy/wrappers/SurfaceControl.java | 64 +++++++++++++++---- 6 files changed, 163 insertions(+), 52 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index 538135d4..0246b216 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -162,6 +162,10 @@ public final class Device { */ public void setScreenPowerMode(int mode) { IBinder d = SurfaceControl.getBuiltInDisplay(0); + if (d == null) { + 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")); } 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 a058a8bb..7dc2e75e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java @@ -1,5 +1,7 @@ package com.genymobile.scrcpy.wrappers; +import com.genymobile.scrcpy.Ln; + import android.content.ClipData; import android.os.IInterface; @@ -8,37 +10,62 @@ import java.lang.reflect.Method; public class ClipboardManager { private final IInterface manager; - private final Method getPrimaryClipMethod; - private final Method setPrimaryClipMethod; + private Method getPrimaryClipMethod; + private Method setPrimaryClipMethod; public ClipboardManager(IInterface manager) { this.manager = manager; - try { - getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class); - setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class); - } catch (NoSuchMethodException e) { - throw new AssertionError(e); + } + + private Method getGetPrimaryClipMethod() { + if (getPrimaryClipMethod == null) { + try { + getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class); + } catch (NoSuchMethodException e) { + Ln.e("Could not find method", e); + } } + return getPrimaryClipMethod; + } + + private Method getSetPrimaryClipMethod() { + if (setPrimaryClipMethod == null) { + try { + setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class); + } catch (NoSuchMethodException e) { + Ln.e("Could not find method", e); + } + } + return setPrimaryClipMethod; } public CharSequence getText() { + Method method = getGetPrimaryClipMethod(); + if (method == null) { + return null; + } try { - ClipData clipData = (ClipData) getPrimaryClipMethod.invoke(manager, "com.android.shell"); + ClipData clipData = (ClipData) method.invoke(manager, "com.android.shell"); if (clipData == null || clipData.getItemCount() == 0) { return null; } return clipData.getItemAt(0).getText(); } catch (InvocationTargetException | IllegalAccessException e) { - throw new AssertionError(e); + Ln.e("Could not invoke " + method.getName(), e); + return null; } } public void setText(CharSequence text) { + Method method = getSetPrimaryClipMethod(); + if (method == null) { + return; + } ClipData clipData = ClipData.newPlainText(null, text); try { - setPrimaryClipMethod.invoke(manager, clipData, "com.android.shell"); + method.invoke(manager, clipData, "com.android.shell"); } catch (InvocationTargetException | IllegalAccessException e) { - throw new AssertionError(e); + Ln.e("Could not invoke " + method.getName(), 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 1fc78c27..788a04c7 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java @@ -1,5 +1,7 @@ package com.genymobile.scrcpy.wrappers; +import com.genymobile.scrcpy.Ln; + import android.os.IInterface; import android.view.InputEvent; @@ -13,22 +15,33 @@ public final class InputManager { public static final int INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH = 2; private final IInterface manager; - private final Method injectInputEventMethod; + private Method injectInputEventMethod; public InputManager(IInterface manager) { this.manager = manager; - try { - injectInputEventMethod = manager.getClass().getMethod("injectInputEvent", InputEvent.class, int.class); - } catch (NoSuchMethodException e) { - throw new AssertionError(e); + } + + private Method getInjectInputEventMethod() { + if (injectInputEventMethod == null) { + try { + injectInputEventMethod = manager.getClass().getMethod("injectInputEvent", InputEvent.class, int.class); + } catch (NoSuchMethodException e) { + Ln.e("Could not find method", e); + } } + return injectInputEventMethod; } public boolean injectInputEvent(InputEvent inputEvent, int mode) { + Method method = getInjectInputEventMethod(); + if (method == null) { + return false; + } try { - return (Boolean) injectInputEventMethod.invoke(manager, inputEvent, mode); + return (Boolean) method.invoke(manager, inputEvent, mode); } catch (InvocationTargetException | IllegalAccessException e) { - throw new AssertionError(e); + Ln.e("Could not invoke " + method.getName(), e); + return false; } } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java index a730d1b1..66acdba8 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java @@ -1,5 +1,7 @@ package com.genymobile.scrcpy.wrappers; +import com.genymobile.scrcpy.Ln; + import android.annotation.SuppressLint; import android.os.Build; import android.os.IInterface; @@ -9,24 +11,35 @@ import java.lang.reflect.Method; public final class PowerManager { private final IInterface manager; - private final Method isScreenOnMethod; + private Method isScreenOnMethod; public PowerManager(IInterface manager) { this.manager = manager; - try { - @SuppressLint("ObsoleteSdkInt") // we may lower minSdkVersion in the future - String methodName = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH ? "isInteractive" : "isScreenOn"; - isScreenOnMethod = manager.getClass().getMethod(methodName); - } catch (NoSuchMethodException e) { - throw new AssertionError(e); + } + + private Method getIsScreenOnMethod() { + if (isScreenOnMethod == null) { + try { + @SuppressLint("ObsoleteSdkInt") // we may lower minSdkVersion in the future + String methodName = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH ? "isInteractive" : "isScreenOn"; + isScreenOnMethod = manager.getClass().getMethod(methodName); + } catch (NoSuchMethodException e) { + Ln.e("Could not find method", e); + } } + return isScreenOnMethod; } public boolean isScreenOn() { + Method method = getIsScreenOnMethod(); + if (method == null) { + return false; + } try { - return (Boolean) isScreenOnMethod.invoke(manager); + return (Boolean) method.invoke(manager); } catch (InvocationTargetException | IllegalAccessException e) { - throw new AssertionError(e); + Ln.e("Could not invoke " + method.getName(), e); + return false; } } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java index 7cd28da6..670de952 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java @@ -17,35 +17,49 @@ public class StatusBarManager { this.manager = manager; } - public void expandNotificationsPanel() { + private Method getExpandNotificationsPanelMethod() { if (expandNotificationsPanelMethod == null) { try { expandNotificationsPanelMethod = manager.getClass().getMethod("expandNotificationsPanel"); } catch (NoSuchMethodException e) { - Ln.e("ServiceBarManager.expandNotificationsPanel() is not available on this device"); - return; + Ln.e("Could not find method", e); } } - try { - expandNotificationsPanelMethod.invoke(manager); - } catch (InvocationTargetException | IllegalAccessException e) { - Ln.e("Could not invoke ServiceBarManager.expandNotificationsPanel()", e); - } + return expandNotificationsPanelMethod; } - public void collapsePanels() { + private Method getCollapsePanelsMethod() { if (collapsePanelsMethod == null) { try { collapsePanelsMethod = manager.getClass().getMethod("collapsePanels"); } catch (NoSuchMethodException e) { - Ln.e("ServiceBarManager.collapsePanels() is not available on this device"); - return; + Ln.e("Could not find method", e); } } + return collapsePanelsMethod; + } + + public void expandNotificationsPanel() { + Method method = getExpandNotificationsPanelMethod(); + if (method == null) { + return; + } try { - collapsePanelsMethod.invoke(manager); + method.invoke(manager); } catch (InvocationTargetException | IllegalAccessException e) { - Ln.e("Could not invoke ServiceBarManager.collapsePanels()", e); + Ln.e("Could not invoke " + method.getName(), e); + } + } + + public void collapsePanels() { + Method method = getCollapsePanelsMethod(); + if (method == null) { + return; + } + try { + method.invoke(manager); + } catch (InvocationTargetException | IllegalAccessException e) { + Ln.e("Could not invoke " + method.getName(), e); } } } 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 5b5586ff..ba37da0d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java @@ -1,11 +1,16 @@ package com.genymobile.scrcpy.wrappers; +import com.genymobile.scrcpy.Ln; + import android.annotation.SuppressLint; import android.graphics.Rect; import android.os.Build; import android.os.IBinder; import android.view.Surface; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + @SuppressLint("PrivateApi") public final class SurfaceControl { @@ -23,6 +28,9 @@ public final class SurfaceControl { } } + private static Method getBuiltInDisplayMethod; + private static Method setDisplayPowerModeMethod; + private SurfaceControl() { // only static methods } @@ -76,24 +84,56 @@ public final class SurfaceControl { } } - public static IBinder getBuiltInDisplay(int builtInDisplayId) { - try { - // the method signature has changed in Android Q - // - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { - return (IBinder) CLASS.getMethod("getBuiltInDisplay", int.class).invoke(null, builtInDisplayId); + private static Method getGetBuiltInDisplayMethod() { + if (getBuiltInDisplayMethod == null) { + try { + // the method signature has changed in Android Q + // + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + getBuiltInDisplayMethod = CLASS.getMethod("getBuiltInDisplay", int.class); + } else { + getBuiltInDisplayMethod = CLASS.getMethod("getPhysicalDisplayToken", long.class); + } + } catch (NoSuchMethodException e) { + Ln.e("Could not find method", e); } - return (IBinder) CLASS.getMethod("getPhysicalDisplayToken", long.class).invoke(null, builtInDisplayId); - } catch (Exception e) { - throw new AssertionError(e); + } + return getBuiltInDisplayMethod; + } + + public static IBinder getBuiltInDisplay(int builtInDisplayId) { + Method method = getGetBuiltInDisplayMethod(); + if (method == null) { + return null; + } + try { + return (IBinder) method.invoke(null, builtInDisplayId); + } catch (InvocationTargetException | IllegalAccessException e) { + Ln.e("Could not invoke " + method.getName(), e); + return null; } } + private static Method getSetDisplayPowerModeMethod() { + if (setDisplayPowerModeMethod == null) { + try { + setDisplayPowerModeMethod = CLASS.getMethod("setDisplayPowerMode", IBinder.class, int.class); + } catch (NoSuchMethodException e) { + Ln.e("Could not find method", e); + } + } + return setDisplayPowerModeMethod; + } + public static void setDisplayPowerMode(IBinder displayToken, int mode) { + Method method = getSetDisplayPowerModeMethod(); + if (method == null) { + return; + } try { - CLASS.getMethod("setDisplayPowerMode", IBinder.class, int.class).invoke(null, displayToken, mode); - } catch (Exception e) { - throw new AssertionError(e); + method.invoke(null, displayToken, mode); + } catch (InvocationTargetException | IllegalAccessException e) { + Ln.e("Could not invoke " + method.getName(), e); } } From 5b7a0cd8e958946a51409767630cb43504807b0f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 17 Oct 2019 22:11:39 +0200 Subject: [PATCH 0073/2244] Extract String literal to static constant --- .../com/genymobile/scrcpy/wrappers/ClipboardManager.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 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 7dc2e75e..5cc71cd4 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java @@ -9,6 +9,9 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public class ClipboardManager { + + private static final String PACKAGE_NAME = "com.android.shell"; + private final IInterface manager; private Method getPrimaryClipMethod; private Method setPrimaryClipMethod; @@ -45,7 +48,7 @@ public class ClipboardManager { return null; } try { - ClipData clipData = (ClipData) method.invoke(manager, "com.android.shell"); + ClipData clipData = (ClipData) method.invoke(manager, PACKAGE_NAME); if (clipData == null || clipData.getItemCount() == 0) { return null; } @@ -63,7 +66,7 @@ public class ClipboardManager { } ClipData clipData = ClipData.newPlainText(null, text); try { - method.invoke(manager, clipData, "com.android.shell"); + method.invoke(manager, clipData, PACKAGE_NAME); } catch (InvocationTargetException | IllegalAccessException e) { Ln.e("Could not invoke " + method.getName(), e); } From 8b33c6c1087a183c7e8a7d45445eb0673b50e458 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 17 Oct 2019 22:21:47 +0200 Subject: [PATCH 0074/2244] Adapt copy-paste methods for Android 10 The methods getPrimaryClip() and setPrimaryClip() expect an additional parameter since Android 10. Fixes . --- .../scrcpy/wrappers/ClipboardManager.java | 36 ++++++++++++++++--- 1 file changed, 32 insertions(+), 4 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 5cc71cd4..27dcb443 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.os.Build; import android.os.IInterface; import java.lang.reflect.InvocationTargetException; @@ -11,6 +12,7 @@ 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; @@ -23,7 +25,11 @@ public class ClipboardManager { private Method getGetPrimaryClipMethod() { if (getPrimaryClipMethod == null) { try { - getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class); + } else { + getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class); + } } catch (NoSuchMethodException e) { Ln.e("Could not find method", e); } @@ -34,7 +40,12 @@ public class ClipboardManager { private Method getSetPrimaryClipMethod() { if (setPrimaryClipMethod == null) { try { - setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class); + } else { + setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, + String.class, int.class); + } } catch (NoSuchMethodException e) { Ln.e("Could not find method", e); } @@ -42,13 +53,30 @@ public class ClipboardManager { return setPrimaryClipMethod; } + 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, PACKAGE_NAME, 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); + } else { + method.invoke(manager, clipData, PACKAGE_NAME, USER_ID); + } + } + public CharSequence getText() { Method method = getGetPrimaryClipMethod(); if (method == null) { return null; } try { - ClipData clipData = (ClipData) method.invoke(manager, PACKAGE_NAME); + ClipData clipData = getPrimaryClip(method, manager); if (clipData == null || clipData.getItemCount() == 0) { return null; } @@ -66,7 +94,7 @@ public class ClipboardManager { } ClipData clipData = ClipData.newPlainText(null, text); try { - method.invoke(manager, clipData, PACKAGE_NAME); + setPrimaryClip(method, manager, clipData); } catch (InvocationTargetException | IllegalAccessException e) { Ln.e("Could not invoke " + method.getName(), e); } From c33a147fd0e9fbd3a5add6f0190eea909d74c26d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 17 Oct 2019 23:14:18 +0200 Subject: [PATCH 0075/2244] Fix "turn screen off" on Android Q Call getInternalDisplayToken(), which retrieve the id of the first physical display (which is not necessarily 0 anymore). Fixes --- .../src/main/java/com/genymobile/scrcpy/Device.java | 2 +- .../genymobile/scrcpy/wrappers/SurfaceControl.java | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index 0246b216..708b9516 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -161,7 +161,7 @@ public final class Device { * @param mode one of the {@code SCREEN_POWER_MODE_*} constants */ public void setScreenPowerMode(int mode) { - IBinder d = SurfaceControl.getBuiltInDisplay(0); + IBinder d = SurfaceControl.getBuiltInDisplay(); if (d == null) { Ln.e("Could not get built-in display"); return; 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 ba37da0d..bef6e5d9 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java @@ -92,7 +92,7 @@ public final class SurfaceControl { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { getBuiltInDisplayMethod = CLASS.getMethod("getBuiltInDisplay", int.class); } else { - getBuiltInDisplayMethod = CLASS.getMethod("getPhysicalDisplayToken", long.class); + getBuiltInDisplayMethod = CLASS.getMethod("getInternalDisplayToken"); } } catch (NoSuchMethodException e) { Ln.e("Could not find method", e); @@ -101,13 +101,19 @@ public final class SurfaceControl { return getBuiltInDisplayMethod; } - public static IBinder getBuiltInDisplay(int builtInDisplayId) { + public static IBinder getBuiltInDisplay() { Method method = getGetBuiltInDisplayMethod(); if (method == null) { return null; } try { - return (IBinder) method.invoke(null, builtInDisplayId); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + // call getBuiltInDisplay(0) + return (IBinder) method.invoke(null, 0); + } + + // call getInternalDisplayToken() + return (IBinder) method.invoke(null); } catch (InvocationTargetException | IllegalAccessException e) { Ln.e("Could not invoke " + method.getName(), e); return null; From f6c8460ebb2e96aa4f1630296893807adfb84a3c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 20 Oct 2019 16:06:28 +0200 Subject: [PATCH 0076/2244] Rename window size functions for clarity Now, get_window_size() returns the current window size (fullscreen or not), while get_windowed_window_size() always returned the windowed size (the size when fullscreen is disabled). --- app/src/screen.c | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/app/src/screen.c b/app/src/screen.c index e34bcf46..4bc4c5c5 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -16,7 +16,7 @@ // get the window size in a struct size static struct size -get_native_window_size(SDL_Window *window) { +get_window_size(SDL_Window *window) { int width; int height; SDL_GetWindowSize(window, &width, &height); @@ -29,11 +29,11 @@ get_native_window_size(SDL_Window *window) { // get the windowed window size static struct size -get_window_size(const struct screen *screen) { +get_windowed_window_size(const struct screen *screen) { if (screen->fullscreen) { return screen->windowed_window_size; } - return get_native_window_size(screen->window); + return get_window_size(screen->window); } // set the window size to be applied when fullscreen is disabled @@ -112,8 +112,8 @@ 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) { - struct size current_size = get_window_size(screen); - return get_optimal_size(current_size, frame_size); + struct size windowed_size = get_windowed_window_size(screen); + return get_optimal_size(windowed_size, frame_size); } // initially, there is no current size, so use the frame size as current size @@ -229,11 +229,11 @@ prepare_for_frame(struct screen *screen, struct size new_frame_size) { // frame dimension changed, destroy texture SDL_DestroyTexture(screen->texture); - struct size current_size = get_window_size(screen); + struct size windowed_size = get_windowed_window_size(screen); struct size target_size = { - (uint32_t) current_size.width * new_frame_size.width + (uint32_t) windowed_size.width * new_frame_size.width / screen->frame_size.width, - (uint32_t) current_size.height * new_frame_size.height + (uint32_t) windowed_size.height * new_frame_size.height / screen->frame_size.height, }; target_size = get_optimal_size(target_size, new_frame_size); @@ -289,7 +289,7 @@ void screen_switch_fullscreen(struct screen *screen) { if (!screen->fullscreen) { // going to fullscreen, store the current windowed window size - screen->windowed_window_size = get_native_window_size(screen->window); + screen->windowed_window_size = get_window_size(screen->window); } uint32_t new_mode = screen->fullscreen ? 0 : SDL_WINDOW_FULLSCREEN_DESKTOP; if (SDL_SetWindowFullscreen(screen->window, new_mode)) { From f9938dbf88fbe35415db65824a3345a0117790f9 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 25 Oct 2019 11:04:04 +0200 Subject: [PATCH 0077/2244] Inject button state for touch/mouse events The buttons state was forwarded, but ignored by the server. --- server/src/main/java/com/genymobile/scrcpy/Controller.java | 4 ++-- 1 file 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 19bf9d94..ce02e333 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -82,7 +82,7 @@ public class Controller { injectText(msg.getText()); break; case ControlMessage.TYPE_INJECT_TOUCH_EVENT: - injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), 0); + 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()); @@ -177,7 +177,7 @@ public class Controller { } MotionEvent event = MotionEvent.obtain(lastTouchDown, now, action, pointerCount, pointerProperties, - pointerCoords, 0, 0, 1f, 1f, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0); + pointerCoords, 0, buttons, 1f, 1f, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0); return injectEvent(event); } From 17d53be3ef9d376e48a7f9e90cf6c87cf8fbf8f5 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 25 Oct 2019 11:06:30 +0200 Subject: [PATCH 0078/2244] Fix mouse events conversion The conversion from SDL mouse state to Android mouse state used wrong constants as mask. Fixes --- app/src/event_converter.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/event_converter.c b/app/src/event_converter.c index da4b2e30..700e5c50 100644 --- a/app/src/event_converter.c +++ b/app/src/event_converter.c @@ -149,10 +149,10 @@ convert_mouse_buttons(uint32_t state) { if (state & SDL_BUTTON_MMASK) { buttons |= AMOTION_EVENT_BUTTON_TERTIARY; } - if (state & SDL_BUTTON_X1) { + if (state & SDL_BUTTON_X1MASK) { buttons |= AMOTION_EVENT_BUTTON_BACK; } - if (state & SDL_BUTTON_X2) { + if (state & SDL_BUTTON_X2MASK) { buttons |= AMOTION_EVENT_BUTTON_FORWARD; } return buttons; From d841718956d2dbd9f2bface70d971c4e0bec545c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 29 Oct 2019 18:46:35 +0100 Subject: [PATCH 0079/2244] Add a script to build the server without gradle Gradle versions may sometimes cause problems. This script offers an alternative. --- server/build_without_gradle.sh | 62 ++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100755 server/build_without_gradle.sh diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh new file mode 100755 index 00000000..5f2eff22 --- /dev/null +++ b/server/build_without_gradle.sh @@ -0,0 +1,62 @@ +#!/bin/bash +# +# This script generates the scrcpy binary "manually" (without gradle). +# +# Adapt Android platform and build tools versions (via ANDROID_PLATFORM and +# ANDROID_BUILD_TOOLS environment variables). +# +# Then execute: +# +# BUILD_DIR=my_build_dir ./build_without_gradle.sh + +set -e + +PLATFORM=${ANDROID_PLATFORM:-29} +BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-29.0.2} + +BUILD_DIR="$(realpath ${BUILD_DIR:-build_manual})" +CLASSES_DIR="$BUILD_DIR/classes" +SERVER_DIR=$(dirname "$0") +SERVER_BINARY=scrcpy-server.jar + +echo "Platform: android-$PLATFORM" +echo "Build-tools: $BUILD_TOOLS" +echo "Build dir: $BUILD_DIR" + +rm -rf "$CLASSES_DIR" "$BUILD_DIR/$SERVER_BINARY" classes.dex +mkdir -p "$CLASSES_DIR/com/genymobile/scrcpy" + +<< EOF cat > "$CLASSES_DIR/com/genymobile/scrcpy/BuildConfig.java" +package com.genymobile.scrcpy; + +public final class BuildConfig { + public static final boolean DEBUG = false; +} +EOF + +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 + +echo "Compiling java sources..." +cd ../java +javac -bootclasspath "$ANDROID_HOME/platforms/android-$PLATFORM/android.jar" \ + -cp "$CLASSES_DIR" -d "$CLASSES_DIR" -source 1.8 -target 1.8 \ + com/genymobile/scrcpy/*.java \ + com/genymobile/scrcpy/wrappers/*.java + +echo "Dexing..." +cd "$CLASSES_DIR" +"$ANDROID_HOME/build-tools/$BUILD_TOOLS/dx" --dex \ + --output "$BUILD_DIR/classes.dex" \ + android/view/*.class \ + com/genymobile/scrcpy/*.class \ + com/genymobile/scrcpy/wrappers/*.class + +echo "Archiving..." +cd "$BUILD_DIR" +jar cvf "$SERVER_BINARY" classes.dex +rm -rf classes.dex classes + +echo "Server generated in $BUILD_DIR/scrcpy-server.jar" From 1380f6e00f9e091f099d4d5184215dfa29b076ec Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 30 Oct 2019 21:55:12 +0100 Subject: [PATCH 0080/2244] Fix help for --record-format Record format requires a parameter. --- 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 c00bf419..41383ed9 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -59,7 +59,7 @@ static void usage(const char *arg0) { " -f, --fullscreen\n" " Start in fullscreen.\n" "\n" - " -F, --record-format\n" + " -F, --record-format format\n" " Force recording format (either mp4 or mkv).\n" "\n" " -h, --help\n" From 3da95b52bd21ae0f4d81ee933caea63b78191deb Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 30 Oct 2019 23:40:10 +0100 Subject: [PATCH 0081/2244] Rename scrcpy-server.jar to scrcpy-server The server name ending with .jar has several drawbacks: - meson requires the jar executable to attempt to modify it: - meson warns during "ninja install" - some users try to execute it on the computer as a java executable Removing the extension solves all these problems. --- BUILD.md | 4 ++-- DEVELOP.md | 4 ++-- Makefile.CrossWindows | 6 +++--- README.md | 2 +- app/meson.build | 2 +- app/src/server.c | 4 ++-- meson_options.txt | 2 +- release.sh | 8 ++++---- run | 2 +- scripts/run-scrcpy.sh | 2 +- server/build_without_gradle.sh | 4 ++-- server/meson.build | 4 ++-- server/src/main/java/com/genymobile/scrcpy/Server.java | 2 +- 13 files changed, 23 insertions(+), 23 deletions(-) diff --git a/BUILD.md b/BUILD.md index 475580f8..161f8f92 100644 --- a/BUILD.md +++ b/BUILD.md @@ -225,7 +225,7 @@ sudo ninja install # without sudo on Windows This installs two files: - `/usr/local/bin/scrcpy` - - `/usr/local/share/scrcpy/scrcpy-server.jar` + - `/usr/local/share/scrcpy/scrcpy-server` Just remove them to "uninstall" the application. @@ -244,7 +244,7 @@ configuration: ```bash meson x --buildtype release --strip -Db_lto=true \ - -Dprebuilt_server=/path/to/scrcpy-server.jar + -Dprebuilt_server=/path/to/scrcpy-server cd x ninja sudo ninja install diff --git a/DEVELOP.md b/DEVELOP.md index dea8137d..fb8ab91d 100644 --- a/DEVELOP.md +++ b/DEVELOP.md @@ -3,7 +3,7 @@ ## Overview This application is composed of two parts: - - the server (`scrcpy-server.jar`), to be executed on the device, + - the server (`scrcpy-server`), to be executed on the device, - the client (the `scrcpy` binary), executed on the host computer. The client is responsible to push the server to the device and start its @@ -49,7 +49,7 @@ application may not replace the server just before the client executes it._ Instead of a raw _dex_ file, `app_process` accepts a _jar_ containing `classes.dex` (e.g. an [APK]). For simplicity, and to benefit from the gradle build system, the server is built to an (unsigned) APK (renamed to -`scrcpy-server.jar`). +`scrcpy-server`). [dex]: https://en.wikipedia.org/wiki/Dalvik_(software) [apk]: https://en.wikipedia.org/wiki/Android_application_package diff --git a/Makefile.CrossWindows b/Makefile.CrossWindows index c07cb24f..59f5a302 100644 --- a/Makefile.CrossWindows +++ b/Makefile.CrossWindows @@ -3,7 +3,7 @@ # # Here, "portable" means that the client and server binaries are expected to be # anywhere, but in the same directory, instead of well-defined separate -# locations (e.g. /usr/bin/scrcpy and /usr/share/scrcpy/scrcpy-server.jar). +# locations (e.g. /usr/bin/scrcpy and /usr/share/scrcpy/scrcpy-server). # # In particular, this implies to change the location from where the client push # the server to the device. @@ -97,7 +97,7 @@ build-win64-noconsole: prepare-deps-win64 dist-win32: build-server build-win32 build-win32-noconsole mkdir -p "$(DIST)/$(WIN32_TARGET_DIR)" - cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server.jar "$(DIST)/$(WIN32_TARGET_DIR)/" + 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.1.4-win32-shared/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/" @@ -112,7 +112,7 @@ dist-win32: build-server build-win32 build-win32-noconsole dist-win64: build-server build-win64 build-win64-noconsole mkdir -p "$(DIST)/$(WIN64_TARGET_DIR)" - cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server.jar "$(DIST)/$(WIN64_TARGET_DIR)/" + 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.1.4-win64-shared/bin/avutil-56.dll "$(DIST)/$(WIN64_TARGET_DIR)/" diff --git a/README.md b/README.md index f698cb4c..ebf593a1 100644 --- a/README.md +++ b/README.md @@ -358,7 +358,7 @@ To use a specific _adb_ binary, configure its path in the environment variable ADB=/path/to/adb scrcpy -To override the path of the `scrcpy-server.jar` file, configure its path in +To override the path of the `scrcpy-server` file, configure its path in `SCRCPY_SERVER_PATH`. [useful]: https://github.com/Genymobile/scrcpy/issues/278#issuecomment-429330345 diff --git a/app/meson.build b/app/meson.build index ccd05fee..5d24fb3a 100644 --- a/app/meson.build +++ b/app/meson.build @@ -93,7 +93,7 @@ conf.set_quoted('SCRCPY_VERSION', meson.project_version()) # the prefix used during configuration (meson --prefix=PREFIX) conf.set_quoted('PREFIX', get_option('prefix')) -# build a "portable" version (with scrcpy-server.jar accessible from the same +# build a "portable" version (with scrcpy-server accessible from the same # directory as the executable) conf.set('PORTABLE', get_option('portable')) diff --git a/app/src/server.c b/app/src/server.c index 85b1b6b8..4fe65402 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -13,7 +13,7 @@ #include "net.h" #define SOCKET_NAME "scrcpy" -#define SERVER_FILENAME "scrcpy-server.jar" +#define SERVER_FILENAME "scrcpy-server" #define DEFAULT_SERVER_PATH PREFIX "/share/scrcpy/" SERVER_FILENAME #define DEVICE_SERVER_PATH "/data/local/tmp/" SERVER_FILENAME @@ -32,7 +32,7 @@ get_server_path(void) { // the absolute path is hardcoded return DEFAULT_SERVER_PATH; #else - // use scrcpy-server.jar in the same directory as the executable + // use scrcpy-server in the same directory as the executable char *executable_path = get_executable_path(); if (!executable_path) { LOGE("Could not get executable path, " diff --git a/meson_options.txt b/meson_options.txt index d93161e3..84889597 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -3,5 +3,5 @@ option('compile_server', type: 'boolean', value: true, description: 'Build the s option('crossbuild_windows', type: 'boolean', value: false, description: 'Build for Windows from Linux') option('windows_noconsole', type: 'boolean', value: false, description: 'Disable console on Windows (pass -mwindows flag)') option('prebuilt_server', type: 'string', description: 'Path of the prebuilt server') -option('portable', type: 'boolean', value: false, description: 'Use scrcpy-server.jar from the same directory as the scrcpy executable') +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') diff --git a/release.sh b/release.sh index fbd1eb54..4c5afbf1 100755 --- a/release.sh +++ b/release.sh @@ -23,21 +23,21 @@ cd - make -f Makefile.CrossWindows # the generated server must be the same everywhere -cmp "$BUILDDIR/server/scrcpy-server.jar" dist/scrcpy-win32/scrcpy-server.jar -cmp "$BUILDDIR/server/scrcpy-server.jar" dist/scrcpy-win64/scrcpy-server.jar +cmp "$BUILDDIR/server/scrcpy-server" dist/scrcpy-win32/scrcpy-server +cmp "$BUILDDIR/server/scrcpy-server" dist/scrcpy-win64/scrcpy-server # get version name TAG=$(git describe --tags --always) # create release directory mkdir -p "release-$TAG" -cp "$BUILDDIR/server/scrcpy-server.jar" "release-$TAG/scrcpy-server-$TAG.jar" +cp "$BUILDDIR/server/scrcpy-server" "release-$TAG/scrcpy-server-$TAG" cp "dist/scrcpy-win32-$TAG.zip" "release-$TAG/" cp "dist/scrcpy-win64-$TAG.zip" "release-$TAG/" # generate checksums cd "release-$TAG" -sha256sum "scrcpy-server-$TAG.jar" \ +sha256sum "scrcpy-server-$TAG" \ "scrcpy-win32-$TAG.zip" \ "scrcpy-win64-$TAG.zip" > SHA256SUMS.txt diff --git a/run b/run index 7abeca05..bfb499ae 100755 --- a/run +++ b/run @@ -20,4 +20,4 @@ then exit 1 fi -SCRCPY_SERVER_PATH="$BUILDDIR/server/scrcpy-server.jar" "$BUILDDIR/app/scrcpy" "$@" +SCRCPY_SERVER_PATH="$BUILDDIR/server/scrcpy-server" "$BUILDDIR/app/scrcpy" "$@" diff --git a/scripts/run-scrcpy.sh b/scripts/run-scrcpy.sh index fa6d7c8f..f3130ee9 100755 --- a/scripts/run-scrcpy.sh +++ b/scripts/run-scrcpy.sh @@ -1,2 +1,2 @@ #!/bin/bash -SCRCPY_SERVER_PATH="$MESON_BUILD_ROOT/server/scrcpy-server.jar" "$MESON_BUILD_ROOT/app/scrcpy" +SCRCPY_SERVER_PATH="$MESON_BUILD_ROOT/server/scrcpy-server" "$MESON_BUILD_ROOT/app/scrcpy" diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index 5f2eff22..daf85008 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -17,7 +17,7 @@ BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-29.0.2} BUILD_DIR="$(realpath ${BUILD_DIR:-build_manual})" CLASSES_DIR="$BUILD_DIR/classes" SERVER_DIR=$(dirname "$0") -SERVER_BINARY=scrcpy-server.jar +SERVER_BINARY=scrcpy-server echo "Platform: android-$PLATFORM" echo "Build-tools: $BUILD_TOOLS" @@ -59,4 +59,4 @@ cd "$BUILD_DIR" jar cvf "$SERVER_BINARY" classes.dex rm -rf classes.dex classes -echo "Server generated in $BUILD_DIR/scrcpy-server.jar" +echo "Server generated in $BUILD_DIR/$SERVER_BINARY" diff --git a/server/meson.build b/server/meson.build index 43901246..4ba481d5 100644 --- a/server/meson.build +++ b/server/meson.build @@ -4,7 +4,7 @@ prebuilt_server = get_option('prebuilt_server') if prebuilt_server == '' custom_target('scrcpy-server', build_always: true, # gradle is responsible for tracking source changes - output: 'scrcpy-server.jar', + output: 'scrcpy-server', command: [find_program('./scripts/build-wrapper.sh'), meson.current_source_dir(), '@OUTPUT@', get_option('buildtype')], console: true, install: true, @@ -16,7 +16,7 @@ else endif custom_target('scrcpy-server-prebuilt', input: prebuilt_server, - output: 'scrcpy-server.jar', + output: 'scrcpy-server', command: ['cp', '@INPUT@', '@OUTPUT@'], install: true, install_dir: 'share/scrcpy') diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 3bd2fcdc..eba89bdb 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -7,7 +7,7 @@ import java.io.IOException; public final class Server { - private static final String SERVER_PATH = "/data/local/tmp/scrcpy-server.jar"; + private static final String SERVER_PATH = "/data/local/tmp/scrcpy-server"; private Server() { // not instantiable From 4696878a979bcf49bd5ecd8dc38765a61e85bada Mon Sep 17 00:00:00 2001 From: yangfl Date: Wed, 30 Oct 2019 22:14:30 +0800 Subject: [PATCH 0082/2244] Add manpage for scrcpy --- app/meson.build | 2 + app/scrcpy.1 | 231 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 233 insertions(+) create mode 100644 app/scrcpy.1 diff --git a/app/meson.build b/app/meson.build index ccd05fee..34f642d4 100644 --- a/app/meson.build +++ b/app/meson.build @@ -134,6 +134,8 @@ executable('scrcpy', src, c_args: c_args, link_args: link_args) +install_man('scrcpy.1') + ### TESTS diff --git a/app/scrcpy.1 b/app/scrcpy.1 new file mode 100644 index 00000000..67db3569 --- /dev/null +++ b/app/scrcpy.1 @@ -0,0 +1,231 @@ +.TH "scrcpy" "1" +.SH NAME +scrcpy \- Display and control your Android device + + +.SH SYNOPSIS +.B scrcpy +.RI [ options ] + + +.SH DESCRIPTION +.B scrcpy +provides display and control of Android devices connected on USB (or over TCP/IP). It does not require any root access. + + +.SH OPTIONS + +.TP +.BI "\-b, \-\-bit\-rate " value +Encode the video at the given bit\-rate, expressed in bits/s. Unit suffixes are supported: '\fBK\fR' (x1000) and '\fBM\fR' (x1000000). + +Default is 8000000. + +.TP +.BI "\-c, \-\-crop " width\fR:\fIheight\fR:\fIx\fR:\fIy +Crop the device screen on the server. + +The values are expressed in the device natural orientation (typically, portrait for a phone, landscape for a tablet). Any +.B \-\-max\-size +value is computed on the cropped size. + +.TP +.B \-f, \-\-fullscreen +Start in fullscreen. + +.TP +.BI "\-F, \-\-record\-format " format +Force recording format (either mp4 or mkv). + +.TP +.B \-h, \-\-help +Print this help. + +.TP +.BI "\-m, \-\-max\-size " value +Limit both the width and height of the video to \fIvalue\fR. The other dimension is computed so that the device aspect\-ratio is preserved. + +Default is 0 (unlimited). + +.TP +.B \-n, \-\-no\-control +Disable device control (mirror the device in read\-only). + +.TP +.B \-N, \-\-no\-display +Do not display device (only when screen recording is enabled). + +.TP +.BI "\-p, \-\-port " port +Set the TCP port the client listens on. + +Default is 27183. + +.TP +.BI "\-\-push\-target " path +Set the target directory for pushing files to the device by drag & drop. It is passed as\-is to "adb push". + +Default is "/sdcard/". + +.TP +.BI "\-r, \-\-record " file +Record screen to +.IR file . + +The format is determined by the +.B \-F/\-\-record\-format +option if set, or by the file extension (.mp4 or .mkv). + +.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. + +.TP +.BI "\-s, \-\-serial " number +The device serial number. Mandatory only if several devices are connected to adb. + +.TP +.B \-S, \-\-turn\-screen\-off +Turn the device screen off immediately. + +.TP +.B \-t, \-\-show\-touches +Enable "show touches" on start, disable on quit. + +It only shows physical touches (not clicks from scrcpy). + +.TP +.B \-T, \-\-always\-on\-top +Make scrcpy window always on top (above other windows). + +.TP +.B \-v, \-\-version +Print the version of scrcpy. + +.TP +.B \-\-window\-title text +Set a custom window title. + + +.SH SHORTCUTS + +.TP +.B Ctrl+f +switch fullscreen mode + +.TP +.B Ctrl+g +resize window to 1:1 (pixel\-perfect) + +.TP +.B Ctrl+x, Double\-click on black borders +resize window to remove black borders + +.TP +.B Ctrl+h, Home, Middle\-click +Click on HOME + +.TP +.B Ctrl+b, Ctrl+Backspace, Right\-click (when screen is on) +Click on BACK + +.TP +.B Ctrl+s +Click on APP_SWITCH + +.TP +.B Ctrl+m +Click on MENU + +.TP +.B Ctrl+Up +Click on VOLUME_UP + +.TP +.B Ctrl+Down +Click on VOLUME_DOWN + +.TP +.B Ctrl+p +Click on POWER (turn screen on/off) + +.TP +.B Right\-click (when screen is off) +turn screen on + +.TP +.B Ctrl+o +turn device screen off (keep mirroring) + +.TP +.B Ctrl+n +expand notification panel + +.TP +.B Ctrl+Shift+n +collapse notification panel + +.TP +.B Ctrl+c +copy device clipboard to computer + +.TP +.B Ctrl+v +paste computer clipboard to device + +.TP +.B Ctrl+Shift+v +copy computer clipboard to device + +.TP +.B Ctrl+i +enable/disable FPS counter (print frames/second in logs) + +.TP +.B Drag & drop APK file +install APK from computer + + +.SH Environment variables + +.TP +.B ADB +Specify the path to adb. + +.TP +.B SCRCPY_SERVER_PATH +Specify the path to server binary. + + +.SH AUTHORS +.B scrcpy +is written by Romain Vimont. + +This manual page was written by +.MT mmyangfl@gmail.com +Yangfl +.ME +for the Debian Project (and may be used by others). + + +.SH "REPORTING BUGS" +Report bugs to +.UR https://github.com/Genymobile/scrcpy/issues +.UE . + +.SH COPYRIGHT +Copyright \(co 2018 Genymobile +.UR https://www.genymobile.com +Genymobile +.UE + +Copyright \(co 2018\-2019 +.MT rom@rom1v.com +Romain Vimont +.ME + +Licensed under the Apache License, Version 2.0. + +.SH WWW +.UR https://github.com/Genymobile/scrcpy +.UE From 95fd64b5dea57c4b94f2ab0a08f6421a134a48bf Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 31 Oct 2019 20:57:57 +0100 Subject: [PATCH 0083/2244] Add scrcpy version in recorded video metadata It might help to understand problems in recorded videos. --- app/src/recorder.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/recorder.c b/app/src/recorder.c index 77186350..f96bcd26 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -135,6 +135,9 @@ recorder_open(struct recorder *recorder, const AVCodec *input_codec) { // recorder->ctx->oformat = (AVOutputFormat *) format; + av_dict_set(&recorder->ctx->metadata, "comment", + "Recorded by scrcpy " SCRCPY_VERSION, 0); + AVStream *ostream = avformat_new_stream(recorder->ctx, input_codec); if (!ostream) { avformat_free_context(recorder->ctx); From 3ea47423217775d2519fb5ac61edd55072639a64 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 31 Oct 2019 21:05:04 +0100 Subject: [PATCH 0084/2244] Call ninja without changing directory In build instructions, use: ninja -Cx ... instead of: cd x ninja ... --- BUILD.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/BUILD.md b/BUILD.md index 475580f8..3fef0f23 100644 --- a/BUILD.md +++ b/BUILD.md @@ -195,8 +195,7 @@ Then, build: ```bash meson x --buildtype release --strip -Db_lto=true -cd x -ninja +ninja -Cx ``` _Note: `ninja` [must][ninja-user] be run as a non-root user (only `ninja @@ -219,7 +218,7 @@ To run without installing: After a successful build, you can install _scrcpy_ on the system: ```bash -sudo ninja install # without sudo on Windows +sudo ninja -Cx install # without sudo on Windows ``` This installs two files: @@ -245,7 +244,6 @@ configuration: ```bash meson x --buildtype release --strip -Db_lto=true \ -Dprebuilt_server=/path/to/scrcpy-server.jar -cd x -ninja -sudo ninja install +ninja -Cx +sudo ninja -Cx install ``` From 120f08ee96d29464bed9f4b1390244d8e2ccb81c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 3 Nov 2019 16:51:47 +0100 Subject: [PATCH 0085/2244] Fix manpage option parameter format The parameter for --window-title was not underlined the same way as others. --- app/scrcpy.1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 67db3569..1dafbc6a 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -103,7 +103,7 @@ Make scrcpy window always on top (above other windows). Print the version of scrcpy. .TP -.B \-\-window\-title text +.BI \-\-window\-title " text Set a custom window title. From 683f7ca848ad4785557d116dcea466f1b5654ef9 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 3 Nov 2019 19:31:56 +0100 Subject: [PATCH 0086/2244] Document how to attach a debugger to the server --- DEVELOP.md | 30 ++++++++++++++++++++++++++++++ app/meson.build | 3 +++ app/src/server.c | 16 ++++++++++++++++ meson_options.txt | 1 + 4 files changed, 50 insertions(+) diff --git a/DEVELOP.md b/DEVELOP.md index fb8ab91d..92c3ce87 100644 --- a/DEVELOP.md +++ b/DEVELOP.md @@ -268,3 +268,33 @@ For more details, go read the code! If you find a bug, or have an awesome idea to implement, please discuss and contribute ;-) + + +### Debug the server + +The server is pushed to the device by the client on startup. + +To debug it, enable the server debugger during configuration: + +```bash +meson x -Dserver_debugger=true +# or, if x is already configured +meson configure x -Dserver_debugger=true +``` + +Then recompile. + +When you start scrcpy, it will start a debugger on port 5005 on the device. +Redirect that port to the computer: + +```bash +adb forward tcp:5005 tcp:5005 +``` + +In Android Studio, _Run_ > _Debug_ > _Edit configurations..._ On the left, click on +`+`, _Remote_, and fill the form: + + - Host: `localhost` + - Port: `5005` + +Then click on _Debug_. diff --git a/app/meson.build b/app/meson.build index 95587980..145e0ef6 100644 --- a/app/meson.build +++ b/app/meson.build @@ -115,6 +115,9 @@ conf.set('HIDPI_SUPPORT', get_option('hidpi_support')) # disable console on Windows 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')) + 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 4fe65402..de61001f 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -124,6 +124,11 @@ execute_server(struct server *server, const struct server_params *params) { "shell", "CLASSPATH=/data/local/tmp/" SERVER_FILENAME, "app_process", +#ifdef SERVER_DEBUGGER +# define SERVER_DEBUGGER_PORT "5005" + "-agentlib:jdwp=transport=dt_socket,suspend=y,server=y,address=" + SERVER_DEBUGGER_PORT, +#endif "/", // unused "com.genymobile.scrcpy.Server", max_size_string, @@ -133,6 +138,17 @@ execute_server(struct server *server, const struct server_params *params) { "true", // always send frame meta (packet boundaries + timestamp) params->control ? "true" : "false", }; +#ifdef SERVER_DEBUGGER + LOGI("Server debugger waiting for a client on device port " + SERVER_DEBUGGER_PORT "..."); + // From the computer, run + // adb forward tcp:5005 tcp:5005 + // Then, from Android Studio: Run > Debug > Edit configurations... + // On the left, click on '+', "Remote", with: + // Host: localhost + // Port: 5005 + // Then click on "Debug" +#endif return adb_execute(server->serial, cmd, sizeof(cmd) / sizeof(cmd[0])); } diff --git a/meson_options.txt b/meson_options.txt index 84889597..4cf4a8bf 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -5,3 +5,4 @@ option('windows_noconsole', type: 'boolean', value: false, description: 'Disable option('prebuilt_server', type: 'string', description: 'Path of the prebuilt server') 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') From 8d601d3210c0714c947a3029a1299182a849e348 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 20 Oct 2019 11:51:54 +0200 Subject: [PATCH 0087/2244] Rename "input_manager" variables to "im" It is used a lot, a short name improves readability. --- app/src/input_manager.c | 56 ++++++++++++++++++++--------------------- app/src/input_manager.h | 12 ++++----- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index db15da75..8dfc712d 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -212,7 +212,7 @@ clipboard_paste(struct controller *controller) { } void -input_manager_process_text_input(struct input_manager *input_manager, +input_manager_process_text_input(struct input_manager *im, const SDL_TextInputEvent *event) { char c = event->text[0]; if (isalpha(c) || c == ' ') { @@ -227,14 +227,14 @@ input_manager_process_text_input(struct input_manager *input_manager, LOGW("Could not strdup input text"); return; } - if (!controller_push_msg(input_manager->controller, &msg)) { + if (!controller_push_msg(im->controller, &msg)) { SDL_free(msg.inject_text.text); LOGW("Could not request 'inject text'"); } } void -input_manager_process_key(struct input_manager *input_manager, +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 @@ -261,7 +261,7 @@ input_manager_process_key(struct input_manager *input_manager, return; } - struct controller *controller = input_manager->controller; + struct controller *controller = im->controller; // capture all Ctrl events if (ctrl || cmd) { @@ -336,23 +336,23 @@ input_manager_process_key(struct input_manager *input_manager, return; case SDLK_f: if (!shift && cmd && !repeat && down) { - screen_switch_fullscreen(input_manager->screen); + screen_switch_fullscreen(im->screen); } return; case SDLK_x: if (!shift && cmd && !repeat && down) { - screen_resize_to_fit(input_manager->screen); + screen_resize_to_fit(im->screen); } return; case SDLK_g: if (!shift && cmd && !repeat && down) { - screen_resize_to_pixel_perfect(input_manager->screen); + screen_resize_to_pixel_perfect(im->screen); } return; case SDLK_i: if (!shift && cmd && !repeat && down) { struct fps_counter *fps_counter = - input_manager->video_buffer->fps_counter; + im->video_buffer->fps_counter; switch_fps_counter_state(fps_counter); } return; @@ -383,7 +383,7 @@ input_manager_process_key(struct input_manager *input_manager, } void -input_manager_process_mouse_motion(struct input_manager *input_manager, +input_manager_process_mouse_motion(struct input_manager *im, const SDL_MouseMotionEvent *event) { if (!event->state) { // do not send motion events when no button is pressed @@ -394,33 +394,33 @@ input_manager_process_mouse_motion(struct input_manager *input_manager, return; } struct control_msg msg; - if (convert_mouse_motion(event, input_manager->screen->frame_size, &msg)) { - if (!controller_push_msg(input_manager->controller, &msg)) { + if (convert_mouse_motion(event, im->screen->frame_size, &msg)) { + if (!controller_push_msg(im->controller, &msg)) { LOGW("Could not request 'inject mouse motion event'"); } } } void -input_manager_process_touch(struct input_manager *input_manager, +input_manager_process_touch(struct input_manager *im, const SDL_TouchFingerEvent *event) { struct control_msg msg; - if (convert_touch(event, input_manager->screen->frame_size, &msg)) { - if (!controller_push_msg(input_manager->controller, &msg)) { + if (convert_touch(event, im->screen->frame_size, &msg)) { + if (!controller_push_msg(im->controller, &msg)) { LOGW("Could not request 'inject touch event'"); } } } static bool -is_outside_device_screen(struct input_manager *input_manager, int x, int y) +is_outside_device_screen(struct input_manager *im, int x, int y) { - return x < 0 || x >= input_manager->screen->frame_size.width || - y < 0 || y >= input_manager->screen->frame_size.height; + return x < 0 || x >= im->screen->frame_size.width || + y < 0 || y >= im->screen->frame_size.height; } void -input_manager_process_mouse_button(struct input_manager *input_manager, +input_manager_process_mouse_button(struct input_manager *im, const SDL_MouseButtonEvent *event, bool control) { if (event->which == SDL_TOUCH_MOUSEID) { @@ -429,19 +429,19 @@ input_manager_process_mouse_button(struct input_manager *input_manager, } if (event->type == SDL_MOUSEBUTTONDOWN) { if (control && event->button == SDL_BUTTON_RIGHT) { - press_back_or_turn_screen_on(input_manager->controller); + press_back_or_turn_screen_on(im->controller); return; } if (control && event->button == SDL_BUTTON_MIDDLE) { - action_home(input_manager->controller, ACTION_DOWN | ACTION_UP); + 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(input_manager, event->x, event->y); + is_outside_device_screen(im, event->x, event->y); if (outside) { - screen_resize_to_fit(input_manager->screen); + screen_resize_to_fit(im->screen); return; } } @@ -453,23 +453,23 @@ input_manager_process_mouse_button(struct input_manager *input_manager, } struct control_msg msg; - if (convert_mouse_button(event, input_manager->screen->frame_size, &msg)) { - if (!controller_push_msg(input_manager->controller, &msg)) { + if (convert_mouse_button(event, im->screen->frame_size, &msg)) { + if (!controller_push_msg(im->controller, &msg)) { LOGW("Could not request 'inject mouse button event'"); } } } void -input_manager_process_mouse_wheel(struct input_manager *input_manager, +input_manager_process_mouse_wheel(struct input_manager *im, const SDL_MouseWheelEvent *event) { struct position position = { - .screen_size = input_manager->screen->frame_size, - .point = get_mouse_point(input_manager->screen), + .screen_size = im->screen->frame_size, + .point = get_mouse_point(im->screen), }; struct control_msg msg; if (convert_mouse_wheel(event, position, &msg)) { - if (!controller_push_msg(input_manager->controller, &msg)) { + if (!controller_push_msg(im->controller, &msg)) { LOGW("Could not request 'inject mouse wheel event'"); } } diff --git a/app/src/input_manager.h b/app/src/input_manager.h index 0009cb81..934c529e 100644 --- a/app/src/input_manager.h +++ b/app/src/input_manager.h @@ -17,29 +17,29 @@ struct input_manager { }; void -input_manager_process_text_input(struct input_manager *input_manager, +input_manager_process_text_input(struct input_manager *im, const SDL_TextInputEvent *event); void -input_manager_process_key(struct input_manager *input_manager, +input_manager_process_key(struct input_manager *im, const SDL_KeyboardEvent *event, bool control); void -input_manager_process_mouse_motion(struct input_manager *input_manager, +input_manager_process_mouse_motion(struct input_manager *im, const SDL_MouseMotionEvent *event); void -input_manager_process_touch(struct input_manager *input_manager, +input_manager_process_touch(struct input_manager *im, const SDL_TouchFingerEvent *event); void -input_manager_process_mouse_button(struct input_manager *input_manager, +input_manager_process_mouse_button(struct input_manager *im, const SDL_MouseButtonEvent *event, bool control); void -input_manager_process_mouse_wheel(struct input_manager *input_manager, +input_manager_process_mouse_wheel(struct input_manager *im, const SDL_MouseWheelEvent *event); #endif From b0db1178d1f61e9cef42189e60c4ce68cc1c0aee Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 20 Oct 2019 12:49:47 +0200 Subject: [PATCH 0088/2244] Move event conversion to input_manager Only keep helper functions separated. This will help to convert coordinates internally when necessary. --- app/src/event_converter.c | 97 ++------------------------------------- app/src/event_converter.h | 34 ++++---------- app/src/input_manager.c | 89 +++++++++++++++++++++++++++++++++++ 3 files changed, 103 insertions(+), 117 deletions(-) diff --git a/app/src/event_converter.c b/app/src/event_converter.c index a634614e..00e989f7 100644 --- a/app/src/event_converter.c +++ b/app/src/event_converter.c @@ -5,7 +5,7 @@ #define MAP(FROM, TO) case FROM: *to = TO; return true #define FAIL default: return false -static bool +bool convert_keycode_action(SDL_EventType from, enum android_keyevent_action *to) { switch (from) { MAP(SDL_KEYDOWN, AKEY_EVENT_ACTION_DOWN); @@ -33,7 +33,7 @@ autocomplete_metastate(enum android_metastate metastate) { return metastate; } -static enum android_metastate +enum android_metastate convert_meta_state(SDL_Keymod mod) { enum android_metastate metastate = 0; if (mod & KMOD_LSHIFT) { @@ -74,7 +74,7 @@ convert_meta_state(SDL_Keymod mod) { return autocomplete_metastate(metastate); } -static bool +bool convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod) { switch (from) { MAP(SDLK_RETURN, AKEYCODE_ENTER); @@ -128,7 +128,7 @@ convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod) { } } -static enum android_motionevent_buttons +enum android_motionevent_buttons convert_mouse_buttons(uint32_t state) { enum android_motionevent_buttons buttons = 0; if (state & SDL_BUTTON_LMASK) { @@ -150,24 +150,6 @@ convert_mouse_buttons(uint32_t state) { } bool -convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to) { - to->type = CONTROL_MSG_TYPE_INJECT_KEYCODE; - - if (!convert_keycode_action(from->type, &to->inject_keycode.action)) { - return false; - } - - uint16_t mod = from->keysym.mod; - if (!convert_keycode(from->keysym.sym, &to->inject_keycode.keycode, mod)) { - return false; - } - - to->inject_keycode.metastate = convert_meta_state(mod); - - return true; -} - -static bool convert_mouse_action(SDL_EventType from, enum android_motionevent_action *to) { switch (from) { MAP(SDL_MOUSEBUTTONDOWN, AMOTION_EVENT_ACTION_DOWN); @@ -177,41 +159,6 @@ convert_mouse_action(SDL_EventType from, enum android_motionevent_action *to) { } bool -convert_mouse_button(const SDL_MouseButtonEvent *from, struct size screen_size, - struct control_msg *to) { - to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT; - - if (!convert_mouse_action(from->type, &to->inject_touch_event.action)) { - return false; - } - - to->inject_touch_event.pointer_id = POINTER_ID_MOUSE; - to->inject_touch_event.position.screen_size = screen_size; - to->inject_touch_event.position.point.x = from->x; - to->inject_touch_event.position.point.y = from->y; - to->inject_touch_event.pressure = 1.f; - to->inject_touch_event.buttons = - convert_mouse_buttons(SDL_BUTTON(from->button)); - - return true; -} - -bool -convert_mouse_motion(const SDL_MouseMotionEvent *from, struct size screen_size, - struct control_msg *to) { - to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT; - 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_size; - to->inject_touch_event.position.point.x = from->x; - to->inject_touch_event.position.point.y = from->y; - to->inject_touch_event.pressure = 1.f; - to->inject_touch_event.buttons = convert_mouse_buttons(from->state); - - return true; -} - -static bool convert_touch_action(SDL_EventType from, enum android_motionevent_action *to) { switch (from) { MAP(SDL_FINGERMOTION, AMOTION_EVENT_ACTION_MOVE); @@ -220,39 +167,3 @@ convert_touch_action(SDL_EventType from, enum android_motionevent_action *to) { FAIL; } } - -bool -convert_touch(const SDL_TouchFingerEvent *from, struct size screen_size, - struct control_msg *to) { - to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT; - - if (!convert_touch_action(from->type, &to->inject_touch_event.action)) { - return false; - } - - to->inject_touch_event.pointer_id = from->fingerId; - to->inject_touch_event.position.screen_size = screen_size; - // SDL touch event coordinates are normalized in the range [0; 1] - to->inject_touch_event.position.point.x = from->x * screen_size.width; - to->inject_touch_event.position.point.y = from->y * screen_size.height; - to->inject_touch_event.pressure = from->pressure; - to->inject_touch_event.buttons = 0; - return true; -} - -bool -convert_mouse_wheel(const SDL_MouseWheelEvent *from, struct position position, - struct control_msg *to) { - to->type = CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT; - - to->inject_scroll_event.position = position; - - int mul = from->direction == SDL_MOUSEWHEEL_NORMAL ? 1 : -1; - // SDL behavior seems inconsistent between horizontal and vertical scrolling - // so reverse the horizontal - // - to->inject_scroll_event.hscroll = -mul * from->x; - to->inject_scroll_event.vscroll = mul * from->y; - - return true; -} diff --git a/app/src/event_converter.h b/app/src/event_converter.h index f6f136a3..8bad7358 100644 --- a/app/src/event_converter.h +++ b/app/src/event_converter.h @@ -7,36 +7,22 @@ #include "config.h" #include "control_msg.h" -struct complete_mouse_motion_event { - SDL_MouseMotionEvent *mouse_motion_event; - struct size screen_size; -}; +bool +convert_keycode_action(SDL_EventType from, enum android_keyevent_action *to); -struct complete_mouse_wheel_event { - SDL_MouseWheelEvent *mouse_wheel_event; - struct point position; -}; +enum android_metastate +convert_meta_state(SDL_Keymod mod); bool -convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to); +convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod); + +enum android_motionevent_buttons +convert_mouse_buttons(uint32_t state); bool -convert_mouse_button(const SDL_MouseButtonEvent *from, struct size screen_size, - struct control_msg *to); - -// the video size may be different from the real device size, so we need the -// size to which the absolute position apply, to scale it accordingly -bool -convert_mouse_motion(const SDL_MouseMotionEvent *from, struct size screen_size, - struct control_msg *to); +convert_mouse_action(SDL_EventType from, enum android_motionevent_action *to); bool -convert_touch(const SDL_TouchFingerEvent *from, struct size screen_size, - struct control_msg *to); - -// on Android, a scroll event requires the current mouse position -bool -convert_mouse_wheel(const SDL_MouseWheelEvent *from, struct position position, - struct control_msg *to); +convert_touch_action(SDL_EventType from, enum android_motionevent_action *to); #endif diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 8dfc712d..0fce979b 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -233,6 +233,24 @@ input_manager_process_text_input(struct input_manager *im, } } +static bool +convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to) { + to->type = CONTROL_MSG_TYPE_INJECT_KEYCODE; + + if (!convert_keycode_action(from->type, &to->inject_keycode.action)) { + return false; + } + + uint16_t mod = from->keysym.mod; + if (!convert_keycode(from->keysym.sym, &to->inject_keycode.keycode, mod)) { + return false; + } + + to->inject_keycode.metastate = convert_meta_state(mod); + + return true; +} + void input_manager_process_key(struct input_manager *im, const SDL_KeyboardEvent *event, @@ -382,6 +400,21 @@ input_manager_process_key(struct input_manager *im, } } +static bool +convert_mouse_motion(const SDL_MouseMotionEvent *from, struct size screen_size, + struct control_msg *to) { + to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT; + 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_size; + to->inject_touch_event.position.point.x = from->x; + to->inject_touch_event.position.point.y = from->y; + to->inject_touch_event.pressure = 1.f; + to->inject_touch_event.buttons = convert_mouse_buttons(from->state); + + return true; +} + void input_manager_process_mouse_motion(struct input_manager *im, const SDL_MouseMotionEvent *event) { @@ -401,6 +434,25 @@ input_manager_process_mouse_motion(struct input_manager *im, } } +static bool +convert_touch(const SDL_TouchFingerEvent *from, struct size screen_size, + struct control_msg *to) { + to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT; + + if (!convert_touch_action(from->type, &to->inject_touch_event.action)) { + return false; + } + + to->inject_touch_event.pointer_id = from->fingerId; + to->inject_touch_event.position.screen_size = screen_size; + // SDL touch event coordinates are normalized in the range [0; 1] + to->inject_touch_event.position.point.x = from->x * screen_size.width; + to->inject_touch_event.position.point.y = from->y * screen_size.height; + to->inject_touch_event.pressure = from->pressure; + to->inject_touch_event.buttons = 0; + return true; +} + void input_manager_process_touch(struct input_manager *im, const SDL_TouchFingerEvent *event) { @@ -419,6 +471,26 @@ is_outside_device_screen(struct input_manager *im, int x, int y) y < 0 || y >= im->screen->frame_size.height; } +static bool +convert_mouse_button(const SDL_MouseButtonEvent *from, struct size screen_size, + struct control_msg *to) { + to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT; + + if (!convert_mouse_action(from->type, &to->inject_touch_event.action)) { + return false; + } + + to->inject_touch_event.pointer_id = POINTER_ID_MOUSE; + to->inject_touch_event.position.screen_size = screen_size; + to->inject_touch_event.position.point.x = from->x; + to->inject_touch_event.position.point.y = from->y; + to->inject_touch_event.pressure = 1.f; + to->inject_touch_event.buttons = + convert_mouse_buttons(SDL_BUTTON(from->button)); + + return true; +} + void input_manager_process_mouse_button(struct input_manager *im, const SDL_MouseButtonEvent *event, @@ -460,6 +532,23 @@ input_manager_process_mouse_button(struct input_manager *im, } } +static bool +convert_mouse_wheel(const SDL_MouseWheelEvent *from, struct position position, + struct control_msg *to) { + to->type = CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT; + + to->inject_scroll_event.position = position; + + int mul = from->direction == SDL_MOUSEWHEEL_NORMAL ? 1 : -1; + // SDL behavior seems inconsistent between horizontal and vertical scrolling + // so reverse the horizontal + // + to->inject_scroll_event.hscroll = -mul * from->x; + to->inject_scroll_event.vscroll = mul * from->y; + + return true; +} + void input_manager_process_mouse_wheel(struct input_manager *im, const SDL_MouseWheelEvent *event) { From c42ff75b74a617d20a24e86d15da26a13a9bb828 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 20 Oct 2019 14:09:07 +0200 Subject: [PATCH 0089/2244] Pass screen to mouse event converters Mouse events coordinates depend on the screen size and location, so the converter need to access the screen. The fact that it needs the position or the size is an internal detail, so pass a pointer to the whole screen structure. --- app/src/input_manager.c | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 0fce979b..fe891990 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -401,12 +401,12 @@ input_manager_process_key(struct input_manager *im, } static bool -convert_mouse_motion(const SDL_MouseMotionEvent *from, struct size screen_size, +convert_mouse_motion(const SDL_MouseMotionEvent *from, struct screen *screen, struct control_msg *to) { to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT; 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_size; + 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.pressure = 1.f; @@ -427,7 +427,7 @@ input_manager_process_mouse_motion(struct input_manager *im, return; } struct control_msg msg; - if (convert_mouse_motion(event, im->screen->frame_size, &msg)) { + if (convert_mouse_motion(event, im->screen, &msg)) { if (!controller_push_msg(im->controller, &msg)) { LOGW("Could not request 'inject mouse motion event'"); } @@ -435,7 +435,7 @@ input_manager_process_mouse_motion(struct input_manager *im, } static bool -convert_touch(const SDL_TouchFingerEvent *from, struct size screen_size, +convert_touch(const SDL_TouchFingerEvent *from, struct screen *screen, struct control_msg *to) { to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT; @@ -443,11 +443,13 @@ convert_touch(const SDL_TouchFingerEvent *from, struct size screen_size, return false; } + struct size frame_size = screen->frame_size; + to->inject_touch_event.pointer_id = from->fingerId; - to->inject_touch_event.position.screen_size = screen_size; + 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 * screen_size.width; - to->inject_touch_event.position.point.y = from->y * screen_size.height; + to->inject_touch_event.position.point.x = from->x * frame_size.width; + to->inject_touch_event.position.point.y = from->y * frame_size.height; to->inject_touch_event.pressure = from->pressure; to->inject_touch_event.buttons = 0; return true; @@ -457,7 +459,7 @@ void input_manager_process_touch(struct input_manager *im, const SDL_TouchFingerEvent *event) { struct control_msg msg; - if (convert_touch(event, im->screen->frame_size, &msg)) { + if (convert_touch(event, im->screen, &msg)) { if (!controller_push_msg(im->controller, &msg)) { LOGW("Could not request 'inject touch event'"); } @@ -472,7 +474,7 @@ is_outside_device_screen(struct input_manager *im, int x, int y) } static bool -convert_mouse_button(const SDL_MouseButtonEvent *from, struct size screen_size, +convert_mouse_button(const SDL_MouseButtonEvent *from, struct screen *screen, struct control_msg *to) { to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT; @@ -481,7 +483,7 @@ convert_mouse_button(const SDL_MouseButtonEvent *from, struct size screen_size, } to->inject_touch_event.pointer_id = POINTER_ID_MOUSE; - to->inject_touch_event.position.screen_size = screen_size; + 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.pressure = 1.f; @@ -525,7 +527,7 @@ input_manager_process_mouse_button(struct input_manager *im, } struct control_msg msg; - if (convert_mouse_button(event, im->screen->frame_size, &msg)) { + if (convert_mouse_button(event, im->screen, &msg)) { if (!controller_push_msg(im->controller, &msg)) { LOGW("Could not request 'inject mouse button event'"); } @@ -533,8 +535,13 @@ input_manager_process_mouse_button(struct input_manager *im, } static bool -convert_mouse_wheel(const SDL_MouseWheelEvent *from, struct position position, +convert_mouse_wheel(const SDL_MouseWheelEvent *from, struct screen *screen, struct control_msg *to) { + struct position position = { + .screen_size = screen->frame_size, + .point = get_mouse_point(screen), + }; + to->type = CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT; to->inject_scroll_event.position = position; @@ -552,12 +559,8 @@ convert_mouse_wheel(const SDL_MouseWheelEvent *from, struct position position, void input_manager_process_mouse_wheel(struct input_manager *im, const SDL_MouseWheelEvent *event) { - struct position position = { - .screen_size = im->screen->frame_size, - .point = get_mouse_point(im->screen), - }; struct control_msg msg; - if (convert_mouse_wheel(event, position, &msg)) { + if (convert_mouse_wheel(event, im->screen, &msg)) { if (!controller_push_msg(im->controller, &msg)) { LOGW("Could not request 'inject mouse wheel event'"); } From 0e301ddf19c45de50ed728419adff9e6792332bf Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 6 Nov 2019 22:06:54 +0100 Subject: [PATCH 0090/2244] Factorize scrcpy options and command-line args Do not duplicate all scrcpy options fields in the structure storing the parsed command-line arguments. --- app/src/main.c | 105 +++++++++++++-------------------------------- app/src/recorder.h | 3 +- app/src/scrcpy.h | 19 ++++++++ 3 files changed, 52 insertions(+), 75 deletions(-) diff --git a/app/src/main.c b/app/src/main.c index 41383ed9..d2a13237 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -14,24 +14,9 @@ #include "recorder.h" struct args { - const char *serial; - const char *crop; - const char *record_filename; - const char *window_title; - const char *push_target; - enum recorder_format record_format; - bool fullscreen; - bool no_control; - bool no_display; + struct scrcpy_options opts; bool help; bool version; - bool show_touches; - uint16_t port; - uint16_t max_size; - uint32_t bit_rate; - bool always_on_top; - bool turn_screen_off; - bool render_expired_frames; }; static void usage(const char *arg0) { @@ -339,23 +324,26 @@ parse_args(struct args *args, int argc, char *argv[]) { OPT_WINDOW_TITLE}, {NULL, 0, NULL, 0 }, }; + + struct scrcpy_options *opts = &args->opts; + int c; while ((c = getopt_long(argc, argv, "b:c:fF:hm:nNp:r:s:StTv", long_options, NULL)) != -1) { switch (c) { case 'b': - if (!parse_bit_rate(optarg, &args->bit_rate)) { + if (!parse_bit_rate(optarg, &opts->bit_rate)) { return false; } break; case 'c': - args->crop = optarg; + opts->crop = optarg; break; case 'f': - args->fullscreen = true; + opts->fullscreen = true; break; case 'F': - if (!parse_record_format(optarg, &args->record_format)) { + if (!parse_record_format(optarg, &opts->record_format)) { return false; } break; @@ -363,47 +351,47 @@ parse_args(struct args *args, int argc, char *argv[]) { args->help = true; break; case 'm': - if (!parse_max_size(optarg, &args->max_size)) { + if (!parse_max_size(optarg, &opts->max_size)) { return false; } break; case 'n': - args->no_control = true; + opts->control = false; break; case 'N': - args->no_display = true; + opts->display = false; break; case 'p': - if (!parse_port(optarg, &args->port)) { + if (!parse_port(optarg, &opts->port)) { return false; } break; case 'r': - args->record_filename = optarg; + opts->record_filename = optarg; break; case 's': - args->serial = optarg; + opts->serial = optarg; break; case 'S': - args->turn_screen_off = true; + opts->turn_screen_off = true; break; case 't': - args->show_touches = true; + opts->show_touches = true; break; case 'T': - args->always_on_top = true; + opts->always_on_top = true; break; case 'v': args->version = true; break; case OPT_RENDER_EXPIRED_FRAMES: - args->render_expired_frames = true; + opts->render_expired_frames = true; break; case OPT_WINDOW_TITLE: - args->window_title = optarg; + opts->window_title = optarg; break; case OPT_PUSH_TARGET: - args->push_target = optarg; + opts->push_target = optarg; break; default: // getopt prints the error message on stderr @@ -411,12 +399,12 @@ parse_args(struct args *args, int argc, char *argv[]) { } } - if (args->no_display && !args->record_filename) { + if (!opts->display && !opts->record_filename) { LOGE("-N/--no-display requires screen recording (-r/--record)"); return false; } - if (args->no_display && args->fullscreen) { + if (!opts->display && opts->fullscreen) { LOGE("-f/--fullscreen-window is incompatible with -N/--no-display"); return false; } @@ -427,21 +415,21 @@ parse_args(struct args *args, int argc, char *argv[]) { return false; } - if (args->record_format && !args->record_filename) { + if (opts->record_format && !opts->record_filename) { LOGE("Record format specified without recording"); return false; } - if (args->record_filename && !args->record_format) { - args->record_format = guess_record_format(args->record_filename); - if (!args->record_format) { + if (opts->record_filename && !opts->record_format) { + opts->record_format = guess_record_format(opts->record_filename); + if (!opts->record_format) { LOGE("No format specified for \"%s\" (try with -F mkv)", - args->record_filename); + opts->record_filename); return false; } } - if (args->no_control && args->turn_screen_off) { + if (!opts->control && opts->turn_screen_off) { LOGE("Could not request to turn screen off if control is disabled"); return false; } @@ -458,24 +446,11 @@ main(int argc, char *argv[]) { setbuf(stderr, NULL); #endif struct args args = { - .serial = NULL, - .crop = NULL, - .record_filename = NULL, - .window_title = NULL, - .push_target = NULL, - .record_format = 0, + .opts = SCRCPY_OPTIONS_DEFAULT, .help = false, .version = false, - .show_touches = false, - .port = DEFAULT_LOCAL_PORT, - .max_size = DEFAULT_MAX_SIZE, - .bit_rate = DEFAULT_BIT_RATE, - .always_on_top = false, - .no_control = false, - .no_display = false, - .turn_screen_off = false, - .render_expired_frames = false, }; + if (!parse_args(&args, argc, argv)) { return 1; } @@ -504,25 +479,7 @@ main(int argc, char *argv[]) { SDL_LogSetAllPriority(SDL_LOG_PRIORITY_DEBUG); #endif - struct scrcpy_options options = { - .serial = args.serial, - .crop = args.crop, - .port = args.port, - .record_filename = args.record_filename, - .window_title = args.window_title, - .push_target = args.push_target, - .record_format = args.record_format, - .max_size = args.max_size, - .bit_rate = args.bit_rate, - .show_touches = args.show_touches, - .fullscreen = args.fullscreen, - .always_on_top = args.always_on_top, - .control = !args.no_control, - .display = !args.no_display, - .turn_screen_off = args.turn_screen_off, - .render_expired_frames = args.render_expired_frames, - }; - int res = scrcpy(&options) ? 0 : 1; + int res = scrcpy(&args.opts) ? 0 : 1; avformat_network_deinit(); // ignore failure diff --git a/app/src/recorder.h b/app/src/recorder.h index b1953fcb..4ad77197 100644 --- a/app/src/recorder.h +++ b/app/src/recorder.h @@ -11,7 +11,8 @@ #include "queue.h" enum recorder_format { - RECORDER_FORMAT_MP4 = 1, + RECORDER_FORMAT_AUTO, + RECORDER_FORMAT_MP4, RECORDER_FORMAT_MKV, }; diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index 1593fb1e..62430e79 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -26,6 +26,25 @@ struct scrcpy_options { bool render_expired_frames; }; +#define SCRCPY_OPTIONS_DEFAULT { \ + .serial = NULL, \ + .crop = NULL, \ + .record_filename = NULL, \ + .window_title = NULL, \ + .push_target = NULL, \ + .record_format = RECORDER_FORMAT_AUTO, \ + .port = DEFAULT_LOCAL_PORT, \ + .max_size = DEFAULT_LOCAL_PORT, \ + .bit_rate = DEFAULT_BIT_RATE, \ + .show_touches = false, \ + .fullscreen = false, \ + .always_on_top = false, \ + .control = true, \ + .display = true, \ + .turn_screen_off = false, \ + .render_expired_frames = false, \ +} + bool scrcpy(const struct scrcpy_options *options); From 2d90e1befdd8e751a1d710ab4824cc8fa28cce90 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 6 Nov 2019 22:22:23 +0100 Subject: [PATCH 0091/2244] Fix include recorder.h --- app/src/scrcpy.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index 62430e79..4bc24742 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -3,9 +3,9 @@ #include #include -#include #include "config.h" +#include "recorder.h" struct scrcpy_options { const char *serial; From 157c60feb4de2010de2b4d357076f1645b816cba Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 7 Nov 2019 09:48:48 +0100 Subject: [PATCH 0092/2244] Fix indentation --- app/src/main.c | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/src/main.c b/app/src/main.c index d2a13237..cd03f195 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -310,18 +310,17 @@ parse_args(struct args *args, int argc, char *argv[]) { {"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}, + {"push-target", required_argument, NULL, OPT_PUSH_TARGET}, {"record", required_argument, NULL, 'r'}, {"record-format", required_argument, NULL, 'F'}, {"render-expired-frames", no_argument, NULL, - OPT_RENDER_EXPIRED_FRAMES}, + OPT_RENDER_EXPIRED_FRAMES}, {"serial", required_argument, NULL, 's'}, {"show-touches", no_argument, NULL, 't'}, {"turn-screen-off", no_argument, NULL, 'S'}, {"version", no_argument, NULL, 'v'}, {"window-title", required_argument, NULL, - OPT_WINDOW_TITLE}, + OPT_WINDOW_TITLE}, {NULL, 0, NULL, 0 }, }; From ff061b4f30c54dedc5073a588c6c697477b805db Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 7 Nov 2019 09:58:06 +0100 Subject: [PATCH 0093/2244] Deprecate short options for advanced features The short options will be removed in the future (and may be reused for other features). --- README.md | 2 -- app/src/main.c | 18 +++++++++++++++--- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index ebf593a1..f0717c2a 100644 --- a/README.md +++ b/README.md @@ -143,7 +143,6 @@ This is useful for example to mirror only one eye of the Oculus Go: ```bash scrcpy --crop 1224:1440:0:0 # 1224x1440 at offset (0,0) -scrcpy -c 1224:1440:0:0 # short version ``` If `--max-size` is also specified, resizing is applied after cropping. @@ -226,7 +225,6 @@ The window of app can always be above others by: ```bash scrcpy --always-on-top -scrcpy -T # short version ``` diff --git a/app/src/main.c b/app/src/main.c index cd03f195..1d5beb64 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -297,13 +297,16 @@ guess_record_format(const char *filename) { #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 static bool parse_args(struct args *args, int argc, char *argv[]) { static const struct option long_options[] = { - {"always-on-top", no_argument, NULL, 'T'}, + {"always-on-top", no_argument, NULL, OPT_ALWAYS_ON_TOP}, {"bit-rate", required_argument, NULL, 'b'}, - {"crop", required_argument, NULL, 'c'}, + {"crop", required_argument, NULL, OPT_CROP}, {"fullscreen", no_argument, NULL, 'f'}, {"help", no_argument, NULL, 'h'}, {"max-size", required_argument, NULL, 'm'}, @@ -312,7 +315,7 @@ parse_args(struct args *args, int argc, char *argv[]) { {"port", required_argument, NULL, 'p'}, {"push-target", required_argument, NULL, OPT_PUSH_TARGET}, {"record", required_argument, NULL, 'r'}, - {"record-format", required_argument, NULL, 'F'}, + {"record-format", required_argument, NULL, OPT_RECORD_FORMAT}, {"render-expired-frames", no_argument, NULL, OPT_RENDER_EXPIRED_FRAMES}, {"serial", required_argument, NULL, 's'}, @@ -336,12 +339,18 @@ parse_args(struct args *args, int argc, char *argv[]) { } break; case 'c': + LOGW("Deprecated option -c. Use --crop instead."); + // fall through + case OPT_CROP: opts->crop = optarg; break; case 'f': opts->fullscreen = true; break; case 'F': + LOGW("Deprecated option -F. Use --record-format instead."); + // fall through + case OPT_RECORD_FORMAT: if (!parse_record_format(optarg, &opts->record_format)) { return false; } @@ -378,6 +387,9 @@ parse_args(struct args *args, int argc, char *argv[]) { opts->show_touches = true; break; case 'T': + LOGW("Deprecated option -T. Use --always-on-top instead."); + // fall through + case OPT_ALWAYS_ON_TOP: opts->always_on_top = true; break; case 'v': From c916af0984f72a60301d13fa8ef9a85112f54202 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 7 Nov 2019 19:01:35 +0100 Subject: [PATCH 0094/2244] Add --prefer-text option Expose an option to configure how key/text events are forwarded to the Android device. Enabling the option avoids issues when combining multiple keys to enter special characters, but breaks the expected behavior of alpha keys in games (typically WASD). Fixes --- app/scrcpy.1 | 7 +++++++ app/src/event_converter.c | 9 ++++++++- app/src/event_converter.h | 3 ++- app/src/input_manager.c | 21 +++++++++++++-------- app/src/input_manager.h | 1 + app/src/main.c | 12 ++++++++++++ app/src/scrcpy.c | 3 +++ app/src/scrcpy.h | 3 +++ 8 files changed, 49 insertions(+), 10 deletions(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 1dafbc6a..203395a4 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -61,6 +61,13 @@ Set the TCP port the client listens on. Default is 27183. +.TP +.B \-\-prefer\-text +Inject alpha characters and space as text events instead of key events. + +This avoids issues when combining multiple keys to enter special characters, +but breaks the expected behavior of alpha keys in games (typically WASD). + .TP .BI "\-\-push\-target " path Set the target directory for pushing files to the device by drag & drop. It is passed as\-is to "adb push". diff --git a/app/src/event_converter.c b/app/src/event_converter.c index 00e989f7..80ead615 100644 --- a/app/src/event_converter.c +++ b/app/src/event_converter.c @@ -75,7 +75,8 @@ convert_meta_state(SDL_Keymod mod) { } bool -convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod) { +convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod, + bool prefer_text) { switch (from) { MAP(SDLK_RETURN, AKEYCODE_ENTER); MAP(SDLK_KP_ENTER, AKEYCODE_NUMPAD_ENTER); @@ -92,6 +93,12 @@ convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod) { MAP(SDLK_DOWN, AKEYCODE_DPAD_DOWN); MAP(SDLK_UP, AKEYCODE_DPAD_UP); } + + if (prefer_text) { + // do not forward alpha and space key events + return false; + } + if (mod & (KMOD_LALT | KMOD_RALT | KMOD_LGUI | KMOD_RGUI)) { return false; } diff --git a/app/src/event_converter.h b/app/src/event_converter.h index 8bad7358..c41887e1 100644 --- a/app/src/event_converter.h +++ b/app/src/event_converter.h @@ -14,7 +14,8 @@ enum android_metastate convert_meta_state(SDL_Keymod mod); bool -convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod); +convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod, + bool prefer_text); enum android_motionevent_buttons convert_mouse_buttons(uint32_t state); diff --git a/app/src/input_manager.c b/app/src/input_manager.c index fe891990..7d333c1b 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -214,12 +214,15 @@ clipboard_paste(struct controller *controller) { void input_manager_process_text_input(struct input_manager *im, const SDL_TextInputEvent *event) { - char c = event->text[0]; - if (isalpha(c) || c == ' ') { - SDL_assert(event->text[1] == '\0'); - // letters and space are handled as raw key event - return; + if (!im->prefer_text) { + char c = event->text[0]; + if (isalpha(c) || c == ' ') { + SDL_assert(event->text[1] == '\0'); + // letters and space are handled as raw key event + return; + } } + struct control_msg msg; msg.type = CONTROL_MSG_TYPE_INJECT_TEXT; msg.inject_text.text = SDL_strdup(event->text); @@ -234,7 +237,8 @@ input_manager_process_text_input(struct input_manager *im, } static bool -convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to) { +convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to, + bool prefer_text) { to->type = CONTROL_MSG_TYPE_INJECT_KEYCODE; if (!convert_keycode_action(from->type, &to->inject_keycode.action)) { @@ -242,7 +246,8 @@ convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to) { } uint16_t mod = from->keysym.mod; - if (!convert_keycode(from->keysym.sym, &to->inject_keycode.keycode, mod)) { + if (!convert_keycode(from->keysym.sym, &to->inject_keycode.keycode, mod, + prefer_text)) { return false; } @@ -393,7 +398,7 @@ input_manager_process_key(struct input_manager *im, } struct control_msg msg; - if (convert_input_key(event, &msg)) { + if (convert_input_key(event, &msg, im->prefer_text)) { 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 934c529e..43fc0eeb 100644 --- a/app/src/input_manager.h +++ b/app/src/input_manager.h @@ -14,6 +14,7 @@ struct input_manager { struct controller *controller; struct video_buffer *video_buffer; struct screen *screen; + bool prefer_text; }; void diff --git a/app/src/main.c b/app/src/main.c index 1d5beb64..12c65ed4 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -67,6 +67,13 @@ static void usage(const char *arg0) { " Set the TCP port the client listens on.\n" " Default is %d.\n" "\n" + " --prefer-text\n" + " Inject alpha characters and space as text events instead of\n" + " key events.\n" + " This avoids issues when combining multiple keys to enter a\n" + " special character, but breaks the expected behavior of alpha\n" + " keys in games (typically WASD).\n" + "\n" " --push-target path\n" " Set the target directory for pushing files to the device by\n" " drag & drop. It is passed as-is to \"adb push\".\n" @@ -300,6 +307,7 @@ guess_record_format(const char *filename) { #define OPT_ALWAYS_ON_TOP 1003 #define OPT_CROP 1004 #define OPT_RECORD_FORMAT 1005 +#define OPT_PREFER_TEXT 1006 static bool parse_args(struct args *args, int argc, char *argv[]) { @@ -321,6 +329,7 @@ parse_args(struct 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}, @@ -404,6 +413,9 @@ parse_args(struct args *args, int argc, char *argv[]) { case OPT_PUSH_TARGET: opts->push_target = optarg; break; + case OPT_PREFER_TEXT: + opts->prefer_text = 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 c219c9e5..16f1b4f7 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -42,6 +42,7 @@ static struct input_manager input_manager = { .controller = &controller, .video_buffer = &video_buffer, .screen = &screen, + .prefer_text = false, // initialized later }; // init SDL and set appropriate hints @@ -414,6 +415,8 @@ scrcpy(const struct scrcpy_options *options) { show_touches_waited = true; } + input_manager.prefer_text = options->prefer_text; + ret = event_loop(options->display, options->control); LOGD("quit..."); diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index 4bc24742..70a41ec1 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -5,6 +5,7 @@ #include #include "config.h" +#include "input_manager.h" #include "recorder.h" struct scrcpy_options { @@ -24,6 +25,7 @@ struct scrcpy_options { bool display; bool turn_screen_off; bool render_expired_frames; + bool prefer_text; }; #define SCRCPY_OPTIONS_DEFAULT { \ @@ -43,6 +45,7 @@ struct scrcpy_options { .display = true, \ .turn_screen_off = false, \ .render_expired_frames = false, \ + .prefer_text = false, \ } bool From b08a98324d35298903098ec8ea9023ecf9515a2f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 7 Nov 2019 21:58:57 +0100 Subject: [PATCH 0095/2244] Fix segfault on empty file recorded Write the file trailer only if the file header have been written, to avoid a segfault in libav. Fixes . --- app/src/recorder.c | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/app/src/recorder.c b/app/src/recorder.c index f96bcd26..c09e21ae 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -174,9 +174,14 @@ recorder_open(struct recorder *recorder, const AVCodec *input_codec) { void recorder_close(struct recorder *recorder) { - int ret = av_write_trailer(recorder->ctx); - if (ret < 0) { - LOGE("Failed to write trailer to %s", recorder->filename); + if (recorder->header_written) { + int ret = av_write_trailer(recorder->ctx); + if (ret < 0) { + LOGE("Failed to write trailer to %s", recorder->filename); + recorder->failed = true; + } + } else { + // the recorded file is empty recorder->failed = true; } avio_close(recorder->ctx->pb); From e282100d0b796b579b2a9b9656cf962cca88aca0 Mon Sep 17 00:00:00 2001 From: olbb Date: Tue, 19 Feb 2019 10:33:59 +0800 Subject: [PATCH 0096/2244] Call Looper.prepareMainLooper() to avoid exception Some devices internally create a Handler when creating an input Surface, causing an exception: > Surface: java.lang.RuntimeException: Can't create handler inside > thread that has not called Looper.prepare() As a workaround, call Looper.prepareMainLooper() beforehand. Fixes: - - Signed-off-by: Romain Vimont --- .../java/com/genymobile/scrcpy/ScreenEncoder.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index 8357b061..52f6f26b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -7,6 +7,7 @@ import android.media.MediaCodec; import android.media.MediaCodecInfo; import android.media.MediaFormat; import android.os.IBinder; +import android.os.Looper; import android.view.Surface; import java.io.FileDescriptor; @@ -54,6 +55,16 @@ public class ScreenEncoder implements Device.RotationListener { } public void streamScreen(Device device, FileDescriptor fd) throws IOException { + // Some devices internally create a Handler when creating an input Surface, causing an exception: + // "Can't create handler inside thread that has not called Looper.prepare()" + // + // + // Use Looper.prepareMainLooper() instead of Looper.prepare() to avoid a NullPointerException: + // "Attempt to read from field 'android.os.MessageQueue android.os.Looper.mQueue' + // on a null object reference" + // + Looper.prepareMainLooper(); + MediaFormat format = createFormat(bitRate, frameRate, iFrameInterval); device.setRotationListener(this); boolean alive; From 6996cbf5d38b2f5bfdc061b421ea204e3aafce67 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 9 Nov 2019 21:13:20 +0100 Subject: [PATCH 0097/2244] Log device disconnection If scrcpy closes due to socket disconnection, log a warning. --- app/src/scrcpy.c | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 16f1b4f7..64165ac5 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -218,6 +218,7 @@ event_loop(bool display, bool control) { case EVENT_RESULT_STOPPED_BY_USER: return true; case EVENT_RESULT_STOPPED_BY_EOS: + LOGW("Device disconnected"); return false; case EVENT_RESULT_CONTINUE: break; From f6f2868868497b2804e3308b774f52bcc5418a5c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 20 Oct 2019 15:32:33 +0200 Subject: [PATCH 0098/2244] Handle window events from screen.c Only the screen knows what to do on window events. This paves the way to handle more window events. --- app/src/scrcpy.c | 7 +------ app/src/screen.c | 11 +++++++++++ app/src/screen.h | 4 ++++ 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 64165ac5..0e56696a 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -144,12 +144,7 @@ handle_event(SDL_Event *event, bool control) { } break; case SDL_WINDOWEVENT: - switch (event->window.event) { - case SDL_WINDOWEVENT_EXPOSED: - case SDL_WINDOWEVENT_SIZE_CHANGED: - screen_render(&screen); - break; - } + screen_handle_window_event(&screen, &event->window); break; case SDL_TEXTINPUT: if (!control) { diff --git a/app/src/screen.c b/app/src/screen.c index 4bc4c5c5..0ef803fe 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -327,3 +327,14 @@ screen_resize_to_pixel_perfect(struct screen *screen) { LOGD("Resized to pixel-perfect"); } } + +void +screen_handle_window_event(struct screen *screen, + const SDL_WindowEvent *event) { + switch (event->event) { + case SDL_WINDOWEVENT_EXPOSED: + case SDL_WINDOWEVENT_SIZE_CHANGED: + screen_render(screen); + break; + } +} diff --git a/app/src/screen.h b/app/src/screen.h index bc189189..1c5695bc 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -76,4 +76,8 @@ screen_resize_to_fit(struct screen *screen); void screen_resize_to_pixel_perfect(struct screen *screen); +// react to window events +void +screen_handle_window_event(struct screen *screen, const SDL_WindowEvent *event); + #endif From 35c05bb3cec7e5270229574d0f9d17d4989c107e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 11 Nov 2019 21:26:35 +0100 Subject: [PATCH 0099/2244] Fix rotation while the window is maximized Keep the windowed window size to handle maximized window the same way as fullscreen window. Fixes --- app/src/screen.c | 68 ++++++++++++++++++++++++++++++++++-------------- app/src/screen.h | 39 ++++++++++++++++----------- 2 files changed, 73 insertions(+), 34 deletions(-) diff --git a/app/src/screen.c b/app/src/screen.c index 0ef803fe..df9af985 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -30,23 +30,28 @@ get_window_size(SDL_Window *window) { // get the windowed window size static struct size get_windowed_window_size(const struct screen *screen) { - if (screen->fullscreen) { + 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 - if (screen->fullscreen) { - // SDL_SetWindowSize will be called when fullscreen will be disabled - screen->windowed_window_size = new_size; - } else { - SDL_SetWindowSize(screen->window, new_size.width, new_size.height); - } + screen->windowed_window_size = new_size; + apply_windowed_size(screen); } // get the preferred display bounds (i.e. the screen bounds with some margins) @@ -194,6 +199,8 @@ screen_init_rendering(struct screen *screen, const char *window_title, return false; } + screen->windowed_window_size = window_size; + return true; } @@ -287,10 +294,6 @@ screen_render(struct screen *screen) { void screen_switch_fullscreen(struct screen *screen) { - if (!screen->fullscreen) { - // going to fullscreen, store the current windowed window size - screen->windowed_window_size = get_window_size(screen->window); - } uint32_t new_mode = screen->fullscreen ? 0 : SDL_WINDOW_FULLSCREEN_DESKTOP; if (SDL_SetWindowFullscreen(screen->window, new_mode)) { LOGW("Could not switch fullscreen mode: %s", SDL_GetError()); @@ -298,11 +301,7 @@ screen_switch_fullscreen(struct screen *screen) { } screen->fullscreen = !screen->fullscreen; - if (!screen->fullscreen) { - // fullscreen disabled, restore expected windowed window size - SDL_SetWindowSize(screen->window, screen->windowed_window_size.width, - screen->windowed_window_size.height); - } + apply_windowed_size(screen); LOGD("Switched to %s mode", screen->fullscreen ? "fullscreen" : "windowed"); screen_render(screen); @@ -310,7 +309,7 @@ screen_switch_fullscreen(struct screen *screen) { void screen_resize_to_fit(struct screen *screen) { - if (!screen->fullscreen) { + if (!screen->fullscreen && !screen->maximized) { struct size optimal_size = get_optimal_window_size(screen, screen->frame_size); SDL_SetWindowSize(screen->window, optimal_size.width, @@ -321,7 +320,7 @@ screen_resize_to_fit(struct screen *screen) { void screen_resize_to_pixel_perfect(struct screen *screen) { - if (!screen->fullscreen) { + if (!screen->fullscreen && !screen->maximized) { SDL_SetWindowSize(screen->window, screen->frame_size.width, screen->frame_size.height); LOGD("Resized to pixel-perfect"); @@ -333,8 +332,39 @@ screen_handle_window_event(struct screen *screen, const SDL_WindowEvent *event) { switch (event->event) { case SDL_WINDOWEVENT_EXPOSED: - case SDL_WINDOWEVENT_SIZE_CHANGED: 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. + SDL_assert(screen->windowed_window_size_backup.width); + SDL_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); + break; } } diff --git a/app/src/screen.h b/app/src/screen.h index 1c5695bc..275609ba 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -15,28 +15,37 @@ struct screen { SDL_Renderer *renderer; SDL_Texture *texture; struct size frame_size; - //used only in fullscreen mode to know the windowed window 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 has_frame; bool fullscreen; + bool maximized; bool no_window; }; -#define SCREEN_INITIALIZER { \ - .window = NULL, \ - .renderer = NULL, \ - .texture = NULL, \ - .frame_size = { \ - .width = 0, \ - .height = 0, \ - }, \ +#define SCREEN_INITIALIZER { \ + .window = NULL, \ + .renderer = NULL, \ + .texture = NULL, \ + .frame_size = { \ + .width = 0, \ + .height = 0, \ + }, \ .windowed_window_size = { \ - .width = 0, \ - .height = 0, \ - }, \ - .has_frame = false, \ - .fullscreen = false, \ - .no_window = false, \ + .width = 0, \ + .height = 0, \ + }, \ + .windowed_window_size_backup = { \ + .width = 0, \ + .height = 0, \ + }, \ + .has_frame = false, \ + .fullscreen = false, \ + .maximized = false, \ + .no_window = false, \ } // initialize default values From aa0f77c8983ef8049c2eba17bace430cf39ba171 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 11 Nov 2019 21:44:27 +0100 Subject: [PATCH 0100/2244] Accept resize shortcuts on maximized window Allow "resize to fit" and "resize to pixel-perfect" on maximized window: restore the window to normal size then resize. --- app/src/screen.c | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/app/src/screen.c b/app/src/screen.c index df9af985..7de57031 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -309,22 +309,35 @@ screen_switch_fullscreen(struct screen *screen) { void screen_resize_to_fit(struct screen *screen) { - if (!screen->fullscreen && !screen->maximized) { - struct size optimal_size = get_optimal_window_size(screen, - screen->frame_size); - SDL_SetWindowSize(screen->window, optimal_size.width, - optimal_size.height); - LOGD("Resized to optimal size"); + if (screen->fullscreen) { + return; } + + if (screen->maximized) { + SDL_RestoreWindow(screen->window); + screen->maximized = false; + } + + struct size optimal_size = + get_optimal_window_size(screen, screen->frame_size); + SDL_SetWindowSize(screen->window, optimal_size.width, optimal_size.height); + LOGD("Resized to optimal size"); } void screen_resize_to_pixel_perfect(struct screen *screen) { - if (!screen->fullscreen && !screen->maximized) { - SDL_SetWindowSize(screen->window, screen->frame_size.width, - screen->frame_size.height); - LOGD("Resized to pixel-perfect"); + if (screen->fullscreen) { + return; } + + if (screen->maximized) { + SDL_RestoreWindow(screen->window); + screen->maximized = false; + } + + SDL_SetWindowSize(screen->window, screen->frame_size.width, + screen->frame_size.height); + LOGD("Resized to pixel-perfect"); } void From b963a3b9d56f744cceba2e19cba3f9fef858c058 Mon Sep 17 00:00:00 2001 From: Yu-Chen Lin Date: Sun, 10 Nov 2019 20:23:58 +0800 Subject: [PATCH 0101/2244] Check client and server mismatch Send client version as first parameter and check it at server start. Signed-off-by: Yu-Chen Lin Signed-off-by: Romain Vimont --- app/src/server.c | 1 + .../java/com/genymobile/scrcpy/Server.java | 26 +++++++++++++------ 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/app/src/server.c b/app/src/server.c index de61001f..b40b065b 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -131,6 +131,7 @@ execute_server(struct server *server, const struct server_params *params) { #endif "/", // unused "com.genymobile.scrcpy.Server", + SCRCPY_VERSION, max_size_string, bit_rate_string, server->tunnel_forward ? "true" : "false", diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index eba89bdb..ba44d07c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -67,29 +67,39 @@ public final class Server { @SuppressWarnings("checkstyle:MagicNumber") private static Options createOptions(String... args) { - if (args.length != 6) { - throw new IllegalArgumentException("Expecting 6 parameters"); + if (args.length < 1) { + throw new IllegalArgumentException("Missing client version"); + } + + 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 + ")"); + } + + if (args.length != 7) { + throw new IllegalArgumentException("Expecting 7 parameters"); } Options options = new Options(); - int maxSize = Integer.parseInt(args[0]) & ~7; // multiple of 8 + int maxSize = Integer.parseInt(args[1]) & ~7; // multiple of 8 options.setMaxSize(maxSize); - int bitRate = Integer.parseInt(args[1]); + int bitRate = Integer.parseInt(args[2]); options.setBitRate(bitRate); // use "adb forward" instead of "adb tunnel"? (so the server must listen) - boolean tunnelForward = Boolean.parseBoolean(args[2]); + boolean tunnelForward = Boolean.parseBoolean(args[3]); options.setTunnelForward(tunnelForward); - Rect crop = parseCrop(args[3]); + Rect crop = parseCrop(args[4]); options.setCrop(crop); - boolean sendFrameMeta = Boolean.parseBoolean(args[4]); + boolean sendFrameMeta = Boolean.parseBoolean(args[5]); options.setSendFrameMeta(sendFrameMeta); - boolean control = Boolean.parseBoolean(args[5]); + boolean control = Boolean.parseBoolean(args[6]); options.setControl(control); return options; From 771bd8404d289d433586fdf25c02ebb4e812623e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 13 Nov 2019 16:04:16 +0100 Subject: [PATCH 0102/2244] Do not write invalid packet duration Configuration packets have no PTS. Do not compute a packet duration from their PTS. Fixes recording to mp4 on device rotation. --- app/src/recorder.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/src/recorder.c b/app/src/recorder.c index c09e21ae..f6f6fd96 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -301,8 +301,12 @@ run_recorder(void *data) { continue; } - // we now know the duration of the previous packet - previous->packet.duration = rec->packet.pts - previous->packet.pts; + // config packets have no PTS, we must ignore them + if (rec->packet.pts != AV_NOPTS_VALUE + && previous->packet.pts != AV_NOPTS_VALUE) { + // we now know the duration of the previous packet + previous->packet.duration = rec->packet.pts - previous->packet.pts; + } bool ok = recorder_write(recorder, &previous->packet); record_packet_delete(previous); From ce5635f28cec04f20dc3ef1c205e1740ac115c74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Fernando=20D=C3=ADaz=20A?= Date: Thu, 29 Aug 2019 00:25:17 -0500 Subject: [PATCH 0103/2244] Add option to specify the initial window position Add --window-x and --window-y parameters. Signed-off-by: Romain Vimont --- app/src/main.c | 46 ++++++++++++++++++++++++++++++++++++++++++++-- app/src/scrcpy.c | 3 ++- app/src/scrcpy.h | 4 ++++ app/src/screen.c | 8 +++++--- app/src/screen.h | 3 ++- 5 files changed, 57 insertions(+), 7 deletions(-) diff --git a/app/src/main.c b/app/src/main.c index 12c65ed4..348c3df0 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -110,6 +110,14 @@ static void usage(const char *arg0) { " --window-title text\n" " Set a custom window title.\n" "\n" + " --window-x value\n" + " Set the initial window horizontal position.\n" + " Default is -1 (automatic).\n" + "\n" + " --window-y value\n" + " Set the initial window vertical position.\n" + " Default is -1 (automatic).\n" + "\n" "Shortcuts:\n" "\n" " " CTRL_OR_CMD "+f\n" @@ -250,6 +258,27 @@ parse_max_size(char *optarg, uint16_t *max_size) { return true; } +static bool +parse_window_position(char *optarg, int16_t *position) { + char *endptr; + if (*optarg == '\0') { + LOGE("Window position parameter is empty"); + return false; + } + long value = strtol(optarg, &endptr, 0); + if (*endptr != '\0') { + LOGE("Invalid window position: %s", optarg); + return false; + } + if (value < -1 || value > 0x7fff) { + LOGE("Window position must be between -1 and 32767: %ld", value); + return false; + } + + *position = (int16_t) value; + return true; +} + static bool parse_port(char *optarg, uint16_t *port) { char *endptr; @@ -308,6 +337,8 @@ guess_record_format(const char *filename) { #define OPT_CROP 1004 #define OPT_RECORD_FORMAT 1005 #define OPT_PREFER_TEXT 1006 +#define OPT_WINDOW_X 1007 +#define OPT_WINDOW_Y 1008 static bool parse_args(struct args *args, int argc, char *argv[]) { @@ -331,8 +362,9 @@ parse_args(struct args *args, int argc, char *argv[]) { {"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-title", required_argument, NULL, OPT_WINDOW_TITLE}, + {"window-x", required_argument, NULL, OPT_WINDOW_X}, + {"window-y", required_argument, NULL, OPT_WINDOW_Y}, {NULL, 0, NULL, 0 }, }; @@ -410,6 +442,16 @@ parse_args(struct args *args, int argc, char *argv[]) { case OPT_WINDOW_TITLE: opts->window_title = optarg; break; + case OPT_WINDOW_X: + if (!parse_window_position(optarg, &opts->window_x)) { + return false; + } + break; + case OPT_WINDOW_Y: + if (!parse_window_position(optarg, &opts->window_y)) { + return false; + } + break; case OPT_PUSH_TARGET: opts->push_target = optarg; break; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 0e56696a..d9f0e308 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -387,7 +387,8 @@ scrcpy(const struct scrcpy_options *options) { options->window_title ? options->window_title : device_name; if (!screen_init_rendering(&screen, window_title, frame_size, - options->always_on_top)) { + options->always_on_top, options->window_x, + options->window_y)) { goto end; } diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index 70a41ec1..d0ef2392 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -18,6 +18,8 @@ struct scrcpy_options { uint16_t port; uint16_t max_size; uint32_t bit_rate; + int16_t window_x; + int16_t window_y; bool show_touches; bool fullscreen; bool always_on_top; @@ -38,6 +40,8 @@ struct scrcpy_options { .port = DEFAULT_LOCAL_PORT, \ .max_size = DEFAULT_LOCAL_PORT, \ .bit_rate = DEFAULT_BIT_RATE, \ + .window_x = -1, \ + .window_y = -1, \ .show_touches = false, \ .fullscreen = false, \ .always_on_top = false, \ diff --git a/app/src/screen.c b/app/src/screen.c index 7de57031..4543fab3 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -141,7 +141,8 @@ create_texture(SDL_Renderer *renderer, struct size frame_size) { bool screen_init_rendering(struct screen *screen, const char *window_title, - struct size frame_size, bool always_on_top) { + struct size frame_size, bool always_on_top, + int16_t window_x, int16_t window_y) { screen->frame_size = frame_size; struct size window_size = get_initial_optimal_size(frame_size); @@ -158,8 +159,9 @@ screen_init_rendering(struct screen *screen, const char *window_title, #endif } - screen->window = SDL_CreateWindow(window_title, SDL_WINDOWPOS_UNDEFINED, - SDL_WINDOWPOS_UNDEFINED, + int x = window_x != -1 ? window_x : SDL_WINDOWPOS_UNDEFINED; + int y = window_y != -1 ? window_y : SDL_WINDOWPOS_UNDEFINED; + screen->window = SDL_CreateWindow(window_title, x, y, window_size.width, window_size.height, window_flags); if (!screen->window) { diff --git a/app/src/screen.h b/app/src/screen.h index 275609ba..204e3226 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -55,7 +55,8 @@ screen_init(struct screen *screen); // initialize screen, create window, renderer and texture (window is hidden) bool screen_init_rendering(struct screen *screen, const char *window_title, - struct size frame_size, bool always_on_top); + struct size frame_size, bool always_on_top, + int16_t window_x, int16_t window_y); // show the window void From b6e2f8ae00737098af5139b56cd3ca15bd5c9335 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 3 Nov 2019 17:34:01 +0100 Subject: [PATCH 0104/2244] Update manpage for --window-{x,y} options --- app/scrcpy.1 | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 203395a4..fb39189b 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -113,6 +113,18 @@ Print the version of scrcpy. .BI \-\-window\-title " text Set a custom window title. +.TP +.BI \-\-window\-x " value +Set the initial window horizontal position. + +Default is -1 (automatic).\n + +.TP +.BI \-\-window\-y " value +Set the initial window vertical position. + +Default is -1 (automatic).\n + .SH SHORTCUTS From 9fd7a80a897103c6675e651b7f1e78b1575ab148 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 3 Nov 2019 18:00:11 +0100 Subject: [PATCH 0105/2244] Add option to specify the initial window size Add --window-width and --window-height parameters. If only one is provided, the other is computed so that the aspect ratio is preserved. --- app/scrcpy.1 | 11 +++++++++++ app/src/main.c | 43 +++++++++++++++++++++++++++++++++++++++++++ app/src/scrcpy.c | 3 ++- app/src/scrcpy.h | 4 ++++ app/src/screen.c | 31 +++++++++++++++++++++++++++---- app/src/screen.h | 3 ++- 6 files changed, 89 insertions(+), 6 deletions(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index fb39189b..bde58d65 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -125,6 +125,17 @@ Set the initial window vertical position. Default is -1 (automatic).\n +.TP +.BI \-\-window\-width " value +Set the initial window width. + +Default is 0 (automatic).\n + +.TP +.BI \-\-window\-height " value +Set the initial window height. + +Default is 0 (automatic).\n .SH SHORTCUTS diff --git a/app/src/main.c b/app/src/main.c index 348c3df0..0046a12d 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -118,6 +118,14 @@ static void usage(const char *arg0) { " Set the initial window vertical position.\n" " Default is -1 (automatic).\n" "\n" + " --window-width value\n" + " Set the initial window width.\n" + " Default is -1 (automatic).\n" + "\n" + " --window-height value\n" + " Set the initial window width.\n" + " Default is -1 (automatic).\n" + "\n" "Shortcuts:\n" "\n" " " CTRL_OR_CMD "+f\n" @@ -279,6 +287,27 @@ parse_window_position(char *optarg, int16_t *position) { return true; } +static bool +parse_window_dimension(char *optarg, uint16_t *dimension) { + char *endptr; + if (*optarg == '\0') { + LOGE("Window dimension parameter is empty"); + return false; + } + long value = strtol(optarg, &endptr, 0); + if (*endptr != '\0') { + LOGE("Invalid window dimension: %s", optarg); + return false; + } + if (value & ~0xffff) { + LOGE("Window position must be between 0 and 65535: %ld", value); + return false; + } + + *dimension = (uint16_t) value; + return true; +} + static bool parse_port(char *optarg, uint16_t *port) { char *endptr; @@ -339,6 +368,8 @@ guess_record_format(const char *filename) { #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 static bool parse_args(struct args *args, int argc, char *argv[]) { @@ -365,6 +396,8 @@ parse_args(struct args *args, int argc, char *argv[]) { {"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}, {NULL, 0, NULL, 0 }, }; @@ -452,6 +485,16 @@ parse_args(struct args *args, int argc, char *argv[]) { return false; } break; + case OPT_WINDOW_WIDTH: + if (!parse_window_dimension(optarg, &opts->window_width)) { + return false; + } + break; + case OPT_WINDOW_HEIGHT: + if (!parse_window_dimension(optarg, &opts->window_height)) { + return false; + } + break; case OPT_PUSH_TARGET: opts->push_target = optarg; break; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index d9f0e308..5213d779 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -388,7 +388,8 @@ scrcpy(const struct scrcpy_options *options) { if (!screen_init_rendering(&screen, window_title, frame_size, options->always_on_top, options->window_x, - options->window_y)) { + options->window_y, options->window_width, + options->window_height)) { goto end; } diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index d0ef2392..d0612172 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -20,6 +20,8 @@ struct scrcpy_options { uint32_t bit_rate; int16_t window_x; int16_t window_y; + uint16_t window_width; + uint16_t window_height; bool show_touches; bool fullscreen; bool always_on_top; @@ -42,6 +44,8 @@ struct scrcpy_options { .bit_rate = DEFAULT_BIT_RATE, \ .window_x = -1, \ .window_y = -1, \ + .window_width = 0, \ + .window_height = 0, \ .show_touches = false, \ .fullscreen = false, \ .always_on_top = false, \ diff --git a/app/src/screen.c b/app/src/screen.c index 4543fab3..3d021e01 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -122,9 +122,30 @@ get_optimal_window_size(const struct screen *screen, struct size frame_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) { - return get_optimal_size(frame_size, frame_size); +get_initial_optimal_size(struct size frame_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); + } 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; + } + 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; + } + } + return window_size; } void @@ -142,10 +163,12 @@ create_texture(SDL_Renderer *renderer, struct size frame_size) { 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) { + int16_t window_x, int16_t window_y, uint16_t window_width, + uint16_t window_height) { screen->frame_size = frame_size; - struct size window_size = get_initial_optimal_size(frame_size); + struct size window_size = + get_initial_optimal_size(frame_size, window_width, window_height); uint32_t window_flags = SDL_WINDOW_HIDDEN | SDL_WINDOW_RESIZABLE; #ifdef HIDPI_SUPPORT window_flags |= SDL_WINDOW_ALLOW_HIGHDPI; diff --git a/app/src/screen.h b/app/src/screen.h index 204e3226..eaa46850 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -56,7 +56,8 @@ screen_init(struct screen *screen); 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); + int16_t window_x, int16_t window_y, uint16_t window_width, + uint16_t window_height); // show the window void From 59bc5bc1f55a2671062fb5b6000067f55fdb751f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Fernando=20D=C3=ADaz=20A?= Date: Thu, 29 Aug 2019 00:25:17 -0500 Subject: [PATCH 0106/2244] Add option to disable window decoration Add --window-borderless parameter. Signed-off-by: Romain Vimont --- app/src/main.c | 9 +++++++++ app/src/scrcpy.c | 3 ++- app/src/scrcpy.h | 2 ++ app/src/screen.c | 5 ++++- app/src/screen.h | 2 +- 5 files changed, 18 insertions(+), 3 deletions(-) diff --git a/app/src/main.c b/app/src/main.c index 0046a12d..2e8b7897 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -107,6 +107,9 @@ static void usage(const char *arg0) { " -v, --version\n" " Print the version of scrcpy.\n" "\n" + " --window-borderless\n" + " Disable window decorations (display borderless window).\n" + "\n" " --window-title text\n" " Set a custom window title.\n" "\n" @@ -370,6 +373,7 @@ guess_record_format(const char *filename) { #define OPT_WINDOW_Y 1008 #define OPT_WINDOW_WIDTH 1009 #define OPT_WINDOW_HEIGHT 1010 +#define OPT_WINDOW_BORDERLESS 1011 static bool parse_args(struct args *args, int argc, char *argv[]) { @@ -398,6 +402,8 @@ parse_args(struct args *args, int argc, char *argv[]) { {"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 }, }; @@ -495,6 +501,9 @@ parse_args(struct args *args, int argc, char *argv[]) { return false; } break; + case OPT_WINDOW_BORDERLESS: + opts->window_borderless = true; + break; case OPT_PUSH_TARGET: opts->push_target = optarg; break; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 5213d779..c64acf49 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -389,7 +389,8 @@ scrcpy(const struct scrcpy_options *options) { if (!screen_init_rendering(&screen, window_title, frame_size, options->always_on_top, options->window_x, options->window_y, options->window_width, - options->window_height)) { + options->window_height, + options->window_borderless)) { goto end; } diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index d0612172..f6779080 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -30,6 +30,7 @@ struct scrcpy_options { bool turn_screen_off; bool render_expired_frames; bool prefer_text; + bool window_borderless; }; #define SCRCPY_OPTIONS_DEFAULT { \ @@ -54,6 +55,7 @@ struct scrcpy_options { .turn_screen_off = false, \ .render_expired_frames = false, \ .prefer_text = false, \ + .window_borderless = false, \ } bool diff --git a/app/src/screen.c b/app/src/screen.c index 3d021e01..ab4d434e 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -164,7 +164,7 @@ 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) { + uint16_t window_height, bool window_borderless) { screen->frame_size = frame_size; struct size window_size = @@ -181,6 +181,9 @@ screen_init_rendering(struct screen *screen, const char *window_title, "(compile with SDL >= 2.0.5 to enable it)"); #endif } + if (window_borderless) { + window_flags |= SDL_WINDOW_BORDERLESS; + } int x = window_x != -1 ? window_x : SDL_WINDOWPOS_UNDEFINED; int y = window_y != -1 ? window_y : SDL_WINDOWPOS_UNDEFINED; diff --git a/app/src/screen.h b/app/src/screen.h index eaa46850..2346ff15 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -57,7 +57,7 @@ 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); + uint16_t window_height, bool window_borderless); // show the window void From 59073223aad2aae42d5033b0a01e7838a8f6cd46 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 15 Nov 2019 18:59:47 +0100 Subject: [PATCH 0107/2244] Update manpage for --window-borderless option --- app/scrcpy.1 | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index bde58d65..2547658e 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -109,6 +109,10 @@ Make scrcpy window always on top (above other windows). .B \-v, \-\-version Print the version of scrcpy. +.TP +.B \-\-window\-borderless +Disable window decorations (display borderless window). + .TP .BI \-\-window\-title " text Set a custom window title. From 1b78a77962bd19cd89c488a55972ec9a50eb1031 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 17 Nov 2019 21:38:10 +0100 Subject: [PATCH 0108/2244] Fix error message --- 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 2e8b7897..23d258d5 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -315,7 +315,7 @@ static bool parse_port(char *optarg, uint16_t *port) { char *endptr; if (*optarg == '\0') { - LOGE("Invalid port parameter is empty"); + LOGE("Port parameter is empty"); return false; } long value = strtol(optarg, &endptr, 0); From fb976816f98d0c9792cf63c414b5fbfaf9e2c4a5 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 17 Nov 2019 15:35:28 +0100 Subject: [PATCH 0109/2244] Do not expose frame rate in ScreenEncoder The KEY_FRAME_RATE parameter value is necessary for the configuration of the encoder, but its actual value does not impact the frame rate (only resources used by the encoder). Therefore, it's an internal detail and should not be exposed by the ScreenEncoder class. --- .../com/genymobile/scrcpy/ScreenEncoder.java | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index 52f6f26b..f0b6db44 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -17,32 +17,27 @@ import java.util.concurrent.atomic.AtomicBoolean; public class ScreenEncoder implements Device.RotationListener { - private static final int DEFAULT_FRAME_RATE = 60; // fps 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 int REPEAT_FRAME_DELAY = 6; // repeat after 6 frames - - private static final int MICROSECONDS_IN_ONE_SECOND = 1_000_000; private static final int NO_PTS = -1; private final AtomicBoolean rotationChanged = new AtomicBoolean(); private final ByteBuffer headerBuffer = ByteBuffer.allocate(12); private int bitRate; - private int frameRate; private int iFrameInterval; private boolean sendFrameMeta; private long ptsOrigin; - public ScreenEncoder(boolean sendFrameMeta, int bitRate, int frameRate, int iFrameInterval) { + public ScreenEncoder(boolean sendFrameMeta, int bitRate, int iFrameInterval) { this.sendFrameMeta = sendFrameMeta; this.bitRate = bitRate; - this.frameRate = frameRate; this.iFrameInterval = iFrameInterval; } public ScreenEncoder(boolean sendFrameMeta, int bitRate) { - this(sendFrameMeta, bitRate, DEFAULT_FRAME_RATE, DEFAULT_I_FRAME_INTERVAL); + this(sendFrameMeta, bitRate, DEFAULT_I_FRAME_INTERVAL); } @Override @@ -65,7 +60,7 @@ public class ScreenEncoder implements Device.RotationListener { // Looper.prepareMainLooper(); - MediaFormat format = createFormat(bitRate, frameRate, iFrameInterval); + MediaFormat format = createFormat(bitRate, iFrameInterval); device.setRotationListener(this); boolean alive; try { @@ -148,15 +143,16 @@ public class ScreenEncoder implements Device.RotationListener { return MediaCodec.createEncoderByType("video/avc"); } - private static MediaFormat createFormat(int bitRate, int frameRate, int iFrameInterval) throws IOException { + private static MediaFormat createFormat(int bitRate, int iFrameInterval) throws IOException { MediaFormat format = new MediaFormat(); format.setString(MediaFormat.KEY_MIME, "video/avc"); format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate); - format.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate); + // 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); // display the very first frame, and recover from bad quality when no new frames - format.setLong(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER, MICROSECONDS_IN_ONE_SECOND * REPEAT_FRAME_DELAY / frameRate); // µs + format.setLong(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER, REPEAT_FRAME_DELAY_US); // µs return format; } From 1d97d7213d004ba4ac781baa13e9a0884b305cc3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 17 Nov 2019 22:07:19 +0100 Subject: [PATCH 0110/2244] Add option --max-fps Add an option to limit the capture frame rate. It only works for devices with Android >= 10. Fixes --- README.md | 8 +++++ app/scrcpy.1 | 4 +++ app/src/main.c | 33 +++++++++++++++++++ app/src/scrcpy.c | 1 + app/src/scrcpy.h | 2 ++ app/src/server.c | 3 ++ app/src/server.h | 1 + .../java/com/genymobile/scrcpy/Options.java | 9 +++++ .../com/genymobile/scrcpy/ScreenEncoder.java | 21 +++++++++--- .../java/com/genymobile/scrcpy/Server.java | 17 ++++++---- 10 files changed, 87 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index f0717c2a..17703152 100644 --- a/README.md +++ b/README.md @@ -134,6 +134,14 @@ scrcpy --bit-rate 2M scrcpy -b 2M # short version ``` +### Limit capture frame rate + +On device with Android >= 10, the capture frame rate can be limited: + +```bash +scrcpy --max-fps 15 +``` + ### Crop diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 2547658e..948cac4d 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -41,6 +41,10 @@ Force recording format (either mp4 or mkv). .B \-h, \-\-help Print this help. +.TP +.BI \-\-max\-fps " value +Limit the framerate of screen capture (only supported on devices with Android >= 10). + .TP .BI "\-m, \-\-max\-size " value Limit both the width and height of the video to \fIvalue\fR. The other dimension is computed so that the device aspect\-ratio is preserved. diff --git a/app/src/main.c b/app/src/main.c index 23d258d5..da0d2074 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -50,6 +50,10 @@ static void usage(const char *arg0) { " -h, --help\n" " 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" + "\n" " -m, --max-size value\n" " Limit both the width and height of the video to value. The\n" " other dimension is computed so that the device aspect-ratio\n" @@ -269,6 +273,28 @@ parse_max_size(char *optarg, uint16_t *max_size) { return true; } +static bool +parse_max_fps(const char *optarg, uint16_t *max_fps) { + char *endptr; + if (*optarg == '\0') { + LOGE("Max FPS parameter is empty"); + return false; + } + long value = strtol(optarg, &endptr, 0); + if (*endptr != '\0') { + LOGE("Invalid max FPS: %s", optarg); + return false; + } + if (value & ~0xffff) { + // in practice, it should not be higher than 60 + LOGE("Max FPS value is invalid: %ld", value); + return false; + } + + *max_fps = (uint16_t) value; + return true; +} + static bool parse_window_position(char *optarg, int16_t *position) { char *endptr; @@ -374,6 +400,7 @@ guess_record_format(const char *filename) { #define OPT_WINDOW_WIDTH 1009 #define OPT_WINDOW_HEIGHT 1010 #define OPT_WINDOW_BORDERLESS 1011 +#define OPT_MAX_FPS 1012 static bool parse_args(struct args *args, int argc, char *argv[]) { @@ -383,6 +410,7 @@ parse_args(struct args *args, int argc, char *argv[]) { {"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'}, @@ -438,6 +466,11 @@ parse_args(struct args *args, int argc, char *argv[]) { case 'h': args->help = true; break; + case OPT_MAX_FPS: + if (!parse_max_fps(optarg, &opts->max_fps)) { + return false; + } + break; case 'm': if (!parse_max_size(optarg, &opts->max_size)) { return false; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index c64acf49..67f1de16 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -280,6 +280,7 @@ scrcpy(const struct scrcpy_options *options) { .local_port = options->port, .max_size = options->max_size, .bit_rate = options->bit_rate, + .max_fps = options->max_fps, .control = options->control, }; if (!server_start(&server, options->serial, ¶ms)) { diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index f6779080..8723f29f 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -18,6 +18,7 @@ struct scrcpy_options { uint16_t port; uint16_t max_size; uint32_t bit_rate; + uint16_t max_fps; int16_t window_x; int16_t window_y; uint16_t window_width; @@ -43,6 +44,7 @@ struct scrcpy_options { .port = DEFAULT_LOCAL_PORT, \ .max_size = DEFAULT_LOCAL_PORT, \ .bit_rate = DEFAULT_BIT_RATE, \ + .max_fps = 0, \ .window_x = -1, \ .window_y = -1, \ .window_width = 0, \ diff --git a/app/src/server.c b/app/src/server.c index b40b065b..36290326 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -118,8 +118,10 @@ static process_t 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]; sprintf(max_size_string, "%"PRIu16, params->max_size); sprintf(bit_rate_string, "%"PRIu32, params->bit_rate); + sprintf(max_fps_string, "%"PRIu16, params->max_fps); const char *const cmd[] = { "shell", "CLASSPATH=/data/local/tmp/" SERVER_FILENAME, @@ -134,6 +136,7 @@ execute_server(struct server *server, const struct server_params *params) { SCRCPY_VERSION, max_size_string, bit_rate_string, + max_fps_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 2140d8ab..f46ced19 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -35,6 +35,7 @@ struct server_params { uint16_t local_port; uint16_t max_size; uint32_t bit_rate; + uint16_t max_fps; bool control; }; diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index af6b2ee1..5b993f30 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -5,6 +5,7 @@ import android.graphics.Rect; public class Options { private int maxSize; private int bitRate; + private int maxFps; private boolean tunnelForward; private Rect crop; private boolean sendFrameMeta; // send PTS so that the client may record properly @@ -26,6 +27,14 @@ public class Options { this.bitRate = bitRate; } + public int getMaxFps() { + return maxFps; + } + + public void setMaxFps(int maxFps) { + this.maxFps = maxFps; + } + public boolean isTunnelForward() { return tunnelForward; } diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index f0b6db44..e58310a1 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -6,6 +6,7 @@ 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.os.Looper; import android.view.Surface; @@ -26,18 +27,20 @@ public class ScreenEncoder implements Device.RotationListener { private final ByteBuffer headerBuffer = ByteBuffer.allocate(12); private int bitRate; + private int maxFps; private int iFrameInterval; private boolean sendFrameMeta; private long ptsOrigin; - public ScreenEncoder(boolean sendFrameMeta, int bitRate, int iFrameInterval) { + public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, int iFrameInterval) { this.sendFrameMeta = sendFrameMeta; this.bitRate = bitRate; + this.maxFps = maxFps; this.iFrameInterval = iFrameInterval; } - public ScreenEncoder(boolean sendFrameMeta, int bitRate) { - this(sendFrameMeta, bitRate, DEFAULT_I_FRAME_INTERVAL); + public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps) { + this(sendFrameMeta, bitRate, maxFps, DEFAULT_I_FRAME_INTERVAL); } @Override @@ -60,7 +63,7 @@ public class ScreenEncoder implements Device.RotationListener { // Looper.prepareMainLooper(); - MediaFormat format = createFormat(bitRate, iFrameInterval); + MediaFormat format = createFormat(bitRate, maxFps, iFrameInterval); device.setRotationListener(this); boolean alive; try { @@ -143,7 +146,8 @@ public class ScreenEncoder implements Device.RotationListener { return MediaCodec.createEncoderByType("video/avc"); } - private static MediaFormat createFormat(int bitRate, int iFrameInterval) throws IOException { + @SuppressWarnings("checkstyle:MagicNumber") + private static MediaFormat createFormat(int bitRate, int maxFps, int iFrameInterval) { MediaFormat format = new MediaFormat(); format.setString(MediaFormat.KEY_MIME, "video/avc"); format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate); @@ -153,6 +157,13 @@ public class ScreenEncoder implements Device.RotationListener { format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iFrameInterval); // 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"); + } + } 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 ba44d07c..ad14e5d8 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -17,7 +17,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()); + ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps()); if (options.getControl()) { Controller controller = new Controller(device, connection); @@ -77,8 +77,8 @@ public final class Server { + "(" + BuildConfig.VERSION_NAME + ")"); } - if (args.length != 7) { - throw new IllegalArgumentException("Expecting 7 parameters"); + if (args.length != 8) { + throw new IllegalArgumentException("Expecting 8 parameters"); } Options options = new Options(); @@ -89,17 +89,20 @@ public final class Server { int bitRate = Integer.parseInt(args[2]); options.setBitRate(bitRate); + int maxFps = Integer.parseInt(args[3]); + options.setMaxFps(maxFps); + // use "adb forward" instead of "adb tunnel"? (so the server must listen) - boolean tunnelForward = Boolean.parseBoolean(args[3]); + boolean tunnelForward = Boolean.parseBoolean(args[4]); options.setTunnelForward(tunnelForward); - Rect crop = parseCrop(args[4]); + Rect crop = parseCrop(args[5]); options.setCrop(crop); - boolean sendFrameMeta = Boolean.parseBoolean(args[5]); + boolean sendFrameMeta = Boolean.parseBoolean(args[6]); options.setSendFrameMeta(sendFrameMeta); - boolean control = Boolean.parseBoolean(args[6]); + boolean control = Boolean.parseBoolean(args[7]); options.setControl(control); return options; From 7fd800d58324a3f7520e9b225a9860ac5c712708 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 18 Nov 2019 14:30:21 +0100 Subject: [PATCH 0111/2244] Generate VERSION_NAME in build_without_gradle.sh Since commit b963a3b9d56f744cceba2e19cba3f9fef858c058, the server uses BuildConfig.VERSION_NAME. Generate this field manually for building without gradle. --- 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 daf85008..d1581ea1 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -11,6 +11,8 @@ set -e +SCRCPY_VERSION_NAME=1.10 + PLATFORM=${ANDROID_PLATFORM:-29} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-29.0.2} @@ -31,6 +33,7 @@ package com.genymobile.scrcpy; public final class BuildConfig { public static final boolean DEBUG = false; + public static final String VERSION_NAME = "$SCRCPY_VERSION_NAME"; } EOF From 601b0fecdd4fff284cb8ac38e545fcec9b3cf0fd Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 18 Nov 2019 14:33:14 +0100 Subject: [PATCH 0112/2244] Extract DEBUG flag in build_without_gradle.sh --- server/build_without_gradle.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index d1581ea1..b4605aa9 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -11,6 +11,7 @@ set -e +SCRCPY_DEBUG=false SCRCPY_VERSION_NAME=1.10 PLATFORM=${ANDROID_PLATFORM:-29} @@ -32,7 +33,7 @@ mkdir -p "$CLASSES_DIR/com/genymobile/scrcpy" package com.genymobile.scrcpy; public final class BuildConfig { - public static final boolean DEBUG = false; + public static final boolean DEBUG = $SCRCPY_DEBUG; public static final String VERSION_NAME = "$SCRCPY_VERSION_NAME"; } EOF From 7aed5d5b60c7ae82f5ca2353c294076a2e0ffc2c Mon Sep 17 00:00:00 2001 From: senta2006 Date: Fri, 15 Nov 2019 17:44:24 +0900 Subject: [PATCH 0113/2244] Fix typos PR Signed-off-by: Romain Vimont --- app/src/server.c | 2 +- config/checkstyle/checkstyle.xml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/server.c b/app/src/server.c index 36290326..b37b39d0 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -281,7 +281,7 @@ server_connect_to(struct server *server) { server->control_socket = net_accept(server->server_socket); if (server->control_socket == INVALID_SOCKET) { - // the video_socket will be clean up on destroy + // the video_socket will be cleaned up on destroy return false; } diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index 63ee315a..798814d9 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -54,7 +54,7 @@ page at http://checkstyle.sourceforge.net/config.html --> - + @@ -99,7 +99,7 @@ page at http://checkstyle.sourceforge.net/config.html --> - + From 18f2e33a8bf967a813142dbe9556813032ec9f0b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 19 Nov 2019 12:22:11 +0100 Subject: [PATCH 0114/2244] Fix noconsole.exe The linker flag "-mwindows" has no effect on my current MinGW. Instead, passing "-Wl,--subsystem,windows" works. Fixes --- app/meson.build | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/meson.build b/app/meson.build index 145e0ef6..159ae695 100644 --- a/app/meson.build +++ b/app/meson.build @@ -123,10 +123,8 @@ configure_file(configuration: conf, output: 'config.h') src_dir = include_directories('src') if get_option('windows_noconsole') - c_args = [ '-mwindows' ] - link_args = [ '-mwindows' ] + link_args = [ '-Wl,--subsystem,windows' ] else - c_args = [] link_args = [] endif @@ -134,7 +132,7 @@ executable('scrcpy', src, dependencies: dependencies, include_directories: src_dir, install: true, - c_args: c_args, + c_args: [], link_args: link_args) install_man('scrcpy.1') From 213c468c2032d1c1490c6a7951c999c36f452b8f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 13 Nov 2019 12:08:36 +0100 Subject: [PATCH 0115/2244] Move workarounds to a separate class Extract workarounds (currently only one) to a separate class to avoid polluting the main code. --- .../com/genymobile/scrcpy/ScreenEncoder.java | 11 +--------- .../com/genymobile/scrcpy/Workarounds.java | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+), 10 deletions(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/Workarounds.java diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index e58310a1..504e9540 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -8,7 +8,6 @@ import android.media.MediaCodecInfo; import android.media.MediaFormat; import android.os.Build; import android.os.IBinder; -import android.os.Looper; import android.view.Surface; import java.io.FileDescriptor; @@ -53,15 +52,7 @@ public class ScreenEncoder implements Device.RotationListener { } public void streamScreen(Device device, FileDescriptor fd) throws IOException { - // Some devices internally create a Handler when creating an input Surface, causing an exception: - // "Can't create handler inside thread that has not called Looper.prepare()" - // - // - // Use Looper.prepareMainLooper() instead of Looper.prepare() to avoid a NullPointerException: - // "Attempt to read from field 'android.os.MessageQueue android.os.Looper.mQueue' - // on a null object reference" - // - Looper.prepareMainLooper(); + Workarounds.prepareMainLooper(); MediaFormat format = createFormat(bitRate, maxFps, iFrameInterval); device.setRotationListener(this); diff --git a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java new file mode 100644 index 00000000..4dbf152e --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java @@ -0,0 +1,21 @@ +package com.genymobile.scrcpy; + +import android.os.Looper; + +public final class Workarounds { + private Workarounds() { + // not instantiable + } + + public static void prepareMainLooper() { + // Some devices internally create a Handler when creating an input Surface, causing an exception: + // "Can't create handler inside thread that has not called Looper.prepare()" + // + // + // Use Looper.prepareMainLooper() instead of Looper.prepare() to avoid a NullPointerException: + // "Attempt to read from field 'android.os.MessageQueue android.os.Looper.mQueue' + // on a null object reference" + // + Looper.prepareMainLooper(); + } +} From 90293240cc622bb58cb1de741f86cbc0889c03e8 Mon Sep 17 00:00:00 2001 From: act262 Date: Mon, 28 Oct 2019 11:18:53 +0800 Subject: [PATCH 0116/2244] Fix meizu 16th NPE Fill AppInfo to avoid NullPointerException on some devices. Fixes Signed-off-by: Romain Vimont --- .../com/genymobile/scrcpy/ScreenEncoder.java | 1 + .../com/genymobile/scrcpy/Workarounds.java | 43 +++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index 504e9540..c9a37f84 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -53,6 +53,7 @@ public class ScreenEncoder implements Device.RotationListener { public void streamScreen(Device device, FileDescriptor fd) throws IOException { Workarounds.prepareMainLooper(); + Workarounds.fillAppInfo(); MediaFormat format = createFormat(bitRate, maxFps, iFrameInterval); device.setRotationListener(this); diff --git a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java index 4dbf152e..f45d82a4 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java +++ b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java @@ -1,7 +1,12 @@ package com.genymobile.scrcpy; +import android.annotation.SuppressLint; +import android.content.pm.ApplicationInfo; import android.os.Looper; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; + public final class Workarounds { private Workarounds() { // not instantiable @@ -18,4 +23,42 @@ public final class Workarounds { // Looper.prepareMainLooper(); } + + @SuppressLint("PrivateApi") + public static void fillAppInfo() { + try { + // ActivityThread activityThread = new ActivityThread(); + Class activityThreadClass = Class.forName("android.app.ActivityThread"); + Constructor activityThreadConstructor = activityThreadClass.getDeclaredConstructor(); + activityThreadConstructor.setAccessible(true); + Object activityThread = activityThreadConstructor.newInstance(); + + // ActivityThread.sCurrentActivityThread = activityThread; + Field sCurrentActivityThreadField = activityThreadClass.getDeclaredField("sCurrentActivityThread"); + sCurrentActivityThreadField.setAccessible(true); + sCurrentActivityThreadField.set(null, activityThread); + + // ActivityThread.AppBindData appBindData = new ActivityThread.AppBindData(); + Class appBindDataClass = Class.forName("android.app.ActivityThread$AppBindData"); + Constructor appBindDataConstructor = appBindDataClass.getDeclaredConstructor(); + appBindDataConstructor.setAccessible(true); + Object appBindData = appBindDataConstructor.newInstance(); + + ApplicationInfo applicationInfo = new ApplicationInfo(); + applicationInfo.packageName = "com.genymobile.scrcpy"; + + // appBindData.appInfo = applicationInfo; + Field appInfo = appBindDataClass.getDeclaredField("appInfo"); + appInfo.setAccessible(true); + appInfo.set(appBindData, applicationInfo); + + // activityThread.mBoundApplication = appBindData; + Field mBoundApplicationField = activityThreadClass.getDeclaredField("mBoundApplication"); + mBoundApplicationField.setAccessible(true); + mBoundApplicationField.set(activityThread, appBindData); + } catch (Throwable throwable) { + // this is a workaround, so failing is not an error + Ln.w("Could not fill app info: " + throwable.getMessage()); + } + } } From b145b8d5f4455ed4ebef852a4f63654dbe57b483 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 19 Nov 2019 18:41:43 +0100 Subject: [PATCH 0117/2244] Reorganize features in README --- README.md | 187 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 105 insertions(+), 82 deletions(-) diff --git a/README.md b/README.md index 17703152..b4e9a20e 100644 --- a/README.md +++ b/README.md @@ -108,8 +108,9 @@ scrcpy --help ## Features +### Capture configuration -### Reduce size +#### Reduce size Sometimes, it is useful to mirror an Android device at a lower definition to increase performance. @@ -125,7 +126,7 @@ The other dimension is computed to that the device aspect ratio is preserved. That way, a device in 1920×1080 will be mirrored at 1024×576. -### Change bit-rate +#### Change bit-rate The default bit-rate is 8 Mbps. To change the video bitrate (e.g. to 2 Mbps): @@ -134,16 +135,15 @@ scrcpy --bit-rate 2M scrcpy -b 2M # short version ``` -### Limit capture frame rate +#### Limit frame rate -On device with Android >= 10, the capture frame rate can be limited: +On devices with Android >= 10, the capture frame rate can be limited: ```bash scrcpy --max-fps 15 ``` - -### Crop +#### Crop The device screen may be cropped to mirror only part of the screen. @@ -156,29 +156,7 @@ scrcpy --crop 1224:1440:0:0 # 1224x1440 at offset (0,0) If `--max-size` is also specified, resizing is applied after cropping. -### Wireless - -_Scrcpy_ uses `adb` to communicate with the device, and `adb` can [connect] to a -device over TCP/IP: - -1. Connect the device to the same Wi-Fi as your computer. -2. Get your device IP address (in Settings → About phone → Status). -3. Enable adb over TCP/IP on your device: `adb tcpip 5555`. -4. Unplug your device. -5. Connect to your device: `adb connect DEVICE_IP:5555` _(replace `DEVICE_IP`)_. -6. Run `scrcpy` as usual. - -It may be useful to decrease the bit-rate and the definition: - -```bash -scrcpy --bit-rate 2M --max-size 800 -scrcpy -b2M -m800 # short version -``` - -[connect]: https://developer.android.com/studio/command-line/adb.html#wireless - - -### Record screen +### Recording It is possible to record the screen while mirroring: @@ -203,7 +181,31 @@ variation] does not impact the recorded file. [packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation -### Multi-devices +### Connection + +#### Wireless + +_Scrcpy_ uses `adb` to communicate with the device, and `adb` can [connect] to a +device over TCP/IP: + +1. Connect the device to the same Wi-Fi as your computer. +2. Get your device IP address (in Settings → About phone → Status). +3. Enable adb over TCP/IP on your device: `adb tcpip 5555`. +4. Unplug your device. +5. Connect to your device: `adb connect DEVICE_IP:5555` _(replace `DEVICE_IP`)_. +6. Run `scrcpy` as usual. + +It may be useful to decrease the bit-rate and the definition: + +```bash +scrcpy --bit-rate 2M --max-size 800 +scrcpy -b2M -m800 # short version +``` + +[connect]: https://developer.android.com/studio/command-line/adb.html#wireless + + +#### Multi-devices If several devices are listed in `adb devices`, you must specify the _serial_: @@ -215,7 +217,41 @@ scrcpy -s 0123456789abcdef # short version You can start several instances of _scrcpy_ for several devices. -### Fullscreen +### Window configuration + +#### Title + +By default, the window title is the device model. It can be changed: + +```bash +scrcpy --window-title 'My device' +``` + +#### Position and size + +The initial window position and size may be specified: + +```bash +scrcpy --window-x 100 --window-y 100 --window-width 800 --window-height 600 +``` + +#### Borderless + +To disable window decorations: + +```bash +scrcpy --window-borderless +``` + +#### Always on top + +To keep the scrcpy window always on top: + +```bash +scrcpy --always-on-top +``` + +#### Fullscreen The app may be started directly in fullscreen: @@ -227,16 +263,45 @@ scrcpy -f # short version Fullscreen can then be toggled dynamically with `Ctrl`+`f`. -### Always on top +### Other mirroring options -The window of app can always be above others by: +#### Read-only + +To disable controls (everything which can interact with the device: input keys, +mouse events, drag&drop files): ```bash -scrcpy --always-on-top +scrcpy --no-control +scrcpy -n ``` +#### Turn screen off -### Show touches +It is possible to turn the device screen off while mirroring on start with a +command-line option: + +```bash +scrcpy --turn-screen-off +scrcpy -S +``` + +Or by pressing `Ctrl`+`o` at any time. + +To turn it back on, press `POWER` (or `Ctrl`+`p`). + +#### Render expired frames + +By default, to minimize latency, _scrcpy_ always renders the last decoded frame +available, and drops any previous one. + +To force the rendering of all frames (at a cost of a possible increased +latency), use: + +```bash +scrcpy --render-expired-frames +``` + +#### Show touches For presentations, it may be useful to show physical touches (on the physical device). @@ -253,7 +318,9 @@ scrcpy -t Note that it only shows _physical_ touches (with the finger on the device). -### Install APK +### File drop + +#### Install APK To install an APK, drag & drop an APK file (ending with `.apk`) to the _scrcpy_ window. @@ -261,7 +328,7 @@ window. There is no visual feedback, a log is printed to the console. -### Push file to device +#### Push file to device To push a file to `/sdcard/` on the device, drag & drop a (non-APK) file to the _scrcpy_ window. @@ -274,53 +341,9 @@ The target directory can be changed on start: scrcpy --push-target /sdcard/foo/bar/ ``` -### Read-only - -To disable controls (everything which can interact with the device: input keys, -mouse events, drag&drop files): - -```bash -scrcpy --no-control -scrcpy -n -``` - -### Turn screen off - -It is possible to turn the device screen off while mirroring on start with a -command-line option: - -```bash -scrcpy --turn-screen-off -scrcpy -S -``` - -Or by pressing `Ctrl`+`o` at any time. - -To turn it back on, press `POWER` (or `Ctrl`+`p`). -### Render expired frames - -By default, to minimize latency, _scrcpy_ always renders the last decoded frame -available, and drops any previous one. - -To force the rendering of all frames (at a cost of a possible increased -latency), use: - -```bash -scrcpy --render-expired-frames -``` - -### Custom window title - -By default, the window title is the device model. It can be changed: - -```bash -scrcpy --window-title 'My device' -``` - - -### Forward audio +### Audio forwarding Audio is not forwarded by _scrcpy_. Use [USBaudio] (Linux-only). From 704c0ff4dd6f260ad25fd1c1d7e61c4b6089ac49 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 19 Nov 2019 19:24:34 +0100 Subject: [PATCH 0118/2244] Document copy-paste and --prefer-text in README --- README.md | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b4e9a20e..776a72e0 100644 --- a/README.md +++ b/README.md @@ -318,6 +318,40 @@ scrcpy -t Note that it only shows _physical_ touches (with the finger on the device). +### Input control + +#### Copy-paste + +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`+`v` _pastes_ the computer clipboard as a sequence of text events (but + breaks non-ASCII characters). + +#### Text injection preference + +There are two kinds of [events][textevents] generated when typing text: + - _key events_, signaling that a key is pressed or released; + - _text events_, signaling that a text has been entered. + +By default, letters are injected using key events, so that the keyboard behaves +as expected in games (typically for WASD keys). + +But this may [cause issues][prefertext]. If you encounter such a problem, you +can avoid it by: + +```bash +scrcpy --prefer-text +``` + +(but this will break keyboard behavior in games) + +[textevents]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-text-input +[prefertext]: https://github.com/Genymobile/scrcpy/issues/650#issuecomment-512945343 + + ### File drop #### Install APK @@ -342,7 +376,6 @@ scrcpy --push-target /sdcard/foo/bar/ ``` - ### Audio forwarding Audio is not forwarded by _scrcpy_. Use [USBaudio] (Linux-only). From c610a6b3c7983c89c92e04bb9e7805db14221e74 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 19 Nov 2019 21:52:46 +0100 Subject: [PATCH 0119/2244] Document scrcpy via SSH tunnel in README --- README.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/README.md b/README.md index 776a72e0..f957724d 100644 --- a/README.md +++ b/README.md @@ -216,6 +216,29 @@ scrcpy -s 0123456789abcdef # short version You can start several instances of _scrcpy_ for several devices. +#### SSH tunnel + +To connect to a remote device, it is possible to connect a local `adb` client to +a remote `adb` server (provided they use the same version of the _adb_ +protocol): + +```bash +adb kill-server # kill the local adb server on 5037 +ssh -CN -L5037:localhost:5037 -R27183:localhost:27183 your_remote_computer +# keep this open +``` + +From another terminal: + +```bash +scrcpy +``` + +Like for wireless connections, it may be useful to reduce quality: + +``` +scrcpy -b2M -m800 --max-fps 15 +``` ### Window configuration From 3599fcaae545c4945d6cc480e5728a8ab815f106 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 19 Nov 2019 22:57:48 +0100 Subject: [PATCH 0120/2244] Fix help for --window-width and --window-height The default value is 0 (automatic), not -1. --- app/src/main.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main.c b/app/src/main.c index da0d2074..04be1ab0 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -127,11 +127,11 @@ static void usage(const char *arg0) { "\n" " --window-width value\n" " Set the initial window width.\n" - " Default is -1 (automatic).\n" + " Default is 0 (automatic).\n" "\n" " --window-height value\n" " Set the initial window width.\n" - " Default is -1 (automatic).\n" + " Default is 0 (automatic).\n" "\n" "Shortcuts:\n" "\n" From c2116082ab293772bb2d154707542b4bf0ccedf0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 19 Nov 2019 23:04:31 +0100 Subject: [PATCH 0121/2244] Remove deprecated options from help and manpage Ref: ff061b4f30c54dedc5073a588c6c697477b805db --- app/scrcpy.1 | 20 ++++++++++---------- app/src/main.c | 16 ++++++++-------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 948cac4d..6cb062b5 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -15,6 +15,10 @@ provides display and control of Android devices connected on USB (or over TCP/IP .SH OPTIONS +.TP +.B \-\-always\-on\-top +Make scrcpy window always on top (above other windows). + .TP .BI "\-b, \-\-bit\-rate " value Encode the video at the given bit\-rate, expressed in bits/s. Unit suffixes are supported: '\fBK\fR' (x1000) and '\fBM\fR' (x1000000). @@ -22,7 +26,7 @@ Encode the video at the given bit\-rate, expressed in bits/s. Unit suffixes are Default is 8000000. .TP -.BI "\-c, \-\-crop " width\fR:\fIheight\fR:\fIx\fR:\fIy +.BI \-\-crop " width\fR:\fIheight\fR:\fIx\fR:\fIy Crop the device screen on the server. The values are expressed in the device natural orientation (typically, portrait for a phone, landscape for a tablet). Any @@ -33,10 +37,6 @@ value is computed on the cropped size. .B \-f, \-\-fullscreen Start in fullscreen. -.TP -.BI "\-F, \-\-record\-format " format -Force recording format (either mp4 or mkv). - .TP .B \-h, \-\-help Print this help. @@ -84,9 +84,13 @@ Record screen to .IR file . The format is determined by the -.B \-F/\-\-record\-format +.B \-\-record\-format option if set, or by the file extension (.mp4 or .mkv). +.TP +.BI \-\-record\-format " format +Force recording format (either mp4 or mkv). + .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. @@ -105,10 +109,6 @@ Enable "show touches" on start, disable on quit. It only shows physical touches (not clicks from scrcpy). -.TP -.B \-T, \-\-always\-on\-top -Make scrcpy window always on top (above other windows). - .TP .B \-v, \-\-version Print the version of scrcpy. diff --git a/app/src/main.c b/app/src/main.c index 04be1ab0..8a835bf1 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -30,12 +30,15 @@ static void usage(const char *arg0) { "\n" "Options:\n" "\n" + " --always-on-top\n" + " Make scrcpy window always on top (above other windows).\n" + "\n" " -b, --bit-rate value\n" " Encode the video at the given bit-rate, expressed in bits/s.\n" " Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n" " Default is %d.\n" "\n" - " -c, --crop width:height:x:y\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" " (typically, portrait for a phone, landscape for a tablet).\n" @@ -44,9 +47,6 @@ static void usage(const char *arg0) { " -f, --fullscreen\n" " Start in fullscreen.\n" "\n" - " -F, --record-format format\n" - " Force recording format (either mp4 or mkv).\n" - "\n" " -h, --help\n" " Print this help.\n" "\n" @@ -85,9 +85,12 @@ static void usage(const char *arg0) { "\n" " -r, --record file.mp4\n" " Record screen to file.\n" - " The format is determined by the -F/--record-format option if\n" + " The format is determined by the --record-format option if\n" " set, or by the file extension (.mp4 or .mkv).\n" "\n" + " --record-format format\n" + " Force recording format (either mp4 or mkv).\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" @@ -105,9 +108,6 @@ static void usage(const char *arg0) { " Enable \"show touches\" on start, disable on quit.\n" " It only shows physical touches (not clicks from scrcpy).\n" "\n" - " -T, --always-on-top\n" - " Make scrcpy window always on top (above other windows).\n" - "\n" " -v, --version\n" " Print the version of scrcpy.\n" "\n" From cb6b300483db9af6c717b5f9e96551ea9da3853f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 19 Nov 2019 22:37:31 +0100 Subject: [PATCH 0122/2244] Upgrade FFmpeg (4.2.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 59f5a302..2b30dcb5 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.1.4-win32-shared/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.1.4-win32-shared/bin/avcodec-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.1.4-win32-shared/bin/avformat-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.1.4-win32-shared/bin/swresample-3.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.1.4-win32-shared/bin/swscale-5.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + 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/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.1.4-win64-shared/bin/avutil-56.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.1.4-win64-shared/bin/avcodec-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.1.4-win64-shared/bin/avformat-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.1.4-win64-shared/bin/swresample-3.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.1.4-win64-shared/bin/swscale-5.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + 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/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 3ad45c79..d13af0e2 100644 --- a/cross_win32.txt +++ b/cross_win32.txt @@ -15,6 +15,6 @@ cpu = 'i686' endian = 'little' [properties] -prebuilt_ffmpeg_shared = 'ffmpeg-4.1.4-win32-shared' -prebuilt_ffmpeg_dev = 'ffmpeg-4.1.4-win32-dev' +prebuilt_ffmpeg_shared = 'ffmpeg-4.2.1-win32-shared' +prebuilt_ffmpeg_dev = 'ffmpeg-4.2.1-win32-dev' prebuilt_sdl2 = 'SDL2-2.0.10/i686-w64-mingw32' diff --git a/cross_win64.txt b/cross_win64.txt index 3f222ba5..09f387e1 100644 --- a/cross_win64.txt +++ b/cross_win64.txt @@ -15,6 +15,6 @@ cpu = 'x86_64' endian = 'little' [properties] -prebuilt_ffmpeg_shared = 'ffmpeg-4.1.4-win64-shared' -prebuilt_ffmpeg_dev = 'ffmpeg-4.1.4-win64-dev' +prebuilt_ffmpeg_shared = 'ffmpeg-4.2.1-win64-shared' +prebuilt_ffmpeg_dev = 'ffmpeg-4.2.1-win64-dev' prebuilt_sdl2 = 'SDL2-2.0.10/x86_64-w64-mingw32' diff --git a/prebuilt-deps/Makefile b/prebuilt-deps/Makefile index 6cfb4100..0c857565 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.1.4-win32-shared.zip \ - 596608277f6b937c3dea7c46e854665d75b3de56790bae07f655ca331440f003 \ - ffmpeg-4.1.4-win32-shared + @./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/shared/ffmpeg-4.2.1-win32-shared.zip \ + 9208255f409410d95147151d7e829b5699bf8d91bfe1e81c3f470f47c2fa66d2 \ + ffmpeg-4.2.1-win32-shared prepare-ffmpeg-dev-win32: - @./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/dev/ffmpeg-4.1.4-win32-dev.zip \ - a80c86e263cfad26e202edfa5e6e939a2c88843ae26f031d3e0d981a39fd03fb \ - ffmpeg-4.1.4-win32-dev + @./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/dev/ffmpeg-4.2.1-win32-dev.zip \ + c3469e6c5f031cbcc8cba88dee92d6548c5c6b6ff14f4097f18f72a92d0d70c4 \ + ffmpeg-4.2.1-win32-dev prepare-ffmpeg-shared-win64: - @./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/shared/ffmpeg-4.1.4-win64-shared.zip \ - a90889871de2cab8a79b392591313a188189a353f69dde1db98aebe20b280989 \ - ffmpeg-4.1.4-win64-shared + @./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/shared/ffmpeg-4.2.1-win64-shared.zip \ + 55063d3cf750a75485c7bf196031773d81a1b25d0980c7db48ecfc7701a42331 \ + ffmpeg-4.2.1-win64-shared prepare-ffmpeg-dev-win64: - @./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/dev/ffmpeg-4.1.4-win64-dev.zip \ - 6c9d53f9e94ce1821e975ec668e5b9d6e9deb4a45d0d7e30264685d3dfbbb068 \ - ffmpeg-4.1.4-win64-dev + @./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/dev/ffmpeg-4.2.1-win64-dev.zip \ + 5af393be5f25c0a71aa29efce768e477c35347f7f8e0d9696767d5b9d405b74e \ + ffmpeg-4.2.1-win64-dev prepare-sdl2: @./prepare-dep https://libsdl.org/release/SDL2-devel-2.0.10-mingw.tar.gz \ From 4906aff4542bb83d2605d580a817ea76df32bd33 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 19 Nov 2019 22:49:08 +0100 Subject: [PATCH 0123/2244] Upgrade platform-tools (29.0.5) 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 0c857565..892af6c7 100644 --- a/prebuilt-deps/Makefile +++ b/prebuilt-deps/Makefile @@ -35,6 +35,6 @@ prepare-sdl2: SDL2-2.0.10 prepare-adb: - @./prepare-dep https://dl.google.com/android/repository/platform-tools_r29.0.2-windows.zip \ - d78f02e5e2c9c4c1d046dcd4e6fbdf586e5f57ef66eb0da5c2b49d745d85d5ee \ + @./prepare-dep https://dl.google.com/android/repository/platform-tools_r29.0.5-windows.zip \ + 2df06160056ec9a84c7334af2a1e42740befbb1a2e34370e7af544a2cc78152c \ platform-tools From 2aa65015bcf09fe3d541f00310ccc69680f89d74 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 19 Nov 2019 22:00:50 +0100 Subject: [PATCH 0124/2244] Bump version to 1.11 --- 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 57b66db6..ba19d7ee 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('scrcpy', 'c', - version: '1.10', + version: '1.11', meson_version: '>= 0.37', default_options: 'c_std=c11') diff --git a/server/build.gradle b/server/build.gradle index f1b48a28..0804a8bd 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -6,8 +6,8 @@ android { applicationId "com.genymobile.scrcpy" minSdkVersion 21 targetSdkVersion 29 - versionCode 11 - versionName "1.10" + versionCode 12 + versionName "1.11" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index b4605aa9..fcd6233e 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.10 +SCRCPY_VERSION_NAME=1.11 PLATFORM=${ANDROID_PLATFORM:-29} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-29.0.2} From 40c3c5761397a4e6f89c69729bf524455e07e7f4 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 19 Nov 2019 23:39:30 +0100 Subject: [PATCH 0125/2244] Update links to v1.11 in README and BUILD --- BUILD.md | 6 +++--- README.md | 14 +++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/BUILD.md b/BUILD.md index d03af618..8801e5fc 100644 --- a/BUILD.md +++ b/BUILD.md @@ -233,10 +233,10 @@ You can then [run](README.md#run) _scrcpy_. ## Prebuilt server - - [`scrcpy-server-v1.10.jar`][direct-scrcpy-server] - _(SHA-256: cbeb1a4e046f1392c1dc73c3ccffd7f86dec4636b505556ea20929687a119390)_ + - [`scrcpy-server-v1.11`][direct-scrcpy-server] + _(SHA-256: ff3a454012e91d9185cfe8ca7691cea16c43a7dcc08e92fa47ab9f0ea675abd1)_ -[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.10/scrcpy-server-v1.10.jar +[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.11/scrcpy-server-v1.11 Download the prebuilt server somewhere, and specify its path during the Meson configuration: diff --git a/README.md b/README.md index f957724d..677e7a1c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# scrcpy (v1.10) +# scrcpy (v1.11) 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. @@ -62,13 +62,13 @@ For Gentoo, an [Ebuild] is available: [`scrcpy/`][ebuild-link]. For Windows, for simplicity, prebuilt archives with all the dependencies (including `adb`) are available: - - [`scrcpy-win32-v1.10.zip`][direct-win32] - _(SHA-256: f98b400b3764404b33b212e9762dd6f1593ddb766c1480fc2609c94768e4a8e1)_ - - [`scrcpy-win64-v1.10.zip`][direct-win64] - _(SHA-256: 95de34575d873c7e95dfcfb5e74d0f6af4f70b2a5bc6fde0f48d1a05480e3a44)_ + - [`scrcpy-win32-v1.11.zip`][direct-win32] + _(SHA-256: f25ed46e6f3e81e0ff9b9b4df7fe1a4bbd13f8396b7391be0a488b64c675b41e)_ + - [`scrcpy-win64-v1.11.zip`][direct-win64] + _(SHA-256: 3802c9ea0307d437947ff150ec65e53990b0beaacd0c8d0bed19c7650ce141bd)_ -[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v1.10/scrcpy-win32-v1.10.zip -[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.10/scrcpy-win64-v1.10.zip +[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v1.11/scrcpy-win32-v1.11.zip +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.11/scrcpy-win64-v1.11.zip You can also [build the app manually][BUILD]. From 7d7f3daff2244e7f22ab80dd2985917d795e9c1f Mon Sep 17 00:00:00 2001 From: yangfl Date: Wed, 20 Nov 2019 17:13:22 +0100 Subject: [PATCH 0126/2244] Fix aidl option in build_without_gradle.sh Debian's aidl complains about the missing path for -o option. Signed-off-by: Romain Vimont --- server/build_without_gradle.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index fcd6233e..b1fdabdd 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -40,7 +40,7 @@ EOF echo "Generating java from aidl..." cd "$SERVER_DIR/src/main/aidl" -"$ANDROID_HOME/build-tools/$BUILD_TOOLS/aidl" -o "$CLASSES_DIR" \ +"$ANDROID_HOME/build-tools/$BUILD_TOOLS/aidl" -o"$CLASSES_DIR" \ android/view/IRotationWatcher.aidl echo "Compiling java sources..." From c9d886f38bb6e3eaa1194a8bcfeda3bc90be4f85 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 22 Nov 2019 15:16:00 +0100 Subject: [PATCH 0127/2244] Use the existing constants for device server path --- 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 b37b39d0..70fa18ca 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -124,7 +124,7 @@ execute_server(struct server *server, const struct server_params *params) { sprintf(max_fps_string, "%"PRIu16, params->max_fps); const char *const cmd[] = { "shell", - "CLASSPATH=/data/local/tmp/" SERVER_FILENAME, + "CLASSPATH=" DEVICE_SERVER_PATH, "app_process", #ifdef SERVER_DEBUGGER # define SERVER_DEBUGGER_PORT "5005" From 83ace842809034f8076296a338c017e49519f068 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 22 Nov 2019 15:23:57 +0100 Subject: [PATCH 0128/2244] Restore the .jar extension on the device side Commit 3da95b52bd21ae0f4d81ee933caea63b78191deb renamed 'scrcpy-server.jar' to 'scrcpy-server' to avoid issues on the client side. However, removing the extension may cause issues with app_process, so restore the extension only on the device side. Fixes --- app/src/server.c | 2 +- server/src/main/java/com/genymobile/scrcpy/Server.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/server.c b/app/src/server.c index 70fa18ca..6061b8b3 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -16,7 +16,7 @@ #define SERVER_FILENAME "scrcpy-server" #define DEFAULT_SERVER_PATH PREFIX "/share/scrcpy/" SERVER_FILENAME -#define DEVICE_SERVER_PATH "/data/local/tmp/" SERVER_FILENAME +#define DEVICE_SERVER_PATH "/data/local/tmp/scrcpy-server.jar" static const char * get_server_path(void) { diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index ad14e5d8..26ef2850 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -7,7 +7,7 @@ import java.io.IOException; public final class Server { - private static final String SERVER_PATH = "/data/local/tmp/scrcpy-server"; + private static final String SERVER_PATH = "/data/local/tmp/scrcpy-server.jar"; private Server() { // not instantiable From 8ec077ce1b5ae3c8a5993e062668220e7c684c30 Mon Sep 17 00:00:00 2001 From: seoyeonK <50603274+seoyeonK@users.noreply.github.com> Date: Sat, 16 Nov 2019 16:19:59 +0900 Subject: [PATCH 0129/2244] Add Korean translation for README and FAQ PR Signed-off-by: Romain Vimont --- FAQ.ko.md | 84 +++++++++ README.ko.md | 498 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 582 insertions(+) create mode 100644 FAQ.ko.md create mode 100644 README.ko.md diff --git a/FAQ.ko.md b/FAQ.ko.md new file mode 100644 index 00000000..6cc1a1d9 --- /dev/null +++ b/FAQ.ko.md @@ -0,0 +1,84 @@ +# 자주하는 질문 (FAQ) + +다음은 자주 제보되는 문제들과 그들의 현황입니다. + + +### Window 운영체제에서, 디바이스가 발견되지 않습니다. + +가장 흔한 제보는 `adb`에 발견되지 않는 디바이스 혹은 권한 관련 문제입니다. +다음 명령어를 호출하여 모든 것들에 이상이 없는지 확인하세요: + + adb devices + +Window는 당신의 디바이스를 감지하기 위해 [drivers]가 필요할 수도 있습니다. + +[drivers]: https://developer.android.com/studio/run/oem-usb.html + + +### 내 디바이스의 미러링만 가능하고, 디바이스와 상호작용을 할 수 없습니다. + +일부 디바이스에서는, [simulating input]을 허용하기 위해서 한가지 옵션을 활성화해야 할 수도 있습니다. +개발자 옵션에서 (developer options) 다음을 활성화 하세요: + +> **USB debugging (Security settings)** +> _권한 부여와 USB 디버깅을 통한 simulating input을 허용한다_ + +[simulating input]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323 + + +### 마우스 클릭이 다른 곳에 적용됩니다. + +Mac 운영체제에서, HiDPI support 와 여러 스크린 창이 있는 경우, 입력 위치가 잘못 파악될 수 있습니다. +[issue 15]를 참고하세요. + +[issue 15]: https://github.com/Genymobile/scrcpy/issues/15 + +차선책은 HiDPI support을 비활성화 하고 build하는 방법입니다: + +```bash +meson x --buildtype release -Dhidpi_support=false +``` + +하지만, 동영상은 낮은 해상도로 재생될 것 입니다. + + +### HiDPI display의 화질이 낮습니다. + +Windows에서는, [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 + + +### KWin compositor가 실행되지 않습니다 + +Plasma Desktop에서는,_scrcpy_ 가 실행중에는 compositor가 비활성화 됩니다. + +차석책으로는, ["Block compositing"를 비활성화하세요][kwin]. + +[kwin]: https://github.com/Genymobile/scrcpy/issues/114#issuecomment-378778613 + + +###비디오 스트림을 열 수 없는 에러가 발생합니다.(Could not open video stream). + +여러가지 원인이 있을 수 있습니다. 가장 흔한 원인은 디바이스의 하드웨어 인코더(hardware encoder)가 +주어진 해상도를 인코딩할 수 없는 경우입니다. + +``` +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 +``` + +더 낮은 해상도로 시도 해보세요: + +``` +scrcpy -m 1920 +scrcpy -m 1024 +scrcpy -m 800 +``` diff --git a/README.ko.md b/README.ko.md new file mode 100644 index 00000000..564acae7 --- /dev/null +++ b/README.ko.md @@ -0,0 +1,498 @@ +# scrcpy (v1.11) + +This document will be updated frequently along with the original Readme file +이 문서는 원어 리드미 파일의 업데이트에 따라 종종 업데이트 될 것입니다 + + 이 어플리케이션은 UBS ( 혹은 [TCP/IP][article-tcpip] ) 로 연결된 Android 디바이스를 화면에 보여주고 관리하는 것을 제공합니다. + _GNU/Linux_, _Windows_ 와 _macOS_ 상에서 작동합니다. + (아래 설명에서 디바이스는 안드로이드 핸드폰을 의미합니다.) + +[article-tcpip]:https://www.genymotion.com/blog/open-source-project-scrcpy-now-works-wirelessly/ + +![screenshot](https://github.com/Genymobile/scrcpy/blob/master/assets/screenshot-debian-600.jpg?raw=true) + +주요 기능은 다음과 같습니다. + + - **가벼움** (기본적이며 디바이스의 화면만을 보여줌) + - **뛰어난 성능** (30~60fps) + - **높은 품질** (1920×1080 이상의 해상도) + - **빠른 반응 속도** ([35~70ms][lowlatency]) + - **짧은 부팅 시간** (첫 사진을 보여주는데 최대 1초 소요됨) + - **장치 설치와는 무관함** (디바이스에 설치하지 않아도 됨) + +[lowlatency]: https://github.com/Genymobile/scrcpy/pull/646 + + +## 요구사항 + +안드로이드 장치는 최소 API 21 (Android 5.0) 을 필요로 합니다. + +디바이스에 [adb debugging][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 (리눅스) + +리눅스 상에서는 보통 [어플을 직접 설치][BUILD] 해야합니다. 어렵지 않으므로 걱정하지 않아도 됩니다. + +[BUILD]:https://github.com/Genymobile/scrcpy/blob/master/BUILD.md + +[Snap] 패키지가 가능합니다 : [`scrcpy`][snap-link]. + +[snap-link]: https://snapstats.org/snaps/scrcpy + +[snap]: https://en.wikipedia.org/wiki/Snappy_(package_manager) + +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 (윈도우) + +윈도우 상에서, 간단하게 설치하기 위해 종속성이 있는 사전 구축된 아카이브가 제공됩니다 (`adb` 포함) : +해당 파일은 Readme원본 링크를 통해서 다운로드가 가능합니다. + - [`scrcpy-win`][direct-win] + +[direct-win]: https://github.com/Genymobile/scrcpy/blob/master/README.md#windows + + +[어플을 직접 설치][BUILD] 할 수도 있습니다. + + +### macOS (맥 OS) + +이 어플리케이션은 아래 사항을 따라 설치한다면 [Homebrew] 에서도 사용 가능합니다 : + +[Homebrew]: https://brew.sh/ + +```bash +brew install scrcpy +``` + +`PATH` 로부터 접근 가능한 `adb` 가 필요합니다. 아직 설치하지 않았다면 다음을 따라 설치해야 합니다 : + +```bash +brew cask install android-platform-tools +``` + +[어플을 직접 설치][BUILD] 할 수도 있습니다. + + +## 실행 + +안드로이드 디바이스를 연결하고 실행하십시오: + +```bash +scrcpy +``` + +다음과 같이 명령창 옵션 기능도 제공합니다. + +```bash +scrcpy --help +``` + +## 기능 + +### 캡쳐 환경 설정 + + +###사이즈 재정의 + +가끔씩 성능을 향상시키기위해 안드로이드 디바이스를 낮은 해상도에서 미러링하는 것이 유용할 때도 있습니다. + +너비와 높이를 제한하기 위해 특정 값으로 지정할 수 있습니다 (e.g. 1024) : + +```bash +scrcpy --max-size 1024 +scrcpy -m 1024 # 축약 버전 +``` + +이 외의 크기도 디바이스의 가로 세로 비율이 유지된 상태에서 계산됩니다. +이러한 방식으로 디바이스 상에서 1920×1080 는 모니터 상에서1024×576로 미러링될 것 입니다. + + +### bit-rate 변경 + +기본 bit-rate 는 8 Mbps입니다. 비디오 bit-rate 를 변경하기 위해선 다음과 같이 입력하십시오 (e.g. 2 Mbps로 변경): + +```bash +scrcpy --bit-rate 2M +scrcpy -b 2M # 축약 버전 +``` + +###프레임 비율 제한 + +안드로이드 버전 10이상의 디바이스에서는, 다음의 명령어로 캡쳐 화면의 프레임 비율을 제한할 수 있습니다: + +```bash +scrcpy --max-fps 15 +``` + + +### Crop (잘라내기) + +디바이스 화면은 화면의 일부만 미러링하기 위해 잘라질 것입니다. + +예를 들어, *Oculus Go* 의 한 쪽 눈만 미러링할 때 유용합니다 : + +```bash +scrcpy --crop 1224:1440:0:0 # 1224x1440 at offset (0,0) +scrcpy -c 1224:1440:0:0 # 축약 버전 +``` + +만약 `--max-size` 도 지정하는 경우, 잘라낸 다음에 재정의된 크기가 적용될 것입니다. + + +### 화면 녹화 + +미러링하는 동안 화면 녹화를 할 수 있습니다 : + +```bash +scrcpy --record file.mp4 +scrcpy -r file.mkv +``` + +녹화하는 동안 미러링을 멈출 수 있습니다 : + +```bash +scrcpy --no-display --record file.mp4 +scrcpy -Nr file.mkv +# Ctrl+C 로 녹화를 중단할 수 있습니다. +# 윈도우 상에서 Ctrl+C 는 정상정으로 종료되지 않을 수 있으므로, 디바이스 연결을 해제하십시오. +``` + +"skipped frames" 은 모니터 화면에 보여지지 않았지만 녹화되었습니다 ( 성능 문제로 인해 ). 프레임은 디바이스 상에서 _타임 스탬프 ( 어느 시점에 데이터가 존재했다는 사실을 증명하기 위해 특정 위치에 시각을 표시 )_ 되었으므로, [packet delay +variation] 은 녹화된 파일에 영향을 끼치지 않습니다. + +[packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation + +## 연결 + +### 무선연결 + +_Scrcpy_ 장치와 정보를 주고받기 위해 `adb` 를 사용합니다. `adb` 는 TCIP/IP 를 통해 디바이스와 [연결][connect] 할 수 있습니다 : + +1. 컴퓨터와 디바이스를 동일한 Wi-Fi 에 연결합니다. +2. 디바이스의 IP address 를 확인합니다 (설정 → 내 기기 → 상태 / 혹은 인터넷에 '내 IP'검색 시 확인 가능합니다. ). +3. TCP/IP 를 통해 디바이스에서 adb 를 사용할 수 있게 합니다: `adb tcpip 5555`. +4. 디바이스 연결을 해제합니다. +5. adb 를 통해 디바이스에 연결을 합니다\: `adb connect DEVICE_IP:5555` _(`DEVICE_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 # 축약 버전 +``` + +_scrcpy_ 로 여러 디바이스를 연결해 사용할 수 있습니다. + + +#### SSH tunnel + +떨어져 있는 디바이스와 연결하기 위해서는, 로컬 `adb` client와 떨어져 있는 `adb` 서버를 연결해야 합니다. (디바이스와 클라이언트가 동일한 버전의 _adb_ protocol을 사용할 경우에 제공됩니다.): + +```bash +adb kill-server # 5037의 로컬 local adb server를 중단 +ssh -CN -L5037:localhost:5037 -R27183:localhost:27183 your_remote_computer +# 실행 유지 +``` + +다른 터미널에서는 : + +```bash +scrcpy +``` + +무선 연결과 동일하게, 화질을 줄이는 것이 나을 수 있습니다: + +``` +scrcpy -b2M -m800 --max-fps 15 +``` + +## Window에서의 배치 + +### 맞춤형 window 제목 + +기본적으로, window의 이름은 디바이스의 모델명 입니다. +다음의 명령어를 통해 변경하세요. + +```bash +scrcpy --window-title 'My device' +``` + + +### 배치와 크기 + +초기 window창의 배치와 크기는 다음과 같이 설정할 수 있습니다: + +```bash +scrcpy --window-x 100 --window-y 100 --window-width 800 --window-height 600 +``` + + +### 경계 없애기 + +윈도우 장식(경계선 등)을 다음과 같이 제거할 수 있습니다: + +```bash +scrcpy --window-borderless +``` + +### 항상 모든 윈도우 위에 실행창 고정 + +이 어플리케이션의 윈도우 창은 다음의 명령어로 다른 window 위에 디스플레이 할 수 있습니다: + +```bash +scrcpy --always-on-top +scrcpy -T # 축약 버전 +``` + +### 전체 화면 + +이 어플리케이션은 전체화면으로 바로 시작할 수 있습니다. + +```bash +scrcpy --fullscreen +scrcpy -f # short version +``` + +전체 화면은 `Ctrl`+`f`키로 끄거나 켤 수 있습니다. + + +## 다른 미러링 옵션 + +### 읽기 전용(Read-only) + +권한을 제한하기 위해서는 (디바이스와 관련된 모든 것: 입력 키, 마우스 이벤트 , 파일의 드래그 앤 드랍(drag&drop)): + +```bash +scrcpy --no-control +scrcpy -n +``` + +### 화면 끄기 + +미러링을 실행하는 와중에 디바이스의 화면을 끌 수 있게 하기 위해서는 +다음의 커맨드 라인 옵션을(command line option) 입력하세요: + +```bash +scrcpy --turn-screen-off +scrcpy -S +``` + +혹은 `Ctrl`+`o`을 눌러 언제든지 디바이스의 화면을 끌 수 있습니다. + +다시 화면을 켜기 위해서는`POWER` (혹은 `Ctrl`+`p`)를 누르세요. + + +### 유효기간이 지난 프레임 제공 (Render expired frames) + +디폴트로, 대기시간을 최소화하기 위해 _scrcpy_ 는 항상 마지막으로 디코딩된 프레임을 제공합니다 +과거의 프레임은 하나씩 삭제합니다. + +모든 프레임을 강제로 렌더링하기 위해서는 (대기 시간이 증가될 수 있습니다) +다음의 명령어를 사용하세요: + +```bash +scrcpy --render-expired-frames +``` + + +### 화면에 터치 나타내기 + +발표를 할 때, 물리적인 기기에 한 물리적 터치를 나타내는 것이 유용할 수 있습니다. + +안드로이드 운영체제는 이런 기능을 _Developers options_에서 제공합니다. + +_Scrcpy_ 는 이런 기능을 시작할 때와 종료할 때 옵션으로 제공합니다. + +```bash +scrcpy --show-touches +scrcpy -t +``` + +화면에 _물리적인 터치만_ 나타나는 것에 유의하세요 (손가락을 디바이스에 대는 행위). + + +### 입력 제어 + +#### 복사-붙여넣기 + +컴퓨터와 디바이스 양방향으로 클립보드를 복사하는 것이 가능합니다: + + - `Ctrl`+`c` 디바이스의 클립보드를 컴퓨터로 복사합니다; + - `Ctrl`+`Shift`+`v` 컴퓨터의 클립보드를 디바이스로 복사합니다; + - `Ctrl`+`v` 컴퓨터의 클립보드를 text event 로써 _붙여넣습니다_ ( 그러나, ASCII 코드가 아닌 경우 실행되지 않습니다 ) + +#### 텍스트 삽입 우선 순위 + +텍스트를 입력할 때 생성되는 두 가지의 [events][textevents] 가 있습니다: + - _key events_, 키가 눌려있는 지에 대한 신호; + - _text events_, 텍스트가 입력되었는지에 대한 신호. + +기본적으로, 글자들은 key event 를 이용해 입력되기 때문에, 키보드는 게임에서처럼 처리합니다 ( 보통 WASD 키에 대해서 ). + +그러나 이는 [issues 를 발생][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 + + +### 파일 드랍 + +### APK 실행하기 + +APK를 실행하기 위해서는, APK file(파일명이`.apk`로 끝나는 파일)을 드래그하고 _scrcpy_ window에 드랍하세요 (drag and drop) + +시각적인 피드백은 없고,log 하나가 콘솔에 출력될 것입니다. + +### 디바이스에 파일 push하기 + +디바이스의`/sdcard/`에 파일을 push하기 위해서는, +APK파일이 아닌 파일을_scrcpy_ window에 드래그하고 드랍하세요.(drag and drop). + +시각적인 피드백은 없고,log 하나가 콘솔에 출력될 것입니다. + +해당 디렉토리는 시작할 때 변경이 가능합니다: + +```bash +scrcpy --push-target /sdcard/foo/bar/ +``` + +### 오디오의 전달 + +_scrcpy_는 오디오를 직접 전달해주지 않습니다. [USBaudio] (Linux-only)를 사용하세요. + +추가적으로 [issue #14]를 참고하세요. + +[USBaudio]: https://github.com/rom1v/usbaudio +[issue #14]: https://github.com/Genymobile/scrcpy/issues/14 + +## 단축키 + + | 실행내용 | 단축키 | 단축키 (macOS) + | -------------------------------------- |:----------------------------- |:----------------------------- + | 전체화면 모드로 전환 | `Ctrl`+`f` | `Cmd`+`f` + | window를 1:1비율로 전환하기(픽셀 맞춤) | `Ctrl`+`g` | `Cmd`+`g` + | 검은 공백 제거 위한 window 크기 조정 | `Ctrl`+`x` \| _Double-click¹_ | `Cmd`+`x` \| _Double-click¹_ + |`HOME` 클릭 | `Ctrl`+`h` \| _Middle-click_ | `Ctrl`+`h` \| _Middle-click_ + | `BACK` 클릭 | `Ctrl`+`b` \| _Right-click²_ | `Cmd`+`b` \| _Right-click²_ + | `APP_SWITCH` 클릭 | `Ctrl`+`s` | `Cmd`+`s` + | `MENU` 클릭 | `Ctrl`+`m` | `Ctrl`+`m` + | `VOLUME_UP` 클릭 | `Ctrl`+`↑` _(up)_ | `Cmd`+`↑` _(up)_ + | `VOLUME_DOWN` 클릭 | `Ctrl`+`↓` _(down)_ | `Cmd`+`↓` _(down)_ + | `POWER` 클릭 | `Ctrl`+`p` | `Cmd`+`p` + | 전원 켜기 | _Right-click²_ | _Right-click²_ + | 미러링 중 디바이스 화면 끄기 | `Ctrl`+`o` | `Cmd`+`o` + | 알림 패널 늘리기 | `Ctrl`+`n` | `Cmd`+`n` + | 알림 패널 닫기 | `Ctrl`+`Shift`+`n` | `Cmd`+`Shift`+`n` + | 디바이스의 clipboard 컴퓨터로 복사하기 | `Ctrl`+`c` | `Cmd`+`c` + | 컴퓨터의 clipboard 디바이스에 붙여넣기 | `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` + +_¹검은 공백을 제거하기 위해서는 그 부분을 더블 클릭하세요_ +_²화면이 꺼진 상태에서 우클릭 시 다시 켜지며, 그 외의 상태에서는 뒤로 돌아갑니다. + +## 맞춤 경로 (custom path) + +특정한 _adb_ binary를 사용하기 위해서는, 그것의 경로를 환경변수로 설정하세요. +`ADB`: + + ADB=/path/to/adb scrcpy + +`scrcpy-server.jar`파일의 경로에 오버라이드 하기 위해서는, 그것의 경로를 `SCRCPY_SERVER_PATH`에 저장하세요. + +[useful]: https://github.com/Genymobile/scrcpy/issues/278#issuecomment-429330345 + + +## _scrcpy_ 인 이유? + +한 동료가 [gnirehtet]와 같이 발음하기 어려운 이름을 찾을 수 있는지 도발했습니다. + +[`strcpy`] 는 **str**ing을 copy하고; `scrcpy`는 **scr**een을 copy합니다. + +[gnirehtet]: https://github.com/Genymobile/gnirehtet +[`strcpy`]: http://man7.org/linux/man-pages/man3/strcpy.3.html + + + +## 빌드하는 방법? + +[BUILD]을 참고하세요. + +[BUILD]: BUILD.md + +## 흔한 issue + +[FAQ](FAQ.md)을 참고하세요. + + +## 개발자들 + +[developers page]를 참고하세요. + +[developers page]: DEVELOP.md + + +## 라이선스 + + Copyright (C) 2018 Genymobile + Copyright (C) 2018-2019 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. + +## 관련 글 (articles) + +- [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 dfd0707a2925e9569aaa2ae6b88b161a12376a49 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 24 Nov 2019 11:53:00 +0100 Subject: [PATCH 0130/2244] Move utilities to util/ --- app/meson.build | 10 +++++----- app/src/command.c | 4 ++-- app/src/control_msg.c | 6 +++--- app/src/controller.c | 4 ++-- app/src/controller.h | 4 ++-- app/src/decoder.c | 6 +++--- app/src/device.c | 2 +- app/src/device.h | 2 +- app/src/device_msg.c | 4 ++-- app/src/file_handler.c | 4 ++-- app/src/file_handler.h | 2 +- app/src/fps_counter.c | 4 ++-- app/src/input_manager.c | 4 ++-- app/src/main.c | 2 +- app/src/receiver.c | 4 ++-- app/src/receiver.h | 2 +- app/src/recorder.c | 4 ++-- app/src/recorder.h | 2 +- app/src/scrcpy.c | 6 +++--- app/src/screen.c | 4 ++-- app/src/server.c | 4 ++-- app/src/server.h | 2 +- app/src/stream.c | 6 +++--- app/src/stream.h | 2 +- app/src/sys/unix/command.c | 3 ++- app/src/sys/unix/net.c | 2 +- app/src/tiny_xpm.c | 2 +- app/src/{ => util}/buffer_util.h | 0 app/src/{ => util}/cbuf.h | 0 app/src/{lock_util.h => util/lock.h} | 4 ++-- app/src/{ => util}/log.h | 0 app/src/{ => util}/net.c | 0 app/src/{ => util}/net.h | 0 app/src/{ => util}/queue.h | 0 app/src/{ => util}/str_util.c | 0 app/src/{ => util}/str_util.h | 0 app/src/video_buffer.c | 4 ++-- app/tests/test_cbuf.c | 2 +- app/tests/test_queue.c | 2 +- app/tests/test_strutil.c | 2 +- 40 files changed, 58 insertions(+), 57 deletions(-) rename app/src/{ => util}/buffer_util.h (100%) rename app/src/{ => util}/cbuf.h (100%) rename app/src/{lock_util.h => util/lock.h} (96%) rename app/src/{ => util}/log.h (100%) rename app/src/{ => util}/net.c (100%) rename app/src/{ => util}/net.h (100%) rename app/src/{ => util}/queue.h (100%) rename app/src/{ => util}/str_util.c (100%) rename app/src/{ => util}/str_util.h (100%) diff --git a/app/meson.build b/app/meson.build index 159ae695..005239b8 100644 --- a/app/meson.build +++ b/app/meson.build @@ -10,16 +10,16 @@ src = [ 'src/file_handler.c', 'src/fps_counter.c', 'src/input_manager.c', - 'src/net.c', 'src/receiver.c', 'src/recorder.c', 'src/scrcpy.c', 'src/screen.c', 'src/server.c', - 'src/str_util.c', - 'src/tiny_xpm.c', 'src/stream.c', + 'src/tiny_xpm.c', 'src/video_buffer.c', + 'src/util/net.c', + 'src/util/str_util.c' ] if not get_option('crossbuild_windows') @@ -147,7 +147,7 @@ tests = [ ['test_control_event_serialize', [ 'tests/test_control_msg_serialize.c', 'src/control_msg.c', - 'src/str_util.c' + 'src/util/str_util.c' ]], ['test_device_event_deserialize', [ 'tests/test_device_msg_deserialize.c', @@ -158,7 +158,7 @@ tests = [ ]], ['test_strutil', [ 'tests/test_strutil.c', - 'src/str_util.c' + 'src/util/str_util.c' ]], ] diff --git a/app/src/command.c b/app/src/command.c index d914e6ab..33a9c587 100644 --- a/app/src/command.c +++ b/app/src/command.c @@ -7,8 +7,8 @@ #include "config.h" #include "common.h" -#include "log.h" -#include "str_util.h" +#include "util/log.h" +#include "util/str_util.h" static const char *adb_command; diff --git a/app/src/control_msg.c b/app/src/control_msg.c index e042dc5a..363483eb 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -4,9 +4,9 @@ #include #include "config.h" -#include "buffer_util.h" -#include "log.h" -#include "str_util.h" +#include "util/buffer_util.h" +#include "util/log.h" +#include "util/str_util.h" static void write_position(uint8_t *buf, const struct position *position) { diff --git a/app/src/controller.c b/app/src/controller.c index 7f90d787..ec706c95 100644 --- a/app/src/controller.c +++ b/app/src/controller.c @@ -3,8 +3,8 @@ #include #include "config.h" -#include "lock_util.h" -#include "log.h" +#include "util/lock.h" +#include "util/log.h" bool controller_init(struct controller *controller, socket_t control_socket) { diff --git a/app/src/controller.h b/app/src/controller.h index 1b0d005b..8011ef6a 100644 --- a/app/src/controller.h +++ b/app/src/controller.h @@ -6,10 +6,10 @@ #include #include "config.h" -#include "cbuf.h" #include "control_msg.h" -#include "net.h" #include "receiver.h" +#include "util/cbuf.h" +#include "util/net.h" struct control_msg_queue CBUF(struct control_msg, 64); diff --git a/app/src/decoder.c b/app/src/decoder.c index cad19913..52176484 100644 --- a/app/src/decoder.c +++ b/app/src/decoder.c @@ -10,12 +10,12 @@ #include "config.h" #include "compat.h" -#include "buffer_util.h" #include "events.h" -#include "lock_util.h" -#include "log.h" #include "recorder.h" #include "video_buffer.h" +#include "util/buffer_util.h" +#include "util/lock.h" +#include "util/log.h" // set the decoded frame as ready for rendering, and notify static void diff --git a/app/src/device.c b/app/src/device.c index 4f50ab48..f4c2628b 100644 --- a/app/src/device.c +++ b/app/src/device.c @@ -1,7 +1,7 @@ #include "device.h" #include "config.h" -#include "log.h" +#include "util/log.h" bool device_read_info(socket_t device_socket, char *device_name, struct size *size) { diff --git a/app/src/device.h b/app/src/device.h index 34a5f17f..8a94cd86 100644 --- a/app/src/device.h +++ b/app/src/device.h @@ -5,7 +5,7 @@ #include "config.h" #include "common.h" -#include "net.h" +#include "util/net.h" #define DEVICE_NAME_FIELD_LENGTH 64 diff --git a/app/src/device_msg.c b/app/src/device_msg.c index 2fc90ae4..aba56bb3 100644 --- a/app/src/device_msg.c +++ b/app/src/device_msg.c @@ -4,8 +4,8 @@ #include #include "config.h" -#include "buffer_util.h" -#include "log.h" +#include "util/buffer_util.h" +#include "util/log.h" ssize_t device_msg_deserialize(const unsigned char *buf, size_t len, diff --git a/app/src/file_handler.c b/app/src/file_handler.c index e02ca2a9..c0b03bcf 100644 --- a/app/src/file_handler.c +++ b/app/src/file_handler.c @@ -5,8 +5,8 @@ #include "config.h" #include "command.h" -#include "lock_util.h" -#include "log.h" +#include "util/lock.h" +#include "util/log.h" #define DEFAULT_PUSH_TARGET "/sdcard/" diff --git a/app/src/file_handler.h b/app/src/file_handler.h index 4c158296..078d0ca5 100644 --- a/app/src/file_handler.h +++ b/app/src/file_handler.h @@ -6,8 +6,8 @@ #include #include "config.h" -#include "cbuf.h" #include "command.h" +#include "util/cbuf.h" typedef enum { ACTION_INSTALL_APK, diff --git a/app/src/fps_counter.c b/app/src/fps_counter.c index 2a9478f6..0c3f13d4 100644 --- a/app/src/fps_counter.c +++ b/app/src/fps_counter.c @@ -4,8 +4,8 @@ #include #include "config.h" -#include "lock_util.h" -#include "log.h" +#include "util/lock.h" +#include "util/log.h" #define FPS_COUNTER_INTERVAL_MS 1000 diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 7d333c1b..e0aa6054 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -4,8 +4,8 @@ #include "config.h" #include "event_converter.h" -#include "lock_util.h" -#include "log.h" +#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) diff --git a/app/src/main.c b/app/src/main.c index 8a835bf1..c17cc6bf 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -10,8 +10,8 @@ #include "config.h" #include "compat.h" -#include "log.h" #include "recorder.h" +#include "util/log.h" struct args { struct scrcpy_options opts; diff --git a/app/src/receiver.c b/app/src/receiver.c index 1c80bb00..23944d5a 100644 --- a/app/src/receiver.c +++ b/app/src/receiver.c @@ -5,8 +5,8 @@ #include "config.h" #include "device_msg.h" -#include "lock_util.h" -#include "log.h" +#include "util/lock.h" +#include "util/log.h" bool receiver_init(struct receiver *receiver, socket_t control_socket) { diff --git a/app/src/receiver.h b/app/src/receiver.h index 6108e545..8387903b 100644 --- a/app/src/receiver.h +++ b/app/src/receiver.h @@ -6,7 +6,7 @@ #include #include "config.h" -#include "net.h" +#include "util/net.h" // receive events from the device // managed by the controller diff --git a/app/src/recorder.c b/app/src/recorder.c index f6f6fd96..b5314daf 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -5,8 +5,8 @@ #include "config.h" #include "compat.h" -#include "lock_util.h" -#include "log.h" +#include "util/lock.h" +#include "util/log.h" static const AVRational SCRCPY_TIME_BASE = {1, 1000000}; // timestamps in us diff --git a/app/src/recorder.h b/app/src/recorder.h index 4ad77197..4f5d526c 100644 --- a/app/src/recorder.h +++ b/app/src/recorder.h @@ -8,7 +8,7 @@ #include "config.h" #include "common.h" -#include "queue.h" +#include "util/queue.h" enum recorder_format { RECORDER_FORMAT_AUTO, diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 67f1de16..673ec678 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -18,15 +18,15 @@ #include "file_handler.h" #include "fps_counter.h" #include "input_manager.h" -#include "log.h" -#include "lock_util.h" -#include "net.h" #include "recorder.h" #include "screen.h" #include "server.h" #include "stream.h" #include "tiny_xpm.h" #include "video_buffer.h" +#include "util/lock.h" +#include "util/log.h" +#include "util/net.h" static struct server server = SERVER_INITIALIZER; static struct screen screen = SCREEN_INITIALIZER; diff --git a/app/src/screen.c b/app/src/screen.c index ab4d434e..4cc7cde3 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -7,10 +7,10 @@ #include "common.h" #include "compat.h" #include "icon.xpm" -#include "lock_util.h" -#include "log.h" #include "tiny_xpm.h" #include "video_buffer.h" +#include "util/lock.h" +#include "util/log.h" #define DISPLAY_MARGINS 96 diff --git a/app/src/server.c b/app/src/server.c index 6061b8b3..d9114356 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -9,8 +9,8 @@ #include "config.h" #include "command.h" -#include "log.h" -#include "net.h" +#include "util/log.h" +#include "util/net.h" #define SOCKET_NAME "scrcpy" #define SERVER_FILENAME "scrcpy-server" diff --git a/app/src/server.h b/app/src/server.h index f46ced19..0cb1ab3a 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -6,7 +6,7 @@ #include "config.h" #include "command.h" -#include "net.h" +#include "util/net.h" struct server { char *serial; diff --git a/app/src/stream.c b/app/src/stream.c index 6c3192f2..c84de981 100644 --- a/app/src/stream.c +++ b/app/src/stream.c @@ -10,12 +10,12 @@ #include "config.h" #include "compat.h" -#include "buffer_util.h" #include "decoder.h" #include "events.h" -#include "lock_util.h" -#include "log.h" #include "recorder.h" +#include "util/buffer_util.h" +#include "util/lock.h" +#include "util/log.h" #define BUFSIZE 0x10000 diff --git a/app/src/stream.h b/app/src/stream.h index cb50468e..f7c5e475 100644 --- a/app/src/stream.h +++ b/app/src/stream.h @@ -8,7 +8,7 @@ #include #include "config.h" -#include "net.h" +#include "util/net.h" struct video_buffer; diff --git a/app/src/sys/unix/command.c b/app/src/sys/unix/command.c index 6a3a5a47..512b9af7 100644 --- a/app/src/sys/unix/command.c +++ b/app/src/sys/unix/command.c @@ -17,7 +17,8 @@ #include #include #include -#include "log.h" + +#include "util/log.h" enum process_result cmd_execute(const char *path, const char *const argv[], pid_t *pid) { diff --git a/app/src/sys/unix/net.c b/app/src/sys/unix/net.c index d940f3bb..d67a660f 100644 --- a/app/src/sys/unix/net.c +++ b/app/src/sys/unix/net.c @@ -1,4 +1,4 @@ -#include "net.h" +#include "util/net.h" #include diff --git a/app/src/tiny_xpm.c b/app/src/tiny_xpm.c index 5ea89078..e96981fb 100644 --- a/app/src/tiny_xpm.c +++ b/app/src/tiny_xpm.c @@ -6,7 +6,7 @@ #include #include "config.h" -#include "log.h" +#include "util/log.h" struct index { char c; diff --git a/app/src/buffer_util.h b/app/src/util/buffer_util.h similarity index 100% rename from app/src/buffer_util.h rename to app/src/util/buffer_util.h diff --git a/app/src/cbuf.h b/app/src/util/cbuf.h similarity index 100% rename from app/src/cbuf.h rename to app/src/util/cbuf.h diff --git a/app/src/lock_util.h b/app/src/util/lock.h similarity index 96% rename from app/src/lock_util.h rename to app/src/util/lock.h index 260d2c12..8ebee241 100644 --- a/app/src/lock_util.h +++ b/app/src/util/lock.h @@ -1,5 +1,5 @@ -#ifndef LOCKUTIL_H -#define LOCKUTIL_H +#ifndef LOCK_H +#define LOCK_H #include #include diff --git a/app/src/log.h b/app/src/util/log.h similarity index 100% rename from app/src/log.h rename to app/src/util/log.h diff --git a/app/src/net.c b/app/src/util/net.c similarity index 100% rename from app/src/net.c rename to app/src/util/net.c diff --git a/app/src/net.h b/app/src/util/net.h similarity index 100% rename from app/src/net.h rename to app/src/util/net.h diff --git a/app/src/queue.h b/app/src/util/queue.h similarity index 100% rename from app/src/queue.h rename to app/src/util/queue.h diff --git a/app/src/str_util.c b/app/src/util/str_util.c similarity index 100% rename from app/src/str_util.c rename to app/src/util/str_util.c diff --git a/app/src/str_util.h b/app/src/util/str_util.h similarity index 100% rename from app/src/str_util.h rename to app/src/util/str_util.h diff --git a/app/src/video_buffer.c b/app/src/video_buffer.c index 2b5f1c2f..9de3ad7f 100644 --- a/app/src/video_buffer.c +++ b/app/src/video_buffer.c @@ -6,8 +6,8 @@ #include #include "config.h" -#include "lock_util.h" -#include "log.h" +#include "util/lock.h" +#include "util/log.h" bool video_buffer_init(struct video_buffer *vb, struct fps_counter *fps_counter, diff --git a/app/tests/test_cbuf.c b/app/tests/test_cbuf.c index 9d5fdc27..dbe50aab 100644 --- a/app/tests/test_cbuf.c +++ b/app/tests/test_cbuf.c @@ -1,7 +1,7 @@ #include #include -#include "cbuf.h" +#include "util/cbuf.h" struct int_queue CBUF(int, 32); diff --git a/app/tests/test_queue.c b/app/tests/test_queue.c index bcbced2b..b0950bb0 100644 --- a/app/tests/test_queue.c +++ b/app/tests/test_queue.c @@ -1,6 +1,6 @@ #include -#include +#include "util/queue.h" struct foo { int value; diff --git a/app/tests/test_strutil.c b/app/tests/test_strutil.c index 18ac4a7d..64e62ed1 100644 --- a/app/tests/test_strutil.c +++ b/app/tests/test_strutil.c @@ -1,7 +1,7 @@ #include #include -#include "str_util.h" +#include "util/str_util.h" static void test_xstrncpy_simple(void) { char s[] = "xxxxxxxxxx"; From ebdc2ee8b5afe900a0f228e0d78577db0cc156af Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 24 Nov 2019 11:55:42 +0100 Subject: [PATCH 0131/2244] Rename variable for consistency Use suffix "Field" for fields variables. --- server/src/main/java/com/genymobile/scrcpy/Workarounds.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java index f45d82a4..ccfe5370 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java +++ b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java @@ -48,9 +48,9 @@ public final class Workarounds { applicationInfo.packageName = "com.genymobile.scrcpy"; // appBindData.appInfo = applicationInfo; - Field appInfo = appBindDataClass.getDeclaredField("appInfo"); - appInfo.setAccessible(true); - appInfo.set(appBindData, applicationInfo); + Field appInfoField = appBindDataClass.getDeclaredField("appInfo"); + appInfoField.setAccessible(true); + appInfoField.set(appBindData, applicationInfo); // activityThread.mBoundApplication = appBindData; Field mBoundApplicationField = activityThreadClass.getDeclaredField("mBoundApplication"); From 2b845680b0d8d6778600d3c46a570d73dc83e2b5 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 24 Nov 2019 11:58:40 +0100 Subject: [PATCH 0132/2244] Initialize Application object to avoid NPE When an ApplicationInfo is set (commit 90293240cc622bb58cb1de741f86cbc0889c03e8), some devices (Nvidia Shield TV) attempt to access the Application object, causing a NullPointerException. As a workaround, initialize an Application object. Fixes --- .../java/com/genymobile/scrcpy/Workarounds.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java index ccfe5370..b1b81903 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java +++ b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java @@ -1,11 +1,15 @@ package com.genymobile.scrcpy; import android.annotation.SuppressLint; +import android.app.Application; +import android.app.Instrumentation; +import android.content.Context; import android.content.pm.ApplicationInfo; import android.os.Looper; import java.lang.reflect.Constructor; import java.lang.reflect.Field; +import java.lang.reflect.Method; public final class Workarounds { private Workarounds() { @@ -56,6 +60,17 @@ public final class Workarounds { Field mBoundApplicationField = activityThreadClass.getDeclaredField("mBoundApplication"); mBoundApplicationField.setAccessible(true); mBoundApplicationField.set(activityThread, appBindData); + + // Context ctx = activityThread.getSystemContext(); + Method getSystemContextMethod = activityThreadClass.getDeclaredMethod("getSystemContext"); + Context ctx = (Context) getSystemContextMethod.invoke(activityThread); + + Application app = Instrumentation.newApplication(Application.class, ctx); + + // activityThread.mInitialApplication = app; + Field mInitialApplicationField = activityThreadClass.getDeclaredField("mInitialApplication"); + mInitialApplicationField.setAccessible(true); + 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()); From 6abb8fd0cd8a4df05128034f662d678be425fee8 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 25 Nov 2019 17:33:06 +0100 Subject: [PATCH 0133/2244] Reformat Java code Reformated by Android studio to match the 150 characters column defined in checkstyle. --- .../com/genymobile/scrcpy/ControlMessage.java | 3 +-- .../java/com/genymobile/scrcpy/Controller.java | 18 ++++++++---------- .../main/java/com/genymobile/scrcpy/Ln.java | 5 +---- .../main/java/com/genymobile/scrcpy/Point.java | 8 ++------ .../java/com/genymobile/scrcpy/Position.java | 8 ++------ .../java/com/genymobile/scrcpy/Server.java | 4 ++-- .../main/java/com/genymobile/scrcpy/Size.java | 8 ++------ .../scrcpy/wrappers/ClipboardManager.java | 10 ++++------ 8 files changed, 22 insertions(+), 42 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java index 30c05a3b..615773fb 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java @@ -47,8 +47,7 @@ public final class ControlMessage { return msg; } - public static ControlMessage createInjectTouchEvent(int action, long pointerId, Position position, float pressure, - int buttons) { + public static ControlMessage createInjectTouchEvent(int action, long pointerId, Position position, float pressure, int buttons) { ControlMessage msg = new ControlMessage(); msg.type = TYPE_INJECT_TOUCH_EVENT; msg.action = action; diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index ce02e333..65feda55 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -21,10 +21,8 @@ public class Controller { private long lastTouchDown; private final PointersState pointersState = new PointersState(); - private final MotionEvent.PointerProperties[] pointerProperties = - new MotionEvent.PointerProperties[PointersState.MAX_POINTERS]; - private final MotionEvent.PointerCoords[] pointerCoords = - new MotionEvent.PointerCoords[PointersState.MAX_POINTERS]; + private final MotionEvent.PointerProperties[] pointerProperties = new MotionEvent.PointerProperties[PointersState.MAX_POINTERS]; + private final MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[PointersState.MAX_POINTERS]; public Controller(Device device, DesktopConnection connection) { this.device = device; @@ -176,8 +174,8 @@ public class Controller { } } - MotionEvent event = MotionEvent.obtain(lastTouchDown, now, action, pointerCount, pointerProperties, - pointerCoords, 0, buttons, 1f, 1f, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0); + MotionEvent event = MotionEvent.obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, 0, 0, + InputDevice.SOURCE_TOUCHSCREEN, 0); return injectEvent(event); } @@ -198,15 +196,15 @@ public class Controller { coords.setAxisValue(MotionEvent.AXIS_HSCROLL, hScroll); coords.setAxisValue(MotionEvent.AXIS_VSCROLL, vScroll); - MotionEvent event = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, - pointerCoords, 0, 0, 1f, 1f, 0, 0, InputDevice.SOURCE_MOUSE, 0); + MotionEvent event = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, 0, 1f, 1f, 0, 0, + InputDevice.SOURCE_MOUSE, 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); + KeyEvent event = new KeyEvent(now, now, action, keyCode, repeat, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0, + InputDevice.SOURCE_KEYBOARD); return injectEvent(event); } diff --git a/server/src/main/java/com/genymobile/scrcpy/Ln.java b/server/src/main/java/com/genymobile/scrcpy/Ln.java index bb741225..26f13a56 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Ln.java +++ b/server/src/main/java/com/genymobile/scrcpy/Ln.java @@ -12,10 +12,7 @@ public final class Ln { private static final String PREFIX = "[server] "; enum Level { - DEBUG, - INFO, - WARN, - ERROR; + DEBUG, INFO, WARN, ERROR } private static final Level THRESHOLD = BuildConfig.DEBUG ? Level.DEBUG : Level.INFO; diff --git a/server/src/main/java/com/genymobile/scrcpy/Point.java b/server/src/main/java/com/genymobile/scrcpy/Point.java index 9ef2db03..c2a30fa8 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Point.java +++ b/server/src/main/java/com/genymobile/scrcpy/Point.java @@ -28,8 +28,7 @@ public class Point { return false; } Point point = (Point) o; - return x == point.x - && y == point.y; + return x == point.x && y == point.y; } @Override @@ -39,9 +38,6 @@ public class Point { @Override public String toString() { - return "Point{" - + "x=" + x - + ", y=" + y - + '}'; + return "Point{" + "x=" + x + ", y=" + y + '}'; } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Position.java b/server/src/main/java/com/genymobile/scrcpy/Position.java index 757fa36e..b46d2f73 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Position.java +++ b/server/src/main/java/com/genymobile/scrcpy/Position.java @@ -32,8 +32,7 @@ public class Position { return false; } Position position = (Position) o; - return Objects.equals(point, position.point) - && Objects.equals(screenSize, position.screenSize); + return Objects.equals(point, position.point) && Objects.equals(screenSize, position.screenSize); } @Override @@ -43,10 +42,7 @@ public class Position { @Override public String toString() { - return "Position{" - + "point=" + point - + ", screenSize=" + screenSize - + '}'; + return "Position{" + "point=" + point + ", screenSize=" + screenSize + '}'; } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 26ef2850..d4691e7c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -73,8 +73,8 @@ 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 + ")"); + throw new IllegalArgumentException( + "The server version (" + clientVersion + ") does not match the client " + "(" + BuildConfig.VERSION_NAME + ")"); } if (args.length != 8) { diff --git a/server/src/main/java/com/genymobile/scrcpy/Size.java b/server/src/main/java/com/genymobile/scrcpy/Size.java index 0d546bbd..fd4b6971 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Size.java +++ b/server/src/main/java/com/genymobile/scrcpy/Size.java @@ -38,8 +38,7 @@ public final class Size { return false; } Size size = (Size) o; - return width == size.width - && height == size.height; + return width == size.width && height == size.height; } @Override @@ -49,9 +48,6 @@ public final class Size { @Override public String toString() { - return "Size{" - + "width=" + width - + ", height=" + height - + '}'; + return "Size{" + "width=" + width + ", height=" + height + '}'; } } 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 27dcb443..ebd89238 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java @@ -43,8 +43,7 @@ public class ClipboardManager { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class); } else { - setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, - String.class, int.class); + setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, int.class); } } catch (NoSuchMethodException e) { Ln.e("Could not find method", e); @@ -53,16 +52,15 @@ public class ClipboardManager { return setPrimaryClipMethod; } - private static ClipData getPrimaryClip(Method method, IInterface manager) throws InvocationTargetException, - IllegalAccessException { + 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, PACKAGE_NAME, USER_ID); } - private static void setPrimaryClip(Method method, IInterface manager, ClipData clipData) throws InvocationTargetException, - IllegalAccessException { + 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); } else { From 31d9d5611730e4753bd72e0cb7807777d979780c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 26 Nov 2019 09:08:12 +0100 Subject: [PATCH 0134/2244] Fix warnings Fix warnings reported with -Dwarning_level=2. --- app/src/receiver.c | 8 ++++---- app/src/scrcpy.c | 2 ++ app/src/screen.c | 4 ++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/app/src/receiver.c b/app/src/receiver.c index 23944d5a..33bbf8b5 100644 --- a/app/src/receiver.c +++ b/app/src/receiver.c @@ -23,7 +23,7 @@ receiver_destroy(struct receiver *receiver) { } static void -process_msg(struct receiver *receiver, struct device_msg *msg) { +process_msg(struct device_msg *msg) { switch (msg->type) { case DEVICE_MSG_TYPE_CLIPBOARD: LOGI("Device clipboard copied"); @@ -33,7 +33,7 @@ process_msg(struct receiver *receiver, struct device_msg *msg) { } static ssize_t -process_msgs(struct receiver *receiver, const unsigned char *buf, size_t len) { +process_msgs(const unsigned char *buf, size_t len) { size_t head = 0; for (;;) { struct device_msg msg; @@ -45,7 +45,7 @@ process_msgs(struct receiver *receiver, const unsigned char *buf, size_t len) { return head; } - process_msg(receiver, &msg); + process_msg(&msg); device_msg_destroy(&msg); head += r; @@ -72,7 +72,7 @@ run_receiver(void *data) { break; } - ssize_t consumed = process_msgs(receiver, buf, r); + ssize_t consumed = process_msgs(buf, r); if (consumed == -1) { // an error occurred break; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 673ec678..06e20e26 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -201,6 +201,7 @@ handle_event(SDL_Event *event, bool control) { static bool event_loop(bool display, bool control) { + (void) display; #ifdef CONTINUOUS_RESIZING_WORKAROUND if (display) { SDL_AddEventWatch(event_watcher, NULL); @@ -256,6 +257,7 @@ sdl_priority_from_av_level(int level) { static void av_log_callback(void *avcl, int level, const char *fmt, va_list vl) { + (void) avcl; SDL_LogPriority priority = sdl_priority_from_av_level(level); if (priority == 0) { return; diff --git a/app/src/screen.c b/app/src/screen.c index 4cc7cde3..5c1fdab3 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, window_flags |= SDL_WINDOW_BORDERLESS; } - int x = window_x != -1 ? window_x : SDL_WINDOWPOS_UNDEFINED; - int y = window_y != -1 ? window_y : SDL_WINDOWPOS_UNDEFINED; + int x = window_x != -1 ? window_x : (int) SDL_WINDOWPOS_UNDEFINED; + int y = window_y != -1 ? window_y : (int) SDL_WINDOWPOS_UNDEFINED; screen->window = SDL_CreateWindow(window_title, x, y, window_size.width, window_size.height, window_flags); From 7637a113e3cb9ce1fb9e19ac1f790d551d2af1f9 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 26 Nov 2019 09:22:20 +0100 Subject: [PATCH 0135/2244] Compile with warning_level=2 by default --- meson.build | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/meson.build b/meson.build index ba19d7ee..78bcd1a8 100644 --- a/meson.build +++ b/meson.build @@ -1,7 +1,10 @@ project('scrcpy', 'c', version: '1.11', meson_version: '>= 0.37', - default_options: 'c_std=c11') + default_options: [ + 'c_std=c11', + 'warning_level=2', + ]) if get_option('compile_app') subdir('app') From 8bc056b9c68eea7bccfa5d9123a70443fb1d75b2 Mon Sep 17 00:00:00 2001 From: yangfl Date: Wed, 27 Nov 2019 17:44:14 +0800 Subject: [PATCH 0136/2244] Fix manpage format --- app/scrcpy.1 | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 6cb062b5..c77fd985 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -26,7 +26,7 @@ Encode the video at the given bit\-rate, expressed in bits/s. Unit suffixes are Default is 8000000. .TP -.BI \-\-crop " width\fR:\fIheight\fR:\fIx\fR:\fIy +.BI "\-\-crop " width\fR:\fIheight\fR:\fIx\fR:\fIy Crop the device screen on the server. The values are expressed in the device natural orientation (typically, portrait for a phone, landscape for a tablet). Any @@ -42,7 +42,7 @@ Start in fullscreen. Print this help. .TP -.BI \-\-max\-fps " value +.BI "\-\-max\-fps " value Limit the framerate of screen capture (only supported on devices with Android >= 10). .TP @@ -88,7 +88,7 @@ The format is determined by the option if set, or by the file extension (.mp4 or .mkv). .TP -.BI \-\-record\-format " format +.BI "\-\-record\-format " format Force recording format (either mp4 or mkv). .TP @@ -118,29 +118,29 @@ Print the version of scrcpy. Disable window decorations (display borderless window). .TP -.BI \-\-window\-title " text +.BI "\-\-window\-title " text Set a custom window title. .TP -.BI \-\-window\-x " value +.BI "\-\-window\-x " value Set the initial window horizontal position. Default is -1 (automatic).\n .TP -.BI \-\-window\-y " value +.BI "\-\-window\-y " value Set the initial window vertical position. Default is -1 (automatic).\n .TP -.BI \-\-window\-width " value +.BI "\-\-window\-width " value Set the initial window width. Default is 0 (automatic).\n .TP -.BI \-\-window\-height " value +.BI "\-\-window\-height " value Set the initial window height. Default is 0 (automatic).\n From 06104a701b6e2d149d536938a967f9707e22ed9f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 27 Nov 2019 13:25:56 +0100 Subject: [PATCH 0137/2244] Fix windows build Utilities have been moved to util/, but includes had not been updated in Windows-specific files. Ref: dfd0707a2925e9569aaa2ae6b88b161a12376a49 --- app/src/sys/win/command.c | 4 ++-- app/src/sys/win/net.c | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/sys/win/command.c b/app/src/sys/win/command.c index f23730a0..ab335089 100644 --- a/app/src/sys/win/command.c +++ b/app/src/sys/win/command.c @@ -1,8 +1,8 @@ #include "command.h" #include "config.h" -#include "log.h" -#include "str_util.h" +#include "util/log.h" +#include "util/str_util.h" static int build_cmd(char *cmd, size_t len, const char *const argv[]) { diff --git a/app/src/sys/win/net.c b/app/src/sys/win/net.c index 55519782..aebce7fc 100644 --- a/app/src/sys/win/net.c +++ b/app/src/sys/win/net.c @@ -1,7 +1,7 @@ -#include "net.h" +#include "util/net.h" #include "config.h" -#include "log.h" +#include "util/log.h" bool net_init(void) { From 8dc11a0286fba384ae88254e6bcec785b2b73473 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 27 Nov 2019 13:39:42 +0100 Subject: [PATCH 0138/2244] Fix warnings on Windows fix warnings reported with -Dwarning_level=2 on Windows. --- app/src/command.h | 4 ++-- app/src/main.c | 2 +- app/src/scrcpy.c | 1 + app/src/stream.c | 5 +++-- app/src/sys/win/command.c | 1 + 5 files changed, 8 insertions(+), 5 deletions(-) diff --git a/app/src/command.h b/app/src/command.h index d119c9bb..e748c1c5 100644 --- a/app/src/command.h +++ b/app/src/command.h @@ -18,6 +18,7 @@ # define PRIsizet PRIu32 # endif # define PROCESS_NONE NULL +# define NO_EXIT_CODE -1u // max value as unsigned typedef HANDLE process_t; typedef DWORD exit_code_t; @@ -28,6 +29,7 @@ # define PRIsizet "zu" # define PRIexitcode "d" # define PROCESS_NONE -1 +# define NO_EXIT_CODE -1 typedef pid_t process_t; typedef int exit_code_t; @@ -35,8 +37,6 @@ #include "config.h" -# define NO_EXIT_CODE -1 - enum process_result { PROCESS_SUCCESS, PROCESS_ERROR_GENERIC, diff --git a/app/src/main.c b/app/src/main.c index c17cc6bf..cdd8c2ef 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -243,7 +243,7 @@ parse_bit_rate(char *optarg, uint32_t *bit_rate) { return false; } } - if (value < 0 || ((uint32_t) -1) / mul < value) { + if (value < 0 || ((uint32_t) -1) / mul < (unsigned long) value) { LOGE("Bitrate must be positive and less than 2^32: %s", optarg); return false; } diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 06e20e26..17be1ed4 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -103,6 +103,7 @@ sdl_init_and_configure(bool display) { // 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! diff --git a/app/src/stream.c b/app/src/stream.c index c84de981..35a541e4 100644 --- a/app/src/stream.c +++ b/app/src/stream.c @@ -44,6 +44,7 @@ stream_recv_packet(struct stream *stream, AVPacket *packet) { uint64_t pts = buffer_read64be(header); uint32_t len = buffer_read32be(&header[8]); + SDL_assert(pts == NO_PTS || (pts & 0x8000000000000000) == 0); SDL_assert(len); if (av_new_packet(packet, len)) { @@ -52,12 +53,12 @@ stream_recv_packet(struct stream *stream, AVPacket *packet) { } r = net_recv_all(stream->socket, packet->data, len); - if (r < len) { + if (r < 0 || ((uint32_t) r) < len) { av_packet_unref(packet); return false; } - packet->pts = pts != NO_PTS ? pts : AV_NOPTS_VALUE; + packet->pts = pts != NO_PTS ? (int64_t) pts : AV_NOPTS_VALUE; return true; } diff --git a/app/src/sys/win/command.c b/app/src/sys/win/command.c index ab335089..6cd0e944 100644 --- a/app/src/sys/win/command.c +++ b/app/src/sys/win/command.c @@ -20,6 +20,7 @@ build_cmd(char *cmd, size_t len, const char *const argv[]) { enum process_result cmd_execute(const char *path, const char *const argv[], HANDLE *handle) { + (void) path; STARTUPINFOW si; PROCESS_INFORMATION pi; memset(&si, 0, sizeof(si)); From 73e8ec1b356eacfc4a77e84fcd4c143727e8eeb0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 27 Nov 2019 13:41:47 +0100 Subject: [PATCH 0139/2244] Remove path argument from cmd_execute() It is always equal to argv[0] (or not used on Windows). --- app/src/command.c | 2 +- app/src/command.h | 2 +- app/src/sys/unix/command.c | 4 ++-- app/src/sys/win/command.c | 3 +-- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/app/src/command.c b/app/src/command.c index 33a9c587..abaa223d 100644 --- a/app/src/command.c +++ b/app/src/command.c @@ -91,7 +91,7 @@ adb_execute(const char *serial, const char *const adb_cmd[], size_t len) { memcpy(&cmd[i], adb_cmd, len * sizeof(const char *)); cmd[len + i] = NULL; - enum process_result r = cmd_execute(cmd[0], cmd, &process); + enum process_result r = cmd_execute(cmd, &process); if (r != PROCESS_SUCCESS) { show_adb_err_msg(r, cmd); return PROCESS_NONE; diff --git a/app/src/command.h b/app/src/command.h index e748c1c5..edbc3fb8 100644 --- a/app/src/command.h +++ b/app/src/command.h @@ -44,7 +44,7 @@ enum process_result { }; enum process_result -cmd_execute(const char *path, const char *const argv[], process_t *process); +cmd_execute(const char *const argv[], process_t *process); bool cmd_terminate(process_t pid); diff --git a/app/src/sys/unix/command.c b/app/src/sys/unix/command.c index 512b9af7..fbcf2355 100644 --- a/app/src/sys/unix/command.c +++ b/app/src/sys/unix/command.c @@ -21,7 +21,7 @@ #include "util/log.h" enum process_result -cmd_execute(const char *path, const char *const argv[], pid_t *pid) { +cmd_execute(const char *const argv[], pid_t *pid) { int fd[2]; if (pipe(fd) == -1) { @@ -52,7 +52,7 @@ cmd_execute(const char *path, const char *const argv[], pid_t *pid) { // child close read side close(fd[0]); if (fcntl(fd[1], F_SETFD, FD_CLOEXEC) == 0) { - execvp(path, (char *const *)argv); + execvp(argv[0], (char *const *)argv); if (errno == ENOENT) { ret = PROCESS_ERROR_MISSING_BINARY; } else { diff --git a/app/src/sys/win/command.c b/app/src/sys/win/command.c index 6cd0e944..55edaf8f 100644 --- a/app/src/sys/win/command.c +++ b/app/src/sys/win/command.c @@ -19,8 +19,7 @@ build_cmd(char *cmd, size_t len, const char *const argv[]) { } enum process_result -cmd_execute(const char *path, const char *const argv[], HANDLE *handle) { - (void) path; +cmd_execute(const char *const argv[], HANDLE *handle) { STARTUPINFOW si; PROCESS_INFORMATION pi; memset(&si, 0, sizeof(si)); From b5ebb234ddbb3683441ec1a6f2492ea0190d2615 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 27 Nov 2019 13:53:29 +0100 Subject: [PATCH 0140/2244] Replace BUILD_DEBUG by NDEBUG Use the "standard" NDEBUG definition, which is used by assert(). --- app/meson.build | 2 +- app/src/main.c | 2 +- app/src/tiny_xpm.c | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/meson.build b/app/meson.build index 005239b8..843bfd9d 100644 --- a/app/meson.build +++ b/app/meson.build @@ -85,7 +85,7 @@ endif conf = configuration_data() # expose the build type -conf.set('BUILD_DEBUG', get_option('buildtype') == 'debug') +conf.set('NDEBUG', get_option('buildtype') != 'debug') # the version, updated on release conf.set_quoted('SCRCPY_VERSION', meson.project_version()) diff --git a/app/src/main.c b/app/src/main.c index cdd8c2ef..78bdcfd2 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -625,7 +625,7 @@ main(int argc, char *argv[]) { return 1; } -#ifdef BUILD_DEBUG +#ifndef NDEBUG SDL_LogSetAllPriority(SDL_LOG_PRIORITY_DEBUG); #endif diff --git a/app/src/tiny_xpm.c b/app/src/tiny_xpm.c index e96981fb..29e0b54f 100644 --- a/app/src/tiny_xpm.c +++ b/app/src/tiny_xpm.c @@ -36,7 +36,7 @@ find_color(struct index *index, int len, char c, uint32_t *color) { // (non-const) "char *" SDL_Surface * read_xpm(char *xpm[]) { -#if SDL_ASSERT_LEVEL >= 2 +#ifndef NDEBUG // patch the XPM to change the icon color in debug mode xpm[2] = ". c #CC00CC"; #endif From 510caff0cd1f6b369e9b00e97d806099d08146ea Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 27 Nov 2019 21:11:40 +0100 Subject: [PATCH 0141/2244] Replace SDL_assert() by assert() SDL_assert() open a dialog on assertion failure. There is no reason not to use assert() directly. --- app/src/control_msg.c | 4 ++-- app/src/controller.c | 5 +++-- app/src/decoder.c | 1 - app/src/device_msg.c | 1 - app/src/file_handler.c | 5 +++-- app/src/fps_counter.c | 4 ++-- app/src/input_manager.c | 4 ++-- app/src/receiver.c | 6 +++--- app/src/recorder.c | 6 +++--- app/src/screen.c | 7 ++++--- app/src/server.c | 6 +++--- app/src/stream.c | 11 ++++++----- app/src/tiny_xpm.c | 24 ++++++++++++++---------- app/src/util/queue.h | 4 ++-- app/src/video_buffer.c | 4 ++-- 15 files changed, 49 insertions(+), 43 deletions(-) diff --git a/app/src/control_msg.c b/app/src/control_msg.c index 363483eb..fda16025 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -1,7 +1,7 @@ #include "control_msg.h" +#include #include -#include #include "config.h" #include "util/buffer_util.h" @@ -27,7 +27,7 @@ write_string(const char *utf8, size_t max_len, unsigned char *buf) { static uint16_t to_fixed_point_16(float f) { - SDL_assert(f >= 0.0f && f <= 1.0f); + assert(f >= 0.0f && f <= 1.0f); uint32_t u = f * 0x1p16f; // 2^16 if (u >= 0xffff) { u = 0xffff; diff --git a/app/src/controller.c b/app/src/controller.c index ec706c95..d59a7411 100644 --- a/app/src/controller.c +++ b/app/src/controller.c @@ -1,6 +1,6 @@ #include "controller.h" -#include +#include #include "config.h" #include "util/lock.h" @@ -85,7 +85,8 @@ run_controller(void *data) { } struct control_msg msg; bool non_empty = cbuf_take(&controller->queue, &msg); - SDL_assert(non_empty); + assert(non_empty); + (void) non_empty; mutex_unlock(controller->mutex); bool ok = process_msg(controller, &msg); diff --git a/app/src/decoder.c b/app/src/decoder.c index 52176484..5ac6cd44 100644 --- a/app/src/decoder.c +++ b/app/src/decoder.c @@ -2,7 +2,6 @@ #include #include -#include #include #include #include diff --git a/app/src/device_msg.c b/app/src/device_msg.c index aba56bb3..db176129 100644 --- a/app/src/device_msg.c +++ b/app/src/device_msg.c @@ -1,7 +1,6 @@ #include "device_msg.h" #include -#include #include "config.h" #include "util/buffer_util.h" diff --git a/app/src/file_handler.c b/app/src/file_handler.c index c0b03bcf..ba689404 100644 --- a/app/src/file_handler.c +++ b/app/src/file_handler.c @@ -1,7 +1,7 @@ #include "file_handler.h" +#include #include -#include #include "config.h" #include "command.h" @@ -120,7 +120,8 @@ run_file_handler(void *data) { } struct file_handler_request req; bool non_empty = cbuf_take(&file_handler->queue, &req); - SDL_assert(non_empty); + assert(non_empty); + (void) non_empty; process_t process; if (req.action == ACTION_INSTALL_APK) { diff --git a/app/src/fps_counter.c b/app/src/fps_counter.c index 0c3f13d4..58c62d55 100644 --- a/app/src/fps_counter.c +++ b/app/src/fps_counter.c @@ -1,6 +1,6 @@ #include "fps_counter.h" -#include +#include #include #include "config.h" @@ -77,7 +77,7 @@ run_fps_counter(void *data) { uint32_t now = SDL_GetTicks(); check_interval_expired(counter, now); - SDL_assert(counter->next_timestamp > now); + assert(counter->next_timestamp > now); uint32_t remaining = counter->next_timestamp - now; // ignore the reason (timeout or signaled), we just loop anyway diff --git a/app/src/input_manager.c b/app/src/input_manager.c index e0aa6054..013fb640 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -1,6 +1,6 @@ #include "input_manager.h" -#include +#include #include "config.h" #include "event_converter.h" @@ -217,7 +217,7 @@ input_manager_process_text_input(struct input_manager *im, if (!im->prefer_text) { char c = event->text[0]; if (isalpha(c) || c == ' ') { - SDL_assert(event->text[1] == '\0'); + assert(event->text[1] == '\0'); // letters and space are handled as raw key event return; } diff --git a/app/src/receiver.c b/app/src/receiver.c index 33bbf8b5..0474ff55 100644 --- a/app/src/receiver.c +++ b/app/src/receiver.c @@ -1,6 +1,6 @@ #include "receiver.h" -#include +#include #include #include "config.h" @@ -49,7 +49,7 @@ process_msgs(const unsigned char *buf, size_t len) { device_msg_destroy(&msg); head += r; - SDL_assert(head <= len); + assert(head <= len); if (head == len) { return head; } @@ -64,7 +64,7 @@ run_receiver(void *data) { size_t head = 0; for (;;) { - SDL_assert(head < DEVICE_MSG_SERIALIZED_MAX_SIZE); + assert(head < DEVICE_MSG_SERIALIZED_MAX_SIZE); ssize_t r = net_recv(receiver->control_socket, buf, DEVICE_MSG_SERIALIZED_MAX_SIZE - head); if (r <= 0) { diff --git a/app/src/recorder.c b/app/src/recorder.c index b5314daf..465b24e8 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -1,7 +1,7 @@ #include "recorder.h" +#include #include -#include #include "config.h" #include "compat.h" @@ -116,7 +116,7 @@ recorder_get_format_name(enum recorder_format format) { bool recorder_open(struct recorder *recorder, const AVCodec *input_codec) { const char *format_name = recorder_get_format_name(recorder->format); - SDL_assert(format_name); + assert(format_name); const AVOutputFormat *format = find_muxer(format_name); if (!format) { LOGE("Could not find muxer"); @@ -357,7 +357,7 @@ recorder_join(struct recorder *recorder) { bool recorder_push(struct recorder *recorder, const AVPacket *packet) { mutex_lock(recorder->mutex); - SDL_assert(!recorder->stopped); + assert(!recorder->stopped); if (recorder->failed) { // reject any new packet (this will stop the stream) diff --git a/app/src/screen.c b/app/src/screen.c index 5c1fdab3..beb10754 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -1,5 +1,6 @@ #include "screen.h" +#include #include #include @@ -110,7 +111,7 @@ get_optimal_size(struct size current_size, struct size frame_size) { } // w and h must fit into 16 bits - SDL_assert_release(w < 0x10000 && h < 0x10000); + assert(w < 0x10000 && h < 0x10000); return (struct size) {w, h}; } @@ -392,8 +393,8 @@ screen_handle_window_event(struct screen *screen, break; case SDL_WINDOWEVENT_MAXIMIZED: // The backup size must be non-nul. - SDL_assert(screen->windowed_window_size_backup.width); - SDL_assert(screen->windowed_window_size_backup.height); + 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 diff --git a/app/src/server.c b/app/src/server.c index d9114356..90eb4c69 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -1,10 +1,10 @@ #include "server.h" +#include #include #include #include #include -#include #include #include "config.h" @@ -199,7 +199,7 @@ connect_to_server(uint16_t port, uint32_t attempts, uint32_t delay) { static void close_socket(socket_t *socket) { - SDL_assert(*socket != INVALID_SOCKET); + assert(*socket != INVALID_SOCKET); net_shutdown(*socket, SHUT_RDWR); if (!net_close(*socket)) { LOGW("Could not close socket"); @@ -323,7 +323,7 @@ server_stop(struct server *server) { close_socket(&server->control_socket); } - SDL_assert(server->process != PROCESS_NONE); + assert(server->process != PROCESS_NONE); if (!cmd_terminate(server->process)) { LOGW("Could not terminate server"); diff --git a/app/src/stream.c b/app/src/stream.c index 35a541e4..64b2bf39 100644 --- a/app/src/stream.c +++ b/app/src/stream.c @@ -1,8 +1,8 @@ #include "stream.h" +#include #include #include -#include #include #include #include @@ -44,8 +44,8 @@ stream_recv_packet(struct stream *stream, AVPacket *packet) { uint64_t pts = buffer_read64be(header); uint32_t len = buffer_read32be(&header[8]); - SDL_assert(pts == NO_PTS || (pts & 0x8000000000000000) == 0); - SDL_assert(len); + assert(pts == NO_PTS || (pts & 0x8000000000000000) == 0); + assert(len); if (av_new_packet(packet, len)) { LOGE("Could not allocate packet"); @@ -108,8 +108,9 @@ stream_parse(struct stream *stream, AVPacket *packet) { AV_NOPTS_VALUE, AV_NOPTS_VALUE, -1); // PARSER_FLAG_COMPLETE_FRAMES is set - SDL_assert(r == in_len); - SDL_assert(out_len == in_len); + assert(r == in_len); + (void) r; + assert(out_len == in_len); if (stream->parser->key_frame == 1) { packet->flags |= AV_PKT_FLAG_KEY; diff --git a/app/src/tiny_xpm.c b/app/src/tiny_xpm.c index 29e0b54f..feb3d1cb 100644 --- a/app/src/tiny_xpm.c +++ b/app/src/tiny_xpm.c @@ -1,5 +1,6 @@ #include "tiny_xpm.h" +#include #include #include #include @@ -51,24 +52,26 @@ read_xpm(char *xpm[]) { int chars = strtol(endptr + 1, &endptr, 10); // sanity checks - SDL_assert(0 <= width && width < 256); - SDL_assert(0 <= height && height < 256); - SDL_assert(0 <= colors && colors < 256); - SDL_assert(chars == 1); // this implementation does not support more + assert(0 <= width && width < 256); + assert(0 <= height && height < 256); + assert(0 <= colors && colors < 256); + assert(chars == 1); // this implementation does not support more + + (void) chars; // init index struct index index[colors]; for (int i = 0; i < colors; ++i) { const char *line = xpm[1+i]; index[i].c = line[0]; - SDL_assert(line[1] == '\t'); - SDL_assert(line[2] == 'c'); - SDL_assert(line[3] == ' '); + assert(line[1] == '\t'); + assert(line[2] == 'c'); + assert(line[3] == ' '); if (line[4] == '#') { index[i].color = 0xff000000 | strtol(&line[5], &endptr, 0x10); - SDL_assert(*endptr == '\0'); + assert(*endptr == '\0'); } else { - SDL_assert(!strcmp("None", &line[4])); + assert(!strcmp("None", &line[4])); index[i].color = 0; } } @@ -85,7 +88,8 @@ read_xpm(char *xpm[]) { char c = line[x]; uint32_t color; bool color_found = find_color(index, colors, c, &color); - SDL_assert(color_found); + assert(color_found); + (void) color_found; pixels[y * width + x] = color; } } diff --git a/app/src/util/queue.h b/app/src/util/queue.h index 6cf7aba6..12bc9e89 100644 --- a/app/src/util/queue.h +++ b/app/src/util/queue.h @@ -2,9 +2,9 @@ #ifndef QUEUE_H #define QUEUE_H +#include #include #include -#include #include "config.h" @@ -67,7 +67,7 @@ // type so that we can "return" it) #define queue_take(PQ, NEXTFIELD, PITEM) \ (void) ({ \ - SDL_assert(!queue_is_empty(PQ)); \ + assert(!queue_is_empty(PQ)); \ *(PITEM) = (PQ)->first; \ (PQ)->first = (PQ)->first->NEXTFIELD; \ }) diff --git a/app/src/video_buffer.c b/app/src/video_buffer.c index 9de3ad7f..629680d9 100644 --- a/app/src/video_buffer.c +++ b/app/src/video_buffer.c @@ -1,6 +1,6 @@ #include "video_buffer.h" -#include +#include #include #include #include @@ -91,7 +91,7 @@ video_buffer_offer_decoded_frame(struct video_buffer *vb, const AVFrame * video_buffer_consume_rendered_frame(struct video_buffer *vb) { - SDL_assert(!vb->rendering_frame_consumed); + assert(!vb->rendering_frame_consumed); vb->rendering_frame_consumed = true; fps_counter_add_rendered_frame(vb->fps_counter); if (vb->render_expired_frames) { From d0f5a7fd9f49e3780c40f34eeeaa1a6574558a47 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 27 Nov 2019 21:24:52 +0100 Subject: [PATCH 0142/2244] Remove unused includes No mutex is used in decoder.c and stream.c. --- app/src/decoder.c | 1 - app/src/stream.c | 1 - 2 files changed, 2 deletions(-) diff --git a/app/src/decoder.c b/app/src/decoder.c index 5ac6cd44..49d4ce86 100644 --- a/app/src/decoder.c +++ b/app/src/decoder.c @@ -13,7 +13,6 @@ #include "recorder.h" #include "video_buffer.h" #include "util/buffer_util.h" -#include "util/lock.h" #include "util/log.h" // set the decoded frame as ready for rendering, and notify diff --git a/app/src/stream.c b/app/src/stream.c index 64b2bf39..dd2dbd76 100644 --- a/app/src/stream.c +++ b/app/src/stream.c @@ -14,7 +14,6 @@ #include "events.h" #include "recorder.h" #include "util/buffer_util.h" -#include "util/lock.h" #include "util/log.h" #define BUFSIZE 0x10000 From 15a206b7fcc9ecab2a1d0ffa782bb3d4c845a25a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 27 Nov 2019 21:32:52 +0100 Subject: [PATCH 0143/2244] Assert return value of mutex functions Mutex functions may only fail due to a programming error. Use assertions in debug builds, and ignore the value in release builds. --- app/src/util/lock.h | 40 +++++++++++++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/app/src/util/lock.h b/app/src/util/lock.h index 8ebee241..cb7c318c 100644 --- a/app/src/util/lock.h +++ b/app/src/util/lock.h @@ -9,44 +9,66 @@ static inline void mutex_lock(SDL_mutex *mutex) { - if (SDL_LockMutex(mutex)) { - LOGC("Could not lock mutex"); + int r = SDL_LockMutex(mutex); +#ifndef NDEBUG + if (r) { + LOGC("Could not lock mutex: %s", SDL_GetError()); abort(); } +#else + (void) r; +#endif } static inline void mutex_unlock(SDL_mutex *mutex) { - if (SDL_UnlockMutex(mutex)) { - LOGC("Could not unlock mutex"); + int r = SDL_UnlockMutex(mutex); +#ifndef NDEBUG + if (r) { + LOGC("Could not unlock mutex: %s", SDL_GetError()); abort(); } +#else + (void) r; +#endif } static inline void cond_wait(SDL_cond *cond, SDL_mutex *mutex) { - if (SDL_CondWait(cond, mutex)) { - LOGC("Could not wait on condition"); + int r = SDL_CondWait(cond, mutex); +#ifndef NDEBUG + if (r) { + LOGC("Could not wait on condition: %s", SDL_GetError()); abort(); } +#else + (void) r; +#endif } static inline int cond_wait_timeout(SDL_cond *cond, SDL_mutex *mutex, uint32_t ms) { int r = SDL_CondWaitTimeout(cond, mutex, ms); +#ifndef NDEBUG if (r < 0) { - LOGC("Could not wait on condition with timeout"); + LOGC("Could not wait on condition with timeout: %s", SDL_GetError()); abort(); } +#endif return r; } static inline void cond_signal(SDL_cond *cond) { - if (SDL_CondSignal(cond)) { - LOGC("Could not signal a condition"); + int r = SDL_CondSignal(cond); +#ifndef NDEBUG + if (r) { + LOGC("Could not signal a condition: %s", SDL_GetError()); abort(); } +#else + (void) r; +#endif } #endif From 26529d377fe8aae2fd1ffa91405aa801a0ce8a57 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 29 Nov 2019 14:15:43 +0100 Subject: [PATCH 0144/2244] Use virtual device id to avoid NPE Inject mouse events using id -1 (virtual device) instead of 0 which does not exist (and causes the InputDevice to be null). Fixes --- .../main/java/com/genymobile/scrcpy/Controller.java | 12 ++++++++---- 1 file changed, 8 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 65feda55..51b13627 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -13,6 +13,8 @@ import java.io.IOException; public class Controller { + private static final int DEVICE_ID_VIRTUAL = -1; + private final Device device; private final DesktopConnection connection; private final DeviceMessageSender sender; @@ -174,8 +176,9 @@ public class Controller { } } - MotionEvent event = MotionEvent.obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, 0, 0, - InputDevice.SOURCE_TOUCHSCREEN, 0); + 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); } @@ -196,8 +199,9 @@ public class Controller { coords.setAxisValue(MotionEvent.AXIS_HSCROLL, hScroll); coords.setAxisValue(MotionEvent.AXIS_VSCROLL, vScroll); - MotionEvent event = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, 0, 1f, 1f, 0, 0, - InputDevice.SOURCE_MOUSE, 0); + MotionEvent event = MotionEvent + .obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, 0, 1f, 1f, DEVICE_ID_VIRTUAL, 0, + InputDevice.SOURCE_MOUSE, 0); return injectEvent(event); } From 8a694a97855333849976bea5e558eaa45add0168 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 30 Nov 2019 10:44:49 +0100 Subject: [PATCH 0145/2244] Suggest workaround for error 0xfffffc0e When the hardware encoder is not able to encode at the given definition, it fails with an error 0xfffffc0e. It is documented in the FAQ: But it is better to directly suggest the workaround in the console. --- .../main/java/com/genymobile/scrcpy/Server.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index d4691e7c..87aeacac 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -1,6 +1,8 @@ package com.genymobile.scrcpy; import android.graphics.Rect; +import android.media.MediaCodec; +import android.os.Build; import java.io.File; import java.io.IOException; @@ -133,11 +135,25 @@ public final class Server { } } + private static void suggestFix(Throwable e) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (e instanceof MediaCodec.CodecException) { + MediaCodec.CodecException mce = (MediaCodec.CodecException) e; + if (mce.getErrorCode() == 0xfffffc0e) { + Ln.e("The hardware encoder is not able to encode at the given definition."); + Ln.e("Try with a lower definition:"); + Ln.e(" scrcpy -m 1024"); + } + } + } + } + public static void main(String... args) throws Exception { Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { @Override public void uncaughtException(Thread t, Throwable e) { Ln.e("Exception on thread " + t, e); + suggestFix(e); } }); From 86fcd89d8046318e6a40a5c7938999db6fe7acd0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 3 Dec 2019 12:06:43 +0100 Subject: [PATCH 0146/2244] Fix max size default value Suggested-by: jurkov Closes --- app/src/scrcpy.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index 8723f29f..75de8717 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -42,7 +42,7 @@ struct scrcpy_options { .push_target = NULL, \ .record_format = RECORDER_FORMAT_AUTO, \ .port = DEFAULT_LOCAL_PORT, \ - .max_size = DEFAULT_LOCAL_PORT, \ + .max_size = DEFAULT_MAX_SIZE, \ .bit_rate = DEFAULT_BIT_RATE, \ .max_fps = 0, \ .window_x = -1, \ From 684e0abb74cf2f5291d2c52bbf758e85f917a4d6 Mon Sep 17 00:00:00 2001 From: Seb Leo <44522755+zadi15@users.noreply.github.com> Date: Fri, 29 Nov 2019 22:36:23 +0000 Subject: [PATCH 0147/2244] Update BUILD.md to install adb package PR Signed-off-by: Romain Vimont --- BUILD.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BUILD.md b/BUILD.md index 8801e5fc..fc6d9035 100644 --- a/BUILD.md +++ b/BUILD.md @@ -40,7 +40,7 @@ Install the required packages from your package manager. ```bash # runtime dependencies -sudo apt install ffmpeg libsdl2-2.0-0 +sudo apt install ffmpeg libsdl2-2.0-0 adb # client build dependencies sudo apt install gcc git pkg-config meson ninja-build \ From 3100533e56b2efdfc6f07b96681a7eef25ff63e0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 3 Dec 2019 21:10:43 +0100 Subject: [PATCH 0148/2244] Fix "natural scrolling" > Movements down (scroll backward) generate negative y values and up > (scroll forward) generate positive y values. > If direction is SDL_MOUSEWHEEL_FLIPPED the values in x and y will be > opposite. Multiply by -1 to change them back. The x and y values already take the scrolling configuration into account. Reversing the values when the direction is flipped cancels the scrolling configuration. Therefore, just ignore the direction field. Fixes --- app/src/input_manager.c | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 013fb640..60879005 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -550,13 +550,8 @@ convert_mouse_wheel(const SDL_MouseWheelEvent *from, struct screen *screen, to->type = CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT; to->inject_scroll_event.position = position; - - int mul = from->direction == SDL_MOUSEWHEEL_NORMAL ? 1 : -1; - // SDL behavior seems inconsistent between horizontal and vertical scrolling - // so reverse the horizontal - // - to->inject_scroll_event.hscroll = -mul * from->x; - to->inject_scroll_event.vscroll = mul * from->y; + to->inject_scroll_event.hscroll = from->x; + to->inject_scroll_event.vscroll = from->y; return true; } From 5eeaed09aed64b088cbc7cf946923aeea92e4f67 Mon Sep 17 00:00:00 2001 From: Yu-Chen Lin Date: Sat, 30 Nov 2019 12:15:58 +0800 Subject: [PATCH 0149/2244] Add test_strquote Signed-off-by: Yu-Chen Lin --- app/tests/test_strutil.c | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/app/tests/test_strutil.c b/app/tests/test_strutil.c index 64e62ed1..1489b1bd 100644 --- a/app/tests/test_strutil.c +++ b/app/tests/test_strutil.c @@ -1,5 +1,6 @@ #include #include +#include #include "util/str_util.h" @@ -126,6 +127,16 @@ static void test_xstrjoin_truncated_after_sep(void) { assert(!strcmp("abc de ", s)); } +static void test_strquote(void) { + const char *s = "abcde"; + char *out = strquote(s); + + // add '"' at the beginning and the end + assert(!strcmp("\"abcde\"", out)); + + SDL_free(out); +} + static void test_utf8_truncate(void) { const char *s = "aÉbÔc"; assert(strlen(s) == 7); // É and Ô are 2 bytes-wide @@ -166,6 +177,7 @@ int main(void) { test_xstrjoin_truncated_in_token(); test_xstrjoin_truncated_before_sep(); test_xstrjoin_truncated_after_sep(); + test_strquote(); test_utf8_truncate(); return 0; } From b2bf25c52ca08d8de812186ab7651128e9989cb3 Mon Sep 17 00:00:00 2001 From: Yu-Chen Lin Date: Sat, 30 Nov 2019 12:33:00 +0800 Subject: [PATCH 0150/2244] Add test_buffer_util Signed-off-by: Yu-Chen Lin --- app/meson.build | 3 ++ app/tests/test_buffer_util.c | 76 ++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 app/tests/test_buffer_util.c diff --git a/app/meson.build b/app/meson.build index 843bfd9d..332407c1 100644 --- a/app/meson.build +++ b/app/meson.build @@ -141,6 +141,9 @@ install_man('scrcpy.1') ### TESTS tests = [ + ['test_buffer_util', [ + 'tests/test_buffer_util.c' + ]], ['test_cbuf', [ 'tests/test_cbuf.c', ]], diff --git a/app/tests/test_buffer_util.c b/app/tests/test_buffer_util.c new file mode 100644 index 00000000..ba3f9f06 --- /dev/null +++ b/app/tests/test_buffer_util.c @@ -0,0 +1,76 @@ +#include + +#include "util/buffer_util.h" + +static void test_buffer_write16be(void) { + uint16_t val = 0xABCD; + uint8_t buf[2]; + + buffer_write16be(buf, val); + + assert(buf[0] == 0xAB); + assert(buf[1] == 0xCD); +} + +static void test_buffer_write32be(void) { + uint32_t val = 0xABCD1234; + uint8_t buf[4]; + + buffer_write32be(buf, val); + + assert(buf[0] == 0xAB); + assert(buf[1] == 0xCD); + assert(buf[2] == 0x12); + assert(buf[3] == 0x34); +} + +static void test_buffer_write64be(void) { + uint64_t val = 0xABCD1234567890EF; + uint8_t buf[8]; + + buffer_write64be(buf, val); + + assert(buf[0] == 0xAB); + assert(buf[1] == 0xCD); + assert(buf[2] == 0x12); + assert(buf[3] == 0x34); + assert(buf[4] == 0x56); + assert(buf[5] == 0x78); + assert(buf[6] == 0x90); + assert(buf[7] == 0xEF); +} + +static void test_buffer_read16be(void) { + uint8_t buf[2] = {0xAB, 0xCD}; + + uint16_t val = buffer_read16be(buf); + + assert(val == 0xABCD); +} + +static void test_buffer_read32be(void) { + uint8_t buf[4] = {0xAB, 0xCD, 0x12, 0x34}; + + uint32_t val = buffer_read32be(buf); + + assert(val == 0xABCD1234); +} + +static void test_buffer_read64be(void) { + uint8_t buf[8] = {0xAB, 0xCD, 0x12, 0x34, + 0x56, 0x78, 0x90, 0xEF}; + + uint64_t val = buffer_read64be(buf); + + assert(val == 0xABCD1234567890EF); +} + +int main(void) { + test_buffer_write16be(); + test_buffer_write32be(); + test_buffer_write64be(); + test_buffer_read16be(); + test_buffer_read32be(); + test_buffer_read64be(); + return 0; +} From fbc86a616c38ea91beee586c417ea405ea6be56a Mon Sep 17 00:00:00 2001 From: Yu-Chen Lin Date: Sat, 30 Nov 2019 12:33:40 +0800 Subject: [PATCH 0151/2244] Correct coding style Signed-off-by: Yu-Chen Lin --- app/src/util/buffer_util.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/util/buffer_util.h b/app/src/util/buffer_util.h index 262df1dc..17234e42 100644 --- a/app/src/util/buffer_util.h +++ b/app/src/util/buffer_util.h @@ -36,8 +36,8 @@ buffer_read32be(const uint8_t *buf) { return (buf[0] << 24) | (buf[1] << 16) | (buf[2] << 8) | buf[3]; } -static inline -uint64_t buffer_read64be(const uint8_t *buf) { +static inline uint64_t +buffer_read64be(const uint8_t *buf) { uint32_t msb = buffer_read32be(buf); uint32_t lsb = buffer_read32be(&buf[4]); return ((uint64_t) msb << 32) | lsb; From 525d6d4a758a9df0baac2c9a0d4dcf6514e6c8e3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 4 Dec 2019 19:31:24 +0100 Subject: [PATCH 0152/2244] Try new methods before legacy ones Use the legacy methods when the new ones do not exist. --- .../com/genymobile/scrcpy/wrappers/WindowManager.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) 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 52096461..b0e44278 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java @@ -14,11 +14,12 @@ public final class WindowManager { try { Class cls = manager.getClass(); try { - return (Integer) cls.getMethod("getRotation").invoke(manager); - } catch (NoSuchMethodException e) { // method changed since this commit: // https://android.googlesource.com/platform/frameworks/base/+/8ee7285128c3843401d4c4d0412cd66e86ba49e3%5E%21/#F2 return (Integer) cls.getMethod("getDefaultDisplayRotation").invoke(manager); + } catch (NoSuchMethodException e) { + // old version + return (Integer) cls.getMethod("getRotation").invoke(manager); } } catch (Exception e) { throw new AssertionError(e); @@ -29,11 +30,12 @@ public final class WindowManager { try { Class cls = manager.getClass(); try { - cls.getMethod("watchRotation", IRotationWatcher.class).invoke(manager, rotationWatcher); - } catch (NoSuchMethodException e) { // 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); + } catch (NoSuchMethodException e) { + // old version + cls.getMethod("watchRotation", IRotationWatcher.class).invoke(manager, rotationWatcher); } } catch (Exception e) { throw new AssertionError(e); From bdd05b4a16a2b0fc15d2ecf8f832afb9670854ee Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 27 Nov 2019 22:05:25 +0100 Subject: [PATCH 0153/2244] Refactor wrappers for Android SDK classes Internally, a failure to invoke a method via reflection was partially managed using exceptions, partially using a null return value. Handle all errors at the same place, by not catching NoSuchMethodException too early. --- .../scrcpy/wrappers/ClipboardManager.java | 48 +++++++------------ .../scrcpy/wrappers/InputManager.java | 19 +++----- .../scrcpy/wrappers/PowerManager.java | 23 ++++----- .../scrcpy/wrappers/StatusBarManager.java | 34 ++++--------- .../scrcpy/wrappers/SurfaceControl.java | 45 +++++++---------- 5 files changed, 57 insertions(+), 112 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 ebd89238..592bdf6b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java @@ -22,31 +22,23 @@ public class ClipboardManager { this.manager = manager; } - private Method getGetPrimaryClipMethod() { + private Method getGetPrimaryClipMethod() throws NoSuchMethodException { if (getPrimaryClipMethod == null) { - try { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { - getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class); - } else { - getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class); - } - } catch (NoSuchMethodException e) { - Ln.e("Could not find method", e); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class); + } else { + getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class); } } return getPrimaryClipMethod; } - private Method getSetPrimaryClipMethod() { + private Method getSetPrimaryClipMethod() throws NoSuchMethodException { if (setPrimaryClipMethod == null) { - try { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { - setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class); - } else { - setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, int.class); - } - } catch (NoSuchMethodException e) { - Ln.e("Could not find method", e); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class); + } else { + setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, int.class); } } return setPrimaryClipMethod; @@ -69,32 +61,26 @@ public class ClipboardManager { } public CharSequence getText() { - Method method = getGetPrimaryClipMethod(); - if (method == null) { - return null; - } try { + Method method = getGetPrimaryClipMethod(); ClipData clipData = getPrimaryClip(method, manager); if (clipData == null || clipData.getItemCount() == 0) { return null; } return clipData.getItemAt(0).getText(); - } catch (InvocationTargetException | IllegalAccessException e) { - Ln.e("Could not invoke " + method.getName(), e); + } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + Ln.e("Could not invoke method", e); return null; } } public void setText(CharSequence text) { - Method method = getSetPrimaryClipMethod(); - if (method == null) { - return; - } - ClipData clipData = ClipData.newPlainText(null, text); try { + Method method = getSetPrimaryClipMethod(); + ClipData clipData = ClipData.newPlainText(null, text); setPrimaryClip(method, manager, clipData); - } catch (InvocationTargetException | IllegalAccessException e) { - Ln.e("Could not invoke " + method.getName(), e); + } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + Ln.e("Could not invoke method", 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 788a04c7..44fa613b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java @@ -21,26 +21,19 @@ public final class InputManager { this.manager = manager; } - private Method getInjectInputEventMethod() { + private Method getInjectInputEventMethod() throws NoSuchMethodException { if (injectInputEventMethod == null) { - try { - injectInputEventMethod = manager.getClass().getMethod("injectInputEvent", InputEvent.class, int.class); - } catch (NoSuchMethodException e) { - Ln.e("Could not find method", e); - } + injectInputEventMethod = manager.getClass().getMethod("injectInputEvent", InputEvent.class, int.class); } return injectInputEventMethod; } public boolean injectInputEvent(InputEvent inputEvent, int mode) { - Method method = getInjectInputEventMethod(); - if (method == null) { - return false; - } try { - return (Boolean) method.invoke(manager, inputEvent, mode); - } catch (InvocationTargetException | IllegalAccessException e) { - Ln.e("Could not invoke " + method.getName(), e); + Method method = getInjectInputEventMethod(); + return (boolean) method.invoke(manager, inputEvent, mode); + } 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/PowerManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java index 66acdba8..8ff074b3 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java @@ -17,28 +17,21 @@ public final class PowerManager { this.manager = manager; } - private Method getIsScreenOnMethod() { + private Method getIsScreenOnMethod() throws NoSuchMethodException { if (isScreenOnMethod == null) { - try { - @SuppressLint("ObsoleteSdkInt") // we may lower minSdkVersion in the future - String methodName = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH ? "isInteractive" : "isScreenOn"; - isScreenOnMethod = manager.getClass().getMethod(methodName); - } catch (NoSuchMethodException e) { - Ln.e("Could not find method", e); - } + @SuppressLint("ObsoleteSdkInt") // we may lower minSdkVersion in the future + String methodName = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH ? "isInteractive" : "isScreenOn"; + isScreenOnMethod = manager.getClass().getMethod(methodName); } return isScreenOnMethod; } public boolean isScreenOn() { - Method method = getIsScreenOnMethod(); - if (method == null) { - return false; - } try { - return (Boolean) method.invoke(manager); - } catch (InvocationTargetException | IllegalAccessException e) { - Ln.e("Could not invoke " + method.getName(), e); + Method method = getIsScreenOnMethod(); + return (boolean) method.invoke(manager); + } 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/StatusBarManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java index 670de952..6f8941bd 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java @@ -17,49 +17,35 @@ public class StatusBarManager { this.manager = manager; } - private Method getExpandNotificationsPanelMethod() { + private Method getExpandNotificationsPanelMethod() throws NoSuchMethodException { if (expandNotificationsPanelMethod == null) { - try { - expandNotificationsPanelMethod = manager.getClass().getMethod("expandNotificationsPanel"); - } catch (NoSuchMethodException e) { - Ln.e("Could not find method", e); - } + expandNotificationsPanelMethod = manager.getClass().getMethod("expandNotificationsPanel"); } return expandNotificationsPanelMethod; } - private Method getCollapsePanelsMethod() { + private Method getCollapsePanelsMethod() throws NoSuchMethodException { if (collapsePanelsMethod == null) { - try { - collapsePanelsMethod = manager.getClass().getMethod("collapsePanels"); - } catch (NoSuchMethodException e) { - Ln.e("Could not find method", e); - } + collapsePanelsMethod = manager.getClass().getMethod("collapsePanels"); } return collapsePanelsMethod; } public void expandNotificationsPanel() { - Method method = getExpandNotificationsPanelMethod(); - if (method == null) { - return; - } try { + Method method = getExpandNotificationsPanelMethod(); method.invoke(manager); - } catch (InvocationTargetException | IllegalAccessException e) { - Ln.e("Could not invoke " + method.getName(), e); + } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + Ln.e("Could not invoke method", e); } } public void collapsePanels() { - Method method = getCollapsePanelsMethod(); - if (method == null) { - return; - } try { + Method method = getCollapsePanelsMethod(); method.invoke(manager); - } catch (InvocationTargetException | IllegalAccessException e) { - Ln.e("Could not invoke " + method.getName(), e); + } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + Ln.e("Could not invoke method", e); } } } 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 bef6e5d9..227bbc85 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java @@ -84,29 +84,23 @@ public final class SurfaceControl { } } - private static Method getGetBuiltInDisplayMethod() { + private static Method getGetBuiltInDisplayMethod() throws NoSuchMethodException { if (getBuiltInDisplayMethod == null) { - try { - // the method signature has changed in Android Q - // - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { - getBuiltInDisplayMethod = CLASS.getMethod("getBuiltInDisplay", int.class); - } else { - getBuiltInDisplayMethod = CLASS.getMethod("getInternalDisplayToken"); - } - } catch (NoSuchMethodException e) { - Ln.e("Could not find method", e); + // the method signature has changed in Android Q + // + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + getBuiltInDisplayMethod = CLASS.getMethod("getBuiltInDisplay", int.class); + } else { + getBuiltInDisplayMethod = CLASS.getMethod("getInternalDisplayToken"); } } return getBuiltInDisplayMethod; } public static IBinder getBuiltInDisplay() { - Method method = getGetBuiltInDisplayMethod(); - if (method == null) { - return null; - } + try { + Method method = getGetBuiltInDisplayMethod(); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { // call getBuiltInDisplay(0) return (IBinder) method.invoke(null, 0); @@ -114,32 +108,25 @@ public final class SurfaceControl { // call getInternalDisplayToken() return (IBinder) method.invoke(null); - } catch (InvocationTargetException | IllegalAccessException e) { - Ln.e("Could not invoke " + method.getName(), e); + } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + Ln.e("Could not invoke method", e); return null; } } - private static Method getSetDisplayPowerModeMethod() { + private static Method getSetDisplayPowerModeMethod() throws NoSuchMethodException { if (setDisplayPowerModeMethod == null) { - try { - setDisplayPowerModeMethod = CLASS.getMethod("setDisplayPowerMode", IBinder.class, int.class); - } catch (NoSuchMethodException e) { - Ln.e("Could not find method", e); - } + setDisplayPowerModeMethod = CLASS.getMethod("setDisplayPowerMode", IBinder.class, int.class); } return setDisplayPowerModeMethod; } public static void setDisplayPowerMode(IBinder displayToken, int mode) { - Method method = getSetDisplayPowerModeMethod(); - if (method == null) { - return; - } try { + Method method = getSetDisplayPowerModeMethod(); method.invoke(null, displayToken, mode); - } catch (InvocationTargetException | IllegalAccessException e) { - Ln.e("Could not invoke " + method.getName(), e); + } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + Ln.e("Could not invoke method", e); } } From eb0f339271862af38693913e5ce1d4078d2c9e56 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 4 Dec 2019 19:55:28 +0100 Subject: [PATCH 0154/2244] Add shortcut to rotate screen On Ctrl+r, disable auto-rotation (if enabled), set the screen rotation and re-enable auto-rotation (if it was enabled). Closes #11 --- README.md | 1 + app/scrcpy.1 | 4 + app/src/control_msg.c | 1 + app/src/control_msg.h | 1 + app/src/input_manager.c | 15 ++++ app/src/main.c | 3 + app/tests/test_control_msg_serialize.c | 16 ++++ .../com/genymobile/scrcpy/ControlMessage.java | 1 + .../scrcpy/ControlMessageReader.java | 1 + .../com/genymobile/scrcpy/Controller.java | 3 + .../java/com/genymobile/scrcpy/Device.java | 22 ++++++ .../scrcpy/wrappers/WindowManager.java | 79 +++++++++++++++++-- .../scrcpy/ControlMessageReaderTest.java | 16 ++++ 13 files changed, 157 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 677e7a1c..2e95cb03 100644 --- a/README.md +++ b/README.md @@ -425,6 +425,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` + | 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` diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 6cb062b5..47fd767a 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -195,6 +195,10 @@ turn screen on .B Ctrl+o turn device screen off (keep mirroring) +.TP +.B Ctrl+r +rotate device screen + .TP .B Ctrl+n expand notification panel diff --git a/app/src/control_msg.c b/app/src/control_msg.c index fda16025..45113139 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -78,6 +78,7 @@ control_msg_serialize(const struct control_msg *msg, unsigned char *buf) { case CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL: case CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL: case CONTROL_MSG_TYPE_GET_CLIPBOARD: + case CONTROL_MSG_TYPE_ROTATE_DEVICE: // no additional data return 1; default: diff --git a/app/src/control_msg.h b/app/src/control_msg.h index 2f319d9d..49a159a6 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -28,6 +28,7 @@ enum control_msg_type { CONTROL_MSG_TYPE_GET_CLIPBOARD, CONTROL_MSG_TYPE_SET_CLIPBOARD, CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE, + CONTROL_MSG_TYPE_ROTATE_DEVICE, }; enum screen_power_mode { diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 60879005..8c4c230a 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -211,6 +211,16 @@ clipboard_paste(struct controller *controller) { } } +static void +rotate_device(struct controller *controller) { + struct control_msg msg; + msg.type = CONTROL_MSG_TYPE_ROTATE_DEVICE; + + if (!controller_push_msg(controller, &msg)) { + LOGW("Could not request device rotation"); + } +} + void input_manager_process_text_input(struct input_manager *im, const SDL_TextInputEvent *event) { @@ -388,6 +398,11 @@ input_manager_process_key(struct input_manager *im, } } return; + case SDLK_r: + if (control && cmd && !shift && !repeat && down) { + rotate_device(controller); + } + return; } return; diff --git a/app/src/main.c b/app/src/main.c index 78bdcfd2..e9a2b9aa 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -175,6 +175,9 @@ static void usage(const char *arg0) { " " CTRL_OR_CMD "+o\n" " turn device screen off (keep mirroring)\n" "\n" + " " CTRL_OR_CMD "+r\n" + " rotate device screen\n" + "\n" " " CTRL_OR_CMD "+n\n" " expand notification panel\n" "\n" diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c index 83ab011f..d6f556f3 100644 --- a/app/tests/test_control_msg_serialize.c +++ b/app/tests/test_control_msg_serialize.c @@ -236,6 +236,21 @@ static void test_serialize_set_screen_power_mode(void) { assert(!memcmp(buf, expected, sizeof(expected))); } +static void test_serialize_rotate_device(void) { + struct control_msg msg = { + .type = CONTROL_MSG_TYPE_ROTATE_DEVICE, + }; + + unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; + int size = control_msg_serialize(&msg, buf); + assert(size == 1); + + const unsigned char expected[] = { + CONTROL_MSG_TYPE_ROTATE_DEVICE, + }; + assert(!memcmp(buf, expected, sizeof(expected))); +} + int main(void) { test_serialize_inject_keycode(); test_serialize_inject_text(); @@ -248,5 +263,6 @@ int main(void) { test_serialize_get_clipboard(); test_serialize_set_clipboard(); test_serialize_set_screen_power_mode(); + test_serialize_rotate_device(); return 0; } diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java index 615773fb..195b04bf 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java @@ -15,6 +15,7 @@ public final class ControlMessage { public static final int TYPE_GET_CLIPBOARD = 7; public static final int TYPE_SET_CLIPBOARD = 8; public static final int TYPE_SET_SCREEN_POWER_MODE = 9; + public static final int TYPE_ROTATE_DEVICE = 10; private int type; private String text; diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java index 2f8b5177..726b5659 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java @@ -76,6 +76,7 @@ public class ControlMessageReader { case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL: case ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL: case ControlMessage.TYPE_GET_CLIPBOARD: + case ControlMessage.TYPE_ROTATE_DEVICE: msg = ControlMessage.createEmpty(type); break; default: diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index 51b13627..dc0fa67b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -106,6 +106,9 @@ public class Controller { case ControlMessage.TYPE_SET_SCREEN_POWER_MODE: device.setScreenPowerMode(msg.getAction()); break; + case ControlMessage.TYPE_ROTATE_DEVICE: + device.rotateDevice(); + break; default: // do nothing } diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index 708b9516..9448098a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -2,6 +2,7 @@ package com.genymobile.scrcpy; import com.genymobile.scrcpy.wrappers.ServiceManager; import com.genymobile.scrcpy.wrappers.SurfaceControl; +import com.genymobile.scrcpy.wrappers.WindowManager; import android.graphics.Rect; import android.os.Build; @@ -170,6 +171,27 @@ public final class Device { Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on")); } + /** + * Disable auto-rotation (if enabled), set the screen rotation and re-enable auto-rotation (if it was enabled). + */ + public void rotateDevice() { + WindowManager wm = serviceManager.getWindowManager(); + + boolean accelerometerRotation = !wm.isRotationFrozen(); + + int currentRotation = wm.getRotation(); + int newRotation = (currentRotation & 1) ^ 1; // 0->1, 1->0, 2->1, 3->0 + String newRotationString = newRotation == 0 ? "portrait" : "landscape"; + + Ln.i("Device rotation requested: " + newRotationString); + wm.freezeRotation(newRotation); + + // restore auto-rotate if necessary + if (accelerometerRotation) { + 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/wrappers/WindowManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java index b0e44278..cc687cd5 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java @@ -1,28 +1,95 @@ package com.genymobile.scrcpy.wrappers; +import com.genymobile.scrcpy.Ln; + import android.os.IInterface; import android.view.IRotationWatcher; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + public final class WindowManager { private final IInterface manager; + private Method getRotationMethod; + private Method freezeRotationMethod; + private Method isRotationFrozenMethod; + private Method thawRotationMethod; public WindowManager(IInterface manager) { this.manager = manager; } - public int getRotation() { - try { + private Method getGetRotationMethod() throws NoSuchMethodException { + if (getRotationMethod == null) { Class cls = manager.getClass(); try { // method changed since this commit: // https://android.googlesource.com/platform/frameworks/base/+/8ee7285128c3843401d4c4d0412cd66e86ba49e3%5E%21/#F2 - return (Integer) cls.getMethod("getDefaultDisplayRotation").invoke(manager); + getRotationMethod = cls.getMethod("getDefaultDisplayRotation"); } catch (NoSuchMethodException e) { // old version - return (Integer) cls.getMethod("getRotation").invoke(manager); + getRotationMethod = cls.getMethod("getRotation"); } - } catch (Exception e) { - throw new AssertionError(e); + } + return getRotationMethod; + } + + private Method getFreezeRotationMethod() throws NoSuchMethodException { + if (freezeRotationMethod == null) { + freezeRotationMethod = manager.getClass().getMethod("freezeRotation", int.class); + } + return freezeRotationMethod; + } + + private Method getIsRotationFrozenMethod() throws NoSuchMethodException { + if (isRotationFrozenMethod == null) { + isRotationFrozenMethod = manager.getClass().getMethod("isRotationFrozen"); + } + return isRotationFrozenMethod; + } + + private Method getThawRotationMethod() throws NoSuchMethodException { + if (thawRotationMethod == null) { + thawRotationMethod = manager.getClass().getMethod("thawRotation"); + } + return thawRotationMethod; + } + + public int getRotation() { + try { + Method method = getGetRotationMethod(); + return (int) method.invoke(manager); + } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + Ln.e("Could not invoke method", e); + return 0; + } + } + + public void freezeRotation(int rotation) { + try { + Method method = getFreezeRotationMethod(); + method.invoke(manager, rotation); + } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + Ln.e("Could not invoke method", e); + } + } + + public boolean isRotationFrozen() { + try { + Method method = getIsRotationFrozenMethod(); + return (boolean) method.invoke(manager); + } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + Ln.e("Could not invoke method", e); + return false; + } + } + + public void thawRotation() { + try { + Method method = getThawRotationMethod(); + method.invoke(manager); + } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + Ln.e("Could not invoke method", e); } } diff --git a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java index ede759dc..5e663bb9 100644 --- a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java @@ -240,6 +240,22 @@ public class ControlMessageReaderTest { Assert.assertEquals(Device.POWER_MODE_NORMAL, event.getAction()); } + @Test + public void testParseRotateDevice() throws IOException { + ControlMessageReader reader = new ControlMessageReader(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(ControlMessage.TYPE_ROTATE_DEVICE); + + byte[] packet = bos.toByteArray(); + + reader.readFrom(new ByteArrayInputStream(packet)); + ControlMessage event = reader.next(); + + Assert.assertEquals(ControlMessage.TYPE_ROTATE_DEVICE, event.getType()); + } + @Test public void testMultiEvents() throws IOException { ControlMessageReader reader = new ControlMessageReader(); From e0b117de137685b9073e72f8c373f5157df6265f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 5 Dec 2019 21:05:45 +0100 Subject: [PATCH 0155/2244] Fix checkstyle warning --- 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 87aeacac..56b738fb 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -135,6 +135,7 @@ 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) { From 3259c60b22598379e86fb4e20352f95f3282d0af Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 5 Dec 2019 21:06:49 +0100 Subject: [PATCH 0156/2244] Fix test compilation on mingw Including SDL2/SDL.h redefines main to SDL_main by default. --- app/tests/test_strutil.c | 1 + 1 file changed, 1 insertion(+) diff --git a/app/tests/test_strutil.c b/app/tests/test_strutil.c index 1489b1bd..ddc8399b 100644 --- a/app/tests/test_strutil.c +++ b/app/tests/test_strutil.c @@ -1,5 +1,6 @@ #include #include +#define SDL_MAIN_HANDLED // avoid to redefine main to SDL_main #include #include "util/str_util.h" From 64bcac91575ba01dda2c9a2178d65f17daa3a3ab Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 5 Dec 2019 21:07:11 +0100 Subject: [PATCH 0157/2244] Refuse to push a non-regular file server If SCRCPY_SERVER_PATH points to a directory, then a directory will be pushed to /data/local/tmp/scrcpy-server.jar. When executing it, app_process will just abort and leave the directory on the device, causing scrcpy to always fail. To avoid the problem, check that the server is a regular file before pushing it. Closes #956 --- app/src/command.c | 14 ++++++++++++++ app/src/command.h | 4 ++++ app/src/server.c | 7 ++++++- 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/app/src/command.c b/app/src/command.c index abaa223d..63afccb4 100644 --- a/app/src/command.c +++ b/app/src/command.c @@ -4,6 +4,9 @@ #include #include #include +#include +#include +#include #include "config.h" #include "common.h" @@ -202,3 +205,14 @@ 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/command.h b/app/src/command.h index edbc3fb8..9fc81c1c 100644 --- a/app/src/command.h +++ b/app/src/command.h @@ -85,4 +85,8 @@ process_check_success(process_t proc, const char *name); char * get_executable_path(void); +// returns true if the file exists and is not a directory +bool +is_regular_file(const char *path); + #endif diff --git a/app/src/server.c b/app/src/server.c index 90eb4c69..ff167aeb 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -67,7 +67,12 @@ get_server_path(void) { static bool push_server(const char *serial) { - process_t process = adb_push(serial, get_server_path(), DEVICE_SERVER_PATH); + const char *server_path = get_server_path(); + if (!is_regular_file(server_path)) { + LOGE("'%s' does not exist or is not a regular file\n", server_path); + return false; + } + process_t process = adb_push(serial, server_path, DEVICE_SERVER_PATH); return process_check_success(process, "adb push"); } From 61274a7cdbab8b921171c4d2fa5b8c3c75bde311 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 7 Dec 2019 11:01:55 +0100 Subject: [PATCH 0158/2244] Factorize integer argument parsing Add util functions for integer parsing (with tests), and factorize integer argument parsing to avoid code duplication. --- app/src/main.c | 138 ++++++++++++++------------------------- app/src/util/str_util.c | 55 ++++++++++++++++ app/src/util/str_util.h | 13 ++++ app/tests/test_strutil.c | 71 ++++++++++++++++++++ 4 files changed, 189 insertions(+), 88 deletions(-) diff --git a/app/src/main.c b/app/src/main.c index e9a2b9aa..53ef253e 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -12,6 +12,7 @@ #include "compat.h" #include "recorder.h" #include "util/log.h" +#include "util/str_util.h" struct args { struct scrcpy_options opts; @@ -224,51 +225,47 @@ print_version(void) { } static bool -parse_bit_rate(char *optarg, uint32_t *bit_rate) { - char *endptr; - if (*optarg == '\0') { - LOGE("Bit-rate parameter is empty"); - return false; +parse_integer_arg(const char *s, long *out, bool accept_suffix, long min, + long max, const char *name) { + long value; + bool ok; + if (accept_suffix) { + ok = parse_integer_with_suffix(s, &value); + } else { + ok = parse_integer(s, &value); } - long value = strtol(optarg, &endptr, 0); - int mul = 1; - if (*endptr != '\0') { - if (optarg == endptr) { - LOGE("Invalid bit-rate: %s", optarg); - return false; - } - if ((*endptr == 'M' || *endptr == 'm') && endptr[1] == '\0') { - mul = 1000000; - } else if ((*endptr == 'K' || *endptr == 'k') && endptr[1] == '\0') { - mul = 1000; - } else { - LOGE("Invalid bit-rate unit: %s", optarg); - return false; - } - } - if (value < 0 || ((uint32_t) -1) / mul < (unsigned long) value) { - LOGE("Bitrate must be positive and less than 2^32: %s", optarg); + if (!ok) { + LOGE("Could not parse %s: %s", name, s); return false; } - *bit_rate = (uint32_t) value * mul; + if (value < min || value > max) { + LOGE("Could not parse %s: value (%ld) out-of-range (%ld; %ld)", + name, value, min, max); + return false; + } + + *out = value; return true; } static bool -parse_max_size(char *optarg, uint16_t *max_size) { - char *endptr; - if (*optarg == '\0') { - LOGE("Max size parameter is empty"); +parse_bit_rate(const char *s, uint32_t *bit_rate) { + long value; + bool ok = parse_integer_arg(s, &value, true, 0, 0xFFFF, "bit-rate"); + if (!ok) { return false; } - long value = strtol(optarg, &endptr, 0); - if (*endptr != '\0') { - LOGE("Invalid max size: %s", optarg); - return false; - } - if (value & ~0xffff) { - LOGE("Max size must be between 0 and 65535: %ld", value); + + *bit_rate = (uint32_t) value; + return true; +} + +static bool +parse_max_size(char *s, uint16_t *max_size) { + long value; + bool ok = parse_integer_arg(s, &value, false, 0, 0xFFFF, "max size"); + if (!ok) { return false; } @@ -277,20 +274,10 @@ parse_max_size(char *optarg, uint16_t *max_size) { } static bool -parse_max_fps(const char *optarg, uint16_t *max_fps) { - char *endptr; - if (*optarg == '\0') { - LOGE("Max FPS parameter is empty"); - return false; - } - long value = strtol(optarg, &endptr, 0); - if (*endptr != '\0') { - LOGE("Invalid max FPS: %s", optarg); - return false; - } - if (value & ~0xffff) { - // in practice, it should not be higher than 60 - LOGE("Max FPS value is invalid: %ld", value); +parse_max_fps(const char *s, uint16_t *max_fps) { + long value; + bool ok = parse_integer_arg(s, &value, false, 0, 1000, "max fps"); + if (!ok) { return false; } @@ -299,19 +286,11 @@ parse_max_fps(const char *optarg, uint16_t *max_fps) { } static bool -parse_window_position(char *optarg, int16_t *position) { - char *endptr; - if (*optarg == '\0') { - LOGE("Window position parameter is empty"); - return false; - } - long value = strtol(optarg, &endptr, 0); - if (*endptr != '\0') { - LOGE("Invalid window position: %s", optarg); - return false; - } - if (value < -1 || value > 0x7fff) { - LOGE("Window position must be between -1 and 32767: %ld", value); +parse_window_position(char *s, int16_t *position) { + long value; + bool ok = parse_integer_arg(s, &value, false, -1, 0x7FFF, + "window position"); + if (!ok) { return false; } @@ -320,19 +299,11 @@ parse_window_position(char *optarg, int16_t *position) { } static bool -parse_window_dimension(char *optarg, uint16_t *dimension) { - char *endptr; - if (*optarg == '\0') { - LOGE("Window dimension parameter is empty"); - return false; - } - long value = strtol(optarg, &endptr, 0); - if (*endptr != '\0') { - LOGE("Invalid window dimension: %s", optarg); - return false; - } - if (value & ~0xffff) { - LOGE("Window position must be between 0 and 65535: %ld", value); +parse_window_dimension(char *s, uint16_t *dimension) { + long value; + bool ok = parse_integer_arg(s, &value, false, 0, 0xFFFF, + "window dimension"); + if (!ok) { return false; } @@ -341,19 +312,10 @@ parse_window_dimension(char *optarg, uint16_t *dimension) { } static bool -parse_port(char *optarg, uint16_t *port) { - char *endptr; - if (*optarg == '\0') { - LOGE("Port parameter is empty"); - return false; - } - long value = strtol(optarg, &endptr, 0); - if (*endptr != '\0') { - LOGE("Invalid port: %s", optarg); - return false; - } - if (value & ~0xffff) { - LOGE("Port out of range: %ld", value); +parse_port(char *s, uint16_t *port) { + long value; + bool ok = parse_integer_arg(s, &value, false, 0, 0xFFFF, "port"); + if (!ok) { return false; } diff --git a/app/src/util/str_util.c b/app/src/util/str_util.c index 15378d8a..4d175407 100644 --- a/app/src/util/str_util.c +++ b/app/src/util/str_util.c @@ -1,5 +1,7 @@ #include "str_util.h" +#include +#include #include #include @@ -60,6 +62,59 @@ strquote(const char *src) { return quoted; } +bool +parse_integer(const char *s, long *out) { + char *endptr; + if (*s == '\0') { + return false; + } + errno = 0; + long value = strtol(s, &endptr, 0); + if (errno == ERANGE) { + return false; + } + if (*endptr != '\0') { + return false; + } + + *out = value; + return true; +} + +bool +parse_integer_with_suffix(const char *s, long *out) { + char *endptr; + if (*s == '\0') { + return false; + } + errno = 0; + long value = strtol(s, &endptr, 0); + if (errno == ERANGE) { + return false; + } + int mul = 1; + if (*endptr != '\0') { + if (s == endptr) { + return false; + } + if ((*endptr == 'M' || *endptr == 'm') && endptr[1] == '\0') { + mul = 1000000; + } else if ((*endptr == 'K' || *endptr == 'k') && endptr[1] == '\0') { + mul = 1000; + } else { + return false; + } + } + + if ((value < 0 && LONG_MIN / mul > value) || + (value > 0 && LONG_MAX / mul < value)) { + return false; + } + + *out = value * mul; + return true; +} + size_t utf8_truncation_index(const char *utf8, size_t max_len) { size_t len = strlen(utf8); diff --git a/app/src/util/str_util.h b/app/src/util/str_util.h index 56490190..8d9b990c 100644 --- a/app/src/util/str_util.h +++ b/app/src/util/str_util.h @@ -1,6 +1,7 @@ #ifndef STRUTIL_H #define STRUTIL_H +#include #include #include "config.h" @@ -25,6 +26,18 @@ xstrjoin(char *dst, const char *const tokens[], char sep, size_t n); char * strquote(const char *src); +// parse s as an integer into value +// returns true if the conversion succeeded, false otherwise +bool +parse_integer(const char *s, long *out); + +// parse s as an integer into value +// like parse_integer(), but accept 'k'/'K' (x1000) and 'm'/'M' (x1000000) as +// suffix +// returns true if the conversion succeeded, false otherwise +bool +parse_integer_with_suffix(const char *s, long *out); + // return the index to truncate a UTF-8 string at a valid position size_t utf8_truncation_index(const char *utf8, size_t max_len); diff --git a/app/tests/test_strutil.c b/app/tests/test_strutil.c index ddc8399b..baa2fd38 100644 --- a/app/tests/test_strutil.c +++ b/app/tests/test_strutil.c @@ -1,4 +1,6 @@ #include +#include +#include #include #define SDL_MAIN_HANDLED // avoid to redefine main to SDL_main #include @@ -169,6 +171,73 @@ static void test_utf8_truncate(void) { assert(count == 7); // no more chars } +static void test_parse_integer(void) { + long value; + bool ok = parse_integer("1234", &value); + assert(ok); + assert(value == 1234); + + ok = parse_integer("-1234", &value); + assert(ok); + assert(value == -1234); + + ok = parse_integer("1234k", &value); + assert(!ok); + + ok = parse_integer("123456789876543212345678987654321", &value); + assert(!ok); // out-of-range +} + +static void test_parse_integer_with_suffix(void) { + long value; + bool ok = parse_integer_with_suffix("1234", &value); + assert(ok); + assert(value == 1234); + + ok = parse_integer_with_suffix("-1234", &value); + assert(ok); + assert(value == -1234); + + ok = parse_integer_with_suffix("1234k", &value); + assert(ok); + assert(value == 1234000); + + ok = parse_integer_with_suffix("1234m", &value); + assert(ok); + assert(value == 1234000000); + + ok = parse_integer_with_suffix("-1234k", &value); + assert(ok); + assert(value == -1234000); + + ok = parse_integer_with_suffix("-1234m", &value); + assert(ok); + assert(value == -1234000000); + + ok = parse_integer_with_suffix("123456789876543212345678987654321", &value); + assert(!ok); // out-of-range + + char buf[32]; + + sprintf(buf, "%ldk", LONG_MAX / 2000); + ok = parse_integer_with_suffix(buf, &value); + assert(ok); + assert(value == LONG_MAX / 2000 * 1000); + + sprintf(buf, "%ldm", LONG_MAX / 2000); + ok = parse_integer_with_suffix(buf, &value); + assert(!ok); + + sprintf(buf, "%ldk", LONG_MIN / 2000); + ok = parse_integer_with_suffix(buf, &value); + assert(ok); + assert(value == LONG_MIN / 2000 * 1000); + + sprintf(buf, "%ldm", LONG_MIN / 2000); + ok = parse_integer_with_suffix(buf, &value); + assert(!ok); +} + int main(void) { test_xstrncpy_simple(); test_xstrncpy_just_fit(); @@ -180,5 +249,7 @@ int main(void) { test_xstrjoin_truncated_after_sep(); test_strquote(); test_utf8_truncate(); + test_parse_integer(); + test_parse_integer_with_suffix(); return 0; } From d950383b729f93d4da84e65013b4c78caddf6d68 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 8 Dec 2019 21:35:19 +0100 Subject: [PATCH 0159/2244] Move command-line parsing to a separate file --- app/meson.build | 1 + app/src/cli.c | 525 +++++++++++++++++++++++++++++++++++++++++++++++ app/src/cli.h | 21 ++ app/src/main.c | 531 +----------------------------------------------- 4 files changed, 551 insertions(+), 527 deletions(-) create mode 100644 app/src/cli.c create mode 100644 app/src/cli.h diff --git a/app/meson.build b/app/meson.build index 332407c1..3e8d6063 100644 --- a/app/meson.build +++ b/app/meson.build @@ -1,5 +1,6 @@ src = [ 'src/main.c', + 'src/cli.c', 'src/command.c', 'src/control_msg.c', 'src/controller.c', diff --git a/app/src/cli.c b/app/src/cli.c new file mode 100644 index 00000000..a219895b --- /dev/null +++ b/app/src/cli.c @@ -0,0 +1,525 @@ +#include "cli.h" + +#include +#include +#include + +#include "config.h" +#include "recorder.h" +#include "util/log.h" +#include "util/str_util.h" + +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" + "Options:\n" + "\n" + " --always-on-top\n" + " Make scrcpy window always on top (above other windows).\n" + "\n" + " -b, --bit-rate value\n" + " Encode the video at the given bit-rate, expressed in bits/s.\n" + " Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n" + " Default is %d.\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" + " (typically, portrait for a phone, landscape for a tablet).\n" + " Any --max-size value is computed on the cropped size.\n" + "\n" + " -f, --fullscreen\n" + " Start in fullscreen.\n" + "\n" + " -h, --help\n" + " 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" + "\n" + " -m, --max-size value\n" + " Limit both the width and height of the video to value. The\n" + " other dimension is computed so that the device aspect-ratio\n" + " is preserved.\n" + " Default is %d%s.\n" + "\n" + " -n, --no-control\n" + " Disable device control (mirror the device in read-only).\n" + "\n" + " -N, --no-display\n" + " 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" + "\n" + " --prefer-text\n" + " Inject alpha characters and space as text events instead of\n" + " key events.\n" + " This avoids issues when combining multiple keys to enter a\n" + " special character, but breaks the expected behavior of alpha\n" + " keys in games (typically WASD).\n" + "\n" + " --push-target path\n" + " Set the target directory for pushing files to the device by\n" + " drag & drop. It is passed as-is to \"adb push\".\n" + " Default is \"/sdcard/\".\n" + "\n" + " -r, --record file.mp4\n" + " Record screen to file.\n" + " The format is determined by the --record-format option if\n" + " set, or by the file extension (.mp4 or .mkv).\n" + "\n" + " --record-format format\n" + " Force recording format (either mp4 or mkv).\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" + " This flag forces to render all frames, at a cost of a\n" + " possible increased latency.\n" + "\n" + " -s, --serial serial\n" + " The device serial number. Mandatory only if several devices\n" + " are connected to adb.\n" + "\n" + " -S, --turn-screen-off\n" + " Turn the device screen off immediately.\n" + "\n" + " -t, --show-touches\n" + " Enable \"show touches\" on start, disable on quit.\n" + " It only shows physical touches (not clicks from scrcpy).\n" + "\n" + " -v, --version\n" + " Print the version of scrcpy.\n" + "\n" + " --window-borderless\n" + " Disable window decorations (display borderless window).\n" + "\n" + " --window-title text\n" + " Set a custom window title.\n" + "\n" + " --window-x value\n" + " Set the initial window horizontal position.\n" + " Default is -1 (automatic).\n" + "\n" + " --window-y value\n" + " Set the initial window vertical position.\n" + " Default is -1 (automatic).\n" + "\n" + " --window-width value\n" + " Set the initial window width.\n" + " Default is 0 (automatic).\n" + "\n" + " --window-height value\n" + " Set the initial window width.\n" + " Default is 0 (automatic).\n" + "\n" + "Shortcuts:\n" + "\n" + " " CTRL_OR_CMD "+f\n" + " switch fullscreen mode\n" + "\n" + " " CTRL_OR_CMD "+g\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" + "\n" + " Ctrl+h\n" + " Middle-click\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" + "\n" + " " CTRL_OR_CMD "+s\n" + " click on APP_SWITCH\n" + "\n" + " Ctrl+m\n" + " click on MENU\n" + "\n" + " " CTRL_OR_CMD "+Up\n" + " click on VOLUME_UP\n" + "\n" + " " CTRL_OR_CMD "+Down\n" + " click on VOLUME_DOWN\n" + "\n" + " " CTRL_OR_CMD "+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" + " turn device screen off (keep mirroring)\n" + "\n" + " " CTRL_OR_CMD "+r\n" + " rotate device screen\n" + "\n" + " " CTRL_OR_CMD "+n\n" + " expand notification panel\n" + "\n" + " " CTRL_OR_CMD "+Shift+n\n" + " collapse notification panel\n" + "\n" + " " CTRL_OR_CMD "+c\n" + " copy device clipboard to computer\n" + "\n" + " " CTRL_OR_CMD "+v\n" + " paste computer clipboard to device\n" + "\n" + " " CTRL_OR_CMD "+Shift+v\n" + " copy computer clipboard to device\n" + "\n" + " " CTRL_OR_CMD "+i\n" + " enable/disable FPS counter (print frames/second in logs)\n" + "\n" + " Drag & drop APK file\n" + " install APK from computer\n" + "\n", + arg0, + DEFAULT_BIT_RATE, + DEFAULT_MAX_SIZE, DEFAULT_MAX_SIZE ? "" : " (unlimited)", + DEFAULT_LOCAL_PORT); +} + +static bool +parse_integer_arg(const char *s, long *out, bool accept_suffix, long min, + long max, const char *name) { + long value; + bool ok; + if (accept_suffix) { + ok = parse_integer_with_suffix(s, &value); + } else { + ok = parse_integer(s, &value); + } + if (!ok) { + LOGE("Could not parse %s: %s", name, s); + return false; + } + + if (value < min || value > max) { + LOGE("Could not parse %s: value (%ld) out-of-range (%ld; %ld)", + name, value, min, max); + return false; + } + + *out = value; + return true; +} + +static bool +parse_bit_rate(const char *s, uint32_t *bit_rate) { + long value; + bool ok = parse_integer_arg(s, &value, true, 0, 0xFFFFFFFF, "bit-rate"); + if (!ok) { + return false; + } + + *bit_rate = (uint32_t) value; + return true; +} + +static bool +parse_max_size(char *s, uint16_t *max_size) { + long value; + bool ok = parse_integer_arg(s, &value, false, 0, 0xFFFF, "max size"); + if (!ok) { + return false; + } + + *max_size = (uint16_t) value; + return true; +} + +static bool +parse_max_fps(const char *s, uint16_t *max_fps) { + long value; + bool ok = parse_integer_arg(s, &value, false, 0, 1000, "max fps"); + if (!ok) { + return false; + } + + *max_fps = (uint16_t) value; + return true; +} + +static bool +parse_window_position(char *s, int16_t *position) { + long value; + bool ok = parse_integer_arg(s, &value, false, -1, 0x7FFF, + "window position"); + if (!ok) { + return false; + } + + *position = (int16_t) value; + return true; +} + +static bool +parse_window_dimension(char *s, uint16_t *dimension) { + long value; + bool ok = parse_integer_arg(s, &value, false, 0, 0xFFFF, + "window dimension"); + if (!ok) { + return false; + } + + *dimension = (uint16_t) value; + return true; +} + +static bool +parse_port(char *s, uint16_t *port) { + long value; + bool ok = parse_integer_arg(s, &value, false, 0, 0xFFFF, "port"); + if (!ok) { + return false; + } + + *port = (uint16_t) value; + return true; +} + +static bool +parse_record_format(const char *optarg, enum recorder_format *format) { + if (!strcmp(optarg, "mp4")) { + *format = RECORDER_FORMAT_MP4; + return true; + } + if (!strcmp(optarg, "mkv")) { + *format = RECORDER_FORMAT_MKV; + return true; + } + LOGE("Unsupported format: %s (expected mp4 or mkv)", optarg); + return false; +} + +static enum recorder_format +guess_record_format(const char *filename) { + size_t len = strlen(filename); + if (len < 4) { + return 0; + } + const char *ext = &filename[len - 4]; + if (!strcmp(ext, ".mp4")) { + return RECORDER_FORMAT_MP4; + } + if (!strcmp(ext, ".mkv")) { + return RECORDER_FORMAT_MKV; + } + 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 + +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 }, + }; + + struct scrcpy_options *opts = &args->opts; + + int c; + while ((c = getopt_long(argc, argv, "b:c:fF:hm:nNp:r:s:StTv", long_options, + NULL)) != -1) { + switch (c) { + case 'b': + if (!parse_bit_rate(optarg, &opts->bit_rate)) { + return false; + } + break; + case 'c': + LOGW("Deprecated option -c. Use --crop instead."); + // fall through + case OPT_CROP: + opts->crop = optarg; + break; + case 'f': + opts->fullscreen = true; + break; + case 'F': + LOGW("Deprecated option -F. Use --record-format instead."); + // fall through + case OPT_RECORD_FORMAT: + if (!parse_record_format(optarg, &opts->record_format)) { + return false; + } + break; + case 'h': + args->help = true; + break; + case OPT_MAX_FPS: + if (!parse_max_fps(optarg, &opts->max_fps)) { + return false; + } + break; + case 'm': + if (!parse_max_size(optarg, &opts->max_size)) { + return false; + } + break; + case 'n': + opts->control = false; + break; + case 'N': + opts->display = false; + break; + case 'p': + if (!parse_port(optarg, &opts->port)) { + return false; + } + break; + case 'r': + opts->record_filename = optarg; + break; + case 's': + opts->serial = optarg; + break; + case 'S': + opts->turn_screen_off = true; + break; + case 't': + opts->show_touches = true; + break; + case 'T': + LOGW("Deprecated option -T. Use --always-on-top instead."); + // fall through + case OPT_ALWAYS_ON_TOP: + opts->always_on_top = true; + break; + case 'v': + args->version = true; + break; + case OPT_RENDER_EXPIRED_FRAMES: + opts->render_expired_frames = true; + break; + case OPT_WINDOW_TITLE: + opts->window_title = optarg; + break; + case OPT_WINDOW_X: + if (!parse_window_position(optarg, &opts->window_x)) { + return false; + } + break; + case OPT_WINDOW_Y: + if (!parse_window_position(optarg, &opts->window_y)) { + return false; + } + break; + case OPT_WINDOW_WIDTH: + if (!parse_window_dimension(optarg, &opts->window_width)) { + return false; + } + break; + case OPT_WINDOW_HEIGHT: + if (!parse_window_dimension(optarg, &opts->window_height)) { + return false; + } + break; + case OPT_WINDOW_BORDERLESS: + opts->window_borderless = true; + break; + case OPT_PUSH_TARGET: + opts->push_target = optarg; + break; + case OPT_PREFER_TEXT: + opts->prefer_text = true; + break; + default: + // getopt prints the error message on stderr + return false; + } + } + + if (!opts->display && !opts->record_filename) { + LOGE("-N/--no-display requires screen recording (-r/--record)"); + 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]); + return false; + } + + if (opts->record_format && !opts->record_filename) { + LOGE("Record format specified without recording"); + return false; + } + + if (opts->record_filename && !opts->record_format) { + opts->record_format = guess_record_format(opts->record_filename); + if (!opts->record_format) { + LOGE("No format specified for \"%s\" (try with -F mkv)", + opts->record_filename); + return false; + } + } + + if (!opts->control && opts->turn_screen_off) { + LOGE("Could not request to turn screen off if control is disabled"); + return false; + } + + return true; +} diff --git a/app/src/cli.h b/app/src/cli.h new file mode 100644 index 00000000..2e2bfe93 --- /dev/null +++ b/app/src/cli.h @@ -0,0 +1,21 @@ +#ifndef SCRCPY_CLI_H +#define SCRCPY_CLI_H + +#include + +#include "config.h" +#include "scrcpy.h" + +struct scrcpy_cli_args { + struct scrcpy_options opts; + bool help; + bool version; +}; + +void +scrcpy_print_usage(const char *arg0); + +bool +scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]); + +#endif diff --git a/app/src/main.c b/app/src/main.c index 53ef253e..43e2cc04 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -1,210 +1,15 @@ #include "scrcpy.h" -#include #include -#include #include #include #define SDL_MAIN_HANDLED // avoid link error on Linux Windows Subsystem #include #include "config.h" +#include "cli.h" #include "compat.h" -#include "recorder.h" #include "util/log.h" -#include "util/str_util.h" - -struct args { - struct scrcpy_options opts; - bool help; - bool version; -}; - -static void 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" - "Options:\n" - "\n" - " --always-on-top\n" - " Make scrcpy window always on top (above other windows).\n" - "\n" - " -b, --bit-rate value\n" - " Encode the video at the given bit-rate, expressed in bits/s.\n" - " Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n" - " Default is %d.\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" - " (typically, portrait for a phone, landscape for a tablet).\n" - " Any --max-size value is computed on the cropped size.\n" - "\n" - " -f, --fullscreen\n" - " Start in fullscreen.\n" - "\n" - " -h, --help\n" - " 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" - "\n" - " -m, --max-size value\n" - " Limit both the width and height of the video to value. The\n" - " other dimension is computed so that the device aspect-ratio\n" - " is preserved.\n" - " Default is %d%s.\n" - "\n" - " -n, --no-control\n" - " Disable device control (mirror the device in read-only).\n" - "\n" - " -N, --no-display\n" - " 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" - "\n" - " --prefer-text\n" - " Inject alpha characters and space as text events instead of\n" - " key events.\n" - " This avoids issues when combining multiple keys to enter a\n" - " special character, but breaks the expected behavior of alpha\n" - " keys in games (typically WASD).\n" - "\n" - " --push-target path\n" - " Set the target directory for pushing files to the device by\n" - " drag & drop. It is passed as-is to \"adb push\".\n" - " Default is \"/sdcard/\".\n" - "\n" - " -r, --record file.mp4\n" - " Record screen to file.\n" - " The format is determined by the --record-format option if\n" - " set, or by the file extension (.mp4 or .mkv).\n" - "\n" - " --record-format format\n" - " Force recording format (either mp4 or mkv).\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" - " This flag forces to render all frames, at a cost of a\n" - " possible increased latency.\n" - "\n" - " -s, --serial serial\n" - " The device serial number. Mandatory only if several devices\n" - " are connected to adb.\n" - "\n" - " -S, --turn-screen-off\n" - " Turn the device screen off immediately.\n" - "\n" - " -t, --show-touches\n" - " Enable \"show touches\" on start, disable on quit.\n" - " It only shows physical touches (not clicks from scrcpy).\n" - "\n" - " -v, --version\n" - " Print the version of scrcpy.\n" - "\n" - " --window-borderless\n" - " Disable window decorations (display borderless window).\n" - "\n" - " --window-title text\n" - " Set a custom window title.\n" - "\n" - " --window-x value\n" - " Set the initial window horizontal position.\n" - " Default is -1 (automatic).\n" - "\n" - " --window-y value\n" - " Set the initial window vertical position.\n" - " Default is -1 (automatic).\n" - "\n" - " --window-width value\n" - " Set the initial window width.\n" - " Default is 0 (automatic).\n" - "\n" - " --window-height value\n" - " Set the initial window width.\n" - " Default is 0 (automatic).\n" - "\n" - "Shortcuts:\n" - "\n" - " " CTRL_OR_CMD "+f\n" - " switch fullscreen mode\n" - "\n" - " " CTRL_OR_CMD "+g\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" - "\n" - " Ctrl+h\n" - " Middle-click\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" - "\n" - " " CTRL_OR_CMD "+s\n" - " click on APP_SWITCH\n" - "\n" - " Ctrl+m\n" - " click on MENU\n" - "\n" - " " CTRL_OR_CMD "+Up\n" - " click on VOLUME_UP\n" - "\n" - " " CTRL_OR_CMD "+Down\n" - " click on VOLUME_DOWN\n" - "\n" - " " CTRL_OR_CMD "+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" - " turn device screen off (keep mirroring)\n" - "\n" - " " CTRL_OR_CMD "+r\n" - " rotate device screen\n" - "\n" - " " CTRL_OR_CMD "+n\n" - " expand notification panel\n" - "\n" - " " CTRL_OR_CMD "+Shift+n\n" - " collapse notification panel\n" - "\n" - " " CTRL_OR_CMD "+c\n" - " copy device clipboard to computer\n" - "\n" - " " CTRL_OR_CMD "+v\n" - " paste computer clipboard to device\n" - "\n" - " " CTRL_OR_CMD "+Shift+v\n" - " copy computer clipboard to device\n" - "\n" - " " CTRL_OR_CMD "+i\n" - " enable/disable FPS counter (print frames/second in logs)\n" - "\n" - " Drag & drop APK file\n" - " install APK from computer\n" - "\n", - arg0, - DEFAULT_BIT_RATE, - DEFAULT_MAX_SIZE, DEFAULT_MAX_SIZE ? "" : " (unlimited)", - DEFAULT_LOCAL_PORT); -} static void print_version(void) { @@ -224,334 +29,6 @@ print_version(void) { LIBAVUTIL_VERSION_MICRO); } -static bool -parse_integer_arg(const char *s, long *out, bool accept_suffix, long min, - long max, const char *name) { - long value; - bool ok; - if (accept_suffix) { - ok = parse_integer_with_suffix(s, &value); - } else { - ok = parse_integer(s, &value); - } - if (!ok) { - LOGE("Could not parse %s: %s", name, s); - return false; - } - - if (value < min || value > max) { - LOGE("Could not parse %s: value (%ld) out-of-range (%ld; %ld)", - name, value, min, max); - return false; - } - - *out = value; - return true; -} - -static bool -parse_bit_rate(const char *s, uint32_t *bit_rate) { - long value; - bool ok = parse_integer_arg(s, &value, true, 0, 0xFFFF, "bit-rate"); - if (!ok) { - return false; - } - - *bit_rate = (uint32_t) value; - return true; -} - -static bool -parse_max_size(char *s, uint16_t *max_size) { - long value; - bool ok = parse_integer_arg(s, &value, false, 0, 0xFFFF, "max size"); - if (!ok) { - return false; - } - - *max_size = (uint16_t) value; - return true; -} - -static bool -parse_max_fps(const char *s, uint16_t *max_fps) { - long value; - bool ok = parse_integer_arg(s, &value, false, 0, 1000, "max fps"); - if (!ok) { - return false; - } - - *max_fps = (uint16_t) value; - return true; -} - -static bool -parse_window_position(char *s, int16_t *position) { - long value; - bool ok = parse_integer_arg(s, &value, false, -1, 0x7FFF, - "window position"); - if (!ok) { - return false; - } - - *position = (int16_t) value; - return true; -} - -static bool -parse_window_dimension(char *s, uint16_t *dimension) { - long value; - bool ok = parse_integer_arg(s, &value, false, 0, 0xFFFF, - "window dimension"); - if (!ok) { - return false; - } - - *dimension = (uint16_t) value; - return true; -} - -static bool -parse_port(char *s, uint16_t *port) { - long value; - bool ok = parse_integer_arg(s, &value, false, 0, 0xFFFF, "port"); - if (!ok) { - return false; - } - - *port = (uint16_t) value; - return true; -} - -static bool -parse_record_format(const char *optarg, enum recorder_format *format) { - if (!strcmp(optarg, "mp4")) { - *format = RECORDER_FORMAT_MP4; - return true; - } - if (!strcmp(optarg, "mkv")) { - *format = RECORDER_FORMAT_MKV; - return true; - } - LOGE("Unsupported format: %s (expected mp4 or mkv)", optarg); - return false; -} - -static enum recorder_format -guess_record_format(const char *filename) { - size_t len = strlen(filename); - if (len < 4) { - return 0; - } - const char *ext = &filename[len - 4]; - if (!strcmp(ext, ".mp4")) { - return RECORDER_FORMAT_MP4; - } - if (!strcmp(ext, ".mkv")) { - return RECORDER_FORMAT_MKV; - } - 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 - -static bool -parse_args(struct 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 }, - }; - - struct scrcpy_options *opts = &args->opts; - - int c; - while ((c = getopt_long(argc, argv, "b:c:fF:hm:nNp:r:s:StTv", long_options, - NULL)) != -1) { - switch (c) { - case 'b': - if (!parse_bit_rate(optarg, &opts->bit_rate)) { - return false; - } - break; - case 'c': - LOGW("Deprecated option -c. Use --crop instead."); - // fall through - case OPT_CROP: - opts->crop = optarg; - break; - case 'f': - opts->fullscreen = true; - break; - case 'F': - LOGW("Deprecated option -F. Use --record-format instead."); - // fall through - case OPT_RECORD_FORMAT: - if (!parse_record_format(optarg, &opts->record_format)) { - return false; - } - break; - case 'h': - args->help = true; - break; - case OPT_MAX_FPS: - if (!parse_max_fps(optarg, &opts->max_fps)) { - return false; - } - break; - case 'm': - if (!parse_max_size(optarg, &opts->max_size)) { - return false; - } - break; - case 'n': - opts->control = false; - break; - case 'N': - opts->display = false; - break; - case 'p': - if (!parse_port(optarg, &opts->port)) { - return false; - } - break; - case 'r': - opts->record_filename = optarg; - break; - case 's': - opts->serial = optarg; - break; - case 'S': - opts->turn_screen_off = true; - break; - case 't': - opts->show_touches = true; - break; - case 'T': - LOGW("Deprecated option -T. Use --always-on-top instead."); - // fall through - case OPT_ALWAYS_ON_TOP: - opts->always_on_top = true; - break; - case 'v': - args->version = true; - break; - case OPT_RENDER_EXPIRED_FRAMES: - opts->render_expired_frames = true; - break; - case OPT_WINDOW_TITLE: - opts->window_title = optarg; - break; - case OPT_WINDOW_X: - if (!parse_window_position(optarg, &opts->window_x)) { - return false; - } - break; - case OPT_WINDOW_Y: - if (!parse_window_position(optarg, &opts->window_y)) { - return false; - } - break; - case OPT_WINDOW_WIDTH: - if (!parse_window_dimension(optarg, &opts->window_width)) { - return false; - } - break; - case OPT_WINDOW_HEIGHT: - if (!parse_window_dimension(optarg, &opts->window_height)) { - return false; - } - break; - case OPT_WINDOW_BORDERLESS: - opts->window_borderless = true; - break; - case OPT_PUSH_TARGET: - opts->push_target = optarg; - break; - case OPT_PREFER_TEXT: - opts->prefer_text = true; - break; - default: - // getopt prints the error message on stderr - return false; - } - } - - if (!opts->display && !opts->record_filename) { - LOGE("-N/--no-display requires screen recording (-r/--record)"); - 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]); - return false; - } - - if (opts->record_format && !opts->record_filename) { - LOGE("Record format specified without recording"); - return false; - } - - if (opts->record_filename && !opts->record_format) { - opts->record_format = guess_record_format(opts->record_filename); - if (!opts->record_format) { - LOGE("No format specified for \"%s\" (try with -F mkv)", - opts->record_filename); - return false; - } - } - - if (!opts->control && opts->turn_screen_off) { - LOGE("Could not request to turn screen off if control is disabled"); - return false; - } - - return true; -} - int main(int argc, char *argv[]) { #ifdef __WINDOWS__ @@ -560,18 +37,18 @@ main(int argc, char *argv[]) { setbuf(stdout, NULL); setbuf(stderr, NULL); #endif - struct args args = { + struct scrcpy_cli_args args = { .opts = SCRCPY_OPTIONS_DEFAULT, .help = false, .version = false, }; - if (!parse_args(&args, argc, argv)) { + if (!scrcpy_parse_args(&args, argc, argv)) { return 1; } if (args.help) { - usage(argv[0]); + scrcpy_print_usage(argv[0]); return 0; } From 929bf48c7e1c6e4f7bbcca9b02e10785e49a72c4 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 8 Dec 2019 22:11:54 +0100 Subject: [PATCH 0160/2244] Add tests for command-line parsing --- app/meson.build | 5 ++ app/src/cli.c | 2 + app/tests/test_cli.c | 127 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 134 insertions(+) create mode 100644 app/tests/test_cli.c diff --git a/app/meson.build b/app/meson.build index 3e8d6063..d4dbecbb 100644 --- a/app/meson.build +++ b/app/meson.build @@ -148,6 +148,11 @@ tests = [ ['test_cbuf', [ 'tests/test_cbuf.c', ]], + ['test_cli', [ + 'tests/test_cli.c', + 'src/cli.c', + 'src/util/str_util.c', + ]], ['test_control_event_serialize', [ 'tests/test_control_msg_serialize.c', 'src/control_msg.c', diff --git a/app/src/cli.c b/app/src/cli.c index a219895b..1c393330 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -374,6 +374,8 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { struct scrcpy_options *opts = &args->opts; + 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, NULL)) != -1) { diff --git a/app/tests/test_cli.c b/app/tests/test_cli.c new file mode 100644 index 00000000..83be6f5f --- /dev/null +++ b/app/tests/test_cli.c @@ -0,0 +1,127 @@ +#include + +#include "cli.h" + +static void test_flag_version(void) { + struct scrcpy_cli_args args = { + .opts = SCRCPY_OPTIONS_DEFAULT, + .help = false, + .version = false, + }; + + char *argv[] = {"scrcpy", "-v"}; + + bool ok = scrcpy_parse_args(&args, 2, argv); + assert(ok); + assert(!args.help); + assert(args.version); +} + +static void test_flag_help(void) { + struct scrcpy_cli_args args = { + .opts = SCRCPY_OPTIONS_DEFAULT, + .help = false, + .version = false, + }; + + char *argv[] = {"scrcpy", "-v"}; + + bool ok = scrcpy_parse_args(&args, 2, argv); + assert(ok); + assert(!args.help); + assert(args.version); +} + +static void test_options(void) { + struct scrcpy_cli_args args = { + .opts = SCRCPY_OPTIONS_DEFAULT, + .help = false, + .version = false, + }; + + char *argv[] = { + "scrcpy", + "--always-on-top", + "--bit-rate", "5M", + "--crop", "100:200:300:400", + "--fullscreen", + "--max-fps", "30", + "--max-size", "1024", + // "--no-control" is not compatible with "--turn-screen-off" + // "--no-display" is not compatible with "--fulscreen" + "--port", "1234", + "--push-target", "/sdcard/Movies", + "--record", "file", + "--record-format", "mkv", + "--render-expired-frames", + "--serial", "0123456789abcdef", + "--show-touches", + "--turn-screen-off", + "--prefer-text", + "--window-title", "my device", + "--window-x", "100", + "--window-y", "-1", + "--window-width", "600", + "--window-height", "0", + "--window-borderless", + }; + + bool ok = scrcpy_parse_args(&args, sizeof(argv) / sizeof(*argv), argv); + assert(ok); + + 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); + assert(opts->max_fps == 30); + assert(opts->max_size == 1024); + assert(opts->port == 1234); + assert(!strcmp(opts->push_target, "/sdcard/Movies")); + assert(!strcmp(opts->record_filename, "file")); + assert(opts->record_format == RECORDER_FORMAT_MKV); + assert(opts->render_expired_frames); + assert(!strcmp(opts->serial, "0123456789abcdef")); + assert(opts->show_touches); + assert(opts->turn_screen_off); + assert(opts->prefer_text); + assert(!strcmp(opts->window_title, "my device")); + assert(opts->window_x == 100); + assert(opts->window_y == -1); + assert(opts->window_width == 600); + assert(opts->window_height == 0); + assert(opts->window_borderless); +} + +static void test_options2(void) { + struct scrcpy_cli_args args = { + .opts = SCRCPY_OPTIONS_DEFAULT, + .help = false, + .version = false, + }; + + char *argv[] = { + "scrcpy", + "--no-control", + "--no-display", + "--record", "file.mp4", // cannot enable --no-display without recording + }; + + bool ok = scrcpy_parse_args(&args, sizeof(argv) / sizeof(*argv), argv); + assert(ok); + + const struct scrcpy_options *opts = &args.opts; + assert(!opts->control); + assert(!opts->display); + assert(!strcmp(opts->record_filename, "file.mp4")); + assert(opts->record_format == RECORDER_FORMAT_MP4); +} + +int main(void) { + test_flag_version(); + test_flag_help(); + test_options(); + test_options2(); + return 0; +}; From 419c869c9c276022bd6ec9ac9dd8523a20c6db8a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 9 Dec 2019 20:59:11 +0100 Subject: [PATCH 0161/2244] Use ARRAY_LEN() macro in tests --- app/tests/test_cli.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/tests/test_cli.c b/app/tests/test_cli.c index 83be6f5f..539c3c94 100644 --- a/app/tests/test_cli.c +++ b/app/tests/test_cli.c @@ -1,6 +1,7 @@ #include #include "cli.h" +#include "common.h" static void test_flag_version(void) { struct scrcpy_cli_args args = { @@ -66,7 +67,7 @@ static void test_options(void) { "--window-borderless", }; - bool ok = scrcpy_parse_args(&args, sizeof(argv) / sizeof(*argv), argv); + bool ok = scrcpy_parse_args(&args, ARRAY_LEN(argv), argv); assert(ok); const struct scrcpy_options *opts = &args.opts; @@ -108,7 +109,7 @@ static void test_options2(void) { "--record", "file.mp4", // cannot enable --no-display without recording }; - bool ok = scrcpy_parse_args(&args, sizeof(argv) / sizeof(*argv), argv); + bool ok = scrcpy_parse_args(&args, ARRAY_LEN(argv), argv); assert(ok); const struct scrcpy_options *opts = &args.opts; From 1eae139b6ee8ee04e7b531132d482ced368af1aa Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 9 Dec 2019 21:01:14 +0100 Subject: [PATCH 0162/2244] Add missing consts String parsing functions should not be able to modify their input. --- app/src/cli.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index 1c393330..0ce1304c 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -234,7 +234,7 @@ parse_bit_rate(const char *s, uint32_t *bit_rate) { } static bool -parse_max_size(char *s, uint16_t *max_size) { +parse_max_size(const char *s, uint16_t *max_size) { long value; bool ok = parse_integer_arg(s, &value, false, 0, 0xFFFF, "max size"); if (!ok) { @@ -258,7 +258,7 @@ parse_max_fps(const char *s, uint16_t *max_fps) { } static bool -parse_window_position(char *s, int16_t *position) { +parse_window_position(const char *s, int16_t *position) { long value; bool ok = parse_integer_arg(s, &value, false, -1, 0x7FFF, "window position"); @@ -271,7 +271,7 @@ parse_window_position(char *s, int16_t *position) { } static bool -parse_window_dimension(char *s, uint16_t *dimension) { +parse_window_dimension(const char *s, uint16_t *dimension) { long value; bool ok = parse_integer_arg(s, &value, false, 0, 0xFFFF, "window dimension"); @@ -284,7 +284,7 @@ parse_window_dimension(char *s, uint16_t *dimension) { } static bool -parse_port(char *s, uint16_t *port) { +parse_port(const char *s, uint16_t *port) { long value; bool ok = parse_integer_arg(s, &value, false, 0, 0xFFFF, "port"); if (!ok) { From 024c2f7e6b11c6e0d9e6a80a6ba355d2f340bbf4 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 9 Dec 2019 21:53:38 +0100 Subject: [PATCH 0163/2244] Configure log priority early The log priority must be configured before parsing command-line arguments, in order to get logs as expected. --- app/src/main.c | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/app/src/main.c b/app/src/main.c index 43e2cc04..d683c508 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -37,6 +37,11 @@ main(int argc, char *argv[]) { setbuf(stdout, NULL); setbuf(stderr, NULL); #endif + +#ifndef NDEBUG + SDL_LogSetAllPriority(SDL_LOG_PRIORITY_DEBUG); +#endif + struct scrcpy_cli_args args = { .opts = SCRCPY_OPTIONS_DEFAULT, .help = false, @@ -67,10 +72,6 @@ main(int argc, char *argv[]) { return 1; } -#ifndef NDEBUG - SDL_LogSetAllPriority(SDL_LOG_PRIORITY_DEBUG); -#endif - int res = scrcpy(&args.opts) ? 0 : 1; avformat_network_deinit(); // ignore failure From 81a8e503e501a6492441bbfa679efdff260aec67 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 9 Dec 2019 22:50:06 +0100 Subject: [PATCH 0164/2244] Describe screen rotation shortcut in README --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 2e95cb03..9b26647a 100644 --- a/README.md +++ b/README.md @@ -343,6 +343,13 @@ Note that it only shows _physical_ touches (with the finger on the device). ### Input control +#### Rotate device screen + +Press `Ctrl`+`r` to switch between portrait and landscape modes. + +Note that it rotates only if the application in foreground supports the +requested orientation. + #### Copy-paste It is possible to synchronize clipboards between the computer and the device, in From ad92a192b5d1baef501acdef6290cb8a92cde5c3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 9 Dec 2019 23:00:33 +0100 Subject: [PATCH 0165/2244] Fix meson.build codestyle --- app/meson.build | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/meson.build b/app/meson.build index d4dbecbb..7aaf936a 100644 --- a/app/meson.build +++ b/app/meson.build @@ -156,18 +156,18 @@ tests = [ ['test_control_event_serialize', [ 'tests/test_control_msg_serialize.c', 'src/control_msg.c', - 'src/util/str_util.c' + 'src/util/str_util.c', ]], ['test_device_event_deserialize', [ 'tests/test_device_msg_deserialize.c', - 'src/device_msg.c' + 'src/device_msg.c', ]], ['test_queue', [ 'tests/test_queue.c', ]], ['test_strutil', [ 'tests/test_strutil.c', - 'src/util/str_util.c' + 'src/util/str_util.c', ]], ] From ba1b36758e8a6cbebe8215d9bb0936d4598d6045 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 9 Dec 2019 23:08:10 +0100 Subject: [PATCH 0166/2244] Define SDL_MAIN_HANDLED in all tests Each test defines its own main() function. If this flag is not set, then SDL redefines it to SDL_main(), causing compilation failures. --- app/meson.build | 3 ++- app/tests/test_strutil.c | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/meson.build b/app/meson.build index 7aaf936a..b3d30458 100644 --- a/app/meson.build +++ b/app/meson.build @@ -174,6 +174,7 @@ tests = [ foreach t : tests exe = executable(t[0], t[1], include_directories: src_dir, - dependencies: dependencies) + dependencies: dependencies, + c_args: ['-DSDL_MAIN_HANDLED']) test(t[0], exe) endforeach diff --git a/app/tests/test_strutil.c b/app/tests/test_strutil.c index baa2fd38..200e0f63 100644 --- a/app/tests/test_strutil.c +++ b/app/tests/test_strutil.c @@ -2,7 +2,6 @@ #include #include #include -#define SDL_MAIN_HANDLED // avoid to redefine main to SDL_main #include #include "util/str_util.h" From e4cebc8d4cfdcb474727c0109d4a00ef1d0cc7fe Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 9 Dec 2019 23:23:40 +0100 Subject: [PATCH 0167/2244] Do not build tests in release mode Assertions would not be executed. And as a side effect, it causes "unused variable" warnings. --- app/meson.build | 75 +++++++++++++++++++++++++------------------------ 1 file changed, 39 insertions(+), 36 deletions(-) diff --git a/app/meson.build b/app/meson.build index b3d30458..3bcb9bc1 100644 --- a/app/meson.build +++ b/app/meson.build @@ -141,40 +141,43 @@ install_man('scrcpy.1') ### TESTS -tests = [ - ['test_buffer_util', [ - 'tests/test_buffer_util.c' - ]], - ['test_cbuf', [ - 'tests/test_cbuf.c', - ]], - ['test_cli', [ - 'tests/test_cli.c', - 'src/cli.c', - 'src/util/str_util.c', - ]], - ['test_control_event_serialize', [ - 'tests/test_control_msg_serialize.c', - 'src/control_msg.c', - 'src/util/str_util.c', - ]], - ['test_device_event_deserialize', [ - 'tests/test_device_msg_deserialize.c', - 'src/device_msg.c', - ]], - ['test_queue', [ - 'tests/test_queue.c', - ]], - ['test_strutil', [ - 'tests/test_strutil.c', - 'src/util/str_util.c', - ]], -] +# do not build tests in release (assertions would not be executed at all) +if get_option('buildtype') == 'debug' + tests = [ + ['test_buffer_util', [ + 'tests/test_buffer_util.c' + ]], + ['test_cbuf', [ + 'tests/test_cbuf.c', + ]], + ['test_cli', [ + 'tests/test_cli.c', + 'src/cli.c', + 'src/util/str_util.c', + ]], + ['test_control_event_serialize', [ + 'tests/test_control_msg_serialize.c', + 'src/control_msg.c', + 'src/util/str_util.c', + ]], + ['test_device_event_deserialize', [ + 'tests/test_device_msg_deserialize.c', + 'src/device_msg.c', + ]], + ['test_queue', [ + 'tests/test_queue.c', + ]], + ['test_strutil', [ + 'tests/test_strutil.c', + 'src/util/str_util.c', + ]], + ] -foreach t : tests - exe = executable(t[0], t[1], - include_directories: src_dir, - dependencies: dependencies, - c_args: ['-DSDL_MAIN_HANDLED']) - test(t[0], exe) -endforeach + foreach t : tests + exe = executable(t[0], t[1], + include_directories: src_dir, + dependencies: dependencies, + c_args: ['-DSDL_MAIN_HANDLED']) + test(t[0], exe) + endforeach +endif From a0f8e7fd9f2d866808e24c3f4a10e6758c6bc403 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 9 Dec 2019 23:24:43 +0100 Subject: [PATCH 0168/2244] Bump version to 1.12 --- 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 78bcd1a8..1b295d6e 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('scrcpy', 'c', - version: '1.11', + version: '1.12', meson_version: '>= 0.37', default_options: [ 'c_std=c11', diff --git a/server/build.gradle b/server/build.gradle index 0804a8bd..fb14db91 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -6,8 +6,8 @@ android { applicationId "com.genymobile.scrcpy" minSdkVersion 21 targetSdkVersion 29 - versionCode 12 - versionName "1.11" + versionCode 13 + versionName "1.12" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index b1fdabdd..54d43fb4 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.11 +SCRCPY_VERSION_NAME=1.12 PLATFORM=${ANDROID_PLATFORM:-29} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-29.0.2} From 71df3175bd1f21987b41f79938c9f2b04cc444e7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 9 Dec 2019 23:50:45 +0100 Subject: [PATCH 0169/2244] Update links to v1.12 in README and BUILD --- BUILD.md | 6 +++--- README.md | 14 +++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/BUILD.md b/BUILD.md index fc6d9035..a395fea8 100644 --- a/BUILD.md +++ b/BUILD.md @@ -233,10 +233,10 @@ You can then [run](README.md#run) _scrcpy_. ## Prebuilt server - - [`scrcpy-server-v1.11`][direct-scrcpy-server] - _(SHA-256: ff3a454012e91d9185cfe8ca7691cea16c43a7dcc08e92fa47ab9f0ea675abd1)_ + - [`scrcpy-server-v1.12`][direct-scrcpy-server] + _(SHA-256: b6595262c230e9773fdb817257abcc8c6e6e00f15b1c32b6a850ccfd8176dc10)_ -[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.11/scrcpy-server-v1.11 +[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.12/scrcpy-server-v1.12 Download the prebuilt server somewhere, and specify its path during the Meson configuration: diff --git a/README.md b/README.md index 9b26647a..c8918a07 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# scrcpy (v1.11) +# scrcpy (v1.12) 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. @@ -62,13 +62,13 @@ For Gentoo, an [Ebuild] is available: [`scrcpy/`][ebuild-link]. For Windows, for simplicity, prebuilt archives with all the dependencies (including `adb`) are available: - - [`scrcpy-win32-v1.11.zip`][direct-win32] - _(SHA-256: f25ed46e6f3e81e0ff9b9b4df7fe1a4bbd13f8396b7391be0a488b64c675b41e)_ - - [`scrcpy-win64-v1.11.zip`][direct-win64] - _(SHA-256: 3802c9ea0307d437947ff150ec65e53990b0beaacd0c8d0bed19c7650ce141bd)_ + - [`scrcpy-win32-v1.12.zip`][direct-win32] + _(SHA-256: b2c8c4a3899c037cf448a2102906775114826ba646ce1b847826925103fa801d)_ + - [`scrcpy-win64-v1.12.zip`][direct-win64] + _(SHA-256: 7d47983b426f7287de0230b88975dc17c1d9c343fa61a93ff2af78b6e9ef5c8c)_ -[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v1.11/scrcpy-win32-v1.11.zip -[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.11/scrcpy-win64-v1.11.zip +[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v1.12/scrcpy-win32-v1.12.zip +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.12/scrcpy-win64-v1.12.zip You can also [build the app manually][BUILD]. From 6965d051ae4c34116367ae5f2319c843d788661c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 10 Dec 2019 09:28:27 +0100 Subject: [PATCH 0170/2244] Limit bitrate range to 31 bits integer A proper solution could be to use "long long" instead (guaranteed to be at least 64 bits), but it adds its own problems (e.g. "%lld" is not supported as a printf format on all platforms). In practice, we don't need such high values, so keep it simple. Fixes #995 --- 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 0ce1304c..d9e1013a 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -224,7 +224,9 @@ parse_integer_arg(const char *s, long *out, bool accept_suffix, long min, static bool parse_bit_rate(const char *s, uint32_t *bit_rate) { long value; - bool ok = parse_integer_arg(s, &value, true, 0, 0xFFFFFFFF, "bit-rate"); + // long may be 32 bits (it is the case on mingw), so do not use more than + // 31 bits (long is signed) + bool ok = parse_integer_arg(s, &value, true, 0, 0x7FFFFFFF, "bit-rate"); if (!ok) { return false; } From 4687a0ebac74f2d34e5ef330e1f9d587e7136222 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 10 Dec 2019 10:07:48 +0100 Subject: [PATCH 0171/2244] Bump version to 1.12.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 1b295d6e..412c9c51 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('scrcpy', 'c', - version: '1.12', + version: '1.12.1', meson_version: '>= 0.37', default_options: [ 'c_std=c11', diff --git a/server/build.gradle b/server/build.gradle index fb14db91..539a97b8 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -6,8 +6,8 @@ android { applicationId "com.genymobile.scrcpy" minSdkVersion 21 targetSdkVersion 29 - versionCode 13 - versionName "1.12" + versionCode 14 + versionName "1.12.1" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index 54d43fb4..c117d89c 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 +SCRCPY_VERSION_NAME=1.12.1 PLATFORM=${ANDROID_PLATFORM:-29} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-29.0.2} From 31bd95022bc525be42ca273d59a3211d964d278b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 10 Dec 2019 17:59:42 +0100 Subject: [PATCH 0172/2244] Update links to v1.12.1 in README and BUILD --- BUILD.md | 6 +++--- README.md | 14 +++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/BUILD.md b/BUILD.md index a395fea8..e35d07d0 100644 --- a/BUILD.md +++ b/BUILD.md @@ -233,10 +233,10 @@ You can then [run](README.md#run) _scrcpy_. ## Prebuilt server - - [`scrcpy-server-v1.12`][direct-scrcpy-server] - _(SHA-256: b6595262c230e9773fdb817257abcc8c6e6e00f15b1c32b6a850ccfd8176dc10)_ + - [`scrcpy-server-v1.12.1`][direct-scrcpy-server] + _(SHA-256: 63e569c8a1d0c1df31d48c4214871c479a601782945fed50c1e61167d78266ea)_ -[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.12/scrcpy-server-v1.12 +[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.12.1/scrcpy-server-v1.12.1 Download the prebuilt server somewhere, and specify its path during the Meson configuration: diff --git a/README.md b/README.md index c8918a07..4d6d29fd 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# scrcpy (v1.12) +# scrcpy (v1.12.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. @@ -62,13 +62,13 @@ For Gentoo, an [Ebuild] is available: [`scrcpy/`][ebuild-link]. For Windows, for simplicity, prebuilt archives with all the dependencies (including `adb`) are available: - - [`scrcpy-win32-v1.12.zip`][direct-win32] - _(SHA-256: b2c8c4a3899c037cf448a2102906775114826ba646ce1b847826925103fa801d)_ - - [`scrcpy-win64-v1.12.zip`][direct-win64] - _(SHA-256: 7d47983b426f7287de0230b88975dc17c1d9c343fa61a93ff2af78b6e9ef5c8c)_ + - [`scrcpy-win32-v1.12.1.zip`][direct-win32] + _(SHA-256: 0f4b3b063536b50a2df05dc42c760f9cc0093a9a26dbdf02d8232c74dab43480)_ + - [`scrcpy-win64-v1.12.1.zip`][direct-win64] + _(SHA-256: 57d34b6d16cfd9fe169bc37c4df58ebd256d05c1ea3febc63d9cb0a027ab47c9)_ -[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v1.12/scrcpy-win32-v1.12.zip -[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.12/scrcpy-win64-v1.12.zip +[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v1.12.1/scrcpy-win32-v1.12.1.zip +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.12.1/scrcpy-win64-v1.12.1.zip You can also [build the app manually][BUILD]. From 7d5845196e6a7e919d0ec3b5fc53fc7dab8d3222 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 14 Dec 2019 18:13:56 +0100 Subject: [PATCH 0173/2244] 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 0174/2244] 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 0175/2244] 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 0176/2244] 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 0177/2244] 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 0178/2244] 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 0179/2244] 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 0180/2244] 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 0181/2244] 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 0182/2244] 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 0183/2244] 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 0fb22c3e98caed6d7b07f8e53caa9da2ed6334d7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 19 Jan 2020 16:04:20 +0100 Subject: [PATCH 0184/2244] Happy new year 2020! --- LICENSE | 2 +- README.ko.md | 2 +- README.md | 2 +- app/scrcpy.1 | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/LICENSE b/LICENSE index 3d6840b1..bc4bb77d 100644 --- a/LICENSE +++ b/LICENSE @@ -188,7 +188,7 @@ identification within third-party archives. Copyright (C) 2018 Genymobile - Copyright (C) 2018-2019 Romain Vimont + 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. diff --git a/README.ko.md b/README.ko.md index 564acae7..b232accd 100644 --- a/README.ko.md +++ b/README.ko.md @@ -475,7 +475,7 @@ _²화면이 꺼진 상태에서 우클릭 시 다시 켜지며, 그 외의 상 ## 라이선스 Copyright (C) 2018 Genymobile - Copyright (C) 2018-2019 Romain Vimont + 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. diff --git a/README.md b/README.md index 4d6d29fd..616802c4 100644 --- a/README.md +++ b/README.md @@ -489,7 +489,7 @@ Read the [developers page]. ## Licence Copyright (C) 2018 Genymobile - Copyright (C) 2018-2019 Romain Vimont + 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. diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 23b168ca..9560df1c 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -261,7 +261,7 @@ Copyright \(co 2018 Genymobile Genymobile .UE -Copyright \(co 2018\-2019 +Copyright \(co 2018\-2020 .MT rom@rom1v.com Romain Vimont .ME From 39356602ed472cc3f533e36ae04a110b247c29e0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 19 Jan 2020 16:16:05 +0100 Subject: [PATCH 0185/2244] Mention scrcpy Debian package in README --- README.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 616802c4..da05bb7e 100644 --- a/README.md +++ b/README.md @@ -37,8 +37,11 @@ control it using keyboard and mouse. ### Linux -On Linux, you typically need to [build the app manually][BUILD]. Don't worry, -it's not that hard. +In Debian (_testing_ and _sid_ for now): + +``` +apt install scrcpy +``` A [Snap] package is available: [`scrcpy`][snap-link]. @@ -56,6 +59,10 @@ For Gentoo, an [Ebuild] is available: [`scrcpy/`][ebuild-link]. [Ebuild]: https://wiki.gentoo.org/wiki/Ebuild [ebuild-link]: https://github.com/maggu2810/maggu2810-overlay/tree/master/app-mobilephone/scrcpy +You could also [build the app manually][BUILD] (don't worry, it's not that +hard). + + ### Windows From 1144f642144103a3b0bbd017998d75edd9d2a8b8 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 6 Feb 2020 18:40:59 +0100 Subject: [PATCH 0186/2244] Indicate that -s can also be used for TCP/IP --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index da05bb7e..cb2fb75f 100644 --- a/README.md +++ b/README.md @@ -221,6 +221,13 @@ scrcpy --serial 0123456789abcdef scrcpy -s 0123456789abcdef # short version ``` +If the device is connected over TCP/IP: + +```bash +scrcpy --serial 192.168.0.1:5555 +scrcpy -s 192.168.0.1:5555 # short version +``` + You can start several instances of _scrcpy_ for several devices. #### SSH tunnel From e8127375aeba200b5c1a24a0c37c61516aae3ba1 Mon Sep 17 00:00:00 2001 From: Camilo Martinez Date: Wed, 12 Feb 2020 11:31:10 -0500 Subject: [PATCH 0187/2244] Add Chocolatey for Windows install PR #1144 Signed-off-by: Romain Vimont --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index cb2fb75f..7be5f5f2 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,20 @@ For Windows, for simplicity, prebuilt archives with all the dependencies [direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v1.12.1/scrcpy-win32-v1.12.1.zip [direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.12.1/scrcpy-win64-v1.12.1.zip +It is also available in [Chocolatey]: + +[Chocolatey]: https://chocolatey.org/ + +```bash +choco install scrcpy +``` + +You need `adb`, accessible from your `PATH`. If you don't have it yet: + +```bash +choco install adb +``` + You can also [build the app manually][BUILD]. From f903cd376d1f32fa6d03cffe8ba9c598e2df5fee Mon Sep 17 00:00:00 2001 From: George Stamoulis Date: Sun, 16 Feb 2020 13:14:08 +0200 Subject: [PATCH 0188/2244] Documentation rectifications PR #1151 Signed-off-by: Romain Vimont --- DEVELOP.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DEVELOP.md b/DEVELOP.md index 92c3ce87..0258782f 100644 --- a/DEVELOP.md +++ b/DEVELOP.md @@ -189,7 +189,7 @@ The client uses 4 threads: recording, - the **controller** thread, sending _control messages_ to the server, - the **receiver** thread (managed by the controller), receiving _device - messages_ from the client. + messages_ from the server. In addition, another thread can be started if necessary to handle APK installation or file push requests (via drag&drop on the main window) or to @@ -214,7 +214,7 @@ When a new decoded frame is available, the decoder _swaps_ the decoding and rendering frame (with proper synchronization). Thus, it immediatly starts to decode a new frame while the main thread renders the last one. -If a [recorder] is present (i.e. `--record` is enabled), then its muxes the raw +If a [recorder] is present (i.e. `--record` is enabled), then it muxes the raw H.264 packet to the output video file. [stream]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/stream.h From 4794ca8ae7515c7376b8e19aa8ec9c07546c965c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 25 Feb 2020 12:18:49 +0100 Subject: [PATCH 0189/2244] 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 0190/2244] 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 0191/2244] 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 0192/2244] 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 c396758b4ed29cb4d4ad51e8bcdd1deecf5761d2 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 3 Mar 2020 21:39:27 +0100 Subject: [PATCH 0193/2244] Remove link to Windows 32 bits release Binaries created with MinGW (even a simple Hello World) are detected as malware by some anti-virus. For some reason, only the 32 bits version of scrcpy is impacted. Since users should use the 64 bits version by default anyway, remove the link to the 32 bits version from the main page. The 32 bits release is still available in the "releases" tab. See --- README.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 7be5f5f2..161b42f1 100644 --- a/README.md +++ b/README.md @@ -66,15 +66,12 @@ hard). ### Windows -For Windows, for simplicity, prebuilt archives with all the dependencies -(including `adb`) are available: +For Windows, for simplicity, a prebuilt archive with all the dependencies +(including `adb`) is available: - - [`scrcpy-win32-v1.12.1.zip`][direct-win32] - _(SHA-256: 0f4b3b063536b50a2df05dc42c760f9cc0093a9a26dbdf02d8232c74dab43480)_ - [`scrcpy-win64-v1.12.1.zip`][direct-win64] _(SHA-256: 57d34b6d16cfd9fe169bc37c4df58ebd256d05c1ea3febc63d9cb0a027ab47c9)_ -[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v1.12.1/scrcpy-win32-v1.12.1.zip [direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.12.1/scrcpy-win64-v1.12.1.zip It is also available in [Chocolatey]: From da18c9cdab1c40c605bb63105e92b40284704867 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 4 Mar 2020 21:56:48 +0100 Subject: [PATCH 0194/2244] 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 0195/2244] 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 0196/2244] 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 0197/2244] 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 24ade6ad7740a5428a531c5b22a8b72ed0987bd5 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 14 Mar 2020 17:07:20 +0100 Subject: [PATCH 0198/2244] Simplify Chocolatey documentation --- README.md | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/README.md b/README.md index 161b42f1..c8257b49 100644 --- a/README.md +++ b/README.md @@ -80,12 +80,7 @@ It is also available in [Chocolatey]: ```bash choco install scrcpy -``` - -You need `adb`, accessible from your `PATH`. If you don't have it yet: - -```bash -choco install adb +choco install adb # if you don't have it yet ``` You can also [build the app manually][BUILD]. From bc7508427b3a6a19a5603caa63d05a860dbacd8a Mon Sep 17 00:00:00 2001 From: Yogev Boaron Ben-Har Date: Sun, 8 Mar 2020 10:31:29 +0200 Subject: [PATCH 0199/2244] Add scoop instructions for Windows PR #1202 Signed-off-by: Romain Vimont --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index c8257b49..593dd2a9 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,15 @@ choco install scrcpy choco install adb # if you don't have it yet ``` +And in [Scoop]: + +```bash +scoop install scrcpy +scoop install adb # if you don't have it yet +``` + +[Scoop]: https://scoop.sh + You can also [build the app manually][BUILD]. From cd69eb4a4fecf8167208399def4ef536b59c9d22 Mon Sep 17 00:00:00 2001 From: Jaafar Biyadi Date: Sat, 29 Feb 2020 18:33:17 +0100 Subject: [PATCH 0200/2244] 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 0201/2244] 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 0202/2244] 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 0203/2244] 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 0204/2244] 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 0205/2244] 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 0206/2244] 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 0207/2244] 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 0208/2244] 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 0209/2244] 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 0210/2244] 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 0211/2244] 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 0212/2244] 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 0213/2244] 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 0214/2244] 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 0215/2244] 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 0216/2244] 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 0217/2244] 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 0218/2244] 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 0219/2244] 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 0220/2244] 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 zcmX@GiTT(m<_+46yi-*#FWbq;z#z%Yz@R^Qg0k3TKSq&y6+^w`oW$bd-k{TYmmNfG zFYhQdT-Nf%wQ`C>yk}&W( z=H!SiF)WrZK6iEJ_jBjt@7FU_bnRvMykm36ZpL>H_x1bA^VgWLip|She7CqUYIUvN z1KH;*g?310FTU5)UT>RvK1Dxync(xzGv}6GO||i@%3HwQ_+hHyDY^7b8X|{}7v}vA zyno6h_@cVDyK?Gc@yd5x#~*}bT`s+3mAZX?iH3t(r<7&m+OQf$`%@mzFM7{ic~0fj zl9%qW>04ix-+HfMd^IfLWxU2z3ClGh|J;Q*cKy7ew#tPoCr0hbijx_fD#F z4&1u$JC*mMknV=^qq}k*Hk)PIi2jH&+H7fj?c19duQn?Se`vq7k)eF&tz9|18HXO% zKl=1z=d%h6dAI8?T(>lp)%}+~GW}J&<%7Kes{93W%Pkl$=v@pleii(Q^WxS`cN~-7 zX=lw(e-Y@K@23!4_42OM;;Qc(d#0{Bl65rZu+`f3+cysIRxt*~9X*&I;LXg!#lXP9 z!N9=a$dJjXvs#{&fkB0ffk77<&ocG5gN4H-{>v8M+@RB8sMXoD(aDoRSbuBShOl0Z z3#__~xm#}Oom5xmo2NG{P>IYZcopoJqOO* z+Bb;z6nB`FQ`~vR6a&>{srRQoI8HTCP7HJ7nVu4JtlH?= zgnH5AdwK-KQeqB<7VeRE@(WnkS`d4PT`0&%X@_F6jJayfjn23l6-VyGz#FH!7c21T zC3+sKV^nBc)RM}Uddg(yT*ECzGp9>7EiVZ!_cQ(>bW)q=Ur*=*p4*{!pKP9bNoo7@ zt**@)iC0f$9}_)$?PzuHi}GounKqE%ZgZ`!+mV` z^*nco{gHg96U|!R7SY^rb~?|z@=J34D&^+tdAUpH?eSQBGx+dw&wbrRQ}^30Ub`bn zsHDhjsYZ1jQaXH$Pn{;(UJEWbf|n9BW%{bMZ`J z5&E?Gxuxn-25Cz!oi!dmA{u#5tz5jI$(rk3S2DwDp=%;FP5z>H4%eR2+gg6=^|BDz z>{HB|LW>?4*Z8f~ifRhF9Tdu1AOFCcHU44tt0h&JjN3w=E!xMm?_h##%vvdv?hqcv zC(*3;A2v3<|JbnXjc9w$gN`Qo56XuxXlogKv0GoC`Dt4O@6$J#bKRm;yr;!p%Gl(u zHgE3LV;{f13d_Ivtn=Pgx9~NM4avG^d?(&_*;JUZ{MhW*R-uza=1&Z1{W$H^6>H`C zpeKH1Yu|hb`dsCq60Kg6Z9Cyq>e**%M`y3e-?VG_^NdalsZVD6bBcR~?o3^O<50j~ z+e=TbE#~@=`Yz>`&c=vGtnaLNc3t0gBVqTIZ8vk2wp4vQzh}vmd48WkZdaZbL;Z;pe!9`$)jQ z+S%ooq!C(@yP&@M%zuvCi~ng=E&BJNPwP-}V}`<>$xD6~} z`Kv4b>wj72-1n>Ad5UAhx7`bZUo0uscf8xq_hsSyI*T6VJ|5TgL4V8(1ysBCI$gZ4 z%qw_jiMWHVMWMAz*^FxM#|ErY2ekSwoOR!J?jrx9i5ik^4NFIf|k9Z z-cyfTd+sq@0Ty#Z zR%%?$n&7aU^P3@)mQ&y4I|{oB)!EWDZHk_=aZQq6#=tKu!Dz$sjpaCp#mSUA`z-ul zGN(LxUmo20pJ8$~+ehB2geN~OSr`~@urn|?f=kePc20Je)U?FXoRZ0}CFJUthTaX9 z4i))-H^+?oG+U5|#*}Rh+e}%!R)uX@vNpFdBST|qfWTU-=}*jZcE5R)#{5_M*Ms2r zUyJ^a9o}F?PS>R1;$p3A2-WCKHgK``grH`4;M8bl=!GUzi0b=-sj(Q`}bCU zIsg1S!=8rkK1V)!*+vyObvbGpsdY{+wWv>hWD=L_z?8n^*_zH`iL3(#ZagWxd(xD; zt}CafEwEE7l9>8vPop8TI8TqUi_H2MF*|4Xd!J9g+;rZ%&qM!bO`_wEmwSxTBwl$P z5=@ojXj^<+R_5EZnB$L^cJB-S=DKv!cJ0Q3sju&`#_QaQ+q$XH?T70_HXa@KAL_Z+ z`TUIQ=lH*KdazQVdYQx_=eW)tW>0g&V^*9y&s9I^yw#G-bJeczd1*JuZD=zbG(MXz8rA0v{GcNzQtl7db^DYSPcu zH4~S|3NF$2ePXnDv5|W7bT1IIcD|1PHy^9@2XDt&ncEfZ)evL4UEJv^9l9-| zG{b9NL2%*9yS$C!1?h9sawb38bxNlq^wY^(lTu4;{r0nL*&r{jyCrFTtJ#+58|{4D z?ic;n{Nmk{n8?J+UEf}KWYPC$uFR9g^b$`U*s^7hXX@QHv%_{vOILh(*||#k#^vyj0%Vs~J zxi8k>(F~beg3EKYx1O}TA05OtGwSiO#Gca&FFgBMYU#Xl&K$+ud&{uL6-c`NVDzaF0uI}y$Sd}jse)1gKVq)*$qqjF@Pxmek6>Z;*RvgVw%u8<8 zdq><4EW0m!iT}mnev8KWBD+Nj)wb}KHCOO?9?cW5P2pV`yWZ-$d$nwo%{8a2s{L70 zS@P}S1A897X2!ob!NRq+x-*&)aU+?mHg8v_-AR5rQZ{& z&YD?5S5GEgI{7>_d6V?Z)X*TGw<$rhyjJnuI$+5h62|=0)!apQJ?AN}s^iJ3Yk#{v z=hg39mMy;~>Acn~`XWqloS10I}`fM?()r{Z5R29W-@moolmUrh%`|1(TI#hPAT}v0Te3 zKWX6cGyInlujxOA`Y9jJO$)muwCeW;Ikw5GUT)XiuDkf~y)<`I*A;)H!Y8I|xZk#L!qkTCx0?)> zW~oTWIe)5B*t71*+G!K+&0ZnRCb)imoQQ_;8MBsKYE|ZwzD}GY-^!z%xA51@`iF5E zQ_ST;%sx&?yg4_bV)2^YW_;fDJl)e7B-hMcp`4v``ug5HP7(-ekq{v^OzHbxD_b zZqAVSu;Jm`FSop8418YiJkc>>)$t9_!rT?SekCrB*&&fsVaWEQ|H_-65;Fq=m!7PD zx@^iEACctnjNtyJlVOPQ=)3Z0&xfR{;*k$%dY)4*mv=m!?b@Z zEp8k)eev@MgT)TdY$3}h?N7=@uI~I}C^Uc06TxM3rXp#+97{Hc z$S{>?*3HRbVPb1hG4XIo=$XGnJu|^X`TQQogyYA`oGcHTIH8?A?<5T}!+#S-h%$uQO}nv98PO8{X}Hf2a6--M{MjKMs}0)$$&w z534-YU;4w;{@2H!TIcpHXp0X&oGhAa3 zACUj`QRbfqZ+w_d{hGG*KQ^+~Cp}or_U}sn)E~z7VGqv#{lW0hN%#L!seh-Aysq2B zU%$3(z2pz+uYaQL@AdEh{P_Jx{;VIzwX%Mz3;%ypfBgDKeYSsBkL>@sk@Nq%)?dN= zA&;eh@Y~NjUO(%HzWqPx`yoR6mTKHL`e1*mFMstzUeg+P-eMV+ECJIHiJ;7m<09+j zWgjh0O`4JP%TOb9*$>~9W=B3P7M^duw9S8Is7`5C*!cw~jqLfn4@`Mlw?|3aCoQ+rh=J)kxDZTO6PWdjovu4RU<0oRyPRs6OovI2pn^81dQ+nzi-&6B$`(~P? zZr1c&W-|LylGkd@+^po?iscecx7QZfyi@sp0fm=$e%{S2Kh(MVqTp@c2@&QOidQWE9&vu5*6J_${{ckNcTwZIH>F;ZK@>||s4^y`L z)o(o8XFkli;JIv3=Z*z+Srd)dKCOSUXXg7UZ+4qY_3zrvGPl<|V#AHEN2i=^W;#Fb zh(_<4*L}z4&inFY!(^K*{^K&DdyaI-N^hAQ_;sFg)tyca^!#S&W^|1_`kFlM=k37 zR(r92#$r>6Z!cCD^_B*nI`vDkG$_wx-sRBfUDH`Fl;$0o8Pt6zX8v`_zTJLCdB-zi ztR$lqw$8qtby-t=XZ4+vp<2TBbL6ChMS}|$P5yS|^5nmLoA%!F-BGsYc4z-?VOOp> z7i~QGj1K-hbbGRg_SrM4&Thrw(_f?(ADJtds;#@iXGwjmqw~S}_qJG1Et4!gKJQ79 z>y+E!Vqu3r{&aPIXOc2ov0QS~-3hX7cYK?Jzs)e7(8G02m*d;BRz^|xs}Y8VMRxCQ zJ?Cn?vvRRpAnzHk<#W6Gbk6*Ewuv+L%~G$m^-DgSTNIGUfBd-6k)?^&Pjl{H6P3Sc zyJ_V{pSM$+XZ$KWezrc(ORnrrCrj~`WgYr2dycMHYrEU6?)1eom2t|yXX&tRxcBh# zoh7fFF1~x^TXyqgL`dOUokv=I6VmPk9(i#&_29`>i)5wBc7J8MT-Ki;dAm+Uc5`g{ z0)fu2PmRM~ZR?APNuFovy6|$R%3jVX9hblAL|lvrYl^?fHuuMhL!V#0@vi5yn0{7Y zG-IEO7Wc-`&Je@G@}ic|_x&oDP71o4Dn{G)8{~TRwI2VlMe1InV((_LWrmp#Wz<-$ zr#mdaz3=ovm(ELx4{hYOeyFWrH#T^yfpYvPp8ZEv@EZ21C_}+r#1U4a?0!dccj;6C>%Y+ zFKbb?MSn@4P6$`>!XuIDlA9iv*goy~P{4gwdFrJp?e)I|%v-;`&S`Or>^b?OAnN0m z)?Z0{hCaF8t~VbpEOAU+(i^+*it40`?!^msW+nQUEn95ZE3t1)CqpXXegE8gd}%5IG`u$gs2 zYRctcp3b&8o1g5lsXrybFLU>L)pphXlefKpe2Shv{h+6#N8tIbb7%K#<#m6&IJY|Y zdf9n@OB)r5+S(Ak*^3y=v!m4ce?6%@c(NvURl~H-S}haVQuS#ka!o|L)b9jn*_uvW zFzcc~=5x--h0V?FGiN(&+kLC(?$TQmj>l?l)NkLUrQ09-WC z_R#R2M_D%hR%=(MB~O?hefRUPme_hDpQuxN{&VlE{iAvOtgPReh>MA_hnA;a(b!rx z=}69s*Pq=#{%O5xUYTYx%jadb@y464)k^+w&G}q@CAGI(|J1=_pTym^hIyAiKH3?* z*&*%ZUt@>$zvjAM+7o-8{na<4?HfMbXW9B?%M!UT(VbB>(P!`7STXBo(dX%DzvP~& zTCS{Lf8*XU;kmE2?pxCGC5X2$($)BXT-DZ-L08k$O+@F}@9%nlww5cJYnqO}^RhZ# zhdZI~xV*w=?{BYKGUdYTD`hT*!26FJ==erCV_ ztJROLM4$Se>^62olkAoSzjQ6a~fB9Rn{K=}{ zTcRz#gp`@P)$_C|%I8Bc8Z7BF$HIVd8hxkvryJgus7 z!2o%!IqX-qch${1ZToZQ!R7P!^gnznBmew1!=4WxBClT2(OqjiVRh6Q;rdw{o07SG zwmf1=_l0JKhy~64(N*9fnu5vm*Ws}$1M=$4e$uH*m8hzT=*LX!nR-NQj-?jRO zFN=KH!;^nmr^flLuCYN}n`hgq%D=B)*j(^iTOROE>`Ib$*aJVdbZN6kdbyXkt$EVq zJDbZa_tLwt7dN+T^E^AHR4(Y}#i^Uu)rUuOui^Nye&H{+tn@_QlRR1L7KYsnTA91@ z>hJeyI`0k^y}7b$?VEjb-gVCkpEmi&-wO56l*Q32&97}u37GEqcca?VSDPy3Hy4!$ zzWY1Je!dsiGuQsm&h{zEbK*~FuU^yc|Nqg&)92Um<$pG1 zF^ahNuzvQ@u7#0nWEMSYlU(zP^OsS?-G`Z3d{t=zDJQO%NI3c@$N>y1O!KhFL&)AwnA67%uBG56j3n0cyaEG?U$TH0~+-dQE5Wy<~6WWu=bJ_Dgyhv$KAFmzw`RNoe~cNwc#mr|r)i`DbzO?5BzuyYC;i zTffLQedj-p+s22^{pb9nvMTeUNL|-vsj5O*``+}>)m`TCPII1mb!^Vz4E3m2)q9*C zpt)+oChI*ZKO*-`4-7o|C-TR0jUC4hSTRTQ*rY$GWPWe_p^jPJz#zlG?9sw`mL_^9 zRcB>8AC}?$s{2gY=R9uHjt9m5oGLz( zi}+r1nrZd(itL$}S0|yL@h_~VrSN=HGk=owo!EtS>5~q(_dmT*9xhyZ=lbPemKE#R z%1kdW<9WBA$NTyZ<~1Lu9e5X6!gn^oKOvtj*4O1#ZB5#S`M%ct!I39wns(_KEb+fI zY5B&k`ULSstltHvKTE6$XN)ToeUK`i{UcJ;hv}|<)`>kU6!$*5y&_F3T;$QtPM<%U z&L0a-3VHSTlxo{s2eyZYbsL){Ww=Vh>!jL^U%brZ{Icr19sm7yEoOHmm9mPc*4KX} zU99#v`d>JG^xwi$&kkJVVLKu4lZS16!SmZ!&Mmw&*Rj8T%Hww*UX;j&?>JTyYVfjl z&Bf$5_tjtg+!)g{OG0u(v(LBFS!KS`tN+XWs%`mu;ry@p3G6WkihIs|wu<97Vp@Ij zV`0p~7PCdiL(kbPS#!+$2aB=AL5cKC|BnGbR4R4^Uh+$^2sSNRDe&6cYTcd^gH6V7 zzH`YwOba;^Si5vxVp;te?Nwj;&u*-;VHM(>bxL=Al3Do4=|7iE+NoO5lz#o;fo7TN zsJsVaOb>6x?EUcM<*N559;9S%i(9v0m)x$P6*Jgs8|peZ{}|-fH@^KQxafgp?ucy#_H_I<8PAl<9+s9&n(5M3<9M!Tfxc0SnS0x-T3wl* zYbQ*5Vj~L*D<+oJ%@K=xc=owgexUwFZo9_YGv%M&Zd~JNdB29|O?pY1j@qNmx3)8! zZ;rpTkF_%D&!&AAEjL{EADTJ6Xxrm(70b^PKet``WBJIcLV9(I?TY^kO?PhVyRrV? z?h4s+2mbE|^`R%fKPk2OhD3`fuVs$Nqc9EzhBhGv2BpcM`Otbjf%=Hb8j*6*|Nk}} z*O|YN^Uwb~SN`|j?|t8Y`=0yzd|^A| znnU)_5}9@KCf(UHL3;LV5%a#Dda)&{G7|+4b+jxtN?B4_)Z@Or{e#lG_%qu251%c) zBlG+jpCsoVA2;*3b7$v%E)b79xOjK!9-;L~lfq9|@V?`GxYg&^E`hl7o9-z*?yV4B zzh|e+_eZ)uKf>niTPqW|XRnOmp61yX1>QY=T7IfR{GM*|gtZO1O1Vck^|#Wix!vc77?_E_^xFM{MgE z=PH}r)jsM0w-axE^tcn2+$JWu@oGxYX64$r`?Ko(z5YB*GPQMnwQB3@s!N=WUj8!| z&id%VFF2zxiNngdD(k3M$ES zZmnkhvo@?}&%O=L=3Cd5Bn52#zM@&C{S+VH>A0)cD(A(1`K-x0rR=iG$F^BsnI?O) zrcGXxvCR3mr%=7?%0tj?hEN}i?qT*8~bW&61XA+ON zFBS%r;GunkJ=ujPX#Em+g|RqRGC6Yf{?F zT#PyTOMYuG0puHV&B7QE1Uo51@&a_+r+_2sKPr@J0nt}|O`!SsrK%=1G( z7^sQX^vyp1`Ih(xSJwDL(?x4o$$I~QxvI^g2hkNF2e%t)KD6fA*ZrQ8 zW!9usZ#90{)j7C5WpLQFI`SdE>K~7T{WD*y|5({FU-O4$-NFYpbt@l)SNI%!{G)zr z)BO$q?koAt$h^#RHnHrGeC3@@{Sz0cf3W7s=Epy*IdAeCuJ5&Ey&w;6%o7^7!^7gqHbD@6o((Y4biBu^_Q$MJ|U{;X?OA_ippsUg-OfdEvGnxl^pK%}OuLsDCL@9`SJp_cf-1 zbp07O56JFYVbIl*cyzgM^yy`}vvy~`wQSGX<1u%3S%ij}>7HFDl3NcvRA|xou6h4^ z#FtGvUZ-n1)>W5n?w*luC+GAlvME`!@0aYJ!)U&NvA5VT zVL{1Hy-?Y`Rg?b9ovOZcw&i+N{kGKasy8O}|M1qVv}BczIC$*;Z^^e^i6rR) zhS|j~dG^Y6pYk5<<=)Z{RYNXM_v1cc$D*zELz5+7zv}JF%_rWz^nLQ}i%wB>mH9>U zMg4WwA8H=uEx*WcN+Ga5HukTCOYC8ZQ?I0sKUmHE`}l+P-0a5$R&cW)7g!_R{)M4F zaN5JPtmY&9YmVq%3A-re>A=2=q5R0K7mf%0nQkfD?u!;ouuc6G+4AePoJ%Cz=W|Z) ztY66`_Ug*Z z5l(+aC0jb{GUuyr`DEwuPx!-cv&(nv8&hLX*Sj0qtmZtu+$ZB>``^gGB&P5*Pwn+e zO~-iFsV(AJwfAgxM9qzQw(}nq-+W&Zdwcrk3MTESJ9nRRZDBdoXuQKOL1cDi$=(Ie zQ&*@)ELqYvrKP#V=lrZSiwX*uw$`fuvz|Fs=EMTM=BjJ%_P26QvyfexZHx%4cjnSJX+pP2$WGdtG1HeWQ(UqipOWg|cpz zZx_mx;NIZlz+sO&=A0xxDT`$ByYm+4djh#J@jah1xxq6@rG|-tL6VJuff2G|r(Tbv-ZQVbBrzw) zIU_YW8#1y#bxN$iu%k%Z{mkvLyY4#a)+sEi@s8Gt=9(pNRKl}I%~jvP_d{Mp+olbF zPvousr<}jzg3F?dN4)j_1RgWq>>cgJYvOzU%+0yqE#KdqdDp(a{tr{YA!X*mC#eyB z0U9ShI4sk)>MDvco#5bkT&lh~@avC#+Hq!CO^I7-j@&Okof)Ao6K=DNoBdAV?RhuE z_MKn7MM-!~LFL}8w_9wM$1M%5+R=5eCiHvsozm@mt+i_|?fS4S=H2f@={L^Ag*-pS zyFQCmZ4JL%zFBK}>y*|wsjC;aN&OB!y{-EXHI0N6I7r zNq;%-W$I_B_kIIs%<`R*YYy=qHAtD<%u#99U{Km8u%q(?oAV*P)Ih1@EB<)Y{S=AX zSJ3rhdOe?-v+99m3pYxt+|Y5mImhW7i;>SGz6EJQ=dSpeNiUSQ{kh>xvDDMOA^)_u z+`q8Z|H2~m62)7OOE(_R)?&H8xSnU?6XR)e2TekGE5c56PkejBGyeQz+c^?xs_jRL zXY5x{Gycn;v!Q~qoB7Da)t&Fl%l2sh>pPM9Xo`@2soCyuy)$~k?7!!P8n$n_Rr@PN zu*K|^JZidqQpQxUScHK=PnUr~0os6OM@&N1gj~I9ufBOtVuem;i$M;bhk|yZJ5$nR z0Tl@*RUO_due?7^>Nz8He~%$&?3T5nTi33Q%DNb~Hugo++7&7pTDNXRMNiMl&0f3v zuK4cSTXl6yeg8k-H}j-QCC887KU#m^+x`7c@%c~o&*xP?Z~wpZy@a6d!~N`;_4?1f zgB8w7UhQGnW?@PFVJ#W?}q-LeB>8<`VJgnK5!pUd9M6*}XHUO6*kh)IGYFDhekYlHvL! z6eqRBDo$7|j1XHR`B={e8$L92IlYDMXU`g^CdmwcYXuzu?EosMYw!NoWM$+}=w$la_h@$eFe>n(2?<%bTkTHy^vr zw8!><^^)ZD30Xg5FKuCYS$S{i$GqPAy6eSRUe?t4PcZw`DitrXw z7fQ94Sz>;KH}=w=gW(x}1bf$~T#|m|dAq~y^{zA9Y+o7YMU~bED$g{T?6a`+>+9@o z=6SmdVx2#4Tg$uk_IA^pyj=y}XSebCu=a{ePnQY}RK6E^H79yv#q+FXoTZ!(#kZGB zT%02Lpw{PK#-h!ax6DX0QCo9vmEY-%{#l{6(=Oi4jSB6}5-?_d+c~eoByo%J*)rvv zi0N|ELwE7+Ud^U-ENbzg%KCM;TXc7t%rLq8CbxUpq>0{VzfN2cdHGtJ+T__iI~Jy_ zE_xhLe~2^TdAx9^`sA_uh+|Jh?k|SM$l}=^5rT)lA(Ur3GtU%Zj?q!_&ns zlF9~qrqtwbU*sb!`>pWux?B6UGOD!hR5=@a zY|>NhS;2-@#+hAEq2Y$yy>~tCjX4inl&d+y;cefTah@obnB0G9@}dU zPdm_gQ}^;Dsd(P)2^(}pTPurM=0zJ#@?IJJrQYDkQ?`WFrdKa2Za8y0_VkmNM%5SP z8}qHNG*ue1oYZM|yp-b|DY$!mmhc76jCI{5vpl8u*vK+9A9}Nj_v5=My;FUZ)h=GU z#r|G;+d&h)H?o{Fmi10t73Qhbm$$8Vo9*nLyjJ7h^%s9Ema2_>+;8Z+_sSL}+1j0A zcV>AoL_Y7Yzf`s6(z*)I$0c!jc}*OyI};+()o!ox*#0;KZl-KI!3 z@xA8ll;ANhO*!zTG`&g9ChO6xiAn2Y*M2bv}a zW${@*$w1brg(qE3Wf_M$^q5!gK5KS6CtSI4MYHD$sj}n2>*`fI>4Z;T74`K)})8uUo}!Zp@X9pM9Z$=jpnePDPe z^;b&I@91~81s`XwE1dS4alYjCwfoixPiZvV$`!dL&P`?QLFSt>iU%hpZmPFj9&faG zLl?)UuA&)Nj|UqX&%R~rrIWu~^LO_)^T}G>$JV`bm}I~3qdMc&Z7${Ue#?@gA|A~; zcqg@Z0o&$Ekr_6DhKUyfmd)LsaZzcR-}SY6oAv+ItF7L1&-Iedw6v2OL{7VKedje- zZhftF(@OJj#!iE%;$J;QuUGJ_7gc{=T7M~L^24n`6`}L^*2f;6e16)=_cpC3m(R}p zTs56<{U)!l$l6;TCwP~C%na)mpSx|>4B4D8$$OohKCJr!PLx#j$X$O~{-t`Z+yeH} zs+n;YUsiwF;1_SLcf*R-Fz{~+J1c=@^Z zpKf#CFA|@rXXSrV`kb!lA?~ip+=t$B8cX)6zcJ7XyWLTL*x-xfZIZ^4?cZsq?!{UpgWhnQEzKZRn?BeUPlt-Z+od874|dS79hg?T +6EP z4m?rkZegffRh@cVr0C|JxrTG=kN9&fUcTh# zM~TWkuJk)Oy^_PSoU5&Lem)nMvS0rB^CJn}i7^uHN0)!(w%UI}Kl9I~ zbL&5>uDAL>slN50+;Y1sKd=9ho?CxqE)9#(UT1dL7i*c(qbv^}P?Q-U1~zUr*J(SGLgXQr7Y{bE;WinqN}iB6WT9v_q_A z;SGZ9=DSz%9pc>K7W2j8X+4vO3_t5swvT2RJ8nGvwcv}&b?dkVYhpia-MuX&uP5m4 z+cw>;;#M*ze%@NtsZ#al?aPi2)25s%4f=ONXXQHk(mBq%gyeEv-6DK-%tGrIWYv#B1AKzBx6_YPZr0qT)RevM& zPtMtW5n11gV~$#V{XExP@+SA|ss|?x#ZLdasrx2dm{VFYp(Ni)@mh}bG+ntFDo^7U zPwZN(vB6C0#mu*L+Qu@fk+sv7oS(Azgnz8~{z}eQf+q`A^|npv=6U|_&`CejwI^Ln zQfCXjU%c$evF@9%Q~nedWp2HiaCu}wPu^DRZY0Yd$N9}X;Eg(^$P-xY44x)2`B7wyz8VU%d<1?bgNggioF7pAZ&S*zzZJs(Z$x#I1*p zU-VPAzqNCQUCP-}>~gNv80ulfJ3nFSzV^5?<|muTE>lx08>1vh07UxBO`~S-Mo`3I9*6 zQ`+n+Hq2jU=iyr3H)*qo)wIT9)|9nNB)14m(hn-iD+oJ%YUi}P1^$jo@@p6Ut^bl) zzi7v1(RO7K%9*TN@j znw|Hrdb9i{yXX3Er@c@7cW||z8~IjtNx^A``m-y)RfhfG{;T+YY%`TJ6>KP}E)U-8a&mcgN^e@*Hqv^^83_~r7?sN%x) zkV<{s1S^MNF0Vb`gEPCAFZ!u5Rd(qoYr~+%GsW6%TJHRn@0PY!vUG6G>JPg5-Em8X z$V48UeZeOc_Ra8re4&n|DxS0VyW)d&Yy#^SUX`%r7rV<-9n&JPE9~WJl{2^2I3JvH z^>*M5GyWL8Mb$6uZoILHsIL=P9QRA?SZQjcI8DU;YN(x$X{spmmL<2MH5GS#QH$c)qIWxTLrgH&H1q8h7dI~2c{Kdy9i}T? zhpz5(jJhfGiTA=yewlNR&R?vwpSFAAG36J_4qQ;&<+(6Tb6z^L@zw(;wrZu+|NX&t zY5T(Y{8zMJT31Z|55B?Y6sbd%U%jV16AkKVfwL+BcJ@a~o7Rl#&%W6c~q%@{q{Qgop6yvvJa^vji^^QnC#Q3G`kZDtX4qTTVBgl6#(lx|Cgam2&*lA({wIIgu{rlZ zL!Ig`=No3{Y#r_$ZVtH;6!)Qzan;QuzRe-GAMKz2D$0iS^@FcXRX=3b#oxDacy3u) z!~Dsjs6OB@W0bvzbTr$`KVm=gWES``UkT~mvrlr{Jtpa2zhBPPz1rBmQe%l|PHB>= zNXEP6&*q-!PkNN=%bvOHa2b2t#$cJHy=N8ui!fH*D&p$OXU{ng1en}UvTz# zeh_W`r@7{u>t=^i$!jrQ(JwOtsyE*I`6c+5WKA(|#{7lz7w6mC{N}M;ec)e!7&HIM z4Q`IyJb#@U_X@12s@LQGR>j5Awjod9aq^$5nIWq$ewNC;G9loF#aXxc3yEtu; zRk2oYIrxpW$}rZY*yQ&Ua9=P`^jgyPco;Dl{E@|m+bgg zJmY)hfxniiJuCUzw@6v}E}0&3rt1WwQ`I~*p(iKbYrhQbKO`{eeb}`6)dtZW?$6cE z--;By!S{TMrbF17C?*%a3E|tW%|7wmvZR$=@rRJdblz^0@T1?oH~cvr@k8{;wbdVv z>Q79oZ(aLNG|H*|gj<5)inyaQ#F@?o-=4x={H*!Yb+5*$k@jnr+}E)0U=lv@Ie*rQ zi}{t;W+zNi-!Hz@yJh)?9WIx?Heab%J?ePG@zc>whu=(;Rco5A!CYpjp7w}+g~ycD z6Z%=Myk4Hya_Dk%3Fo5|S(1ik4ksBm1?sl$xippQC;N+_&Xx0;O%8Uhn>XRxfrAVE zPcGVfNiraRA;YJQ)AbIV%XhTu&iTr7NIl5s-C?~g`q>B68hT&oOZ=7gUH0E`U7hT) zpX~KL-~FxH|4qN=_@8Ncee-nz)rcLuf_%bX>{RBQzTq6cVfwd+mAdn;Z}gX})_!Bu ze~itW0cinpRDUyb3MJc*7mRI-pjH4k}O}YKRmsVTS?(R-^x9w zMA@uxt4$zUQte<@BDeuA8`*b?F0wtxIp4Ti+ zOX@shw@EhX53^dqH^G}(tm}^cyivsL`*g*Nv~6oX-18CE)(YKwy}aOsDMPW@rr`4* z;#PP?@J*P1JlO8F$z=6~Q8Rr$TiBaauMNF*)=r(8oX^h_eywin5gt{i8;Xt1vVtbR z{BKE0$>^rKm!;2@Nocw}mtmb!Scu)p^Y7M_wjW6BJz4ptaONBPttaCgPS1>)>laZM zdS>VOGNDQKtEb+-$W`;_!(MLr_X{sS5q?mZX`J6!s-nx5kTxgTrads$(sKDKhiSnK zbLMPPW3aYP^=)X@y8Jw<#wOwMvvW4zQ}5lHJO6Z{`i4Wo6^mvY-kAHHv&XU0!SqGr z>+jlUstby4c)xdk`hZ+PN^L(*m*F&QsMCfhqk2U z;aZ+YC)}Gl(dV1QwBo48hIjDK8-t?eB(6osbS@%O)tR7FDU{q(h?$d`k+A9n7 zU#f&}I%<%Xr5d$7W$D}#7D*rY7JIBnIuWhwsC)Zq#MS0qkG2M|ly{lWc4Aex*?DXE zFJ9i@Z2PtgymhfE@xjmLT)gw^d-Ucj?&}u_HeTMbcGV*8Kl@apXFXSJcI9U-uwC+S zS42|1#-DgOPD3pxxvR~Oy_`dY*{qkx7d~B|8L`yg=i>RO4_n!~YIHmH@Vfoll)}y# z8_@1sr}Cd?+Wi*?PUV*+*h{TS&)|#n+~&9?n@9PGKx)`7lS9c3onI=_l=fa&bmMqM zzOO^v^`M{X5&ZKHUshXr#$o;K3(_YHK5F0m=eO^Ft5#0pr5l<(lddnfnxPy!lOJ_p zddu^Y3UhV_hKYO(3~JzkX}hz+^;5%M)=OU%wN3VxsK$&b;jOecHOrmrmiky2X}=Y+rJE`#)dZ{r9Ym zr{yS$|5)(-UG=W_cZ#Q%KmYstt@(YX0m+LKdC?@p6{{tnm?g(`;Ixpr@xd|isO1bTc=LD zC%^rpEa&~B-)9_IYp_rL82^tCx`O)L76!>rrq%~VZO#ckZMiM&1=EwBb*rC+cy83W zDpc^}(4n7S&V0G`>Cu-%w~p?7^X1l~OHOIuni@`=34OY2l9pzb;Z_fZs*h_L)@eF# z&vD%zK4sEkt{3y-XH=I|Wv#4RwL3$rb<(b!mCsxkpOKR5Nn0X!(M!v*Ma?93^LfsU z{@!&Q96pk)LG`a^UMwlSrujUi*z@GnkZtFmgo^|}-nw6M!*$anTchL|N2jD~l^zZ; zGYc^`U4P^C^OLKMGH0c{aL8Gox$RnT$lJXiBf7U9s=71#&pF9yp-NYy<5rK1>)2vn(C>&;O`g+9gvcU=7%h8>G*w@y)te><^F+uO!xi@?H z?W0rTO3qwzJsPFnne@W*`_>JYJ{5#VZq-@3V%gS3SvQ&5Y*oF_S}#k>U%F}cf3?2} zd8ePf%GqtB>7XG0yL_Qv{C#0n)~i!K24DLrS?A~IdGZJAdbJPUlKYchhVD^%aHamY ze$}xbTRZNb_@P^;{^+~WGXsBdyV-RCN90rf2#QLsf9P-Yuje}Jmz{4;walD#rAzW| zShvqx-mn=CQ!}r!$m9evGOzvLYQ8!cVS1+!YJZAg&HrK*<`tDWF-r7cY*R7mi(f@r0-*LCu++P!d zndg*buh+k}>A~!&H|v$oFDqHKYUB5kzkh=sr=>|-+sS0VI;-0dwr97?+cS?}=-cpp z-(s_W_vQO{>y^ARyNXT*bnME|48M{ZEL^bU>)Cu4pL=`N?)<;JbI-a-h5IK|E~+W| z&+zv4uS0k2_q-GD=q>usZ7I!qRBcDXn@=jc#k780bK2Om{j&V&Ej|f*uBI>8xOq#S zl=+VMU4pNdm@`h)UopElTw)Vz`KSGdi;Ap$zX#}CzNJ^M^d~8>etzMUNmpB*hkn0# z(Yu-bpMolL)kzPiVwVblDko8>M%iCX>xTbY|kzdTg!s&jD$5ZjrlJ_&@xx2#{XZauLO!%GqCA@G& z+u_Z;@ixDb<^FmFbNNrY7_7H<2Yc}H@~!t4sR)``q8{ z>TT4Kl$ql@Y3uIohFhc*f)1O?ytIuwYCH9fS$*F+H#d#&EHdv-mQqk z$xp>P_Yd2L$oQp`V`^$UIRbZoU$}6(pihXlVRcCYwwicO>^ zAN#y@O4l~jb=a^~N~+kWe%d&FPme&6a&&VkTXV$W%PGnobsBkwCrkFu@ZNoTDf6Lv zLBWa1R$p#>e7VQPKxRjYlF)jkLhgq@UT3UUQSvOmdV0IEQJJgsW78#Cb8PpoQIcO! zRFwVt$+@i-rOP{i2&a7URQf2gG>i9mXV~Mq<*$!i4tU+Eq<8o4m;a*MJ-RC{=m!5f zaH+S7LH0lM=De`=%3LaRv zL&f9DeVT0jKfQEW7V*^I^}Kt+wWw{*S?!5FalI;E*6K!{RtxowoYvcvbmU}J%+-+M zNr~I8-~5x$?L9|#WB2#9hC76-!V)tCo0iU9yCunH^5x1&TU5XH%!!}O8~Uc(Y5RiH z1%nkzh5uAb9*^18LN zHYG0G<{Nt5LWpPWw_9bdUXwS(eNi)ia=l1JKYQniRhcWI^y2hh7N^Cv`O3UXQC8Pm z$J4)WPTo!7m6I+6x!%mYe{Y*0%tV}tyZtB+0m8VbVEsX)8ghAa4C{h9U5cKk0je7JeRqVBpEx?f(W*X_>;-?FONyF~wj|HQK5E$bKG6z8dJ(U#zP zzxbW)+t`fd8zW!b(0O%JW>1cstjcQz*_)4r)-hb`s{h4zQexd?bLp?Ldll}O__Np_ z_{#b2L;987A9&|2{r5h@njxNl;xfe(-!ASbXUP1^aPMotjBWe=IlEq-=Nfh)LpUKv zaBU9fTb)*h=#48_a@E~K)C#!ox;U=MyL`%(v0d4N1m6s_TAC)b6umO24L+aa2D{u}J;h_WOq<4y4XnelpkCcC(ddQS-Ce zd7R~}E&Ch&ju6tf-2R0f)S8`q=c3f+E47cAID5K% zh5s-!FqBR{(BM{IbUQ@gU*Fm%J2qXiTIMOO@PKW(XH92u_Us#(-jm`wrN!^C%_*8J zdUeIdrF}Y=_c8S!h@K)jL&WI9zvhRs6(SPfJo1vR`#mw3`|$kEH#ax`e!JXu_219) z9cR8Ot`itckaU}0y!pGsdx7+pSD`1^O>&i z+PqcU6u;v-TCgZuI`d8}+5jZ)lk38fk3z4c>lzRo32hlW#53KT#3xERq>v z9q`u4FI|U+d0J#i$ZbuVGM>{66<31=8-`;kKD8czBJ(Gl@rg?<7y?4m*5JBi+S`ul$7%_dC%! zM?0Dl=RDkTe1e_W@<*R)S!e&4^4OSFHOKJCpC2I~80H)6{H)rQ|8B-L;mzJ@^K9RE z9JK7=42d^X;V<8i6fR|Yw`86D)h}ssbFydkm=>$l$Exm~F}-19=f_`1Oq`!6L~=iM z-}S7ZX03y3WW^N;y`ODc>O33{-{o(K)e;l`A6@u@b;9$ar=3}^-!|}B&#?ZNz1;oT z{B`bXeanw*xEJjD>TBT1Rm)y}t*R+(le=ES|Gx6n=c@%hS#4d78a%m7tFlDzCR*I* zx_o*{czLudJ4gLAmZpz!0y%{O^BVK0JTRxBU(EJOA%r;VpYyE9r_gZ{X>pN`irVGDTe+t`r&U@!cK0 zh5ynTl}YxB6Fioc3I1vP#<<7%v1j42XBUr)38aaC&^y`ovgeDTSN*n!>sCyg)bo&e zPJYy1Zkbw>)zuTcPKw-iz3=wOon_u)`>)0o4Elnnyp%7!pYipG`abs({vu7!+XYQu zrlwa*Xt?@&?SB-aIYZCRJw?1(|C@mBQuig{QTevT%t8V8m8U0ovlw>=PCG0S?ba3R z))%{UTfx=VeGeLMwXaKJn#@ukxn^00sKbo9AkHNxo@uO8?9I&bGR~XOxPRi~2^LPv zl@*xMXD<}<2wyaF_nnQ*?|!u=nmxDjtI+!0XSwx2ddzW|&wABKTN}9FUElfQ){d6% znQ^Wi`x2%d;#TncyhG*J6P-y{_|9_#PZV&OzlZH2YkDF3qWR|@C2Kb5XZ#DA^q-M+ zGfV4!Cf=GiLFX>8F)*kJGBDUqzF4a=`AfS@{n?0@<-(yN|M!}mU2>Vxd!m+Wq05U& zC5=tiVcD*0wz#ep)7IlTIe~A9acSD6Ch2#(JGwjq3!DqZZGzT3sJg@dzwP-C^9qYs zdyOYA$z{s?`DFU{dw0see^ssj^ZDy`hNzFNJymy97c-W7?{Ih1G@Byq@;Fslzc5Z{ z&x@X2o=){fO^lpvJ+3!&7BfZvn9`N7fk{orO=?nLclFQ9(;l2!ajbwv*(m9TR*F6qQKt~*Wbm6a}s7T5fhh_{bxRNoMLXtK|*DJ{!YZIl;8IIl~b zxp!Ue3{TGUf+=rVIBLsgEz2ukVsiYVY~wV&(|+4ja&;$Os*ir8xp?VeDVZy0T&5?! zGSA$$_;$DYrFcz&Z>k(RR=g|yoo|SKl-a=0sjg($IKktTOn7lA=Xv3+>z(wOS!Q*= zwql$gDtj#`bK9m(*_+MQY+VwoeDzYQh2Gt%-=#c*i!YlQ#@RfrTKa$Pr86ns3FXq8 zqcdl;-Bv4DP}dfx{cdIbqrRClKl0WxE#%X7yf*i!hV9FPy}N2xZ!>hAuu4yR-O^ts zw;#M`kSJp3DGL26by>==cCA|F|4)y&MATHzpPY6xPyKT9u9sXf(evNmYn$0-wkY*y z=+n)@Gh919>fXH-kZ?DruQhGA?XR%uA%YQ6_Q^9hPu^AC{OpO2=Gy!nhmGs|G}?o# zs#l)zwmcPcTK2-*t&S@-oZ~Ev=U2X*EdEwOTV;Ci_b4eSpB(RmZhvnOy|@+Z$xIyUj44@_O)$=ou99I zbLkyTxcA;r0*R+M92a94BJ#bqiSl=->YOasanp;xWn^)-ZubUlIvi?_W(Awxn zS5|U*mxo*~3Jt%#X!Wd>X_L0kj0)DPyqp-tK*H^$pz(o9>2uJ*Ze7 zJB#`*}Fls0szIFiy%4 z3%S^l;lFgv#5YqGOw^J*r?u|>@tE?2MZWF= zo#)gOekS(2ORwctWK7aObwn$;);o<^yS{Nf#dBb4qPx!SV%(DR<`0^J95BZ42! zjkqrK{>ZIqRqEHwUB1ii(7W(XchaE<+l%#vC#p9-kkIG z**(9IQm>k}`RAkJkZ$LIqvG=tj;4!n#JzpUHMQYO3Fnfnm(F=ibhxE{^G1uLUsF+! z5~K1#e%ASM8BcBt9Q50e?;dFG7&i#!?i-JWl3tYS1i*ou8UGV55u#e?-Glx0mp0lwzY?}d=8?s-u6zOE`@tdg2W%g?_jJtP;Qgm3@FVx$Np*({KU&ub z)jtR?JoGtyO3kM3Pupfcis^d%WT)JzkaOpS8~JZMFO`FBFooo$X=lEf*inn+op9y}qBF-4*%#Id5psW6HG)GkcnHM@*!*XWxR1 zlr`L4ZpYrnUHq_8zI;ND(3kFnIk;Kzr~PJ;{uyE{0*h_gH*Z)fCM5b-@0+dM+&y!5^z1*rdH)AOc8=e%GK{*@_riy`qVM${FCrlyoS#gtBo)VA&AMnJ)z~IBez+i%+iGvTSNpbS7 zD}vI<=lYd6<|WhN^Wh)>SBpv5$^2F!hv!9SVzg23d<7o-?}PG-C$JDK~U z8Iyg>i`R>UQ&{Q_Y1ih z7;I4V9qa}xdeI;a*0=GJ6qEP_u+WD~rZOl^M-+W0CQWv{oXPZgCRpwH%dSkH=1g{+ zr8(LDiUZU4`I8&3Br~xu28-}twPs>jI@xiSDcFjMSEZO3R!nxBr9b)3RV$|VtH37s zFA$vEa9Ln-%rz+{=e1y=+1K=BKv`H!h=CzTlYv1E#R0V&CSPn$JwEp7cH^&q~aV4ONVix12 b$#%!p#R9xp*+AlE3}y_kgcumyE`fLe?S@vi delta 19980 zcmX?hiuu4M<_+46T&r%T-C$;AV0gtjd1IR7WIslcdcEYF#Ny)Ku;_5%aFKs~GgA^| zSd$%^yPC~~^g6d*3)`^8Yp(*ME@O_%ZN8I=J$`AIP9E`oFJAqZec!jG{;{iTIk&94 zb@lqc?tk5OncrugbP#GzPKBp?YkGZn$ z-Ld3>(K?M8heGQ&`e<_~E>2+EoV4>=<7=l+W$e#VQc`;~UL4xf!^)bzQRDG3zat;| zdSck~xXQhgcB)RhEn5+Ou(~3??OtQ*#c&Ug>F7e#$aA{pn z;>Vj=nKP|F|Jvj^DdX6=*UuKclX$dOZ_1s}#fK)Gx#4FRTk@spWc}suvU_8m`Tc6j z-7PvjF0JJ7sa?As7p2cpj1i1(OYU5HM)vQ#d1j@nXPn#BmneF?wli_LzNDJ`_fHq+ zD!Jac?Kac#qjTIw?r6srd{r|G+UBI`>+eJT_%P(Egp4zik+=BD*zSg66zZ{BL z_RvFOx<8+7q|+)9O1PKYUe>T9K{l9A((D zIkafn*42LVxGe)Otn<=LT>XfnZzFm6s#LcGfO?R9v(0k#~)2#@{7w%}d;Z zows)go%%nqYyC&wtJTJDVy+hT3f}oF$y)D=&c;5S#qYyilNEmc zd}f?(3!P z(I&=|4xZsq%1y3UWpj7VKC|8`MxCqd>FT}$kv+#}9r_hCv8HwRoJ!1c~d9)dX~TJnoxgyUYX53l?B?{ zoV|1%Zcn(z_QLJSwsYbyC)+sOPx~m&B&v4S~=+=~>XX5j|I{(Ph{Q}iGXX>2! zn?3wKO|owEi~6ncgYi(Iz~L_!?R)ED4khY19L_xywLo~{A5r^*RiBc~dj$Cfj%x(5 zs5tGHzO;pV;!)N)N4!Oznyg*^!KhKp&i}FKC9B}&hwQ7To{LlVw7*fzrFL3k!bRQs z#n*rHpL{Xt<#%p_XQ^im{3gDb9i09*{gjr|`IqW1&hol{Uz=FZ`*G&P<-!Y%gZfKe zxXZ}Boao0nDeFbI+=_<}l3gA>jFaH|kfv&jpwGmW)UIAKWG1Pif*cas8f{{_I}yIotBQXV>4qw`aU_Jegg1 zM(Km3YsX@^KJ(=zDYhOJnRe#ewC2=1WfL0mlaC&gZTpy>DD&z$52yY9qxBPXPR~l% z5y6@Kao676%2%ZK2?rioeL|~$+YYX?JLP>|x!qWFr)^U8-L2d5IwIOb*DXydh(ES5 z;AzM9s3wD>T_Il=tgFx!a&-Tg7QEs?>zgAJpKM*anMt*6<*p0TF7G`;rBAdPrA2)C zJoWn@X8xpw%?}q$>fO!5{qMw~)vA}bZmkbbnPJU-;z7=-ZL7MzUidQ4&QG+5Up&)i z(lN_<5^B;>=G?xQ-`}1VVD@gOhQb2xKeH{K868mRHVccL^^Q6C$--WDp4R`necIl1 zZVc|6InmcRrRqq>@vjM+Z(mB9o_OettEbZ|y~CU0B^xqCk9`gLm(%g^q`(9FAmio9 zDNd)0>YG=aF148H@$2VXBj-Gi&bOOpe)!U1X6V2ESnE%r4QCV_^&9*c*P1Z*td%+N z%S)PD@>l=&Ciz0nG&@@co0z4hojOIepR~JX#k^w|dcWt1;;}u2=j~ixN$bfR_*ta% zT#)yM`c40me?CILnJ?{B&SY9& z`bw4D^oI{-HKqpDGX!`uvv4smaBwg%FgP-pZnK$vfRBM;jw}O%BE#gutIG9rBROkg zjz|8lzw~b3@~f90e%-lmli!<&Ynj%4vZp(Rz4xV@p0sA_lT&4nY&`uh-C7;{^lkKQ zFE3{=-leK7i%rg6T;g!sLG!_uucvx%sj<9rXeBluJw2w zZ<7CPcK);7|BW~Qyt6&-7f-|7PuCQ#J-%Mw^x|Op>KnTk&bl7*`Sq%wyr#b`h4)Jy z$$z+=x5lnRp-#Qy{?YfWA9?EHP3sp_{0lIx7dgWBXJezj#E;1e(;oXbb38oXR3R51 zaydTe?EKk3x)tvA?+-Z}FZpBBuYaWi=O5-b{fMq{HLVYK&8*LVB;WMI{mAnKGFfWX zk)1R%DA{AJ($$$k`KOGS{UwCBP8CHap7PrITFW_1degBf^&2#^c_Y(L{ZeR4Y1WkX z-Rb&PdZF5;lT)I7cb=Vcc3F<$)3V@gvo0+#yOHLxSkaehwf4k}uGPHQNak3bEA`yQRd z>G*fc>^TK$0d}yd8}5ZPS$KKE*osFcy}uPlod!BK`ha+pBG6!XC z`xfJ*Jj?Q|ufA$*l(5!Kzx34!uFf}Qo4U-Ns2mfC3QuRZjEFI_4_&}IX{Am>m}>CR zBh2qKCfi8(F1s7(K2@T)-g^FxwOR7q(?71rlH2@1DfQstO76_7F-KIn@An^nvCs4T z?7X>Q2@6fkpZIx~JyveIrurcB^P}SSqLhBc^2kl$w{&l+mq!=%zuEplZ_nXF{*Tjj zc80%Gc6j=FiL>}dY!<-L;mtd8L2Gisq9ugTy$%aaZwn9_q*wg-f{tn8@hzg zPb!|V`HADz8(!xHuS$oWm5_e-I^sk^Sp9?Wi{6n&dcsZG%lW>1FZn9d$+K)m#>B(E!|4)TbPxTZ94AU@QC`j#qHN4 zK0{|+@1qfc6H6QuwoHv&aYXg*jl-4`qV{fJv%0i#$GQs+VYW}_dUD(M_eTf?hs~SL zJ?%(+>(7Ao*R}=a?Ahvkc#0}#;!3vX2iqO?FF2<=b6R`R&X7H2#fh%DVl`}&B3p}! zx)e_cT%R;$L)YJC=Jx0r*E4rU9GV`#YY&eer&3qd&aaqvrCA^S?juTvop)R?T4LiKR2lce*@CUg@Fq z(lzMi%ax#p* zDns_{u)HbFt9QLA$eNd=61+B2gm+W8(>Bu$Yj01=x;?4OB**jZ<^Q?bucAI}uyNAS z41O5P&Rsr}*IV%hUwzoKZ#8$HerW56eb=-6=hObHNj@GT8^y(o%RQTTe0H7mzUh5^ z*Q2ifb1F8!ivm}KPqW?S^*O(-+}Ue&nx;U1=3Mi|=QpUHy%yeBSuM6lwn_82^^2a{ zFD-=YV=op)99sT;MPO3y^doCtzxv#M_fM<7L207Svt?hlem3Dg>77+yFJkfheZ^@h zv3QfiyifCEver#|rq;#0vFzYHRq-7HYhRVid3`RkdcR_A>O0G)@2_m#vPEUz>TVz1 zeeY7=yUbm>J>b^8qtVM&uV3-6%~&=oB7Tz4&fD3yLhqgF_txi+TvfI0Xi)6ucAt+& zH~TO2be#M&^~99*itVdU#eSM{e*fd*`qqmfhgn?n^yBVWX6_Drdi%(V;G#sU1v^96 z1PM@BWoBkv=bf z-J7OaFU|C2rL4a$N9en%HLF%W^}Tz!*nRD;?NzOZ{cf%dblq8U`%v_!eL2UbAMmR% zjDBb2`+t?k(`~mu{nwZ(z94B*J;$B!w6il^v%AV8zif&4Jonj);}MUNJ~27(-`*pC zeoA5P&f^_L5yyL$*A+#Eo^$%jmm6c{&UIpb?K1NcJyGrYskNWaPdZR>Y3`=ItZGv} zhDZt*#oTE5`l9hbW1~Tm#rc1%45A&$y2~bL^h}oV7w?sK^4KePgMTG^%fDsC@pj5T zcqe@~O1oiq=Ay!%4OTqg0PkMeS79>B`(UO|IZsDelPo-*l6Yrb;ke@c?IX+&ecZ z-kfDybnm<`yON|GwxF@^MjP)HrTCT0y zI_*H$gi6^^^8oAi2OG{T`emiF)O+c*rD5yy7xG7Kl)1e%M|-!P`tP^1!t19^{=rm{VDF%cUl_$-CWtzEc45Jd6A8IDrXSo@_P>4;Tg|B)^Q#$$-z8j~_iB}`X=442GYOAB zyj#`v+^|%3dx}6Mm$rSI@4{%~9l3n7+nuA+3V)woofQ@qyYc&#HLI@Oe-(OFdqe4} zSGMODuKXyxf+zR%f{#3IztACm6 z#_6B6Pj^; zZ~b`+$1huS@-LE?SABlmupzv#Ym@(DZi{C9hdTRu*guHQSzIr+{A1~*dj}!~Z4~O} zOxC)Ql<2bjK!4Ees69>PkKHA%%&idXH!kG$+IMKD`V2k(P1ovs>O-geGW%mu?Rct& z#lCUAi)rbep6?sNowMT(`(F&3@6}V|wQ;()=C@6np^8^4gx)(ZU9sidx)0r*%QGg& ztAAg;V%N#uY@Rjd8x9{Zu4kKP`k{{bykx~ahy9Jh0`gDqr1a_?Rh{)(`o#`UU*>K} z&Oq}_-;+Edu{<>o)VSBxUG(`UFL~q665G`8x>_>9e|Jn=ed+O+MOy29i*Fd-nQ_oB zv6%gT*~j;O|K2{kKQHo5>DGYljC&tA3G(F}I)7>DXC*t2bNmkvTMF)<(R}@HeXsB4 z8@DfilB&^fyER)wySdi?@FmY0^T|6bOU}!D<3IoC-i2C8nG6+?@(;5e)mInG^>4Z{ zV~O15lB>=oDO{^Niv<_mvpur6a?ZW@0~7zU%G~T1mq@VUEPwdi#B5(|a@kSo6=^+T zB0V>J)b_pK9H3dq#ar;KaM$Amwst?0cE0-b0+o_2pDw>R^&rB(_{9#X^4}J6>v-mN zOuF$dTkzcXlnb}t@&7(l=l6{#+^qGNk%ed5z3>b5Q?=QzoH}@EYU2DUkMq8tP0X+; z@nXCY9k;AYt^0vW^DTdG`KRWu7rRxlZMm7!wLWpu_DgH_9yvW(XLX&}6&3H7fnt}F zbyS|N-(3IYfK<^RrqkOe-PvxKCBIbq+oWIKKT_QaCfS|4B$s?;{Ug_Jzc_{Dll>FF zJ!Xx!U-$QvHpkO?nfVL)P964LvBUG@R4emHuInLQ?)jf$K8kv%WN1xKF_>+=WZp9m z)?)XJ8SPg(FJx}BJ!@XTJuxpT;4}84M;@27Nu0I#2n{r00HfY~;#+pgH z_V=fL5lMVEbCJ)@zzcQl=ThSmS9~n3T@=kS$zRN9snjQ5lkD2xmOq7_6;;(ubP7qj zC%^VMVi-WC1|CQxhdUQCp?|M$Y+vWk@q$L91F z=8K7&HLYUgGt!c`xU7#&&e_McIrM|g*FOwb)=yQORx)Fkdqiz?WbLVKiz8c$wy|=Y zn15;FMu`sp?AuN(4d*j%A8PqE{VT8HA+4%bTl?=fd*c)J9nOVseG-4PWuIGULd^eo zU*T8(UHz1P*N_L>)XuyHXp)7^f{KR zySi%L$O&H6uxLdG*QxdYWb5phUVT3ICeLhkZqDDt`=6_A-v6%Mcfb7G*82S)lp1_1 zvL&UR&D>KbPd>XO$LFR@cVfNqGLZeQLp zej>J)H@e5HUX>ypmzA<&zL)lrh+`Z3XYE|H+S{`6+?L1Bf-J+&N4dRfS^fNT+iWTB zU-zN{Umbsam&gCr>}@%+dVbtnSkLL)J-4(?Gok+Z3W2X`XAj-tWm+v=`Eon=yu8Su zyycm*@8vcA7WU@U+tcE<`BbF-&w%d0@;bW}kwUxKF^d5ZlrRAaACG`}UTbGP< z7VpYFRat4Mus~M(Nzl*3aZ`;sYmXf*v@xZXz`UOW$o9h z@HzOHCDzu_@t`nMc<8!={YM)+lil2$BFu80<=oZYRkFQ&qXTa}!zZiYlGwxFg7)r- zc)5;o-i-~xCB|+A;Zc?{cY}H!-b+u{S?(ajBXMuZYCTy<_&B7sfh=C%<|kx&BI_|HTXM zKeDsdU-=;QZ^^^bKaq#yKZJ9cD=5so{90sfPW#&mS=Zlj#`hhcl~nLqU({W|Cs|*0 zaAS4GSKi7+<{!;@>!<(ApIpABYNgJ?x4$NEuZc8zHq~Q_Hsk6CPJKr0&&7Y)HkTeK z)K>re_sX0%v$~s=ry8wWSX~##nUf!}Lu%R_*Ozi$CuUuqWW1+&myNfBU+ZVb+FL99 zdw)LC%DwILah*%E*0zk5XKb}r%@fw)jG3PiQt$fubmHd77po^6Za+Os$dsG$+|wgF z%NIR}a6NCaqI&Wk3ERnrTx)(yNBq1zJ@%rsO!B{VVWtLV@oz*TU%16gzG}YlgX4=e z_1udiPgW<(y>;u{Rw$vJA-!*Eruy{H>q{LaPJPW>X>de8tEW82vgh0>w?^5y?Rz|( z?p6Mr@LSuwe%oWEpXT+8v$k37S@+IMyv^-V(8)NXZxast_gW=+o(f6G|95}G>nY;) zzN;dA?|yT6Tc^5IbFt5>R>#LGn~igR$Fd$cE97wfDTBhV(&;nqJGx!jx6L&FkIjFr zh;1G^(x8h&avrUCNwS9;mrq26L#aZEz`DX-_2UIUA^;@@ma>bN%&I?myk1j5burB*$uRm4S-D}6fBR?+4*WZZ= zzn8&vN6qi?ym=mTU#j@BaV-6~?dw64ka_pE97ybqn(pG#emAtSVo8(ok+ag3uN&7_ zr0{9Zx@#cTKc`V(Z<0sPhxd^;(%XAO|II&N&l0(7u8!~I+3qJ!>sVTPhS_=e*|~(x zQw@$&4Zo-0{?GAj`33cB3G*Y`|0;FfcG1rhcee02ooU#{_>qC_=w=IrdB-PIupWL? zDkGGibS3?vpw8mE(H|Vs9A`|s_PDHX(UslZ|JYC1Z(4umVoL>E^BqwOCBH`ue3O<& zPCPE_B3-XEsXmG0FvsQNTr!JdSd_0^(vW}Cd+D%5cwWOVzDrzJrrADao=yJoBVZUrzaVlFPrmaNlgX2a;UpOD?nM6yaqjeGb$#k4-(S+VaBcgis*^RrKam()+})_$@x z;zn$}Qu6X$n*Gyv{V=K2JN?Wn?%gx3b!Cs&x;+%li|W{GP`1lNd+AD1u5^x8a8hcJ1T^(C9tz=E%Wuv?)X=`W4Juc;1A70VCXWrXoQF%p|z3ki{_U5e;f2I91 zTkoEV{Btj3_lIZmvYzO2W#{Fk%{QqtUprauuCMQ$*Y%IYDz|wVA3vKbQ}D4;@19l; z`)11{iGq5M9+g$j)3=EDfQh1+-o&Kcl-A7HtHUIdkZ8ej*wm8&LSL|}K&%9T&M5n%fbuDk* zy4;xE%gTQ4+PZbMRK3~ybuqgqX)b?flRG<1?#iLHPkY@=1oC3fnC#IK>XVGR5wV{2 z@vd(D#3DB3wQH-UWG2O`+={vE1;-+qkbA-(0O!H#ajSR$-0#5$Cg2C2QLpH#!_y z&U!ujVndC()myh{hH0~>6sB+VHQ;!6ci)P2l2xS&X$HEdG!C-8=4{@^67@LnEQiaX zZf@Ph>`G4;2^+TR*Unk_VTY%ql70QQezsYUKke0%N|8#O$p6~y=PuK&GbYWpTe{JA zwsE~gx`gIXPK{2TbZX!cU(KO?u(x{5f~+^;^DkG;3BKx*zt!PGN1Rxz^!-U#m zi0MR*1E$#%9ymXhYg)OdWO4Xw!+4Vmc{eY;<9AW4f3A_udaU_rDPu6>W42#T;nkPq z_cTA#liYFp(owGXwx_psz6sBZyt?f7wqr-~K6AGj)%Y|%*y43!lfakNKYMnFnFLOd z?LM;D?(UIa275QhewW;JU#5^%cZZ`@e%gvJ-m5=6Rk$jV!JF-7vx@)f#|NU$DQ!!W zcLiSNw2)l1>}plLiAcv$mO}-L4qj34n|2_(X4QSC)_3uXv+J~_j`7s1)BU`R&}2g)Djk@2;yxYuujPsGt9${+(JoSNy^c<)Zt96z_>=zW3sG^GRiuYz|5Lrt`wF z*i1NP+k%XXYiHh?YnOdZXW6cvo!T>B_*r~ODLpqSZJSB)ukEvRqf`ARpImFCwlAb~ zvWkg2hpuIz)61*=ldj!KzHH^D@N&7Sf5y~#9BYeziJzZ-G+p-iBVpb}k#mcGmq*qs z?AEbMEMC6bchZ$Z!qdXU%x7)9-L!AkjBvHfAyu>H1-_K63SSym6|waGW%-b*Ir9Qu z3jLbqC4SjFv}%G}(95EIT+;IcR%&+dTYhQ#OQWe}0-N4_5BiKjlEp$_vHMKHZ z=;qaImp!^1PvYwL3U^xEcsh6Ptm~m|L5u4Dhn{`DF#75I&wm#!XAik|_`5-_m6mYl z-sBrfk9ccM`9kiU=3iB!eRtU>=RN*U^Di%(9MV>eA_-EfKJz$u}}0=KnYSDOno7cEj!=6O6_HBOi7nvf$8gW+~>o$uRR zloi)MGhOs?#*5QhlTNu9&@XKLAd<*)c7VZZK^@TYGo zId1E@C*3{USa9egi=Drswfx#Mk_@|3-oE{?wN**OHHZ88`S73O9|Qs_qn@V!h@4ul z=^-9r)vLDfd4K-HurTQ9%v1Uwww`#u$f@yv_sLA=(>hb*CuIij ztKV}e=tt_*`m^g-{8`1ad(Xm&iVK?-t@5yWVLn%4tHvDV$eya(zgGw5d#gk}oVwzp zbZh~q;B&4pr7ae)SGdg>rw6VdjHv4Yri(f5AOe_ znX?p2XPkQPx#aik2g#QbUayr3SkUXwzvHEI(vI&(`|87A&i|r6)&9Zikbe{M-9Ok= zt#R+1{YG%UTc!E)BmDd-FfZ_gZ0J*Sug%vdFp%e;?h$lcT-&^)o3(E_^!BnAoB9fq-WfpWlUOb99uZ+ z?1{QOHMb{y#$q@0`naNbCfcTIF0Ze%dsLGnF!f1Uk<)g*XVV^snH^U;xMN|rHjmGa z*cC<(=9)cO_eHw(>VswfV{Fu%&97Y4i+9}Fb;+7}nWHZAgNRNl#JrpxN$J*6XZ*gAK4urI1tc{Tr^?WN}_cbpv)7niTgvg~5M zHtTz_A-93#`xQ21sLD5h{~#N~zYQDiWvbY*ODjJ;%XKC7?s7}# z=(W|c{`Ng>G0an!pSF6i?sY~*>5nGCi<1^J2SzWSoMA00G_l#Hgt4}HhTf5Iu~-+& zBB$fkeKOb2)~oZ~dM3O)&*pf-yQg1dw}d}W>!>oZv5+`@0ze#<+x zvfxCrPpk0X!?x$*q;7{iDXfxFJ-XC(%5C4ra*Ow{vwe)@knDEY$+BnKB^NE`=bL@( z!lQOC{So68e%bWF3CHdiQM>p;?wItiYp=_Vh<34GGIv6b(vv+C^A4@aH0iACF;~Ca zbG2;E9@gxFKU<$XZ)?>y5dfL_pW}=^ECHe*)Sz|cGZdKg*U`@6vfItUud0iE#uW^ zi{jW~{X72Zp()jyc4yA|YbNz9(CzH2Ws}V-L|)F?m)PE2{&nwzz+Ia0SCWjcZfdG` zTYQ&g+xevvw@6;N!6ckzGIds_+Rp_V_H&)q2Dmr)E&rUsn|riJVV_fpo$CtjTJgXu zA&J~9Q?71z_Pklbc6Pb-3-u>L3xl)vPI=cd>48*4srX&qr#(w@w{*W_w6_#lT)puS zpOoV-&AaSd^^UIF{qfn6V$Hujb$rHm`K^{dc~O7ZWVzFA(Nn=M7+b$>aH{THR^zz% zBc|7|gy#-no8T<21mQ1;v zkkom0du>2cy;#_~C3&wJi@q%mHx*U4Wxt@Dbz<2XWs}&2&B1IPH@x}PU1~V3MAWjp z<}X{dXp)@P`VG^&?w{E;@flOlX7eX!0)0HYq-Vc*G2`})CmOjqMOANJ z#R*LzL$S9@c2E^EsOB!kd7OG2n&G@6qQg@0ce*b~{m%sFgF5BFeyg28JJlh*TSDss! zy(@i>T6$N%*ix1EVri=HIrkNtPEOk_!*+IMlV#%7SI6X5ZG7~M>xi$!moIa-EUKR# zX7)8GcXG}Ai_Yoa0_M8RsM6T5&`FSYZPuy+Ti;4K)%2~c<^8KS&OUdmp+coG#rLq@ ztdxnd-Qh0J{%+Yf+sxzmP2PqW#iY%bynPo`_?O<9(CnyS7K~98c{MuQz_je4;Fv_0hp1mlocDCt|mnPizqn ze5$zSJ?qlEjMeM~Z&<&)=?s$0I^M%e#8}<)x6n8(_Dt$8X_TIy4_u5u&{vA0h?HSj_KZ!YiY?@ASZ#ddr zuxa{)_Ib-KFU;Fu!GHT{@-_LF{sKuXhgLn2l)iRmf%!|_;D%OBYn>(bv&#e++a|v) zmRfk>w#`ptfs>Qwn+g5=nOdKtmsjemztt-(Z*zj|gf#{8Qd$|}wizi1Z;{d1GWnCs z4xyhM&U^B@4(P|dTv}#v^`|fMu_}#Q&W2O8vzAM)+I~8SD~I6*$FxFc)5Qz8>iMS_ z&b`<6>E6O%#g|4^{8R3+ujQYx%bsN^|AeV_E^fa#zSz0x{Aamh*Y?QH@#|;);G5YC zroSvP``h}Dt2fsv@q^r7UNQOBJ>Ts9%33?8Yo^9CcZIR-W-DR+68`w~tmkdPbuRx| zGmnT!Y0kf_(P|gA;w#5gZIL&NZ!TH#FvhL^!rBJOlyCJlZZ{)qH|$b2WtCt1EI?l8 zxx^NsXcc2uU%_cQ+UfBY95QvKiUX!jHE`O6(R7is!FNq%R$OzqRl`UQq3p9Y^@bd2r9 zvy1;{wVtd0yR2WaUHRX8Tc@3O&VSJmFZ*S3u!J|PbJ^5INlyyG>my&AY@d;_>A}`z z7t%S!M1K8mF%&V-e(IuC-tWuvaJ?C*ENOOn&wozAXa7$VErt+zt5o#Xbro+)cK zot?Vq36tvTf#R{+&zPy!vI><5hcadM~m&@^zEzuWLaZT>%{1ER zi|{zf|6=~N*z3HHlVKP~?F{4R-{!ubefZC>zmL@$YIDvk?hse4h%mnAogDYG;_+gq z54W=>)Q8s|a$2l@&UMYEA0Dco{w^t5VRG>F?yL!)qFccMi7UB*Sfl$wRE`}Irz zxIVf2D?#D>tj;SvpY2wJJeBACdh5-%%u6pbUbj4uOUd<&$XO90$A5lSP`xmZN=xq= z)k(q&r*^7)UguENxp_sG;3mi~KT&*djsm#696T<3F}zv}(I z<-gxqmREj1YybcAF?)tNNAD}k6zO&(+cF7XSDuo0V#4%9=T_C*tdSz2HnR1OES5Jn zx@xsPJQ369aMbtaKGnm^y4G^^JrowK3HsOlc&34!=)vV5XL6L(ge^%At?_$y-n~F5 ze!|0TUC%>o!*9Uc^>TSE>50&Ias^< zb8xZt^fcpO?a$oBFK2dhExLUD>+CA6_0c=8*ziW|3}np}W?lW{#k9>!o}X@OQl9=i zO@8LAh!u0KCT;$jb1yoIPw2ngrkP!D{9w)Ol)Lt^8;UjgwMtK4>vlct7kh2)SHr5U zj=r-U+#mm5$hy`x-QxG-jY9b;uMP8;ZNGjd=H@lm@{$j^f$aB$3OaAziqqzjnw811 zLSfIU={=IMOp@m=tx?~0tCi{QhP_j@>INZq0eF|8i-m zSI1cX7z2;$ur@8hiCM+4-+bSu-{F@=dSQZ~uJ%K6o`v`RdJ*(wIgk z`Q6_l=H7}i?y9SCQWbCF_}sUzOnI^RU#HCNy}gHgld~=xu9+nLt>LMgcVfvDljIlS zP2z{^?hBtT20Ixm@Gy4E=dFwGU0wE3>9oU9ei4_9@;RQx!k6kR8eWMjDOf%` z_|xfToN>zJqx;vdaeLCz7HG5L;bDvQkJ_cwK5S|`{;{R4KRL`<>hhl3)wY3bX_XV z;B@!AnKceaS4-73-_QCXTetop|BuB@e$%hALJ=FSM$Qcn7PzGV8L_z%0bpI*BL*Ft=HZBzcTopI= z-ngmsu+|-=`+d8NH?O|lYnp!l+Mf7F-}fK>efsdngUzf}ZyX*=muIgtzPI55zgG8u z75#v)u!A40cYUkt^b{`U+HE(-?!shgiKtKS1a8kbelfm6=9pC3>%VjN3m&>>?r>XL zidFr8y}$OSTQ0Y1KV3WV^vuI~^0!VOx+JitOss?7%b9y>cjVoLI}`rA+F`(zTDv8{ z=Iz^(qnCM&Cudx_{@R-Rb=kvdCJxsxhS_~mnQv;AzP|G$+nty7-YfU)y6xJ3{Bm#M z(&xWJ6KrCyeO}7^ZOk?7i(Owlc_{RJ=h3RW!G z+tv5^VC>z+ze^VXE{V-tQKzw`Ir((wyM*!;UFY7;vbNqd(zf>VJu+0#ib#TKIli@_Wtt&r7Dnr5jc4YHDn+igz>EIXCCs+6Q}YzsnW$ z3%tJBedgB8MJW=0Bd2?xv5t0rePmPjvY^LY>x51o|0{m$$5z$*!glXA+i03qODz9w z=G= zcwMU4+2XzV^itMS`vpT6a?953{!tRQK$7!Lijh@><0I7;;q{9%ttEYTt>)V)eR1J# ziFHXS(VL!64>dVjRFwU?cK7s0XJ)f^*!x{j_ju&FEUQ(?b;*xZ@8UYT>|g=1^|m>C$JPIhedu8+1zjb0GqxJNmx zW!nXAC4~;LDcvGmj-DUxZ(38j@Xe&RO8c75KjcnW^cZFXH?}$%g1j>h;#J9+i5@TbtWv`akilT)IGQO4Bq6<7?MbYgJdjd^IOWEVQ^| zOS@_8W;Y2ntGieKU)^w1JGII=wwq_4VX|v#hDguNvtLuM*68VrcL<-8>WYb&;_Exd z+h=~k^oL2$rY@cJcvj>VF|+C36L=14#)hBW*c79)YRT32uaci=$aEPmkjapnSU>BO zhr}!1+JlpPwYMi`ww*IPcHzl|K24#hmam(PcWdpRD9jppUR_Kl`0{7xsdMzo&uecu z$-X}4nd>914=+wjZnLut6^mMT#QNoJYbzy(O^eo?Jt6&V`Gbl>8_M)TAIN2&S|@V; z_L7L8Bu`_PRXdL>=t|69wx}ZkNwqimoua7+Q{h~PwcAsJ@=IV%A{YT@@EK%>i zh3|cA!+d@Ej-Hv8v#LGW%5Q>Q?d5>CyTdPbx<1edoO`lsky!V{MwQUae9_*EPq(~W zwp3K;N@)F}!+eG-=6+sv^g^JWVbST;tCHT{)lDxvbMcOq$gSOr)6QCMN&b0Z<+jS@ zt7lbTUG&%L@B_=8hx~)|dk_4dy6Z<~z(u$Hf845MHUxO^m@L}!q2njpuiQ(Slg`E6 zYQA_wy<*YEAAEb{U%YWNUd!urOfBHBnqWe3hGHD|Ne}ty1+4WOG-vr7N#5aiP3Fq? z4~N~W)qV-iJOAP6{sqUH->;ba_Fv-%y&pk6D*rTYJKp`Kw?uW}_U;2gI(};szR%lG zce^ZNhmcr94x4X|*jb%+j@jGqc3QQ4yL;3FUFO&+>f&;DDo4OS9p6|GQ?8U*Z}}DPBBMSq=Y;Fbyo5}J^RVMYw)d$^n6Zv;dY>jgFksUKywk_c^Wvn@PXHvp!71mkN zg#}USTh#WRbSf0hu~a_o8@I6k!gi1JTZ)>Om;AT7y#2-fea_al+Lj4P%|2?ze;41i z{rz%IzWrQVtNMSxUvo1QS12TNbgRFZI8pGRlDl79i>hQ~4BsQJbwxEQVy9)anZDI; zXX~AiM$cyqbzPUMz9UmA~aW=iFuko-1b;t&J*sI5GE%Z{K+dw!1r}yUlVxZMB?PzoKsa zvem01J;ifR8))zJJ3YnxE8qPi2iHf5D#o_k|0*kG_gC5}T;4pbmHBkZ`o`}v{M$T3 zGmctl&5r32=&6s$zPohPu=s$?$Daa)%(3Ori9#5No#-{kjw5DPS;WhgbJ!Iq(Zmu%P-jx|&z3Amy8;k2jlJ&VyJyv<`STVcd zV&}(WM@*bE>?66Kw(m+Su37Ei8tHOHLa$OT^X&v?-tzkbbt@xcYBoQ*z^!Qhm4 z=0FKPi506tOJ+Z;vGz3&j@x^?KJQlg1VNr-)rlKUI##jqulVh39(vE|Wv2d(y;)LK zb2+CLd!3!-wJBxShduRMf~uEVUwRrV`2M}Q#$mbtobTli{a4NVWDzI0^qrQIuOxS? zOXc#J@^*o54}Q>Fn|JA!|6(EkKTPp96Yn(sSC9JrLC)5{%hs&lZd-%G?uPFt&3x*s zTaIh|Y%^Y*?tdV3yMFQdWTwe3(Px%rJaZP9-J$Gh=k7UA zWZBA6Uu*WSf1736j|fU|HM-2nmEGqg6G zvaCmQ7{%x4mt6c<$SHqehf@|`;P#qnI)+lmRWJIcPh7C9(rpJ9tHQ3Ap&fCS-M3r1 zFZ@{fKyi=k0$ zN>(OowouT5(EhD!+v^uU|MJEE#lsc%-=BGtH!1eU5&6GQ&wa0Z|Gjko`Q5*-Ua$Wv z-Qefbet~6c&$PdaErOE;_!SxIdky?-JM6MjK+53MZ9~p>r-)Yd<_fJAF&o)K#ud~2=W!>R*PUvy=YI6Or*fYR{{!(o zdlVkJ+Wk|N|G!FR|FO?PaY@&vtL{;H7|M84rm*Dli*lX29v&5Q4Xo1>Gj3-~)X%y) zIV3lFGwUtoWm7Z!BeHB(ZaqG!s=D9OW{uRxj=YTBA*Zjc*>&?zIro$*DVDsQe5;Q; zUwOJw$B4gswUS-;#0ak(_fiwie1YxhPWH+yx43RwbZa*OrA=n%ykd&syJm^LFy8i5cJP-`_p@XpZ9Db+32)J8R25qiS>B3%}_H zruVj<6qjs0G*AD{i=&pCGaVEzBp*8&eR_tv<@|?aYi>G6A}CzuOrd zxF@vYUgC`8w)CrWGV-)?9O}Y8<(@Fvwbmso!dRxUTDth~)h#zdI9EwlPTl#4Wtu?Z zn}qpRkM8HyYaVynI%kF1y9t_1vc28ccA6}-yd7d)J^9hGW$brP3qJPScF&sUOD9j~ z-IMd=shQq?){ccczRcFtUY2{+^50cUliWF`M!P;ROU^Fjcit~pA2IQ*&fRHCsDQ5)jSUx6Suu%S|J>%YS%oJdoe6_A=(9)s{;E z`Q{ph!J9=7mx#FAuFzbyaEb8V-djm#7t7z-FF1N))BQ)XX8BIX1Z6~T+{x9r^`WC! zDKO_IZdd3ciSs7&f_V2x*r>rDJeZ>S!Cp=r1h|zX}fqy<@~(c zQ$pjOU6}fJTi{gj^Jf3&Epa>hK50cyu!ZhJTgms2dwT32tmxtYu&~FzNM+ZrAW!$@ zRcAW3O*K4wNlfRD&OepM=WBlV7v%L_-+O3c;XK)YhX0fv3NtPZkUTl#$dm)J_czwp zbCu|ASohpO_k449Y)G>}Mx;+E_nPR4=*83KuH6*pzB%OhmJBcPbym^-P*!{DuxVSgsRG)yn;8jbFN6FzE-*A2s%`eYvb@He>@#B`&%hN87@wadK z>$|kq?LYKn@^($`^1wOWh4q~_<@@Ka46l(ddl>ph;%}YUnq_Bpb;$~q z#k8-o>JdciisZX8yx| z%A(FiM2vaO3Z;(FE%i))YHD0-gW4p059TzDFz1J$sLz1Cd*ziV9I8j zTzFM>a?TYAnd6{iRhSqUd{`J5Oc)SgNuwV-Sdq?EO&Qd4G>{eT=A2x3)t70P1X%6e zs}4-7BqtYM6`g!eL1FUAI|7qau1PU%Q~(QoxW+bl_BA`EQe`ODktszN%=EY}FN4^% z#K540u!3Ppqn{pF(Rl~<$@8yE$e^yHMyOoU_|X)qkau#x4K1b^FEDpQD9_}%Hw0u* zr$dnSarlB2LMBHvWKc)dkrj&gLlp{x{g!@HiYYG)EVL_87A&+pO@IfcV@YFO^yG&( zBbh4WCO^C>Gxdd0+Y*bNikhXpUhpXI{EGs839e0dIkoDC50S|7XzOE{$+~x?m{ck!D=bx>T-U-r`NU-b8Ti--Cj)~uiVOIwzzQ>4 zr6%9GtHrdf7R=@C6q;;%Pe2BxQ-`8&Wdlgzq#o(XckT*I-hNMtiKP`JGWpv*QyF*@ zPmqDZ7De0EcCezX`)*7k{b1(R`>ssFlO`)H)tqcTS8VeBM*@?xA4oAtOa%*Vcwoh( zF$2u>Su6;aHG3$6z5J>SgJqSdxgN{0}liyGd+@G5?BB>S#LE& zL((HDrj$isp{BKRATy*vxk*fjfgwi|G?jvI;gZIAlLH?rPv(7W!1QDLq|-{16CWEe zu^yOoT5q!KU4hAB=a3^@^YG-ikJFg`9+~{(y!GS>PqdgW9Gkr7i8qtd@yW86)F&G} 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 0221/2244] 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 0222/2244] 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 0223/2244] 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 0224/2244] 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 0225/2244] 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 0226/2244] 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 0227/2244] 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 0228/2244] 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 0229/2244] 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 0230/2244] 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 0231/2244] 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 0232/2244] 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 0233/2244] 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 0234/2244] 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 0235/2244] 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 0236/2244] 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 0237/2244] 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 0238/2244] 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 0239/2244] 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 0240/2244] 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 0241/2244] 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 0242/2244] 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 0243/2244] 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 0244/2244] 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 0245/2244] 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 0246/2244] 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 0247/2244] 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 0248/2244] 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 0249/2244] 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 0250/2244] 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 0251/2244] 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 0252/2244] 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 0253/2244] 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 0254/2244] 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 0255/2244] 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 0256/2244] 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 0257/2244] 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 0258/2244] 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 0259/2244] 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 0260/2244] 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 0261/2244] 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 0262/2244] 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 0263/2244] 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 0264/2244] 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 0265/2244] 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 0266/2244] 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 0267/2244] 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 0268/2244] 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 0269/2244] 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 0270/2244] 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 0271/2244] 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 0272/2244] 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 0273/2244] 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 0274/2244] 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 0275/2244] 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 0276/2244] 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 0277/2244] 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 0278/2244] 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 0279/2244] 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 0280/2244] 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 0281/2244] 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 0282/2244] 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 0283/2244] 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 0284/2244] 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 0285/2244] 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 0286/2244] 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 0287/2244] 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 0288/2244] 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 0289/2244] 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 0290/2244] 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 0291/2244] 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 0292/2244] 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 0293/2244] 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 0294/2244] 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 0295/2244] 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 0296/2244] 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 0297/2244] 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 0298/2244] 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 0299/2244] 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 0300/2244] 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 0301/2244] 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 0302/2244] 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 0303/2244] 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 0304/2244] 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 0305/2244] 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 0306/2244] 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 0307/2244] 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 0308/2244] 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 0309/2244] 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 0310/2244] 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 0311/2244] 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 0312/2244] 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 0313/2244] 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 0314/2244] 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 0315/2244] 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 0316/2244] 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 0317/2244] 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 0318/2244] 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 0319/2244] 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 0320/2244] 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 0321/2244] 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 0322/2244] 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 0323/2244] 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 0324/2244] 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 0325/2244] 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 0326/2244] 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 0327/2244] 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 0328/2244] 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 0329/2244] 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 0330/2244] 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 0331/2244] 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 0332/2244] 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 0333/2244] 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 0334/2244] 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 0335/2244] 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 0336/2244] 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 0337/2244] 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 0338/2244] 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 0339/2244] 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 0340/2244] 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 0341/2244] 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 0342/2244] 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 0343/2244] 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 0344/2244] 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 0345/2244] 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 0346/2244] 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 0347/2244] 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 0348/2244] 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 0349/2244] 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 0350/2244] 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 0351/2244] 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 0352/2244] 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 0353/2244] 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 0354/2244] 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 0355/2244] 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 0356/2244] 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 0357/2244] 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 0358/2244] 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 0359/2244] 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 0360/2244] 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 0361/2244] 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 0362/2244] 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 0363/2244] 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 0364/2244] 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 0365/2244] 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 0366/2244] 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 0367/2244] 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 0368/2244] 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 0369/2244] 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 0370/2244] 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 0371/2244] 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 0372/2244] 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 0373/2244] 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 0374/2244] 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 0375/2244] 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 0376/2244] 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 0377/2244] 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 0378/2244] 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 0379/2244] 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 0380/2244] 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 0381/2244] 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 0382/2244] 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 0383/2244] 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 0384/2244] 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 0385/2244] 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 0386/2244] 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 0387/2244] 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 0388/2244] 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 0389/2244] 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 0c5e0a4f6d30834e7aa1623636675df5d9bda1f2 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 21 Aug 2020 12:24:11 +0200 Subject: [PATCH 0390/2244] Make Device methods static when possible The behavior of some methods do not depend on the user-provided options. These methods can be static. This will allow to call them directly from the cleanup process. --- .../com/genymobile/scrcpy/Controller.java | 12 +++--- .../java/com/genymobile/scrcpy/Device.java | 40 +++++++++---------- .../java/com/genymobile/scrcpy/Server.java | 2 +- 3 files changed, 27 insertions(+), 27 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index 9100a9db..79feefc1 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -54,7 +54,7 @@ public class Controller { public void control() throws IOException { // on start, power on the device - if (!device.isScreenOn()) { + if (!Device.isScreenOn()) { device.injectKeycode(KeyEvent.KEYCODE_POWER); // dirty hack @@ -105,13 +105,13 @@ public class Controller { } break; case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL: - device.expandNotificationPanel(); + Device.expandNotificationPanel(); break; case ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL: - device.collapsePanels(); + Device.collapsePanels(); break; case ControlMessage.TYPE_GET_CLIPBOARD: - String clipboardText = device.getClipboardText(); + String clipboardText = Device.getClipboardText(); if (clipboardText != null) { sender.pushClipboardText(clipboardText); } @@ -130,7 +130,7 @@ public class Controller { } break; case ControlMessage.TYPE_ROTATE_DEVICE: - device.rotateDevice(); + Device.rotateDevice(); break; default: // do nothing @@ -248,7 +248,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_POWER; if (keepPowerModeOff && keycode == KeyEvent.KEYCODE_POWER) { schedulePowerModeOff(); } diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index f23dd056..a8fdf677 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -25,6 +25,8 @@ public final class Device { public static final int POWER_MODE_OFF = SurfaceControl.POWER_MODE_OFF; public static final int POWER_MODE_NORMAL = SurfaceControl.POWER_MODE_NORMAL; + private static final ServiceManager SERVICE_MANAGER = new ServiceManager(); + public interface RotationListener { void onRotationChanged(int rotation); } @@ -33,8 +35,6 @@ public final class Device { void onClipboardTextChanged(String text); } - private final ServiceManager serviceManager = new ServiceManager(); - private ScreenInfo screenInfo; private RotationListener rotationListener; private ClipboardListener clipboardListener; @@ -54,9 +54,9 @@ public final class Device { public Device(Options options) { displayId = options.getDisplayId(); - DisplayInfo displayInfo = serviceManager.getDisplayManager().getDisplayInfo(displayId); + DisplayInfo displayInfo = SERVICE_MANAGER.getDisplayManager().getDisplayInfo(displayId); if (displayInfo == null) { - int[] displayIds = serviceManager.getDisplayManager().getDisplayIds(); + int[] displayIds = SERVICE_MANAGER.getDisplayManager().getDisplayIds(); throw new InvalidDisplayIdException(displayId, displayIds); } @@ -65,7 +65,7 @@ public final class Device { screenInfo = ScreenInfo.computeScreenInfo(displayInfo, options.getCrop(), options.getMaxSize(), options.getLockedVideoOrientation()); layerStack = displayInfo.getLayerStack(); - serviceManager.getWindowManager().registerRotationWatcher(new IRotationWatcher.Stub() { + SERVICE_MANAGER.getWindowManager().registerRotationWatcher(new IRotationWatcher.Stub() { @Override public void onRotationChanged(int rotation) { synchronized (Device.this) { @@ -81,7 +81,7 @@ public final class Device { if (options.getControl()) { // If control is enabled, synchronize Android clipboard to the computer automatically - ClipboardManager clipboardManager = serviceManager.getClipboardManager(); + ClipboardManager clipboardManager = SERVICE_MANAGER.getClipboardManager(); if (clipboardManager != null) { clipboardManager.addPrimaryClipChangedListener(new IOnPrimaryClipChangedListener.Stub() { @Override @@ -166,7 +166,7 @@ public final class Device { return false; } - return serviceManager.getInputManager().injectInputEvent(inputEvent, mode); + return SERVICE_MANAGER.getInputManager().injectInputEvent(inputEvent, mode); } public boolean injectEvent(InputEvent event) { @@ -184,8 +184,8 @@ public final class Device { return injectKeyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0) && injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0); } - public boolean isScreenOn() { - return serviceManager.getPowerManager().isScreenOn(); + public static boolean isScreenOn() { + return SERVICE_MANAGER.getPowerManager().isScreenOn(); } public synchronized void setRotationListener(RotationListener rotationListener) { @@ -196,16 +196,16 @@ public final class Device { this.clipboardListener = clipboardListener; } - public void expandNotificationPanel() { - serviceManager.getStatusBarManager().expandNotificationsPanel(); + public static void expandNotificationPanel() { + SERVICE_MANAGER.getStatusBarManager().expandNotificationsPanel(); } - public void collapsePanels() { - serviceManager.getStatusBarManager().collapsePanels(); + public static void collapsePanels() { + SERVICE_MANAGER.getStatusBarManager().collapsePanels(); } - public String getClipboardText() { - ClipboardManager clipboardManager = serviceManager.getClipboardManager(); + public static String getClipboardText() { + ClipboardManager clipboardManager = SERVICE_MANAGER.getClipboardManager(); if (clipboardManager == null) { return null; } @@ -217,7 +217,7 @@ public final class Device { } public boolean setClipboardText(String text) { - ClipboardManager clipboardManager = serviceManager.getClipboardManager(); + ClipboardManager clipboardManager = SERVICE_MANAGER.getClipboardManager(); if (clipboardManager == null) { return false; } @@ -252,8 +252,8 @@ public final class Device { /** * Disable auto-rotation (if enabled), set the screen rotation and re-enable auto-rotation (if it was enabled). */ - public void rotateDevice() { - WindowManager wm = serviceManager.getWindowManager(); + public static void rotateDevice() { + WindowManager wm = SERVICE_MANAGER.getWindowManager(); boolean accelerometerRotation = !wm.isRotationFrozen(); @@ -270,7 +270,7 @@ public final class Device { } } - public ContentProvider createSettingsProvider() { - return serviceManager.getActivityManager().createSettingsProvider(); + public static ContentProvider createSettingsProvider() { + return SERVICE_MANAGER.getActivityManager().createSettingsProvider(); } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index d257e319..9b7f9de8 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -26,7 +26,7 @@ public final class Server { boolean mustDisableShowTouchesOnCleanUp = false; int restoreStayOn = -1; if (options.getShowTouches() || options.getStayAwake()) { - try (ContentProvider settings = device.createSettingsProvider()) { + try (ContentProvider settings = Device.createSettingsProvider()) { 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 From 0bf110dd5cc6a9793177c7764fe7fbfb452628f6 Mon Sep 17 00:00:00 2001 From: brunoais Date: Thu, 13 Aug 2020 07:33:36 +0100 Subject: [PATCH 0391/2244] Reset power mode only if screen is on PR #1670 Signed-off-by: Romain Vimont --- server/src/main/java/com/genymobile/scrcpy/CleanUp.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java index d0ea141b..efaa059a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java +++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java @@ -78,7 +78,9 @@ public final class CleanUp { if (restoreNormalPowerMode) { Ln.i("Restoring normal power mode"); - Device.setScreenPowerMode(Device.POWER_MODE_NORMAL); + if (Device.isScreenOn()) { + Device.setScreenPowerMode(Device.POWER_MODE_NORMAL); + } } } } From c243fd4c3fe91e83bacf89d2d6bbf642e5019f39 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 31 Aug 2020 13:37:09 +0200 Subject: [PATCH 0392/2244] Fix more log format warning The expression port + 1 is promoted to int, but printed as uint16_t. This is the same mistake fixed for a different log by 7eb16ce36458011d87972715f08bd9f03ff39807. Refs #1726 --- 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 05b2cf91..422bbfa5 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -201,7 +201,7 @@ enable_tunnel_forward_any_port(struct server *server, if (port < port_range.last) { LOGW("Could not forward port %" PRIu16", retrying on %" PRIu16, - port, port + 1); + port, (uint16_t) (port + 1)); port++; continue; } From bd9f656933e79f7b21b42993f8a70a761ab47226 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 31 Aug 2020 13:52:29 +0200 Subject: [PATCH 0393/2244] Fix feature test macro The expected feature test macro is _POSIX_C_SOURCE having a value greater or equal to 200809L. Fixes #1726 --- app/src/sys/unix/command.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/sys/unix/command.c b/app/src/sys/unix/command.c index 64a54e71..3c2f587d 100644 --- a/app/src/sys/unix/command.c +++ b/app/src/sys/unix/command.c @@ -1,6 +1,6 @@ -// for portability -#define _POSIX_SOURCE // for kill() -#define _BSD_SOURCE // for readlink() +// for portability (kill, readlink, strdup, strtok_r) +#define _POSIX_C_SOURCE 200809L +#define _BSD_SOURCE // modern glibc will complain without this #define _DEFAULT_SOURCE From ae758f99d6af81123bb6b74d307d798c2c17acf3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 13 Sep 2020 21:35:11 +0200 Subject: [PATCH 0394/2244] Adapt call() on ContentProvider for Android 11 This commit in AOSP framework_base added a parameter "attributionTag" to the call() method: https://android.googlesource.com/platform/frameworks/base.git/+/12ac3f406fed87cb9cd3a28b9947e7202a2d14bd%5E%21/#F17 As a consequence, the method did not exist, so scrcpy used the legacy call() method (using the authority "unknown") as a fallback, which fails for security reasons. Fixes #1468 --- .../scrcpy/wrappers/ContentProvider.java | 32 +++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java index b43494c7..f8393e59 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java @@ -35,7 +35,7 @@ public class ContentProvider implements Closeable { private final IBinder token; private Method callMethod; - private boolean callMethodLegacy; + private int callMethodVersion; ContentProvider(ActivityManager manager, Object provider, String name, IBinder token) { this.manager = manager; @@ -46,12 +46,20 @@ public class ContentProvider implements Closeable { private Method getCallMethod() throws NoSuchMethodException { if (callMethod == null) { + try { - callMethod = provider.getClass().getMethod("call", String.class, String.class, String.class, String.class, Bundle.class); + callMethod = provider.getClass() + .getMethod("call", String.class, String.class, String.class, String.class, String.class, Bundle.class); + callMethodVersion = 0; } catch (NoSuchMethodException e) { - // old version - callMethod = provider.getClass().getMethod("call", String.class, String.class, String.class, Bundle.class); - callMethodLegacy = true; + // old versions + try { + callMethod = provider.getClass().getMethod("call", String.class, String.class, String.class, String.class, Bundle.class); + callMethodVersion = 1; + } catch (NoSuchMethodException e2) { + callMethod = provider.getClass().getMethod("call", String.class, String.class, String.class, Bundle.class); + callMethodVersion = 2; + } } } return callMethod; @@ -61,10 +69,16 @@ public class ContentProvider implements Closeable { 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}; + switch (callMethodVersion) { + case 0: + args = new Object[]{ServiceManager.PACKAGE_NAME, null, "settings", callMethod, arg, extras}; + break; + case 1: + args = new Object[]{ServiceManager.PACKAGE_NAME, "settings", callMethod, arg, extras}; + break; + default: + args = new Object[]{ServiceManager.PACKAGE_NAME, callMethod, arg, extras}; + break; } return (Bundle) method.invoke(provider, args); } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { From cf7bf3148caca8d4428cbd70578696a8230689e0 Mon Sep 17 00:00:00 2001 From: CapsLock Date: Wed, 6 Feb 2019 01:13:31 +0100 Subject: [PATCH 0395/2244] Use "/usr/bin/env bash" for build-wrapper.sh PR #426 Signed-off-by: Romain Vimont --- server/scripts/build-wrapper.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/scripts/build-wrapper.sh b/server/scripts/build-wrapper.sh index f55e1ea4..7e16dc94 100755 --- a/server/scripts/build-wrapper.sh +++ b/server/scripts/build-wrapper.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # Wrapper script to invoke gradle from meson set -e From 02a882a0a2fbe40504889bf753aa055bfc3c4fdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Ferreira?= Date: Thu, 27 Aug 2020 18:00:40 +0100 Subject: [PATCH 0396/2244] Use a more portable shebang for bash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This should increase the portability of bash scripts across various *nix systems such as BSD-like distributions. PR #1716 Signed-off-by: Luís Ferreira Signed-off-by: Romain Vimont --- server/build_without_gradle.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index e757be9a..3060b8e1 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # # This script generates the scrcpy binary "manually" (without gradle). # From 1c44dc2259f7111f8432fcf1ab8509c68f48d9a0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 15 Sep 2020 13:54:00 +0200 Subject: [PATCH 0397/2244] Use portable shebang for all bash scripts Refs #426 Refs #1716 --- prebuilt-deps/prepare-dep | 2 +- run | 2 +- scripts/run-scrcpy.sh | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/prebuilt-deps/prepare-dep b/prebuilt-deps/prepare-dep index 34ddcbf5..f152e6cf 100755 --- a/prebuilt-deps/prepare-dep +++ b/prebuilt-deps/prepare-dep @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash set -e url="$1" sum="$2" diff --git a/run b/run index bfb499ae..628c5c7e 100755 --- a/run +++ b/run @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # Run scrcpy generated in the specified BUILDDIR. # # This provides the same feature as "ninja run", except that it is possible to diff --git a/scripts/run-scrcpy.sh b/scripts/run-scrcpy.sh index f3130ee9..e93b639f 100755 --- a/scripts/run-scrcpy.sh +++ b/scripts/run-scrcpy.sh @@ -1,2 +1,2 @@ -#!/bin/bash +#!/usr/bin/env bash SCRCPY_SERVER_PATH="$MESON_BUILD_ROOT/server/scrcpy-server" "$MESON_BUILD_ROOT/app/scrcpy" From d662f73bdceda1ceb95ea61fe5fd3b556c1ba932 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 15 Sep 2020 14:53:37 +0200 Subject: [PATCH 0398/2244] Upgrade Android SDK to 30 --- server/build.gradle | 4 ++-- server/build_without_gradle.sh | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/server/build.gradle b/server/build.gradle index c7f8bc0f..42252472 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -1,11 +1,11 @@ apply plugin: 'com.android.application' android { - compileSdkVersion 29 + compileSdkVersion 30 defaultConfig { applicationId "com.genymobile.scrcpy" minSdkVersion 21 - targetSdkVersion 29 + targetSdkVersion 30 versionCode 19 versionName "1.16" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index 3060b8e1..e03939a5 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -14,8 +14,8 @@ set -e SCRCPY_DEBUG=false SCRCPY_VERSION_NAME=1.16 -PLATFORM=${ANDROID_PLATFORM:-29} -BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-29.0.2} +PLATFORM=${ANDROID_PLATFORM:-30} +BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-30.0.0} BUILD_DIR="$(realpath ${BUILD_DIR:-build_manual})" CLASSES_DIR="$BUILD_DIR/classes" From 52560faa344f19d6ec89a586ba85ab8958482e82 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 17 Sep 2020 13:03:18 +0200 Subject: [PATCH 0399/2244] 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 0400/2244] 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 a65ebceac1e1f214328b3be776e7dbc724656db6 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 20 Sep 2020 01:07:18 +0200 Subject: [PATCH 0401/2244] Add missing mutex unlock on error Fixes #1770 Reported-by: lordnn --- app/src/recorder.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/recorder.c b/app/src/recorder.c index 76edbd03..e31492c0 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -361,12 +361,14 @@ recorder_push(struct recorder *recorder, const AVPacket *packet) { if (recorder->failed) { // reject any new packet (this will stop the stream) + mutex_unlock(recorder->mutex); return false; } struct record_packet *rec = record_packet_new(packet); if (!rec) { LOGC("Could not allocate record packet"); + mutex_unlock(recorder->mutex); return false; } From acc65f8c9dc477e978d438a2f2a3501e49230211 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 20 Sep 2020 01:09:05 +0200 Subject: [PATCH 0402/2244] Remove unused field Fixes #1775 Reported-by: lordnn --- app/src/stream.h | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/stream.h b/app/src/stream.h index f7c5e475..cd09d959 100644 --- a/app/src/stream.h +++ b/app/src/stream.h @@ -14,7 +14,6 @@ struct video_buffer; struct stream { socket_t socket; - struct video_buffer *video_buffer; SDL_Thread *thread; struct decoder *decoder; struct recorder *recorder; From 56d237f152fc4d454a65fe594412b4bd9409fd64 Mon Sep 17 00:00:00 2001 From: Brinan Sjostrom Date: Tue, 22 Sep 2020 18:02:18 -0400 Subject: [PATCH 0403/2244] Fix "press Enter key" message The message said "Press any key to continue...", whereas only Enter/Return is accepted. PR #1783 Fixes #1757 Reviewed-by: Yu-Chen Lin Signed-off-by: Romain Vimont --- 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 202c217c..d32d9896 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -100,7 +100,7 @@ main(int argc, char *argv[]) { #if defined (__WINDOWS__) && ! defined (WINDOWS_NOCONSOLE) if (res != 0) { - fprintf(stderr, "Press any key to continue...\n"); + fprintf(stderr, "Press Enter to continue...\n"); getchar(); } #endif 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 0404/2244] 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 d50ecf40b603f8a0e5a73699babce7301c0bd870 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 1 Oct 2020 15:06:34 +0200 Subject: [PATCH 0405/2244] Fix options order --- app/scrcpy.1 | 8 ++++---- app/src/cli.c | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 6a890f14..e957a4d0 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -92,14 +92,14 @@ 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 .B \-\-no\-key\-repeat Do not forward repeated key events when a key is held down. +.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 9c791fbf..41de8ca5 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -87,14 +87,14 @@ scrcpy_print_usage(const char *arg0) { " Do not display device (only when screen recording is\n" " enabled).\n" "\n" + " --no-key-repeat\n" + " Do not forward repeated key events when a key is held down.\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" - " --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" @@ -672,8 +672,8 @@ 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}, {"no-key-repeat", no_argument, NULL, OPT_NO_KEY_REPEAT}, + {"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}, From 2edf192e3ae80252d136e6d1a614ab07f93fc561 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 5 Oct 2020 21:09:01 +0200 Subject: [PATCH 0406/2244] Remove deprecation warning As a workaround for some devices, we need to prepare the main looper. The method is now deprecated, but we still want to call it. --- server/src/main/java/com/genymobile/scrcpy/Workarounds.java | 1 + 1 file changed, 1 insertion(+) diff --git a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java index 351cc574..0f473bc1 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java +++ b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java @@ -16,6 +16,7 @@ public final class Workarounds { // not instantiable } + @SuppressWarnings("deprecation") public static void prepareMainLooper() { // Some devices internally create a Handler when creating an input Surface, causing an exception: // "Can't create handler inside thread that has not called Looper.prepare()" From 83082406d36d177976c54d93e8eda5a5ea4811e5 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 5 Oct 2020 21:11:50 +0200 Subject: [PATCH 0407/2244] Enable Java deprecation warnings details Without the option, gradle reports a lint issue, but without any details. --- build.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build.gradle b/build.gradle index 94862d2e..2755fd62 100644 --- a/build.gradle +++ b/build.gradle @@ -19,6 +19,9 @@ allprojects { google() jcenter() } + tasks.withType(JavaCompile) { + options.compilerArgs << "-Xlint:deprecation" + } } task clean(type: Delete) { From aade92fd1058a88cfdcacf168f2b2b8857b5d562 Mon Sep 17 00:00:00 2001 From: yanuarakhid Date: Fri, 2 Oct 2020 00:50:38 +0700 Subject: [PATCH 0408/2244] 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 0409/2244] 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) From ad5f567f07627baea09c7bcf596f921923dca4b2 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 3 Nov 2020 17:08:21 +0100 Subject: [PATCH 0410/2244] Remove spurious space --- 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 45068cbb..eae9a1eb 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -422,7 +422,7 @@ scrcpy(const struct scrcpy_options *options) { options->window_y, options->window_width, options->window_height, options->window_borderless, - options->rotation, options-> mipmaps)) { + options->rotation, options->mipmaps)) { goto end; } From 5dcfc0ebab563e7bfdcf4d28025fa1996a79c214 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 3 Nov 2020 17:09:03 +0100 Subject: [PATCH 0411/2244] Add local.properties to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 7bc30289..2829d835 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ build/ .idea/ .gradle/ /x/ +local.properties From adc547fa6e8e6167cd9633a97d98de6665b8c23a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 5 Oct 2020 20:45:53 +0200 Subject: [PATCH 0412/2244] Add an option to forward all clicks Add --forward-all-clicks to disable mouse shortcuts and forward middle and right clicks to the device instead. Fixes #1302 Fixes #1613 --- README.md | 10 ++++++++++ app/scrcpy.1 | 4 ++++ app/src/cli.c | 11 +++++++++++ app/src/input_manager.c | 3 ++- app/src/input_manager.h | 1 + app/src/scrcpy.h | 2 ++ .../main/java/com/genymobile/scrcpy/Controller.java | 8 ++++++-- 7 files changed, 36 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e8b54465..a61407ac 100644 --- a/README.md +++ b/README.md @@ -595,6 +595,16 @@ scrcpy --no-key-repeat ``` +#### Right-click and middle-click + +By default, right-click triggers BACK (or POWER on) and middle-click triggers +HOME. To disable these shortcuts and forward the clicks to the device instead: + +```bash +scrcpy --forward-all-clicks +``` + + ### File drop #### Install APK diff --git a/app/scrcpy.1 b/app/scrcpy.1 index e957a4d0..e2b999de 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -60,6 +60,10 @@ Default is 0. .B \-\-force\-adb\-forward Do not attempt to use "adb reverse" to connect to the device. +.TP +.B \-\-forward\-all\-clicks +By default, right-click triggers BACK (or POWER on) and middle-click triggers HOME. This option disables these shortcuts and forward the clicks to the device instead. + .TP .B \-f, \-\-fullscreen Start in fullscreen. diff --git a/app/src/cli.c b/app/src/cli.c index 41de8ca5..ffe31455 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -57,6 +57,11 @@ scrcpy_print_usage(const char *arg0) { " Do not attempt to use \"adb reverse\" to connect to the\n" " the device.\n" "\n" + " --forward-all-clicks\n" + " By default, right-click triggers BACK (or POWER on) and\n" + " middle-click triggers HOME. This option disables these\n" + " shortcuts and forward the clicks to the device instead.\n" + "\n" " -f, --fullscreen\n" " Start in fullscreen.\n" "\n" @@ -651,6 +656,7 @@ guess_record_format(const char *filename) { #define OPT_DISABLE_SCREENSAVER 1020 #define OPT_SHORTCUT_MOD 1021 #define OPT_NO_KEY_REPEAT 1022 +#define OPT_FORWARD_ALL_CLICKS 1023 bool scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { @@ -664,6 +670,8 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { {"display", required_argument, NULL, OPT_DISPLAY_ID}, {"force-adb-forward", no_argument, NULL, OPT_FORCE_ADB_FORWARD}, + {"forward-all-clicks", no_argument, NULL, + OPT_FORWARD_ALL_CLICKS}, {"fullscreen", no_argument, NULL, 'f'}, {"help", no_argument, NULL, 'h'}, {"lock-video-orientation", required_argument, NULL, @@ -856,6 +864,9 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { return false; } break; + case OPT_FORWARD_ALL_CLICKS: + opts->forward_all_clicks = true; + break; default: // getopt prints the error message on stderr return false; diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 962db1d3..b03da383 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -60,6 +60,7 @@ input_manager_init(struct input_manager *im, im->control = options->control; im->forward_key_repeat = options->forward_key_repeat; im->prefer_text = options->prefer_text; + im->forward_all_clicks = options->forward_all_clicks; const struct sc_shortcut_mods *shortcut_mods = &options->shortcut_mods; assert(shortcut_mods->count); @@ -629,7 +630,7 @@ input_manager_process_mouse_button(struct input_manager *im, } bool down = event->type == SDL_MOUSEBUTTONDOWN; - if (down) { + if (!im->forward_all_clicks && down) { if (control && event->button == SDL_BUTTON_RIGHT) { press_back_or_turn_screen_on(im->controller); return; diff --git a/app/src/input_manager.h b/app/src/input_manager.h index c3756e40..157c2832 100644 --- a/app/src/input_manager.h +++ b/app/src/input_manager.h @@ -25,6 +25,7 @@ struct input_manager { bool control; bool forward_key_repeat; bool prefer_text; + bool forward_all_clicks; struct { unsigned data[SC_MAX_SHORTCUT_MODS]; diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index 86a2b57b..8ab05a9d 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -79,6 +79,7 @@ struct scrcpy_options { bool force_adb_forward; bool disable_screensaver; bool forward_key_repeat; + bool forward_all_clicks; }; #define SCRCPY_OPTIONS_DEFAULT { \ @@ -123,6 +124,7 @@ struct scrcpy_options { .force_adb_forward = false, \ .disable_screensaver = false, \ .forward_key_repeat = true, \ + .forward_all_clicks = false, \ } bool diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index 79feefc1..84780239 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -205,9 +205,13 @@ public class Controller { } } + // Right-click and middle-click only work if the source is a mouse + boolean nonPrimaryButtonPressed = (buttons & ~MotionEvent.BUTTON_PRIMARY) != 0; + int source = nonPrimaryButtonPressed ? InputDevice.SOURCE_MOUSE : InputDevice.SOURCE_TOUCHSCREEN; + MotionEvent event = MotionEvent - .obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEVICE_ID_VIRTUAL, 0, - InputDevice.SOURCE_TOUCHSCREEN, 0); + .obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEVICE_ID_VIRTUAL, 0, source, + 0); return device.injectEvent(event); } From d5f059c7cbef7d13889086670716d9f33f805dc4 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 6 Oct 2020 21:30:10 +0200 Subject: [PATCH 0413/2244] Add option to force legacy method for pasting Some devices do not behave as expected when setting the device clipboard programmatically. Add an option --legacy-paste to change the behavior of Ctrl+v and MOD+v so that they inject the computer clipboard text as a sequence of key events (the same way as MOD+Shift+v). Fixes #1750 Fixes #1771 --- README.md | 5 +++++ app/scrcpy.1 | 6 ++++++ app/src/cli.c | 11 +++++++++++ app/src/input_manager.c | 8 +++++++- app/src/input_manager.h | 1 + app/src/scrcpy.h | 2 ++ 6 files changed, 32 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a61407ac..95a38409 100644 --- a/README.md +++ b/README.md @@ -548,6 +548,11 @@ 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. +Some devices do not behave as expected when setting the device clipboard +programmatically. An option `--legacy-paste` is provided to change the behavior +of Ctrl+v and MOD+v so that they +also inject the computer clipboard text as a sequence of key events (the same +way as MOD+Shift+v). #### Pinch-to-zoom diff --git a/app/scrcpy.1 b/app/scrcpy.1 index e2b999de..b16d1004 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -72,6 +72,12 @@ Start in fullscreen. .B \-h, \-\-help Print this help. +.TP +.B \-\-legacy\-paste +Inject computer clipboard text as a sequence of key events on Ctrl+v (like MOD+Shift+v). + +This is a workaround for some devices not behaving as expected when setting the device clipboard programmatically. + .TP .BI "\-\-lock\-video\-orientation " value 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. diff --git a/app/src/cli.c b/app/src/cli.c index ffe31455..4f15f2e8 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -68,6 +68,12 @@ scrcpy_print_usage(const char *arg0) { " -h, --help\n" " Print this help.\n" "\n" + " --legacy-paste\n" + " Inject computer clipboard text as a sequence of key events\n" + " on Ctrl+v (like MOD+Shift+v).\n" + " This is a workaround for some devices not behaving as\n" + " expected when setting the device clipboard programmatically.\n" + "\n" " --lock-video-orientation value\n" " Lock video orientation to value.\n" " Possible values are -1 (unlocked), 0, 1, 2 and 3.\n" @@ -657,6 +663,7 @@ guess_record_format(const char *filename) { #define OPT_SHORTCUT_MOD 1021 #define OPT_NO_KEY_REPEAT 1022 #define OPT_FORWARD_ALL_CLICKS 1023 +#define OPT_LEGACY_PASTE 1024 bool scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { @@ -674,6 +681,7 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { OPT_FORWARD_ALL_CLICKS}, {"fullscreen", no_argument, NULL, 'f'}, {"help", no_argument, NULL, 'h'}, + {"legacy-paste", no_argument, NULL, OPT_LEGACY_PASTE}, {"lock-video-orientation", required_argument, NULL, OPT_LOCK_VIDEO_ORIENTATION}, {"max-fps", required_argument, NULL, OPT_MAX_FPS}, @@ -867,6 +875,9 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { case OPT_FORWARD_ALL_CLICKS: opts->forward_all_clicks = true; break; + case OPT_LEGACY_PASTE: + opts->legacy_paste = true; + break; default: // getopt prints the error message on stderr return false; diff --git a/app/src/input_manager.c b/app/src/input_manager.c index b03da383..bab85660 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -61,6 +61,7 @@ input_manager_init(struct input_manager *im, im->forward_key_repeat = options->forward_key_repeat; im->prefer_text = options->prefer_text; im->forward_all_clicks = options->forward_all_clicks; + im->legacy_paste = options->legacy_paste; const struct sc_shortcut_mods *shortcut_mods = &options->shortcut_mods; assert(shortcut_mods->count); @@ -441,7 +442,7 @@ input_manager_process_key(struct input_manager *im, return; case SDLK_v: if (control && !repeat && down) { - if (shift) { + if (shift || im->legacy_paste) { // inject the text as input events clipboard_paste(controller); } else { @@ -505,6 +506,11 @@ input_manager_process_key(struct input_manager *im, } if (ctrl && !shift && keycode == SDLK_v && down && !repeat) { + if (im->legacy_paste) { + // inject the text as input events + clipboard_paste(controller); + return; + } // Synchronize the computer clipboard to the device clipboard before // sending Ctrl+v, to allow seamless copy-paste. set_device_clipboard(controller, false); diff --git a/app/src/input_manager.h b/app/src/input_manager.h index 157c2832..df9b071f 100644 --- a/app/src/input_manager.h +++ b/app/src/input_manager.h @@ -26,6 +26,7 @@ struct input_manager { bool forward_key_repeat; bool prefer_text; bool forward_all_clicks; + bool legacy_paste; struct { unsigned data[SC_MAX_SHORTCUT_MODS]; diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index 8ab05a9d..5f761f75 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -80,6 +80,7 @@ struct scrcpy_options { bool disable_screensaver; bool forward_key_repeat; bool forward_all_clicks; + bool legacy_paste; }; #define SCRCPY_OPTIONS_DEFAULT { \ @@ -125,6 +126,7 @@ struct scrcpy_options { .disable_screensaver = false, \ .forward_key_repeat = true, \ .forward_all_clicks = false, \ + .legacy_paste = false, \ } bool From 76c2c6e69dc074031a81c4e050347e374cbfb776 Mon Sep 17 00:00:00 2001 From: Tzah Mazuz Date: Mon, 12 Oct 2020 12:23:06 +0300 Subject: [PATCH 0414/2244] Adding new option --encoder Some devices have more than one encoder, and some encoders may cause issues or crash. With this option we can specify which encoder we want the device to use. PR #1827 Fixes #1810 Signed-off-by: Romain Vimont --- app/src/cli.c | 8 ++++++++ app/src/scrcpy.c | 1 + app/src/scrcpy.h | 2 ++ app/src/server.c | 1 + app/src/server.h | 1 + .../src/main/java/com/genymobile/scrcpy/Options.java | 9 +++++++++ .../java/com/genymobile/scrcpy/ScreenEncoder.java | 12 +++++++++--- .../src/main/java/com/genymobile/scrcpy/Server.java | 8 ++++++-- 8 files changed, 37 insertions(+), 5 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index 4f15f2e8..f01b7941 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -53,6 +53,9 @@ scrcpy_print_usage(const char *arg0) { "\n" " Default is 0.\n" "\n" + " --encoder name\n" + " Use a specific MediaCodec encoder (must be a H.264 encoder).\n" + "\n" " --force-adb-forward\n" " Do not attempt to use \"adb reverse\" to connect to the\n" " the device.\n" @@ -664,6 +667,7 @@ guess_record_format(const char *filename) { #define OPT_NO_KEY_REPEAT 1022 #define OPT_FORWARD_ALL_CLICKS 1023 #define OPT_LEGACY_PASTE 1024 +#define OPT_ENCODER_NAME 1025 bool scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { @@ -675,6 +679,7 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { {"disable-screensaver", no_argument, NULL, OPT_DISABLE_SCREENSAVER}, {"display", required_argument, NULL, OPT_DISPLAY_ID}, + {"encoder", required_argument, NULL, OPT_ENCODER_NAME}, {"force-adb-forward", no_argument, NULL, OPT_FORCE_ADB_FORWARD}, {"forward-all-clicks", no_argument, NULL, @@ -861,6 +866,9 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { case OPT_CODEC_OPTIONS: opts->codec_options = optarg; break; + case OPT_ENCODER_NAME: + opts->encoder_name = optarg; + break; case OPT_FORCE_ADB_FORWARD: opts->force_adb_forward = true; break; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index eae9a1eb..a543e11c 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -318,6 +318,7 @@ scrcpy(const struct scrcpy_options *options) { .show_touches = options->show_touches, .stay_awake = options->stay_awake, .codec_options = options->codec_options, + .encoder_name = options->encoder_name, .force_adb_forward = options->force_adb_forward, }; if (!server_start(&server, options->serial, ¶ms)) { diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index 5f761f75..8548d1f7 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -51,6 +51,7 @@ struct scrcpy_options { const char *push_target; const char *render_driver; const char *codec_options; + const char *encoder_name; enum sc_log_level log_level; enum sc_record_format record_format; struct sc_port_range port_range; @@ -91,6 +92,7 @@ struct scrcpy_options { .push_target = NULL, \ .render_driver = NULL, \ .codec_options = NULL, \ + .encoder_name = NULL, \ .log_level = SC_LOG_LEVEL_INFO, \ .record_format = SC_RECORD_FORMAT_AUTO, \ .port_range = { \ diff --git a/app/src/server.c b/app/src/server.c index 422bbfa5..9267356b 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -294,6 +294,7 @@ execute_server(struct server *server, const struct server_params *params) { params->show_touches ? "true" : "false", params->stay_awake ? "true" : "false", params->codec_options ? params->codec_options : "-", + params->encoder_name ? params->encoder_name : "-", }; #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 254afe30..30ce09bc 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -48,6 +48,7 @@ struct server_params { enum sc_log_level log_level; const char *crop; const char *codec_options; + const char *encoder_name; struct sc_port_range port_range; uint16_t max_size; uint32_t bit_rate; diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 06312a37..150d06a8 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -16,6 +16,7 @@ public class Options { private boolean showTouches; private boolean stayAwake; private String codecOptions; + private String encoderName; public Ln.Level getLogLevel() { return logLevel; @@ -120,4 +121,12 @@ public class Options { public void setCodecOptions(String codecOptions) { this.codecOptions = codecOptions; } + + public String getEncoderName() { + return encoderName; + } + + public void setEncoderName(String encoderName) { + this.encoderName = encoderName; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index d722388c..ee9ea935 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -26,17 +26,19 @@ public class ScreenEncoder implements Device.RotationListener { private final AtomicBoolean rotationChanged = new AtomicBoolean(); private final ByteBuffer headerBuffer = ByteBuffer.allocate(12); + private String encoderName; private List codecOptions; private int bitRate; private int maxFps; private boolean sendFrameMeta; private long ptsOrigin; - public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, List codecOptions) { + public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, List codecOptions, String encoderName) { this.sendFrameMeta = sendFrameMeta; this.bitRate = bitRate; this.maxFps = maxFps; this.codecOptions = codecOptions; + this.encoderName = encoderName; } @Override @@ -69,7 +71,7 @@ public class ScreenEncoder implements Device.RotationListener { boolean alive; try { do { - MediaCodec codec = createCodec(); + MediaCodec codec = createCodec(encoderName); IBinder display = createDisplay(); ScreenInfo screenInfo = device.getScreenInfo(); Rect contentRect = screenInfo.getContentRect(); @@ -150,7 +152,11 @@ public class ScreenEncoder implements Device.RotationListener { IO.writeFully(fd, headerBuffer); } - private static MediaCodec createCodec() throws IOException { + private static MediaCodec createCodec(String encoderName) throws IOException { + if (encoderName != null) { + Ln.d("Creating encoder by name: '" + encoderName + "'"); + return MediaCodec.createByCodecName(encoderName); + } return MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC); } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 9b7f9de8..0e7bd244 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -54,7 +54,8 @@ public final class Server { boolean tunnelForward = options.isTunnelForward(); try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) { - ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps(), codecOptions); + ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps(), codecOptions, + options.getEncoderName()); if (options.getControl()) { final Controller controller = new Controller(device, connection); @@ -120,7 +121,7 @@ public final class Server { "The server version (" + BuildConfig.VERSION_NAME + ") does not match the client " + "(" + clientVersion + ")"); } - final int expectedParameters = 14; + final int expectedParameters = 15; if (args.length != expectedParameters) { throw new IllegalArgumentException("Expecting " + expectedParameters + " parameters"); } @@ -167,6 +168,9 @@ public final class Server { String codecOptions = args[13]; options.setCodecOptions(codecOptions); + String encoderName = "-".equals(args[14]) ? null : args[14]; + options.setEncoderName(encoderName); + return options; } From 363eeea19eb0d347643a4dd9592dac0063396dff Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 8 Nov 2020 17:27:33 +0100 Subject: [PATCH 0415/2244] Log encoder name When the encoder is selected automatically, log the name of the selected encoder. --- server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java | 4 +++- 1 file changed, 3 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 ee9ea935..1dac7629 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -157,7 +157,9 @@ public class ScreenEncoder implements Device.RotationListener { Ln.d("Creating encoder by name: '" + encoderName + "'"); return MediaCodec.createByCodecName(encoderName); } - return MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC); + MediaCodec codec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC); + Ln.d("Using encoder: '" + codec.getName() + "'"); + return codec; } private static void setCodecOption(MediaFormat format, CodecOption codecOption) { From 42ab8fd61156a7894fb8c6802f7a1b408598e14f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 7 Nov 2020 15:53:48 +0100 Subject: [PATCH 0416/2244] List available encoders on invalid name specified If an invalid encoder name is given via the --encoder option, list all the H.264 encoders available on the device. --- .../scrcpy/InvalidEncoderException.java | 23 +++++++++++++++++++ .../com/genymobile/scrcpy/ScreenEncoder.java | 21 ++++++++++++++++- .../java/com/genymobile/scrcpy/Server.java | 10 ++++++++ 3 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/InvalidEncoderException.java diff --git a/server/src/main/java/com/genymobile/scrcpy/InvalidEncoderException.java b/server/src/main/java/com/genymobile/scrcpy/InvalidEncoderException.java new file mode 100644 index 00000000..1efd2989 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/InvalidEncoderException.java @@ -0,0 +1,23 @@ +package com.genymobile.scrcpy; + +import android.media.MediaCodecInfo; + +public class InvalidEncoderException extends RuntimeException { + + private final String name; + private final MediaCodecInfo[] availableEncoders; + + public InvalidEncoderException(String name, MediaCodecInfo[] availableEncoders) { + super("There is no encoder having name '" + name + '"'); + this.name = name; + this.availableEncoders = availableEncoders; + } + + public String getName() { + return name; + } + + public MediaCodecInfo[] getAvailableEncoders() { + return availableEncoders; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index 1dac7629..c7c104e4 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -5,6 +5,7 @@ import com.genymobile.scrcpy.wrappers.SurfaceControl; import android.graphics.Rect; import android.media.MediaCodec; import android.media.MediaCodecInfo; +import android.media.MediaCodecList; import android.media.MediaFormat; import android.os.IBinder; import android.view.Surface; @@ -12,6 +13,8 @@ import android.view.Surface; import java.io.FileDescriptor; import java.io.IOException; import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; @@ -152,10 +155,26 @@ public class ScreenEncoder implements Device.RotationListener { IO.writeFully(fd, headerBuffer); } + private static MediaCodecInfo[] listEncoders() { + List result = new ArrayList<>(); + MediaCodecList list = new MediaCodecList(MediaCodecList.REGULAR_CODECS); + for (MediaCodecInfo codecInfo : list.getCodecInfos()) { + if (codecInfo.isEncoder() && Arrays.asList(codecInfo.getSupportedTypes()).contains(MediaFormat.MIMETYPE_VIDEO_AVC)) { + result.add(codecInfo); + } + } + return result.toArray(new MediaCodecInfo[result.size()]); + } + private static MediaCodec createCodec(String encoderName) throws IOException { if (encoderName != null) { Ln.d("Creating encoder by name: '" + encoderName + "'"); - return MediaCodec.createByCodecName(encoderName); + try { + return MediaCodec.createByCodecName(encoderName); + } catch (IllegalArgumentException e) { + MediaCodecInfo[] encoders = listEncoders(); + throw new InvalidEncoderException(encoderName, encoders); + } } MediaCodec codec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC); Ln.d("Using encoder: '" + codec.getName() + "'"); diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 0e7bd244..f501d3d8 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.media.MediaCodecInfo; import android.os.BatteryManager; import android.os.Build; @@ -210,6 +211,15 @@ public final class Server { Ln.e(" scrcpy --display " + id); } } + } else if (e instanceof InvalidEncoderException) { + InvalidEncoderException iee = (InvalidEncoderException) e; + MediaCodecInfo[] encoders = iee.getAvailableEncoders(); + if (encoders != null && encoders.length > 0) { + Ln.e("Try to use one of the available encoders:"); + for (MediaCodecInfo encoder : encoders) { + Ln.e(" scrcpy --encoder-name '" + encoder.getName() + "'"); + } + } } } From 576814bcec5718d325de3a85528e5133cc46be68 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 8 Nov 2020 21:11:12 +0100 Subject: [PATCH 0417/2244] Document --encoder option Add documentation for the new option --encoder in the manpage and in README.md. --- README.md | 16 ++++++++++++++++ app/scrcpy.1 | 4 ++++ 2 files changed, 20 insertions(+) diff --git a/README.md b/README.md index 95a38409..1283aa50 100644 --- a/README.md +++ b/README.md @@ -203,6 +203,22 @@ scrcpy --lock-video-orientation 3 # 90° clockwise This affects recording orientation. +#### Encoder + +Some devices have more than one encoder, and some of them may cause issues or +crash. It is possible to select a different encoder: + +```bash +scrcpy --encoder OMX.qcom.video.encoder.avc +``` + +To list the available encoders, you could pass an invalid encoder name, the +error will give the available encoders: + +```bash +scrcpy --encoder _ +``` + ### Recording It is possible to record the screen while mirroring: diff --git a/app/scrcpy.1 b/app/scrcpy.1 index b16d1004..92b8e1e3 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -56,6 +56,10 @@ The list of possible display ids can be listed by "adb shell dumpsys display" Default is 0. +.TP +.BI "\-\-encoder " name +Use a specific MediaCodec encoder (must be a H.264 encoder). + .TP .B \-\-force\-adb\-forward Do not attempt to use "adb reverse" to connect to the device. From 868e762d71695f0a45f593c83215aeb428e475b3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 14 Nov 2020 22:08:59 +0100 Subject: [PATCH 0418/2244] Fix size_t format specifier for Windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use "%Iu" on Windows. This fixes the following warning: ../app/src/sys/win/command.c:17:14: warning: unknown conversion type character ‘l’ in format [-Wformat=] 17 | LOGE("Command too long (%" PRIsizet " chars)", len - 1); --- app/src/command.h | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/app/src/command.h b/app/src/command.h index 28f9fbcf..7035139b 100644 --- a/app/src/command.h +++ b/app/src/command.h @@ -12,11 +12,7 @@ # define PATH_SEPARATOR '\\' # define PRIexitcode "lu" // -# ifdef _WIN64 -# define PRIsizet PRIu64 -# else -# define PRIsizet PRIu32 -# endif +# define PRIsizet "Iu" # define PROCESS_NONE NULL # define NO_EXIT_CODE -1u // max value as unsigned typedef HANDLE process_t; From 30434afc0ad89198e8f7f5567d9d50bbbf8c055c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 24 Nov 2020 09:45:18 +0100 Subject: [PATCH 0419/2244] Upgrade gradle build tools to 4.0.1 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 2755fd62..c977c398 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.6.2' + classpath 'com.android.tools.build:gradle:4.0.1' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files From 47c897126758ed016881bdf51b2eb643c4a5fe07 Mon Sep 17 00:00:00 2001 From: "SamBe.ng" Date: Mon, 30 Nov 2020 10:16:50 +0100 Subject: [PATCH 0420/2244] Document shell command to get the device IP PR #1944 Signed-off-by: Romain Vimont --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d10aaa74..67f2c14c 100644 --- a/README.md +++ b/README.md @@ -235,7 +235,13 @@ _Scrcpy_ uses `adb` to communicate with the device, and `adb` can [connect] to a device over TCP/IP: 1. Connect the device to the same Wi-Fi as your computer. -2. Get your device IP address (in Settings → About phone → Status). +2. Get your device IP address, in Settings → About phone → Status, or by + executing this command: + + ```bash + adb shell ip route | awk '{print $9}' + ``` + 3. Enable adb over TCP/IP on your device: `adb tcpip 5555`. 4. Unplug your device. 5. Connect to your device: `adb connect DEVICE_IP:5555` _(replace `DEVICE_IP`)_. From d6078cf20244e6651aea5832e6b78b3aa3459766 Mon Sep 17 00:00:00 2001 From: jianzhang4 Date: Wed, 9 Dec 2020 11:41:17 +0800 Subject: [PATCH 0421/2244] Fix build errors for macOS PR #1960 Signed-off-by: Romain Vimont --- app/src/sys/unix/command.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/sys/unix/command.c b/app/src/sys/unix/command.c index 64a54e71..09925c50 100644 --- a/app/src/sys/unix/command.c +++ b/app/src/sys/unix/command.c @@ -5,6 +5,10 @@ // modern glibc will complain without this #define _DEFAULT_SOURCE +#ifdef __APPLE__ +# define _DARWIN_C_SOURCE // for strdup(), strtok_r(), memset_pattern4() +#endif + #include "command.h" #include "config.h" From c9a4bdb8905042cc9bf14e55b09f4430214c9b31 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 12 Dec 2020 15:56:32 +0100 Subject: [PATCH 0422/2244] Upgrade platform-tools (30.0.5) 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 9cccd0bd..c2297b82 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.4-windows.zip \ - 413182fff6c5957911e231b9e97e6be4fc6a539035e3dfb580b5c54bd5950fee \ + @./prepare-dep https://dl.google.com/android/repository/platform-tools_r30.0.5-windows.zip \ + 549ba2bdc31f335eb8a504f005f77606a479cc216d6b64a3e8b64c780003661f \ platform-tools From 5dc3285dbf807553f86346ab16d0891cfeb32ab7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 12 Dec 2020 15:57:54 +0100 Subject: [PATCH 0423/2244] Reference FFmpeg Windows builds from GitHub Refs #1753 --- prebuilt-deps/Makefile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/prebuilt-deps/Makefile b/prebuilt-deps/Makefile index c2297b82..535d5692 100644 --- a/prebuilt-deps/Makefile +++ b/prebuilt-deps/Makefile @@ -10,22 +10,22 @@ 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.3.1-win32-shared.zip \ + @./prepare-dep https://github.com/Genymobile/scrcpy/releases/download/v1.16/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.3.1-win32-dev.zip \ + @./prepare-dep https://github.com/Genymobile/scrcpy/releases/download/v1.16/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.3.1-win64-shared.zip \ + @./prepare-dep https://github.com/Genymobile/scrcpy/releases/download/v1.16/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.3.1-win64-dev.zip \ + @./prepare-dep https://github.com/Genymobile/scrcpy/releases/download/v1.16/ffmpeg-4.3.1-win64-dev.zip \ 2e8038242cf8e1bd095c2978f196ff0462b122cc6ef7e74626a6af15459d8b81 \ ffmpeg-4.3.1-win64-dev From 6d151eaef914dd836e206e2877263e47882bba18 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 14 Dec 2020 09:04:14 +0100 Subject: [PATCH 0424/2244] Reference encoder section from FAQ --- FAQ.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/FAQ.md b/FAQ.md index ba33542e..a2298e60 100644 --- a/FAQ.md +++ b/FAQ.md @@ -199,3 +199,5 @@ scrcpy -m 1920 scrcpy -m 1024 scrcpy -m 800 ``` + +You could also try another [encoder](README.md#encoder). From 904d47057987fcd2bc2bb9494758a611fa6ab2bc Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 14 Dec 2020 09:35:55 +0100 Subject: [PATCH 0425/2244] Pause on error from a wrapper script On Windows, scrcpy paused on error before exiting to give the user a chance to see the user message. This was a hack and causes issues when using scrcpy from batch scripts. Disable this pause from the scrcpy binary, and provide a batch wrapper (scrcpy-console.bat) to pause on error. Fixes #1875 --- Makefile.CrossWindows | 2 ++ app/src/main.c | 6 ------ data/scrcpy-console.bat | 4 ++++ 3 files changed, 6 insertions(+), 6 deletions(-) create mode 100644 data/scrcpy-console.bat diff --git a/Makefile.CrossWindows b/Makefile.CrossWindows index 0415af82..f8a360d9 100644 --- a/Makefile.CrossWindows +++ b/Makefile.CrossWindows @@ -100,6 +100,7 @@ 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 data/scrcpy-console.bat "$(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)/" @@ -115,6 +116,7 @@ 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 data/scrcpy-console.bat "$(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)/" diff --git a/app/src/main.c b/app/src/main.c index d32d9896..71125673 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -98,11 +98,5 @@ main(int argc, char *argv[]) { avformat_network_deinit(); // ignore failure -#if defined (__WINDOWS__) && ! defined (WINDOWS_NOCONSOLE) - if (res != 0) { - fprintf(stderr, "Press Enter to continue...\n"); - getchar(); - } -#endif return res; } diff --git a/data/scrcpy-console.bat b/data/scrcpy-console.bat new file mode 100644 index 00000000..b90be29a --- /dev/null +++ b/data/scrcpy-console.bat @@ -0,0 +1,4 @@ +@echo off +scrcpy.exe %* +:: if the exit code is >= 1, then pause +if errorlevel 1 pause From ea12783bbc935e4bc74329a924cda784b5cfc25a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 15 Dec 2020 21:50:58 +0100 Subject: [PATCH 0426/2244] Upgrade JUnit to 4.13 --- server/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/build.gradle b/server/build.gradle index 42252472..94a492c6 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -20,7 +20,7 @@ android { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - testImplementation 'junit:junit:4.12' + testImplementation 'junit:junit:4.13' } apply from: "$project.rootDir/config/android-checkstyle.gradle" From a5f4f582956264aa05563df5de847d4a36f8d2aa Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 22 Dec 2020 01:14:30 +0100 Subject: [PATCH 0427/2244] Remove duplicate include --- app/src/sys/unix/command.c | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/sys/unix/command.c b/app/src/sys/unix/command.c index 4c3ff7e2..c4c262e4 100644 --- a/app/src/sys/unix/command.c +++ b/app/src/sys/unix/command.c @@ -21,7 +21,6 @@ #include #include #include -#include #include #include From 43d3dcbd970640c70272fac9d5e54ff90f8e5eea Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 14 Dec 2020 10:12:01 +0100 Subject: [PATCH 0428/2244] Document Windows command line usage PR #1973 Reviewed-by: Yu-Chen Lin --- FAQ.md | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/FAQ.md b/FAQ.md index ba33542e..4fda4dd4 100644 --- a/FAQ.md +++ b/FAQ.md @@ -199,3 +199,36 @@ scrcpy -m 1920 scrcpy -m 1024 scrcpy -m 800 ``` + + +## Command line on Windows + +Some Windows users are not familiar with the command line. Here is how to open a +terminal and run `scrcpy` with arguments: + + 1. Press Windows+r, this opens a dialog box. + 2. Type `cmd` and press Enter, this opens a terminal. + 3. Go to your _scrcpy_ directory, by typing (adapt the path): + + ```bat + cd C:\Users\user\Downloads\scrcpy-win64-xxx + ``` + + and press Enter + 4. Type your command. For example: + + ```bat + scrcpy --record file.mkv + ``` + +If you plan to always use the same arguments, create a file `myscrcpy.bat` +(enable [show file extensions] to avoid confusion) in the `scrcpy` directory, +containing your command. For example: + +```bat +scrcpy --prefer-text --turn-screen-off --stay-awake +``` + +Then just double-click on that file. + +[show file extensions]: https://www.howtogeek.com/205086/beginner-how-to-make-windows-show-file-extensions/ From 431c9ee33b49f5ce7317b4c687e00bc436adbc02 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 20 Dec 2020 08:24:04 +0100 Subject: [PATCH 0429/2244] Improve rotation documentation --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 67f2c14c..816afc79 100644 --- a/README.md +++ b/README.md @@ -202,6 +202,8 @@ scrcpy --lock-video-orientation 3 # 90° clockwise This affects recording orientation. +The [window may also be rotated](#rotation) independently. + ### Recording @@ -391,9 +393,9 @@ 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). - - `--lock-video-orientation` changes the mirroring orientation (the orientation - of the video sent from the device to the computer). This affects the - recording. + - [`--lock-video-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. From a46733906acf3b59d9d1e26abef99ff4b003f5af Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 15 Dec 2020 21:50:46 +0100 Subject: [PATCH 0430/2244] Replace noconsole binary by a wrapper script This simplifies the build system. Refs --- Makefile.CrossWindows | 37 ++++++------------------------------- app/meson.build | 12 +----------- app/src/sys/win/command.c | 7 +------ data/scrcpy-noconsole.vbs | 1 + meson_options.txt | 1 - 5 files changed, 9 insertions(+), 49 deletions(-) create mode 100644 data/scrcpy-noconsole.vbs diff --git a/Makefile.CrossWindows b/Makefile.CrossWindows index f8a360d9..8c093a2f 100644 --- a/Makefile.CrossWindows +++ b/Makefile.CrossWindows @@ -11,8 +11,7 @@ .PHONY: default clean \ build-server \ prepare-deps-win32 prepare-deps-win64 \ - build-win32 build-win32-noconsole \ - build-win64 build-win64-noconsole \ + build-win32 build-win64 \ dist-win32 dist-win64 \ zip-win32 zip-win64 \ sums release @@ -21,9 +20,7 @@ GRADLE ?= ./gradlew SERVER_BUILD_DIR := build-server WIN32_BUILD_DIR := build-win32 -WIN32_NOCONSOLE_BUILD_DIR := build-win32-noconsole WIN64_BUILD_DIR := build-win64 -WIN64_NOCONSOLE_BUILD_DIR := build-win64-noconsole DIST := dist WIN32_TARGET_DIR := scrcpy-win32 @@ -39,7 +36,7 @@ release: clean zip-win32 zip-win64 sums clean: $(GRADLE) clean rm -rf "$(SERVER_BUILD_DIR)" "$(WIN32_BUILD_DIR)" "$(WIN64_BUILD_DIR)" \ - "$(WIN32_NOCONSOLE_BUILD_DIR)" "$(WIN64_NOCONSOLE_BUILD_DIR)" "$(DIST)" + "$(DIST)" build-server: [ -d "$(SERVER_BUILD_DIR)" ] || ( mkdir "$(SERVER_BUILD_DIR)" && \ @@ -60,17 +57,6 @@ build-win32: prepare-deps-win32 -Dportable=true ) ninja -C "$(WIN32_BUILD_DIR)" -build-win32-noconsole: prepare-deps-win32 - [ -d "$(WIN32_NOCONSOLE_BUILD_DIR)" ] || ( mkdir "$(WIN32_NOCONSOLE_BUILD_DIR)" && \ - meson "$(WIN32_NOCONSOLE_BUILD_DIR)" \ - --cross-file cross_win32.txt \ - --buildtype release --strip -Db_lto=true \ - -Dcrossbuild_windows=true \ - -Dcompile_server=false \ - -Dwindows_noconsole=true \ - -Dportable=true ) - ninja -C "$(WIN32_NOCONSOLE_BUILD_DIR)" - prepare-deps-win64: -$(MAKE) -C prebuilt-deps prepare-win64 @@ -84,23 +70,12 @@ build-win64: prepare-deps-win64 -Dportable=true ) ninja -C "$(WIN64_BUILD_DIR)" -build-win64-noconsole: prepare-deps-win64 - [ -d "$(WIN64_NOCONSOLE_BUILD_DIR)" ] || ( mkdir "$(WIN64_NOCONSOLE_BUILD_DIR)" && \ - meson "$(WIN64_NOCONSOLE_BUILD_DIR)" \ - --cross-file cross_win64.txt \ - --buildtype release --strip -Db_lto=true \ - -Dcrossbuild_windows=true \ - -Dcompile_server=false \ - -Dwindows_noconsole=true \ - -Dportable=true ) - ninja -C "$(WIN64_NOCONSOLE_BUILD_DIR)" - -dist-win32: build-server build-win32 build-win32-noconsole +dist-win32: build-server build-win32 mkdir -p "$(DIST)/$(WIN32_TARGET_DIR)" 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 data/scrcpy-console.bat "$(DIST)/$(WIN32_TARGET_DIR)" + cp data/scrcpy-noconsole.vbs "$(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)/" @@ -111,12 +86,12 @@ dist-win32: build-server build-win32 build-win32-noconsole cp prebuilt-deps/platform-tools/AdbWinUsbApi.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 +dist-win64: build-server build-win64 mkdir -p "$(DIST)/$(WIN64_TARGET_DIR)" 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 data/scrcpy-console.bat "$(DIST)/$(WIN64_TARGET_DIR)" + cp data/scrcpy-noconsole.vbs "$(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)/" diff --git a/app/meson.build b/app/meson.build index 0163dd7f..28b9d141 100644 --- a/app/meson.build +++ b/app/meson.build @@ -119,9 +119,6 @@ conf.set('DEFAULT_BIT_RATE', '8000000') # 8Mbps # enable High DPI support conf.set('HIDPI_SUPPORT', get_option('hidpi_support')) -# disable console on Windows -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')) @@ -132,18 +129,11 @@ configure_file(configuration: conf, output: 'config.h') src_dir = include_directories('src') -if get_option('windows_noconsole') - link_args = [ '-Wl,--subsystem,windows' ] -else - link_args = [] -endif - executable('scrcpy', src, dependencies: dependencies, include_directories: src_dir, install: true, - c_args: [], - link_args: link_args) + c_args: []) install_man('scrcpy.1') diff --git a/app/src/sys/win/command.c b/app/src/sys/win/command.c index 105234b0..fbc0d04c 100644 --- a/app/src/sys/win/command.c +++ b/app/src/sys/win/command.c @@ -39,12 +39,7 @@ cmd_execute(const char *const argv[], HANDLE *handle) { return PROCESS_ERROR_GENERIC; } -#ifdef WINDOWS_NOCONSOLE - int flags = CREATE_NO_WINDOW; -#else - int flags = 0; -#endif - if (!CreateProcessW(NULL, wide, NULL, NULL, FALSE, flags, NULL, NULL, &si, + if (!CreateProcessW(NULL, wide, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi)) { SDL_free(wide); *handle = NULL; diff --git a/data/scrcpy-noconsole.vbs b/data/scrcpy-noconsole.vbs new file mode 100644 index 00000000..e11adba5 --- /dev/null +++ b/data/scrcpy-noconsole.vbs @@ -0,0 +1 @@ +CreateObject("Wscript.Shell").Run "cmd /c scrcpy.exe", 0, false diff --git a/meson_options.txt b/meson_options.txt index c213e7dd..b962380c 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -1,7 +1,6 @@ option('compile_app', type: 'boolean', value: true, description: 'Build the client') option('compile_server', type: 'boolean', value: true, description: 'Build the server') option('crossbuild_windows', type: 'boolean', value: false, description: 'Build for Windows from Linux') -option('windows_noconsole', type: 'boolean', value: false, description: 'Disable console on Windows (pass -mwindows flag)') option('prebuilt_server', type: 'string', description: 'Path of the prebuilt server') 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') From 230afd8966d3bb4d792a41e4d87dbdd1cf4e9347 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 17 Dec 2020 14:54:04 +0100 Subject: [PATCH 0431/2244] Unify release makefile Before this change, release.sh built some native stuff, and Makefile.CrossWindows built the Windows releases. Instead, use a single release.make to build the whole release. It also avoids to build the server one more time. --- Makefile.CrossWindows => release.make | 38 +++++++++++++++-------- release.sh | 44 +-------------------------- 2 files changed, 27 insertions(+), 55 deletions(-) rename Makefile.CrossWindows => release.make (82%) diff --git a/Makefile.CrossWindows b/release.make similarity index 82% rename from Makefile.CrossWindows rename to release.make index 8c093a2f..873762b4 100644 --- a/Makefile.CrossWindows +++ b/release.make @@ -9,15 +9,17 @@ # the server to the device. .PHONY: default clean \ + test \ build-server \ prepare-deps-win32 prepare-deps-win64 \ build-win32 build-win64 \ dist-win32 dist-win64 \ zip-win32 zip-win64 \ - sums release + release GRADLE ?= ./gradlew +TEST_BUILD_DIR := build-test SERVER_BUILD_DIR := build-server WIN32_BUILD_DIR := build-win32 WIN64_BUILD_DIR := build-win64 @@ -30,19 +32,35 @@ VERSION := $(shell git describe --tags --always) WIN32_TARGET := $(WIN32_TARGET_DIR)-$(VERSION).zip WIN64_TARGET := $(WIN64_TARGET_DIR)-$(VERSION).zip -release: clean zip-win32 zip-win64 sums - @echo "Windows archives generated in $(DIST)/" +RELEASE_DIR := release-$(VERSION) + +release: clean test build-server zip-win32 zip-win64 + mkdir -p "$(RELEASE_DIR)" + cp "$(SERVER_BUILD_DIR)/server/scrcpy-server" \ + "$(RELEASE_DIR)/scrcpy-server-$(VERSION)" + cp "$(DIST)/$(WIN32_TARGET)" "$(RELEASE_DIR)" + cp "$(DIST)/$(WIN64_TARGET)" "$(RELEASE_DIR)" + cd "$(RELEASE_DIR)" && \ + sha256sum "scrcpy-server-$(VERSION)" \ + "scrcpy-win32-$(VERSION).zip" \ + "scrcpy-win64-$(VERSION).zip" > SHA256SUMS.txt + @echo "Release generated in $(RELEASE_DIR)/" clean: $(GRADLE) clean - rm -rf "$(SERVER_BUILD_DIR)" "$(WIN32_BUILD_DIR)" "$(WIN64_BUILD_DIR)" \ - "$(DIST)" + rm -rf "$(DIST)" "$(TEST_BUILD_DIR)" "$(SERVER_BUILD_DIR)" \ + "$(WIN32_BUILD_DIR)" "$(WIN64_BUILD_DIR)" + +test: + [ -d "$(TEST_BUILD_DIR)" ] || ( mkdir "$(TEST_BUILD_DIR)" && \ + meson "$(TEST_BUILD_DIR)" -Db_sanitize=address ) + ninja -C "$(TEST_BUILD_DIR)" + $(GRADLE) -p server check build-server: [ -d "$(SERVER_BUILD_DIR)" ] || ( mkdir "$(SERVER_BUILD_DIR)" && \ - meson "$(SERVER_BUILD_DIR)" \ - --buildtype release -Dcompile_app=false ) - ninja -C "$(SERVER_BUILD_DIR)" + meson "$(SERVER_BUILD_DIR)" --buildtype release -Dcompile_app=false ) + ninja -C "$(SERVER_BUILD_DIR)" prepare-deps-win32: -$(MAKE) -C prebuilt-deps prepare-win32 @@ -109,7 +127,3 @@ zip-win32: dist-win32 zip-win64: dist-win64 cd "$(DIST)/$(WIN64_TARGET_DIR)"; \ zip -r "../$(WIN64_TARGET)" . - -sums: - cd "$(DIST)"; \ - sha256sum *.zip > SHA256SUMS.txt diff --git a/release.sh b/release.sh index 4c5afbf1..a824e958 100755 --- a/release.sh +++ b/release.sh @@ -1,44 +1,2 @@ #!/bin/bash -set -e - -# test locally -TESTDIR=build_test -rm -rf "$TESTDIR" -# run client tests with ASAN enabled -meson "$TESTDIR" -Db_sanitize=address -ninja -C"$TESTDIR" test - -# test server -GRADLE=${GRADLE:-./gradlew} -$GRADLE -p server check - -BUILDDIR=build_release -rm -rf "$BUILDDIR" -meson "$BUILDDIR" --buildtype release --strip -Db_lto=true -cd "$BUILDDIR" -ninja -cd - - -# build Windows releases -make -f Makefile.CrossWindows - -# the generated server must be the same everywhere -cmp "$BUILDDIR/server/scrcpy-server" dist/scrcpy-win32/scrcpy-server -cmp "$BUILDDIR/server/scrcpy-server" dist/scrcpy-win64/scrcpy-server - -# get version name -TAG=$(git describe --tags --always) - -# create release directory -mkdir -p "release-$TAG" -cp "$BUILDDIR/server/scrcpy-server" "release-$TAG/scrcpy-server-$TAG" -cp "dist/scrcpy-win32-$TAG.zip" "release-$TAG/" -cp "dist/scrcpy-win64-$TAG.zip" "release-$TAG/" - -# generate checksums -cd "release-$TAG" -sha256sum "scrcpy-server-$TAG" \ - "scrcpy-win32-$TAG.zip" \ - "scrcpy-win64-$TAG.zip" > SHA256SUMS.txt - -echo "Release generated in release-$TAG/" +make -f release.make From 6ab80e4ce8311ae65c09d2add5f620bdbef369a2 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 1 Jan 2021 15:49:28 +0100 Subject: [PATCH 0432/2244] Rename release.make to release.mk It's more standard, and benefits from syntax coloration in vi. --- release.make => release.mk | 0 release.sh | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename release.make => release.mk (100%) diff --git a/release.make b/release.mk similarity index 100% rename from release.make rename to release.mk diff --git a/release.sh b/release.sh index a824e958..51ce2e38 100755 --- a/release.sh +++ b/release.sh @@ -1,2 +1,2 @@ #!/bin/bash -make -f release.make +make -f release.mk From d039a7a39a80940c3eca226ce3d7d0555157fa8c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 1 Jan 2021 15:53:56 +0100 Subject: [PATCH 0433/2244] Upgrade SDL (2.0.14) for Windows Include the latest version of SDL in Windows releases. --- cross_win32.txt | 2 +- cross_win64.txt | 2 +- prebuilt-deps/Makefile | 6 +++--- release.mk | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cross_win32.txt b/cross_win32.txt index bef2e5d5..a662ae20 100644 --- a/cross_win32.txt +++ b/cross_win32.txt @@ -17,4 +17,4 @@ endian = 'little' [properties] 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' +prebuilt_sdl2 = 'SDL2-2.0.14/i686-w64-mingw32' diff --git a/cross_win64.txt b/cross_win64.txt index 5a348738..de3836d5 100644 --- a/cross_win64.txt +++ b/cross_win64.txt @@ -17,4 +17,4 @@ endian = 'little' [properties] 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' +prebuilt_sdl2 = 'SDL2-2.0.14/x86_64-w64-mingw32' diff --git a/prebuilt-deps/Makefile b/prebuilt-deps/Makefile index 535d5692..356a944e 100644 --- a/prebuilt-deps/Makefile +++ b/prebuilt-deps/Makefile @@ -30,9 +30,9 @@ prepare-ffmpeg-dev-win64: ffmpeg-4.3.1-win64-dev prepare-sdl2: - @./prepare-dep https://libsdl.org/release/SDL2-devel-2.0.12-mingw.tar.gz \ - e614a60f797e35ef9f3f96aef3dc6a1d786de3cc7ca6216f97e435c0b6aafc46 \ - SDL2-2.0.12 + @./prepare-dep https://libsdl.org/release/SDL2-devel-2.0.14-mingw.tar.gz \ + 405eaff3eb18f2e08fe669ef9e63bc9a8710b7d343756f238619761e9b60407d \ + SDL2-2.0.14 prepare-adb: @./prepare-dep https://dl.google.com/android/repository/platform-tools_r30.0.5-windows.zip \ diff --git a/release.mk b/release.mk index 873762b4..2a026135 100644 --- a/release.mk +++ b/release.mk @@ -102,7 +102,7 @@ dist-win32: build-server build-win32 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.12/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/SDL2-2.0.14/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/" dist-win64: build-server build-win64 mkdir -p "$(DIST)/$(WIN64_TARGET_DIR)" @@ -118,7 +118,7 @@ dist-win64: build-server build-win64 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.12/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/SDL2-2.0.14/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/" zip-win32: dist-win32 cd "$(DIST)/$(WIN32_TARGET_DIR)"; \ From 112adbba87f93d85808561301588219b833f9686 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 1 Jan 2021 17:16:44 +0100 Subject: [PATCH 0434/2244] Happy new year 2021! --- LICENSE | 2 +- README.id.md | 2 +- README.ko.md | 2 +- README.md | 2 +- README.pt-br.md | 2 +- README.zh-Hans.md | 2 +- README.zh-Hant.md | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/LICENSE b/LICENSE index bc4bb77d..b320f699 100644 --- a/LICENSE +++ b/LICENSE @@ -188,7 +188,7 @@ identification within third-party archives. Copyright (C) 2018 Genymobile - Copyright (C) 2018-2020 Romain Vimont + Copyright (C) 2018-2021 Romain Vimont Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.id.md b/README.id.md index 5af56663..a2cfa3d5 100644 --- a/README.id.md +++ b/README.id.md @@ -675,7 +675,7 @@ Baca [halaman pengembang]. ## Lisensi Copyright (C) 2018 Genymobile - Copyright (C) 2018-2020 Romain Vimont + Copyright (C) 2018-2021 Romain Vimont Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.ko.md b/README.ko.md index 4e6d8fc5..58b44dfe 100644 --- a/README.ko.md +++ b/README.ko.md @@ -477,7 +477,7 @@ _²화면이 꺼진 상태에서 우클릭 시 다시 켜지며, 그 외의 상 ## 라이선스 Copyright (C) 2018 Genymobile - Copyright (C) 2018-2020 Romain Vimont + Copyright (C) 2018-2021 Romain Vimont Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index 816afc79..2e31b6d5 100644 --- a/README.md +++ b/README.md @@ -737,7 +737,7 @@ Read the [developers page]. ## Licence Copyright (C) 2018 Genymobile - Copyright (C) 2018-2020 Romain Vimont + Copyright (C) 2018-2021 Romain Vimont Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.pt-br.md b/README.pt-br.md index 654f62cb..4f2122a6 100644 --- a/README.pt-br.md +++ b/README.pt-br.md @@ -508,7 +508,7 @@ Leia a [developers page]. ## Licença Copyright (C) 2018 Genymobile - Copyright (C) 2018-2020 Romain Vimont + Copyright (C) 2018-2021 Romain Vimont Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.zh-Hans.md b/README.zh-Hans.md index 85e178d6..3774c5d9 100644 --- a/README.zh-Hans.md +++ b/README.zh-Hans.md @@ -703,7 +703,7 @@ _³需要安卓版本 Android >= 7。_ ## 许可协议 Copyright (C) 2018 Genymobile - Copyright (C) 2018-2020 Romain Vimont + Copyright (C) 2018-2021 Romain Vimont Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.zh-Hant.md b/README.zh-Hant.md index 82eb1d9b..b4dc69ec 100644 --- a/README.zh-Hant.md +++ b/README.zh-Hant.md @@ -682,7 +682,7 @@ _³只支援 Android 7+。_ ## Licence Copyright (C) 2018 Genymobile - Copyright (C) 2018-2020 Romain Vimont + Copyright (C) 2018-2021 Romain Vimont Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From 3ba51211d6bb21fa02c5d05079f15aad089de7ca Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 1 Jan 2021 17:31:07 +0100 Subject: [PATCH 0435/2244] Mention how to add default arguments on Windows Mention that it is possible to add default arguments by editing the wrapper scripts. --- FAQ.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/FAQ.md b/FAQ.md index 44cc06d8..9801f91c 100644 --- a/FAQ.md +++ b/FAQ.md @@ -233,4 +233,7 @@ scrcpy --prefer-text --turn-screen-off --stay-awake Then just double-click on that file. +You could also edit (a copy of) `scrcpy-console.bat` or `scrcpy-noconsole.vbs` +to add some arguments. + [show file extensions]: https://www.howtogeek.com/205086/beginner-how-to-make-windows-show-file-extensions/ From 90f835663004d9dc4969a76a1ef2641af9b02633 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 1 Jan 2021 12:26:44 +0100 Subject: [PATCH 0436/2244] Interrupt device threads on stop The (non-daemon) threads were not interrupted on video stream stopped, leaving the server process alive. Interrupt them to wake up their blocking call so that they terminate properly. Refs #1992 --- .../java/com/genymobile/scrcpy/Server.java | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index f501d3d8..f58e3867 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -58,12 +58,14 @@ public final class Server { ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps(), codecOptions, options.getEncoderName()); + Thread controllerThread = null; + Thread deviceMessageSenderThread = null; if (options.getControl()) { final Controller controller = new Controller(device, connection); // asynchronous - startController(controller); - startDeviceMessageSender(controller.getSender()); + controllerThread = startController(controller); + deviceMessageSenderThread = startDeviceMessageSender(controller.getSender()); device.setClipboardListener(new Device.ClipboardListener() { @Override @@ -79,12 +81,19 @@ public final class Server { } catch (IOException e) { // this is expected on close Ln.d("Screen streaming stopped"); + } finally { + if (controllerThread != null) { + controllerThread.interrupt(); + } + if (deviceMessageSenderThread != null) { + deviceMessageSenderThread.interrupt(); + } } } } - private static void startController(final Controller controller) { - new Thread(new Runnable() { + private static Thread startController(final Controller controller) { + Thread thread = new Thread(new Runnable() { @Override public void run() { try { @@ -94,11 +103,13 @@ public final class Server { Ln.d("Controller stopped"); } } - }).start(); + }); + thread.start(); + return thread; } - private static void startDeviceMessageSender(final DeviceMessageSender sender) { - new Thread(new Runnable() { + private static Thread startDeviceMessageSender(final DeviceMessageSender sender) { + Thread thread = new Thread(new Runnable() { @Override public void run() { try { @@ -108,7 +119,9 @@ public final class Server { Ln.d("Device message sender stopped"); } } - }).start(); + }); + thread.start(); + return thread; } private static Options createOptions(String... args) { From 83910d3b9c961aa30eec4cf01aee7d38fb050fae Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 1 Jan 2021 16:34:47 +0100 Subject: [PATCH 0437/2244] Initialize server struct dynamically This will allow to add mutex/cond fields. --- app/src/scrcpy.c | 35 +++++++++++++++++++++-------------- app/src/server.c | 21 +++++++++++++++++++-- app/src/server.h | 19 +------------------ 3 files changed, 41 insertions(+), 34 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index a543e11c..4c502ff6 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -34,7 +34,7 @@ #include "util/log.h" #include "util/net.h" -static struct server server = SERVER_INITIALIZER; +static struct server server; static struct screen screen = SCREEN_INITIALIZER; static struct fps_counter fps_counter; static struct video_buffer video_buffer; @@ -304,6 +304,19 @@ av_log_callback(void *avcl, int level, const char *fmt, va_list vl) { bool scrcpy(const struct scrcpy_options *options) { + if (!server_init(&server)) { + return false; + } + + bool server_started = false; + bool fps_counter_initialized = false; + bool video_buffer_initialized = false; + bool file_handler_initialized = false; + bool recorder_initialized = false; + bool stream_started = false; + bool controller_initialized = false; + bool controller_started = false; + bool record = !!options->record_filename; struct server_params params = { .log_level = options->log_level, @@ -322,18 +335,10 @@ scrcpy(const struct scrcpy_options *options) { .force_adb_forward = options->force_adb_forward, }; if (!server_start(&server, options->serial, ¶ms)) { - return false; + goto end; } - bool ret = false; - - bool fps_counter_initialized = false; - bool video_buffer_initialized = false; - bool file_handler_initialized = false; - bool recorder_initialized = false; - bool stream_started = false; - bool controller_initialized = false; - bool controller_started = false; + server_started = true; if (!sdl_init_and_configure(options->display, options->render_driver, options->disable_screensaver)) { @@ -444,7 +449,7 @@ scrcpy(const struct scrcpy_options *options) { input_manager_init(&input_manager, options); - ret = event_loop(options); + bool ret = event_loop(options); LOGD("quit..."); screen_destroy(&screen); @@ -465,8 +470,10 @@ end: fps_counter_interrupt(&fps_counter); } - // shutdown the sockets and kill the server - server_stop(&server); + if (server_started) { + // shutdown the sockets and kill the server + server_stop(&server); + } // now that the sockets are shutdown, the stream and controller are // interrupted, we can join them diff --git a/app/src/server.c b/app/src/server.c index 9267356b..42e633af 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -353,9 +353,26 @@ close_socket(socket_t socket) { } } -void +bool server_init(struct server *server) { - *server = (struct server) SERVER_INITIALIZER; + server->serial = NULL; + server->process = PROCESS_NONE; + server->wait_server_thread = NULL; + atomic_flag_clear_explicit(&server->server_socket_closed, + memory_order_relaxed); + + server->server_socket = INVALID_SOCKET; + server->video_socket = INVALID_SOCKET; + server->control_socket = INVALID_SOCKET; + + server->port_range.first = 0; + server->port_range.last = 0; + server->local_port = 0; + + server->tunnel_enabled = false; + server->tunnel_forward = false; + + return true; } static int diff --git a/app/src/server.h b/app/src/server.h index 30ce09bc..823db0ea 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -27,23 +27,6 @@ struct server { bool tunnel_forward; // use "adb forward" instead of "adb reverse" }; -#define SERVER_INITIALIZER { \ - .serial = NULL, \ - .process = PROCESS_NONE, \ - .wait_server_thread = NULL, \ - .server_socket_closed = ATOMIC_FLAG_INIT, \ - .server_socket = INVALID_SOCKET, \ - .video_socket = INVALID_SOCKET, \ - .control_socket = INVALID_SOCKET, \ - .port_range = { \ - .first = 0, \ - .last = 0, \ - }, \ - .local_port = 0, \ - .tunnel_enabled = false, \ - .tunnel_forward = false, \ -} - struct server_params { enum sc_log_level log_level; const char *crop; @@ -62,7 +45,7 @@ struct server_params { }; // init default values -void +bool server_init(struct server *server); // push, enable tunnel et start the server From 05e8c1a3c5e955d2941d882e5fe784e6695e3ec4 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 1 Jan 2021 23:39:29 +0100 Subject: [PATCH 0438/2244] Call CloseHandle() after wait on Windows TerminateProcess() is "equivalent" to kill(), while WaitForSingleObject() is "equivalent" to waitpid(), so the handle must be closed after WaitForSingleObject(). --- app/src/sys/win/command.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/sys/win/command.c b/app/src/sys/win/command.c index fbc0d04c..7b483300 100644 --- a/app/src/sys/win/command.c +++ b/app/src/sys/win/command.c @@ -56,7 +56,7 @@ cmd_execute(const char *const argv[], HANDLE *handle) { bool cmd_terminate(HANDLE handle) { - return TerminateProcess(handle, 1) && CloseHandle(handle); + return TerminateProcess(handle, 1); } bool @@ -70,6 +70,7 @@ cmd_simple_wait(HANDLE handle, DWORD *exit_code) { if (exit_code) { *exit_code = code; } + CloseHandle(handle); return !code; } From 10b749e27d34f91d04ff165134fda23e6ec2cc3b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 1 Jan 2021 12:41:25 +0100 Subject: [PATCH 0439/2244] Kill the server only after a small delay Let the server terminate properly once all the sockets are closed. If it does not terminate (this can happen if the device is asleep), then kill it. Note: since the server process termination is detected by a flag set after waitpid() returns, there is a small chance that the process terminates (and the PID assigned to a new process) before the flag is set but before the kill() call. This race condition already existed before this commit. Fixes #1992 --- app/src/server.c | 46 ++++++++++++++++++++++++++++++++++++++++++++-- app/src/server.h | 5 +++++ 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/app/src/server.c b/app/src/server.c index 42e633af..cac7b367 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -11,6 +11,7 @@ #include "config.h" #include "command.h" +#include "util/lock.h" #include "util/log.h" #include "util/net.h" #include "util/str_util.h" @@ -361,6 +362,19 @@ server_init(struct server *server) { atomic_flag_clear_explicit(&server->server_socket_closed, memory_order_relaxed); + server->mutex = SDL_CreateMutex(); + if (!server->mutex) { + return false; + } + + server->process_terminated_cond = SDL_CreateCond(); + if (!server->process_terminated_cond) { + SDL_DestroyMutex(server->mutex); + return false; + } + + server->process_terminated = false; + server->server_socket = INVALID_SOCKET; server->video_socket = INVALID_SOCKET; server->control_socket = INVALID_SOCKET; @@ -379,6 +393,12 @@ static int run_wait_server(void *data) { struct server *server = data; cmd_simple_wait(server->process, NULL); // ignore exit code + + mutex_lock(server->mutex); + server->process_terminated = true; + cond_signal(server->process_terminated_cond); + mutex_unlock(server->mutex); + // no need for synchronization, server_socket is initialized before this // thread was created if (server->server_socket != INVALID_SOCKET @@ -510,17 +530,39 @@ server_stop(struct server *server) { assert(server->process != PROCESS_NONE); - cmd_terminate(server->process); - if (server->tunnel_enabled) { // ignore failure disable_tunnel(server); } + // Give some delay for the server to terminate properly + mutex_lock(server->mutex); + int r = 0; + if (!server->process_terminated) { +#define WATCHDOG_DELAY_MS 1000 + r = cond_wait_timeout(server->process_terminated_cond, + server->mutex, + WATCHDOG_DELAY_MS); + } + mutex_unlock(server->mutex); + + // After this delay, kill the server if it's not dead already. + // On some devices, closing the sockets is not sufficient to wake up the + // blocking calls while the device is asleep. + if (r == SDL_MUTEX_TIMEDOUT) { + // FIXME There is a race condition here: there is a small chance that + // the process is already terminated, and the PID assigned to a new + // process. + LOGW("Killing the server..."); + cmd_terminate(server->process); + } + SDL_WaitThread(server->wait_server_thread, NULL); } void server_destroy(struct server *server) { SDL_free(server->serial); + SDL_DestroyCond(server->process_terminated_cond); + SDL_DestroyMutex(server->mutex); } diff --git a/app/src/server.h b/app/src/server.h index 823db0ea..9b558aee 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -18,6 +18,11 @@ struct server { process_t process; SDL_Thread *wait_server_thread; atomic_flag server_socket_closed; + + SDL_mutex *mutex; + SDL_cond *process_terminated_cond; + bool process_terminated; + socket_t server_socket; // only used if !tunnel_forward socket_t video_socket; socket_t control_socket; From f682b87ba5ed6a5126cc23dff905218b177a2436 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 2 Jan 2021 00:53:32 +0100 Subject: [PATCH 0440/2244] Bump version to 1.17 --- 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 11964cf6..230f8d21 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('scrcpy', 'c', - version: '1.16', + version: '1.17', meson_version: '>= 0.48', default_options: [ 'c_std=c11', diff --git a/server/build.gradle b/server/build.gradle index 94a492c6..08ce2d27 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -6,8 +6,8 @@ android { applicationId "com.genymobile.scrcpy" minSdkVersion 21 targetSdkVersion 30 - versionCode 19 - versionName "1.16" + versionCode 20 + versionName "1.17" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index e03939a5..9afc5ba7 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.16 +SCRCPY_VERSION_NAME=1.17 PLATFORM=${ANDROID_PLATFORM:-30} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-30.0.0} From c5c5fc18aec537a5c4134a5bcd45e9f8bbadb66d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 2 Jan 2021 01:26:23 +0100 Subject: [PATCH 0441/2244] Update links to v1.17 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 a7c395ee..5dcf7b27 100644 --- a/BUILD.md +++ b/BUILD.md @@ -254,10 +254,10 @@ You can then [run](README.md#run) _scrcpy_. ## Prebuilt server - - [`scrcpy-server-v1.16`][direct-scrcpy-server] - _(SHA-256: 94a79e05b4498d0460ab7bd9d12cbf05156e3a47bf0c5d1420cee1d4493b3832)_ + - [`scrcpy-server-v1.17`][direct-scrcpy-server] + _(SHA-256: 11b5ad2d1bc9b9730fb7254a78efd71a8ff46b1938ff468e47a21b653a1b6725_ -[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.16/scrcpy-server-v1.16 +[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.17/scrcpy-server-v1.17 Download the prebuilt server somewhere, and specify its path during the Meson configuration: diff --git a/README.md b/README.md index 17b8e134..d8214e02 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# scrcpy (v1.16) +# scrcpy (v1.17) [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.16.zip`][direct-win64] - _(SHA-256: 3f30dc5db1a2f95c2b40a0f5de91ec1642d9f53799250a8c529bc882bc0918f0)_ + - [`scrcpy-win64-v1.17.zip`][direct-win64] + _(SHA-256: 8b9e57993c707367ed10ebfe0e1ef563c7a29d9af4a355cd8b6a52a317c73eea)_ -[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.16/scrcpy-win64-v1.16.zip +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.17/scrcpy-win64-v1.17.zip It is also available in [Chocolatey]: From 192fbd8450058deb5e70aa1b9bfec3637b5c5e72 Mon Sep 17 00:00:00 2001 From: clesiemo3 Date: Sat, 2 Jan 2021 11:18:23 -0600 Subject: [PATCH 0442/2244] Use --cask for latest versions of brew PR #2004 Signed-off-by: Romain Vimont --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index d8214e02..6a987a73 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,10 @@ brew install scrcpy You need `adb`, accessible from your `PATH`. If you don't have it yet: ```bash +# Homebrew >= 2.6.0 +brew install --cask android-platform-tools + +# Homebrew < 2.6.0 brew cask install android-platform-tools ``` From ed130e05d55615d6014d93f15cfcb92ad62b01d8 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 3 Jan 2021 22:40:33 +0100 Subject: [PATCH 0443/2244] Fix possibly uninitialized value Due to gotos, "ret" may be returned uninitialized. --- app/src/scrcpy.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 4c502ff6..3d043b95 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -308,6 +308,8 @@ scrcpy(const struct scrcpy_options *options) { return false; } + bool ret = false; + bool server_started = false; bool fps_counter_initialized = false; bool video_buffer_initialized = false; @@ -449,7 +451,7 @@ scrcpy(const struct scrcpy_options *options) { input_manager_init(&input_manager, options); - bool ret = event_loop(options); + ret = event_loop(options); LOGD("quit..."); screen_destroy(&screen); From aa8b5713896935e2dfe22ab01e21b84d660c22a1 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 4 Jan 2021 08:16:32 +0100 Subject: [PATCH 0444/2244] Increase display id range Some devices use big display id values. Refs #2009 --- app/src/cli.c | 6 +++--- app/src/scrcpy.h | 2 +- app/src/server.c | 4 ++-- app/src/server.h | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index f01b7941..694282a2 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -478,14 +478,14 @@ parse_port_range(const char *s, struct sc_port_range *port_range) { } static bool -parse_display_id(const char *s, uint16_t *display_id) { +parse_display_id(const char *s, uint32_t *display_id) { long value; - bool ok = parse_integer_arg(s, &value, false, 0, 0xFFFF, "display id"); + bool ok = parse_integer_arg(s, &value, false, 0, 0x7FFFFFFF, "display id"); if (!ok) { return false; } - *display_id = (uint16_t) value; + *display_id = (uint32_t) value; return true; } diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index 8548d1f7..c1d7b612 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -65,7 +65,7 @@ struct scrcpy_options { int16_t window_y; // SC_WINDOW_POSITION_UNDEFINED for "auto" uint16_t window_width; uint16_t window_height; - uint16_t display_id; + uint32_t display_id; bool show_touches; bool fullscreen; bool always_on_top; diff --git a/app/src/server.c b/app/src/server.c index cac7b367..217e7e41 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -258,12 +258,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[5]; - char display_id_string[6]; + char display_id_string[11]; 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); + sprintf(display_id_string, "%"PRIu32, params->display_id); const char *const cmd[] = { "shell", "CLASSPATH=" DEVICE_SERVER_PATH, diff --git a/app/src/server.h b/app/src/server.h index 9b558aee..c97a9153 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -43,7 +43,7 @@ struct server_params { uint16_t max_fps; int8_t lock_video_orientation; bool control; - uint16_t display_id; + uint32_t display_id; bool show_touches; bool stay_awake; bool force_adb_forward; From 4bd9da4c93d3727c6cd7597452cb255086b47818 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 3 Jan 2021 14:55:15 +0100 Subject: [PATCH 0445/2244] Split command into process and adb The process API provides the system-specific implementation, the adb API uses it to expose adb commands. --- app/meson.build | 7 +-- app/src/{command.c => adb.c} | 24 ++--------- app/src/adb.h | 34 +++++++++++++++ app/src/file_handler.c | 6 +-- app/src/file_handler.h | 2 +- app/src/scrcpy.c | 1 - app/src/server.c | 10 ++--- app/src/server.h | 2 +- app/src/sys/unix/{command.c => process.c} | 10 ++--- app/src/sys/win/{command.c => process.c} | 8 ++-- app/src/util/process.c | 21 +++++++++ app/src/{command.h => util/process.h} | 52 +++++++---------------- 12 files changed, 97 insertions(+), 80 deletions(-) rename app/src/{command.c => adb.c} (90%) create mode 100644 app/src/adb.h rename app/src/sys/unix/{command.c => process.c} (95%) rename app/src/sys/win/{command.c => process.c} (93%) create mode 100644 app/src/util/process.c rename app/src/{command.h => util/process.h} (59%) diff --git a/app/meson.build b/app/meson.build index 28b9d141..d031e5cc 100644 --- a/app/meson.build +++ b/app/meson.build @@ -1,7 +1,7 @@ src = [ 'src/main.c', + 'src/adb.c', 'src/cli.c', - 'src/command.c', 'src/control_msg.c', 'src/controller.c', 'src/decoder.c', @@ -21,6 +21,7 @@ src = [ 'src/tiny_xpm.c', 'src/video_buffer.c', 'src/util/net.c', + 'src/util/process.c', 'src/util/str_util.c' ] @@ -76,10 +77,10 @@ endif cc = meson.get_compiler('c') if host_machine.system() == 'windows' - src += [ 'src/sys/win/command.c' ] + src += [ 'src/sys/win/process.c' ] dependencies += cc.find_library('ws2_32') else - src += [ 'src/sys/unix/command.c' ] + src += [ 'src/sys/unix/process.c' ] endif conf = configuration_data() diff --git a/app/src/command.c b/app/src/adb.c similarity index 90% rename from app/src/command.c rename to app/src/adb.c index 81047b7a..339c854d 100644 --- a/app/src/command.c +++ b/app/src/adb.c @@ -1,4 +1,4 @@ -#include "command.h" +#include "adb.h" #include #include @@ -70,7 +70,7 @@ show_adb_installation_msg() { {"pacman", "pacman -S android-tools"}, }; for (size_t i = 0; i < ARRAY_LEN(pkg_managers); ++i) { - if (cmd_search(pkg_managers[i].binary)) { + if (search_executable(pkg_managers[i].binary)) { LOGI("You may install 'adb' by \"%s\"", pkg_managers[i].command); return; } @@ -118,7 +118,7 @@ adb_execute(const char *serial, const char *const adb_cmd[], size_t len) { memcpy(&cmd[i], adb_cmd, len * sizeof(const char *)); cmd[len + i] = NULL; - enum process_result r = cmd_execute(cmd, &process); + enum process_result r = process_execute(cmd, &process); if (r != PROCESS_SUCCESS) { show_adb_err_msg(r, cmd); return PROCESS_NONE; @@ -211,21 +211,3 @@ adb_install(const char *serial, const char *local) { return proc; } - -bool -process_check_success(process_t proc, const char *name) { - if (proc == PROCESS_NONE) { - LOGE("Could not execute \"%s\"", name); - return false; - } - exit_code_t exit_code; - if (!cmd_simple_wait(proc, &exit_code)) { - if (exit_code != NO_EXIT_CODE) { - LOGE("\"%s\" returned with value %" PRIexitcode, name, exit_code); - } else { - LOGE("\"%s\" exited unexpectedly", name); - } - return false; - } - return true; -} diff --git a/app/src/adb.h b/app/src/adb.h new file mode 100644 index 00000000..52cdf823 --- /dev/null +++ b/app/src/adb.h @@ -0,0 +1,34 @@ +#ifndef SC_ADB_H +#define SC_ADB_H + +#include +#include + +#include "config.h" + +#include "util/process.h" + +process_t +adb_execute(const char *serial, const char *const adb_cmd[], size_t len); + +process_t +adb_forward(const char *serial, uint16_t local_port, + const char *device_socket_name); + +process_t +adb_forward_remove(const char *serial, uint16_t local_port); + +process_t +adb_reverse(const char *serial, const char *device_socket_name, + uint16_t local_port); + +process_t +adb_reverse_remove(const char *serial, const char *device_socket_name); + +process_t +adb_push(const char *serial, const char *local, const char *remote); + +process_t +adb_install(const char *serial, const char *local); + +#endif diff --git a/app/src/file_handler.c b/app/src/file_handler.c index ba689404..502b3e96 100644 --- a/app/src/file_handler.c +++ b/app/src/file_handler.c @@ -4,7 +4,7 @@ #include #include "config.h" -#include "command.h" +#include "adb.h" #include "util/lock.h" #include "util/log.h" @@ -176,10 +176,10 @@ file_handler_stop(struct file_handler *file_handler) { file_handler->stopped = true; cond_signal(file_handler->event_cond); if (file_handler->current_process != PROCESS_NONE) { - if (!cmd_terminate(file_handler->current_process)) { + if (!process_terminate(file_handler->current_process)) { LOGW("Could not terminate install process"); } - cmd_simple_wait(file_handler->current_process, NULL); + process_simple_wait(file_handler->current_process, NULL); file_handler->current_process = PROCESS_NONE; } mutex_unlock(file_handler->mutex); diff --git a/app/src/file_handler.h b/app/src/file_handler.h index 078d0ca5..71b58952 100644 --- a/app/src/file_handler.h +++ b/app/src/file_handler.h @@ -6,7 +6,7 @@ #include #include "config.h" -#include "command.h" +#include "adb.h" #include "util/cbuf.h" typedef enum { diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 3d043b95..2d593234 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -14,7 +14,6 @@ #endif #include "config.h" -#include "command.h" #include "common.h" #include "compat.h" #include "controller.h" diff --git a/app/src/server.c b/app/src/server.c index 217e7e41..2766b0e1 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -10,7 +10,7 @@ #include #include "config.h" -#include "command.h" +#include "adb.h" #include "util/lock.h" #include "util/log.h" #include "util/net.h" @@ -392,7 +392,7 @@ server_init(struct server *server) { static int run_wait_server(void *data) { struct server *server = data; - cmd_simple_wait(server->process, NULL); // ignore exit code + process_simple_wait(server->process, NULL); // ignore exit code mutex_lock(server->mutex); server->process_terminated = true; @@ -447,8 +447,8 @@ 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) { - cmd_terminate(server->process); - cmd_simple_wait(server->process, NULL); // ignore exit code + process_terminate(server->process); + process_simple_wait(server->process, NULL); // ignore exit code goto error2; } @@ -554,7 +554,7 @@ server_stop(struct server *server) { // the process is already terminated, and the PID assigned to a new // process. LOGW("Killing the server..."); - cmd_terminate(server->process); + process_terminate(server->process); } SDL_WaitThread(server->wait_server_thread, NULL); diff --git a/app/src/server.h b/app/src/server.h index c97a9153..48d8cd32 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -7,7 +7,7 @@ #include #include "config.h" -#include "command.h" +#include "adb.h" #include "common.h" #include "scrcpy.h" #include "util/log.h" diff --git a/app/src/sys/unix/command.c b/app/src/sys/unix/process.c similarity index 95% rename from app/src/sys/unix/command.c rename to app/src/sys/unix/process.c index c4c262e4..d029319d 100644 --- a/app/src/sys/unix/command.c +++ b/app/src/sys/unix/process.c @@ -9,7 +9,7 @@ # define _DARWIN_C_SOURCE // for strdup(), strtok_r(), memset_pattern4() #endif -#include "command.h" +#include "util/process.h" #include "config.h" @@ -27,7 +27,7 @@ #include "util/log.h" bool -cmd_search(const char *file) { +search_executable(const char *file) { char *path = getenv("PATH"); if (!path) return false; @@ -63,7 +63,7 @@ cmd_search(const char *file) { } enum process_result -cmd_execute(const char *const argv[], pid_t *pid) { +process_execute(const char *const argv[], pid_t *pid) { int fd[2]; if (pipe(fd) == -1) { @@ -125,7 +125,7 @@ end: } bool -cmd_terminate(pid_t pid) { +process_terminate(pid_t pid) { if (pid <= 0) { LOGC("Requested to kill %d, this is an error. Please report the bug.\n", (int) pid); @@ -135,7 +135,7 @@ cmd_terminate(pid_t pid) { } bool -cmd_simple_wait(pid_t pid, int *exit_code) { +process_simple_wait(pid_t pid, int *exit_code) { int status; int code; if (waitpid(pid, &status, 0) == -1 || !WIFEXITED(status)) { diff --git a/app/src/sys/win/command.c b/app/src/sys/win/process.c similarity index 93% rename from app/src/sys/win/command.c rename to app/src/sys/win/process.c index 7b483300..da776e8c 100644 --- a/app/src/sys/win/command.c +++ b/app/src/sys/win/process.c @@ -1,4 +1,4 @@ -#include "command.h" +#include "util/process.h" #include @@ -21,7 +21,7 @@ build_cmd(char *cmd, size_t len, const char *const argv[]) { } enum process_result -cmd_execute(const char *const argv[], HANDLE *handle) { +process_execute(const char *const argv[], HANDLE *handle) { STARTUPINFOW si; PROCESS_INFORMATION pi; memset(&si, 0, sizeof(si)); @@ -55,12 +55,12 @@ cmd_execute(const char *const argv[], HANDLE *handle) { } bool -cmd_terminate(HANDLE handle) { +process_terminate(HANDLE handle) { return TerminateProcess(handle, 1); } bool -cmd_simple_wait(HANDLE handle, DWORD *exit_code) { +process_simple_wait(HANDLE handle, DWORD *exit_code) { DWORD code; if (WaitForSingleObject(handle, INFINITE) != WAIT_OBJECT_0 || !GetExitCodeProcess(handle, &code)) { diff --git a/app/src/util/process.c b/app/src/util/process.c new file mode 100644 index 00000000..e4eb8282 --- /dev/null +++ b/app/src/util/process.c @@ -0,0 +1,21 @@ +#include "process.h" + +#include "log.h" + +bool +process_check_success(process_t proc, const char *name) { + if (proc == PROCESS_NONE) { + LOGE("Could not execute \"%s\"", name); + return false; + } + exit_code_t exit_code; + if (!process_simple_wait(proc, &exit_code)) { + if (exit_code != NO_EXIT_CODE) { + LOGE("\"%s\" returned with value %" PRIexitcode, name, exit_code); + } else { + LOGE("\"%s\" exited unexpectedly", name); + } + return false; + } + return true; +} diff --git a/app/src/command.h b/app/src/util/process.h similarity index 59% rename from app/src/command.h rename to app/src/util/process.h index 7035139b..1e6f256b 100644 --- a/app/src/command.h +++ b/app/src/util/process.h @@ -1,8 +1,9 @@ -#ifndef COMMAND_H -#define COMMAND_H +#ifndef SC_PROCESS_H +#define SC_PROCESS_H #include -#include + +#include "config.h" #ifdef _WIN32 @@ -31,56 +32,35 @@ #endif -#include "config.h" - enum process_result { PROCESS_SUCCESS, PROCESS_ERROR_GENERIC, PROCESS_ERROR_MISSING_BINARY, }; -#ifndef __WINDOWS__ -bool -cmd_search(const char *file); -#endif - +// execute the command and write the result to the output parameter "process" enum process_result -cmd_execute(const char *const argv[], process_t *process); +process_execute(const char *const argv[], process_t *process); +// kill the process bool -cmd_terminate(process_t pid); +process_terminate(process_t pid); +// wait and close the process bool -cmd_simple_wait(process_t pid, exit_code_t *exit_code); - -process_t -adb_execute(const char *serial, const char *const adb_cmd[], size_t len); - -process_t -adb_forward(const char *serial, uint16_t local_port, - const char *device_socket_name); - -process_t -adb_forward_remove(const char *serial, uint16_t local_port); - -process_t -adb_reverse(const char *serial, const char *device_socket_name, - uint16_t local_port); - -process_t -adb_reverse_remove(const char *serial, const char *device_socket_name); - -process_t -adb_push(const char *serial, const char *local, const char *remote); - -process_t -adb_install(const char *serial, const char *local); +process_simple_wait(process_t pid, exit_code_t *exit_code); // convenience function to wait for a successful process execution // automatically log process errors with the provided process name bool process_check_success(process_t proc, const char *name); +#ifndef _WIN32 +// only used to find package manager, not implemented for Windows +bool +search_executable(const char *file); +#endif + // return the absolute path of the executable (the scrcpy binary) // may be NULL on error; to be freed by SDL_free char * From cc6f5020d8988c137f04eb22f45e70baa4c9465d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 3 Jan 2021 15:09:01 +0100 Subject: [PATCH 0446/2244] Move conditional src files in meson.build Declare all the source files (including the platform-specific ones) at the beginning. --- app/meson.build | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/meson.build b/app/meson.build index d031e5cc..651e756b 100644 --- a/app/meson.build +++ b/app/meson.build @@ -25,6 +25,12 @@ src = [ 'src/util/str_util.c' ] +if host_machine.system() == 'windows' + src += [ 'src/sys/win/process.c' ] +else + src += [ 'src/sys/unix/process.c' ] +endif + if not get_option('crossbuild_windows') # native build @@ -77,10 +83,7 @@ endif cc = meson.get_compiler('c') if host_machine.system() == 'windows' - src += [ 'src/sys/win/process.c' ] dependencies += cc.find_library('ws2_32') -else - src += [ 'src/sys/unix/process.c' ] endif conf = configuration_data() From 821c175730df6d4d47241ae72346c3be35d1fe99 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 3 Jan 2021 16:40:34 +0100 Subject: [PATCH 0447/2244] Rename process_simple_wait to process_wait Adding "simple" in the function name brings no benefit. --- app/src/file_handler.c | 2 +- app/src/server.c | 4 ++-- app/src/sys/unix/process.c | 2 +- app/src/sys/win/process.c | 2 +- app/src/util/process.c | 2 +- app/src/util/process.h | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/src/file_handler.c b/app/src/file_handler.c index 502b3e96..5efbce0e 100644 --- a/app/src/file_handler.c +++ b/app/src/file_handler.c @@ -179,7 +179,7 @@ file_handler_stop(struct file_handler *file_handler) { if (!process_terminate(file_handler->current_process)) { LOGW("Could not terminate install process"); } - process_simple_wait(file_handler->current_process, NULL); + process_wait(file_handler->current_process, NULL); file_handler->current_process = PROCESS_NONE; } mutex_unlock(file_handler->mutex); diff --git a/app/src/server.c b/app/src/server.c index 2766b0e1..1697a000 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -392,7 +392,7 @@ server_init(struct server *server) { static int run_wait_server(void *data) { struct server *server = data; - process_simple_wait(server->process, NULL); // ignore exit code + process_wait(server->process, NULL); // ignore exit code mutex_lock(server->mutex); server->process_terminated = true; @@ -448,7 +448,7 @@ server_start(struct server *server, const char *serial, SDL_CreateThread(run_wait_server, "wait-server", server); if (!server->wait_server_thread) { process_terminate(server->process); - process_simple_wait(server->process, NULL); // ignore exit code + process_wait(server->process, NULL); // ignore exit code goto error2; } diff --git a/app/src/sys/unix/process.c b/app/src/sys/unix/process.c index d029319d..3f95ce7f 100644 --- a/app/src/sys/unix/process.c +++ b/app/src/sys/unix/process.c @@ -135,7 +135,7 @@ process_terminate(pid_t pid) { } bool -process_simple_wait(pid_t pid, int *exit_code) { +process_wait(pid_t pid, int *exit_code) { int status; int code; if (waitpid(pid, &status, 0) == -1 || !WIFEXITED(status)) { diff --git a/app/src/sys/win/process.c b/app/src/sys/win/process.c index da776e8c..c30e751b 100644 --- a/app/src/sys/win/process.c +++ b/app/src/sys/win/process.c @@ -60,7 +60,7 @@ process_terminate(HANDLE handle) { } bool -process_simple_wait(HANDLE handle, DWORD *exit_code) { +process_wait(HANDLE handle, DWORD *exit_code) { DWORD code; if (WaitForSingleObject(handle, INFINITE) != WAIT_OBJECT_0 || !GetExitCodeProcess(handle, &code)) { diff --git a/app/src/util/process.c b/app/src/util/process.c index e4eb8282..e485ce17 100644 --- a/app/src/util/process.c +++ b/app/src/util/process.c @@ -9,7 +9,7 @@ process_check_success(process_t proc, const char *name) { return false; } exit_code_t exit_code; - if (!process_simple_wait(proc, &exit_code)) { + if (!process_wait(proc, &exit_code)) { if (exit_code != NO_EXIT_CODE) { LOGE("\"%s\" returned with value %" PRIexitcode, name, exit_code); } else { diff --git a/app/src/util/process.h b/app/src/util/process.h index 1e6f256b..58328a3e 100644 --- a/app/src/util/process.h +++ b/app/src/util/process.h @@ -48,7 +48,7 @@ process_terminate(process_t pid); // wait and close the process bool -process_simple_wait(process_t pid, exit_code_t *exit_code); +process_wait(process_t pid, exit_code_t *exit_code); // convenience function to wait for a successful process execution // automatically log process errors with the provided process name From d580ee30f1b388667568b0a30710e365939ca994 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 3 Jan 2021 17:06:08 +0100 Subject: [PATCH 0448/2244] Separate process wait and close On Linux, waitpid() both waits for the process to terminate and reaps it (closes its handle). On Windows, these actions are separated into WaitForSingleObject() and CloseHandle(). Expose these actions separately, so that it is possible to send a signal to a process while waiting for its termination without race condition. This allows to wait for server termination normally, but kill the process without race condition if it is not terminated after some delay. --- app/src/server.c | 8 ++++---- app/src/sys/unix/process.c | 31 ++++++++++++++++++++++++++----- app/src/sys/win/process.c | 26 +++++++++++++++++++++++--- app/src/util/process.h | 12 +++++++++++- 4 files changed, 64 insertions(+), 13 deletions(-) diff --git a/app/src/server.c b/app/src/server.c index 1697a000..cb2a24db 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -392,7 +392,7 @@ server_init(struct server *server) { static int run_wait_server(void *data) { struct server *server = data; - process_wait(server->process, NULL); // ignore exit code + process_wait_noclose(server->process, NULL); // ignore exit code mutex_lock(server->mutex); server->process_terminated = true; @@ -550,14 +550,14 @@ server_stop(struct server *server) { // On some devices, closing the sockets is not sufficient to wake up the // blocking calls while the device is asleep. if (r == SDL_MUTEX_TIMEDOUT) { - // FIXME There is a race condition here: there is a small chance that - // the process is already terminated, and the PID assigned to a new - // process. + // The process is terminated, but not reaped (closed) yet, so its PID + // is still valid. LOGW("Killing the server..."); process_terminate(server->process); } SDL_WaitThread(server->wait_server_thread, NULL); + process_close(server->process); } void diff --git a/app/src/sys/unix/process.c b/app/src/sys/unix/process.c index 3f95ce7f..00a8d81c 100644 --- a/app/src/sys/unix/process.c +++ b/app/src/sys/unix/process.c @@ -134,15 +134,21 @@ process_terminate(pid_t pid) { return kill(pid, SIGTERM) != -1; } -bool -process_wait(pid_t pid, int *exit_code) { - int status; +static bool +process_wait_internal(pid_t pid, int *exit_code, bool close) { int code; - if (waitpid(pid, &status, 0) == -1 || !WIFEXITED(status)) { + int options = WEXITED; + if (!close) { + options |= WNOWAIT; + } + + siginfo_t info; + int r = waitid(P_PID, pid, &info, options); + if (r == -1 || info.si_code != CLD_EXITED) { // could not wait, or exited unexpectedly, probably by a signal code = -1; } else { - code = WEXITSTATUS(status); + code = info.si_status; } if (exit_code) { *exit_code = code; @@ -150,6 +156,21 @@ process_wait(pid_t pid, int *exit_code) { return !code; } +bool +process_wait(pid_t pid, int *exit_code) { + return process_wait_internal(pid, exit_code, true); +} + +bool +process_wait_noclose(pid_t pid, int *exit_code) { + return process_wait_internal(pid, exit_code, false); +} + +void +process_close(pid_t pid) { + process_wait_internal(pid, NULL, true); +} + char * get_executable_path(void) { // diff --git a/app/src/sys/win/process.c b/app/src/sys/win/process.c index c30e751b..f2a86b22 100644 --- a/app/src/sys/win/process.c +++ b/app/src/sys/win/process.c @@ -1,5 +1,6 @@ #include "util/process.h" +#include #include #include "config.h" @@ -59,8 +60,8 @@ process_terminate(HANDLE handle) { return TerminateProcess(handle, 1); } -bool -process_wait(HANDLE handle, DWORD *exit_code) { +static bool +process_wait_internal(HANDLE handle, DWORD *exit_code, bool close) { DWORD code; if (WaitForSingleObject(handle, INFINITE) != WAIT_OBJECT_0 || !GetExitCodeProcess(handle, &code)) { @@ -70,10 +71,29 @@ process_wait(HANDLE handle, DWORD *exit_code) { if (exit_code) { *exit_code = code; } - CloseHandle(handle); + if (close) { + CloseHandle(handle); + } return !code; } +bool +process_wait(HANDLE handle, DWORD *exit_code) { + return process_wait_internal(handle, exit_code, true); +} + +bool +process_wait_noclose(HANDLE handle, DWORD *exit_code) { + return process_wait_internal(handle, exit_code, false); +} + +void +process_close(HANDLE handle) { + bool closed = CloseHandle(handle); + assert(closed); + (void) closed; +} + char * get_executable_path(void) { HMODULE hModule = GetModuleHandleW(NULL); diff --git a/app/src/util/process.h b/app/src/util/process.h index 58328a3e..ad2bc0d0 100644 --- a/app/src/util/process.h +++ b/app/src/util/process.h @@ -46,10 +46,20 @@ process_execute(const char *const argv[], process_t *process); bool process_terminate(process_t pid); -// wait and close the process +// wait and close the process (like waitpid()) bool process_wait(process_t pid, exit_code_t *exit_code); +// wait (but does not close) the process (waitid() with WNOWAIT) +bool +process_wait_noclose(process_t pid, exit_code_t *exit_code); + +// close the process +// +// Semantically, process_wait = process_wait_noclose + process_close. +void +process_close(process_t pid); + // convenience function to wait for a successful process execution // automatically log process errors with the provided process name bool From 1e215199dc682c93b37ebaca9df6f1c0eec68e97 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 8 Jan 2021 19:13:53 +0100 Subject: [PATCH 0449/2244] Remove unused struct port_range It had been replaced by struct sc_port_range in scrcpy.h. --- app/src/common.h | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/src/common.h b/app/src/common.h index 4cbf1d74..e5cbe953 100644 --- a/app/src/common.h +++ b/app/src/common.h @@ -27,9 +27,4 @@ struct position { struct point point; }; -struct port_range { - uint16_t first; - uint16_t last; -}; - #endif From 037be4af218c2bb5521dce65d01fb8ebd150f113 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 8 Jan 2021 19:21:54 +0100 Subject: [PATCH 0450/2244] Fix compat missing include The header libavformat/version.h was included, but not libavcodec/version.h. As a consequence, the LIBAVCODEC_VERSION_INT definition depended on the caller includes. --- app/src/compat.h | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/compat.h b/app/src/compat.h index de667bbf..21f19214 100644 --- a/app/src/compat.h +++ b/app/src/compat.h @@ -1,6 +1,7 @@ #ifndef COMPAT_H #define COMPAT_H +#include #include #include From 6385b8c162e58ed167f317a333ceeb13f3979f8c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 8 Jan 2021 19:15:29 +0100 Subject: [PATCH 0451/2244] Move common structs to coords.h The size, point and position structs were defined in common.h. Move them to coords.h so that common.h could be used for generic code to be included in all source files. --- app/src/common.h | 20 -------------------- app/src/control_msg.h | 2 +- app/src/coords.h | 24 ++++++++++++++++++++++++ app/src/device.h | 2 +- app/src/input_manager.h | 1 - app/src/recorder.h | 2 +- app/src/scrcpy.c | 1 - app/src/screen.h | 2 +- app/src/server.h | 1 - 9 files changed, 28 insertions(+), 27 deletions(-) create mode 100644 app/src/coords.h diff --git a/app/src/common.h b/app/src/common.h index e5cbe953..3f517d5b 100644 --- a/app/src/common.h +++ b/app/src/common.h @@ -1,30 +1,10 @@ #ifndef COMMON_H #define COMMON_H -#include - #include "config.h" #define ARRAY_LEN(a) (sizeof(a) / sizeof(a[0])) #define MIN(X,Y) (X) < (Y) ? (X) : (Y) #define MAX(X,Y) (X) > (Y) ? (X) : (Y) -struct size { - uint16_t width; - uint16_t height; -}; - -struct point { - int32_t x; - int32_t y; -}; - -struct position { - // The video screen size may be different from the real device screen size, - // so store to which size the absolute position apply, to scale it - // accordingly. - struct size screen_size; - struct point point; -}; - #endif diff --git a/app/src/control_msg.h b/app/src/control_msg.h index 6e3f239c..5014a84c 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -8,7 +8,7 @@ #include "config.h" #include "android/input.h" #include "android/keycodes.h" -#include "common.h" +#include "coords.h" #define CONTROL_MSG_MAX_SIZE (1 << 18) // 256k diff --git a/app/src/coords.h b/app/src/coords.h new file mode 100644 index 00000000..7be6836d --- /dev/null +++ b/app/src/coords.h @@ -0,0 +1,24 @@ +#ifndef SC_COORDS +#define SC_COORDS + +#include + +struct size { + uint16_t width; + uint16_t height; +}; + +struct point { + int32_t x; + int32_t y; +}; + +struct position { + // The video screen size may be different from the real device screen size, + // so store to which size the absolute position apply, to scale it + // accordingly. + struct size screen_size; + struct point point; +}; + +#endif diff --git a/app/src/device.h b/app/src/device.h index 8a94cd86..5f30194f 100644 --- a/app/src/device.h +++ b/app/src/device.h @@ -4,7 +4,7 @@ #include #include "config.h" -#include "common.h" +#include "coords.h" #include "util/net.h" #define DEVICE_NAME_FIELD_LENGTH 64 diff --git a/app/src/input_manager.h b/app/src/input_manager.h index df9b071f..ce53792b 100644 --- a/app/src/input_manager.h +++ b/app/src/input_manager.h @@ -6,7 +6,6 @@ #include #include "config.h" -#include "common.h" #include "controller.h" #include "fps_counter.h" #include "scrcpy.h" diff --git a/app/src/recorder.h b/app/src/recorder.h index bc87a23b..43a0a395 100644 --- a/app/src/recorder.h +++ b/app/src/recorder.h @@ -7,7 +7,7 @@ #include #include "config.h" -#include "common.h" +#include "coords.h" #include "scrcpy.h" #include "util/queue.h" diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 2d593234..c1aedcca 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -14,7 +14,6 @@ #endif #include "config.h" -#include "common.h" #include "compat.h" #include "controller.h" #include "decoder.h" diff --git a/app/src/screen.h b/app/src/screen.h index c4fbbf66..0e87d1c9 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -6,7 +6,7 @@ #include #include "config.h" -#include "common.h" +#include "coords.h" #include "opengl.h" struct video_buffer; diff --git a/app/src/server.h b/app/src/server.h index 48d8cd32..b607d691 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -8,7 +8,6 @@ #include "config.h" #include "adb.h" -#include "common.h" #include "scrcpy.h" #include "util/log.h" #include "util/net.h" From 59feb2a15c2e48091675d49561a6258b9d083710 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 8 Jan 2021 19:18:02 +0100 Subject: [PATCH 0452/2244] Group common includes into common.h Include config.h and compat.h in common.h, and include common.h from all source files. --- app/src/adb.c | 2 -- app/src/adb.h | 2 +- app/src/cli.c | 1 - app/src/cli.h | 2 +- app/src/common.h | 1 + app/src/control_msg.c | 1 - app/src/control_msg.h | 2 +- app/src/controller.c | 1 - app/src/controller.h | 2 +- app/src/decoder.c | 2 -- app/src/decoder.h | 2 +- app/src/device.c | 1 - app/src/device.h | 2 +- app/src/device_msg.c | 1 - app/src/device_msg.h | 2 +- app/src/event_converter.c | 2 -- app/src/event_converter.h | 2 +- app/src/file_handler.c | 1 - app/src/file_handler.h | 2 +- app/src/fps_counter.c | 1 - app/src/fps_counter.h | 2 +- app/src/input_manager.c | 1 - app/src/input_manager.h | 2 +- app/src/main.c | 3 +-- app/src/opengl.h | 2 +- app/src/receiver.c | 1 - app/src/receiver.h | 2 +- app/src/recorder.c | 2 -- app/src/recorder.h | 2 +- app/src/scrcpy.c | 2 -- app/src/scrcpy.h | 2 +- app/src/screen.c | 3 --- app/src/screen.h | 2 +- app/src/server.c | 1 - app/src/server.h | 2 +- app/src/stream.c | 2 -- app/src/stream.h | 2 +- app/src/sys/unix/process.c | 2 -- app/src/sys/win/process.c | 1 - app/src/tiny_xpm.c | 1 - app/src/tiny_xpm.h | 2 +- app/src/util/buffer_util.h | 2 +- app/src/util/cbuf.h | 2 +- app/src/util/lock.h | 2 +- app/src/util/net.c | 1 - app/src/util/net.h | 2 +- app/src/util/process.h | 2 +- app/src/util/queue.h | 2 +- app/src/util/str_util.c | 2 -- app/src/util/str_util.h | 2 +- app/src/video_buffer.c | 1 - app/src/video_buffer.h | 2 +- app/tests/test_cli.c | 2 +- 53 files changed, 30 insertions(+), 63 deletions(-) diff --git a/app/src/adb.c b/app/src/adb.c index 339c854d..086db174 100644 --- a/app/src/adb.c +++ b/app/src/adb.c @@ -5,8 +5,6 @@ #include #include -#include "config.h" -#include "common.h" #include "util/log.h" #include "util/str_util.h" diff --git a/app/src/adb.h b/app/src/adb.h index 52cdf823..453e3019 100644 --- a/app/src/adb.h +++ b/app/src/adb.h @@ -4,7 +4,7 @@ #include #include -#include "config.h" +#include "common.h" #include "util/process.h" diff --git a/app/src/cli.c b/app/src/cli.c index 694282a2..fbdef07f 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -6,7 +6,6 @@ #include #include -#include "config.h" #include "scrcpy.h" #include "util/log.h" #include "util/str_util.h" diff --git a/app/src/cli.h b/app/src/cli.h index 73dfe228..22bded79 100644 --- a/app/src/cli.h +++ b/app/src/cli.h @@ -3,7 +3,7 @@ #include -#include "config.h" +#include "common.h" #include "scrcpy.h" struct scrcpy_cli_args { diff --git a/app/src/common.h b/app/src/common.h index 3f517d5b..27c8d2fb 100644 --- a/app/src/common.h +++ b/app/src/common.h @@ -2,6 +2,7 @@ #define COMMON_H #include "config.h" +#include "compat.h" #define ARRAY_LEN(a) (sizeof(a) / sizeof(a[0])) #define MIN(X,Y) (X) < (Y) ? (X) : (Y) diff --git a/app/src/control_msg.c b/app/src/control_msg.c index 27c7903d..77e534cd 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -3,7 +3,6 @@ #include #include -#include "config.h" #include "util/buffer_util.h" #include "util/log.h" #include "util/str_util.h" diff --git a/app/src/control_msg.h b/app/src/control_msg.h index 5014a84c..e23cc89c 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -5,7 +5,7 @@ #include #include -#include "config.h" +#include "common.h" #include "android/input.h" #include "android/keycodes.h" #include "coords.h" diff --git a/app/src/controller.c b/app/src/controller.c index c5897e5d..6bfb80d3 100644 --- a/app/src/controller.c +++ b/app/src/controller.c @@ -2,7 +2,6 @@ #include -#include "config.h" #include "util/lock.h" #include "util/log.h" diff --git a/app/src/controller.h b/app/src/controller.h index 8011ef6a..5ef8755a 100644 --- a/app/src/controller.h +++ b/app/src/controller.h @@ -5,7 +5,7 @@ #include #include -#include "config.h" +#include "common.h" #include "control_msg.h" #include "receiver.h" #include "util/cbuf.h" diff --git a/app/src/decoder.c b/app/src/decoder.c index 49d4ce86..50c55ab2 100644 --- a/app/src/decoder.c +++ b/app/src/decoder.c @@ -7,8 +7,6 @@ #include #include -#include "config.h" -#include "compat.h" #include "events.h" #include "recorder.h" #include "video_buffer.h" diff --git a/app/src/decoder.h b/app/src/decoder.h index f243812c..8a3bc297 100644 --- a/app/src/decoder.h +++ b/app/src/decoder.h @@ -4,7 +4,7 @@ #include #include -#include "config.h" +#include "common.h" struct video_buffer; diff --git a/app/src/device.c b/app/src/device.c index f4c2628b..094dc616 100644 --- a/app/src/device.c +++ b/app/src/device.c @@ -1,6 +1,5 @@ #include "device.h" -#include "config.h" #include "util/log.h" bool diff --git a/app/src/device.h b/app/src/device.h index 5f30194f..c21b1ace 100644 --- a/app/src/device.h +++ b/app/src/device.h @@ -3,7 +3,7 @@ #include -#include "config.h" +#include "common.h" #include "coords.h" #include "util/net.h" diff --git a/app/src/device_msg.c b/app/src/device_msg.c index 09e68936..c82ce628 100644 --- a/app/src/device_msg.c +++ b/app/src/device_msg.c @@ -2,7 +2,6 @@ #include -#include "config.h" #include "util/buffer_util.h" #include "util/log.h" diff --git a/app/src/device_msg.h b/app/src/device_msg.h index 4b681e2c..b1860726 100644 --- a/app/src/device_msg.h +++ b/app/src/device_msg.h @@ -5,7 +5,7 @@ #include #include -#include "config.h" +#include "common.h" #define DEVICE_MSG_MAX_SIZE (1 << 18) // 256k // type: 1 byte; length: 4 bytes diff --git a/app/src/event_converter.c b/app/src/event_converter.c index ab48898d..cf010a16 100644 --- a/app/src/event_converter.c +++ b/app/src/event_converter.c @@ -1,7 +1,5 @@ #include "event_converter.h" -#include "config.h" - #define MAP(FROM, TO) case FROM: *to = TO; return true #define FAIL default: return false diff --git a/app/src/event_converter.h b/app/src/event_converter.h index c41887e1..cbd578a0 100644 --- a/app/src/event_converter.h +++ b/app/src/event_converter.h @@ -4,7 +4,7 @@ #include #include -#include "config.h" +#include "common.h" #include "control_msg.h" bool diff --git a/app/src/file_handler.c b/app/src/file_handler.c index 5efbce0e..3ad92c05 100644 --- a/app/src/file_handler.c +++ b/app/src/file_handler.c @@ -3,7 +3,6 @@ #include #include -#include "config.h" #include "adb.h" #include "util/lock.h" #include "util/log.h" diff --git a/app/src/file_handler.h b/app/src/file_handler.h index 71b58952..6649b9ea 100644 --- a/app/src/file_handler.h +++ b/app/src/file_handler.h @@ -5,7 +5,7 @@ #include #include -#include "config.h" +#include "common.h" #include "adb.h" #include "util/cbuf.h" diff --git a/app/src/fps_counter.c b/app/src/fps_counter.c index b4dd8b9b..e7607409 100644 --- a/app/src/fps_counter.c +++ b/app/src/fps_counter.c @@ -3,7 +3,6 @@ #include #include -#include "config.h" #include "util/lock.h" #include "util/log.h" diff --git a/app/src/fps_counter.h b/app/src/fps_counter.h index 52157172..2c13b3e2 100644 --- a/app/src/fps_counter.h +++ b/app/src/fps_counter.h @@ -7,7 +7,7 @@ #include #include -#include "config.h" +#include "common.h" struct fps_counter { SDL_Thread *thread; diff --git a/app/src/input_manager.c b/app/src/input_manager.c index bab85660..df01ea38 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -3,7 +3,6 @@ #include #include -#include "config.h" #include "event_converter.h" #include "util/lock.h" #include "util/log.h" diff --git a/app/src/input_manager.h b/app/src/input_manager.h index ce53792b..66daa9dd 100644 --- a/app/src/input_manager.h +++ b/app/src/input_manager.h @@ -5,7 +5,7 @@ #include -#include "config.h" +#include "common.h" #include "controller.h" #include "fps_counter.h" #include "scrcpy.h" diff --git a/app/src/main.c b/app/src/main.c index 71125673..18b9c710 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -7,9 +7,8 @@ #define SDL_MAIN_HANDLED // avoid link error on Linux Windows Subsystem #include -#include "config.h" +#include "common.h" #include "cli.h" -#include "compat.h" #include "util/log.h" static void diff --git a/app/src/opengl.h b/app/src/opengl.h index f0a89a14..1c1e4658 100644 --- a/app/src/opengl.h +++ b/app/src/opengl.h @@ -4,7 +4,7 @@ #include #include -#include "config.h" +#include "common.h" struct sc_opengl { const char *version; diff --git a/app/src/receiver.c b/app/src/receiver.c index 307eb5d5..5b97f88b 100644 --- a/app/src/receiver.c +++ b/app/src/receiver.c @@ -3,7 +3,6 @@ #include #include -#include "config.h" #include "device_msg.h" #include "util/lock.h" #include "util/log.h" diff --git a/app/src/receiver.h b/app/src/receiver.h index 8387903b..8f628238 100644 --- a/app/src/receiver.h +++ b/app/src/receiver.h @@ -5,7 +5,7 @@ #include #include -#include "config.h" +#include "common.h" #include "util/net.h" // receive events from the device diff --git a/app/src/recorder.c b/app/src/recorder.c index e31492c0..6558d804 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -3,8 +3,6 @@ #include #include -#include "config.h" -#include "compat.h" #include "util/lock.h" #include "util/log.h" diff --git a/app/src/recorder.h b/app/src/recorder.h index 43a0a395..fc05d06c 100644 --- a/app/src/recorder.h +++ b/app/src/recorder.h @@ -6,7 +6,7 @@ #include #include -#include "config.h" +#include "common.h" #include "coords.h" #include "scrcpy.h" #include "util/queue.h" diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index c1aedcca..ecc7a0e6 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -13,8 +13,6 @@ # include #endif -#include "config.h" -#include "compat.h" #include "controller.h" #include "decoder.h" #include "device.h" diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index c1d7b612..08bce7e3 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -5,7 +5,7 @@ #include #include -#include "config.h" +#include "common.h" enum sc_log_level { SC_LOG_LEVEL_DEBUG, diff --git a/app/src/screen.c b/app/src/screen.c index fe2bc867..5bdfac2a 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -4,9 +4,6 @@ #include #include -#include "config.h" -#include "common.h" -#include "compat.h" #include "icon.xpm" #include "scrcpy.h" #include "tiny_xpm.h" diff --git a/app/src/screen.h b/app/src/screen.h index 0e87d1c9..820c7382 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -5,7 +5,7 @@ #include #include -#include "config.h" +#include "common.h" #include "coords.h" #include "opengl.h" diff --git a/app/src/server.c b/app/src/server.c index cb2a24db..841af82f 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -9,7 +9,6 @@ #include #include -#include "config.h" #include "adb.h" #include "util/lock.h" #include "util/log.h" diff --git a/app/src/server.h b/app/src/server.h index b607d691..b48bcd61 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -6,7 +6,7 @@ #include #include -#include "config.h" +#include "common.h" #include "adb.h" #include "scrcpy.h" #include "util/log.h" diff --git a/app/src/stream.c b/app/src/stream.c index dd2dbd76..e4c9f387 100644 --- a/app/src/stream.c +++ b/app/src/stream.c @@ -8,8 +8,6 @@ #include #include -#include "config.h" -#include "compat.h" #include "decoder.h" #include "events.h" #include "recorder.h" diff --git a/app/src/stream.h b/app/src/stream.h index cd09d959..7b0a5d42 100644 --- a/app/src/stream.h +++ b/app/src/stream.h @@ -7,7 +7,7 @@ #include #include -#include "config.h" +#include "common.h" #include "util/net.h" struct video_buffer; diff --git a/app/src/sys/unix/process.c b/app/src/sys/unix/process.c index 00a8d81c..dc4f5649 100644 --- a/app/src/sys/unix/process.c +++ b/app/src/sys/unix/process.c @@ -11,8 +11,6 @@ #include "util/process.h" -#include "config.h" - #include #include #include diff --git a/app/src/sys/win/process.c b/app/src/sys/win/process.c index f2a86b22..f087625c 100644 --- a/app/src/sys/win/process.c +++ b/app/src/sys/win/process.c @@ -3,7 +3,6 @@ #include #include -#include "config.h" #include "util/log.h" #include "util/str_util.h" diff --git a/app/src/tiny_xpm.c b/app/src/tiny_xpm.c index feb3d1cb..df1f9e53 100644 --- a/app/src/tiny_xpm.c +++ b/app/src/tiny_xpm.c @@ -6,7 +6,6 @@ #include #include -#include "config.h" #include "util/log.h" struct index { diff --git a/app/src/tiny_xpm.h b/app/src/tiny_xpm.h index 6e6f8035..2dcbeb07 100644 --- a/app/src/tiny_xpm.h +++ b/app/src/tiny_xpm.h @@ -3,7 +3,7 @@ #include -#include "config.h" +#include "common.h" SDL_Surface * read_xpm(char *xpm[]); diff --git a/app/src/util/buffer_util.h b/app/src/util/buffer_util.h index 17234e42..265704bc 100644 --- a/app/src/util/buffer_util.h +++ b/app/src/util/buffer_util.h @@ -4,7 +4,7 @@ #include #include -#include "config.h" +#include "common.h" static inline void buffer_write16be(uint8_t *buf, uint16_t value) { diff --git a/app/src/util/cbuf.h b/app/src/util/cbuf.h index c18e4680..6abc984b 100644 --- a/app/src/util/cbuf.h +++ b/app/src/util/cbuf.h @@ -5,7 +5,7 @@ #include #include -#include "config.h" +#include "common.h" // To define a circular buffer type of 20 ints: // struct cbuf_int CBUF(int, 20); diff --git a/app/src/util/lock.h b/app/src/util/lock.h index cb7c318c..a0a044b1 100644 --- a/app/src/util/lock.h +++ b/app/src/util/lock.h @@ -4,7 +4,7 @@ #include #include -#include "config.h" +#include "common.h" #include "log.h" static inline void diff --git a/app/src/util/net.c b/app/src/util/net.c index efce6fa9..bbf57bbc 100644 --- a/app/src/util/net.c +++ b/app/src/util/net.c @@ -3,7 +3,6 @@ #include #include -#include "config.h" #include "log.h" #ifdef __WINDOWS__ diff --git a/app/src/util/net.h b/app/src/util/net.h index ffd5dd89..f86c048d 100644 --- a/app/src/util/net.h +++ b/app/src/util/net.h @@ -17,7 +17,7 @@ typedef int socket_t; #endif -#include "config.h" +#include "common.h" bool net_init(void); diff --git a/app/src/util/process.h b/app/src/util/process.h index ad2bc0d0..f91553d4 100644 --- a/app/src/util/process.h +++ b/app/src/util/process.h @@ -3,7 +3,7 @@ #include -#include "config.h" +#include "common.h" #ifdef _WIN32 diff --git a/app/src/util/queue.h b/app/src/util/queue.h index 12bc9e89..6092c712 100644 --- a/app/src/util/queue.h +++ b/app/src/util/queue.h @@ -6,7 +6,7 @@ #include #include -#include "config.h" +#include "common.h" // To define a queue type of "struct foo": // struct queue_foo QUEUE(struct foo); diff --git a/app/src/util/str_util.c b/app/src/util/str_util.c index ce0498a5..babce4a1 100644 --- a/app/src/util/str_util.c +++ b/app/src/util/str_util.c @@ -12,8 +12,6 @@ #include -#include "config.h" - size_t xstrncpy(char *dest, const char *src, size_t n) { size_t i; diff --git a/app/src/util/str_util.h b/app/src/util/str_util.h index c7f26cdb..dd523400 100644 --- a/app/src/util/str_util.h +++ b/app/src/util/str_util.h @@ -4,7 +4,7 @@ #include #include -#include "config.h" +#include "common.h" // like strncpy, except: // - it copies at most n-1 chars diff --git a/app/src/video_buffer.c b/app/src/video_buffer.c index 629680d9..d656c6f4 100644 --- a/app/src/video_buffer.c +++ b/app/src/video_buffer.c @@ -5,7 +5,6 @@ #include #include -#include "config.h" #include "util/lock.h" #include "util/log.h" diff --git a/app/src/video_buffer.h b/app/src/video_buffer.h index 303b3fc2..ddd639ad 100644 --- a/app/src/video_buffer.h +++ b/app/src/video_buffer.h @@ -4,7 +4,7 @@ #include #include -#include "config.h" +#include "common.h" #include "fps_counter.h" // forward declarations diff --git a/app/tests/test_cli.c b/app/tests/test_cli.c index 1024dba6..9699b024 100644 --- a/app/tests/test_cli.c +++ b/app/tests/test_cli.c @@ -1,8 +1,8 @@ #include #include -#include "cli.h" #include "common.h" +#include "cli.h" #include "scrcpy.h" static void test_flag_version(void) { From af516e33ee2561dfe57b30cfe557bd134a6b50cc Mon Sep 17 00:00:00 2001 From: Natan Junges Date: Thu, 14 Jan 2021 01:03:10 -0300 Subject: [PATCH 0453/2244] Update README.pt-br to v1.17 PR #2034 Reviewed-by: latreta Signed-off-by: Romain Vimont --- README.md | 2 +- README.pt-br.md | 518 ++++++++++++++++++++++++++++++++++++------------ 2 files changed, 392 insertions(+), 128 deletions(-) diff --git a/README.md b/README.md index 6a987a73..c38ac2cf 100644 --- a/README.md +++ b/README.md @@ -800,7 +800,7 @@ This README is available in other languages: - [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) +- [português brasileiro (Brazilian Portuguese, `pt-BR`) - v1.17](README.pt-br.md) - [简体中文 (Simplified Chinese, `zh-Hans`) - v1.16](README.zh-Hans.md) - [繁體中文 (Traditional Chinese, `zh-Hant`) - v1.15](README.zh-Hant.md) diff --git a/README.pt-br.md b/README.pt-br.md index 4f2122a6..ebf7c7ec 100644 --- a/README.pt-br.md +++ b/README.pt-br.md @@ -1,16 +1,16 @@ -_Only the original [README](README.md) is guaranteed to be up-to-date._ +_Apenas o [README](README.md) original é garantido estar atualizado._ -# scrcpy (v1.12.1) +# scrcpy (v1.17) -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. +Esta aplicação fornece exibiçã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) + - **leveza** (nativo, mostra apenas a tela do dispositivo) - **performance** (30~60fps) - **qualidade** (1920×1080 ou acima) - **baixa latência** ([35~70ms][lowlatency]) @@ -22,36 +22,41 @@ Foco em: ## Requisitos -O Dispositivo Android requer pelo menos a API 21 (Android 5.0). +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). +Tenha certeza de ter [ativado a depuração adb][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. +Em alguns dispositivos, você também precisa ativar [uma opção adicional][control] para +controlá-lo usando teclado e mouse. [control]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323 -## Obtendo o app +## Obter o app +Packaging status ### Linux -No Debian (_em testes_ e _sid_ por enquanto): +No Debian (_testing_ e _sid_ por enquanto) e Ubuntu (20.04): ``` apt install scrcpy ``` -O pacote [Snap] está disponível: [`scrcpy`][snap-link]. +Um 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 Fedora, um pacote [COPR] está disponível: [`scrcpy`][copr-link]. + +[COPR]: https://fedoraproject.org/wiki/Category:Copr +[copr-link]: https://copr.fedorainfracloud.org/coprs/zeno/scrcpy/ + Para Arch Linux, um pacote [AUR] está disponível: [`scrcpy`][aur-link]. [AUR]: https://wiki.archlinux.org/index.php/Arch_User_Repository @@ -62,21 +67,22 @@ 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 o app manualmente][BUILD] (não se preocupe, não é tão +difícil). -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 +Para Windows, por 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)_ + - [`scrcpy-win64-v1.17.zip`][direct-win64] + _(SHA-256: 8b9e57993c707367ed10ebfe0e1ef563c7a29d9af4a355cd8b6a52a317c73eea)_ -[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.17/scrcpy-win64-v1.17.zip -Também disponível em [Chocolatey]: +Também está disponível em [Chocolatey]: [Chocolatey]: https://chocolatey.org/ @@ -94,12 +100,12 @@ scoop install adb # se você ainda não o tem [Scoop]: https://scoop.sh -Você também pode [compilar a aplicação manualmente][BUILD]. +Você também pode [compilar o app manualmente][BUILD]. ### macOS -A aplicação está disponível em [Homebrew]. Apenas a instale: +A aplicação está disponível em [Homebrew]. Apenas instale-a: [Homebrew]: https://brew.sh/ @@ -107,18 +113,22 @@ A aplicação está disponível em [Homebrew]. Apenas a instale: brew install scrcpy ``` -Você precisa do `adb`, acessível através do seu `PATH`. Se você ainda não o tem: +Você precisa do `adb`, acessível pelo seu `PATH`. Se você ainda não o tem: ```bash +# Homebrew >= 2.6.0 +brew install --cask android-platform-tools + +# Homebrew < 2.6.0 brew cask install android-platform-tools ``` -Você também pode [compilar a aplicação manualmente][BUILD]. +Você também pode [compilar o app manualmente][BUILD]. ## Executar -Plugue um dispositivo Android e execute: +Conecte um dispositivo Android e execute: ```bash scrcpy @@ -134,52 +144,87 @@ scrcpy --help ### Configuração de captura -#### Redução de tamanho +#### Reduzir tamanho Algumas vezes, é útil espelhar um dispositivo Android em uma resolução menor para -aumentar performance. +aumentar a performance. -Para limitar ambos(largura e altura) para algum valor (ex: 1024): +Para limitar ambos (largura e altura) para algum valor (ex: 1024): ```bash scrcpy --max-size 1024 -scrcpy -m 1024 # versão reduzida +scrcpy -m 1024 # versão curta ``` A outra dimensão é calculada para que a proporção do dispositivo seja preservada. -Dessa forma, um dispositivo em 1920x1080 será espelhado em 1024x576. +Dessa forma, um dispositivo de 1920x1080 será espelhado em 1024x576. -#### Mudanças no bit-rate +#### Mudar bit-rate -O Padrão de bit-rate é 8 mbps. Para mudar o bitrate do vídeo (ex: para 2 Mbps): +O bit-rate padrão é 8 Mbps. Para mudar o bit-rate do vídeo (ex: para 2 Mbps): ```bash scrcpy --bit-rate 2M -scrcpy -b 2M # versão reduzida +scrcpy -b 2M # versão curta ``` -#### Limitar frame rates +#### Limitar frame rate -Em dispositivos com Android >= 10, a captura de frame rate pode ser limitada: +O frame rate de captura pode ser limitado: ```bash scrcpy --max-fps 15 ``` +Isso é oficialmente suportado desde o Android 10, mas pode funcionar em versões anteriores. + #### 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: +Isso é útil por exemplo, para 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. +Se `--max-size` também for especificado, o redimensionamento é aplicado após o corte. +#### Travar orientação do vídeo + + +Para travar a orientação do espelhamento: + +```bash +scrcpy --lock-video-orientation 0 # orientação natural +scrcpy --lock-video-orientation 1 # 90° sentido anti-horário +scrcpy --lock-video-orientation 2 # 180° +scrcpy --lock-video-orientation 3 # 90° sentido horário +``` + +Isso afeta a orientação de gravação. + +A [janela também pode ser rotacionada](#rotação) independentemente. + + +#### Encoder + +Alguns dispositivos têm mais de um encoder, e alguns deles podem causar problemas ou +travar. É possível selecionar um encoder diferente: + +```bash +scrcpy --encoder OMX.qcom.video.encoder.avc +``` + +Para listar os encoders disponíveis, você pode passar um nome de encoder inválido, o +erro dará os encoders disponíveis: + +```bash +scrcpy --encoder _ +``` + ### Gravando É possível gravar a tela enquanto ocorre o espelhamento: @@ -194,65 +239,84 @@ 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 +# interrompa a gravação com Ctrl+C ``` -"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. +"Frames pulados" são gravados, mesmo que não sejam exibidos em tempo real (por +motivos de performance). Frames têm seu _horário carimbado_ no dispositivo, então [variação de atraso nos +pacotes][packet delay variation] não impacta o arquivo gravado. -[Variação de atraso de pacote]: https://en.wikipedia.org/wiki/Packet_delay_variation +[packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation ### Conexão -#### Wireless/Sem fio +#### Sem fio -_Scrcpy_ usa `adb` para se comunicar com o dispositivo, e `adb` pode [conectar-se] à um dispositivo via TCP/IP: +_Scrcpy_ usa `adb` para se comunicar com o dispositivo, e `adb` pode [conectar-se][connect] a um +dispositivo via TCP/IP: + +1. Conecte o dispositivo no mesmo Wi-Fi do seu computador. +2. Pegue o endereço IP do seu dispositivo, em Configurações → Sobre o telefone → Status, ou + executando este comando: + + ```bash + adb shell ip route | awk '{print $9}' + ``` -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`)_. +4. Desconecte seu dispositivo. +5. Conecte-se ao seu dispositivo: `adb connect DEVICE_IP:5555` _(substitua `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 +scrcpy -b2M -m800 # versão curta ``` -[conectar-se]: https://developer.android.com/studio/command-line/adb.html#wireless +[connect]: https://developer.android.com/studio/command-line/adb.html#wireless -#### N-dispositivos +#### Múltiplos dispositivos -Se alguns dispositivos estão listados em `adb devices`, você precisa especificar o _serial_: +Se vários dispositivos são listados em `adb devices`, você deve especificar o _serial_: ```bash scrcpy --serial 0123456789abcdef -scrcpy -s 0123456789abcdef # versão reduzida +scrcpy -s 0123456789abcdef # versão curta ``` 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 +scrcpy -s 192.168.0.1:5555 # versão curta ``` -Você pode iniciar algumas instâncias do _scrcpy_ para alguns dispositivos. +Você pode iniciar várias instâncias do _scrcpy_ para vários dispositivos. -#### Conexão via SSH +#### Iniciar automaticamente quando dispositivo é conectado -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_): +Você pode usar [AutoAdb]: ```bash -adb kill-server # encerra o servidor local na 5037 +autoadb scrcpy -s '{}' +``` + +[AutoAdb]: https://github.com/rom1v/autoadb + +#### Túnel SSH + +Para conectar-se a um dispositivo remoto, é possível conectar um cliente `adb` local a +um servidor `adb` remoto (contanto que eles usem a mesma versão do protocolo +_adb_): + +```bash +adb kill-server # encerra o servidor adb local em 5037 ssh -CN -L5037:localhost:5037 -R27183:localhost:27183 your_remote_computer -# mantém isso aberto +# mantenha isso aberto ``` De outro terminal: @@ -261,17 +325,33 @@ De outro terminal: scrcpy ``` -Igual para conexões sem fio, pode ser útil reduzir a qualidade: +Para evitar ativar o encaminhamento de porta remota, você pode forçar uma conexão +de encaminhamento (note o `-L` em vez de `-R`): + +```bash +adb kill-server # encerra o servidor adb local em 5037 +ssh -CN -L5037:localhost:5037 -L27183:localhost:27183 your_remote_computer +# mantenha isso aberto +``` + +De outro terminal: + +```bash +scrcpy --force-adb-forward +``` + + +Igual a conexões sem fio, pode ser útil reduzir a qualidade: ``` scrcpy -b2M -m800 --max-fps 15 ``` -### Configurações de Janela +### Configuração de janela #### Título -Por padrão, o título da janela é o modelo do dispositivo. Isto pode ser mudado: +Por padrão, o título da janela é o modelo do dispositivo. Isso pode ser mudado: ```bash scrcpy --window-title 'Meu dispositivo' @@ -287,15 +367,15 @@ scrcpy --window-x 100 --window-y 100 --window-width 800 --window-height 600 #### Sem bordas -Para desativar decorações da janela: +Para desativar decorações de janela: ```bash scrcpy --window-borderless ``` -#### Sempre visível +#### Sempre no topo -Para manter a janela do scrcpy sempre visível: +Para manter a janela do scrcpy sempre no topo: ```bash scrcpy --always-on-top @@ -307,41 +387,117 @@ A aplicação pode ser iniciada diretamente em tela cheia: ```bash scrcpy --fullscreen -scrcpy -f # versão reduzida +scrcpy -f # versão curta ``` -Tela cheia pode ser alternada dinamicamente com `Ctrl`+`f`. +Tela cheia pode ser alternada dinamicamente com MOD+f. + +#### Rotação + +A janela pode ser rotacionada: + +```bash +scrcpy --rotation 1 +``` + +Valores possíveis são: + - `0`: sem rotação + - `1`: 90 graus sentido anti-horário + - `2`: 180 graus + - `3`: 90 graus sentido horário + +A rotação também pode ser mudada dinamicamente com MOD+ +_(esquerda)_ e MOD+ _(direita)_. + +Note que _scrcpy_ controla 3 rotações diferentes: + - MOD+r requisita ao dispositivo para mudar entre retrato + e paisagem (a aplicação em execução pode se recusar, se ela não suporta a + orientação requisitada). + - [`--lock-video-orientation`](#travar-orientação-do-vídeo) muda a orientação de + espelhamento (a orientação do vídeo enviado pelo dispositivo para o + computador). Isso afeta a gravação. + - `--rotation` (ou MOD+/MOD+) + rotaciona apenas o conteúdo da janela. Isso afeta apenas a exibição, não a + gravação. ### 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): +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 +#### Display -É possível desligar a tela do dispositivo durante o início do espelhamento com uma opção de linha de comando: +Se vários displays estão disponíveis, é possível selecionar o display para +espelhar: + +```bash +scrcpy --display 1 +``` + +A lista de IDs dos displays pode ser obtida por: + +``` +adb shell dumpsys display # busca "mDisplayId=" na saída +``` + +O display secundário pode apenas ser controlado se o dispositivo roda pelo menos Android +10 (caso contrário é espelhado como apenas leitura). + + +#### Permanecer ativo + +Para evitar que o dispositivo seja suspenso após um delay quando o dispositivo é conectado: + +```bash +scrcpy --stay-awake +scrcpy -w +``` + +O estado inicial é restaurado quando o scrcpy é fechado. + + +#### Desligar 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. +Ou apertando MOD+o a qualquer momento. -Para ligar novamente, pressione `POWER` (ou `Ctrl`+`p`). +Para ligar novamente, pressione MOD+Shift+o. -#### Frames expirados de renderização +No Android, o botão de `POWER` sempre liga a tela. Por conveniência, se +`POWER` é enviado via scrcpy (via clique-direito ou MOD+p), ele +forçará a desligar a tela após um delay pequeno (numa base de melhor esforço). +O botão `POWER` físico ainda causará a tela ser ligada. -Por padrão, para minimizar a latência, _scrcpy_ sempre renderiza o último frame decodificado disponível e descarta o anterior. +Também pode ser útil evitar que o dispositivo seja suspenso: -Para forçar a renderização de todos os frames ( com o custo de aumento de latência), use: +```bash +scrcpy --turn-screen-off --stay-awake +scrcpy -Sw +``` + + +#### Renderizar frames expirados + +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 um possível aumento de +latência), use: ```bash scrcpy --render-expired-frames @@ -349,11 +505,13 @@ scrcpy --render-expired-frames #### Mostrar toques -Para apresentações, pode ser útil mostrar toques físicos(dispositivo físico). +Para apresentações, pode ser útil mostrar toques físicos (no dispositivo +físico). -Android fornece esta funcionalidade nas _Opções do Desenvolvedor_. +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: +_Scrcpy_ fornece esta opção de ativar esta funcionalidade no início e restaurar o +valor inicial no encerramento: ```bash scrcpy --show-touches @@ -363,59 +521,137 @@ scrcpy -t Note que isto mostra apenas toques _físicos_ (com o dedo no dispositivo). +#### Desativar descanso de tela + +Por padrão, scrcpy não evita que o descanso de tela rode no computador. + +Para desativá-lo: + +```bash +scrcpy --disable-screensaver +``` + + ### Controle de entrada #### Rotacionar a tela do dispositivo -Pressione `Ctrl`+`r` para mudar entre os modos Retrato e Paisagem. +Pressione MOD+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. +Note que só será rotacionado se a aplicação em primeiro plano suportar a +orientação requisitada. -#### Copiar-Colar +#### Copiar-colar -É possível sincronizar áreas de transferência entre computador e o dispositivo, -para ambas direções: +Sempre que a área de transferência do Android muda, é automaticamente sincronizada com a +área de transferência do computador. - - `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). +Qualquer atalho com Ctrl é encaminhado para o dispositivo. Em particular: + - Ctrl+c tipicamente copia + - Ctrl+x tipicamente recorta + - Ctrl+v tipicamente cola (após a sincronização de área de transferência + computador-para-dispositivo) -#### Preferências de injeção de texto +Isso tipicamente funciona como esperado. -Existe dois tipos de [eventos][textevents] gerados ao digitar um texto: - - _eventos de teclas_, sinalizando que a tecla foi pressionada ou solta; +O comportamento de fato depende da aplicação ativa, no entanto. Por exemplo, +_Termux_ envia SIGINT com Ctrl+c, e _K-9 Mail_ +compõe uma nova mensagem. + +Para copiar, recortar e colar em tais casos (mas apenas suportado no Android >= 7): + - MOD+c injeta `COPY` + - MOD+x injeta `CUT` + - MOD+v injeta `PASTE` (após a sincronização de área de transferência + computador-para-dispositivo) + +Em adição, MOD+Shift+v permite injetar o +texto da área de transferência do computador como uma sequência de eventos de tecla. Isso é útil quando o +componente não aceita colar texto (por exemplo no _Termux_), mas pode +quebrar conteúdo não-ASCII. + +**ADVERTÊNCIA:** Colar a área de transferência do computador para o dispositivo (tanto via +Ctrl+v quanto MOD+v) copia o conteúdo +para a área de transferência do dispositivo. Como consequência, qualquer aplicação Android pode ler +o seu conteúdo. Você deve evitar colar conteúdo sensível (como senhas) dessa +forma. + +Alguns dispositivos não se comportam como esperado quando a área de transferência é definida +programaticamente. Uma opção `--legacy-paste` é fornecida para mudar o comportamento +de Ctrl+v e MOD+v para que eles +também injetem o texto da área de transferência do computador como uma sequência de eventos de tecla (da mesma +forma que MOD+Shift+v). + +#### Pinçar para dar zoom + +Para simular "pinçar para dar zoom": Ctrl+_clicar-e-mover_. + +Mais precisamente, segure Ctrl enquanto pressiona o botão de clique-esquerdo. Até que +o botão de clique-esquerdo seja liberado, todos os movimentos do mouse ampliar e rotacionam o +conteúdo (se suportado pelo app) relativo ao centro da tela. + +Concretamente, scrcpy gera eventos adicionais de toque de um "dedo virtual" em +uma posição invertida em relação ao centro da tela. + + +#### Preferência de injeção de texto + +Existem dois tipos de [eventos][textevents] gerados ao digitar um texto: + - _eventos de tecla_, 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) +Por padrão, letras são injetadas usando eventos de tecla, assim o teclado comporta-se +como esperado em jogos (normalmente para teclas WASD). -Mas isto pode [causar problemas][prefertext]. Se você encontrar tal problema, -pode evitá-lo usando: +Mas isso pode [causar problemas][prefertext]. Se você encontrar tal problema, você +pode evitá-lo com: ```bash scrcpy --prefer-text ``` -(mas isto vai quebrar o comportamento do teclado em jogos) +(mas isso 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 +#### Repetir tecla + +Por padrão, segurar uma tecla gera eventos de tecla repetidos. Isso pode causar +problemas de performance em alguns jogos, onde esses eventos são inúteis de qualquer forma. + +Para evitar o encaminhamento eventos de tecla repetidos: + +```bash +scrcpy --no-key-repeat +``` + + +#### Clique-direito e clique-do-meio + +Por padrão, clique-direito dispara BACK (ou POWER) e clique-do-meio dispara +HOME. Para desabilitar esses atalhos e encaminhar os cliques para o dispositivo: + +```bash +scrcpy --forward-all-clicks +``` + + +### Soltar arquivo #### Instalar APK -Para instalar um APK, arraste e solte o arquivo APK(com extensão `.apk`) na janela _scrcpy_. +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 +#### Enviar arquivo para 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_. +Para enviar um arquivo para `/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. @@ -428,45 +664,73 @@ scrcpy --push-target /sdcard/foo/bar/ ### Encaminhamento de áudio -Áudio não é encaminhando pelo _scrcpy_. Use [USBaudio] (Apenas linux). +Áudio não é encaminhado pelo _scrcpy_. Use [sndcpy]. Também veja [issue #14]. -[USBaudio]: https://github.com/rom1v/usbaudio +[sndcpy]: https://github.com/rom1v/sndcpy [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` +Na lista a seguir, MOD é o modificador de atalho. Por padrão, é +Alt (esquerdo) ou Super (esquerdo). + +Ele pode ser mudado usando `--shortcut-mod`. Possíveis teclas são `lctrl`, `rctrl`, +`lalt`, `ralt`, `lsuper` e `rsuper`. Por exemplo: + +```bash +# usar RCtrl para atalhos +scrcpy --shortcut-mod=rctrl + +# usar tanto LCtrl+LAlt quanto LSuper para atalhos +scrcpy --shortcut-mod=lctrl+lalt,lsuper +``` + +_[Super] é tipicamente a tecla Windows ou Cmd._ + +[Super]: https://en.wikipedia.org/wiki/Super_key_(keyboard_button) + + | Ação | Atalho + | ------------------------------------------- |:----------------------------- + | Mudar modo de tela cheia | MOD+f + | Rotacionar display para esquerda | MOD+ _(esquerda)_ + | Rotacionar display para direita | MOD+ _(direita)_ + | Redimensionar janela para 1:1 (pixel-perfect) | MOD+g + | Redimensionar janela para remover bordas pretas | MOD+w \| _Clique-duplo¹_ + | Clicar em `HOME` | MOD+h \| _Clique-do-meio_ + | Clicar em `BACK` | MOD+b \| _Clique-direito²_ + | Clicar em `APP_SWITCH` | MOD+s + | Clicar em `MENU` (desbloquear tela | MOD+m + | Clicar em `VOLUME_UP` | MOD+ _(cima)_ + | Clicar em `VOLUME_DOWN` | MOD+ _(baixo)_ + | Clicar em `POWER` | MOD+p + | Ligar | _Clique-direito²_ + | Desligar tela do dispositivo (continuar espelhando) | MOD+o + | Ligar tela do dispositivo | MOD+Shift+o + | Rotacionar tela do dispositivo | MOD+r + | Expandir painel de notificação | MOD+n + | Colapsar painel de notificação | MOD+Shift+n + | Copiar para área de transferência³ | MOD+c + | Recortar para área de transferência³ | MOD+x + | Sincronizar áreas de transferência e colar³ | MOD+v + | Injetar texto da área de transferência do computador | MOD+Shift+v + | Ativar/desativar contador de FPS (em stdout) | MOD+i + | Pinçar para dar zoom | Ctrl+_clicar-e-mover_ _¹Clique-duplo em bordas pretas para removê-las._ -_²Botão direito liga a tela se ela estiver desligada, clique BACK para o contrário._ +_²Clique-direito liga a tela se ela estiver desligada, pressiona BACK caso contrário._ +_³Apenas em Android >= 7._ + +Todos os atalhos Ctrl+_tecla_ são encaminhados para o dispositivo, para que eles sejam +tratados pela aplicação ativa. ## Caminhos personalizados -Para usar um binário específico _adb_, configure seu caminho na variável de ambiente `ADB`: +Para usar um binário _adb_ específico, configure seu caminho na variável de ambiente +`ADB`: ADB=/caminho/para/adb scrcpy @@ -478,7 +742,7 @@ Para sobrepor o caminho do arquivo `scrcpy-server`, configure seu caminho em ## Por quê _scrcpy_? -Um colega me desafiou a encontrar um nome impronunciável como [gnirehtet]. +Um colega me desafiou a encontrar um nome tão impronunciável quanto [gnirehtet]. [`strcpy`] copia uma **str**ing; `scrcpy` copia uma **scr**een. @@ -495,12 +759,12 @@ Veja [BUILD]. ## Problemas comuns -Veja [FAQ](FAQ.md). +Veja o [FAQ](FAQ.md). ## Desenvolvedores -Leia a [developers page]. +Leia a [página dos desenvolvedores][developers page]. [developers page]: DEVELOP.md From ce43fad645d4eb30f322dbeb50d5197601564931 Mon Sep 17 00:00:00 2001 From: Simon Chan Date: Tue, 12 Jan 2021 13:54:42 +0800 Subject: [PATCH 0454/2244] Update README.zh-Hans to v1.17 PR #2029 Reviewed-by: Win7GM Signed-off-by: Romain Vimont --- README.md | 2 +- README.zh-Hans.md | 443 +++++++++++++++++++++++----------------------- 2 files changed, 227 insertions(+), 218 deletions(-) diff --git a/README.md b/README.md index c38ac2cf..759e7920 100644 --- a/README.md +++ b/README.md @@ -801,7 +801,7 @@ This README is available in other languages: - [Indonesian (Indonesia, `id`) - v1.16](README.id.md) - [한국어 (Korean, `ko`) - v1.11](README.ko.md) - [português brasileiro (Brazilian Portuguese, `pt-BR`) - v1.17](README.pt-br.md) -- [简体中文 (Simplified Chinese, `zh-Hans`) - v1.16](README.zh-Hans.md) +- [简体中文 (Simplified Chinese, `zh-Hans`) - v1.17](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 index 3774c5d9..e758c18d 100644 --- a/README.zh-Hans.md +++ b/README.zh-Hans.md @@ -2,115 +2,111 @@ _Only the original [README](README.md) is guaranteed to be up-to-date._ 只有原版的[README](README.md)会保持最新。 -本文根据[479d10d]进行翻译。 +本文根据[ed130e05]进行翻译。 -[479d10d]: https://github.com/Genymobile/scrcpy/commit/479d10dc22b70272187e0963c6ad24d754a669a2#diff-04c6e90faac2675aa89e2176d2eec7d8 +[ed130e05]: https://github.com/Genymobile/scrcpy/blob/ed130e05d55615d6014d93f15cfcb92ad62b01d8/README.md +# scrcpy (v1.17) - -# 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/ +本应用程序可以显示并控制通过 USB (或 [TCP/IP][article-tcpip]) 连接的安卓设备,且不需要任何 _root_ 权限。本程序支持 _GNU/Linux_, _Windows_ 和 _macOS_。 ![screenshot](assets/screenshot-debian-600.jpg) 它专注于: - - **轻量** (原生,仅显示设备屏幕) - - **性能** (30~60fps) - - **质量** (分辨率可达1920x1080或更高) - - **低延迟** (35-70ms) - - **快速启动** (数秒内即能开始显示) - - **无侵入性** (不需要在安卓设备上安装任何程序) + - **轻量** (原生,仅显示设备屏幕) + - **性能** (30~60fps) + - **质量** (分辨率可达 1920×1080 或更高) + - **低延迟** ([35~70ms][lowlatency]) + - **快速启动** (最快 1 秒内即可显示第一帧) + - **无侵入性** (不会在设备上遗留任何程序) + +[lowlatency]: https://github.com/Genymobile/scrcpy/pull/646 -## 使用要求 +## 系统要求 -安卓设备系统版本需要在Android 5.0(API 21)或以上。 +安卓设备最低需要支持 API 21 (Android 5.0)。 -确保您在设备上开启了[adb调试]。 +确保设备已[开启 adb 调试][enable-adb]。 -[adb调试]: https://developer.android.com/studio/command-line/adb.html#Enabling +[enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling -在某些设备上,你还需要开启[额外的选项]以用鼠标和键盘进行控制。 +在某些设备上,还需要开启[额外的选项][control]以使用鼠标和键盘进行控制。 -[额外的选项]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323 +[control]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323 -## 获取scrcpy +## 获取本程序 Packaging status ### Linux -在Debian(目前仅测试版和不稳定版,即 _testing_ 和 _sid_ 版本)和Ubuntu (20.04)上: +在 Debian (目前仅支持 _testing_ 和 _sid_ 分支) 和Ubuntu (20.04) 上: ``` apt install scrcpy ``` -[Snap]包也是可用的: [`scrcpy`][snap-link]. +我们也提供 [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]. +对 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]. +对 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]. +对 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 -您也可以[自行编译][编译](不必担心,这并不困难)。 +您也可以[自行构建][BUILD] (不必担心,这并不困难)。 ### Windows -在Windows上,简便起见,我们准备了包含所有依赖项(包括adb)的程序包。 +在 Windows 上,简便起见,我们提供包含了所有依赖 (包括 `adb`) 的预编译包。 - - [`scrcpy-win64-v1.16.zip`][direct-win64] - _(SHA-256: 3f30dc5db1a2f95c2b40a0f5de91ec1642d9f53799250a8c529bc882bc0918f0)_ + - [`scrcpy-win64-v1.17.zip`][direct-win64] + _(SHA-256: 8b9e57993c707367ed10ebfe0e1ef563c7a29d9af4a355cd8b6a52a317c73eea)_ -[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.16/scrcpy-win64-v1.16.zip +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.17/scrcpy-win64-v1.17.zip -您也可以在[Chocolatey]下载: +也可以使用 [Chocolatey]: [Chocolatey]: https://chocolatey.org/ ```bash choco install scrcpy -choco install adb # 如果你没有adb +choco install adb # 如果还没有 adb ``` -也可以使用 [Scoop]: +或者 [Scoop]: ```bash scoop install scrcpy -scoop install adb # 如果你没有adb +scoop install adb # 如果还没有 adb ``` [Scoop]: https://scoop.sh -您也可以[自行编译][编译]。 +您也可以[自行构建][BUILD]。 ### macOS -您可以使用[Homebrew]下载scrcpy。直接安装就可以了: +本程序已发布到 [Homebrew]。直接安装即可: [Homebrew]: https://brew.sh/ @@ -118,24 +114,28 @@ scoop install adb # 如果你没有adb brew install scrcpy ``` -您需要 `adb`以使用scrcpy,并且它需要可以通过 `PATH`被访问。如果您没有: +你还需要在 `PATH` 内有 `adb`。如果还没有: ```bash +# Homebrew >= 2.6.0 +brew install --cask android-platform-tools + +# Homebrew < 2.6.0 brew cask install android-platform-tools ``` -您也可以[自行编译][编译]。 +您也可以[自行构建][BUILD]。 -## 运行scrcpy +## 运行 -用USB链接电脑和安卓设备,并执行: +连接安卓设备,然后执行: ```bash scrcpy ``` -支持带命令行参数执行,查看参数列表: +本程序支持命令行参数,查看参数列表: ```bash scrcpy --help @@ -143,111 +143,129 @@ scrcpy --help ## 功能介绍 -### 画面设置 +### 捕获设置 -#### 缩小分辨率 +#### 降低分辨率 -有时候,将设备屏幕镜像分辨率降低可以有效地提升性能。 +有时候,可以通过降低镜像的分辨率来提高性能。 -我们可以将高度和宽度都限制在一定大小内(如 1024): +要同时限制宽度和高度到某个值 (例如 1024): ```bash scrcpy --max-size 1024 -scrcpy -m 1024 # short version +scrcpy -m 1024 # 简写 ``` -较短的一边会被按比例缩小以保持设备的显示比例。 -这样,1920x1080 的设备会以 1024x576 的分辨率显示。 +另一边会被按比例缩小以保持设备的显示比例。这样,1920×1080 分辨率的设备会以 1024×576 的分辨率进行镜像。 -#### 修改画面比特率 +#### 修改码率 -默认的比特率是8Mbps。如果要改变画面的比特率 (比如说改成2Mbps): +默认码率是 8Mbps。要改变视频的码率 (例如改为 2Mbps): ```bash scrcpy --bit-rate 2M -scrcpy -b 2M # short version +scrcpy -b 2M # 简写 ``` -#### 限制画面帧率 +#### 限制帧率 -画面的帧率可以通过下面的命令被限制: +要限制捕获的帧率: ```bash scrcpy --max-fps 15 ``` -这个功能仅在Android 10和以后的版本被Android官方支持,但也有可能在更早的版本可用。 +本功能从 Android 10 开始才被官方支持,但在一些旧版本中也能生效。 #### 画面裁剪 -设备画面可在裁切后进行镜像,以显示部分屏幕。 +可以对设备屏幕进行裁剪,只镜像屏幕的一部分。 -这项功能可以用于,例如,只显示Oculus Go的一只眼睛。 +例如可以只镜像 Oculus Go 的一只眼睛。 ```bash -scrcpy --crop 1224:1440:0:0 # 1224x1440 at offset (0,0) +scrcpy --crop 1224:1440:0:0 # 以 (0,0) 为原点的 1224x1440 像素 ``` -如果`--max-size`在同时被指定,分辨率的改变将在画面裁切后进行。 +如果同时指定了 `--max-size`,会先进行裁剪,再进行缩放。 -#### 锁定屏幕朝向 +#### 锁定屏幕方向 -可以使用如下命令锁定屏幕朝向: +要锁定镜像画面的方向: ```bash -scrcpy --lock-video-orientation 0 # 自然朝向 -scrcpy --lock-video-orientation 1 # 90° 逆时针旋转 +scrcpy --lock-video-orientation 0 # 自然方向 +scrcpy --lock-video-orientation 1 # 逆时针旋转 90° scrcpy --lock-video-orientation 2 # 180° -scrcpy --lock-video-orientation 3 # 90° 顺时针旋转 +scrcpy --lock-video-orientation 3 # 顺时针旋转 90° ``` -该设定影响录制。 +只影响录制的方向。 +[窗口可以独立旋转](#旋转)。 + + +#### 编码器 + +一些设备内置了多种编码器,但是有的编码器会导致问题或崩溃。可以手动选择其它编码器: + +```bash +scrcpy --encoder OMX.qcom.video.encoder.avc +``` + +要列出可用的编码器,可以指定一个不存在的编码器名称,错误信息中会包含所有的编码器: + +```bash +scrcpy --encoder _ +``` ### 屏幕录制 -可以在屏幕镜像的同时录制视频: +可以在镜像的同时录制视频: ```bash scrcpy --record file.mp4 scrcpy -r file.mkv ``` -在不开启屏幕镜像的同时录制: +仅录制,不显示镜像: ```bash scrcpy --no-display --record file.mp4 scrcpy -Nr file.mkv -# 按Ctrl+C以停止录制 +# 按 Ctrl+C 停止录制 ``` -在显示中“被跳过的帧”会被录制,虽然它们由于性能原因没有实时显示。 -在传输中每一帧都有 _时间戳_ ,所以 [包时延变化] 并不影响录制的文件。 +录制时会包含“被跳过的帧”,即使它们由于性能原因没有实时显示。设备会为每一帧打上 _时间戳_ ,所以 [包时延抖动][packet delay variation] 不会影响录制的文件。 -[包时延变化]: https://en.wikipedia.org/wiki/Packet_delay_variation +[packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation -### 连接方式 +### 连接 #### 无线 -_Scrcpy_ 使用`adb`来与安卓设备连接。同时,`adb`能够通过TCP/IP[连接]到安卓设备: +_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`。 +1. 将设备和电脑连接至同一 Wi-Fi。 +2. 打开 设置 → 关于手机 → 状态信息,获取设备的 IP 地址,也可以执行以下的命令: + ```bash + adb shell ip route | awk '{print $9}' + ``` -降低比特率和分辨率可能有助于性能: +3. 启用设备的网络 adb 功能 `adb tcpip 5555`。 +4. 断开设备的 USB 连接。 +5. 连接到您的设备:`adb connect DEVICE_IP:5555` _(将 `DEVICE_IP` 替换为设备 IP)_. +6. 正常运行 `scrcpy`。 + +可能需要降低码率和分辨率: ```bash scrcpy --bit-rate 2M --max-size 800 -scrcpy -b2M -m800 # short version +scrcpy -b2M -m800 # 简写 ``` [连接]: https://developer.android.com/studio/command-line/adb.html#wireless @@ -255,18 +273,18 @@ scrcpy -b2M -m800 # short version #### 多设备 -如果多个设备在执行`adb devices`后被列出,您必须指定设备的 _序列号_ : +如果 `adb devices` 列出了多个设备,您必须指定设备的 _序列号_ : ```bash scrcpy --serial 0123456789abcdef -scrcpy -s 0123456789abcdef # short version +scrcpy -s 0123456789abcdef # 简写 ``` -如果设备是通过TCP/IP方式连接到电脑的: +如果设备通过 TCP/IP 连接: ```bash scrcpy --serial 192.168.0.1:5555 -scrcpy -s 192.168.0.1:5555 # short version +scrcpy -s 192.168.0.1:5555 # 简写 ``` 您可以同时启动多个 _scrcpy_ 实例以同时显示多个设备的画面。 @@ -281,38 +299,38 @@ autoadb scrcpy -s '{}' [AutoAdb]: https://github.com/rom1v/autoadb -#### SSH 连接 +#### SSH 隧道 -本地的 adb 可以远程连接到另一个 adb 服务器(假设两者的adb版本相同),来远程连接到设备: +要远程连接到设备,可以将本地的 adb 客户端连接到远程的 adb 服务端 (需要两端的 _adb_ 协议版本相同): ```bash -adb kill-server # 关闭本地5037端口上的adb服务器 +adb kill-server # 关闭本地 5037 端口上的 adb 服务端 ssh -CN -L5037:localhost:5037 -R27183:localhost:27183 your_remote_computer # 保持该窗口开启 ``` -从另一个终端: +在另一个终端: ```bash scrcpy ``` -为了避免启动远程端口转发,你可以强制启动一个转发连接(注意`-L`和`-R`的区别: +若要不使用远程端口转发,可以强制使用正向连接 (注意 `-L` 和 `-R` 的区别): ```bash -adb kill-server # kill the local adb server on 5037 +adb kill-server # 关闭本地 5037 端口上的 adb 服务端 ssh -CN -L5037:localhost:5037 -L27183:localhost:27183 your_remote_computer # 保持该窗口开启 ``` -从另一个终端: +在另一个终端: ```bash scrcpy --force-adb-forward ``` -和无线网络连接类似,下列设置可能对改善性能有帮助: +类似无线网络连接,可能需要降低画面质量: ``` scrcpy -b2M -m800 --max-fps 15 @@ -322,7 +340,7 @@ scrcpy -b2M -m800 --max-fps 15 #### 标题 -窗口的标题默认为设备型号。您可以通过如下命令修改它: +窗口的标题默认为设备型号。可以通过如下命令修改: ```bash scrcpy --window-title 'My device' @@ -358,14 +376,14 @@ scrcpy --always-on-top ```bash scrcpy --fullscreen -scrcpy -f # short version +scrcpy -f # 简写 ``` -全屏状态可以通过MOD+f实时改变。 +全屏状态可以通过 MOD+f 随时切换。 #### 旋转 -通过如下命令,窗口可以旋转: +可以通过以下命令旋转窗口: ```bash scrcpy --rotation 1 @@ -373,27 +391,23 @@ scrcpy --rotation 1 可选的值有: - `0`: 无旋转 - - `1`: 逆时针旋转90° - - `2`: 旋转180° - - `3`: 顺时针旋转90° + - `1`: 逆时针旋转 90° + - `2`: 旋转 180° + - `3`: 顺时针旋转 90° -这同样可以使用MOD+ -_(左)_ 和 MOD+ _(右)_ 的快捷键实时更改。 +也可以使用 MOD+ _(左箭头)_ 和 MOD+ _(右箭头)_ 随时更改。 -需要注意的是, _scrcpy_ 控制三个不同的朝向: - - MOD+r 请求设备在竖屏和横屏之间切换(如果前台应用程序不支持所请求的朝向,可能会拒绝该请求)。 - - - `--lock-video-orientation` 改变镜像的朝向(设备镜像到电脑的画面朝向)。这会影响录制。 - - - `--rotation` (或MOD+/MOD+) - 只旋转窗口的画面。这只影响显示,不影响录制。 +需要注意的是, _scrcpy_ 有三个不同的方向: + - MOD+r 请求设备在竖屏和横屏之间切换 (如果前台应用程序不支持请求的朝向,可能会拒绝该请求)。 + - [`--lock-video-orientation`](#锁定屏幕方向) 改变镜像的朝向 (设备传输到电脑的画面的朝向)。这会影响录制。 + - `--rotation` (或 MOD+/MOD+) 只旋转窗口的内容。这只影响显示,不影响录制。 ### 其他镜像设置 #### 只读 -关闭电脑对设备的控制(如键盘输入、鼠标移动和文件传输): +禁用电脑对设备的控制 (如键盘输入、鼠标事件和文件拖放): ```bash scrcpy --no-control @@ -402,53 +416,49 @@ scrcpy -n #### 显示屏 -如果有多个显示屏可用,您可以选择特定显示屏进行镜像: +如果设备有多个显示屏,可以选择要镜像的显示屏: ```bash scrcpy --display 1 ``` -您可以通过如下命令找到显示屏的id: +可以通过如下命令列出所有显示屏的 id: ``` -adb shell dumpsys display # 在回显中搜索“mDisplayId=” +adb shell dumpsys display # 在输出中搜索 “mDisplayId=” ``` -第二显示屏可能只能在设备运行Android 10或以上的情况下被控制(它可能会在电脑上显示,但无法通过电脑操作)。 +控制第二显示屏需要设备运行 Android 10 或更高版本 (否则将在只读状态下镜像)。 #### 保持常亮 -防止设备在已连接的状态下休眠: +阻止设备在连接时休眠: ```bash scrcpy --stay-awake scrcpy -w ``` -程序关闭后,设备设置会恢复原样。 +程序关闭时会恢复设备原来的设置。 #### 关闭设备屏幕 -在启动屏幕镜像时,可以通过如下命令关闭设备的屏幕: +可以通过以下的命令行参数在关闭设备屏幕的状态下进行镜像: ```bash scrcpy --turn-screen-off scrcpy -S ``` -或者在需要的时候按MOD+o。 +或者在任何时候按 MOD+o。 -要重新打开屏幕的话,需要按MOD+Shift+o. +要重新打开屏幕,按下 MOD+Shift+o. -在Android上,`电源`按钮始终能把屏幕打开。 +在Android上,`电源` 按钮始终能把屏幕打开。为了方便,对于在 _scrcpy_ 中发出的 `电源` 事件 (通过鼠标右键或 MOD+p),会 (尽最大的努力) 在短暂的延迟后将屏幕关闭。设备上的 `电源` 按钮仍然能打开设备屏幕。 -为了方便,如果按下`电源`按钮的事件是通过 _scrcpy_ 发出的(通过点按鼠标右键或MOD+p),它会在短暂的延迟后将屏幕关闭。 - -物理的`电源`按钮仍然能打开设备屏幕。 - -同时,这项功能还能被用于防止设备休眠: +还可以同时阻止设备休眠: ```bash scrcpy --turn-screen-off --stay-awake @@ -456,11 +466,11 @@ scrcpy -Sw ``` -#### 渲染超时帧 +#### 渲染过期帧 -为了降低延迟, _scrcpy_ 默认渲染解码成功的最近一帧,并跳过前面任意帧。 +默认状态下,为了降低延迟, _scrcpy_ 永远渲染解码成功的最近一帧,并跳过前面任意帧。 -强制渲染所有帧(可能导致延迟变高): +强制渲染所有帧 (可能导致延迟变高): ```bash scrcpy --render-expired-frames @@ -468,9 +478,9 @@ scrcpy --render-expired-frames #### 显示触摸 -在展示时,有些时候可能会用到显示触摸点这项功能(在设备上显示)。 +在演示时,可能会需要显示物理触摸点 (在物理设备上的触摸点)。 -Android在 _开发者设置_ 中提供了这项功能。 +Android 在 _开发者选项_ 中提供了这项功能。 _Scrcpy_ 提供一个选项可以在启动时开启这项功能并在退出时恢复初始设置: @@ -479,12 +489,12 @@ scrcpy --show-touches scrcpy -t ``` -请注意这项功能只能显示 _物理_ 触摸(要用手在屏幕上触摸)。 +请注意这项功能只能显示 _物理_ 触摸 (用手指在屏幕上的触摸)。 #### 关闭屏保 -_Scrcpy_ 不会默认关闭屏幕保护。 +_Scrcpy_ 默认不会阻止电脑上开启的屏幕保护。 关闭屏幕保护: @@ -497,64 +507,58 @@ scrcpy --disable-screensaver #### 旋转设备屏幕 -使用MOD+r以在竖屏和横屏模式之间切换。 +使用 MOD+r 在竖屏和横屏模式之间切换。 需要注意的是,只有在前台应用程序支持所要求的模式时,才会进行切换。 -#### 复制黏贴 +#### 复制粘贴 -每次Android的剪贴板变化的时候,它都会被自动同步到电脑的剪贴板上。 +每次安卓的剪贴板变化时,其内容都会被自动同步到电脑的剪贴板上。 所有的 Ctrl 快捷键都会被转发至设备。其中: - - Ctrl+c 复制 - - Ctrl+x 剪切 - - Ctrl+v 黏贴 (在电脑到设备的剪贴板同步完成之后) + - Ctrl+c 通常执行复制 + - Ctrl+x 通常执行剪切 + - Ctrl+v 通常执行粘贴 (在电脑到设备的剪贴板同步完成之后) -这通常如您所期望的那样运作。 +大多数时候这些按键都会执行以上的功能。 -但实际的行为取决于设备上的前台程序。 -例如 _Termux_ 在Ctrl+c被按下时发送 SIGINT, -又如 _K-9 Mail_ 会新建一封新邮件。 +但实际的行为取决于设备上的前台程序。例如,_Termux_ 会在按下 Ctrl+c 时发送 SIGINT,又如 _K-9 Mail_ 会新建一封邮件。 -在这种情况下剪切复制黏贴(仅在Android >= 7时可用): - - MOD+c 注入 `COPY`(复制) - - MOD+x 注入 `CUT`(剪切) - - MOD+v 注入 `PASTE`(黏贴)(在电脑到设备的剪贴板同步完成之后) +要在这种情况下进行剪切,复制和粘贴 (仅支持 Android >= 7): + - MOD+c 注入 `COPY` (复制) + - MOD+x 注入 `CUT` (剪切) + - MOD+v 注入 `PASTE` (粘贴) (在电脑到设备的剪贴板同步完成之后) -另外,MOD+Shift+v可以将电脑的剪贴板内容转换为一串按键事件输入到设备。 -在应用程序不接受黏贴时(比如 _Termux_ ),这项功能可以排上一定的用场。 -需要注意的是,这项功能可能会导致非ASCII编码的内容出现错误。 +另外,MOD+Shift+v 会将电脑的剪贴板内容转换为一串按键事件输入到设备。在应用程序不接受粘贴时 (比如 _Termux_),这项功能可以派上一定的用场。不过这项功能可能会导致非 ASCII 编码的内容出现错误。 -**警告:** 将电脑剪贴板的内容黏贴至设备(无论是通过Ctrl+v还是MOD+v) -都需要将内容保存至设备的剪贴板。如此,任何一个应用程序都可以读取它。 -您应当避免将敏感内容通过这种方式传输(如密码)。 +**警告:** 将电脑剪贴板的内容粘贴至设备 (无论是通过 Ctrl+v 还是 MOD+v) 都会将内容复制到设备的剪贴板。如此,任何安卓应用程序都能读取到。您应避免将敏感内容 (如密码) 通过这种方式粘贴。 +一些设备不支持通过程序设置剪贴板。通过 `--legacy-paste` 选项可以修改 Ctrl+vMOD+v 的工作方式,使它们通过按键事件 (同 MOD+Shift+v) 来注入电脑剪贴板内容。 -#### 捏拉缩放 +#### 双指缩放 -模拟 “捏拉缩放”:Ctrl+_按住并移动鼠标_。 +模拟“双指缩放”:Ctrl+_按住并移动鼠标_。 -更准确的说,您需要在按住Ctrl的同时按住并移动鼠标。 -在鼠标左键松开之后,光标的任何操作都会相对于屏幕的中央进行。 +更准确的说,在按住鼠标左键时按住 Ctrl。直到松开鼠标左键,所有鼠标移动将以屏幕中心为原点,缩放或旋转内容 (如果应用支持)。 -具体来说, _scrcpy_ 使用“虚拟手指”以在相对于屏幕中央相反的位置产生触摸事件。 +实际上,_scrcpy_ 会在以屏幕中心对称的位置上生成由“虚拟手指”发出的额外触摸事件。 #### 文字注入偏好 打字的时候,系统会产生两种[事件][textevents]: - - _按键事件_ ,代表一个按键被按下/松开。 - - _文本事件_ ,代表一个文本被输入。 + - _按键事件_ ,代表一个按键被按下或松开。 + - _文本事件_ ,代表一个字符被输入。 -程序默认使用按键事件来输入字母。只有这样,键盘才会在游戏中正常运作(尤其WASD键)。 +程序默认使用按键事件来输入字母。只有这样,键盘才会在游戏中正常运作 (例如 WASD 键)。 -但这也有可能[造成问题][prefertext]。如果您遇到了这样的问题,您可以通过下列操作避免它: +但这也有可能[造成一些问题][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 @@ -562,8 +566,7 @@ scrcpy --prefer-text #### 按键重复 -当你一直按着一个按键不放时,程序默认产生多个按键事件。 -在某些游戏中这可能会导致性能问题。 +默认状态下,按住一个按键不放会生成多个重复按键事件。在某些游戏中这可能会导致性能问题。 避免转发重复按键事件: @@ -572,18 +575,27 @@ scrcpy --no-key-repeat ``` -### 文件传输 +#### 右键和中键 + +默认状态下,右键会触发返回键 (或电源键),中键会触发 HOME 键。要禁用这些快捷键并把所有点击转发到设备: + +```bash +scrcpy --forward-all-clicks +``` + + +### 文件拖放 #### 安装APK -如果您要要安装APK,请拖放APK文件(文件名以`.apk`结尾)到 _scrcpy_ 窗口。 +将 APK 文件 (文件名以 `.apk` 结尾) 拖放到 _scrcpy_ 窗口来安装。 该操作在屏幕上不会出现任何变化,而会在控制台输出一条日志。 #### 将文件推送至设备 -如果您要推送文件到设备的 `/sdcard/`,请拖放文件至(不能是APK文件)_scrcpy_ 窗口。 +要推送文件到设备的 `/sdcard/`,将 (非 APK) 文件拖放至 _scrcpy_ 窗口。 该操作没有可见的响应,只会在控制台输出日志。 @@ -596,7 +608,7 @@ scrcpy --push-target /sdcard/foo/bar/ ### 音频转发 -_scrcpy_ 不支持音频。请使用 [sndcpy]. +_Scrcpy_ 不支持音频。请使用 [sndcpy]. 另外请阅读 [issue #14]。 @@ -604,93 +616,90 @@ _scrcpy_ 不支持音频。请使用 [sndcpy]. [issue #14]: https://github.com/Genymobile/scrcpy/issues/14 -## 热键 +## 快捷键 -在下列表格中, MOD 是热键的修饰键。 -默认是(左)Alt或者(左)Super。 +在以下列表中, MOD 是快捷键的修饰键。 +默认是 (左) Alt 或 (左) Super。 -您可以使用 `--shortcut-mod`后缀来修改它。可选的按键有`lctrl`、`rctrl`、 -`lalt`、`ralt`、`lsuper`和`rsuper`。如下例: +您可以使用 `--shortcut-mod` 来修改。可选的按键有 `lctrl`、`rctrl`、`lalt`、`ralt`、`lsuper` 和 `rsuper`。例如: ```bash -# 使用右侧的Ctrl键 +# 使用右 Ctrl 键 scrcpy --shortcut-mod=rctrl -# 使用左侧的Ctrl键、Alt键或Super键 +# 使用左 Ctrl 键 + 左 Alt 键,或 Super 键 scrcpy --shortcut-mod=lctrl+lalt,lsuper ``` -_一般来说,[Super]就是Windows或者Cmd。_ +_[Super] 键通常是指 WindowsCmd 键。_ [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+_点按并移动鼠标_ + | 操作 | 快捷键 | + | --------------------------------- | :------------------------------------------- | + | 全屏 | MOD+f | + | 向左旋转屏幕 | MOD+ _(左箭头)_ | + | 向右旋转屏幕 | MOD+ _(右箭头)_ | + | 将窗口大小重置为1:1 (匹配像素) | MOD+g | + | 将窗口大小重置为消除黑边 | MOD+w \| _双击¹_ | + | 点按 `主屏幕` | MOD+h \| _鼠标中键_ | + | 点按 `返回` | MOD+b \| _鼠标右键²_ | + | 点按 `切换应用` | 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 | + | 捏拉缩放 | Ctrl+_按住并移动鼠标_ | -_¹双击黑色边界以关闭黑色边界_ -_²点击鼠标右键将在屏幕熄灭时点亮屏幕,其余情况则视为按下 返回键 。_ +_¹双击黑边可以去除黑边_ +_²点击鼠标右键将在屏幕熄灭时点亮屏幕,其余情况则视为按下返回键 。_ _³需要安卓版本 Android >= 7。_ -所有的 Ctrl+_按键_ 的热键都是被转发到设备进行处理的,所以实际上会由当前应用程序对其做出响应。 +所有的 Ctrl+_按键_ 的快捷键都会被转发到设备,所以会由当前应用程序进行处理。 ## 自定义路径 -为了使用您想使用的 _adb_ ,您可以在环境变量 -`ADB`中设置它的路径: +要使用指定的 _adb_ 二进制文件,可以设置环境变量 `ADB`: ADB=/path/to/adb scrcpy -如果需要覆盖`scrcpy-server`的路径,您可以在 -`SCRCPY_SERVER_PATH`中设置它。 +要覆盖 `scrcpy-server` 的路径,可以设置 `SCRCPY_SERVER_PATH`。 [useful]: https://github.com/Genymobile/scrcpy/issues/278#issuecomment-429330345 ## 为什么叫 _scrcpy_ ? -一个同事让我找出一个和[gnirehtet]一样难以发音的名字。 +一个同事让我找出一个和 [gnirehtet] 一样难以发音的名字。 -[`strcpy`] 可以复制**str**ing; `scrcpy` 可以复制**scr**een。 +[`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.md +[BUILD]: BUILD.md ## 常见问题 -请查看[FAQ](FAQ.md). +请查看[FAQ](FAQ.md)。 ## 开发者 From ab912c23e756a8a97c48a0735b8027b570f65298 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 8 Jan 2021 19:24:51 +0100 Subject: [PATCH 0455/2244] Define feature test macros in common.h This enables necessary functions once for all. As a consequence, define common.h before any other header. --- app/src/adb.h | 4 ++-- app/src/cli.h | 3 ++- app/src/compat.h | 7 +++++++ app/src/control_msg.h | 3 ++- app/src/controller.h | 3 ++- app/src/decoder.h | 4 ++-- app/src/device.h | 3 ++- app/src/device_msg.h | 4 ++-- app/src/event_converter.h | 3 ++- app/src/file_handler.h | 3 ++- app/src/fps_counter.h | 4 ++-- app/src/input_manager.h | 3 ++- app/src/main.c | 3 ++- app/src/opengl.h | 4 ++-- app/src/receiver.h | 3 ++- app/src/recorder.h | 3 ++- app/src/scrcpy.h | 4 ++-- app/src/screen.h | 3 ++- app/src/server.h | 3 ++- app/src/stream.h | 3 ++- app/src/sys/unix/process.c | 11 ----------- app/src/tiny_xpm.h | 4 ++-- app/src/util/buffer_util.h | 4 ++-- app/src/util/cbuf.h | 4 ++-- app/src/util/lock.h | 3 ++- app/src/util/net.h | 4 ++-- app/src/util/process.h | 4 ++-- app/src/util/queue.h | 4 ++-- app/src/util/str_util.h | 4 ++-- app/src/video_buffer.h | 3 ++- app/tests/test_buffer_util.c | 2 ++ app/tests/test_cbuf.c | 2 ++ app/tests/test_cli.c | 3 ++- app/tests/test_control_msg_serialize.c | 2 ++ app/tests/test_device_msg_deserialize.c | 2 ++ app/tests/test_queue.c | 2 ++ app/tests/test_strutil.c | 2 ++ 37 files changed, 77 insertions(+), 53 deletions(-) diff --git a/app/src/adb.h b/app/src/adb.h index 453e3019..e27f34fa 100644 --- a/app/src/adb.h +++ b/app/src/adb.h @@ -1,11 +1,11 @@ #ifndef SC_ADB_H #define SC_ADB_H +#include "common.h" + #include #include -#include "common.h" - #include "util/process.h" process_t diff --git a/app/src/cli.h b/app/src/cli.h index 22bded79..419f156f 100644 --- a/app/src/cli.h +++ b/app/src/cli.h @@ -1,9 +1,10 @@ #ifndef SCRCPY_CLI_H #define SCRCPY_CLI_H +#include "common.h" + #include -#include "common.h" #include "scrcpy.h" struct scrcpy_cli_args { diff --git a/app/src/compat.h b/app/src/compat.h index 21f19214..5464589a 100644 --- a/app/src/compat.h +++ b/app/src/compat.h @@ -1,6 +1,13 @@ #ifndef COMPAT_H #define COMPAT_H +#define _POSIX_C_SOURCE 200809L +#define _XOPEN_SOURCE 700 +#define _GNU_SOURCE +#ifdef __APPLE__ +# define _DARWIN_C_SOURCE +#endif + #include #include #include diff --git a/app/src/control_msg.h b/app/src/control_msg.h index e23cc89c..20b2ef45 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -1,11 +1,12 @@ #ifndef CONTROLMSG_H #define CONTROLMSG_H +#include "common.h" + #include #include #include -#include "common.h" #include "android/input.h" #include "android/keycodes.h" #include "coords.h" diff --git a/app/src/controller.h b/app/src/controller.h index 5ef8755a..d6fe35e2 100644 --- a/app/src/controller.h +++ b/app/src/controller.h @@ -1,11 +1,12 @@ #ifndef CONTROLLER_H #define CONTROLLER_H +#include "common.h" + #include #include #include -#include "common.h" #include "control_msg.h" #include "receiver.h" #include "util/cbuf.h" diff --git a/app/src/decoder.h b/app/src/decoder.h index 8a3bc297..27afcd8e 100644 --- a/app/src/decoder.h +++ b/app/src/decoder.h @@ -1,11 +1,11 @@ #ifndef DECODER_H #define DECODER_H +#include "common.h" + #include #include -#include "common.h" - struct video_buffer; struct decoder { diff --git a/app/src/device.h b/app/src/device.h index c21b1ace..376e3d4b 100644 --- a/app/src/device.h +++ b/app/src/device.h @@ -1,9 +1,10 @@ #ifndef DEVICE_H #define DEVICE_H +#include "common.h" + #include -#include "common.h" #include "coords.h" #include "util/net.h" diff --git a/app/src/device_msg.h b/app/src/device_msg.h index b1860726..bc13cebb 100644 --- a/app/src/device_msg.h +++ b/app/src/device_msg.h @@ -1,12 +1,12 @@ #ifndef DEVICEMSG_H #define DEVICEMSG_H +#include "common.h" + #include #include #include -#include "common.h" - #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/app/src/event_converter.h b/app/src/event_converter.h index cbd578a0..d28e9fdc 100644 --- a/app/src/event_converter.h +++ b/app/src/event_converter.h @@ -1,10 +1,11 @@ #ifndef CONVERT_H #define CONVERT_H +#include "common.h" + #include #include -#include "common.h" #include "control_msg.h" bool diff --git a/app/src/file_handler.h b/app/src/file_handler.h index 6649b9ea..a8f469e2 100644 --- a/app/src/file_handler.h +++ b/app/src/file_handler.h @@ -1,11 +1,12 @@ #ifndef FILE_HANDLER_H #define FILE_HANDLER_H +#include "common.h" + #include #include #include -#include "common.h" #include "adb.h" #include "util/cbuf.h" diff --git a/app/src/fps_counter.h b/app/src/fps_counter.h index 2c13b3e2..68255bb6 100644 --- a/app/src/fps_counter.h +++ b/app/src/fps_counter.h @@ -1,14 +1,14 @@ #ifndef FPSCOUNTER_H #define FPSCOUNTER_H +#include "common.h" + #include #include #include #include #include -#include "common.h" - struct fps_counter { SDL_Thread *thread; SDL_mutex *mutex; diff --git a/app/src/input_manager.h b/app/src/input_manager.h index 66daa9dd..a23a731d 100644 --- a/app/src/input_manager.h +++ b/app/src/input_manager.h @@ -1,11 +1,12 @@ #ifndef INPUTMANAGER_H #define INPUTMANAGER_H +#include "common.h" + #include #include -#include "common.h" #include "controller.h" #include "fps_counter.h" #include "scrcpy.h" diff --git a/app/src/main.c b/app/src/main.c index 18b9c710..e1e44f68 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -1,5 +1,7 @@ #include "scrcpy.h" +#include "common.h" + #include #include #include @@ -7,7 +9,6 @@ #define SDL_MAIN_HANDLED // avoid link error on Linux Windows Subsystem #include -#include "common.h" #include "cli.h" #include "util/log.h" diff --git a/app/src/opengl.h b/app/src/opengl.h index 1c1e4658..81163704 100644 --- a/app/src/opengl.h +++ b/app/src/opengl.h @@ -1,11 +1,11 @@ #ifndef SC_OPENGL_H #define SC_OPENGL_H +#include "common.h" + #include #include -#include "common.h" - struct sc_opengl { const char *version; bool is_opengles; diff --git a/app/src/receiver.h b/app/src/receiver.h index 8f628238..3b1cc03a 100644 --- a/app/src/receiver.h +++ b/app/src/receiver.h @@ -1,11 +1,12 @@ #ifndef RECEIVER_H #define RECEIVER_H +#include "common.h" + #include #include #include -#include "common.h" #include "util/net.h" // receive events from the device diff --git a/app/src/recorder.h b/app/src/recorder.h index fc05d06c..1e942110 100644 --- a/app/src/recorder.h +++ b/app/src/recorder.h @@ -1,12 +1,13 @@ #ifndef RECORDER_H #define RECORDER_H +#include "common.h" + #include #include #include #include -#include "common.h" #include "coords.h" #include "scrcpy.h" #include "util/queue.h" diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index 08bce7e3..2253cc28 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -1,12 +1,12 @@ #ifndef SCRCPY_H #define SCRCPY_H +#include "common.h" + #include #include #include -#include "common.h" - enum sc_log_level { SC_LOG_LEVEL_DEBUG, SC_LOG_LEVEL_INFO, diff --git a/app/src/screen.h b/app/src/screen.h index 820c7382..ea94d538 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -1,11 +1,12 @@ #ifndef SCREEN_H #define SCREEN_H +#include "common.h" + #include #include #include -#include "common.h" #include "coords.h" #include "opengl.h" diff --git a/app/src/server.h b/app/src/server.h index b48bcd61..7a66670f 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -1,12 +1,13 @@ #ifndef SERVER_H #define SERVER_H +#include "common.h" + #include #include #include #include -#include "common.h" #include "adb.h" #include "scrcpy.h" #include "util/log.h" diff --git a/app/src/stream.h b/app/src/stream.h index 7b0a5d42..d308df88 100644 --- a/app/src/stream.h +++ b/app/src/stream.h @@ -1,13 +1,14 @@ #ifndef STREAM_H #define STREAM_H +#include "common.h" + #include #include #include #include #include -#include "common.h" #include "util/net.h" struct video_buffer; diff --git a/app/src/sys/unix/process.c b/app/src/sys/unix/process.c index dc4f5649..3bb55010 100644 --- a/app/src/sys/unix/process.c +++ b/app/src/sys/unix/process.c @@ -1,14 +1,3 @@ -// for portability (kill, readlink, strdup, strtok_r) -#define _POSIX_C_SOURCE 200809L -#define _BSD_SOURCE - -// modern glibc will complain without this -#define _DEFAULT_SOURCE - -#ifdef __APPLE__ -# define _DARWIN_C_SOURCE // for strdup(), strtok_r(), memset_pattern4() -#endif - #include "util/process.h" #include diff --git a/app/src/tiny_xpm.h b/app/src/tiny_xpm.h index 2dcbeb07..29b42d14 100644 --- a/app/src/tiny_xpm.h +++ b/app/src/tiny_xpm.h @@ -1,10 +1,10 @@ #ifndef TINYXPM_H #define TINYXPM_H -#include - #include "common.h" +#include + SDL_Surface * read_xpm(char *xpm[]); diff --git a/app/src/util/buffer_util.h b/app/src/util/buffer_util.h index 265704bc..ab22ab24 100644 --- a/app/src/util/buffer_util.h +++ b/app/src/util/buffer_util.h @@ -1,11 +1,11 @@ #ifndef BUFFER_UTIL_H #define BUFFER_UTIL_H +#include "common.h" + #include #include -#include "common.h" - static inline void buffer_write16be(uint8_t *buf, uint16_t value) { buf[0] = value >> 8; diff --git a/app/src/util/cbuf.h b/app/src/util/cbuf.h index 6abc984b..01e41044 100644 --- a/app/src/util/cbuf.h +++ b/app/src/util/cbuf.h @@ -2,11 +2,11 @@ #ifndef CBUF_H #define CBUF_H +#include "common.h" + #include #include -#include "common.h" - // To define a circular buffer type of 20 ints: // struct cbuf_int CBUF(int, 20); // diff --git a/app/src/util/lock.h b/app/src/util/lock.h index a0a044b1..f031bd69 100644 --- a/app/src/util/lock.h +++ b/app/src/util/lock.h @@ -1,10 +1,11 @@ #ifndef LOCK_H #define LOCK_H +#include "common.h" + #include #include -#include "common.h" #include "log.h" static inline void diff --git a/app/src/util/net.h b/app/src/util/net.h index f86c048d..d3b1f941 100644 --- a/app/src/util/net.h +++ b/app/src/util/net.h @@ -1,6 +1,8 @@ #ifndef NET_H #define NET_H +#include "common.h" + #include #include #include @@ -17,8 +19,6 @@ typedef int socket_t; #endif -#include "common.h" - bool net_init(void); diff --git a/app/src/util/process.h b/app/src/util/process.h index f91553d4..46e481bb 100644 --- a/app/src/util/process.h +++ b/app/src/util/process.h @@ -1,10 +1,10 @@ #ifndef SC_PROCESS_H #define SC_PROCESS_H -#include - #include "common.h" +#include + #ifdef _WIN32 // not needed here, but winsock2.h must never be included AFTER windows.h diff --git a/app/src/util/queue.h b/app/src/util/queue.h index 6092c712..0681070c 100644 --- a/app/src/util/queue.h +++ b/app/src/util/queue.h @@ -2,12 +2,12 @@ #ifndef QUEUE_H #define QUEUE_H +#include "common.h" + #include #include #include -#include "common.h" - // To define a queue type of "struct foo": // struct queue_foo QUEUE(struct foo); #define QUEUE(TYPE) { \ diff --git a/app/src/util/str_util.h b/app/src/util/str_util.h index dd523400..25bec444 100644 --- a/app/src/util/str_util.h +++ b/app/src/util/str_util.h @@ -1,11 +1,11 @@ #ifndef STRUTIL_H #define STRUTIL_H +#include "common.h" + #include #include -#include "common.h" - // like strncpy, except: // - it copies at most n-1 chars // - the dest string is nul-terminated diff --git a/app/src/video_buffer.h b/app/src/video_buffer.h index ddd639ad..68ef8e04 100644 --- a/app/src/video_buffer.h +++ b/app/src/video_buffer.h @@ -1,10 +1,11 @@ #ifndef VIDEO_BUFFER_H #define VIDEO_BUFFER_H +#include "common.h" + #include #include -#include "common.h" #include "fps_counter.h" // forward declarations diff --git a/app/tests/test_buffer_util.c b/app/tests/test_buffer_util.c index d61e6918..c7c13bdd 100644 --- a/app/tests/test_buffer_util.c +++ b/app/tests/test_buffer_util.c @@ -1,3 +1,5 @@ +#include "common.h" + #include #include "util/buffer_util.h" diff --git a/app/tests/test_cbuf.c b/app/tests/test_cbuf.c index f8beb880..16674e92 100644 --- a/app/tests/test_cbuf.c +++ b/app/tests/test_cbuf.c @@ -1,3 +1,5 @@ +#include "common.h" + #include #include diff --git a/app/tests/test_cli.c b/app/tests/test_cli.c index 9699b024..cd222d63 100644 --- a/app/tests/test_cli.c +++ b/app/tests/test_cli.c @@ -1,7 +1,8 @@ +#include "common.h" + #include #include -#include "common.h" #include "cli.h" #include "scrcpy.h" diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c index b58c8e20..ffd5f300 100644 --- a/app/tests/test_control_msg_serialize.c +++ b/app/tests/test_control_msg_serialize.c @@ -1,3 +1,5 @@ +#include "common.h" + #include #include diff --git a/app/tests/test_device_msg_deserialize.c b/app/tests/test_device_msg_deserialize.c index 3dfd0b0f..3427d640 100644 --- a/app/tests/test_device_msg_deserialize.c +++ b/app/tests/test_device_msg_deserialize.c @@ -1,3 +1,5 @@ +#include "common.h" + #include #include diff --git a/app/tests/test_queue.c b/app/tests/test_queue.c index e10821cd..fcbafc62 100644 --- a/app/tests/test_queue.c +++ b/app/tests/test_queue.c @@ -1,3 +1,5 @@ +#include "common.h" + #include #include "util/queue.h" diff --git a/app/tests/test_strutil.c b/app/tests/test_strutil.c index 7b9c61da..9d35f983 100644 --- a/app/tests/test_strutil.c +++ b/app/tests/test_strutil.c @@ -1,3 +1,5 @@ +#include "common.h" + #include #include #include From 8dbb1676b72f4f88dc7967c7d9f6f4802576bcfa Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 8 Jan 2021 19:13:53 +0100 Subject: [PATCH 0456/2244] Factorize meson compiler variable initialization --- app/meson.build | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/meson.build b/app/meson.build index 651e756b..8e77cb93 100644 --- a/app/meson.build +++ b/app/meson.build @@ -31,6 +31,8 @@ else src += [ 'src/sys/unix/process.c' ] endif +cc = meson.get_compiler('c') + if not get_option('crossbuild_windows') # native build @@ -44,8 +46,6 @@ if not get_option('crossbuild_windows') else # cross-compile mingw32 build (from Linux to Windows) - cc = meson.get_compiler('c') - prebuilt_sdl2 = meson.get_cross_property('prebuilt_sdl2') sdl2_bin_dir = meson.current_source_dir() + '/../prebuilt-deps/' + prebuilt_sdl2 + '/bin' sdl2_lib_dir = meson.current_source_dir() + '/../prebuilt-deps/' + prebuilt_sdl2 + '/lib' @@ -80,8 +80,6 @@ else endif -cc = meson.get_compiler('c') - if host_machine.system() == 'windows' dependencies += cc.find_library('ws2_32') endif From 94eff0a4bb0853bc30cbb131e38878f34e28c0f6 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 17 Jan 2021 14:22:23 +0100 Subject: [PATCH 0457/2244] Fix size_t incorrectly assigned to int The function control_msg_serialize() returns a size_t. --- app/src/controller.c | 4 ++-- app/tests/test_control_msg_serialize.c | 24 ++++++++++++------------ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/app/src/controller.c b/app/src/controller.c index 6bfb80d3..da90fbf1 100644 --- a/app/src/controller.c +++ b/app/src/controller.c @@ -60,12 +60,12 @@ static bool process_msg(struct controller *controller, const struct control_msg *msg) { static unsigned char serialized_msg[CONTROL_MSG_MAX_SIZE]; - int length = control_msg_serialize(msg, serialized_msg); + size_t length = control_msg_serialize(msg, serialized_msg); if (!length) { return false; } int w = net_send_all(controller->control_socket, serialized_msg, length); - return w == length; + return (size_t) w == length; } static int diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c index ffd5f300..dc6e0821 100644 --- a/app/tests/test_control_msg_serialize.c +++ b/app/tests/test_control_msg_serialize.c @@ -17,7 +17,7 @@ static void test_serialize_inject_keycode(void) { }; unsigned char buf[CONTROL_MSG_MAX_SIZE]; - int size = control_msg_serialize(&msg, buf); + size_t size = control_msg_serialize(&msg, buf); assert(size == 14); const unsigned char expected[] = { @@ -39,7 +39,7 @@ static void test_serialize_inject_text(void) { }; unsigned char buf[CONTROL_MSG_MAX_SIZE]; - int size = control_msg_serialize(&msg, buf); + size_t size = control_msg_serialize(&msg, buf); assert(size == 18); const unsigned char expected[] = { @@ -59,7 +59,7 @@ static void test_serialize_inject_text_long(void) { msg.inject_text.text = text; unsigned char buf[CONTROL_MSG_MAX_SIZE]; - int size = control_msg_serialize(&msg, buf); + size_t size = control_msg_serialize(&msg, buf); assert(size == 5 + CONTROL_MSG_INJECT_TEXT_MAX_LENGTH); unsigned char expected[5 + CONTROL_MSG_INJECT_TEXT_MAX_LENGTH]; @@ -95,7 +95,7 @@ static void test_serialize_inject_touch_event(void) { }; unsigned char buf[CONTROL_MSG_MAX_SIZE]; - int size = control_msg_serialize(&msg, buf); + size_t size = control_msg_serialize(&msg, buf); assert(size == 28); const unsigned char expected[] = { @@ -130,7 +130,7 @@ static void test_serialize_inject_scroll_event(void) { }; unsigned char buf[CONTROL_MSG_MAX_SIZE]; - int size = control_msg_serialize(&msg, buf); + size_t size = control_msg_serialize(&msg, buf); assert(size == 21); const unsigned char expected[] = { @@ -149,7 +149,7 @@ static void test_serialize_back_or_screen_on(void) { }; unsigned char buf[CONTROL_MSG_MAX_SIZE]; - int size = control_msg_serialize(&msg, buf); + size_t size = control_msg_serialize(&msg, buf); assert(size == 1); const unsigned char expected[] = { @@ -164,7 +164,7 @@ static void test_serialize_expand_notification_panel(void) { }; unsigned char buf[CONTROL_MSG_MAX_SIZE]; - int size = control_msg_serialize(&msg, buf); + size_t size = control_msg_serialize(&msg, buf); assert(size == 1); const unsigned char expected[] = { @@ -179,7 +179,7 @@ static void test_serialize_collapse_notification_panel(void) { }; unsigned char buf[CONTROL_MSG_MAX_SIZE]; - int size = control_msg_serialize(&msg, buf); + size_t size = control_msg_serialize(&msg, buf); assert(size == 1); const unsigned char expected[] = { @@ -194,7 +194,7 @@ static void test_serialize_get_clipboard(void) { }; unsigned char buf[CONTROL_MSG_MAX_SIZE]; - int size = control_msg_serialize(&msg, buf); + size_t size = control_msg_serialize(&msg, buf); assert(size == 1); const unsigned char expected[] = { @@ -213,7 +213,7 @@ static void test_serialize_set_clipboard(void) { }; unsigned char buf[CONTROL_MSG_MAX_SIZE]; - int size = control_msg_serialize(&msg, buf); + size_t size = control_msg_serialize(&msg, buf); assert(size == 19); const unsigned char expected[] = { @@ -234,7 +234,7 @@ static void test_serialize_set_screen_power_mode(void) { }; unsigned char buf[CONTROL_MSG_MAX_SIZE]; - int size = control_msg_serialize(&msg, buf); + size_t size = control_msg_serialize(&msg, buf); assert(size == 2); const unsigned char expected[] = { @@ -250,7 +250,7 @@ static void test_serialize_rotate_device(void) { }; unsigned char buf[CONTROL_MSG_MAX_SIZE]; - int size = control_msg_serialize(&msg, buf); + size_t size = control_msg_serialize(&msg, buf); assert(size == 1); const unsigned char expected[] = { From b8edcf52b0fb5e1502397c88929868bc79dac1ec Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 22 Jan 2021 18:29:21 +0100 Subject: [PATCH 0458/2244] Simplify process_wait() The function process_wait() returned a bool (true if the process terminated successfully) and provided the exit code via an output parameter exit_code. But the returned value was always equivalent to exit_code == 0, so just return the exit code instead. --- app/src/file_handler.c | 2 +- app/src/server.c | 4 ++-- app/src/sys/unix/process.c | 25 +++++++++++-------------- app/src/sys/win/process.c | 23 ++++++++++------------- app/src/util/process.c | 4 ++-- app/src/util/process.h | 8 ++++---- 6 files changed, 30 insertions(+), 36 deletions(-) diff --git a/app/src/file_handler.c b/app/src/file_handler.c index 3ad92c05..930fd218 100644 --- a/app/src/file_handler.c +++ b/app/src/file_handler.c @@ -178,7 +178,7 @@ file_handler_stop(struct file_handler *file_handler) { if (!process_terminate(file_handler->current_process)) { LOGW("Could not terminate install process"); } - process_wait(file_handler->current_process, NULL); + process_wait(file_handler->current_process); // ignore exit code file_handler->current_process = PROCESS_NONE; } mutex_unlock(file_handler->mutex); diff --git a/app/src/server.c b/app/src/server.c index 841af82f..c5982b18 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -391,7 +391,7 @@ server_init(struct server *server) { static int run_wait_server(void *data) { struct server *server = data; - process_wait_noclose(server->process, NULL); // ignore exit code + process_wait_noclose(server->process); // ignore exit code mutex_lock(server->mutex); server->process_terminated = true; @@ -447,7 +447,7 @@ server_start(struct server *server, const char *serial, SDL_CreateThread(run_wait_server, "wait-server", server); if (!server->wait_server_thread) { process_terminate(server->process); - process_wait(server->process, NULL); // ignore exit code + process_wait(server->process); // ignore exit code goto error2; } diff --git a/app/src/sys/unix/process.c b/app/src/sys/unix/process.c index 3bb55010..2d1d0ff7 100644 --- a/app/src/sys/unix/process.c +++ b/app/src/sys/unix/process.c @@ -121,8 +121,8 @@ process_terminate(pid_t pid) { return kill(pid, SIGTERM) != -1; } -static bool -process_wait_internal(pid_t pid, int *exit_code, bool close) { +static exit_code_t +process_wait_internal(pid_t pid, bool close) { int code; int options = WEXITED; if (!close) { @@ -133,29 +133,26 @@ process_wait_internal(pid_t pid, int *exit_code, bool close) { int r = waitid(P_PID, pid, &info, options); if (r == -1 || info.si_code != CLD_EXITED) { // could not wait, or exited unexpectedly, probably by a signal - code = -1; + code = NO_EXIT_CODE; } else { code = info.si_status; } - if (exit_code) { - *exit_code = code; - } - return !code; + return code; } -bool -process_wait(pid_t pid, int *exit_code) { - return process_wait_internal(pid, exit_code, true); +exit_code_t +process_wait(pid_t pid) { + return process_wait_internal(pid, true); } -bool -process_wait_noclose(pid_t pid, int *exit_code) { - return process_wait_internal(pid, exit_code, false); +exit_code_t +process_wait_noclose(pid_t pid) { + return process_wait_internal(pid, false); } void process_close(pid_t pid) { - process_wait_internal(pid, NULL, true); + process_wait_internal(pid, true); // ignore exit code } char * diff --git a/app/src/sys/win/process.c b/app/src/sys/win/process.c index f087625c..bb5d9bc6 100644 --- a/app/src/sys/win/process.c +++ b/app/src/sys/win/process.c @@ -59,31 +59,28 @@ process_terminate(HANDLE handle) { return TerminateProcess(handle, 1); } -static bool -process_wait_internal(HANDLE handle, DWORD *exit_code, bool close) { +static exit_code_t +process_wait_internal(HANDLE handle, bool close) { DWORD code; if (WaitForSingleObject(handle, INFINITE) != WAIT_OBJECT_0 || !GetExitCodeProcess(handle, &code)) { // could not wait or retrieve the exit code - code = -1; // max value, it's unsigned - } - if (exit_code) { - *exit_code = code; + code = NO_EXIT_CODE; // max value, it's unsigned } if (close) { CloseHandle(handle); } - return !code; + return code; } -bool -process_wait(HANDLE handle, DWORD *exit_code) { - return process_wait_internal(handle, exit_code, true); +exit_code_t +process_wait(HANDLE handle) { + return process_wait_internal(handle, true); } -bool -process_wait_noclose(HANDLE handle, DWORD *exit_code) { - return process_wait_internal(handle, exit_code, false); +exit_code_t +process_wait_noclose(HANDLE handle) { + return process_wait_internal(handle, false); } void diff --git a/app/src/util/process.c b/app/src/util/process.c index e485ce17..e9d71d86 100644 --- a/app/src/util/process.c +++ b/app/src/util/process.c @@ -8,8 +8,8 @@ process_check_success(process_t proc, const char *name) { LOGE("Could not execute \"%s\"", name); return false; } - exit_code_t exit_code; - if (!process_wait(proc, &exit_code)) { + exit_code_t exit_code = process_wait(proc); + if (exit_code) { if (exit_code != NO_EXIT_CODE) { LOGE("\"%s\" returned with value %" PRIexitcode, name, exit_code); } else { diff --git a/app/src/util/process.h b/app/src/util/process.h index 46e481bb..e199e317 100644 --- a/app/src/util/process.h +++ b/app/src/util/process.h @@ -47,12 +47,12 @@ bool process_terminate(process_t pid); // wait and close the process (like waitpid()) -bool -process_wait(process_t pid, exit_code_t *exit_code); +exit_code_t +process_wait(process_t pid); // wait (but does not close) the process (waitid() with WNOWAIT) -bool -process_wait_noclose(process_t pid, exit_code_t *exit_code); +exit_code_t +process_wait_noclose(process_t pid); // close the process // From 6a50231698f0ef635d4ab35ea01d531eb2e0ba2a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 22 Jan 2021 18:48:17 +0100 Subject: [PATCH 0459/2244] Expose a single process_wait() There were two versions: process_wait() and process_wait_noclose(). Expose a single version with a flag (it was already implemented that way internally). --- app/src/file_handler.c | 2 +- app/src/server.c | 4 ++-- app/src/sys/unix/process.c | 16 +++------------- app/src/sys/win/process.c | 14 ++------------ app/src/util/process.c | 2 +- app/src/util/process.h | 10 ++++------ 6 files changed, 13 insertions(+), 35 deletions(-) diff --git a/app/src/file_handler.c b/app/src/file_handler.c index 930fd218..c39b7bf4 100644 --- a/app/src/file_handler.c +++ b/app/src/file_handler.c @@ -178,7 +178,7 @@ file_handler_stop(struct file_handler *file_handler) { if (!process_terminate(file_handler->current_process)) { LOGW("Could not terminate install process"); } - process_wait(file_handler->current_process); // ignore exit code + process_wait(file_handler->current_process, true); // ignore exit code file_handler->current_process = PROCESS_NONE; } mutex_unlock(file_handler->mutex); diff --git a/app/src/server.c b/app/src/server.c index c5982b18..8637ca4e 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -391,7 +391,7 @@ server_init(struct server *server) { static int run_wait_server(void *data) { struct server *server = data; - process_wait_noclose(server->process); // ignore exit code + process_wait(server->process, false); // ignore exit code mutex_lock(server->mutex); server->process_terminated = true; @@ -447,7 +447,7 @@ server_start(struct server *server, const char *serial, SDL_CreateThread(run_wait_server, "wait-server", server); if (!server->wait_server_thread) { process_terminate(server->process); - process_wait(server->process); // ignore exit code + process_wait(server->process, true); // ignore exit code goto error2; } diff --git a/app/src/sys/unix/process.c b/app/src/sys/unix/process.c index 2d1d0ff7..dc535dcf 100644 --- a/app/src/sys/unix/process.c +++ b/app/src/sys/unix/process.c @@ -121,8 +121,8 @@ process_terminate(pid_t pid) { return kill(pid, SIGTERM) != -1; } -static exit_code_t -process_wait_internal(pid_t pid, bool close) { +exit_code_t +process_wait(pid_t pid, bool close) { int code; int options = WEXITED; if (!close) { @@ -140,19 +140,9 @@ process_wait_internal(pid_t pid, bool close) { return code; } -exit_code_t -process_wait(pid_t pid) { - return process_wait_internal(pid, true); -} - -exit_code_t -process_wait_noclose(pid_t pid) { - return process_wait_internal(pid, false); -} - void process_close(pid_t pid) { - process_wait_internal(pid, true); // ignore exit code + process_wait(pid, true); // ignore exit code } char * diff --git a/app/src/sys/win/process.c b/app/src/sys/win/process.c index bb5d9bc6..88242af3 100644 --- a/app/src/sys/win/process.c +++ b/app/src/sys/win/process.c @@ -59,8 +59,8 @@ process_terminate(HANDLE handle) { return TerminateProcess(handle, 1); } -static exit_code_t -process_wait_internal(HANDLE handle, bool close) { +exit_code_t +process_wait(HANDLE handle, bool close) { DWORD code; if (WaitForSingleObject(handle, INFINITE) != WAIT_OBJECT_0 || !GetExitCodeProcess(handle, &code)) { @@ -73,16 +73,6 @@ process_wait_internal(HANDLE handle, bool close) { return code; } -exit_code_t -process_wait(HANDLE handle) { - return process_wait_internal(handle, true); -} - -exit_code_t -process_wait_noclose(HANDLE handle) { - return process_wait_internal(handle, false); -} - void process_close(HANDLE handle) { bool closed = CloseHandle(handle); diff --git a/app/src/util/process.c b/app/src/util/process.c index e9d71d86..667e614a 100644 --- a/app/src/util/process.c +++ b/app/src/util/process.c @@ -8,7 +8,7 @@ process_check_success(process_t proc, const char *name) { LOGE("Could not execute \"%s\"", name); return false; } - exit_code_t exit_code = process_wait(proc); + exit_code_t exit_code = process_wait(proc, true); if (exit_code) { if (exit_code != NO_EXIT_CODE) { LOGE("\"%s\" returned with value %" PRIexitcode, name, exit_code); diff --git a/app/src/util/process.h b/app/src/util/process.h index e199e317..5090eaf7 100644 --- a/app/src/util/process.h +++ b/app/src/util/process.h @@ -47,16 +47,14 @@ bool process_terminate(process_t pid); // wait and close the process (like waitpid()) +// the "close" flag indicates if the process must be "closed" (reaped) +// (passing false is equivalent to enable WNOWAIT in waitid()) exit_code_t -process_wait(process_t pid); - -// wait (but does not close) the process (waitid() with WNOWAIT) -exit_code_t -process_wait_noclose(process_t pid); +process_wait(process_t pid, bool close); // close the process // -// Semantically, process_wait = process_wait_noclose + process_close. +// Semantically, process_wait(close) = process_wait(noclose) + process_close void process_close(process_t pid); From 7afd149f4bccc6b8e0d5d27e48cb5fa05e9f2fec Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 22 Jan 2021 19:20:30 +0100 Subject: [PATCH 0460/2244] Fix file_handler process race condition The current process could be waited both by run_file_handler() and file_handler_stop(). To avoid the race condition, wait the process without closing, then close with mutex locked. --- app/src/file_handler.c | 14 ++++++++++---- app/src/server.c | 10 +++++----- app/src/util/process.c | 4 ++-- app/src/util/process.h | 2 +- 4 files changed, 18 insertions(+), 12 deletions(-) diff --git a/app/src/file_handler.c b/app/src/file_handler.c index c39b7bf4..d8881e30 100644 --- a/app/src/file_handler.c +++ b/app/src/file_handler.c @@ -135,13 +135,13 @@ run_file_handler(void *data) { mutex_unlock(file_handler->mutex); if (req.action == ACTION_INSTALL_APK) { - if (process_check_success(process, "adb install")) { + if (process_check_success(process, "adb install", false)) { LOGI("%s successfully installed", req.file); } else { LOGE("Failed to install %s", req.file); } } else { - if (process_check_success(process, "adb push")) { + if (process_check_success(process, "adb push", false)) { LOGI("%s successfully pushed to %s", req.file, file_handler->push_target); } else { @@ -150,6 +150,14 @@ run_file_handler(void *data) { } } + mutex_lock(file_handler->mutex); + // Close the process (it is necessary already terminated) + // Execute this call with mutex locked to avoid race conditions with + // file_handler_stop() + process_close(file_handler->current_process); + file_handler->current_process = PROCESS_NONE; + mutex_unlock(file_handler->mutex); + file_handler_request_destroy(&req); } return 0; @@ -178,8 +186,6 @@ file_handler_stop(struct file_handler *file_handler) { if (!process_terminate(file_handler->current_process)) { LOGW("Could not terminate install process"); } - process_wait(file_handler->current_process, true); // ignore exit code - file_handler->current_process = PROCESS_NONE; } mutex_unlock(file_handler->mutex); } diff --git a/app/src/server.c b/app/src/server.c index 8637ca4e..b7e7938c 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -100,31 +100,31 @@ push_server(const char *serial) { } process_t process = adb_push(serial, server_path, DEVICE_SERVER_PATH); SDL_free(server_path); - return process_check_success(process, "adb push"); + return process_check_success(process, "adb push", true); } static bool enable_tunnel_reverse(const char *serial, uint16_t local_port) { process_t process = adb_reverse(serial, SOCKET_NAME, local_port); - return process_check_success(process, "adb reverse"); + return process_check_success(process, "adb reverse", true); } static bool disable_tunnel_reverse(const char *serial) { process_t process = adb_reverse_remove(serial, SOCKET_NAME); - return process_check_success(process, "adb reverse --remove"); + return process_check_success(process, "adb reverse --remove", true); } static bool enable_tunnel_forward(const char *serial, uint16_t local_port) { process_t process = adb_forward(serial, local_port, SOCKET_NAME); - return process_check_success(process, "adb forward"); + return process_check_success(process, "adb forward", true); } static bool disable_tunnel_forward(const char *serial, uint16_t local_port) { process_t process = adb_forward_remove(serial, local_port); - return process_check_success(process, "adb forward --remove"); + return process_check_success(process, "adb forward --remove", true); } static bool diff --git a/app/src/util/process.c b/app/src/util/process.c index 667e614a..5edeeee6 100644 --- a/app/src/util/process.c +++ b/app/src/util/process.c @@ -3,12 +3,12 @@ #include "log.h" bool -process_check_success(process_t proc, const char *name) { +process_check_success(process_t proc, const char *name, bool close) { if (proc == PROCESS_NONE) { LOGE("Could not execute \"%s\"", name); return false; } - exit_code_t exit_code = process_wait(proc, true); + exit_code_t exit_code = process_wait(proc, close); if (exit_code) { if (exit_code != NO_EXIT_CODE) { LOGE("\"%s\" returned with value %" PRIexitcode, name, exit_code); diff --git a/app/src/util/process.h b/app/src/util/process.h index 5090eaf7..7e619a2b 100644 --- a/app/src/util/process.h +++ b/app/src/util/process.h @@ -61,7 +61,7 @@ process_close(process_t pid); // convenience function to wait for a successful process execution // automatically log process errors with the provided process name bool -process_check_success(process_t proc, const char *name); +process_check_success(process_t proc, const char *name, bool close); #ifndef _WIN32 // only used to find package manager, not implemented for Windows From b566700bfdedd4115ff9f701aa53eb6be2f8924a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 22 Jan 2021 19:21:35 +0100 Subject: [PATCH 0461/2244] Kill process with SIGKILL signal An "adb push" command is not terminated by SIGTERM. --- app/src/sys/unix/process.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/sys/unix/process.c b/app/src/sys/unix/process.c index dc535dcf..293b2aff 100644 --- a/app/src/sys/unix/process.c +++ b/app/src/sys/unix/process.c @@ -118,7 +118,7 @@ process_terminate(pid_t pid) { (int) pid); abort(); } - return kill(pid, SIGTERM) != -1; + return kill(pid, SIGKILL) != -1; } exit_code_t From d8e9ad20b004549b8c001ae5624376fce2070cd4 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 22 Jan 2021 19:22:40 +0100 Subject: [PATCH 0462/2244] Improve file handler error message Terminating the file handler current process may be either a "push" or "install" command. --- app/src/file_handler.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/file_handler.c b/app/src/file_handler.c index d8881e30..3652ef90 100644 --- a/app/src/file_handler.c +++ b/app/src/file_handler.c @@ -184,7 +184,7 @@ file_handler_stop(struct file_handler *file_handler) { cond_signal(file_handler->event_cond); if (file_handler->current_process != PROCESS_NONE) { if (!process_terminate(file_handler->current_process)) { - LOGW("Could not terminate install process"); + LOGW("Could not terminate push/install process"); } } mutex_unlock(file_handler->mutex); From 97b001e7c0108ebe474494acba5a331aac4dedb3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 24 Jan 2021 15:22:14 +0100 Subject: [PATCH 0463/2244] Fix undefined left shift Small unsigned integers promote to signed int. As a consequence, if v is a uint8_t, then (v << 24) yields an int, so the left shift is undefined if the MSB is 1. Cast to uint32_t to yield an unsigned value. Reported by USAN (meson x -Db_sanitize=undefined): runtime error: left shift of 255 by 24 places cannot be represented in type 'int' --- app/src/util/buffer_util.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/util/buffer_util.h b/app/src/util/buffer_util.h index ab22ab24..337bb262 100644 --- a/app/src/util/buffer_util.h +++ b/app/src/util/buffer_util.h @@ -33,7 +33,7 @@ buffer_read16be(const uint8_t *buf) { static inline uint32_t buffer_read32be(const uint8_t *buf) { - return (buf[0] << 24) | (buf[1] << 16) | (buf[2] << 8) | buf[3]; + return ((uint32_t) buf[0] << 24) | (buf[1] << 16) | (buf[2] << 8) | buf[3]; } static inline uint64_t From f8524a2be738e6471ed2e48265f2663d8c21c0be Mon Sep 17 00:00:00 2001 From: Michael Richardson Date: Wed, 27 Jan 2021 15:39:28 -0500 Subject: [PATCH 0464/2244] Update java version in BUILD.md PR #2064 Signed-off-by: Romain Vimont --- BUILD.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BUILD.md b/BUILD.md index 5dcf7b27..52029b6b 100644 --- a/BUILD.md +++ b/BUILD.md @@ -64,7 +64,7 @@ sudo apt install gcc git pkg-config meson ninja-build \ libsdl2-dev # server build dependencies -sudo apt install openjdk-8-jdk +sudo apt install openjdk-11-jdk ``` On old versions (like Ubuntu 16.04), `meson` is too old. In that case, install From 8e83f3e8af595ee0a8fe0538abff7501146fcf12 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 7 Aug 2020 18:42:17 +0200 Subject: [PATCH 0465/2244] Remove unused custom event --- app/src/events.h | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/src/events.h b/app/src/events.h index e9512048..a4d6f3df 100644 --- a/app/src/events.h +++ b/app/src/events.h @@ -1,3 +1,2 @@ -#define EVENT_NEW_SESSION SDL_USEREVENT -#define EVENT_NEW_FRAME (SDL_USEREVENT + 1) -#define EVENT_STREAM_STOPPED (SDL_USEREVENT + 2) +#define EVENT_NEW_FRAME SDL_USEREVENT +#define EVENT_STREAM_STOPPED (SDL_USEREVENT + 1) From ace438e52abef2d0ccb8380a2c46501fcdc1c630 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 6 Dec 2020 15:18:33 +0100 Subject: [PATCH 0466/2244] Remove unused port_range field The port_range is used from "struct server_params", the copy in "struct server" was unused. --- app/src/server.c | 4 ---- app/src/server.h | 1 - 2 files changed, 5 deletions(-) diff --git a/app/src/server.c b/app/src/server.c index b7e7938c..f1ac7b46 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -378,8 +378,6 @@ server_init(struct server *server) { server->video_socket = INVALID_SOCKET; server->control_socket = INVALID_SOCKET; - server->port_range.first = 0; - server->port_range.last = 0; server->local_port = 0; server->tunnel_enabled = false; @@ -413,8 +411,6 @@ run_wait_server(void *data) { bool server_start(struct server *server, const char *serial, const struct server_params *params) { - server->port_range = params->port_range; - if (serial) { server->serial = SDL_strdup(serial); if (!server->serial) { diff --git a/app/src/server.h b/app/src/server.h index 7a66670f..1ac12b5f 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -26,7 +26,6 @@ struct server { socket_t server_socket; // only used if !tunnel_forward socket_t video_socket; socket_t control_socket; - 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" From c0dde0fade6d2b582015816a6f08f9114c919094 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 17 Jan 2021 14:11:59 +0100 Subject: [PATCH 0467/2244] Provide strdup() compat Make strdup() available on all platforms. --- app/meson.build | 12 ++++++++++++ app/src/compat.c | 14 ++++++++++++++ app/src/compat.h | 4 ++++ 3 files changed, 30 insertions(+) create mode 100644 app/src/compat.c diff --git a/app/meson.build b/app/meson.build index 8e77cb93..2dade249 100644 --- a/app/meson.build +++ b/app/meson.build @@ -2,6 +2,7 @@ src = [ 'src/main.c', 'src/adb.c', 'src/cli.c', + 'src/compat.c', 'src/control_msg.c', 'src/controller.c', 'src/decoder.c', @@ -31,6 +32,10 @@ else src += [ 'src/sys/unix/process.c' ] endif +check_functions = [ + 'strdup' +] + cc = meson.get_compiler('c') if not get_option('crossbuild_windows') @@ -86,6 +91,13 @@ endif conf = configuration_data() +foreach f : check_functions + if cc.has_function(f) + define = 'HAVE_' + f.underscorify().to_upper() + conf.set(define, true) + endif +endforeach + # expose the build type conf.set('NDEBUG', get_option('buildtype') != 'debug') diff --git a/app/src/compat.c b/app/src/compat.c new file mode 100644 index 00000000..b3b98bf1 --- /dev/null +++ b/app/src/compat.c @@ -0,0 +1,14 @@ +#include "compat.h" + +#include "config.h" + +#ifndef HAVE_STRDUP +char *strdup(const char *s) { + size_t size = strlen(s) + 1; + char *dup = malloc(size); + if (dup) { + memcpy(dup, s, size); + } + return dup; +} +#endif diff --git a/app/src/compat.h b/app/src/compat.h index 5464589a..9a84a4c1 100644 --- a/app/src/compat.h +++ b/app/src/compat.h @@ -56,4 +56,8 @@ # define SCRCPY_SDL_HAS_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR #endif +#ifndef HAVE_STRDUP +char *strdup(const char *s); +#endif + #endif From 30e619d37f867621ada9d549dc735ceaff72a635 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 24 Jan 2021 15:14:53 +0100 Subject: [PATCH 0468/2244] Replace SDL_strdup() by strdup() The functions SDL_malloc(), SDL_free() and SDL_strdup() were used only because strdup() was not available everywhere. Now that it is available, use the native version of these functions. --- app/src/adb.c | 8 ++++---- app/src/control_msg.c | 5 +++-- app/src/control_msg.h | 4 ++-- app/src/device_msg.c | 5 +++-- app/src/device_msg.h | 2 +- app/src/file_handler.c | 6 +++--- app/src/file_handler.h | 2 +- app/src/input_manager.c | 26 ++++++++++++++++++++------ app/src/recorder.c | 14 +++++++------- app/src/scrcpy.c | 15 +++++++++++---- app/src/server.c | 20 ++++++++++---------- app/src/sys/unix/process.c | 2 +- app/src/sys/win/process.c | 6 +++--- app/src/util/process.h | 2 +- app/src/util/str_util.c | 8 +++----- app/tests/test_strutil.c | 3 +-- 16 files changed, 74 insertions(+), 54 deletions(-) diff --git a/app/src/adb.c b/app/src/adb.c index 086db174..be973c41 100644 --- a/app/src/adb.c +++ b/app/src/adb.c @@ -173,7 +173,7 @@ adb_push(const char *serial, const char *local, const char *remote) { } remote = strquote(remote); if (!remote) { - SDL_free((void *) local); + free((void *) local); return PROCESS_NONE; } #endif @@ -182,8 +182,8 @@ adb_push(const char *serial, const char *local, const char *remote) { process_t proc = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd)); #ifdef __WINDOWS__ - SDL_free((void *) remote); - SDL_free((void *) local); + free((void *) remote); + free((void *) local); #endif return proc; @@ -204,7 +204,7 @@ adb_install(const char *serial, const char *local) { process_t proc = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd)); #ifdef __WINDOWS__ - SDL_free((void *) local); + free((void *) local); #endif return proc; diff --git a/app/src/control_msg.c b/app/src/control_msg.c index 77e534cd..436a8861 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -1,6 +1,7 @@ #include "control_msg.h" #include +#include #include #include "util/buffer_util.h" @@ -93,10 +94,10 @@ void control_msg_destroy(struct control_msg *msg) { switch (msg->type) { case CONTROL_MSG_TYPE_INJECT_TEXT: - SDL_free(msg->inject_text.text); + free(msg->inject_text.text); break; case CONTROL_MSG_TYPE_SET_CLIPBOARD: - SDL_free(msg->set_clipboard.text); + free(msg->set_clipboard.text); break; default: // do nothing diff --git a/app/src/control_msg.h b/app/src/control_msg.h index 20b2ef45..1b25591d 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -50,7 +50,7 @@ struct control_msg { enum android_metastate metastate; } inject_keycode; struct { - char *text; // owned, to be freed by SDL_free() + char *text; // owned, to be freed by free() } inject_text; struct { enum android_motionevent_action action; @@ -65,7 +65,7 @@ struct control_msg { int32_t vscroll; } inject_scroll_event; struct { - char *text; // owned, to be freed by SDL_free() + char *text; // owned, to be freed by free() bool paste; } set_clipboard; struct { diff --git a/app/src/device_msg.c b/app/src/device_msg.c index c82ce628..827f4213 100644 --- a/app/src/device_msg.c +++ b/app/src/device_msg.c @@ -1,5 +1,6 @@ #include "device_msg.h" +#include #include #include "util/buffer_util.h" @@ -20,7 +21,7 @@ device_msg_deserialize(const unsigned char *buf, size_t len, if (clipboard_len > len - 5) { return 0; // not available } - char *text = SDL_malloc(clipboard_len + 1); + char *text = malloc(clipboard_len + 1); if (!text) { LOGW("Could not allocate text for clipboard"); return -1; @@ -42,6 +43,6 @@ device_msg_deserialize(const unsigned char *buf, size_t len, void device_msg_destroy(struct device_msg *msg) { if (msg->type == DEVICE_MSG_TYPE_CLIPBOARD) { - SDL_free(msg->clipboard.text); + free(msg->clipboard.text); } } diff --git a/app/src/device_msg.h b/app/src/device_msg.h index bc13cebb..888d9216 100644 --- a/app/src/device_msg.h +++ b/app/src/device_msg.h @@ -19,7 +19,7 @@ struct device_msg { enum device_msg_type type; union { struct { - char *text; // owned, to be freed by SDL_free() + char *text; // owned, to be freed by free() } clipboard; }; }; diff --git a/app/src/file_handler.c b/app/src/file_handler.c index 3652ef90..4f60a101 100644 --- a/app/src/file_handler.c +++ b/app/src/file_handler.c @@ -11,7 +11,7 @@ static void file_handler_request_destroy(struct file_handler_request *req) { - SDL_free(req->file); + free(req->file); } bool @@ -30,7 +30,7 @@ file_handler_init(struct file_handler *file_handler, const char *serial, } if (serial) { - file_handler->serial = SDL_strdup(serial); + file_handler->serial = strdup(serial); if (!file_handler->serial) { LOGW("Could not strdup serial"); SDL_DestroyCond(file_handler->event_cond); @@ -56,7 +56,7 @@ void file_handler_destroy(struct file_handler *file_handler) { SDL_DestroyCond(file_handler->event_cond); SDL_DestroyMutex(file_handler->mutex); - SDL_free(file_handler->serial); + free(file_handler->serial); struct file_handler_request req; while (cbuf_take(&file_handler->queue, &req)) { diff --git a/app/src/file_handler.h b/app/src/file_handler.h index a8f469e2..193f68c3 100644 --- a/app/src/file_handler.h +++ b/app/src/file_handler.h @@ -50,7 +50,7 @@ file_handler_stop(struct file_handler *file_handler); void file_handler_join(struct file_handler *file_handler); -// take ownership of file, and will SDL_free() it +// take ownership of file, and will free() it bool file_handler_request(struct file_handler *file_handler, file_handler_action_t action, diff --git a/app/src/input_manager.c b/app/src/input_manager.c index df01ea38..432c4c83 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -190,13 +190,20 @@ set_device_clipboard(struct controller *controller, bool paste) { return; } + char *text_dup = strdup(text); + SDL_free(text); + if (!text_dup) { + LOGW("Could not strdup input text"); + return; + } + struct control_msg msg; msg.type = CONTROL_MSG_TYPE_SET_CLIPBOARD; - msg.set_clipboard.text = text; + msg.set_clipboard.text = text_dup; msg.set_clipboard.paste = paste; if (!controller_push_msg(controller, &msg)) { - SDL_free(text); + free(text_dup); LOGW("Could not request 'set device clipboard'"); } } @@ -242,11 +249,18 @@ clipboard_paste(struct controller *controller) { return; } + char *text_dup = strdup(text); + SDL_free(text); + if (!text_dup) { + LOGW("Could not strdup input text"); + return; + } + struct control_msg msg; msg.type = CONTROL_MSG_TYPE_INJECT_TEXT; - msg.inject_text.text = text; + msg.inject_text.text = text_dup; if (!controller_push_msg(controller, &msg)) { - SDL_free(text); + free(text_dup); LOGW("Could not request 'paste clipboard'"); } } @@ -291,13 +305,13 @@ input_manager_process_text_input(struct input_manager *im, struct control_msg msg; msg.type = CONTROL_MSG_TYPE_INJECT_TEXT; - msg.inject_text.text = SDL_strdup(event->text); + msg.inject_text.text = strdup(event->text); if (!msg.inject_text.text) { LOGW("Could not strdup input text"); return; } if (!controller_push_msg(im->controller, &msg)) { - SDL_free(msg.inject_text.text); + free(msg.inject_text.text); LOGW("Could not request 'inject text'"); } } diff --git a/app/src/recorder.c b/app/src/recorder.c index 6558d804..12047369 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -27,7 +27,7 @@ find_muxer(const char *name) { static struct record_packet * record_packet_new(const AVPacket *packet) { - struct record_packet *rec = SDL_malloc(sizeof(*rec)); + struct record_packet *rec = malloc(sizeof(*rec)); if (!rec) { return NULL; } @@ -37,7 +37,7 @@ record_packet_new(const AVPacket *packet) { av_init_packet(&rec->packet); if (av_packet_ref(&rec->packet, packet)) { - SDL_free(rec); + free(rec); return NULL; } return rec; @@ -46,7 +46,7 @@ record_packet_new(const AVPacket *packet) { static void record_packet_delete(struct record_packet *rec) { av_packet_unref(&rec->packet); - SDL_free(rec); + free(rec); } static void @@ -63,7 +63,7 @@ recorder_init(struct recorder *recorder, const char *filename, enum sc_record_format format, struct size declared_frame_size) { - recorder->filename = SDL_strdup(filename); + recorder->filename = strdup(filename); if (!recorder->filename) { LOGE("Could not strdup filename"); return false; @@ -72,7 +72,7 @@ recorder_init(struct recorder *recorder, recorder->mutex = SDL_CreateMutex(); if (!recorder->mutex) { LOGC("Could not create mutex"); - SDL_free(recorder->filename); + free(recorder->filename); return false; } @@ -80,7 +80,7 @@ recorder_init(struct recorder *recorder, if (!recorder->queue_cond) { LOGC("Could not create cond"); SDL_DestroyMutex(recorder->mutex); - SDL_free(recorder->filename); + free(recorder->filename); return false; } @@ -99,7 +99,7 @@ void recorder_destroy(struct recorder *recorder) { SDL_DestroyCond(recorder->queue_cond); SDL_DestroyMutex(recorder->mutex); - SDL_free(recorder->filename); + free(recorder->filename); } static const char * diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index ecc7a0e6..f1560130 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -226,13 +226,20 @@ handle_event(SDL_Event *event, const struct scrcpy_options *options) { if (!options->control) { break; } + char *file = strdup(event->drop.file); + SDL_free(event->drop.file); + if (!file) { + LOGW("Could not strdup drop filename\n"); + break; + } + file_handler_action_t action; - if (is_apk(event->drop.file)) { + if (is_apk(file)) { action = ACTION_INSTALL_APK; } else { action = ACTION_PUSH_FILE; } - file_handler_request(&file_handler, action, event->drop.file); + file_handler_request(&file_handler, action, file); break; } } @@ -286,7 +293,7 @@ av_log_callback(void *avcl, int level, const char *fmt, va_list vl) { if (priority == 0) { return; } - char *local_fmt = SDL_malloc(strlen(fmt) + 10); + char *local_fmt = malloc(strlen(fmt) + 10); if (!local_fmt) { LOGC("Could not allocate string"); return; @@ -295,7 +302,7 @@ av_log_callback(void *avcl, int level, const char *fmt, va_list vl) { strcpy(local_fmt, "[FFmpeg] "); strcpy(local_fmt + 9, fmt); SDL_LogMessageV(SDL_LOG_CATEGORY_VIDEO, priority, local_fmt, vl); - SDL_free(local_fmt); + free(local_fmt); } bool diff --git a/app/src/server.c b/app/src/server.c index f1ac7b46..bb08d56e 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -33,7 +33,7 @@ get_server_path(void) { #ifdef __WINDOWS__ char *server_path = utf8_from_wide_char(server_path_env); #else - char *server_path = SDL_strdup(server_path_env); + char *server_path = strdup(server_path_env); #endif if (!server_path) { LOGE("Could not allocate memory"); @@ -45,7 +45,7 @@ get_server_path(void) { #ifndef PORTABLE LOGD("Using server: " DEFAULT_SERVER_PATH); - char *server_path = SDL_strdup(DEFAULT_SERVER_PATH); + char *server_path = strdup(DEFAULT_SERVER_PATH); if (!server_path) { LOGE("Could not allocate memory"); return NULL; @@ -67,11 +67,11 @@ get_server_path(void) { // sizeof(SERVER_FILENAME) gives statically the size including the null byte size_t len = dirlen + 1 + sizeof(SERVER_FILENAME); - char *server_path = SDL_malloc(len); + char *server_path = malloc(len); if (!server_path) { LOGE("Could not alloc server path string, " "using " SERVER_FILENAME " from current directory"); - SDL_free(executable_path); + free(executable_path); return SERVER_FILENAME; } @@ -80,7 +80,7 @@ get_server_path(void) { memcpy(&server_path[dirlen + 1], SERVER_FILENAME, sizeof(SERVER_FILENAME)); // the final null byte has been copied with SERVER_FILENAME - SDL_free(executable_path); + free(executable_path); LOGD("Using server (portable): %s", server_path); return server_path; @@ -95,11 +95,11 @@ push_server(const char *serial) { } if (!is_regular_file(server_path)) { LOGE("'%s' does not exist or is not a regular file\n", server_path); - SDL_free(server_path); + free(server_path); return false; } process_t process = adb_push(serial, server_path, DEVICE_SERVER_PATH); - SDL_free(server_path); + free(server_path); return process_check_success(process, "adb push", true); } @@ -412,7 +412,7 @@ bool server_start(struct server *server, const char *serial, const struct server_params *params) { if (serial) { - server->serial = SDL_strdup(serial); + server->serial = strdup(serial); if (!server->serial) { return false; } @@ -462,7 +462,7 @@ error2: } disable_tunnel(server); error1: - SDL_free(server->serial); + free(server->serial); return false; } @@ -557,7 +557,7 @@ server_stop(struct server *server) { void server_destroy(struct server *server) { - SDL_free(server->serial); + free(server->serial); SDL_DestroyCond(server->process_terminated_cond); SDL_DestroyMutex(server->mutex); } diff --git a/app/src/sys/unix/process.c b/app/src/sys/unix/process.c index 293b2aff..8683a2da 100644 --- a/app/src/sys/unix/process.c +++ b/app/src/sys/unix/process.c @@ -156,7 +156,7 @@ get_executable_path(void) { return NULL; } buf[len] = '\0'; - return SDL_strdup(buf); + return strdup(buf); #else // in practice, we only need this feature for portable builds, only used on // Windows, so we don't care implementing it for every platform diff --git a/app/src/sys/win/process.c b/app/src/sys/win/process.c index 88242af3..f170e40d 100644 --- a/app/src/sys/win/process.c +++ b/app/src/sys/win/process.c @@ -41,7 +41,7 @@ process_execute(const char *const argv[], HANDLE *handle) { if (!CreateProcessW(NULL, wide, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi)) { - SDL_free(wide); + free(wide); *handle = NULL; if (GetLastError() == ERROR_FILE_NOT_FOUND) { return PROCESS_ERROR_MISSING_BINARY; @@ -49,7 +49,7 @@ process_execute(const char *const argv[], HANDLE *handle) { return PROCESS_ERROR_GENERIC; } - SDL_free(wide); + free(wide); *handle = pi.hProcess; return PROCESS_SUCCESS; } @@ -105,7 +105,7 @@ is_regular_file(const char *path) { struct _stat path_stat; int r = _wstat(wide_path, &path_stat); - SDL_free(wide_path); + free(wide_path); if (r) { perror("stat"); diff --git a/app/src/util/process.h b/app/src/util/process.h index 7e619a2b..7838a848 100644 --- a/app/src/util/process.h +++ b/app/src/util/process.h @@ -70,7 +70,7 @@ search_executable(const char *file); #endif // return the absolute path of the executable (the scrcpy binary) -// may be NULL on error; to be freed by SDL_free +// may be NULL on error; to be freed by free() char * get_executable_path(void); diff --git a/app/src/util/str_util.c b/app/src/util/str_util.c index babce4a1..352d1d2f 100644 --- a/app/src/util/str_util.c +++ b/app/src/util/str_util.c @@ -10,8 +10,6 @@ # include #endif -#include - size_t xstrncpy(char *dest, const char *src, size_t n) { size_t i; @@ -49,7 +47,7 @@ truncated: char * strquote(const char *src) { size_t len = strlen(src); - char *quoted = SDL_malloc(len + 3); + char *quoted = malloc(len + 3); if (!quoted) { return NULL; } @@ -167,7 +165,7 @@ utf8_to_wide_char(const char *utf8) { return NULL; } - wchar_t *wide = SDL_malloc(len * sizeof(wchar_t)); + wchar_t *wide = malloc(len * sizeof(wchar_t)); if (!wide) { return NULL; } @@ -183,7 +181,7 @@ utf8_from_wide_char(const wchar_t *ws) { return NULL; } - char *utf8 = SDL_malloc(len); + char *utf8 = malloc(len); if (!utf8) { return NULL; } diff --git a/app/tests/test_strutil.c b/app/tests/test_strutil.c index 9d35f983..ce0d5d30 100644 --- a/app/tests/test_strutil.c +++ b/app/tests/test_strutil.c @@ -4,7 +4,6 @@ #include #include #include -#include #include "util/str_util.h" @@ -138,7 +137,7 @@ static void test_strquote(void) { // add '"' at the beginning and the end assert(!strcmp("\"abcde\"", out)); - SDL_free(out); + free(out); } static void test_utf8_truncate(void) { From f6320c7e317ba9c5e5fda1fd7d4358646060412b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 31 Jan 2021 18:24:35 +0100 Subject: [PATCH 0469/2244] Wrap SDL thread functions into scrcpy-specific API The goal is to expose a consistent API for system tools, and paves the way to make the "core" independant of SDL in the future. --- app/meson.build | 3 +- app/src/controller.c | 46 +++++++------- app/src/controller.h | 9 ++- app/src/decoder.c | 2 - app/src/file_handler.c | 49 +++++++-------- app/src/file_handler.h | 9 ++- app/src/fps_counter.c | 66 ++++++++++---------- app/src/fps_counter.h | 12 ++-- app/src/input_manager.c | 1 - app/src/receiver.c | 13 ++-- app/src/receiver.h | 7 +-- app/src/recorder.c | 50 +++++++-------- app/src/recorder.h | 9 ++- app/src/scrcpy.c | 1 - app/src/screen.c | 7 +-- app/src/server.c | 45 +++++++------- app/src/server.h | 8 +-- app/src/stream.c | 8 +-- app/src/stream.h | 4 +- app/src/util/lock.h | 75 ---------------------- app/src/util/thread.c | 133 ++++++++++++++++++++++++++++++++++++++++ app/src/util/thread.h | 66 ++++++++++++++++++++ app/src/video_buffer.c | 34 +++++----- app/src/video_buffer.h | 12 ++-- 24 files changed, 395 insertions(+), 274 deletions(-) delete mode 100644 app/src/util/lock.h create mode 100644 app/src/util/thread.c create mode 100644 app/src/util/thread.h diff --git a/app/meson.build b/app/meson.build index 2dade249..1f50209e 100644 --- a/app/meson.build +++ b/app/meson.build @@ -23,7 +23,8 @@ src = [ 'src/video_buffer.c', 'src/util/net.c', 'src/util/process.c', - 'src/util/str_util.c' + 'src/util/str_util.c', + 'src/util/thread.c', ] if host_machine.system() == 'windows' diff --git a/app/src/controller.c b/app/src/controller.c index da90fbf1..38b5e702 100644 --- a/app/src/controller.c +++ b/app/src/controller.c @@ -2,25 +2,27 @@ #include -#include "util/lock.h" #include "util/log.h" bool controller_init(struct controller *controller, socket_t control_socket) { cbuf_init(&controller->queue); - if (!receiver_init(&controller->receiver, control_socket)) { + bool ok = receiver_init(&controller->receiver, control_socket); + if (!ok) { return false; } - if (!(controller->mutex = SDL_CreateMutex())) { + ok = sc_mutex_init(&controller->mutex); + if (!ok) { receiver_destroy(&controller->receiver); return false; } - if (!(controller->msg_cond = SDL_CreateCond())) { + ok = sc_cond_init(&controller->msg_cond); + if (!ok) { receiver_destroy(&controller->receiver); - SDL_DestroyMutex(controller->mutex); + sc_mutex_destroy(&controller->mutex); return false; } @@ -32,8 +34,8 @@ controller_init(struct controller *controller, socket_t control_socket) { void controller_destroy(struct controller *controller) { - SDL_DestroyCond(controller->msg_cond); - SDL_DestroyMutex(controller->mutex); + sc_cond_destroy(&controller->msg_cond); + sc_mutex_destroy(&controller->mutex); struct control_msg msg; while (cbuf_take(&controller->queue, &msg)) { @@ -46,13 +48,13 @@ controller_destroy(struct controller *controller) { bool controller_push_msg(struct controller *controller, const struct control_msg *msg) { - mutex_lock(controller->mutex); + sc_mutex_lock(&controller->mutex); bool was_empty = cbuf_is_empty(&controller->queue); bool res = cbuf_push(&controller->queue, *msg); if (was_empty) { - cond_signal(controller->msg_cond); + sc_cond_signal(&controller->msg_cond); } - mutex_unlock(controller->mutex); + sc_mutex_unlock(&controller->mutex); return res; } @@ -73,20 +75,20 @@ run_controller(void *data) { struct controller *controller = data; for (;;) { - mutex_lock(controller->mutex); + sc_mutex_lock(&controller->mutex); while (!controller->stopped && cbuf_is_empty(&controller->queue)) { - cond_wait(controller->msg_cond, controller->mutex); + sc_cond_wait(&controller->msg_cond, &controller->mutex); } if (controller->stopped) { // stop immediately, do not process further msgs - mutex_unlock(controller->mutex); + sc_mutex_unlock(&controller->mutex); break; } struct control_msg msg; bool non_empty = cbuf_take(&controller->queue, &msg); assert(non_empty); (void) non_empty; - mutex_unlock(controller->mutex); + sc_mutex_unlock(&controller->mutex); bool ok = process_msg(controller, &msg); control_msg_destroy(&msg); @@ -102,16 +104,16 @@ bool controller_start(struct controller *controller) { LOGD("Starting controller thread"); - controller->thread = SDL_CreateThread(run_controller, "controller", - controller); - if (!controller->thread) { + bool ok = sc_thread_create(&controller->thread, run_controller, + "controller", controller); + if (!ok) { LOGC("Could not start controller thread"); return false; } if (!receiver_start(&controller->receiver)) { controller_stop(controller); - SDL_WaitThread(controller->thread, NULL); + sc_thread_join(&controller->thread, NULL); return false; } @@ -120,14 +122,14 @@ controller_start(struct controller *controller) { void controller_stop(struct controller *controller) { - mutex_lock(controller->mutex); + sc_mutex_lock(&controller->mutex); controller->stopped = true; - cond_signal(controller->msg_cond); - mutex_unlock(controller->mutex); + sc_cond_signal(&controller->msg_cond); + sc_mutex_unlock(&controller->mutex); } void controller_join(struct controller *controller) { - SDL_WaitThread(controller->thread, NULL); + sc_thread_join(&controller->thread, NULL); receiver_join(&controller->receiver); } diff --git a/app/src/controller.h b/app/src/controller.h index d6fe35e2..c53d0a61 100644 --- a/app/src/controller.h +++ b/app/src/controller.h @@ -4,21 +4,20 @@ #include "common.h" #include -#include -#include #include "control_msg.h" #include "receiver.h" #include "util/cbuf.h" #include "util/net.h" +#include "util/thread.h" struct control_msg_queue CBUF(struct control_msg, 64); struct controller { socket_t control_socket; - SDL_Thread *thread; - SDL_mutex *mutex; - SDL_cond *msg_cond; + sc_thread thread; + sc_mutex mutex; + sc_cond msg_cond; bool stopped; struct control_msg_queue queue; struct receiver receiver; diff --git a/app/src/decoder.c b/app/src/decoder.c index 50c55ab2..23f9ce9d 100644 --- a/app/src/decoder.c +++ b/app/src/decoder.c @@ -3,8 +3,6 @@ #include #include #include -#include -#include #include #include "events.h" diff --git a/app/src/file_handler.c b/app/src/file_handler.c index 4f60a101..2b08240c 100644 --- a/app/src/file_handler.c +++ b/app/src/file_handler.c @@ -4,7 +4,6 @@ #include #include "adb.h" -#include "util/lock.h" #include "util/log.h" #define DEFAULT_PUSH_TARGET "/sdcard/" @@ -20,12 +19,14 @@ file_handler_init(struct file_handler *file_handler, const char *serial, cbuf_init(&file_handler->queue); - if (!(file_handler->mutex = SDL_CreateMutex())) { + bool ok = sc_mutex_init(&file_handler->mutex); + if (!ok) { return false; } - if (!(file_handler->event_cond = SDL_CreateCond())) { - SDL_DestroyMutex(file_handler->mutex); + ok = sc_cond_init(&file_handler->event_cond); + if (!ok) { + sc_mutex_destroy(&file_handler->mutex); return false; } @@ -33,8 +34,8 @@ file_handler_init(struct file_handler *file_handler, const char *serial, file_handler->serial = strdup(serial); if (!file_handler->serial) { LOGW("Could not strdup serial"); - SDL_DestroyCond(file_handler->event_cond); - SDL_DestroyMutex(file_handler->mutex); + sc_cond_destroy(&file_handler->event_cond); + sc_mutex_destroy(&file_handler->mutex); return false; } } else { @@ -54,8 +55,8 @@ file_handler_init(struct file_handler *file_handler, const char *serial, void file_handler_destroy(struct file_handler *file_handler) { - SDL_DestroyCond(file_handler->event_cond); - SDL_DestroyMutex(file_handler->mutex); + sc_cond_destroy(&file_handler->event_cond); + sc_mutex_destroy(&file_handler->mutex); free(file_handler->serial); struct file_handler_request req; @@ -92,13 +93,13 @@ file_handler_request(struct file_handler *file_handler, .file = file, }; - mutex_lock(file_handler->mutex); + sc_mutex_lock(&file_handler->mutex); bool was_empty = cbuf_is_empty(&file_handler->queue); bool res = cbuf_push(&file_handler->queue, req); if (was_empty) { - cond_signal(file_handler->event_cond); + sc_cond_signal(&file_handler->event_cond); } - mutex_unlock(file_handler->mutex); + sc_mutex_unlock(&file_handler->mutex); return res; } @@ -107,14 +108,14 @@ run_file_handler(void *data) { struct file_handler *file_handler = data; for (;;) { - mutex_lock(file_handler->mutex); + sc_mutex_lock(&file_handler->mutex); file_handler->current_process = PROCESS_NONE; while (!file_handler->stopped && cbuf_is_empty(&file_handler->queue)) { - cond_wait(file_handler->event_cond, file_handler->mutex); + sc_cond_wait(&file_handler->event_cond, &file_handler->mutex); } if (file_handler->stopped) { // stop immediately, do not process further events - mutex_unlock(file_handler->mutex); + sc_mutex_unlock(&file_handler->mutex); break; } struct file_handler_request req; @@ -132,7 +133,7 @@ run_file_handler(void *data) { file_handler->push_target); } file_handler->current_process = process; - mutex_unlock(file_handler->mutex); + sc_mutex_unlock(&file_handler->mutex); if (req.action == ACTION_INSTALL_APK) { if (process_check_success(process, "adb install", false)) { @@ -150,13 +151,13 @@ run_file_handler(void *data) { } } - mutex_lock(file_handler->mutex); + sc_mutex_lock(&file_handler->mutex); // Close the process (it is necessary already terminated) // Execute this call with mutex locked to avoid race conditions with // file_handler_stop() process_close(file_handler->current_process); file_handler->current_process = PROCESS_NONE; - mutex_unlock(file_handler->mutex); + sc_mutex_unlock(&file_handler->mutex); file_handler_request_destroy(&req); } @@ -167,9 +168,9 @@ bool file_handler_start(struct file_handler *file_handler) { LOGD("Starting file_handler thread"); - file_handler->thread = SDL_CreateThread(run_file_handler, "file_handler", - file_handler); - if (!file_handler->thread) { + bool ok = sc_thread_create(&file_handler->thread, run_file_handler, + "file_handler", file_handler); + if (!ok) { LOGC("Could not start file_handler thread"); return false; } @@ -179,18 +180,18 @@ file_handler_start(struct file_handler *file_handler) { void file_handler_stop(struct file_handler *file_handler) { - mutex_lock(file_handler->mutex); + sc_mutex_lock(&file_handler->mutex); file_handler->stopped = true; - cond_signal(file_handler->event_cond); + sc_cond_signal(&file_handler->event_cond); if (file_handler->current_process != PROCESS_NONE) { if (!process_terminate(file_handler->current_process)) { LOGW("Could not terminate push/install process"); } } - mutex_unlock(file_handler->mutex); + sc_mutex_unlock(&file_handler->mutex); } void file_handler_join(struct file_handler *file_handler) { - SDL_WaitThread(file_handler->thread, NULL); + sc_thread_join(&file_handler->thread, NULL); } diff --git a/app/src/file_handler.h b/app/src/file_handler.h index 193f68c3..fe1d1804 100644 --- a/app/src/file_handler.h +++ b/app/src/file_handler.h @@ -4,11 +4,10 @@ #include "common.h" #include -#include -#include #include "adb.h" #include "util/cbuf.h" +#include "util/thread.h" typedef enum { ACTION_INSTALL_APK, @@ -25,9 +24,9 @@ struct file_handler_request_queue CBUF(struct file_handler_request, 16); struct file_handler { char *serial; const char *push_target; - SDL_Thread *thread; - SDL_mutex *mutex; - SDL_cond *event_cond; + sc_thread thread; + sc_mutex mutex; + sc_cond event_cond; bool stopped; bool initialized; process_t current_process; diff --git a/app/src/fps_counter.c b/app/src/fps_counter.c index e7607409..281c58cf 100644 --- a/app/src/fps_counter.c +++ b/app/src/fps_counter.c @@ -3,25 +3,24 @@ #include #include -#include "util/lock.h" #include "util/log.h" #define FPS_COUNTER_INTERVAL_MS 1000 bool fps_counter_init(struct fps_counter *counter) { - counter->mutex = SDL_CreateMutex(); - if (!counter->mutex) { + bool ok = sc_mutex_init(&counter->mutex); + if (!ok) { return false; } - counter->state_cond = SDL_CreateCond(); - if (!counter->state_cond) { - SDL_DestroyMutex(counter->mutex); + ok = sc_cond_init(&counter->state_cond); + if (!ok) { + sc_mutex_destroy(&counter->mutex); return false; } - counter->thread = NULL; + counter->thread_started = false; atomic_init(&counter->started, 0); // no need to initialize the other fields, they are unused until started @@ -30,8 +29,8 @@ fps_counter_init(struct fps_counter *counter) { void fps_counter_destroy(struct fps_counter *counter) { - SDL_DestroyCond(counter->state_cond); - SDL_DestroyMutex(counter->mutex); + sc_cond_destroy(&counter->state_cond); + sc_mutex_destroy(&counter->mutex); } static inline bool @@ -77,10 +76,10 @@ static int run_fps_counter(void *data) { struct fps_counter *counter = data; - mutex_lock(counter->mutex); + sc_mutex_lock(&counter->mutex); while (!counter->interrupted) { while (!counter->interrupted && !is_started(counter)) { - cond_wait(counter->state_cond, counter->mutex); + sc_cond_wait(&counter->state_cond, &counter->mutex); } while (!counter->interrupted && is_started(counter)) { uint32_t now = SDL_GetTicks(); @@ -90,32 +89,35 @@ run_fps_counter(void *data) { uint32_t remaining = counter->next_timestamp - now; // ignore the reason (timeout or signaled), we just loop anyway - cond_wait_timeout(counter->state_cond, counter->mutex, remaining); + sc_cond_timedwait(&counter->state_cond, &counter->mutex, remaining); } } - mutex_unlock(counter->mutex); + sc_mutex_unlock(&counter->mutex); return 0; } bool fps_counter_start(struct fps_counter *counter) { - mutex_lock(counter->mutex); + sc_mutex_lock(&counter->mutex); counter->next_timestamp = SDL_GetTicks() + FPS_COUNTER_INTERVAL_MS; counter->nr_rendered = 0; counter->nr_skipped = 0; - mutex_unlock(counter->mutex); + sc_mutex_unlock(&counter->mutex); set_started(counter, true); - cond_signal(counter->state_cond); + sc_cond_signal(&counter->state_cond); - // counter->thread is always accessed from the same thread, no need to lock - if (!counter->thread) { - counter->thread = - SDL_CreateThread(run_fps_counter, "fps counter", counter); - if (!counter->thread) { + // counter->thread_started and counter->thread are always accessed from the + // same thread, no need to lock + if (!counter->thread_started) { + bool ok = sc_thread_create(&counter->thread, run_fps_counter, + "fps counter", counter); + if (!ok) { LOGE("Could not start FPS counter thread"); return false; } + + counter->thread_started = true; } return true; @@ -124,7 +126,7 @@ fps_counter_start(struct fps_counter *counter) { void fps_counter_stop(struct fps_counter *counter) { set_started(counter, false); - cond_signal(counter->state_cond); + sc_cond_signal(&counter->state_cond); } bool @@ -134,21 +136,21 @@ fps_counter_is_started(struct fps_counter *counter) { void fps_counter_interrupt(struct fps_counter *counter) { - if (!counter->thread) { + if (!counter->thread_started) { return; } - mutex_lock(counter->mutex); + sc_mutex_lock(&counter->mutex); counter->interrupted = true; - mutex_unlock(counter->mutex); + sc_mutex_unlock(&counter->mutex); // wake up blocking wait - cond_signal(counter->state_cond); + sc_cond_signal(&counter->state_cond); } void fps_counter_join(struct fps_counter *counter) { - if (counter->thread) { - SDL_WaitThread(counter->thread, NULL); + if (counter->thread_started) { + sc_thread_join(&counter->thread, NULL); } } @@ -158,11 +160,11 @@ fps_counter_add_rendered_frame(struct fps_counter *counter) { return; } - mutex_lock(counter->mutex); + sc_mutex_lock(&counter->mutex); uint32_t now = SDL_GetTicks(); check_interval_expired(counter, now); ++counter->nr_rendered; - mutex_unlock(counter->mutex); + sc_mutex_unlock(&counter->mutex); } void @@ -171,9 +173,9 @@ fps_counter_add_skipped_frame(struct fps_counter *counter) { return; } - mutex_lock(counter->mutex); + sc_mutex_lock(&counter->mutex); uint32_t now = SDL_GetTicks(); check_interval_expired(counter, now); ++counter->nr_skipped; - mutex_unlock(counter->mutex); + sc_mutex_unlock(&counter->mutex); } diff --git a/app/src/fps_counter.h b/app/src/fps_counter.h index 68255bb6..de252586 100644 --- a/app/src/fps_counter.h +++ b/app/src/fps_counter.h @@ -6,13 +6,15 @@ #include #include #include -#include -#include + +#include "util/thread.h" struct fps_counter { - SDL_Thread *thread; - SDL_mutex *mutex; - SDL_cond *state_cond; + sc_thread thread; + sc_mutex mutex; + sc_cond state_cond; + + bool thread_started; // atomic so that we can check without locking the mutex // if the FPS counter is disabled, we don't want to lock unnecessarily diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 432c4c83..fd780ae6 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -4,7 +4,6 @@ #include #include "event_converter.h" -#include "util/lock.h" #include "util/log.h" static const int ACTION_DOWN = 1; diff --git a/app/src/receiver.c b/app/src/receiver.c index 5b97f88b..337d2a17 100644 --- a/app/src/receiver.c +++ b/app/src/receiver.c @@ -4,12 +4,12 @@ #include #include "device_msg.h" -#include "util/lock.h" #include "util/log.h" bool receiver_init(struct receiver *receiver, socket_t control_socket) { - if (!(receiver->mutex = SDL_CreateMutex())) { + bool ok = sc_mutex_init(&receiver->mutex); + if (!ok) { return false; } receiver->control_socket = control_socket; @@ -18,7 +18,7 @@ receiver_init(struct receiver *receiver, socket_t control_socket) { void receiver_destroy(struct receiver *receiver) { - SDL_DestroyMutex(receiver->mutex); + sc_mutex_destroy(&receiver->mutex); } static void @@ -101,8 +101,9 @@ bool receiver_start(struct receiver *receiver) { LOGD("Starting receiver thread"); - receiver->thread = SDL_CreateThread(run_receiver, "receiver", receiver); - if (!receiver->thread) { + bool ok = sc_thread_create(&receiver->thread, run_receiver, "receiver", + receiver); + if (!ok) { LOGC("Could not start receiver thread"); return false; } @@ -112,5 +113,5 @@ receiver_start(struct receiver *receiver) { void receiver_join(struct receiver *receiver) { - SDL_WaitThread(receiver->thread, NULL); + sc_thread_join(&receiver->thread, NULL); } diff --git a/app/src/receiver.h b/app/src/receiver.h index 3b1cc03a..36523b62 100644 --- a/app/src/receiver.h +++ b/app/src/receiver.h @@ -4,17 +4,16 @@ #include "common.h" #include -#include -#include #include "util/net.h" +#include "util/thread.h" // receive events from the device // managed by the controller struct receiver { socket_t control_socket; - SDL_Thread *thread; - SDL_mutex *mutex; + sc_thread thread; + sc_mutex mutex; }; bool diff --git a/app/src/recorder.c b/app/src/recorder.c index 12047369..0aacb1a4 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -3,7 +3,6 @@ #include #include -#include "util/lock.h" #include "util/log.h" static const AVRational SCRCPY_TIME_BASE = {1, 1000000}; // timestamps in us @@ -69,17 +68,17 @@ recorder_init(struct recorder *recorder, return false; } - recorder->mutex = SDL_CreateMutex(); - if (!recorder->mutex) { + bool ok = sc_mutex_init(&recorder->mutex); + if (!ok) { LOGC("Could not create mutex"); free(recorder->filename); return false; } - recorder->queue_cond = SDL_CreateCond(); - if (!recorder->queue_cond) { + ok = sc_cond_init(&recorder->queue_cond); + if (!ok) { LOGC("Could not create cond"); - SDL_DestroyMutex(recorder->mutex); + sc_mutex_destroy(&recorder->mutex); free(recorder->filename); return false; } @@ -97,8 +96,8 @@ recorder_init(struct recorder *recorder, void recorder_destroy(struct recorder *recorder) { - SDL_DestroyCond(recorder->queue_cond); - SDL_DestroyMutex(recorder->mutex); + sc_cond_destroy(&recorder->queue_cond); + sc_mutex_destroy(&recorder->mutex); free(recorder->filename); } @@ -258,17 +257,17 @@ run_recorder(void *data) { struct recorder *recorder = data; for (;;) { - mutex_lock(recorder->mutex); + sc_mutex_lock(&recorder->mutex); while (!recorder->stopped && queue_is_empty(&recorder->queue)) { - cond_wait(recorder->queue_cond, recorder->mutex); + sc_cond_wait(&recorder->queue_cond, &recorder->mutex); } // if stopped is set, continue to process the remaining events (to // finish the recording) before actually stopping if (recorder->stopped && queue_is_empty(&recorder->queue)) { - mutex_unlock(recorder->mutex); + sc_mutex_unlock(&recorder->mutex); struct record_packet *last = recorder->previous; if (last) { // assign an arbitrary duration to the last packet @@ -288,7 +287,7 @@ run_recorder(void *data) { struct record_packet *rec; queue_take(&recorder->queue, next, &rec); - mutex_unlock(recorder->mutex); + sc_mutex_unlock(&recorder->mutex); // recorder->previous is only written from this thread, no need to lock struct record_packet *previous = recorder->previous; @@ -311,11 +310,11 @@ run_recorder(void *data) { if (!ok) { LOGE("Could not record packet"); - mutex_lock(recorder->mutex); + sc_mutex_lock(&recorder->mutex); recorder->failed = true; // discard pending packets recorder_queue_clear(&recorder->queue); - mutex_unlock(recorder->mutex); + sc_mutex_unlock(&recorder->mutex); break; } @@ -330,8 +329,9 @@ bool recorder_start(struct recorder *recorder) { LOGD("Starting recorder thread"); - recorder->thread = SDL_CreateThread(run_recorder, "recorder", recorder); - if (!recorder->thread) { + bool ok = sc_thread_create(&recorder->thread, run_recorder, "recorder", + recorder); + if (!ok) { LOGC("Could not start recorder thread"); return false; } @@ -341,38 +341,38 @@ recorder_start(struct recorder *recorder) { void recorder_stop(struct recorder *recorder) { - mutex_lock(recorder->mutex); + sc_mutex_lock(&recorder->mutex); recorder->stopped = true; - cond_signal(recorder->queue_cond); - mutex_unlock(recorder->mutex); + sc_cond_signal(&recorder->queue_cond); + sc_mutex_unlock(&recorder->mutex); } void recorder_join(struct recorder *recorder) { - SDL_WaitThread(recorder->thread, NULL); + sc_thread_join(&recorder->thread, NULL); } bool recorder_push(struct recorder *recorder, const AVPacket *packet) { - mutex_lock(recorder->mutex); + sc_mutex_lock(&recorder->mutex); assert(!recorder->stopped); if (recorder->failed) { // reject any new packet (this will stop the stream) - mutex_unlock(recorder->mutex); + sc_mutex_unlock(&recorder->mutex); return false; } struct record_packet *rec = record_packet_new(packet); if (!rec) { LOGC("Could not allocate record packet"); - mutex_unlock(recorder->mutex); + sc_mutex_unlock(&recorder->mutex); return false; } queue_push(&recorder->queue, next, rec); - cond_signal(recorder->queue_cond); + sc_cond_signal(&recorder->queue_cond); - mutex_unlock(recorder->mutex); + sc_mutex_unlock(&recorder->mutex); return true; } diff --git a/app/src/recorder.h b/app/src/recorder.h index 1e942110..be2b2dff 100644 --- a/app/src/recorder.h +++ b/app/src/recorder.h @@ -5,12 +5,11 @@ #include #include -#include -#include #include "coords.h" #include "scrcpy.h" #include "util/queue.h" +#include "util/thread.h" struct record_packet { AVPacket packet; @@ -26,9 +25,9 @@ struct recorder { struct size declared_frame_size; bool header_written; - SDL_Thread *thread; - SDL_mutex *mutex; - SDL_cond *queue_cond; + sc_thread thread; + sc_mutex mutex; + sc_cond queue_cond; bool stopped; // set on recorder_stop() by the stream reader bool failed; // set on packet write failure struct recorder_queue queue; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index f1560130..747e25a6 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -26,7 +26,6 @@ #include "stream.h" #include "tiny_xpm.h" #include "video_buffer.h" -#include "util/lock.h" #include "util/log.h" #include "util/net.h" diff --git a/app/src/screen.c b/app/src/screen.c index 5bdfac2a..0f8a2226 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -8,7 +8,6 @@ #include "scrcpy.h" #include "tiny_xpm.h" #include "video_buffer.h" -#include "util/lock.h" #include "util/log.h" #define DISPLAY_MARGINS 96 @@ -454,15 +453,15 @@ update_texture(struct screen *screen, const AVFrame *frame) { bool screen_update_frame(struct screen *screen, struct video_buffer *vb) { - mutex_lock(vb->mutex); + sc_mutex_lock(&vb->mutex); const AVFrame *frame = video_buffer_consume_rendered_frame(vb); struct size new_frame_size = {frame->width, frame->height}; if (!prepare_for_frame(screen, new_frame_size)) { - mutex_unlock(vb->mutex); + sc_mutex_unlock(&vb->mutex); return false; } update_texture(screen, frame); - mutex_unlock(vb->mutex); + sc_mutex_unlock(&vb->mutex); screen_render(screen, false); return true; diff --git a/app/src/server.c b/app/src/server.c index bb08d56e..096ac18f 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -5,12 +5,10 @@ #include #include #include -#include #include #include #include "adb.h" -#include "util/lock.h" #include "util/log.h" #include "util/net.h" #include "util/str_util.h" @@ -357,18 +355,17 @@ bool server_init(struct server *server) { server->serial = NULL; server->process = PROCESS_NONE; - server->wait_server_thread = NULL; atomic_flag_clear_explicit(&server->server_socket_closed, memory_order_relaxed); - server->mutex = SDL_CreateMutex(); - if (!server->mutex) { + bool ok = sc_mutex_init(&server->mutex); + if (!ok) { return false; } - server->process_terminated_cond = SDL_CreateCond(); - if (!server->process_terminated_cond) { - SDL_DestroyMutex(server->mutex); + ok = sc_cond_init(&server->process_terminated_cond); + if (!ok) { + sc_mutex_destroy(&server->mutex); return false; } @@ -391,10 +388,10 @@ run_wait_server(void *data) { struct server *server = data; process_wait(server->process, false); // ignore exit code - mutex_lock(server->mutex); + sc_mutex_lock(&server->mutex); server->process_terminated = true; - cond_signal(server->process_terminated_cond); - mutex_unlock(server->mutex); + sc_cond_signal(&server->process_terminated_cond); + sc_mutex_unlock(&server->mutex); // no need for synchronization, server_socket is initialized before this // thread was created @@ -439,9 +436,9 @@ server_start(struct server *server, const char *serial, // 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) { + bool ok = sc_thread_create(&server->wait_server_thread, run_wait_server, + "wait-server", server); + if (!ok) { process_terminate(server->process); process_wait(server->process, true); // ignore exit code goto error2; @@ -531,33 +528,33 @@ server_stop(struct server *server) { } // Give some delay for the server to terminate properly - mutex_lock(server->mutex); - int r = 0; + sc_mutex_lock(&server->mutex); + bool signaled = false; if (!server->process_terminated) { #define WATCHDOG_DELAY_MS 1000 - r = cond_wait_timeout(server->process_terminated_cond, - server->mutex, - WATCHDOG_DELAY_MS); + signaled = sc_cond_timedwait(&server->process_terminated_cond, + &server->mutex, + WATCHDOG_DELAY_MS); } - mutex_unlock(server->mutex); + sc_mutex_unlock(&server->mutex); // After this delay, kill the server if it's not dead already. // On some devices, closing the sockets is not sufficient to wake up the // blocking calls while the device is asleep. - if (r == SDL_MUTEX_TIMEDOUT) { + if (!signaled) { // The process is terminated, but not reaped (closed) yet, so its PID // is still valid. LOGW("Killing the server..."); process_terminate(server->process); } - SDL_WaitThread(server->wait_server_thread, NULL); + sc_thread_join(&server->wait_server_thread, NULL); process_close(server->process); } void server_destroy(struct server *server) { free(server->serial); - SDL_DestroyCond(server->process_terminated_cond); - SDL_DestroyMutex(server->mutex); + sc_cond_destroy(&server->process_terminated_cond); + sc_mutex_destroy(&server->mutex); } diff --git a/app/src/server.h b/app/src/server.h index 1ac12b5f..83c528ef 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -6,21 +6,21 @@ #include #include #include -#include #include "adb.h" #include "scrcpy.h" #include "util/log.h" #include "util/net.h" +#include "util/thread.h" struct server { char *serial; process_t process; - SDL_Thread *wait_server_thread; + sc_thread wait_server_thread; atomic_flag server_socket_closed; - SDL_mutex *mutex; - SDL_cond *process_terminated_cond; + sc_mutex mutex; + sc_cond process_terminated_cond; bool process_terminated; socket_t server_socket; // only used if !tunnel_forward diff --git a/app/src/stream.c b/app/src/stream.c index e4c9f387..ba72f164 100644 --- a/app/src/stream.c +++ b/app/src/stream.c @@ -4,8 +4,6 @@ #include #include #include -#include -#include #include #include "decoder.h" @@ -279,8 +277,8 @@ bool stream_start(struct stream *stream) { LOGD("Starting stream thread"); - stream->thread = SDL_CreateThread(run_stream, "stream", stream); - if (!stream->thread) { + bool ok = sc_thread_create(&stream->thread, run_stream, "stream", stream); + if (!ok) { LOGC("Could not start stream thread"); return false; } @@ -296,5 +294,5 @@ stream_stop(struct stream *stream) { void stream_join(struct stream *stream) { - SDL_WaitThread(stream->thread, NULL); + sc_thread_join(&stream->thread, NULL); } diff --git a/app/src/stream.h b/app/src/stream.h index d308df88..784e0402 100644 --- a/app/src/stream.h +++ b/app/src/stream.h @@ -7,15 +7,15 @@ #include #include #include -#include #include "util/net.h" +#include "util/thread.h" struct video_buffer; struct stream { socket_t socket; - SDL_Thread *thread; + sc_thread thread; struct decoder *decoder; struct recorder *recorder; AVCodecContext *codec_ctx; diff --git a/app/src/util/lock.h b/app/src/util/lock.h deleted file mode 100644 index f031bd69..00000000 --- a/app/src/util/lock.h +++ /dev/null @@ -1,75 +0,0 @@ -#ifndef LOCK_H -#define LOCK_H - -#include "common.h" - -#include -#include - -#include "log.h" - -static inline void -mutex_lock(SDL_mutex *mutex) { - int r = SDL_LockMutex(mutex); -#ifndef NDEBUG - if (r) { - LOGC("Could not lock mutex: %s", SDL_GetError()); - abort(); - } -#else - (void) r; -#endif -} - -static inline void -mutex_unlock(SDL_mutex *mutex) { - int r = SDL_UnlockMutex(mutex); -#ifndef NDEBUG - if (r) { - LOGC("Could not unlock mutex: %s", SDL_GetError()); - abort(); - } -#else - (void) r; -#endif -} - -static inline void -cond_wait(SDL_cond *cond, SDL_mutex *mutex) { - int r = SDL_CondWait(cond, mutex); -#ifndef NDEBUG - if (r) { - LOGC("Could not wait on condition: %s", SDL_GetError()); - abort(); - } -#else - (void) r; -#endif -} - -static inline int -cond_wait_timeout(SDL_cond *cond, SDL_mutex *mutex, uint32_t ms) { - int r = SDL_CondWaitTimeout(cond, mutex, ms); -#ifndef NDEBUG - if (r < 0) { - LOGC("Could not wait on condition with timeout: %s", SDL_GetError()); - abort(); - } -#endif - return r; -} - -static inline void -cond_signal(SDL_cond *cond) { - int r = SDL_CondSignal(cond); -#ifndef NDEBUG - if (r) { - LOGC("Could not signal a condition: %s", SDL_GetError()); - abort(); - } -#else - (void) r; -#endif -} - -#endif diff --git a/app/src/util/thread.c b/app/src/util/thread.c new file mode 100644 index 00000000..bb4feb62 --- /dev/null +++ b/app/src/util/thread.c @@ -0,0 +1,133 @@ +#include "thread.h" + +#include +#include + +#include "log.h" + +bool +sc_thread_create(sc_thread *thread, sc_thread_fn fn, const char *name, + void *userdata) { + SDL_Thread *sdl_thread = SDL_CreateThread(fn, name, userdata); + if (!sdl_thread) { + return false; + } + + thread->thread = sdl_thread; + return true; +} + +void +sc_thread_join(sc_thread *thread, int *status) { + SDL_WaitThread(thread->thread, status); +} + +bool +sc_mutex_init(sc_mutex *mutex) { + SDL_mutex *sdl_mutex = SDL_CreateMutex(); + if (!sdl_mutex) { + return false; + } + + mutex->mutex = sdl_mutex; + return true; +} + +void +sc_mutex_destroy(sc_mutex *mutex) { + SDL_DestroyMutex(mutex->mutex); +} + +void +sc_mutex_lock(sc_mutex *mutex) { + int r = SDL_LockMutex(mutex->mutex); +#ifndef NDEBUG + if (r) { + LOGC("Could not lock mutex: %s", SDL_GetError()); + abort(); + } +#else + (void) r; +#endif +} + +void +sc_mutex_unlock(sc_mutex *mutex) { + int r = SDL_UnlockMutex(mutex->mutex); +#ifndef NDEBUG + if (r) { + LOGC("Could not lock mutex: %s", SDL_GetError()); + abort(); + } +#else + (void) r; +#endif +} + +bool +sc_cond_init(sc_cond *cond) { + SDL_cond *sdl_cond = SDL_CreateCond(); + if (!sdl_cond) { + return false; + } + + cond->cond = sdl_cond; + return true; +} + +void +sc_cond_destroy(sc_cond *cond) { + SDL_DestroyCond(cond->cond); +} + +void +sc_cond_wait(sc_cond *cond, sc_mutex *mutex) { + int r = SDL_CondWait(cond->cond, mutex->mutex); +#ifndef NDEBUG + if (r) { + LOGC("Could not wait on condition: %s", SDL_GetError()); + abort(); + } +#else + (void) r; +#endif +} + +bool +sc_cond_timedwait(sc_cond *cond, sc_mutex *mutex, uint32_t ms) { + int r = SDL_CondWaitTimeout(cond->cond, mutex->mutex, ms); +#ifndef NDEBUG + if (r < 0) { + LOGC("Could not wait on condition with timeout: %s", SDL_GetError()); + abort(); + } +#endif + assert(r == 0 || r == SDL_MUTEX_TIMEDOUT); + return r == 0; +} + +void +sc_cond_signal(sc_cond *cond) { + int r = SDL_CondSignal(cond->cond); +#ifndef NDEBUG + if (r) { + LOGC("Could not signal a condition: %s", SDL_GetError()); + abort(); + } +#else + (void) r; +#endif +} + +void +sc_cond_broadcast(sc_cond *cond) { + int r = SDL_CondBroadcast(cond->cond); +#ifndef NDEBUG + if (r) { + LOGC("Could not broadcast a condition: %s", SDL_GetError()); + abort(); + } +#else + (void) r; +#endif +} diff --git a/app/src/util/thread.h b/app/src/util/thread.h new file mode 100644 index 00000000..5fbaf3c1 --- /dev/null +++ b/app/src/util/thread.h @@ -0,0 +1,66 @@ +#ifndef SC_THREAD_H +#define SC_THREAD_H + +#include "common.h" + +#include +#include + +/* Forward declarations */ +typedef struct SDL_Thread SDL_Thread; +typedef struct SDL_mutex SDL_mutex; +typedef struct SDL_cond SDL_cond; + +typedef int sc_thread_fn(void *); + +typedef struct sc_thread { + SDL_Thread *thread; +} sc_thread; + +typedef struct sc_mutex { + SDL_mutex *mutex; +} sc_mutex; + +typedef struct sc_cond { + SDL_cond *cond; +} sc_cond; + +bool +sc_thread_create(sc_thread *thread, sc_thread_fn fn, const char *name, + void *userdata); + +void +sc_thread_join(sc_thread *thread, int *status); + +bool +sc_mutex_init(sc_mutex *mutex); + +void +sc_mutex_destroy(sc_mutex *mutex); + +void +sc_mutex_lock(sc_mutex *mutex); + +void +sc_mutex_unlock(sc_mutex *mutex); + +bool +sc_cond_init(sc_cond *cond); + +void +sc_cond_destroy(sc_cond *cond); + +void +sc_cond_wait(sc_cond *cond, sc_mutex *mutex); + +// return true on signaled, false on timeout +bool +sc_cond_timedwait(sc_cond *cond, sc_mutex *mutex, uint32_t ms); + +void +sc_cond_signal(sc_cond *cond); + +void +sc_cond_broadcast(sc_cond *cond); + +#endif diff --git a/app/src/video_buffer.c b/app/src/video_buffer.c index d656c6f4..660c6929 100644 --- a/app/src/video_buffer.c +++ b/app/src/video_buffer.c @@ -1,11 +1,9 @@ #include "video_buffer.h" #include -#include #include #include -#include "util/lock.h" #include "util/log.h" bool @@ -13,22 +11,26 @@ video_buffer_init(struct video_buffer *vb, struct fps_counter *fps_counter, bool render_expired_frames) { vb->fps_counter = fps_counter; - if (!(vb->decoding_frame = av_frame_alloc())) { + vb->decoding_frame = av_frame_alloc(); + if (!vb->decoding_frame) { goto error_0; } - if (!(vb->rendering_frame = av_frame_alloc())) { + vb->rendering_frame = av_frame_alloc(); + if (!vb->rendering_frame) { goto error_1; } - if (!(vb->mutex = SDL_CreateMutex())) { + bool ok = sc_mutex_init(&vb->mutex); + if (!ok) { goto error_2; } vb->render_expired_frames = render_expired_frames; if (render_expired_frames) { - if (!(vb->rendering_frame_consumed_cond = SDL_CreateCond())) { - SDL_DestroyMutex(vb->mutex); + ok = sc_cond_init(&vb->rendering_frame_consumed_cond); + if (!ok) { + sc_mutex_destroy(&vb->mutex); goto error_2; } // interrupted is not used if expired frames are not rendered @@ -53,9 +55,9 @@ error_0: void video_buffer_destroy(struct video_buffer *vb) { if (vb->render_expired_frames) { - SDL_DestroyCond(vb->rendering_frame_consumed_cond); + sc_cond_destroy(&vb->rendering_frame_consumed_cond); } - SDL_DestroyMutex(vb->mutex); + sc_mutex_destroy(&vb->mutex); av_frame_free(&vb->rendering_frame); av_frame_free(&vb->decoding_frame); } @@ -70,11 +72,11 @@ video_buffer_swap_frames(struct video_buffer *vb) { void video_buffer_offer_decoded_frame(struct video_buffer *vb, bool *previous_frame_skipped) { - mutex_lock(vb->mutex); + sc_mutex_lock(&vb->mutex); if (vb->render_expired_frames) { // wait for the current (expired) frame to be consumed while (!vb->rendering_frame_consumed && !vb->interrupted) { - cond_wait(vb->rendering_frame_consumed_cond, vb->mutex); + sc_cond_wait(&vb->rendering_frame_consumed_cond, &vb->mutex); } } else if (!vb->rendering_frame_consumed) { fps_counter_add_skipped_frame(vb->fps_counter); @@ -85,7 +87,7 @@ video_buffer_offer_decoded_frame(struct video_buffer *vb, *previous_frame_skipped = !vb->rendering_frame_consumed; vb->rendering_frame_consumed = false; - mutex_unlock(vb->mutex); + sc_mutex_unlock(&vb->mutex); } const AVFrame * @@ -95,7 +97,7 @@ video_buffer_consume_rendered_frame(struct video_buffer *vb) { fps_counter_add_rendered_frame(vb->fps_counter); if (vb->render_expired_frames) { // unblock video_buffer_offer_decoded_frame() - cond_signal(vb->rendering_frame_consumed_cond); + sc_cond_signal(&vb->rendering_frame_consumed_cond); } return vb->rendering_frame; } @@ -103,10 +105,10 @@ video_buffer_consume_rendered_frame(struct video_buffer *vb) { void video_buffer_interrupt(struct video_buffer *vb) { if (vb->render_expired_frames) { - mutex_lock(vb->mutex); + sc_mutex_lock(&vb->mutex); vb->interrupted = true; - mutex_unlock(vb->mutex); + sc_mutex_unlock(&vb->mutex); // wake up blocking wait - cond_signal(vb->rendering_frame_consumed_cond); + sc_cond_signal(&vb->rendering_frame_consumed_cond); } } diff --git a/app/src/video_buffer.h b/app/src/video_buffer.h index 68ef8e04..fbab046b 100644 --- a/app/src/video_buffer.h +++ b/app/src/video_buffer.h @@ -4,9 +4,9 @@ #include "common.h" #include -#include #include "fps_counter.h" +#include "util/thread.h" // forward declarations typedef struct AVFrame AVFrame; @@ -14,10 +14,10 @@ typedef struct AVFrame AVFrame; struct video_buffer { AVFrame *decoding_frame; AVFrame *rendering_frame; - SDL_mutex *mutex; + sc_mutex mutex; bool render_expired_frames; bool interrupted; - SDL_cond *rendering_frame_consumed_cond; + sc_cond rendering_frame_consumed_cond; bool rendering_frame_consumed; struct fps_counter *fps_counter; }; @@ -30,16 +30,16 @@ void video_buffer_destroy(struct video_buffer *vb); // set the decoded frame as ready for rendering -// this function locks frames->mutex during its execution +// this function locks vb->mutex during its execution // the output flag is set to report whether the previous frame has been skipped void video_buffer_offer_decoded_frame(struct video_buffer *vb, bool *previous_frame_skipped); // mark the rendering frame as consumed and return it -// MUST be called with frames->mutex locked!!! +// MUST be called with vb->mutex locked!!! // the caller is expected to render the returned frame to some texture before -// unlocking frames->mutex +// unlocking vb->mutex const AVFrame * video_buffer_consume_rendered_frame(struct video_buffer *vb); From d2689fc1683a18bfc9e0af9b5dcbe623a28b6e02 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 31 Jan 2021 18:54:52 +0100 Subject: [PATCH 0470/2244] Expose thread id --- app/src/util/thread.c | 5 +++++ app/src/util/thread.h | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/app/src/util/thread.c b/app/src/util/thread.c index bb4feb62..1fc6bb19 100644 --- a/app/src/util/thread.c +++ b/app/src/util/thread.c @@ -64,6 +64,11 @@ sc_mutex_unlock(sc_mutex *mutex) { #endif } +sc_thread_id +sc_thread_get_id(void) { + return SDL_ThreadID(); +} + bool sc_cond_init(sc_cond *cond) { SDL_cond *sdl_cond = SDL_CreateCond(); diff --git a/app/src/util/thread.h b/app/src/util/thread.h index 5fbaf3c1..85a2aca0 100644 --- a/app/src/util/thread.h +++ b/app/src/util/thread.h @@ -12,6 +12,7 @@ typedef struct SDL_mutex SDL_mutex; typedef struct SDL_cond SDL_cond; typedef int sc_thread_fn(void *); +typedef unsigned int sc_thread_id; typedef struct sc_thread { SDL_Thread *thread; @@ -44,6 +45,9 @@ sc_mutex_lock(sc_mutex *mutex); void sc_mutex_unlock(sc_mutex *mutex); +sc_thread_id +sc_thread_get_id(void); + bool sc_cond_init(sc_cond *cond); From 21d206f360535047e16cc4ea7de889a55bd9444d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 31 Jan 2021 18:55:03 +0100 Subject: [PATCH 0471/2244] Expose mutex assertions Add a function to assert that the mutex is held (or not). --- app/src/util/thread.c | 19 +++++++++++++++++++ app/src/util/thread.h | 11 +++++++++++ 2 files changed, 30 insertions(+) diff --git a/app/src/util/thread.c b/app/src/util/thread.c index 1fc6bb19..fa774cfe 100644 --- a/app/src/util/thread.c +++ b/app/src/util/thread.c @@ -30,6 +30,9 @@ sc_mutex_init(sc_mutex *mutex) { } mutex->mutex = sdl_mutex; +#ifndef NDEBUG + mutex->locker = 0; +#endif return true; } @@ -46,6 +49,8 @@ sc_mutex_lock(sc_mutex *mutex) { LOGC("Could not lock mutex: %s", SDL_GetError()); abort(); } + + mutex->locker = sc_thread_get_id(); #else (void) r; #endif @@ -53,6 +58,9 @@ sc_mutex_lock(sc_mutex *mutex) { void sc_mutex_unlock(sc_mutex *mutex) { +#ifndef NDEBUG + mutex->locker = 0; +#endif int r = SDL_UnlockMutex(mutex->mutex); #ifndef NDEBUG if (r) { @@ -69,6 +77,13 @@ sc_thread_get_id(void) { return SDL_ThreadID(); } +#ifndef NDEBUG +bool +sc_mutex_held(struct sc_mutex *mutex) { + return mutex->locker == sc_thread_get_id(); +} +#endif + bool sc_cond_init(sc_cond *cond) { SDL_cond *sdl_cond = SDL_CreateCond(); @@ -93,6 +108,8 @@ sc_cond_wait(sc_cond *cond, sc_mutex *mutex) { LOGC("Could not wait on condition: %s", SDL_GetError()); abort(); } + + mutex->locker = sc_thread_get_id(); #else (void) r; #endif @@ -106,6 +123,8 @@ sc_cond_timedwait(sc_cond *cond, sc_mutex *mutex, uint32_t ms) { LOGC("Could not wait on condition with timeout: %s", SDL_GetError()); abort(); } + + mutex->locker = sc_thread_get_id(); #endif assert(r == 0 || r == SDL_MUTEX_TIMEDOUT); return r == 0; diff --git a/app/src/util/thread.h b/app/src/util/thread.h index 85a2aca0..d23e1432 100644 --- a/app/src/util/thread.h +++ b/app/src/util/thread.h @@ -20,6 +20,9 @@ typedef struct sc_thread { typedef struct sc_mutex { SDL_mutex *mutex; +#ifndef NDEBUG + sc_thread_id locker; +#endif } sc_mutex; typedef struct sc_cond { @@ -48,6 +51,14 @@ sc_mutex_unlock(sc_mutex *mutex); sc_thread_id sc_thread_get_id(void); +#ifndef NDEBUG +bool +sc_mutex_held(struct sc_mutex *mutex); +# define sc_mutex_assert(mutex) assert(sc_mutex_held(mutex)) +#else +# define sc_mutex_assert(mutex) +#endif + bool sc_cond_init(sc_cond *cond); From 54f5c42d7b0e8b9778f9a6423a8cfe174ae06d08 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 31 Jan 2021 18:56:50 +0100 Subject: [PATCH 0472/2244] Add mutex assertions --- app/src/video_buffer.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/video_buffer.c b/app/src/video_buffer.c index 660c6929..95cfd755 100644 --- a/app/src/video_buffer.c +++ b/app/src/video_buffer.c @@ -64,6 +64,7 @@ video_buffer_destroy(struct video_buffer *vb) { static void video_buffer_swap_frames(struct video_buffer *vb) { + sc_mutex_assert(&vb->mutex); AVFrame *tmp = vb->decoding_frame; vb->decoding_frame = vb->rendering_frame; vb->rendering_frame = tmp; @@ -92,6 +93,7 @@ video_buffer_offer_decoded_frame(struct video_buffer *vb, const AVFrame * video_buffer_consume_rendered_frame(struct video_buffer *vb) { + sc_mutex_assert(&vb->mutex); assert(!vb->rendering_frame_consumed); vb->rendering_frame_consumed = true; fps_counter_add_rendered_frame(vb->fps_counter); From c53bd4d8b68d63ed7e6c4cb20e81fd0d7bb9c0ec Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 1 Feb 2021 22:20:02 +0100 Subject: [PATCH 0473/2244] Assert non-recursive usage of mutexes --- app/src/util/thread.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/util/thread.c b/app/src/util/thread.c index fa774cfe..a0a99f20 100644 --- a/app/src/util/thread.c +++ b/app/src/util/thread.c @@ -43,6 +43,8 @@ sc_mutex_destroy(sc_mutex *mutex) { void sc_mutex_lock(sc_mutex *mutex) { + // SDL mutexes are recursive, but we don't want to use recursive mutexes + assert(!sc_mutex_held(mutex)); int r = SDL_LockMutex(mutex->mutex); #ifndef NDEBUG if (r) { @@ -59,6 +61,7 @@ sc_mutex_lock(sc_mutex *mutex) { void sc_mutex_unlock(sc_mutex *mutex) { #ifndef NDEBUG + assert(sc_mutex_held(mutex)); mutex->locker = 0; #endif int r = SDL_UnlockMutex(mutex->mutex); From c0c4ba700904d54f087f6cc5a2e5821124ff42db Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 1 Feb 2021 09:38:11 +0100 Subject: [PATCH 0474/2244] Add intermediate frame in video buffer There were only two frames simultaneously: - one used by the decoder; - one used by the renderer. When the decoder finished decoding a frame, it swapped it with the rendering frame. Adding a third frame provides several benefits: - the decoder do not have to wait for the renderer to release the mutex; - it simplifies the video_buffer API; - it makes the rendering frame valid until the next call to video_buffer_take_rendering_frame(), which will be useful for swscaling on window resize. --- app/src/screen.c | 5 +--- app/src/video_buffer.c | 65 ++++++++++++++++++++++++++++-------------- app/src/video_buffer.h | 32 ++++++++++++++++----- 3 files changed, 70 insertions(+), 32 deletions(-) diff --git a/app/src/screen.c b/app/src/screen.c index 0f8a2226..5ec416f9 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -453,15 +453,12 @@ update_texture(struct screen *screen, const AVFrame *frame) { bool screen_update_frame(struct screen *screen, struct video_buffer *vb) { - sc_mutex_lock(&vb->mutex); - const AVFrame *frame = video_buffer_consume_rendered_frame(vb); + const AVFrame *frame = video_buffer_take_rendering_frame(vb); struct size new_frame_size = {frame->width, frame->height}; if (!prepare_for_frame(screen, new_frame_size)) { - sc_mutex_unlock(&vb->mutex); return false; } update_texture(screen, frame); - sc_mutex_unlock(&vb->mutex); screen_render(screen, false); return true; diff --git a/app/src/video_buffer.c b/app/src/video_buffer.c index 95cfd755..4c8dd5da 100644 --- a/app/src/video_buffer.c +++ b/app/src/video_buffer.c @@ -16,19 +16,24 @@ video_buffer_init(struct video_buffer *vb, struct fps_counter *fps_counter, goto error_0; } + vb->pending_frame = av_frame_alloc(); + if (!vb->pending_frame) { + goto error_1; + } + vb->rendering_frame = av_frame_alloc(); if (!vb->rendering_frame) { - goto error_1; + goto error_2; } bool ok = sc_mutex_init(&vb->mutex); if (!ok) { - goto error_2; + goto error_3; } vb->render_expired_frames = render_expired_frames; if (render_expired_frames) { - ok = sc_cond_init(&vb->rendering_frame_consumed_cond); + ok = sc_cond_init(&vb->pending_frame_consumed_cond); if (!ok) { sc_mutex_destroy(&vb->mutex); goto error_2; @@ -40,12 +45,14 @@ video_buffer_init(struct video_buffer *vb, struct fps_counter *fps_counter, // there is initially no rendering frame, so consider it has already been // consumed - vb->rendering_frame_consumed = true; + vb->pending_frame_consumed = true; return true; -error_2: +error_3: av_frame_free(&vb->rendering_frame); +error_2: + av_frame_free(&vb->pending_frame); error_1: av_frame_free(&vb->decoding_frame); error_0: @@ -55,19 +62,28 @@ error_0: void video_buffer_destroy(struct video_buffer *vb) { if (vb->render_expired_frames) { - sc_cond_destroy(&vb->rendering_frame_consumed_cond); + sc_cond_destroy(&vb->pending_frame_consumed_cond); } sc_mutex_destroy(&vb->mutex); av_frame_free(&vb->rendering_frame); + av_frame_free(&vb->pending_frame); av_frame_free(&vb->decoding_frame); } static void -video_buffer_swap_frames(struct video_buffer *vb) { +video_buffer_swap_decoding_frame(struct video_buffer *vb) { sc_mutex_assert(&vb->mutex); AVFrame *tmp = vb->decoding_frame; - vb->decoding_frame = vb->rendering_frame; - vb->rendering_frame = tmp; + vb->decoding_frame = vb->pending_frame; + vb->pending_frame = tmp; +} + +static void +video_buffer_swap_rendering_frame(struct video_buffer *vb) { + sc_mutex_assert(&vb->mutex); + AVFrame *tmp = vb->rendering_frame; + vb->rendering_frame = vb->pending_frame; + vb->pending_frame = tmp; } void @@ -76,31 +92,38 @@ video_buffer_offer_decoded_frame(struct video_buffer *vb, sc_mutex_lock(&vb->mutex); if (vb->render_expired_frames) { // wait for the current (expired) frame to be consumed - while (!vb->rendering_frame_consumed && !vb->interrupted) { - sc_cond_wait(&vb->rendering_frame_consumed_cond, &vb->mutex); + while (!vb->pending_frame_consumed && !vb->interrupted) { + sc_cond_wait(&vb->pending_frame_consumed_cond, &vb->mutex); } - } else if (!vb->rendering_frame_consumed) { + } else if (!vb->pending_frame_consumed) { fps_counter_add_skipped_frame(vb->fps_counter); } - video_buffer_swap_frames(vb); + video_buffer_swap_decoding_frame(vb); - *previous_frame_skipped = !vb->rendering_frame_consumed; - vb->rendering_frame_consumed = false; + *previous_frame_skipped = !vb->pending_frame_consumed; + vb->pending_frame_consumed = false; sc_mutex_unlock(&vb->mutex); } const AVFrame * -video_buffer_consume_rendered_frame(struct video_buffer *vb) { - sc_mutex_assert(&vb->mutex); - assert(!vb->rendering_frame_consumed); - vb->rendering_frame_consumed = true; +video_buffer_take_rendering_frame(struct video_buffer *vb) { + sc_mutex_lock(&vb->mutex); + assert(!vb->pending_frame_consumed); + vb->pending_frame_consumed = true; + fps_counter_add_rendered_frame(vb->fps_counter); + + video_buffer_swap_rendering_frame(vb); + if (vb->render_expired_frames) { // unblock video_buffer_offer_decoded_frame() - sc_cond_signal(&vb->rendering_frame_consumed_cond); + sc_cond_signal(&vb->pending_frame_consumed_cond); } + sc_mutex_unlock(&vb->mutex); + + // rendering_frame is only written from this thread, no need to lock return vb->rendering_frame; } @@ -111,6 +134,6 @@ video_buffer_interrupt(struct video_buffer *vb) { vb->interrupted = true; sc_mutex_unlock(&vb->mutex); // wake up blocking wait - sc_cond_signal(&vb->rendering_frame_consumed_cond); + sc_cond_signal(&vb->pending_frame_consumed_cond); } } diff --git a/app/src/video_buffer.h b/app/src/video_buffer.h index fbab046b..3e52caa7 100644 --- a/app/src/video_buffer.h +++ b/app/src/video_buffer.h @@ -11,14 +11,35 @@ // forward declarations typedef struct AVFrame AVFrame; +/** + * There are 3 frames in memory: + * - one frame is held by the decoder (decoding_frame) + * - one frame is held by the renderer (rendering_frame) + * - one frame is shared between the decoder and the renderer (pending_frame) + * + * The decoder decodes a packet into the decoding_frame (it may takes time). + * + * Once the frame is decoded, it calls video_buffer_offer_decoded_frame(), + * which swaps the decoding and pending frames. + * + * When the renderer is notified that a new frame is available, it calls + * video_buffer_take_rendering_frame() to retrieve it, which swaps the pending + * and rendering frames. The frame is valid until the next call, without + * blocking the decoder. + */ + struct video_buffer { AVFrame *decoding_frame; + AVFrame *pending_frame; AVFrame *rendering_frame; + sc_mutex mutex; bool render_expired_frames; bool interrupted; - sc_cond rendering_frame_consumed_cond; - bool rendering_frame_consumed; + + sc_cond pending_frame_consumed_cond; + bool pending_frame_consumed; + struct fps_counter *fps_counter; }; @@ -30,18 +51,15 @@ void video_buffer_destroy(struct video_buffer *vb); // set the decoded frame as ready for rendering -// this function locks vb->mutex during its execution // the output flag is set to report whether the previous frame has been skipped void video_buffer_offer_decoded_frame(struct video_buffer *vb, bool *previous_frame_skipped); // mark the rendering frame as consumed and return it -// MUST be called with vb->mutex locked!!! -// the caller is expected to render the returned frame to some texture before -// unlocking vb->mutex +// the frame is valid until the next call to this function const AVFrame * -video_buffer_consume_rendered_frame(struct video_buffer *vb); +video_buffer_take_rendering_frame(struct video_buffer *vb); // wake up and avoid any blocking call void From 862948b13267fa1516840a464c59a8566d9ac28a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 7 Feb 2021 19:12:14 +0100 Subject: [PATCH 0475/2244] Make use_opengl local The flag is used only locally, there is no need to store it in the screen structure. --- app/src/screen.c | 5 ++--- app/src/screen.h | 2 -- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/app/src/screen.c b/app/src/screen.c index 5ec416f9..fb2eb6cd 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -280,8 +280,8 @@ screen_init_rendering(struct screen *screen, const char *window_title, LOGI("Renderer: %s", renderer_name ? renderer_name : "(unknown)"); // starts with "opengl" - screen->use_opengl = renderer_name && !strncmp(renderer_name, "opengl", 6); - if (screen->use_opengl) { + bool use_opengl = renderer_name && !strncmp(renderer_name, "opengl", 6); + if (use_opengl) { struct sc_opengl *gl = &screen->gl; sc_opengl_init(gl); @@ -444,7 +444,6 @@ update_texture(struct screen *screen, const AVFrame *frame) { 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); diff --git a/app/src/screen.h b/app/src/screen.h index ea94d538..35d5df50 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -16,7 +16,6 @@ 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 @@ -41,7 +40,6 @@ struct screen { .window = NULL, \ .renderer = NULL, \ .texture = NULL, \ - .use_opengl = false, \ .gl = {0}, \ .frame_size = { \ .width = 0, \ From a566635c43ea7b8d6c60a9cad9b63dca8ac3d078 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 13 Feb 2021 14:32:24 +0100 Subject: [PATCH 0476/2244] Log mipmaps error only if mipmaps are enabled --- 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 fb2eb6cd..1136d547 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -301,7 +301,7 @@ screen_init_rendering(struct screen *screen, const char *window_title, } else { LOGI("Trilinear filtering disabled"); } - } else { + } else if (mipmaps) { LOGD("Trilinear filtering disabled (not an OpenGL renderer)"); } From 626094ad130225df0bcd319656c97fe44c7024e7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 12 Feb 2021 23:18:13 +0100 Subject: [PATCH 0477/2244] Handle window events only once visible This will avoid corner cases where we need to resize while no frame has been received yet. --- app/src/scrcpy.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 747e25a6..505b5eaf 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -184,7 +184,9 @@ handle_event(SDL_Event *event, const struct scrcpy_options *options) { } break; case SDL_WINDOWEVENT: - screen_handle_window_event(&screen, &event->window); + if (screen.has_frame) { + screen_handle_window_event(&screen, &event->window); + } break; case SDL_TEXTINPUT: if (!options->control) { From 0538e9645b3aebddbe9d579cbc5c4ca8f64c3659 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 13 Feb 2021 14:40:00 +0100 Subject: [PATCH 0478/2244] Improve error handling in screen initialization After the struct screen is initialized, the window, the renderer and the texture are necessarily valid, so there is no need to check in screen_destroy(). --- app/src/screen.c | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/app/src/screen.c b/app/src/screen.c index 1136d547..959af9c7 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -270,7 +270,7 @@ screen_init_rendering(struct screen *screen, const char *window_title, SDL_RENDERER_ACCELERATED); if (!screen->renderer) { LOGC("Could not create renderer: %s", SDL_GetError()); - screen_destroy(screen); + SDL_DestroyWindow(screen->window); return false; } @@ -318,7 +318,8 @@ screen_init_rendering(struct screen *screen, const char *window_title, screen->texture = create_texture(screen); if (!screen->texture) { LOGC("Could not create texture: %s", SDL_GetError()); - screen_destroy(screen); + SDL_DestroyRenderer(screen->renderer); + SDL_DestroyWindow(screen->window); return false; } @@ -339,15 +340,9 @@ screen_show_window(struct screen *screen) { void screen_destroy(struct screen *screen) { - if (screen->texture) { - SDL_DestroyTexture(screen->texture); - } - if (screen->renderer) { - SDL_DestroyRenderer(screen->renderer); - } - if (screen->window) { - SDL_DestroyWindow(screen->window); - } + SDL_DestroyTexture(screen->texture); + SDL_DestroyRenderer(screen->renderer); + SDL_DestroyWindow(screen->window); } static void From ea2369f568ccb2dd8fceb83487171eb0ffba26c9 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 15 Feb 2021 18:28:41 +0100 Subject: [PATCH 0479/2244] Reference video buffer from screen This paves the way to handle EVENT_NEW_FRAME from screen.c, by allowing to call screen_update_frame() without an explicit video_buffer instance. --- app/src/scrcpy.c | 6 ++++-- app/src/screen.c | 7 ++++--- app/src/screen.h | 6 ++++-- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 505b5eaf..f669ef40 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -30,7 +30,7 @@ #include "util/net.h" static struct server server; -static struct screen screen = SCREEN_INITIALIZER; +static struct screen screen; static struct fps_counter fps_counter; static struct video_buffer video_buffer; static struct stream stream; @@ -179,7 +179,7 @@ handle_event(SDL_Event *event, const struct scrcpy_options *options) { // this is the very first frame, show the window screen_show_window(&screen); } - if (!screen_update_frame(&screen, &video_buffer)) { + if (!screen_update_frame(&screen)) { return EVENT_RESULT_CONTINUE; } break; @@ -429,6 +429,8 @@ scrcpy(const struct scrcpy_options *options) { const char *window_title = options->window_title ? options->window_title : device_name; + screen_init(&screen, &video_buffer); + if (!screen_init_rendering(&screen, window_title, frame_size, options->always_on_top, options->window_x, options->window_y, options->window_width, diff --git a/app/src/screen.c b/app/src/screen.c index 959af9c7..4d01ba7e 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -191,8 +191,9 @@ screen_update_content_rect(struct screen *screen) { } void -screen_init(struct screen *screen) { +screen_init(struct screen *screen, struct video_buffer *vb) { *screen = (struct screen) SCREEN_INITIALIZER; + screen->vb = vb; } static inline SDL_Texture * @@ -446,8 +447,8 @@ update_texture(struct screen *screen, const AVFrame *frame) { } bool -screen_update_frame(struct screen *screen, struct video_buffer *vb) { - const AVFrame *frame = video_buffer_take_rendering_frame(vb); +screen_update_frame(struct screen *screen) { + const AVFrame *frame = video_buffer_take_rendering_frame(screen->vb); struct size new_frame_size = {frame->width, frame->height}; if (!prepare_for_frame(screen, new_frame_size)) { return false; diff --git a/app/src/screen.h b/app/src/screen.h index 35d5df50..6db52dec 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -13,6 +13,7 @@ struct video_buffer; struct screen { + struct video_buffer *vb; SDL_Window *window; SDL_Renderer *renderer; SDL_Texture *texture; @@ -37,6 +38,7 @@ struct screen { }; #define SCREEN_INITIALIZER { \ + .vb = NULL, \ .window = NULL, \ .renderer = NULL, \ .texture = NULL, \ @@ -70,7 +72,7 @@ struct screen { // initialize default values void -screen_init(struct screen *screen); +screen_init(struct screen *screen, struct video_buffer *vb); // initialize screen, create window, renderer and texture (window is hidden) // window_x and window_y accept SC_WINDOW_POSITION_UNDEFINED @@ -91,7 +93,7 @@ screen_destroy(struct screen *screen); // resize if necessary and write the rendered frame into the texture bool -screen_update_frame(struct screen *screen, struct video_buffer *vb); +screen_update_frame(struct screen *screen); // render the texture to the renderer // From 50b4a730e31e220a465e3b72bd8a5a10f0c6ef08 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 15 Feb 2021 18:44:53 +0100 Subject: [PATCH 0480/2244] Handle screen-related events from screen.c --- app/src/scrcpy.c | 19 ++++--------------- app/src/screen.c | 32 ++++++++++++++++++++++++++++++-- app/src/screen.h | 10 +++------- 3 files changed, 37 insertions(+), 24 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index f669ef40..a150c3dd 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -173,21 +173,6 @@ handle_event(SDL_Event *event, const struct scrcpy_options *options) { case SDL_QUIT: LOGD("User requested to quit"); return EVENT_RESULT_STOPPED_BY_USER; - case EVENT_NEW_FRAME: - if (!screen.has_frame) { - screen.has_frame = true; - // this is the very first frame, show the window - screen_show_window(&screen); - } - if (!screen_update_frame(&screen)) { - return EVENT_RESULT_CONTINUE; - } - break; - case SDL_WINDOWEVENT: - if (screen.has_frame) { - screen_handle_window_event(&screen, &event->window); - } - break; case SDL_TEXTINPUT: if (!options->control) { break; @@ -244,6 +229,10 @@ handle_event(SDL_Event *event, const struct scrcpy_options *options) { break; } } + + bool consumed = screen_handle_event(&screen, event); + (void) consumed; + return EVENT_RESULT_CONTINUE; } diff --git a/app/src/screen.c b/app/src/screen.c index 4d01ba7e..0cc90ad8 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -4,6 +4,7 @@ #include #include +#include "events.h" #include "icon.xpm" #include "scrcpy.h" #include "tiny_xpm.h" @@ -446,7 +447,7 @@ update_texture(struct screen *screen, const AVFrame *frame) { } } -bool +static bool screen_update_frame(struct screen *screen) { const AVFrame *frame = video_buffer_take_rendering_frame(screen->vb); struct size new_frame_size = {frame->width, frame->height}; @@ -540,7 +541,7 @@ screen_resize_to_pixel_perfect(struct screen *screen) { content_size.height); } -void +static void screen_handle_window_event(struct screen *screen, const SDL_WindowEvent *event) { switch (event->event) { @@ -567,6 +568,33 @@ screen_handle_window_event(struct screen *screen, } } + +bool +screen_handle_event(struct screen *screen, SDL_Event *event) { + switch (event->type) { + case EVENT_NEW_FRAME: + if (!screen->has_frame) { + screen->has_frame = true; + // this is the very first frame, show the window + screen_show_window(screen); + } + bool ok = screen_update_frame(screen); + if (!ok) { + LOGW("Frame update failed\n"); + } + return true; + case SDL_WINDOWEVENT: + if (!screen->has_frame) { + // Do nothing + return true; + } + screen_handle_window_event(screen, &event->window); + return true; + } + + return false; +} + struct point screen_convert_drawable_to_frame_coords(struct screen *screen, int32_t x, int32_t y) { diff --git a/app/src/screen.h b/app/src/screen.h index 6db52dec..171717ab 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -91,10 +91,6 @@ screen_show_window(struct screen *screen); void screen_destroy(struct screen *screen); -// resize if necessary and write the rendered frame into the texture -bool -screen_update_frame(struct screen *screen); - // render the texture to the renderer // // Set the update_content_rect flag if the window or content size may have @@ -118,9 +114,9 @@ screen_resize_to_pixel_perfect(struct screen *screen); 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); +// react to SDL events +bool +screen_handle_event(struct screen *screen, SDL_Event *event); // convert point from window coordinates to frame coordinates // x and y are expressed in pixels From 76a3d9805bf44f1d73fc47605b26f031c23773fa Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 15 Feb 2021 18:47:17 +0100 Subject: [PATCH 0481/2244] Inline window events handling Now that all screen-related events are handled from screen.c, there is no need for a separate method for window events. --- app/src/screen.c | 52 +++++++++++++++++++++--------------------------- 1 file changed, 23 insertions(+), 29 deletions(-) diff --git a/app/src/screen.c b/app/src/screen.c index 0cc90ad8..4b0321eb 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -541,34 +541,6 @@ screen_resize_to_pixel_perfect(struct screen *screen) { content_size.height); } -static void -screen_handle_window_event(struct screen *screen, - const SDL_WindowEvent *event) { - switch (event->event) { - case SDL_WINDOWEVENT_EXPOSED: - screen_render(screen, true); - break; - case SDL_WINDOWEVENT_SIZE_CHANGED: - screen_render(screen, true); - break; - case SDL_WINDOWEVENT_MAXIMIZED: - 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; - } -} - - bool screen_handle_event(struct screen *screen, SDL_Event *event) { switch (event->type) { @@ -588,7 +560,29 @@ screen_handle_event(struct screen *screen, SDL_Event *event) { // Do nothing return true; } - screen_handle_window_event(screen, &event->window); + switch (event->window.event) { + case SDL_WINDOWEVENT_EXPOSED: + screen_render(screen, true); + break; + case SDL_WINDOWEVENT_SIZE_CHANGED: + screen_render(screen, true); + break; + case SDL_WINDOWEVENT_MAXIMIZED: + 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; + } return true; } From 24b637b9728818755f2db84f18025261047d213a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 15 Feb 2021 18:53:23 +0100 Subject: [PATCH 0482/2244] Handle im-related events from input_manager.c --- app/src/input_manager.c | 55 ++++++++++++++++++++++++++++++++++++----- app/src/input_manager.h | 25 ++----------------- app/src/scrcpy.c | 41 +++++------------------------- 3 files changed, 57 insertions(+), 64 deletions(-) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index fd780ae6..20ea05c5 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -286,7 +286,7 @@ rotate_client_right(struct screen *screen) { screen_set_rotation(screen, new_rotation); } -void +static void input_manager_process_text_input(struct input_manager *im, const SDL_TextInputEvent *event) { if (is_shortcut_mod(im, SDL_GetModState())) { @@ -366,7 +366,7 @@ convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to, return true; } -void +static void input_manager_process_key(struct input_manager *im, const SDL_KeyboardEvent *event) { // control: indicates the state of the command-line option --no-control @@ -551,7 +551,7 @@ convert_mouse_motion(const SDL_MouseMotionEvent *from, struct screen *screen, return true; } -void +static void input_manager_process_mouse_motion(struct input_manager *im, const SDL_MouseMotionEvent *event) { if (!event->state) { @@ -605,7 +605,7 @@ convert_touch(const SDL_TouchFingerEvent *from, struct screen *screen, return true; } -void +static void input_manager_process_touch(struct input_manager *im, const SDL_TouchFingerEvent *event) { struct control_msg msg; @@ -637,7 +637,7 @@ convert_mouse_button(const SDL_MouseButtonEvent *from, struct screen *screen, return true; } -void +static void input_manager_process_mouse_button(struct input_manager *im, const SDL_MouseButtonEvent *event) { bool control = im->control; @@ -736,7 +736,7 @@ convert_mouse_wheel(const SDL_MouseWheelEvent *from, struct screen *screen, return true; } -void +static void input_manager_process_mouse_wheel(struct input_manager *im, const SDL_MouseWheelEvent *event) { struct control_msg msg; @@ -746,3 +746,46 @@ input_manager_process_mouse_wheel(struct input_manager *im, } } } + +bool +input_manager_handle_event(struct input_manager *im, SDL_Event *event) { + switch (event->type) { + case SDL_TEXTINPUT: + if (!im->control) { + return true; + } + input_manager_process_text_input(im, &event->text); + return true; + case SDL_KEYDOWN: + 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(im, &event->key); + return true; + case SDL_MOUSEMOTION: + if (!im->control) { + break; + } + input_manager_process_mouse_motion(im, &event->motion); + return true; + case SDL_MOUSEWHEEL: + if (!im->control) { + break; + } + input_manager_process_mouse_wheel(im, &event->wheel); + return true; + case SDL_MOUSEBUTTONDOWN: + 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(im, &event->button); + return true; + case SDL_FINGERMOTION: + case SDL_FINGERDOWN: + case SDL_FINGERUP: + input_manager_process_touch(im, &event->tfinger); + return true; + } + + return false; +} diff --git a/app/src/input_manager.h b/app/src/input_manager.h index a23a731d..6acb354d 100644 --- a/app/src/input_manager.h +++ b/app/src/input_manager.h @@ -40,28 +40,7 @@ void input_manager_init(struct input_manager *im, const struct scrcpy_options *options); -void -input_manager_process_text_input(struct input_manager *im, - const SDL_TextInputEvent *event); - -void -input_manager_process_key(struct input_manager *im, - const SDL_KeyboardEvent *event); - -void -input_manager_process_mouse_motion(struct input_manager *im, - const SDL_MouseMotionEvent *event); - -void -input_manager_process_touch(struct input_manager *im, - const SDL_TouchFingerEvent *event); - -void -input_manager_process_mouse_button(struct input_manager *im, - const SDL_MouseButtonEvent *event); - -void -input_manager_process_mouse_wheel(struct input_manager *im, - const SDL_MouseWheelEvent *event); +bool +input_manager_handle_event(struct input_manager *im, SDL_Event *event); #endif diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index a150c3dd..d24bba2c 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -173,41 +173,6 @@ handle_event(SDL_Event *event, const struct scrcpy_options *options) { case SDL_QUIT: LOGD("User requested to quit"); return EVENT_RESULT_STOPPED_BY_USER; - case SDL_TEXTINPUT: - if (!options->control) { - break; - } - input_manager_process_text_input(&input_manager, &event->text); - break; - case SDL_KEYDOWN: - 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); - break; - case SDL_MOUSEMOTION: - if (!options->control) { - break; - } - input_manager_process_mouse_motion(&input_manager, &event->motion); - break; - case SDL_MOUSEWHEEL: - if (!options->control) { - break; - } - input_manager_process_mouse_wheel(&input_manager, &event->wheel); - break; - case SDL_MOUSEBUTTONDOWN: - 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); - break; - case SDL_FINGERMOTION: - case SDL_FINGERDOWN: - case SDL_FINGERUP: - input_manager_process_touch(&input_manager, &event->tfinger); - break; case SDL_DROPFILE: { if (!options->control) { break; @@ -231,8 +196,14 @@ handle_event(SDL_Event *event, const struct scrcpy_options *options) { } bool consumed = screen_handle_event(&screen, event); + if (consumed) { + goto end; + } + + consumed = input_manager_handle_event(&input_manager, event); (void) consumed; +end: return EVENT_RESULT_CONTINUE; } From 9cd1a7380db1760807f5ea8077d315e678dabaf2 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 16 Feb 2021 23:26:35 +0100 Subject: [PATCH 0483/2244] Enable NDEBUG via Meson built-in option --- app/meson.build | 3 --- meson.build | 1 + 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/app/meson.build b/app/meson.build index 1f50209e..75a2fd71 100644 --- a/app/meson.build +++ b/app/meson.build @@ -99,9 +99,6 @@ foreach f : check_functions endif endforeach -# expose the build type -conf.set('NDEBUG', get_option('buildtype') != 'debug') - # the version, updated on release conf.set_quoted('SCRCPY_VERSION', meson.project_version()) diff --git a/meson.build b/meson.build index 230f8d21..c2989ec7 100644 --- a/meson.build +++ b/meson.build @@ -4,6 +4,7 @@ project('scrcpy', 'c', default_options: [ 'c_std=c11', 'warning_level=2', + 'b_ndebug=if-release', ]) if get_option('compile_app') From 0207e3df334435d481fec23465f4165c46e6ba22 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 21 Feb 2021 21:44:30 +0100 Subject: [PATCH 0484/2244] Remove unused no_window field --- app/src/screen.h | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/screen.h b/app/src/screen.h index 171717ab..f2224ff1 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -33,7 +33,6 @@ struct screen { bool has_frame; bool fullscreen; bool maximized; - bool no_window; bool mipmaps; }; @@ -66,7 +65,6 @@ struct screen { .has_frame = false, \ .fullscreen = false, \ .maximized = false, \ - .no_window = false, \ .mipmaps = false, \ } From a3aa5ac95e4cd5179e79881f15e8152933cbdd4d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 22 Feb 2021 22:03:50 +0100 Subject: [PATCH 0485/2244] Insert numerical values statically in usage string --- app/src/cli.c | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index fbdef07f..d9cacf15 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -10,6 +10,9 @@ #include "util/log.h" #include "util/str_util.h" +#define STR_IMPL_(x) #x +#define STR(x) STR_IMPL_(x) + void scrcpy_print_usage(const char *arg0) { fprintf(stderr, @@ -23,7 +26,7 @@ scrcpy_print_usage(const char *arg0) { " -b, --bit-rate value\n" " Encode the video at the given bit-rate, expressed in bits/s.\n" " Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n" - " Default is %d.\n" + " Default is " STR(DEFAULT_BIT_RATE) ".\n" "\n" " --codec-options key[:type]=value[,...]\n" " Set a list of comma-separated key:type=value options for the\n" @@ -81,7 +84,12 @@ scrcpy_print_usage(const char *arg0) { " 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" +#if DEFAULT_LOCK_VIDEO_ORIENTATION == -1 +# define DEFAULT_LOCK_VIDEO_ORIENTATION_STR "-1 (unlocked)" +#else +# define DEFAULT_LOCK_VIDEO_ORIENTATION_STR STR(DEFAULT_LOCK_VIDEO_ORIENTATION) +#endif + " Default is " DEFAULT_LOCK_VIDEO_ORIENTATION_STR ".\n" "\n" " --max-fps value\n" " Limit the frame rate of screen capture (officially supported\n" @@ -91,7 +99,12 @@ scrcpy_print_usage(const char *arg0) { " Limit both the width and height of the video to value. The\n" " other dimension is computed so that the device aspect-ratio\n" " is preserved.\n" - " Default is %d%s.\n" +#if DEFAULT_MAX_SIZE == 0 +# define DEFAULT_MAX_SIZE_STR "0 (unlimited)" +#else +# define DEFAULT_MAX_SIZE_STR STR(DEFAULT_MAX_SIZE) +#endif + " Default is " DEFAULT_MAX_SIZE_STR ".\n" "\n" " -n, --no-control\n" " Disable device control (mirror the device in read-only).\n" @@ -110,7 +123,8 @@ scrcpy_print_usage(const char *arg0) { "\n" " -p, --port port[:port]\n" " Set the TCP port (range) used by the client to listen.\n" - " Default is %d:%d.\n" + " Default is " STR(DEFAULT_LOCAL_PORT_RANGE_FIRST) ":" + STR(DEFAULT_LOCAL_PORT_RANGE_LAST) ".\n" "\n" " --prefer-text\n" " Inject alpha characters and space as text events instead of\n" @@ -297,12 +311,7 @@ scrcpy_print_usage(const char *arg0) { "\n" " Drag & drop APK file\n" " Install APK from computer\n" - "\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); + "\n", arg0); } static bool From b16b65a715485b3bf2babe51e7c17e3c3a355adf Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 23 Feb 2021 09:37:16 +0100 Subject: [PATCH 0486/2244] Simplify default values It makes sense to extract default values for bitrate and port range (which are arbitrary and might be changed in the future). However, the default values for "max size" and "lock video orientation" are naturally unlimited/unlocked, and will never be changed. Extracting these options just added complexity for no benefit, so hardcode them. --- app/meson.build | 10 ---------- app/src/cli.c | 14 ++------------ app/src/scrcpy.h | 4 ++-- 3 files changed, 4 insertions(+), 24 deletions(-) diff --git a/app/meson.build b/app/meson.build index 75a2fd71..d230702e 100644 --- a/app/meson.build +++ b/app/meson.build @@ -114,16 +114,6 @@ conf.set('PORTABLE', get_option('portable')) 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 -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/src/cli.c b/app/src/cli.c index d9cacf15..7f31b32c 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -84,12 +84,7 @@ scrcpy_print_usage(const char *arg0) { " 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" -#if DEFAULT_LOCK_VIDEO_ORIENTATION == -1 -# define DEFAULT_LOCK_VIDEO_ORIENTATION_STR "-1 (unlocked)" -#else -# define DEFAULT_LOCK_VIDEO_ORIENTATION_STR STR(DEFAULT_LOCK_VIDEO_ORIENTATION) -#endif - " Default is " DEFAULT_LOCK_VIDEO_ORIENTATION_STR ".\n" + " Default is -1 (unlocked).\n" "\n" " --max-fps value\n" " Limit the frame rate of screen capture (officially supported\n" @@ -99,12 +94,7 @@ scrcpy_print_usage(const char *arg0) { " Limit both the width and height of the video to value. The\n" " other dimension is computed so that the device aspect-ratio\n" " is preserved.\n" -#if DEFAULT_MAX_SIZE == 0 -# define DEFAULT_MAX_SIZE_STR "0 (unlimited)" -#else -# define DEFAULT_MAX_SIZE_STR STR(DEFAULT_MAX_SIZE) -#endif - " Default is " DEFAULT_MAX_SIZE_STR ".\n" + " Default is 0 (unlimited).\n" "\n" " -n, --no-control\n" " Disable device control (mirror the device in read-only).\n" diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index 2253cc28..b877a987 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -103,10 +103,10 @@ struct scrcpy_options { .data = {SC_MOD_LALT, SC_MOD_LSUPER}, \ .count = 2, \ }, \ - .max_size = DEFAULT_MAX_SIZE, \ + .max_size = 0, \ .bit_rate = DEFAULT_BIT_RATE, \ .max_fps = 0, \ - .lock_video_orientation = DEFAULT_LOCK_VIDEO_ORIENTATION, \ + .lock_video_orientation = -1, \ .rotation = 0, \ .window_x = SC_WINDOW_POSITION_UNDEFINED, \ .window_y = SC_WINDOW_POSITION_UNDEFINED, \ From a2919b3ef2d3b08f426ac53ff1999c0f7f925e9f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 27 Feb 2021 00:20:18 +0100 Subject: [PATCH 0487/2244] Fix release instructions in BUILD.md Makefile.CrossWindows have been renamed to release.mk, which is called from release.sh. --- BUILD.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BUILD.md b/BUILD.md index 52029b6b..30a911e1 100644 --- a/BUILD.md +++ b/BUILD.md @@ -112,7 +112,7 @@ sudo apt install openjdk-8-jdk Then generate the releases: ```bash -make -f Makefile.CrossWindows +./release.sh ``` It will generate win32 and win64 releases into `dist/`. From 7b51a0313ed9423f9252108fa573af68c11e6a2c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 27 Feb 2021 00:27:58 +0100 Subject: [PATCH 0488/2244] Update another java version in BUILD.md Commit f8524a2be738e6471ed2e48265f2663d8c21c0be updated one reference to the openjdk package, but there was another one. --- BUILD.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BUILD.md b/BUILD.md index 30a911e1..fdcc569d 100644 --- a/BUILD.md +++ b/BUILD.md @@ -106,7 +106,7 @@ sudo apt install mingw-w64 mingw-w64-tools You also need the JDK to build the server: ```bash -sudo apt install openjdk-8-jdk +sudo apt install openjdk-11-jdk ``` Then generate the releases: From 1863ca7ad1b87f9b32aa1c5aa9554741f5845fba Mon Sep 17 00:00:00 2001 From: yangfl Date: Tue, 16 Feb 2021 00:52:53 +0800 Subject: [PATCH 0489/2244] Remove unnecessary escape characters in manpage PR #2123 Signed-off-by: Romain Vimont --- app/scrcpy.1 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 92b8e1e3..f5fc5d75 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -213,25 +213,25 @@ Set a custom window title. .BI "\-\-window\-x " value Set the initial window horizontal position. -Default is "auto".\n +Default is "auto". .TP .BI "\-\-window\-y " value Set the initial window vertical position. -Default is "auto".\n +Default is "auto". .TP .BI "\-\-window\-width " value Set the initial window width. -Default is 0 (automatic).\n +Default is 0 (automatic). .TP .BI "\-\-window\-height " value Set the initial window height. -Default is 0 (automatic).\n +Default is 0 (automatic). .SH SHORTCUTS From 218636dc1045d17c8c58e08cda5961a5a83a7281 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 3 Mar 2021 18:15:49 +0100 Subject: [PATCH 0490/2244] Inject touch events with smallest detectable size A value of 1 means the "largest detectable size", a value of 0 means the "smallest detectable size". https://developer.android.com/reference/android/view/MotionEvent.PointerCoords#size https://developer.android.com/reference/android/view/MotionEvent#AXIS_SIZE Fixes #2125 --- 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 84780239..d8059b43 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -45,7 +45,7 @@ public class Controller { MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords(); coords.orientation = 0; - coords.size = 1; + coords.size = 0; pointerProperties[i] = props; pointerCoords[i] = coords; From 08baaf4b575aef7ee56d14683be3f4e3a86d39aa Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 4 Mar 2021 15:19:00 +0100 Subject: [PATCH 0491/2244] Mention adb debugging in FAQ --- FAQ.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/FAQ.md b/FAQ.md index 9801f91c..59a8648f 100644 --- a/FAQ.md +++ b/FAQ.md @@ -39,8 +39,11 @@ Check [stackoverflow][device-unauthorized]. > adb: error: failed to get feature set: no devices/emulators found +Check that you correctly enabled [adb debugging][enable-adb]. + If your device is not detected, you may need some [drivers] (on Windows). +[enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling [drivers]: https://developer.android.com/studio/run/oem-usb.html From dca11f6c513e3b63e5494fce5bfffdc5c29be002 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 6 Mar 2021 14:28:06 +0100 Subject: [PATCH 0492/2244] Remove obsolete FAQ entry Issue #15 had been fixed in v1.14 by e40532a3761da8b3aef484f1d435ef5f50d94574. --- FAQ.md | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/FAQ.md b/FAQ.md index 59a8648f..a84aade8 100644 --- a/FAQ.md +++ b/FAQ.md @@ -114,16 +114,6 @@ In developer options, enable: [simulating input]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323 -### Mouse clicks at wrong location - -On MacOS, with HiDPI support and multiple screens, input location are wrongly -scaled. See [#15]. - -[#15]: https://github.com/Genymobile/scrcpy/issues/15 - -Open _scrcpy_ directly on the monitor you use it. - - ### Special characters do not work Injecting text input is [limited to ASCII characters][text-input]. A trick From cb197ee3a2215784f088768f5e39102fd5f55ddd Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 19 Feb 2021 20:56:09 +0100 Subject: [PATCH 0493/2244] Move fps counter out of video buffer In order to make video buffer more generic, move out its specific responsibility to count the fps between the decoder and the renderer. --- app/src/decoder.c | 5 ++++- app/src/decoder.h | 5 ++++- app/src/input_manager.c | 4 +--- app/src/input_manager.h | 1 + app/src/scrcpy.c | 8 ++++---- app/src/screen.c | 7 ++++++- app/src/screen.h | 6 +++++- app/src/video_buffer.c | 9 +-------- app/src/video_buffer.h | 5 +---- 9 files changed, 27 insertions(+), 23 deletions(-) diff --git a/app/src/decoder.c b/app/src/decoder.c index 23f9ce9d..4b1f0fce 100644 --- a/app/src/decoder.c +++ b/app/src/decoder.c @@ -18,6 +18,7 @@ push_frame(struct decoder *decoder) { video_buffer_offer_decoded_frame(decoder->video_buffer, &previous_frame_skipped); if (previous_frame_skipped) { + fps_counter_add_skipped_frame(decoder->fps_counter); // the previous EVENT_NEW_FRAME will consume this frame return; } @@ -28,8 +29,10 @@ push_frame(struct decoder *decoder) { } void -decoder_init(struct decoder *decoder, struct video_buffer *vb) { +decoder_init(struct decoder *decoder, struct video_buffer *vb, + struct fps_counter *fps_counter) { decoder->video_buffer = vb; + decoder->fps_counter = fps_counter; } bool diff --git a/app/src/decoder.h b/app/src/decoder.h index 27afcd8e..306cc77c 100644 --- a/app/src/decoder.h +++ b/app/src/decoder.h @@ -10,11 +10,14 @@ struct video_buffer; struct decoder { struct video_buffer *video_buffer; + struct fps_counter *fps_counter; + AVCodecContext *codec_ctx; }; void -decoder_init(struct decoder *decoder, struct video_buffer *vb); +decoder_init(struct decoder *decoder, struct video_buffer *vb, + struct fps_counter *fps_counter); bool decoder_open(struct decoder *decoder, const AVCodec *codec); diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 20ea05c5..7226d68f 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -480,9 +480,7 @@ input_manager_process_key(struct input_manager *im, return; case SDLK_i: if (!shift && !repeat && down) { - struct fps_counter *fps_counter = - im->video_buffer->fps_counter; - switch_fps_counter_state(fps_counter); + switch_fps_counter_state(im->fps_counter); } return; case SDLK_n: diff --git a/app/src/input_manager.h b/app/src/input_manager.h index 6acb354d..d10c96b5 100644 --- a/app/src/input_manager.h +++ b/app/src/input_manager.h @@ -16,6 +16,7 @@ struct input_manager { struct controller *controller; struct video_buffer *video_buffer; + struct fps_counter *fps_counter; struct screen *screen; // SDL reports repeated events as a boolean, but Android expects the actual diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index d24bba2c..a671c5e4 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -42,6 +42,7 @@ static struct file_handler file_handler; static struct input_manager input_manager = { .controller = &controller, .video_buffer = &video_buffer, + .fps_counter = &fps_counter, .screen = &screen, .repeat = 0, @@ -332,8 +333,7 @@ scrcpy(const struct scrcpy_options *options) { } fps_counter_initialized = true; - if (!video_buffer_init(&video_buffer, &fps_counter, - options->render_expired_frames)) { + if (!video_buffer_init(&video_buffer, options->render_expired_frames)) { goto end; } video_buffer_initialized = true; @@ -346,7 +346,7 @@ scrcpy(const struct scrcpy_options *options) { file_handler_initialized = true; } - decoder_init(&decoder, &video_buffer); + decoder_init(&decoder, &video_buffer, &fps_counter); dec = &decoder; } @@ -389,7 +389,7 @@ scrcpy(const struct scrcpy_options *options) { const char *window_title = options->window_title ? options->window_title : device_name; - screen_init(&screen, &video_buffer); + screen_init(&screen, &video_buffer, &fps_counter); if (!screen_init_rendering(&screen, window_title, frame_size, options->always_on_top, options->window_x, diff --git a/app/src/screen.c b/app/src/screen.c index 4b0321eb..7c2cea04 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -192,9 +192,11 @@ screen_update_content_rect(struct screen *screen) { } void -screen_init(struct screen *screen, struct video_buffer *vb) { +screen_init(struct screen *screen, struct video_buffer *vb, + struct fps_counter *fps_counter) { *screen = (struct screen) SCREEN_INITIALIZER; screen->vb = vb; + screen->fps_counter = fps_counter; } static inline SDL_Texture * @@ -450,6 +452,9 @@ update_texture(struct screen *screen, const AVFrame *frame) { static bool screen_update_frame(struct screen *screen) { const AVFrame *frame = video_buffer_take_rendering_frame(screen->vb); + + fps_counter_add_rendered_frame(screen->fps_counter); + struct size new_frame_size = {frame->width, frame->height}; if (!prepare_for_frame(screen, new_frame_size)) { return false; diff --git a/app/src/screen.h b/app/src/screen.h index f2224ff1..8941416e 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -14,6 +14,8 @@ struct video_buffer; struct screen { struct video_buffer *vb; + struct fps_counter *fps_counter; + SDL_Window *window; SDL_Renderer *renderer; SDL_Texture *texture; @@ -38,6 +40,7 @@ struct screen { #define SCREEN_INITIALIZER { \ .vb = NULL, \ + .fps_counter = NULL, \ .window = NULL, \ .renderer = NULL, \ .texture = NULL, \ @@ -70,7 +73,8 @@ struct screen { // initialize default values void -screen_init(struct screen *screen, struct video_buffer *vb); +screen_init(struct screen *screen, struct video_buffer *vb, + struct fps_counter *fps_counter); // initialize screen, create window, renderer and texture (window is hidden) // window_x and window_y accept SC_WINDOW_POSITION_UNDEFINED diff --git a/app/src/video_buffer.c b/app/src/video_buffer.c index 4c8dd5da..903df070 100644 --- a/app/src/video_buffer.c +++ b/app/src/video_buffer.c @@ -7,10 +7,7 @@ #include "util/log.h" bool -video_buffer_init(struct video_buffer *vb, struct fps_counter *fps_counter, - bool render_expired_frames) { - vb->fps_counter = fps_counter; - +video_buffer_init(struct video_buffer *vb, bool render_expired_frames) { vb->decoding_frame = av_frame_alloc(); if (!vb->decoding_frame) { goto error_0; @@ -95,8 +92,6 @@ video_buffer_offer_decoded_frame(struct video_buffer *vb, while (!vb->pending_frame_consumed && !vb->interrupted) { sc_cond_wait(&vb->pending_frame_consumed_cond, &vb->mutex); } - } else if (!vb->pending_frame_consumed) { - fps_counter_add_skipped_frame(vb->fps_counter); } video_buffer_swap_decoding_frame(vb); @@ -113,8 +108,6 @@ video_buffer_take_rendering_frame(struct video_buffer *vb) { assert(!vb->pending_frame_consumed); vb->pending_frame_consumed = true; - fps_counter_add_rendered_frame(vb->fps_counter); - video_buffer_swap_rendering_frame(vb); if (vb->render_expired_frames) { diff --git a/app/src/video_buffer.h b/app/src/video_buffer.h index 3e52caa7..4c82fba4 100644 --- a/app/src/video_buffer.h +++ b/app/src/video_buffer.h @@ -39,13 +39,10 @@ struct video_buffer { sc_cond pending_frame_consumed_cond; bool pending_frame_consumed; - - struct fps_counter *fps_counter; }; bool -video_buffer_init(struct video_buffer *vb, struct fps_counter *fps_counter, - bool render_expired_frames); +video_buffer_init(struct video_buffer *vb, bool render_expired_frames); void video_buffer_destroy(struct video_buffer *vb); From 441d3fb119ce05287e94b0ae0befbb2e75173688 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 19 Feb 2021 21:16:57 +0100 Subject: [PATCH 0494/2244] Make video buffer more generic Video buffer is a tool between a frame producer and a frame consumer. For now, it is used between a decoder and a renderer, but in the future another instance might be used to swscale decoded frames. --- app/src/decoder.c | 6 ++-- app/src/screen.c | 2 +- app/src/video_buffer.c | 63 +++++++++++++++++++++--------------------- app/src/video_buffer.h | 38 ++++++++++++------------- 4 files changed, 54 insertions(+), 55 deletions(-) diff --git a/app/src/decoder.c b/app/src/decoder.c index 4b1f0fce..87cd0fe7 100644 --- a/app/src/decoder.c +++ b/app/src/decoder.c @@ -15,8 +15,8 @@ static void push_frame(struct decoder *decoder) { bool previous_frame_skipped; - video_buffer_offer_decoded_frame(decoder->video_buffer, - &previous_frame_skipped); + video_buffer_producer_offer_frame(decoder->video_buffer, + &previous_frame_skipped); if (previous_frame_skipped) { fps_counter_add_skipped_frame(decoder->fps_counter); // the previous EVENT_NEW_FRAME will consume this frame @@ -69,7 +69,7 @@ decoder_push(struct decoder *decoder, const AVPacket *packet) { return false; } ret = avcodec_receive_frame(decoder->codec_ctx, - decoder->video_buffer->decoding_frame); + decoder->video_buffer->producer_frame); if (!ret) { // a frame was received push_frame(decoder); diff --git a/app/src/screen.c b/app/src/screen.c index 7c2cea04..473d2e35 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -451,7 +451,7 @@ update_texture(struct screen *screen, const AVFrame *frame) { static bool screen_update_frame(struct screen *screen) { - const AVFrame *frame = video_buffer_take_rendering_frame(screen->vb); + const AVFrame *frame = video_buffer_consumer_take_frame(screen->vb); fps_counter_add_rendered_frame(screen->fps_counter); diff --git a/app/src/video_buffer.c b/app/src/video_buffer.c index 903df070..e7e150a2 100644 --- a/app/src/video_buffer.c +++ b/app/src/video_buffer.c @@ -7,9 +7,9 @@ #include "util/log.h" bool -video_buffer_init(struct video_buffer *vb, bool render_expired_frames) { - vb->decoding_frame = av_frame_alloc(); - if (!vb->decoding_frame) { +video_buffer_init(struct video_buffer *vb, bool wait_consumer) { + vb->producer_frame = av_frame_alloc(); + if (!vb->producer_frame) { goto error_0; } @@ -18,8 +18,8 @@ video_buffer_init(struct video_buffer *vb, bool render_expired_frames) { goto error_1; } - vb->rendering_frame = av_frame_alloc(); - if (!vb->rendering_frame) { + vb->consumer_frame = av_frame_alloc(); + if (!vb->consumer_frame) { goto error_2; } @@ -28,73 +28,72 @@ video_buffer_init(struct video_buffer *vb, bool render_expired_frames) { goto error_3; } - vb->render_expired_frames = render_expired_frames; - if (render_expired_frames) { + vb->wait_consumer = wait_consumer; + if (wait_consumer) { ok = sc_cond_init(&vb->pending_frame_consumed_cond); if (!ok) { sc_mutex_destroy(&vb->mutex); goto error_2; } - // interrupted is not used if expired frames are not rendered - // since offering a frame will never block + // interrupted is not used if wait_consumer is disabled since offering + // a frame will never block vb->interrupted = false; } - // there is initially no rendering frame, so consider it has already been - // consumed + // there is initially no frame, so consider it has already been consumed vb->pending_frame_consumed = true; return true; error_3: - av_frame_free(&vb->rendering_frame); + av_frame_free(&vb->consumer_frame); error_2: av_frame_free(&vb->pending_frame); error_1: - av_frame_free(&vb->decoding_frame); + av_frame_free(&vb->producer_frame); error_0: return false; } void video_buffer_destroy(struct video_buffer *vb) { - if (vb->render_expired_frames) { + if (vb->wait_consumer) { sc_cond_destroy(&vb->pending_frame_consumed_cond); } sc_mutex_destroy(&vb->mutex); - av_frame_free(&vb->rendering_frame); + av_frame_free(&vb->consumer_frame); av_frame_free(&vb->pending_frame); - av_frame_free(&vb->decoding_frame); + av_frame_free(&vb->producer_frame); } static void -video_buffer_swap_decoding_frame(struct video_buffer *vb) { +video_buffer_swap_producer_frame(struct video_buffer *vb) { sc_mutex_assert(&vb->mutex); - AVFrame *tmp = vb->decoding_frame; - vb->decoding_frame = vb->pending_frame; + AVFrame *tmp = vb->producer_frame; + vb->producer_frame = vb->pending_frame; vb->pending_frame = tmp; } static void -video_buffer_swap_rendering_frame(struct video_buffer *vb) { +video_buffer_swap_consumer_frame(struct video_buffer *vb) { sc_mutex_assert(&vb->mutex); - AVFrame *tmp = vb->rendering_frame; - vb->rendering_frame = vb->pending_frame; + AVFrame *tmp = vb->consumer_frame; + vb->consumer_frame = vb->pending_frame; vb->pending_frame = tmp; } void -video_buffer_offer_decoded_frame(struct video_buffer *vb, - bool *previous_frame_skipped) { +video_buffer_producer_offer_frame(struct video_buffer *vb, + bool *previous_frame_skipped) { sc_mutex_lock(&vb->mutex); - if (vb->render_expired_frames) { + if (vb->wait_consumer) { // wait for the current (expired) frame to be consumed while (!vb->pending_frame_consumed && !vb->interrupted) { sc_cond_wait(&vb->pending_frame_consumed_cond, &vb->mutex); } } - video_buffer_swap_decoding_frame(vb); + video_buffer_swap_producer_frame(vb); *previous_frame_skipped = !vb->pending_frame_consumed; vb->pending_frame_consumed = false; @@ -103,26 +102,26 @@ video_buffer_offer_decoded_frame(struct video_buffer *vb, } const AVFrame * -video_buffer_take_rendering_frame(struct video_buffer *vb) { +video_buffer_consumer_take_frame(struct video_buffer *vb) { sc_mutex_lock(&vb->mutex); assert(!vb->pending_frame_consumed); vb->pending_frame_consumed = true; - video_buffer_swap_rendering_frame(vb); + video_buffer_swap_consumer_frame(vb); - if (vb->render_expired_frames) { + if (vb->wait_consumer) { // unblock video_buffer_offer_decoded_frame() sc_cond_signal(&vb->pending_frame_consumed_cond); } sc_mutex_unlock(&vb->mutex); - // rendering_frame is only written from this thread, no need to lock - return vb->rendering_frame; + // consumer_frame is only written from this thread, no need to lock + return vb->consumer_frame; } void video_buffer_interrupt(struct video_buffer *vb) { - if (vb->render_expired_frames) { + if (vb->wait_consumer) { sc_mutex_lock(&vb->mutex); vb->interrupted = true; sc_mutex_unlock(&vb->mutex); diff --git a/app/src/video_buffer.h b/app/src/video_buffer.h index 4c82fba4..6c5d197c 100644 --- a/app/src/video_buffer.h +++ b/app/src/video_buffer.h @@ -13,28 +13,28 @@ typedef struct AVFrame AVFrame; /** * There are 3 frames in memory: - * - one frame is held by the decoder (decoding_frame) - * - one frame is held by the renderer (rendering_frame) - * - one frame is shared between the decoder and the renderer (pending_frame) + * - one frame is held by the producer (producer_frame) + * - one frame is held by the consumer (consumer_frame) + * - one frame is shared between the producer and the consumer (pending_frame) * - * The decoder decodes a packet into the decoding_frame (it may takes time). + * The producer generates a frame into the producer_frame (it may takes time). * - * Once the frame is decoded, it calls video_buffer_offer_decoded_frame(), - * which swaps the decoding and pending frames. + * Once the frame is produced, it calls video_buffer_producer_offer_frame(), + * which swaps the producer and pending frames. * - * When the renderer is notified that a new frame is available, it calls - * video_buffer_take_rendering_frame() to retrieve it, which swaps the pending - * and rendering frames. The frame is valid until the next call, without - * blocking the decoder. + * When the consumer is notified that a new frame is available, it calls + * video_buffer_consumer_take_frame() to retrieve it, which swaps the pending + * and consumer frames. The frame is valid until the next call, without + * blocking the producer. */ struct video_buffer { - AVFrame *decoding_frame; + AVFrame *producer_frame; AVFrame *pending_frame; - AVFrame *rendering_frame; + AVFrame *consumer_frame; sc_mutex mutex; - bool render_expired_frames; + bool wait_consumer; // never overwrite a pending frame if it is not consumed bool interrupted; sc_cond pending_frame_consumed_cond; @@ -42,21 +42,21 @@ struct video_buffer { }; bool -video_buffer_init(struct video_buffer *vb, bool render_expired_frames); +video_buffer_init(struct video_buffer *vb, bool wait_consumer); void video_buffer_destroy(struct video_buffer *vb); -// set the decoded frame as ready for rendering +// set the producer frame as ready for consuming // the output flag is set to report whether the previous frame has been skipped void -video_buffer_offer_decoded_frame(struct video_buffer *vb, - bool *previous_frame_skipped); +video_buffer_producer_offer_frame(struct video_buffer *vb, + bool *previous_frame_skipped); -// mark the rendering frame as consumed and return it +// mark the consumer frame as consumed and return it // the frame is valid until the next call to this function const AVFrame * -video_buffer_take_rendering_frame(struct video_buffer *vb); +video_buffer_consumer_take_frame(struct video_buffer *vb); // wake up and avoid any blocking call void From c50b958ee415843aebc23cd7cc45d7b1e7642c71 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 19 Feb 2021 22:40:12 +0100 Subject: [PATCH 0495/2244] Initialize screen before starting the stream As soon as the stream is started, the video buffer could notify a new frame available. In order to pass this event to the screen without race condition, the screen must be initialized before the screen is started. --- app/src/scrcpy.c | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index a671c5e4..f3bfecfb 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -366,13 +366,6 @@ scrcpy(const struct scrcpy_options *options) { stream_init(&stream, server.video_socket, dec, rec); - // now we consumed the header values, the socket receives the video stream - // start the stream - if (!stream_start(&stream)) { - goto end; - } - stream_started = true; - if (options->display) { if (options->control) { if (!controller_init(&controller, server.control_socket)) { @@ -415,6 +408,13 @@ scrcpy(const struct scrcpy_options *options) { } } + // now we consumed the header values, the socket receives the video stream + // start the stream + if (!stream_start(&stream)) { + goto end; + } + stream_started = true; + input_manager_init(&input_manager, options); ret = event_loop(options); From fb9f9848bd2c5196a91bbe04fc1d22eacd7abe12 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 19 Feb 2021 22:02:36 +0100 Subject: [PATCH 0496/2244] Use a callback to notify a new frame Make the decoder independant of the SDL even mechanism, by making the consumer register a callback on the video_buffer. --- app/src/decoder.c | 6 ------ app/src/screen.c | 19 +++++++++++++++++++ app/src/video_buffer.c | 26 +++++++++++++++++++++++++- app/src/video_buffer.h | 15 +++++++++++++++ 4 files changed, 59 insertions(+), 7 deletions(-) diff --git a/app/src/decoder.c b/app/src/decoder.c index 87cd0fe7..7b016d21 100644 --- a/app/src/decoder.c +++ b/app/src/decoder.c @@ -19,13 +19,7 @@ push_frame(struct decoder *decoder) { &previous_frame_skipped); if (previous_frame_skipped) { fps_counter_add_skipped_frame(decoder->fps_counter); - // the previous EVENT_NEW_FRAME will consume this frame - return; } - static SDL_Event new_frame_event = { - .type = EVENT_NEW_FRAME, - }; - SDL_PushEvent(&new_frame_event); } void diff --git a/app/src/screen.c b/app/src/screen.c index 473d2e35..397aa2d1 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -191,12 +191,31 @@ screen_update_content_rect(struct screen *screen) { } } +static void +on_frame_available(struct video_buffer *vb, void *userdata) { + (void) vb; + (void) userdata; + + static SDL_Event new_frame_event = { + .type = EVENT_NEW_FRAME, + }; + + // Post the event on the UI thread + SDL_PushEvent(&new_frame_event); +} + void screen_init(struct screen *screen, struct video_buffer *vb, struct fps_counter *fps_counter) { *screen = (struct screen) SCREEN_INITIALIZER; screen->vb = vb; screen->fps_counter = fps_counter; + + static const struct video_buffer_callbacks cbs = { + .on_frame_available = on_frame_available, + }; + + video_buffer_set_consumer_callbacks(vb, &cbs, NULL); } static inline SDL_Texture * diff --git a/app/src/video_buffer.c b/app/src/video_buffer.c index e7e150a2..c145de08 100644 --- a/app/src/video_buffer.c +++ b/app/src/video_buffer.c @@ -43,6 +43,10 @@ video_buffer_init(struct video_buffer *vb, bool wait_consumer) { // there is initially no frame, so consider it has already been consumed vb->pending_frame_consumed = true; + // The callbacks must be set by the consumer via + // video_buffer_set_consumer_callbacks() + vb->cbs = NULL; + return true; error_3: @@ -82,9 +86,22 @@ video_buffer_swap_consumer_frame(struct video_buffer *vb) { vb->pending_frame = tmp; } +void +video_buffer_set_consumer_callbacks(struct video_buffer *vb, + const struct video_buffer_callbacks *cbs, + void *cbs_userdata) { + assert(!vb->cbs); // must be set only once + assert(cbs); + assert(cbs->on_frame_available); + vb->cbs = cbs; + vb->cbs_userdata = cbs_userdata; +} + void video_buffer_producer_offer_frame(struct video_buffer *vb, bool *previous_frame_skipped) { + assert(vb->cbs); + sc_mutex_lock(&vb->mutex); if (vb->wait_consumer) { // wait for the current (expired) frame to be consumed @@ -95,10 +112,17 @@ video_buffer_producer_offer_frame(struct video_buffer *vb, video_buffer_swap_producer_frame(vb); - *previous_frame_skipped = !vb->pending_frame_consumed; + bool skipped = !vb->pending_frame_consumed; + *previous_frame_skipped = skipped; vb->pending_frame_consumed = false; sc_mutex_unlock(&vb->mutex); + + if (!skipped) { + // If skipped, then the previous call will consume this frame, the + // callback must not be called + vb->cbs->on_frame_available(vb, vb->cbs_userdata); + } } const AVFrame * diff --git a/app/src/video_buffer.h b/app/src/video_buffer.h index 6c5d197c..a41521a3 100644 --- a/app/src/video_buffer.h +++ b/app/src/video_buffer.h @@ -39,6 +39,16 @@ struct video_buffer { sc_cond pending_frame_consumed_cond; bool pending_frame_consumed; + + const struct video_buffer_callbacks *cbs; + void *cbs_userdata; +}; + +struct video_buffer_callbacks { + // Called when a new frame can be consumed by + // video_buffer_consumer_take_frame(vb) + // This callback is mandatory (it must not be NULL). + void (*on_frame_available)(struct video_buffer *vb, void *userdata); }; bool @@ -47,6 +57,11 @@ video_buffer_init(struct video_buffer *vb, bool wait_consumer); void video_buffer_destroy(struct video_buffer *vb); +void +video_buffer_set_consumer_callbacks(struct video_buffer *vb, + const struct video_buffer_callbacks *cbs, + void *cbs_userdata); + // set the producer frame as ready for consuming // the output flag is set to report whether the previous frame has been skipped void From cb9c42bdcb198b0e3ebed893e00257b95f4dcedb Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 23 Feb 2021 19:59:43 +0100 Subject: [PATCH 0497/2244] Use a callback to notify frame skip A skipped frame is detected when the producer offers a frame while the current pending frame has not been consumed. However, the producer (in practice the decoder) is not interested in the fact that a frame has been skipped, only the consumer (the renderer) is. Therefore, notify frame skip via a consumer callback. This allows to manage the skipped and rendered frames count at the same place, and remove fps_counter from decoder. --- app/src/decoder.c | 17 ++--------------- app/src/decoder.h | 4 +--- app/src/scrcpy.c | 2 +- app/src/screen.c | 11 ++++++++++- app/src/video_buffer.c | 11 +++++------ app/src/video_buffer.h | 8 +++++--- 6 files changed, 24 insertions(+), 29 deletions(-) diff --git a/app/src/decoder.c b/app/src/decoder.c index 7b016d21..7da959c6 100644 --- a/app/src/decoder.c +++ b/app/src/decoder.c @@ -11,22 +11,9 @@ #include "util/buffer_util.h" #include "util/log.h" -// set the decoded frame as ready for rendering, and notify -static void -push_frame(struct decoder *decoder) { - bool previous_frame_skipped; - video_buffer_producer_offer_frame(decoder->video_buffer, - &previous_frame_skipped); - if (previous_frame_skipped) { - fps_counter_add_skipped_frame(decoder->fps_counter); - } -} - void -decoder_init(struct decoder *decoder, struct video_buffer *vb, - struct fps_counter *fps_counter) { +decoder_init(struct decoder *decoder, struct video_buffer *vb) { decoder->video_buffer = vb; - decoder->fps_counter = fps_counter; } bool @@ -66,7 +53,7 @@ decoder_push(struct decoder *decoder, const AVPacket *packet) { decoder->video_buffer->producer_frame); if (!ret) { // a frame was received - push_frame(decoder); + video_buffer_producer_offer_frame(decoder->video_buffer); } else if (ret != AVERROR(EAGAIN)) { LOGE("Could not receive video frame: %d", ret); return false; diff --git a/app/src/decoder.h b/app/src/decoder.h index 306cc77c..bbd7a9a7 100644 --- a/app/src/decoder.h +++ b/app/src/decoder.h @@ -10,14 +10,12 @@ struct video_buffer; struct decoder { struct video_buffer *video_buffer; - struct fps_counter *fps_counter; AVCodecContext *codec_ctx; }; void -decoder_init(struct decoder *decoder, struct video_buffer *vb, - struct fps_counter *fps_counter); +decoder_init(struct decoder *decoder, struct video_buffer *vb); bool decoder_open(struct decoder *decoder, const AVCodec *codec); diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index f3bfecfb..e7879d5b 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -346,7 +346,7 @@ scrcpy(const struct scrcpy_options *options) { file_handler_initialized = true; } - decoder_init(&decoder, &video_buffer, &fps_counter); + decoder_init(&decoder, &video_buffer); dec = &decoder; } diff --git a/app/src/screen.c b/app/src/screen.c index 397aa2d1..bcccdd32 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -204,6 +204,14 @@ on_frame_available(struct video_buffer *vb, void *userdata) { SDL_PushEvent(&new_frame_event); } +static void +on_frame_skipped(struct video_buffer *vb, void *userdata) { + (void) vb; + + struct screen *screen = userdata; + fps_counter_add_skipped_frame(screen->fps_counter); +} + void screen_init(struct screen *screen, struct video_buffer *vb, struct fps_counter *fps_counter) { @@ -213,9 +221,10 @@ screen_init(struct screen *screen, struct video_buffer *vb, static const struct video_buffer_callbacks cbs = { .on_frame_available = on_frame_available, + .on_frame_skipped = on_frame_skipped, }; - video_buffer_set_consumer_callbacks(vb, &cbs, NULL); + video_buffer_set_consumer_callbacks(vb, &cbs, screen); } static inline SDL_Texture * diff --git a/app/src/video_buffer.c b/app/src/video_buffer.c index c145de08..3ebffa7e 100644 --- a/app/src/video_buffer.c +++ b/app/src/video_buffer.c @@ -98,8 +98,7 @@ video_buffer_set_consumer_callbacks(struct video_buffer *vb, } void -video_buffer_producer_offer_frame(struct video_buffer *vb, - bool *previous_frame_skipped) { +video_buffer_producer_offer_frame(struct video_buffer *vb) { assert(vb->cbs); sc_mutex_lock(&vb->mutex); @@ -113,14 +112,14 @@ video_buffer_producer_offer_frame(struct video_buffer *vb, video_buffer_swap_producer_frame(vb); bool skipped = !vb->pending_frame_consumed; - *previous_frame_skipped = skipped; vb->pending_frame_consumed = false; sc_mutex_unlock(&vb->mutex); - if (!skipped) { - // If skipped, then the previous call will consume this frame, the - // callback must not be called + if (skipped) { + if (vb->cbs->on_frame_skipped) + vb->cbs->on_frame_skipped(vb, vb->cbs_userdata); + } else { vb->cbs->on_frame_available(vb, vb->cbs_userdata); } } diff --git a/app/src/video_buffer.h b/app/src/video_buffer.h index a41521a3..4d11e3ab 100644 --- a/app/src/video_buffer.h +++ b/app/src/video_buffer.h @@ -49,6 +49,10 @@ struct video_buffer_callbacks { // video_buffer_consumer_take_frame(vb) // This callback is mandatory (it must not be NULL). void (*on_frame_available)(struct video_buffer *vb, void *userdata); + + // Called when a pending frame has been overwritten by the producer + // This callback is optional (it may be NULL). + void (*on_frame_skipped)(struct video_buffer *vb, void *userdata); }; bool @@ -63,10 +67,8 @@ video_buffer_set_consumer_callbacks(struct video_buffer *vb, void *cbs_userdata); // set the producer frame as ready for consuming -// the output flag is set to report whether the previous frame has been skipped void -video_buffer_producer_offer_frame(struct video_buffer *vb, - bool *previous_frame_skipped); +video_buffer_producer_offer_frame(struct video_buffer *vb); // mark the consumer frame as consumed and return it // the frame is valid until the next call to this function From 955da3b57801b6ea14abdb6cbc7e530670b66ae7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 21 Feb 2021 21:47:55 +0100 Subject: [PATCH 0498/2244] Remove screen static initializer Most of the fields are initialized dynamically. --- app/src/screen.c | 8 +++++++- app/src/screen.h | 33 --------------------------------- 2 files changed, 7 insertions(+), 34 deletions(-) diff --git a/app/src/screen.c b/app/src/screen.c index bcccdd32..362ba1a8 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -215,10 +215,14 @@ on_frame_skipped(struct video_buffer *vb, void *userdata) { void screen_init(struct screen *screen, struct video_buffer *vb, struct fps_counter *fps_counter) { - *screen = (struct screen) SCREEN_INITIALIZER; screen->vb = vb; screen->fps_counter = fps_counter; + screen->resize_pending = false; + screen->has_frame = false; + screen->fullscreen = false; + screen->maximized = false; + static const struct video_buffer_callbacks cbs = { .on_frame_available = on_frame_available, .on_frame_skipped = on_frame_skipped, @@ -311,6 +315,8 @@ 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)"); + screen->mipmaps = false; + // starts with "opengl" bool use_opengl = renderer_name && !strncmp(renderer_name, "opengl", 6); if (use_opengl) { diff --git a/app/src/screen.h b/app/src/screen.h index 8941416e..da80fa2d 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -38,39 +38,6 @@ struct screen { bool mipmaps; }; -#define SCREEN_INITIALIZER { \ - .vb = NULL, \ - .fps_counter = NULL, \ - .window = NULL, \ - .renderer = NULL, \ - .texture = NULL, \ - .gl = {0}, \ - .frame_size = { \ - .width = 0, \ - .height = 0, \ - }, \ - .content_size = { \ - .width = 0, \ - .height = 0, \ - }, \ - .resize_pending = false, \ - .windowed_content_size = { \ - .width = 0, \ - .height = 0, \ - }, \ - .rotation = 0, \ - .rect = { \ - .x = 0, \ - .y = 0, \ - .w = 0, \ - .h = 0, \ - }, \ - .has_frame = false, \ - .fullscreen = false, \ - .maximized = false, \ - .mipmaps = false, \ -} - // initialize default values void screen_init(struct screen *screen, struct video_buffer *vb, From 597c54f0491eb279fed52a1386120ba06ababceb Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 25 Feb 2021 22:00:34 +0100 Subject: [PATCH 0499/2244] Group screen parameters into a struct The function screen_init_rendering had too many parameters. --- app/src/scrcpy.c | 20 ++++++++++++++------ app/src/screen.c | 45 ++++++++++++++++++++++----------------------- app/src/screen.h | 24 ++++++++++++++++++------ 3 files changed, 54 insertions(+), 35 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index e7879d5b..e683956c 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -384,12 +384,20 @@ scrcpy(const struct scrcpy_options *options) { screen_init(&screen, &video_buffer, &fps_counter); - if (!screen_init_rendering(&screen, window_title, frame_size, - options->always_on_top, options->window_x, - options->window_y, options->window_width, - options->window_height, - options->window_borderless, - options->rotation, options->mipmaps)) { + struct screen_params screen_params = { + .window_title = window_title, + .frame_size = frame_size, + .always_on_top = options->always_on_top, + .window_x = options->window_x, + .window_y = options->window_y, + .window_width = options->window_width, + .window_height = options->window_height, + .window_borderless = options->window_borderless, + .rotation = options->rotation, + .mipmaps = options->mipmaps, + }; + + if (!screen_init_rendering(&screen, &screen_params)) { goto end; } diff --git a/app/src/screen.c b/app/src/screen.c index 362ba1a8..850d0405 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -259,26 +259,25 @@ create_texture(struct screen *screen) { } 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, - uint8_t rotation, bool mipmaps) { - screen->frame_size = frame_size; - screen->rotation = rotation; - if (rotation) { - LOGI("Initial display rotation set to %u", rotation); +screen_init_rendering(struct screen *screen, + const struct screen_params *params) { + screen->frame_size = params->frame_size; + screen->rotation = params->rotation; + if (screen->rotation) { + LOGI("Initial display rotation set to %u", screen->rotation); } - struct size content_size = get_rotated_size(frame_size, screen->rotation); + struct size content_size = + get_rotated_size(screen->frame_size, screen->rotation); screen->content_size = content_size; - struct size window_size = - get_initial_optimal_size(content_size, window_width, window_height); + struct size window_size = get_initial_optimal_size(content_size, + params->window_width, + params->window_height); uint32_t window_flags = SDL_WINDOW_HIDDEN | SDL_WINDOW_RESIZABLE; #ifdef HIDPI_SUPPORT window_flags |= SDL_WINDOW_ALLOW_HIGHDPI; #endif - if (always_on_top) { + if (params->always_on_top) { #ifdef SCRCPY_SDL_HAS_WINDOW_ALWAYS_ON_TOP window_flags |= SDL_WINDOW_ALWAYS_ON_TOP; #else @@ -286,15 +285,15 @@ screen_init_rendering(struct screen *screen, const char *window_title, "(compile with SDL >= 2.0.5 to enable it)"); #endif } - if (window_borderless) { + if (params->window_borderless) { window_flags |= SDL_WINDOW_BORDERLESS; } - int x = window_x != SC_WINDOW_POSITION_UNDEFINED - ? window_x : (int) SDL_WINDOWPOS_UNDEFINED; - int y = window_y != SC_WINDOW_POSITION_UNDEFINED - ? window_y : (int) SDL_WINDOWPOS_UNDEFINED; - screen->window = SDL_CreateWindow(window_title, x, y, + int x = params->window_x != SC_WINDOW_POSITION_UNDEFINED + ? params->window_x : (int) SDL_WINDOWPOS_UNDEFINED; + int y = params->window_y != SC_WINDOW_POSITION_UNDEFINED + ? params->window_y : (int) SDL_WINDOWPOS_UNDEFINED; + screen->window = SDL_CreateWindow(params->window_title, x, y, window_size.width, window_size.height, window_flags); if (!screen->window) { @@ -325,7 +324,7 @@ screen_init_rendering(struct screen *screen, const char *window_title, LOGI("OpenGL version: %s", gl->version); - if (mipmaps) { + if (params->mipmaps) { bool supports_mipmaps = sc_opengl_version_at_least(gl, 3, 0, /* OpenGL 3.0+ */ 2, 0 /* OpenGL ES 2.0+ */); @@ -339,7 +338,7 @@ screen_init_rendering(struct screen *screen, const char *window_title, } else { LOGI("Trilinear filtering disabled"); } - } else if (mipmaps) { + } else if (params->mipmaps) { LOGD("Trilinear filtering disabled (not an OpenGL renderer)"); } @@ -351,8 +350,8 @@ screen_init_rendering(struct screen *screen, const char *window_title, LOGW("Could not load icon"); } - LOGI("Initial texture: %" PRIu16 "x%" PRIu16, frame_size.width, - frame_size.height); + LOGI("Initial texture: %" PRIu16 "x%" PRIu16, params->frame_size.width, + params->frame_size.height); screen->texture = create_texture(screen); if (!screen->texture) { LOGC("Could not create texture: %s", SDL_GetError()); diff --git a/app/src/screen.h b/app/src/screen.h index da80fa2d..1d22cbc5 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -38,19 +38,31 @@ struct screen { bool mipmaps; }; +struct screen_params { + const char *window_title; + struct size frame_size; + bool always_on_top; + + int16_t window_x; + int16_t window_y; + uint16_t window_width; // accepts SC_WINDOW_POSITION_UNDEFINED + uint16_t window_height; // accepts SC_WINDOW_POSITION_UNDEFINED + + bool window_borderless; + + uint8_t rotation; + bool mipmaps; +}; + // initialize default values void screen_init(struct screen *screen, struct video_buffer *vb, struct fps_counter *fps_counter); // initialize screen, create window, renderer and texture (window is hidden) -// 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, - int16_t window_x, int16_t window_y, uint16_t window_width, - uint16_t window_height, bool window_borderless, - uint8_t rotation, bool mipmaps); +screen_init_rendering(struct screen *screen, + const struct screen_params *params); // show the window void From cc48b243240c36e2a2043098ec80fb037d0b0e9b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 25 Feb 2021 22:06:33 +0100 Subject: [PATCH 0500/2244] Simplify screen initialization Use a single function to initialize the screen instance. --- app/src/scrcpy.c | 5 ++--- app/src/screen.c | 38 +++++++++++++++++--------------------- app/src/screen.h | 10 +++------- 3 files changed, 22 insertions(+), 31 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index e683956c..7ed9cb2c 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -382,8 +382,6 @@ scrcpy(const struct scrcpy_options *options) { const char *window_title = options->window_title ? options->window_title : device_name; - screen_init(&screen, &video_buffer, &fps_counter); - struct screen_params screen_params = { .window_title = window_title, .frame_size = frame_size, @@ -397,7 +395,8 @@ scrcpy(const struct scrcpy_options *options) { .mipmaps = options->mipmaps, }; - if (!screen_init_rendering(&screen, &screen_params)) { + if (!screen_init(&screen, &video_buffer, &fps_counter, + &screen_params)) { goto end; } diff --git a/app/src/screen.c b/app/src/screen.c index 850d0405..aa6f32b7 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -212,25 +212,6 @@ on_frame_skipped(struct video_buffer *vb, void *userdata) { fps_counter_add_skipped_frame(screen->fps_counter); } -void -screen_init(struct screen *screen, struct video_buffer *vb, - struct fps_counter *fps_counter) { - screen->vb = vb; - screen->fps_counter = fps_counter; - - screen->resize_pending = false; - screen->has_frame = false; - screen->fullscreen = false; - screen->maximized = false; - - static const struct video_buffer_callbacks cbs = { - .on_frame_available = on_frame_available, - .on_frame_skipped = on_frame_skipped, - }; - - video_buffer_set_consumer_callbacks(vb, &cbs, screen); -} - static inline SDL_Texture * create_texture(struct screen *screen) { SDL_Renderer *renderer = screen->renderer; @@ -259,8 +240,23 @@ create_texture(struct screen *screen) { } bool -screen_init_rendering(struct screen *screen, - const struct screen_params *params) { +screen_init(struct screen *screen, struct video_buffer *vb, + struct fps_counter *fps_counter, + const struct screen_params *params) { + screen->vb = vb; + screen->fps_counter = fps_counter; + + screen->resize_pending = false; + screen->has_frame = false; + screen->fullscreen = false; + screen->maximized = false; + + static const struct video_buffer_callbacks cbs = { + .on_frame_available = on_frame_available, + .on_frame_skipped = on_frame_skipped, + }; + video_buffer_set_consumer_callbacks(vb, &cbs, screen); + screen->frame_size = params->frame_size; screen->rotation = params->rotation; if (screen->rotation) { diff --git a/app/src/screen.h b/app/src/screen.h index 1d22cbc5..4e1d5e63 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -54,15 +54,11 @@ struct screen_params { bool mipmaps; }; -// initialize default values -void -screen_init(struct screen *screen, struct video_buffer *vb, - struct fps_counter *fps_counter); - // initialize screen, create window, renderer and texture (window is hidden) bool -screen_init_rendering(struct screen *screen, - const struct screen_params *params); +screen_init(struct screen *screen, struct video_buffer *vb, + struct fps_counter *fps_counter, + const struct screen_params *params); // show the window void From 386f017ba9ae52b55e32d3e3c1f068e29e893e1f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 21 Feb 2021 17:22:25 +0100 Subject: [PATCH 0501/2244] Factorize frame swap --- app/src/video_buffer.c | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/app/src/video_buffer.c b/app/src/video_buffer.c index 3ebffa7e..1aaf6945 100644 --- a/app/src/video_buffer.c +++ b/app/src/video_buffer.c @@ -70,20 +70,11 @@ video_buffer_destroy(struct video_buffer *vb) { av_frame_free(&vb->producer_frame); } -static void -video_buffer_swap_producer_frame(struct video_buffer *vb) { - sc_mutex_assert(&vb->mutex); - AVFrame *tmp = vb->producer_frame; - vb->producer_frame = vb->pending_frame; - vb->pending_frame = tmp; -} - -static void -video_buffer_swap_consumer_frame(struct video_buffer *vb) { - sc_mutex_assert(&vb->mutex); - AVFrame *tmp = vb->consumer_frame; - vb->consumer_frame = vb->pending_frame; - vb->pending_frame = tmp; +static inline void +swap_frames(AVFrame **lhs, AVFrame **rhs) { + AVFrame *tmp = *lhs; + *lhs = *rhs; + *rhs = tmp; } void @@ -109,7 +100,7 @@ video_buffer_producer_offer_frame(struct video_buffer *vb) { } } - video_buffer_swap_producer_frame(vb); + swap_frames(&vb->producer_frame, &vb->pending_frame); bool skipped = !vb->pending_frame_consumed; vb->pending_frame_consumed = false; @@ -130,7 +121,7 @@ video_buffer_consumer_take_frame(struct video_buffer *vb) { assert(!vb->pending_frame_consumed); vb->pending_frame_consumed = true; - video_buffer_swap_consumer_frame(vb); + swap_frames(&vb->consumer_frame, &vb->pending_frame); if (vb->wait_consumer) { // unblock video_buffer_offer_decoded_frame() From eb7e1070cf4d0e2940a27664ae17ae7f974eb839 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 21 Feb 2021 17:29:45 +0100 Subject: [PATCH 0502/2244] Release frame data as soon as possible During a frame swap, one of the two frames involved can be released. --- app/src/video_buffer.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/video_buffer.c b/app/src/video_buffer.c index 1aaf6945..94619840 100644 --- a/app/src/video_buffer.c +++ b/app/src/video_buffer.c @@ -100,6 +100,7 @@ video_buffer_producer_offer_frame(struct video_buffer *vb) { } } + av_frame_unref(vb->pending_frame); swap_frames(&vb->producer_frame, &vb->pending_frame); bool skipped = !vb->pending_frame_consumed; @@ -122,6 +123,7 @@ video_buffer_consumer_take_frame(struct video_buffer *vb) { vb->pending_frame_consumed = true; swap_frames(&vb->consumer_frame, &vb->pending_frame); + av_frame_unref(vb->pending_frame); if (vb->wait_consumer) { // unblock video_buffer_offer_decoded_frame() From d1789f082a6bbb883e964f2b6136f0c89ead3cde Mon Sep 17 00:00:00 2001 From: Biswapriyo Nath Date: Tue, 9 Mar 2021 13:22:58 +0530 Subject: [PATCH 0503/2244] meson: Do not use full path with mingw tools name This helps to use mingw toolchains which are not in /usr/bin path. PR #2185 Signed-off-by: Romain Vimont --- cross_win32.txt | 10 +++++----- cross_win64.txt | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/cross_win32.txt b/cross_win32.txt index a662ae20..0d8a843a 100644 --- a/cross_win32.txt +++ b/cross_win32.txt @@ -2,11 +2,11 @@ [binaries] name = 'mingw' -c = '/usr/bin/i686-w64-mingw32-gcc' -cpp = '/usr/bin/i686-w64-mingw32-g++' -ar = '/usr/bin/i686-w64-mingw32-ar' -strip = '/usr/bin/i686-w64-mingw32-strip' -pkgconfig = '/usr/bin/i686-w64-mingw32-pkg-config' +c = 'i686-w64-mingw32-gcc' +cpp = 'i686-w64-mingw32-g++' +ar = 'i686-w64-mingw32-ar' +strip = 'i686-w64-mingw32-strip' +pkgconfig = 'i686-w64-mingw32-pkg-config' [host_machine] system = 'windows' diff --git a/cross_win64.txt b/cross_win64.txt index de3836d5..6a39c391 100644 --- a/cross_win64.txt +++ b/cross_win64.txt @@ -2,11 +2,11 @@ [binaries] name = 'mingw' -c = '/usr/bin/x86_64-w64-mingw32-gcc' -cpp = '/usr/bin/x86_64-w64-mingw32-g++' -ar = '/usr/bin/x86_64-w64-mingw32-ar' -strip = '/usr/bin/x86_64-w64-mingw32-strip' -pkgconfig = '/usr/bin/x86_64-w64-mingw32-pkg-config' +c = 'x86_64-w64-mingw32-gcc' +cpp = 'x86_64-w64-mingw32-g++' +ar = 'x86_64-w64-mingw32-ar' +strip = 'x86_64-w64-mingw32-strip' +pkgconfig = 'x86_64-w64-mingw32-pkg-config' [host_machine] system = 'windows' From 429fdef04fbc40da9050f4b008a438feb56d3450 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 11 Mar 2021 10:55:12 +0100 Subject: [PATCH 0504/2244] Fix encoder parameter suggestion The option is --encoder, not --encoder-name. --- 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 f58e3867..7887a5a7 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -230,7 +230,7 @@ public final class Server { if (encoders != null && encoders.length > 0) { Ln.e("Try to use one of the available encoders:"); for (MediaCodecInfo encoder : encoders) { - Ln.e(" scrcpy --encoder-name '" + encoder.getName() + "'"); + Ln.e(" scrcpy --encoder '" + encoder.getName() + "'"); } } } From 40febf4a91f0d9c9d9e1a6390930639b3bf5a70e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 14 Mar 2021 18:03:42 +0100 Subject: [PATCH 0505/2244] Use device id 0 for touch/mouse events Virtual device is only for keyboard sources, not mouse or touchscreen sources. Here is the value of InputDevice.getDevice(-1).toString(): Input Device -1: Virtual Descriptor: ... Generation: 2 Location: built-in Keyboard Type: alphabetic Has Vibrator: false Has mic: false Sources: 0x301 ( keyboard dpad ) InputDevice.getDeviceId() documentation says: > An id of zero indicates that the event didn't come from a physical > device and maps to the default keymap. However, injecting events with a device id of 0 causes event.getDevice() to be null on the client-side. Commit 26529d377fe8aae2fd1ffa91405aa801a0ce8a57 used -1 as a workaround to avoid a NPE on a specific Android TV device. But this is a bug in the device system, which wrongly assumes that input device may not be null. A similar issue was present in Flutter, but it is now fixed: - - On the other hand, using an id of -1 for touchscreen events (which is invalid) causes issues for some apps: Therefore, use a device id of 0. An alternative could be to find an existing device matching the source, like "adb shell input" does. See getInputDeviceId(): But it seems better to indicate that the event didn't come from a physical device, and it would not solve #962 anyway, because an Android TV has no touchscreen. Refs #962 Fixes #2125 --- 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 d8059b43..2c5a1277 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -14,7 +14,7 @@ import java.util.concurrent.TimeUnit; public class Controller { - private static final int DEVICE_ID_VIRTUAL = -1; + private static final int DEFAULT_DEVICE_ID = 0; private static final ScheduledExecutorService EXECUTOR = Executors.newSingleThreadScheduledExecutor(); @@ -210,7 +210,7 @@ public class Controller { int source = nonPrimaryButtonPressed ? InputDevice.SOURCE_MOUSE : InputDevice.SOURCE_TOUCHSCREEN; MotionEvent event = MotionEvent - .obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEVICE_ID_VIRTUAL, 0, source, + .obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0); return device.injectEvent(event); } @@ -233,7 +233,7 @@ public class Controller { coords.setAxisValue(MotionEvent.AXIS_VSCROLL, vScroll); MotionEvent event = MotionEvent - .obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, 0, 1f, 1f, DEVICE_ID_VIRTUAL, 0, + .obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, 0, 1f, 1f, DEFAULT_DEVICE_ID, 0, InputDevice.SOURCE_TOUCHSCREEN, 0); return device.injectEvent(event); } From 0308ef43f22b5e3e937869b30326bd7209242e9a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 15 Mar 2021 18:43:30 +0100 Subject: [PATCH 0506/2244] Do not set buttons on touch events BUTTON_PRIMARY must not be set for touch events: > This button constant is not set in response to simple touches with a > finger or stylus tip. The user must actually push a button. Fixes #2169 --- server/src/main/java/com/genymobile/scrcpy/Controller.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index 2c5a1277..8f262ab6 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -208,6 +208,10 @@ public class Controller { // Right-click and middle-click only work if the source is a mouse boolean nonPrimaryButtonPressed = (buttons & ~MotionEvent.BUTTON_PRIMARY) != 0; int source = nonPrimaryButtonPressed ? InputDevice.SOURCE_MOUSE : InputDevice.SOURCE_TOUCHSCREEN; + if (source != InputDevice.SOURCE_MOUSE) { + // Buttons must not be set for touch events + buttons = 0; + } MotionEvent event = MotionEvent .obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, From dd453ad041b0dcf0486610a2215c2d86499d66a7 Mon Sep 17 00:00:00 2001 From: slingmint Date: Wed, 20 Jan 2021 13:13:42 -0600 Subject: [PATCH 0507/2244] Pass scrcpy-noconsole arguments through to scrcpy PR #2052 Signed-off-by: Romain Vimont --- data/scrcpy-noconsole.vbs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/data/scrcpy-noconsole.vbs b/data/scrcpy-noconsole.vbs index e11adba5..d509ad7f 100644 --- a/data/scrcpy-noconsole.vbs +++ b/data/scrcpy-noconsole.vbs @@ -1 +1,7 @@ -CreateObject("Wscript.Shell").Run "cmd /c scrcpy.exe", 0, false +strCommand = "cmd /c scrcpy.exe" + +For Each Arg In WScript.Arguments + strCommand = strCommand & " """ & replace(Arg, """", """""""""") & """" +Next + +CreateObject("Wscript.Shell").Run strCommand, 0, false From fb0bcaebc2d081f0b6992b49bfe2ce9b12298b51 Mon Sep 17 00:00:00 2001 From: Yu-Chen Lin Date: Sun, 21 Feb 2021 08:31:37 +0800 Subject: [PATCH 0508/2244] Export static method to power off screen in Device PR #824 Signed-off-by: Yu-Chen Lin Signed-off-by: Romain Vimont --- .../java/com/genymobile/scrcpy/Device.java | 40 +++++++++++++++++-- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index a8fdf677..624c9fa0 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -153,13 +153,17 @@ public final class Device { return Build.MODEL; } + public static boolean supportsInputEvents(int displayId) { + return displayId == 0 || Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q; + } + public boolean supportsInputEvents() { return supportsInputEvents; } - public boolean injectEvent(InputEvent inputEvent, int mode) { - if (!supportsInputEvents()) { - throw new AssertionError("Could not inject input event if !supportsInputEvents()"); + public static boolean injectEvent(InputEvent inputEvent, int mode, int displayId) { + if (!supportsInputEvents(displayId)) { + return false; } if (displayId != 0 && !InputManager.setDisplayId(inputEvent, displayId)) { @@ -169,10 +173,29 @@ public final class Device { return SERVICE_MANAGER.getInputManager().injectInputEvent(inputEvent, mode); } + public boolean injectEvent(InputEvent inputEvent, int mode) { + if (!supportsInputEvents()) { + throw new AssertionError("Could not inject input event if !supportsInputEvents()"); + } + + return injectEvent(inputEvent, mode, displayId); + } + + public static boolean injectEventOnDisplay(InputEvent event, int displayId) { + return injectEvent(event, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC, displayId); + } + public boolean injectEvent(InputEvent event) { return injectEvent(event, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC); } + public static boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState, int displayId) { + long now = SystemClock.uptimeMillis(); + KeyEvent event = new KeyEvent(now, now, action, keyCode, repeat, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0, + InputDevice.SOURCE_KEYBOARD); + return injectEventOnDisplay(event, displayId); + } + 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, @@ -180,6 +203,10 @@ public final class Device { return injectEvent(event); } + public static boolean injectKeycode(int keyCode, int displayId) { + return injectKeyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0, displayId) && injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0, displayId); + } + public boolean injectKeycode(int keyCode) { return injectKeyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0) && injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0); } @@ -249,6 +276,13 @@ public final class Device { return SurfaceControl.setDisplayPowerMode(d, mode); } + public static boolean powerOffScreen(int displayId) { + if (!isScreenOn()) { + return true; + } + return injectKeycode(KeyEvent.KEYCODE_POWER, displayId); + } + /** * Disable auto-rotation (if enabled), set the screen rotation and re-enable auto-rotation (if it was enabled). */ From 1d615a0d51d5f912a415c03009e3da058992fcf6 Mon Sep 17 00:00:00 2001 From: Yu-Chen Lin Date: Sun, 21 Feb 2021 08:42:04 +0800 Subject: [PATCH 0509/2244] Support power off on close PR #824 Signed-off-by: Yu-Chen Lin Signed-off-by: Romain Vimont --- app/src/cli.c | 6 +++++ 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 | 7 ++++-- 8 files changed, 40 insertions(+), 10 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index 7f31b32c..484c48ef 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -666,6 +666,7 @@ guess_record_format(const char *filename) { #define OPT_FORWARD_ALL_CLICKS 1023 #define OPT_LEGACY_PASTE 1024 #define OPT_ENCODER_NAME 1025 +#define OPT_POWER_OFF_ON_CLOSE 1026 bool scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { @@ -716,6 +717,8 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { {"window-height", required_argument, NULL, OPT_WINDOW_HEIGHT}, {"window-borderless", no_argument, NULL, OPT_WINDOW_BORDERLESS}, + {"power-off-on-close", no_argument, NULL, + OPT_POWER_OFF_ON_CLOSE}, {NULL, 0, NULL, 0 }, }; @@ -884,6 +887,9 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { case OPT_LEGACY_PASTE: opts->legacy_paste = true; break; + case OPT_POWER_OFF_ON_CLOSE: + opts->power_off_on_close = 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 7ed9cb2c..624ddbca 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -300,6 +300,7 @@ scrcpy(const struct scrcpy_options *options) { .codec_options = options->codec_options, .encoder_name = options->encoder_name, .force_adb_forward = options->force_adb_forward, + .power_off_on_close = options->power_off_on_close, }; if (!server_start(&server, options->serial, ¶ms)) { goto end; diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index b877a987..8ee70f60 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -82,6 +82,7 @@ struct scrcpy_options { bool forward_key_repeat; bool forward_all_clicks; bool legacy_paste; + bool power_off_on_close; }; #define SCRCPY_OPTIONS_DEFAULT { \ @@ -129,6 +130,7 @@ struct scrcpy_options { .forward_key_repeat = true, \ .forward_all_clicks = false, \ .legacy_paste = false, \ + .power_off_on_close = false, \ } bool diff --git a/app/src/server.c b/app/src/server.c index 096ac18f..a0b40f96 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -293,6 +293,7 @@ execute_server(struct server *server, const struct server_params *params) { params->stay_awake ? "true" : "false", params->codec_options ? params->codec_options : "-", params->encoder_name ? params->encoder_name : "-", + params->power_off_on_close ? "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 83c528ef..15306e4f 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -46,6 +46,7 @@ struct server_params { bool show_touches; bool stay_awake; bool force_adb_forward; + bool power_off_on_close; }; // 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 efaa059a..ccdc9fd8 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java +++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java @@ -19,19 +19,21 @@ public final class CleanUp { // not instantiable } - public static void configure(boolean disableShowTouches, int restoreStayOn, boolean restoreNormalPowerMode) throws IOException { - boolean needProcess = disableShowTouches || restoreStayOn != -1 || restoreNormalPowerMode; + public static void configure(boolean disableShowTouches, int restoreStayOn, boolean restoreNormalPowerMode, boolean powerOffScreen, int displayId) + throws IOException { + boolean needProcess = disableShowTouches || restoreStayOn != -1 || restoreNormalPowerMode || powerOffScreen; if (needProcess) { - startProcess(disableShowTouches, restoreStayOn, restoreNormalPowerMode); + startProcess(disableShowTouches, restoreStayOn, restoreNormalPowerMode, powerOffScreen, displayId); } else { // There is no additional clean up to do when scrcpy dies unlinkSelf(); } } - private static void startProcess(boolean disableShowTouches, int restoreStayOn, boolean restoreNormalPowerMode) throws IOException { + private static void startProcess(boolean disableShowTouches, int restoreStayOn, boolean restoreNormalPowerMode, boolean powerOffScreen, + int displayId) throws IOException { String[] cmd = {"app_process", "/", CleanUp.class.getName(), String.valueOf(disableShowTouches), String.valueOf( - restoreStayOn), String.valueOf(restoreNormalPowerMode)}; + restoreStayOn), String.valueOf(restoreNormalPowerMode), String.valueOf(powerOffScreen), String.valueOf(displayId)}; ProcessBuilder builder = new ProcessBuilder(cmd); builder.environment().put("CLASSPATH", SERVER_PATH); @@ -61,6 +63,8 @@ public final class CleanUp { boolean disableShowTouches = Boolean.parseBoolean(args[0]); int restoreStayOn = Integer.parseInt(args[1]); boolean restoreNormalPowerMode = Boolean.parseBoolean(args[2]); + boolean powerOffScreen = Boolean.parseBoolean(args[3]); + int displayId = Integer.parseInt(args[4]); if (disableShowTouches || restoreStayOn != -1) { ServiceManager serviceManager = new ServiceManager(); @@ -76,9 +80,12 @@ public final class CleanUp { } } - if (restoreNormalPowerMode) { - Ln.i("Restoring normal power mode"); - if (Device.isScreenOn()) { + if (Device.isScreenOn()) { + if (powerOffScreen) { + Ln.i("Power off screen"); + Device.powerOffScreen(displayId); + } else if (restoreNormalPowerMode) { + Ln.i("Restoring normal power mode"); Device.setScreenPowerMode(Device.POWER_MODE_NORMAL); } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 150d06a8..cf11df0f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -17,6 +17,7 @@ public class Options { private boolean stayAwake; private String codecOptions; private String encoderName; + private boolean powerOffScreenOnClose; public Ln.Level getLogLevel() { return logLevel; @@ -129,4 +130,12 @@ public class Options { public void setEncoderName(String encoderName) { this.encoderName = encoderName; } + + public void setPowerOffScreenOnClose(boolean powerOffScreenOnClose) { + this.powerOffScreenOnClose = powerOffScreenOnClose; + } + + public boolean getPowerOffScreenOnClose() { + return this.powerOffScreenOnClose; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 7887a5a7..674f314b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -50,7 +50,7 @@ public final class Server { } } - CleanUp.configure(mustDisableShowTouchesOnCleanUp, restoreStayOn, true); + CleanUp.configure(mustDisableShowTouchesOnCleanUp, restoreStayOn, true, options.getPowerOffScreenOnClose(), options.getDisplayId()); boolean tunnelForward = options.isTunnelForward(); @@ -135,7 +135,7 @@ public final class Server { "The server version (" + BuildConfig.VERSION_NAME + ") does not match the client " + "(" + clientVersion + ")"); } - final int expectedParameters = 15; + final int expectedParameters = 16; if (args.length != expectedParameters) { throw new IllegalArgumentException("Expecting " + expectedParameters + " parameters"); } @@ -185,6 +185,9 @@ public final class Server { String encoderName = "-".equals(args[14]) ? null : args[14]; options.setEncoderName(encoderName); + boolean powerOffScreenOnClose = Boolean.parseBoolean(args[15]); + options.setPowerOffScreenOnClose(powerOffScreenOnClose); + return options; } From 19ad107f1f277b1a7dcdf3678ed5e14f3a706be7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 16 Mar 2021 21:38:12 +0100 Subject: [PATCH 0510/2244] Add "Get the app" summary section Give quick instructions to download/install scrcpy. --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index 759e7920..74925f0d 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,15 @@ control it using keyboard and mouse. Packaging status +### Summary + + - Linux: `apt install scrcpy` + - Windows: [download][direct-win64] + - macOS: `brew install scrcpy` + +Build from sources: [BUILD] + + ### Linux On Debian (_testing_ and _sid_ for now) and Ubuntu (20.04): From 1fb79575250460e159161bbe5ca55335409ffec5 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 17 Mar 2021 08:52:16 +0100 Subject: [PATCH 0511/2244] Fix typo in README Refs --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 74925f0d..dd8a1850 100644 --- a/README.md +++ b/README.md @@ -420,7 +420,7 @@ _(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 + and landscape (the current running app may refuse, if it does not support the requested orientation). - [`--lock-video-orientation`](#lock-video-orientation) changes the mirroring orientation (the orientation of the video sent from the device to the From 3a4b10a38dec2514d4f588abe04dee508fdd772b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 28 Mar 2021 12:22:11 +0200 Subject: [PATCH 0512/2244] Improve --push-target example in README A common use case for the --push-target option is to pass the device Download directory. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index dd8a1850..b396571b 100644 --- a/README.md +++ b/README.md @@ -667,7 +667,7 @@ There is no visual feedback, a log is printed to the console. The target directory can be changed on start: ```bash -scrcpy --push-target /sdcard/foo/bar/ +scrcpy --push-target=/sdcard/Download/ ``` From 6a217b70f4f4f751a869597670df1a87656a4597 Mon Sep 17 00:00:00 2001 From: aruko-210 Date: Sun, 14 Mar 2021 00:21:26 +0900 Subject: [PATCH 0513/2244] Add Japanese translation for README.md PR #2195 Signed-off-by: Romain Vimont --- README.jp.md | 728 +++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 1 + 2 files changed, 729 insertions(+) create mode 100644 README.jp.md diff --git a/README.jp.md b/README.jp.md new file mode 100644 index 00000000..a37a6ba1 --- /dev/null +++ b/README.jp.md @@ -0,0 +1,728 @@ +_Only the original [README](README.md) is guaranteed to be up-to-date._ + +# scrcpy (v1.17) + +このアプリケーションはUSB(もしくは[TCP/IP経由][article-tcpip])で接続されたAndroidデバイスの表示と制御を提供します。このアプリケーションは _root_ でのアクセスを必要としません。このアプリケーションは _GNU/Linux_ 、 _Windows_ そして _macOS_ 上で動作します。 + +![screenshot](assets/screenshot-debian-600.jpg) + +以下に焦点を当てています: + + - **軽量** (ネイティブ、デバイス画面表示のみ) + - **パフォーマンス** (30~60fps) + - **クオリティ** (1920x1080以上) + - **低遅延** ([35~70ms][lowlatency]) + - **短い起動時間** (初回画像を1秒以内に表示) + - **非侵入型** (デバイスに何もインストールされていない状態になる) + +[lowlatency]: https://github.com/Genymobile/scrcpy/pull/646 + + +## 必要要件 + +AndroidデバイスはAPI21(Android 5.0)以上。 + +Androidデバイスで[adbデバッグが有効][enable-adb]であること。 + +[enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling + +一部のAndroidデバイスでは、キーボードとマウスを使用して制御する[追加オプション][control]を有効にする必要がある。 + +[control]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323 + + +## アプリの取得 + +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 + +[自分でビルド][BUILD]も可能(心配しないでください、それほど難しくはありません。) + + +### Windows + +Windowsでは簡単に、(`adb`を含む)すべての依存関係を構築済みのアーカイブを利用可能です。 + + - [`scrcpy-win64-v1.17.zip`][direct-win64] + _(SHA-256: 8b9e57993c707367ed10ebfe0e1ef563c7a29d9af4a355cd8b6a52a317c73eea)_ + +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.17/scrcpy-win64-v1.17.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 + +また、[アプリケーションをビルド][BUILD]することも可能です。 + +### macOS + +アプリケーションは[Homebrew]で利用可能です。ただインストールするだけです。 + +[Homebrew]: https://brew.sh/ + +```bash +brew install scrcpy +``` + +`PATH`から`adb`へのアクセスが必要です。もしまだ持っていない場合: + +```bash +# Homebrew >= 2.6.0 +brew install --cask android-platform-tools + +# Homebrew < 2.6.0 +brew cask install android-platform-tools +``` + +また、[アプリケーションをビルド][BUILD]することも可能です。 + + +## 実行 + +Androidデバイスを接続し、実行: + +```bash +scrcpy +``` + +次のコマンドでリストされるコマンドライン引数も受け付けます: + +```bash +scrcpy --help +``` + +## 機能 + +### キャプチャ構成 + +#### サイズ削減 + +Androidデバイスを低解像度でミラーリングする場合、パフォーマンス向上に便利な場合があります。 + +幅と高さをある値(例:1024)に制限するには: + +```bash +scrcpy --max-size 1024 +scrcpy -m 1024 # 短縮版 +``` + +一方のサイズはデバイスのアスペクト比が維持されるように計算されます。この方法では、1920x1080のデバイスでは1024x576にミラーリングされます。 + + +#### ビットレート変更 + +ビットレートの初期値は8Mbpsです。ビットレートを変更するには(例:2Mbpsに変更): + +```bash +scrcpy --bit-rate 2M +scrcpy -b 2M # 短縮版 +``` + +#### フレームレート制限 + +キャプチャするフレームレートを制限できます: + +```bash +scrcpy --max-fps 15 +``` + +この機能は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 --encoder OMX.qcom.video.encoder.avc +``` + +利用可能なエンコーダをリストするために、無効なエンコーダ名を渡すことができます。エラー表示で利用可能なエンコーダを提供します。 + +```bash +scrcpy --encoder _ +``` + +### 録画 + +ミラーリング中に画面の録画をすることが可能です: + +```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アドレスを取得します。設定 → 端末情報 → ステータス情報、もしくは、このコマンドを実行します: + + ```bash + adb shell ip route | awk '{print $9}' + ``` + +3. あなたのデバイスでTCP/IPを介したadbを有効にします: `adb tcpip 5555` +4. あなたのデバイスの接続を外します。 +5. あなたのデバイスに接続します: + `adb connect DEVICE_IP:5555` _(`DEVICE_IP`は置き換える)_ +6. 通常通り`scrcpy`を実行します。 + +この方法はビットレートと解像度を減らすのにおそらく有用です: + +```bash +scrcpy --bit-rate 2M --max-size 800 +scrcpy -b2M -m800 # 短縮版 +``` + +[接続]: https://developer.android.com/studio/command-line/adb.html#wireless + + +#### マルチデバイス + +もし`adb devices`でいくつかのデバイスがリストされる場合、 _シリアルナンバー_ を指定する必要があります: + +```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トンネル + +リモートデバイスに接続するため、ローカル`adb`クライアントからリモート`adb`サーバーへ接続することが可能です(同じバージョンの _adb_ プロトコルを使用している場合): + +```bash +adb kill-server # 5037ポートのローカルadbサーバーを終了する +ssh -CN -L5037:localhost:5037 -R27183:localhost:27183 your_remote_computer +# オープンしたままにする +``` + +他の端末から: + +```bash +scrcpy +``` + +リモートポート転送の有効化を回避するためには、代わりに転送接続を強制することができます(`-R`の代わりに`-L`を使用することに注意): + +```bash +adb kill-server # 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はデバイスに縦向きと横向きの切り替えを要求する(現在実行中のアプリで要求している向きをサポートしていない場合、拒否することがある) + - [`--lock-video-orientation`](#ビデオの向きをロックする)は、ミラーリングする向きを変更する(デバイスからPCへ送信される向き)。録画に影響します。 + - `--rotation` (もしくはMOD+/MOD+)は、ウィンドウのコンテンツのみを回転します。これは表示にのみに影響し、録画には影響しません。 + +### 他のミラーリングオプション + +#### Read-only リードオンリー + +制御を無効にするには(デバイスと対話する全てのもの:入力キー、マウスイベント、ファイルのドラッグ&ドロップ): + +```bash +scrcpy --no-control +scrcpy -n +``` + +#### ディスプレイ + +いくつか利用可能なディスプレイがある場合、ミラーリングするディスプレイを選択できます: + +```bash +scrcpy --display 1 +``` + +ディスプレイIDのリストは次の方法で取得できます: + +``` +adb shell dumpsys display # search "mDisplayId=" in the output +``` + +セカンダリディスプレイは、デバイスが少なくともAndroid 10の場合にコントロール可能です。(それ以外ではリードオンリーでミラーリングされます) + + +#### 起動状態にする + +デバイス接続時、少し遅れてからデバイスのスリープを防ぐには: + +```bash +scrcpy --stay-awake +scrcpy -w +``` + +scrcpyが閉じられた時、初期状態に復元されます。 + +#### 画面OFF + +コマンドラインオプションを使用することで、ミラーリングの開始時にデバイスの画面をOFFにすることができます: + +```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 +``` + +(デバイス上で指を使った) _物理的な_ タッチのみ表示されることに注意してください。 + + +#### スクリーンセーバー無効 + +初期状態では、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を介して)ペーストすることは、デバイスのクリップボードにコンテンツをコピーします。結果としてどのAndoridアプリケーションもそのコンテンツを読み取ることができます。機密性の高いコンテンツ(例えばパスワードなど)をこの方法でペーストすることは避けてください。 + +プログラムでデバイスのクリップボードを設定した場合、一部のデバイスは期待どおりに動作しません。`--legacy-paste`オプションは、コンピュータのクリップボードテキストをキーイベントのシーケンスとして挿入するため(MOD+Shift+vと同じ方法)、Ctrl+vMOD+vの動作の変更を提供します。 + +#### ピンチしてズームする + +"ピンチしてズームする"をシミュレートするには: Ctrl+_クリック&移動_ + +より正確にするには、左クリックボタンを押している間、Ctrlを押したままにします。左クリックボタンを離すまで、全てのマウスの動きは、(アプリでサポートされている場合)画面の中心を基準として、コンテンツを拡大縮小および回転します。 + +具体的には、scrcpyは画面の中央を反転した位置にある"バーチャルフィンガー"から追加のタッチイベントを生成します。 + + +#### テキストインジェクション環境設定 + +テキストをタイプした時に生成される2種類の[イベント][textevents]があります: + - _key events_ はキーを押したときと離したことを通知します。 + - _text 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 +``` + + +#### 右クリックと真ん中クリック + +初期状態では、右クリックはバックの動作(もしくはパワーオン)を起こし、真ん中クリックではホーム画面へ戻ります。このショートカットを無効にし、代わりにデバイスへクリックを転送するには: + +```bash +scrcpy --forward-all-clicks +``` + + +### ファイルのドロップ + +#### 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でショートカット変更します。初期状態では、(left)Altまたは(left)Superです。 + +これは`--shortcut-mod`で変更することができます。可能なキーは`lctrl`、`rctrl`、`lalt`、 `ralt`、 `lsuper`そして`rsuper`です。例えば: + +```bash +# RCtrlをショートカットとして使用します +scrcpy --shortcut-mod=rctrl + +# ショートカットにLCtrl+LAltまたはLSuperのいずれかを使用します +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 \| _ダブルクリック¹_ + | `HOME`をクリック | MOD+h \| _真ん中クリック_ + | `BACK`をクリック | MOD+b \| _右クリック²_ + | `APP_SWITCH`をクリック | MOD+s + | `MENU` (画面のアンロック)をクリック | MOD+m + | `VOLUME_UP`をクリック | MOD+ _(上)_ + | `VOLUME_DOWN`をクリック | MOD+ _(下)_ + | `POWER`をクリック | MOD+p + | 電源オン | _右クリック²_ + | デバイス画面をオフにする(ミラーリングしたまま) | MOD+o + | デバイス画面をオンにする | MOD+Shift+o + | デバイス画面を回転する | MOD+r + | 通知パネルを展開する | MOD+n + | 通知パネルを折りたたむ | MOD+Shift+n + | クリップボードへのコピー³ | MOD+c + | クリップボードへのカット³ | MOD+x + | クリップボードの同期とペースト³ | MOD+v + | コンピュータのクリップボードテキストの挿入 | MOD+Shift+v + | FPSカウンタ有効/無効(標準入出力上) | MOD+i + | ピンチしてズームする | Ctrl+_クリック&移動_ + +_¹黒い境界線を削除するため、境界線上でダブルクリック_ +_²もしスクリーンがオフの場合、右クリックでスクリーンをオンする。それ以外の場合はBackを押します._ +_³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]を参照してください。 + +[BUILD]: BUILD.md + + +## よくある質問 + +[FAQ](FAQ.md)を参照してください。 + + +## 開発者 + +[開発者のページ]を読んでください。 + +[開発者のページ]: DEVELOP.md + + +## ライセンス + + Copyright (C) 2018 Genymobile + Copyright (C) 2018-2021 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/ diff --git a/README.md b/README.md index b396571b..a62d2295 100644 --- a/README.md +++ b/README.md @@ -808,6 +808,7 @@ Read the [developers page]. This README is available in other languages: - [Indonesian (Indonesia, `id`) - v1.16](README.id.md) +- [日本語 (Japanese, `jp`) - v1.17](README.jp.md) - [한국어 (Korean, `ko`) - v1.11](README.ko.md) - [português brasileiro (Brazilian Portuguese, `pt-BR`) - v1.17](README.pt-br.md) - [简体中文 (Simplified Chinese, `zh-Hans`) - v1.17](README.zh-Hans.md) From 8414b688f00182e6e5a9f94c880f70dd76fa6ddf Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 28 Mar 2021 21:33:53 +0200 Subject: [PATCH 0514/2244] Link release to main README in translations This avoids to link an older version. --- README.id.md | 5 +---- README.jp.md | 5 +---- README.ko.md | 4 +--- README.pt-br.md | 5 +---- README.zh-Hans.md | 5 +---- README.zh-Hant.md | 5 +---- 6 files changed, 6 insertions(+), 23 deletions(-) diff --git a/README.id.md b/README.id.md index a2cfa3d5..b4b16735 100644 --- a/README.id.md +++ b/README.id.md @@ -69,10 +69,7 @@ Anda juga bisa [membangun aplikasi secara manual][BUILD] (jangan khawatir, tidak 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 + - [README](README.md#windows) Ini juga tersedia di [Chocolatey]: diff --git a/README.jp.md b/README.jp.md index a37a6ba1..e42c528e 100644 --- a/README.jp.md +++ b/README.jp.md @@ -71,10 +71,7 @@ Gentoo用[Ebuild]が利用可能: [`scrcpy`][ebuild-link] Windowsでは簡単に、(`adb`を含む)すべての依存関係を構築済みのアーカイブを利用可能です。 - - [`scrcpy-win64-v1.17.zip`][direct-win64] - _(SHA-256: 8b9e57993c707367ed10ebfe0e1ef563c7a29d9af4a355cd8b6a52a317c73eea)_ - -[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.17/scrcpy-win64-v1.17.zip + - [README](README.md#windows) [Chocolatey]でも利用可能です: diff --git a/README.ko.md b/README.ko.md index 58b44dfe..2da50562 100644 --- a/README.ko.md +++ b/README.ko.md @@ -68,9 +68,7 @@ Gentoo에서 ,[Ebuild] 가 가능합니다 : [`scrcpy/`][ebuild-link]. 윈도우 상에서, 간단하게 설치하기 위해 종속성이 있는 사전 구축된 아카이브가 제공됩니다 (`adb` 포함) : 해당 파일은 Readme원본 링크를 통해서 다운로드가 가능합니다. - - [`scrcpy-win`][direct-win] - -[direct-win]: https://github.com/Genymobile/scrcpy/blob/master/README.md#windows + - [README](README.md#windows) [어플을 직접 설치][BUILD] 할 수도 있습니다. diff --git a/README.pt-br.md b/README.pt-br.md index ebf7c7ec..3549f0fb 100644 --- a/README.pt-br.md +++ b/README.pt-br.md @@ -77,10 +77,7 @@ difícil). Para Windows, por simplicidade, um arquivo pré-compilado com todas as dependências (incluindo `adb`) está disponível: - - [`scrcpy-win64-v1.17.zip`][direct-win64] - _(SHA-256: 8b9e57993c707367ed10ebfe0e1ef563c7a29d9af4a355cd8b6a52a317c73eea)_ - -[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.17/scrcpy-win64-v1.17.zip + - [README](README.md#windows) Também está disponível em [Chocolatey]: diff --git a/README.zh-Hans.md b/README.zh-Hans.md index e758c18d..bdd8023c 100644 --- a/README.zh-Hans.md +++ b/README.zh-Hans.md @@ -78,10 +78,7 @@ apt install scrcpy 在 Windows 上,简便起见,我们提供包含了所有依赖 (包括 `adb`) 的预编译包。 - - [`scrcpy-win64-v1.17.zip`][direct-win64] - _(SHA-256: 8b9e57993c707367ed10ebfe0e1ef563c7a29d9af4a355cd8b6a52a317c73eea)_ - -[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.17/scrcpy-win64-v1.17.zip + - [README](README.md#windows) 也可以使用 [Chocolatey]: diff --git a/README.zh-Hant.md b/README.zh-Hant.md index b4dc69ec..c0e30254 100644 --- a/README.zh-Hant.md +++ b/README.zh-Hant.md @@ -80,10 +80,7 @@ apt install scrcpy 為了保持簡單,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 + - [README](README.md#windows) [Chocolatey] 上也可以下載: From 38f392f08ff9d473a052205e3217c38a3fbff492 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 29 Mar 2021 08:39:02 +0200 Subject: [PATCH 0515/2244] Fix typo in BUILD.md --- BUILD.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BUILD.md b/BUILD.md index fdcc569d..326c5971 100644 --- a/BUILD.md +++ b/BUILD.md @@ -255,7 +255,7 @@ You can then [run](README.md#run) _scrcpy_. ## Prebuilt server - [`scrcpy-server-v1.17`][direct-scrcpy-server] - _(SHA-256: 11b5ad2d1bc9b9730fb7254a78efd71a8ff46b1938ff468e47a21b653a1b6725_ + _(SHA-256: 11b5ad2d1bc9b9730fb7254a78efd71a8ff46b1938ff468e47a21b653a1b6725)_ [direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.17/scrcpy-server-v1.17 From fda293f9bbdfeb21c010550051dfb3aa87aea8dc Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 29 Mar 2021 09:11:07 +0200 Subject: [PATCH 0516/2244] Fix BUILD.md line wrapping --- BUILD.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/BUILD.md b/BUILD.md index 326c5971..a0c0f868 100644 --- a/BUILD.md +++ b/BUILD.md @@ -60,8 +60,7 @@ sudo apt install ffmpeg libsdl2-2.0-0 adb # client build dependencies sudo apt install gcc git pkg-config meson ninja-build \ - libavcodec-dev libavformat-dev libavutil-dev \ - libsdl2-dev + libavcodec-dev libavformat-dev libavutil-dev libsdl2-dev # server build dependencies sudo apt install openjdk-11-jdk From fc5de88eaacfd3ef0af2939f19706d72f9bc427d Mon Sep 17 00:00:00 2001 From: Ray Foss Date: Sun, 28 Mar 2021 23:11:04 -0500 Subject: [PATCH 0517/2244] Clarify the order of operations in BUILD.md PR #2223 Signed-off-by: Romain Vimont --- BUILD.md | 82 +++++++++++++++++++++++++++++--------------------------- 1 file changed, 42 insertions(+), 40 deletions(-) diff --git a/BUILD.md b/BUILD.md index a0c0f868..98837e69 100644 --- a/BUILD.md +++ b/BUILD.md @@ -2,11 +2,6 @@ Here are the instructions to build _scrcpy_ (client and server). -You may want to build only the client: the server binary, which will be pushed -to the Android device, does not depend on your system and architecture. In that -case, use the [prebuilt server] (so you will not need Java or the Android SDK). - -[prebuilt server]: #prebuilt-server ## Branches @@ -188,8 +183,27 @@ 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_SDK_ROOT` to its directory. For example: +**As a non-root user**, clone the project: + +```bash +git clone https://github.com/Genymobile/scrcpy +cd scrcpy +``` + + +### Build + +You may want to build only the client: the server binary, which will be pushed +to the Android device, does not depend on your system and architecture. In that +case, use the [prebuilt server] (so you will not need Java or the Android SDK). + +[prebuilt server]: #option-2-use-prebuilt-server + + +#### Option 1: Build everything from sources + +Install the [Android SDK] (_Android Studio_), and set `ANDROID_SDK_ROOT` to its +directory. For example: [Android SDK]: https://developer.android.com/studio/index.html @@ -202,20 +216,11 @@ export ANDROID_SDK_ROOT=~/Library/Android/sdk set ANDROID_SDK_ROOT=%LOCALAPPDATA%\Android\sdk ``` -If you don't want to build the server, use the [prebuilt server]. - -Clone the project: - -```bash -git clone https://github.com/Genymobile/scrcpy -cd scrcpy -``` - Then, build: ```bash meson x --buildtype release --strip -Db_lto=true -ninja -Cx +ninja -Cx # DO NOT RUN AS ROOT ``` _Note: `ninja` [must][ninja-user] be run as a non-root user (only `ninja @@ -224,9 +229,27 @@ install` must be run as root)._ [ninja-user]: https://github.com/Genymobile/scrcpy/commit/4c49b27e9f6be02b8e63b508b60535426bd0291a -### Run +#### Option 2: Use prebuilt server -To run without installing: + - [`scrcpy-server-v1.17`][direct-scrcpy-server] + _(SHA-256: 11b5ad2d1bc9b9730fb7254a78efd71a8ff46b1938ff468e47a21b653a1b6725)_ + +[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.17/scrcpy-server-v1.17 + +Download the prebuilt server somewhere, and specify its path during the Meson +configuration: + +```bash +meson x --buildtype release --strip -Db_lto=true \ + -Dprebuilt_server=/path/to/scrcpy-server +ninja -Cx # DO NOT RUN AS ROOT +``` + +The server only works with a matching client version (this server works with the +`master` branch). + + +### Run without installing: ```bash ./run x [options] @@ -249,24 +272,3 @@ This installs two files: Just remove them to "uninstall" the application. You can then [run](README.md#run) _scrcpy_. - - -## Prebuilt server - - - [`scrcpy-server-v1.17`][direct-scrcpy-server] - _(SHA-256: 11b5ad2d1bc9b9730fb7254a78efd71a8ff46b1938ff468e47a21b653a1b6725)_ - -[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.17/scrcpy-server-v1.17 - -Download the prebuilt server somewhere, and specify its path during the Meson -configuration: - -```bash -meson x --buildtype release --strip -Db_lto=true \ - -Dprebuilt_server=/path/to/scrcpy-server -ninja -Cx -sudo ninja -Cx install -``` - -The server only works with a matching client version (this server works with the -`master` branch). From 47d16a57aca02907e9e9a39c86d359ccf200181c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 29 Mar 2021 09:16:42 +0200 Subject: [PATCH 0518/2244] Add simplified installation script Add a script to download the server and build scrcpy using the prebuilt server. --- BUILD.md | 31 +++++++++++++++++++++++++++++++ README.md | 12 ++++++------ install_release.sh | 21 +++++++++++++++++++++ 3 files changed, 58 insertions(+), 6 deletions(-) create mode 100755 install_release.sh diff --git a/BUILD.md b/BUILD.md index 98837e69..f81ca495 100644 --- a/BUILD.md +++ b/BUILD.md @@ -3,6 +3,37 @@ Here are the instructions to build _scrcpy_ (client and server). +## Simple + +If you just want to install the latest release from `master`, follow this +simplified process. + +First, you need to install the required packages: + +```bash +# for Debian/Ubuntu +sudo apt install ffmpeg libsdl2-2.0-0 adb wget \ + gcc git pkg-config meson ninja-build \ + libavcodec-dev libavformat-dev libavutil-dev libsdl2-dev +``` + +Then clone the repo and execute the installation script +([source](install_release.sh)): + +```bash +git clone https://github.com/Genymobile/scrcpy +cd scrcpy +./install_release.sh +``` + +When a new release is out, update the repo and reinstall: + +```bash +git pull +./install_release.sh +``` + + ## Branches ### `master` diff --git a/README.md b/README.md index a62d2295..0b0dc8a6 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,10 @@ control it using keyboard and mouse. - Windows: [download][direct-win64] - macOS: `brew install scrcpy` -Build from sources: [BUILD] +Build from sources: [BUILD] ([simplified process][BUILD_simple]) + +[BUILD]: BUILD.md +[BUILD_simple]: BUILD.md#simple ### Linux @@ -76,9 +79,8 @@ For Gentoo, an [Ebuild] is available: [`scrcpy/`][ebuild-link]. [Ebuild]: https://wiki.gentoo.org/wiki/Ebuild [ebuild-link]: https://github.com/maggu2810/maggu2810-overlay/tree/master/app-mobilephone/scrcpy -You could also [build the app manually][BUILD] (don't worry, it's not that -hard). - +You could also [build the app manually][BUILD] ([simplified +process][BUILD_simple]). ### Windows @@ -763,8 +765,6 @@ A colleague challenged me to find a name as unpronounceable as [gnirehtet]. See [BUILD]. -[BUILD]: BUILD.md - ## Common issues diff --git a/install_release.sh b/install_release.sh new file mode 100755 index 00000000..5179c447 --- /dev/null +++ b/install_release.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +set -e + +BUILDDIR=build-auto +PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v1.17/scrcpy-server-v1.17 +PREBUILT_SERVER_SHA256=11b5ad2d1bc9b9730fb7254a78efd71a8ff46b1938ff468e47a21b653a1b6725 + +echo "[scrcpy] Downloading prebuilt server..." +wget "$PREBUILT_SERVER_URL" -O scrcpy-server +echo "[scrcpy] Verifying prebuilt server..." +echo "$PREBUILT_SERVER_SHA256 scrcpy-server" | sha256sum --check + +echo "[scrcpy] Building client..." +rm -rf "$BUILDDIR" +meson "$BUILDDIR" --buildtype release --strip -Db_lto=true \ + -Dprebuilt_server=scrcpy-server +cd "$BUILDDIR" +ninja + +echo "[scrcpy] Installing (sudo)..." +sudo ninja install From b77932a5b70bc3f60dbe589cfa5ae92472e139dd Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 1 Apr 2021 09:47:15 +0200 Subject: [PATCH 0519/2244] Add instructions to uninstall --- BUILD.md | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/BUILD.md b/BUILD.md index f81ca495..f20d5921 100644 --- a/BUILD.md +++ b/BUILD.md @@ -33,6 +33,12 @@ git pull ./install_release.sh ``` +To uninstall: + +```bash +sudo ninja -Cbuild-auto uninstall +``` + ## Branches @@ -295,11 +301,16 @@ After a successful build, you can install _scrcpy_ on the system: sudo ninja -Cx install # without sudo on Windows ``` -This installs two files: +This installs three files: - `/usr/local/bin/scrcpy` - `/usr/local/share/scrcpy/scrcpy-server` - -Just remove them to "uninstall" the application. + - `/usr/local/share/man/man1/scrcpy.1` You can then [run](README.md#run) _scrcpy_. + +### Uninstall + +```bash +sudo ninja -Cx uninstall # without sudo on Windows +``` From d50c678a5f9174b6ba99b27bed8faa789ed5e070 Mon Sep 17 00:00:00 2001 From: quyleanh Date: Fri, 2 Apr 2021 20:47:37 +0700 Subject: [PATCH 0520/2244] Update brew cask documentation The command `brew cask` has been deprecated: Should be `brew install` directly now. PR #2231 Signed-off-by: Romain Vimont --- BUILD.md | 2 +- README.md | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/BUILD.md b/BUILD.md index f20d5921..2ca08173 100644 --- a/BUILD.md +++ b/BUILD.md @@ -208,7 +208,7 @@ make it avaliable from the `PATH`: ```bash brew tap homebrew/cask-versions -brew cask install adoptopenjdk/openjdk/adoptopenjdk8 +brew install adoptopenjdk/openjdk/adoptopenjdk8 export JAVA_HOME="$(/usr/libexec/java_home --version 1.8)" export PATH="$JAVA_HOME/bin:$PATH" ``` diff --git a/README.md b/README.md index 0b0dc8a6..7a4b7d41 100644 --- a/README.md +++ b/README.md @@ -127,11 +127,7 @@ brew install scrcpy You need `adb`, accessible from your `PATH`. If you don't have it yet: ```bash -# Homebrew >= 2.6.0 -brew install --cask android-platform-tools - -# Homebrew < 2.6.0 -brew cask install android-platform-tools +brew install android-platform-tools ``` You can also [build the app manually][BUILD]. From 2812de8a9a0f20fa17126924407ecb6f613ac7e4 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 4 Apr 2021 14:36:48 +0200 Subject: [PATCH 0521/2244] Update brew java version to JDK 11 Refs f8524a2be738e6471ed2e48265f2663d8c21c0be Refs 7b51a0313ed9423f9252108fa573af68c11e6a2c --- BUILD.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/BUILD.md b/BUILD.md index 2ca08173..5fc505bf 100644 --- a/BUILD.md +++ b/BUILD.md @@ -208,8 +208,8 @@ make it avaliable from the `PATH`: ```bash brew tap homebrew/cask-versions -brew install adoptopenjdk/openjdk/adoptopenjdk8 -export JAVA_HOME="$(/usr/libexec/java_home --version 1.8)" +brew install adoptopenjdk/openjdk/adoptopenjdk11 +export JAVA_HOME="$(/usr/libexec/java_home --version 1.11)" export PATH="$JAVA_HOME/bin:$PATH" ``` From 9826c5c4a4d19f59a732c3322338b0df37fa2f5e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 4 Apr 2021 15:00:13 +0200 Subject: [PATCH 0522/2244] Remove HiDPI compilation flag Always enable HiDPI support, there is no reason to expose a compilation flag. --- app/meson.build | 3 --- app/src/screen.c | 7 +++---- meson_options.txt | 1 - 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/app/meson.build b/app/meson.build index d230702e..c2a59741 100644 --- a/app/meson.build +++ b/app/meson.build @@ -118,9 +118,6 @@ conf.set('DEFAULT_LOCAL_PORT_RANGE_LAST', '27199') # overridden by option --bit-rate conf.set('DEFAULT_BIT_RATE', '8000000') # 8Mbps -# enable High DPI support -conf.set('HIDPI_SUPPORT', get_option('hidpi_support')) - # run a server debugger and wait for a client to be attached conf.set('SERVER_DEBUGGER', get_option('server_debugger')) diff --git a/app/src/screen.c b/app/src/screen.c index aa6f32b7..93c059e6 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -269,10 +269,9 @@ screen_init(struct screen *screen, struct video_buffer *vb, struct size window_size = get_initial_optimal_size(content_size, params->window_width, params->window_height); - uint32_t window_flags = SDL_WINDOW_HIDDEN | SDL_WINDOW_RESIZABLE; -#ifdef HIDPI_SUPPORT - window_flags |= SDL_WINDOW_ALLOW_HIGHDPI; -#endif + uint32_t window_flags = SDL_WINDOW_HIDDEN + | SDL_WINDOW_RESIZABLE + | SDL_WINDOW_ALLOW_HIGHDPI; if (params->always_on_top) { #ifdef SCRCPY_SDL_HAS_WINDOW_ALWAYS_ON_TOP window_flags |= SDL_WINDOW_ALWAYS_ON_TOP; diff --git a/meson_options.txt b/meson_options.txt index b962380c..66ad5b25 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -3,6 +3,5 @@ option('compile_server', type: 'boolean', value: true, description: 'Build the s option('crossbuild_windows', type: 'boolean', value: false, description: 'Build for Windows from Linux') option('prebuilt_server', type: 'string', description: 'Path of the prebuilt server') 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 6231f683af03d967eeb1266272beae208abb919f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 5 Apr 2021 18:13:23 +0200 Subject: [PATCH 0523/2244] Fix compilation error for old decoding API Commits cb9c42bdcb198b0e3ebed893e00257b95f4dcedb and 441d3fb119ce05287e94b0ae0befbb2e75173688 updated the code only for the new decoding API. --- app/src/decoder.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/decoder.c b/app/src/decoder.c index 7da959c6..3408138c 100644 --- a/app/src/decoder.c +++ b/app/src/decoder.c @@ -61,7 +61,7 @@ decoder_push(struct decoder *decoder, const AVPacket *packet) { #else int got_picture; int len = avcodec_decode_video2(decoder->codec_ctx, - decoder->video_buffer->decoding_frame, + decoder->video_buffer->producer_frame, &got_picture, packet); if (len < 0) { @@ -69,7 +69,7 @@ decoder_push(struct decoder *decoder, const AVPacket *packet) { return false; } if (got_picture) { - push_frame(decoder); + video_buffer_producer_offer_frame(decoder->video_buffer); } #endif return true; From 07a85b7c94fc519c4950a12d7c992f51117b7e81 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 7 Apr 2021 15:00:30 +0200 Subject: [PATCH 0524/2244] Fix typo in command-line help Refs #2237 --- 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 f01b7941..5d5bcf10 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -210,7 +210,7 @@ scrcpy_print_usage(const char *arg0) { " Default is 0 (automatic).\n" "\n" " --window-height value\n" - " Set the initial window width.\n" + " Set the initial window height.\n" " Default is 0 (automatic).\n" "\n" "Shortcuts:\n" From a09733d175dc610bf81edf5273d028cf03e766bc Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 7 Apr 2021 10:21:03 +0200 Subject: [PATCH 0525/2244] Remove useless includes from decoder.c --- app/src/decoder.c | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/src/decoder.c b/app/src/decoder.c index 3408138c..a13cf75e 100644 --- a/app/src/decoder.c +++ b/app/src/decoder.c @@ -1,14 +1,9 @@ #include "decoder.h" #include -#include -#include -#include #include "events.h" -#include "recorder.h" #include "video_buffer.h" -#include "util/buffer_util.h" #include "util/log.h" void From 33006561c74c1655aaeb29c9a13ba704892beb37 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 10 Apr 2021 18:44:59 +0200 Subject: [PATCH 0526/2244] Remove useless forward declaration from stream.h --- app/src/stream.h | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/stream.h b/app/src/stream.h index 784e0402..d4a9bd4a 100644 --- a/app/src/stream.h +++ b/app/src/stream.h @@ -11,8 +11,6 @@ #include "util/net.h" #include "util/thread.h" -struct video_buffer; - struct stream { socket_t socket; sc_thread thread; From fb7870500a8e69bd79b6ca9f0e9ec95e8fb1315e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 10 Apr 2021 18:04:31 +0200 Subject: [PATCH 0527/2244] Remove unused field from input_manager --- app/src/input_manager.h | 2 -- app/src/scrcpy.c | 1 - 2 files changed, 3 deletions(-) diff --git a/app/src/input_manager.h b/app/src/input_manager.h index d10c96b5..160977b3 100644 --- a/app/src/input_manager.h +++ b/app/src/input_manager.h @@ -11,11 +11,9 @@ #include "fps_counter.h" #include "scrcpy.h" #include "screen.h" -#include "video_buffer.h" struct input_manager { struct controller *controller; - struct video_buffer *video_buffer; struct fps_counter *fps_counter; struct screen *screen; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 624ddbca..f996f2cf 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -41,7 +41,6 @@ static struct file_handler file_handler; static struct input_manager input_manager = { .controller = &controller, - .video_buffer = &video_buffer, .fps_counter = &fps_counter, .screen = &screen, .repeat = 0, From d0983db5924cc1bee69fb5fa868e7b74541a4b8c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 7 Apr 2021 16:41:57 +0200 Subject: [PATCH 0528/2244] Make internal recorder function static --- app/src/recorder.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/recorder.c b/app/src/recorder.c index 0aacb1a4..2e3b0c28 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -228,7 +228,7 @@ recorder_rescale_packet(struct recorder *recorder, AVPacket *packet) { av_packet_rescale_ts(packet, SCRCPY_TIME_BASE, ostream->time_base); } -bool +static bool recorder_write(struct recorder *recorder, AVPacket *packet) { if (!recorder->header_written) { if (packet->pts != AV_NOPTS_VALUE) { From 08fc6694e1adf98014d3fdfa3d4cfd886618aba0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 11 Apr 2021 13:07:44 +0200 Subject: [PATCH 0529/2244] Do not destroy uninitialized screen When --no-display was passed, screen_destroy() was called while screen_init() was never called. In practice, it did not crash because it just freed NULL pointers, but it was still incorrect. --- app/src/scrcpy.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index f996f2cf..6278cfd7 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -282,6 +282,7 @@ scrcpy(const struct scrcpy_options *options) { bool stream_started = false; bool controller_initialized = false; bool controller_started = false; + bool screen_initialized = false; bool record = !!options->record_filename; struct server_params params = { @@ -399,6 +400,7 @@ scrcpy(const struct scrcpy_options *options) { &screen_params)) { goto end; } + screen_initialized = true; if (options->turn_screen_off) { struct control_msg msg; @@ -427,9 +429,11 @@ scrcpy(const struct scrcpy_options *options) { ret = event_loop(options); LOGD("quit..."); - screen_destroy(&screen); - end: + if (screen_initialized) { + screen_destroy(&screen); + } + // stop stream and controller so that they don't continue once their socket // is shutdown if (stream_started) { From 28f6cbaea6a309d86d3267a9189cb0035df99860 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 11 Apr 2021 14:32:42 +0200 Subject: [PATCH 0530/2244] Destroy screen once stream is finished The screen receives callbacks from the decoder, fed by the stream. The decoder is run from the stream thread, so waiting for the end of stream is sufficient to avoid possible use-after-destroy. --- app/src/scrcpy.c | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 6278cfd7..e6ae4f4c 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -430,10 +430,6 @@ scrcpy(const struct scrcpy_options *options) { LOGD("quit..."); end: - if (screen_initialized) { - screen_destroy(&screen); - } - // stop stream and controller so that they don't continue once their socket // is shutdown if (stream_started) { @@ -459,6 +455,13 @@ end: if (stream_started) { stream_join(&stream); } + + // Destroy the screen only after the stream is guaranteed to be finished, + // because otherwise the screen could receive new frames after destruction + if (screen_initialized) { + screen_destroy(&screen); + } + if (controller_started) { controller_join(&controller); } From c6d7f5ee96b8e23267fbc6f7810b139ead39f3e7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 13 Apr 2021 22:04:38 +0200 Subject: [PATCH 0531/2244] Make screen_show_window() static It is only used from screen.c now. --- app/src/screen.c | 2 +- app/src/screen.h | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/app/src/screen.c b/app/src/screen.c index 93c059e6..3ded4263 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -365,7 +365,7 @@ screen_init(struct screen *screen, struct video_buffer *vb, return true; } -void +static void screen_show_window(struct screen *screen) { SDL_ShowWindow(screen->window); } diff --git a/app/src/screen.h b/app/src/screen.h index 4e1d5e63..e725bad6 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -60,10 +60,6 @@ screen_init(struct screen *screen, struct video_buffer *vb, struct fps_counter *fps_counter, const struct screen_params *params); -// show the window -void -screen_show_window(struct screen *screen); - // destroy window, renderer and texture (if any) void screen_destroy(struct screen *screen); From 65c4f487b306cb288ec9ee1155356de7939eac16 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 13 Apr 2021 22:10:45 +0200 Subject: [PATCH 0532/2244] Set initial fullscreen from screen.c --- app/src/scrcpy.c | 5 +---- app/src/screen.c | 4 ++++ app/src/screen.h | 2 ++ 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index e6ae4f4c..6f803d23 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -394,6 +394,7 @@ scrcpy(const struct scrcpy_options *options) { .window_borderless = options->window_borderless, .rotation = options->rotation, .mipmaps = options->mipmaps, + .fullscreen = options->fullscreen, }; if (!screen_init(&screen, &video_buffer, &fps_counter, @@ -411,10 +412,6 @@ scrcpy(const struct scrcpy_options *options) { LOGW("Could not request 'set screen power mode'"); } } - - if (options->fullscreen) { - screen_switch_fullscreen(&screen); - } } // now we consumed the header values, the socket receives the video stream diff --git a/app/src/screen.c b/app/src/screen.c index 3ded4263..ed810264 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -362,6 +362,10 @@ screen_init(struct screen *screen, struct video_buffer *vb, screen_update_content_rect(screen); + if (params->fullscreen) { + screen_switch_fullscreen(screen); + } + return true; } diff --git a/app/src/screen.h b/app/src/screen.h index e725bad6..dca65d41 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -52,6 +52,8 @@ struct screen_params { uint8_t rotation; bool mipmaps; + + bool fullscreen; }; // initialize screen, create window, renderer and texture (window is hidden) From c23c38f99dbed2776d9c0ec92b37da5788866df7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 13 Apr 2021 22:14:09 +0200 Subject: [PATCH 0533/2244] Move resizing workaround to screen.c --- app/src/scrcpy.c | 29 ----------------------------- app/src/screen.c | 27 +++++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 29 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 6f803d23..7d47d016 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -128,30 +128,6 @@ sdl_init_and_configure(bool display, const char *render_driver, return true; } - -#if defined(__APPLE__) || defined(__WINDOWS__) -# define CONTINUOUS_RESIZING_WORKAROUND -#endif - -#ifdef CONTINUOUS_RESIZING_WORKAROUND -// On Windows and MacOS, resizing blocks the event loop, so resizing events are -// not triggered. As a workaround, handle them in an event handler. -// -// -// -static int -event_watcher(void *data, SDL_Event *event) { - (void) data; - if (event->type == SDL_WINDOWEVENT - && 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, true); - } - return 0; -} -#endif - static bool is_apk(const char *file) { const char *ext = strrchr(file, '.'); @@ -209,11 +185,6 @@ end: static bool event_loop(const struct scrcpy_options *options) { -#ifdef CONTINUOUS_RESIZING_WORKAROUND - if (options->display) { - SDL_AddEventWatch(event_watcher, NULL); - } -#endif SDL_Event event; while (SDL_WaitEvent(&event)) { enum event_result result = handle_event(&event, options); diff --git a/app/src/screen.c b/app/src/screen.c index ed810264..934e418f 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -239,6 +239,29 @@ create_texture(struct screen *screen) { return texture; } +#if defined(__APPLE__) || defined(__WINDOWS__) +# define CONTINUOUS_RESIZING_WORKAROUND +#endif + +#ifdef CONTINUOUS_RESIZING_WORKAROUND +// On Windows and MacOS, resizing blocks the event loop, so resizing events are +// not triggered. As a workaround, handle them in an event handler. +// +// +// +static int +event_watcher(void *data, SDL_Event *event) { + struct screen *screen = data; + if (event->type == SDL_WINDOWEVENT + && 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, true); + } + return 0; +} +#endif + bool screen_init(struct screen *screen, struct video_buffer *vb, struct fps_counter *fps_counter, @@ -366,6 +389,10 @@ screen_init(struct screen *screen, struct video_buffer *vb, screen_switch_fullscreen(screen); } +#ifdef CONTINUOUS_RESIZING_WORKAROUND + SDL_AddEventWatch(event_watcher, screen); +#endif + return true; } From 8ef4c044fa476dc40e20ac8d3045c0de41371a7c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 13 Apr 2021 22:17:46 +0200 Subject: [PATCH 0534/2244] Do not forward SDL_DROPFILE event The event is handled by scrcpy.c, it is not necessary to send it to screen or input_manager. --- 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 7d47d016..388bb73d 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -167,7 +167,7 @@ handle_event(SDL_Event *event, const struct scrcpy_options *options) { action = ACTION_PUSH_FILE; } file_handler_request(&file_handler, action, file); - break; + goto end; } } From edee69d6374639d165d12fecc5614f38c6b117c8 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 16 Apr 2021 17:40:39 +0200 Subject: [PATCH 0535/2244] Fix options alphabetical order "verbosity" < "version" --- app/scrcpy.1 | 8 ++++---- app/src/cli.c | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 92b8e1e3..bf3bc9cd 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -187,16 +187,16 @@ Enable "show touches" on start, restore the initial value on exit. It only shows physical touches (not clicks from scrcpy). -.TP -.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 \-v, \-\-version +Print the version of scrcpy. + .TP .B \-w, \-\-stay-awake Keep the device on while scrcpy is running, when the device is plugged in. diff --git a/app/src/cli.c b/app/src/cli.c index 484c48ef..042a1f4c 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -179,9 +179,6 @@ scrcpy_print_usage(const char *arg0) { " on exit.\n" " It only shows physical touches (not clicks from scrcpy).\n" "\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 @@ -189,6 +186,9 @@ scrcpy_print_usage(const char *arg0) { #else " Default is info.\n" #endif + "\n" + " -v, --version\n" + " Print the version of scrcpy.\n" "\n" " -w, --stay-awake\n" " Keep the device on while scrcpy is running, when the device\n" From 8cc057c8f14d051b2480bad5116083bbeb2a2270 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 16 Apr 2021 17:53:37 +0200 Subject: [PATCH 0536/2244] Prevent forwarding only "mouse released" events Some mouse clicks DOWN are captured for shortcuts, but the matching UP events were still forwarded to the device. Instead, capture both DOWN and UP for shortcuts, and do nothing on UP. PR #2259 Refs #2258 --- app/src/input_manager.c | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 7226d68f..f6b1a96a 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -646,13 +646,17 @@ input_manager_process_mouse_button(struct input_manager *im, } bool down = event->type == SDL_MOUSEBUTTONDOWN; - if (!im->forward_all_clicks && down) { + if (!im->forward_all_clicks) { if (control && event->button == SDL_BUTTON_RIGHT) { - press_back_or_turn_screen_on(im->controller); + if (down) { + press_back_or_turn_screen_on(im->controller); + } return; } if (control && event->button == SDL_BUTTON_MIDDLE) { - action_home(im->controller, ACTION_DOWN | ACTION_UP); + if (down) { + action_home(im->controller, ACTION_DOWN | ACTION_UP); + } return; } @@ -665,7 +669,9 @@ input_manager_process_mouse_button(struct input_manager *im, 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); + if (down) { + screen_resize_to_fit(im->screen); + } return; } } From 964b6d2243fa1921543e48810f3064b9bd2d50d1 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 16 Apr 2021 17:58:22 +0200 Subject: [PATCH 0537/2244] Forward DOWN and UP separately for middle-click As a consequence of this change, the HOME button is now handled by Android on mouse released. This is consistent with the keyboard shortcut (MOD+h) behavior. PR #2259 Refs #2258 --- 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 f6b1a96a..10af6e8b 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -647,6 +647,8 @@ input_manager_process_mouse_button(struct input_manager *im, bool down = event->type == SDL_MOUSEBUTTONDOWN; if (!im->forward_all_clicks) { + int action = down ? ACTION_DOWN : ACTION_UP; + if (control && event->button == SDL_BUTTON_RIGHT) { if (down) { press_back_or_turn_screen_on(im->controller); @@ -654,9 +656,7 @@ input_manager_process_mouse_button(struct input_manager *im, return; } if (control && event->button == SDL_BUTTON_MIDDLE) { - if (down) { - action_home(im->controller, ACTION_DOWN | ACTION_UP); - } + action_home(im->controller, action); return; } From 498ad23e9804cd7d7bb56bb1cd0d26b88e1830b9 Mon Sep 17 00:00:00 2001 From: Andrea Gelmini Date: Sun, 18 Apr 2021 13:33:15 +0200 Subject: [PATCH 0538/2244] Fix typos PR #2263 Signed-off-by: Romain Vimont --- BUILD.md | 2 +- DEVELOP.md | 2 +- app/src/android/input.h | 2 +- app/src/event_converter.c | 2 +- app/src/util/str_util.h | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/BUILD.md b/BUILD.md index 5fc505bf..4dc6d64c 100644 --- a/BUILD.md +++ b/BUILD.md @@ -204,7 +204,7 @@ brew install pkg-config meson ``` Additionally, if you want to build the server, install Java 8 from Caskroom, and -make it avaliable from the `PATH`: +make it available from the `PATH`: ```bash brew tap homebrew/cask-versions diff --git a/DEVELOP.md b/DEVELOP.md index 4d8acc59..d11f139e 100644 --- a/DEVELOP.md +++ b/DEVELOP.md @@ -211,7 +211,7 @@ There are two [frames][video_buffer] simultaneously in memory: - the **rendering** frame, rendered in a texture from the main thread. When a new decoded frame is available, the decoder _swaps_ the decoding and -rendering frame (with proper synchronization). Thus, it immediatly starts +rendering frame (with proper synchronization). Thus, it immediately starts to decode a new frame while the main thread renders the last one. If a [recorder] is present (i.e. `--record` is enabled), then it muxes the raw diff --git a/app/src/android/input.h b/app/src/android/input.h index b51731b4..30c4bcb9 100644 --- a/app/src/android/input.h +++ b/app/src/android/input.h @@ -21,7 +21,7 @@ #define _ANDROID_INPUT_H /** - * Meta key / modifer state. + * Meta key / modifier state. */ enum android_metastate { /** No meta keys are pressed. */ diff --git a/app/src/event_converter.c b/app/src/event_converter.c index ab48898d..84c3999a 100644 --- a/app/src/event_converter.c +++ b/app/src/event_converter.c @@ -16,7 +16,7 @@ convert_keycode_action(SDL_EventType from, enum android_keyevent_action *to) { static enum android_metastate autocomplete_metastate(enum android_metastate metastate) { - // fill dependant flags + // fill dependent flags if (metastate & (AMETA_SHIFT_LEFT_ON | AMETA_SHIFT_RIGHT_ON)) { metastate |= AMETA_SHIFT_ON; } diff --git a/app/src/util/str_util.h b/app/src/util/str_util.h index c7f26cdb..2fb6b4ee 100644 --- a/app/src/util/str_util.h +++ b/app/src/util/str_util.h @@ -16,7 +16,7 @@ size_t xstrncpy(char *dest, const char *src, size_t n); // join tokens by sep into dst -// returns the number of chars actually written (max n-1) if no trucation +// returns the number of chars actually written (max n-1) if no truncation // occurred, or n if truncated size_t xstrjoin(char *dst, const char *const tokens[], char sep, size_t n); From d0739911a3e413b70275ded3eef839f9dc57ba7a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 16 Apr 2021 18:37:50 +0200 Subject: [PATCH 0539/2244] Forward DOWN and UP separately for right-click The shortcut "back on screen on" is a bit special: the control is requested by the client, but the actual event injection (POWER or BACK) is determined on the device. To properly inject DOWN and UP events for BACK, transmit the action as a control parameter. If the screen is off: - on DOWN, inject POWER (DOWN and UP) (wake up the device immediately) - on UP, do nothing If the screen is on: - on DOWN, inject BACK DOWN - on UP, inject BACK UP A corner case is when the screen turns off between the DOWN and UP event. In that case, a BACK UP event will be injected, so it's harmless. As a consequence of this change, the BACK button is now handled by Android on mouse released. This is consistent with the keyboard shortcut (Mod+b) behavior. PR #2259 Refs #2258 --- app/src/control_msg.c | 4 +++- app/src/control_msg.h | 4 ++++ app/src/input_manager.c | 22 ++++++++++++++----- app/tests/test_control_msg_serialize.c | 6 ++++- .../com/genymobile/scrcpy/ControlMessage.java | 7 ++++++ .../scrcpy/ControlMessageReader.java | 13 ++++++++++- .../com/genymobile/scrcpy/Controller.java | 20 ++++++++++++----- .../scrcpy/ControlMessageReaderTest.java | 2 ++ 8 files changed, 64 insertions(+), 14 deletions(-) diff --git a/app/src/control_msg.c b/app/src/control_msg.c index 436a8861..69e75014 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -67,6 +67,9 @@ control_msg_serialize(const struct control_msg *msg, unsigned char *buf) { buffer_write32be(&buf[17], (uint32_t) msg->inject_scroll_event.vscroll); return 21; + case CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON: + buf[1] = msg->inject_keycode.action; + return 2; case CONTROL_MSG_TYPE_SET_CLIPBOARD: { buf[1] = !!msg->set_clipboard.paste; size_t len = write_string(msg->set_clipboard.text, @@ -77,7 +80,6 @@ control_msg_serialize(const struct control_msg *msg, unsigned char *buf) { case CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE: buf[1] = msg->set_screen_power_mode.mode; return 2; - case CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON: case CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL: case CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL: case CONTROL_MSG_TYPE_GET_CLIPBOARD: diff --git a/app/src/control_msg.h b/app/src/control_msg.h index 1b25591d..8d9ab7d4 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -64,6 +64,10 @@ struct control_msg { int32_t hscroll; int32_t vscroll; } inject_scroll_event; + struct { + enum android_keyevent_action action; // action for the BACK key + // screen may only be turned on on ACTION_DOWN + } back_or_screen_on; struct { char *text; // owned, to be freed by free() bool paste; diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 10af6e8b..c10e53af 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -146,13 +146,25 @@ action_cut(struct controller *controller, int actions) { } // turn the screen on if it was off, press BACK otherwise +// If the screen is off, it is turned on only on ACTION_DOWN static void -press_back_or_turn_screen_on(struct controller *controller) { +press_back_or_turn_screen_on(struct controller *controller, int actions) { struct control_msg msg; msg.type = CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON; - if (!controller_push_msg(controller, &msg)) { - LOGW("Could not request 'press back or turn screen on'"); + if (actions & ACTION_DOWN) { + msg.back_or_screen_on.action = AKEY_EVENT_ACTION_DOWN; + if (!controller_push_msg(controller, &msg)) { + LOGW("Could not request 'press back or turn screen on'"); + return; + } + } + + if (actions & ACTION_UP) { + msg.back_or_screen_on.action = AKEY_EVENT_ACTION_UP; + if (!controller_push_msg(controller, &msg)) { + LOGW("Could not request 'press back or turn screen on'"); + } } } @@ -650,9 +662,7 @@ input_manager_process_mouse_button(struct input_manager *im, int action = down ? ACTION_DOWN : ACTION_UP; if (control && event->button == SDL_BUTTON_RIGHT) { - if (down) { - press_back_or_turn_screen_on(im->controller); - } + press_back_or_turn_screen_on(im->controller, action); return; } if (control && event->button == SDL_BUTTON_MIDDLE) { diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c index dc6e0821..4771ce1f 100644 --- a/app/tests/test_control_msg_serialize.c +++ b/app/tests/test_control_msg_serialize.c @@ -146,14 +146,18 @@ static void test_serialize_inject_scroll_event(void) { static void test_serialize_back_or_screen_on(void) { struct control_msg msg = { .type = CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON, + .back_or_screen_on = { + .action = AKEY_EVENT_ACTION_UP, + }, }; unsigned char buf[CONTROL_MSG_MAX_SIZE]; size_t size = control_msg_serialize(&msg, buf); - assert(size == 1); + assert(size == 2); const unsigned char expected[] = { CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON, + 0x01, // AKEY_EVENT_ACTION_UP }; 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 736acf80..44cb1b59 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java @@ -71,6 +71,13 @@ public final class ControlMessage { return msg; } + public static ControlMessage createBackOrScreenOn(int action) { + ControlMessage msg = new ControlMessage(); + msg.type = TYPE_BACK_OR_SCREEN_ON; + msg.action = action; + return msg; + } + public static ControlMessage createSetClipboard(String text, boolean paste) { ControlMessage msg = new ControlMessage(); msg.type = TYPE_SET_CLIPBOARD; diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java index ce185103..7ebecf76 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java @@ -11,6 +11,7 @@ public class ControlMessageReader { 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 BACK_OR_SCREEN_ON_LENGTH = 1; static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1; static final int SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH = 1; @@ -66,13 +67,15 @@ public class ControlMessageReader { case ControlMessage.TYPE_INJECT_SCROLL_EVENT: msg = parseInjectScrollEvent(); break; + case ControlMessage.TYPE_BACK_OR_SCREEN_ON: + msg = parseBackOrScreenOnEvent(); + break; case ControlMessage.TYPE_SET_CLIPBOARD: msg = parseSetClipboard(); break; case ControlMessage.TYPE_SET_SCREEN_POWER_MODE: msg = parseSetScreenPowerMode(); break; - case ControlMessage.TYPE_BACK_OR_SCREEN_ON: case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL: case ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL: case ControlMessage.TYPE_GET_CLIPBOARD: @@ -150,6 +153,14 @@ public class ControlMessageReader { return ControlMessage.createInjectScrollEvent(position, hScroll, vScroll); } + private ControlMessage parseBackOrScreenOnEvent() { + if (buffer.remaining() < BACK_OR_SCREEN_ON_LENGTH) { + return null; + } + int action = toUnsigned(buffer.get()); + return ControlMessage.createBackOrScreenOn(action); + } + private ControlMessage parseSetClipboard() { if (buffer.remaining() < SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH) { return null; diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index 8f262ab6..6af5ddf6 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -101,7 +101,7 @@ public class Controller { break; case ControlMessage.TYPE_BACK_OR_SCREEN_ON: if (device.supportsInputEvents()) { - pressBackOrTurnScreenOn(); + pressBackOrTurnScreenOn(msg.getAction()); } break; case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL: @@ -255,12 +255,22 @@ public class Controller { }, 200, TimeUnit.MILLISECONDS); } - private boolean pressBackOrTurnScreenOn() { - int keycode = Device.isScreenOn() ? KeyEvent.KEYCODE_BACK : KeyEvent.KEYCODE_POWER; - if (keepPowerModeOff && keycode == KeyEvent.KEYCODE_POWER) { + private boolean pressBackOrTurnScreenOn(int action) { + if (Device.isScreenOn()) { + return device.injectKeyEvent(action, KeyEvent.KEYCODE_BACK, 0, 0); + } + + // Screen is off + // Only press POWER on ACTION_DOWN + if (action != KeyEvent.ACTION_DOWN) { + // do nothing, + return true; + } + + if (keepPowerModeOff) { schedulePowerModeOff(); } - return device.injectKeycode(keycode); + return device.injectKeycode(KeyEvent.KEYCODE_POWER); } private boolean setClipboard(String text, boolean paste) { diff --git a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java index 5eb52760..6167ddeb 100644 --- a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java @@ -154,6 +154,7 @@ public class ControlMessageReaderTest { ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_BACK_OR_SCREEN_ON); + dos.writeByte(KeyEvent.ACTION_UP); byte[] packet = bos.toByteArray(); @@ -161,6 +162,7 @@ public class ControlMessageReaderTest { ControlMessage event = reader.next(); Assert.assertEquals(ControlMessage.TYPE_BACK_OR_SCREEN_ON, event.getType()); + Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction()); } @Test From b9c3f65fd84f27ebbe18f96c9eb207b6bf04eb78 Mon Sep 17 00:00:00 2001 From: brunoais Date: Mon, 12 Apr 2021 10:15:33 +0100 Subject: [PATCH 0540/2244] Provide actions for the extra mouse buttons Bind APP_SWITCH to button 4 and expand notification panel on button 5. PR #2258 Signed-off-by: Romain Vimont --- app/src/input_manager.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index c10e53af..b8a8c846 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -661,6 +661,14 @@ input_manager_process_mouse_button(struct input_manager *im, if (!im->forward_all_clicks) { int action = down ? ACTION_DOWN : ACTION_UP; + if (control && event->button == SDL_BUTTON_X1) { + action_app_switch(im->controller, action); + return; + } + if (control && event->button == SDL_BUTTON_X2 && down) { + expand_notification_panel(im->controller); + return; + } if (control && event->button == SDL_BUTTON_RIGHT) { press_back_or_turn_screen_on(im->controller, action); return; From aaf7875d923e492047e66ca88585079704abf5ad Mon Sep 17 00:00:00 2001 From: Frank Leon Rose Date: Thu, 22 Apr 2021 13:59:46 -0400 Subject: [PATCH 0541/2244] Ensure get_server_path() retval is freeable PR #2276 Signed-off-by: Romain Vimont --- app/src/server.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/server.c b/app/src/server.c index a0b40f96..a4bba33c 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -58,7 +58,7 @@ get_server_path(void) { LOGE("Could not get executable path, " "using " SERVER_FILENAME " from current directory"); // not found, use current directory - return SERVER_FILENAME; + return strdup(SERVER_FILENAME); } char *dir = dirname(executable_path); size_t dirlen = strlen(dir); @@ -70,7 +70,7 @@ get_server_path(void) { LOGE("Could not alloc server path string, " "using " SERVER_FILENAME " from current directory"); free(executable_path); - return SERVER_FILENAME; + return strdup(SERVER_FILENAME); } memcpy(server_path, dir, dirlen); From bb4614d55899bd1df00b14d3c7f3bdac3a5c818a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 20 Apr 2021 20:59:58 +0200 Subject: [PATCH 0542/2244] Reverse boolean logic for readability Refs #2260 --- .../com/genymobile/scrcpy/wrappers/ActivityManager.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java index 71967c50..93ed4528 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java @@ -14,7 +14,7 @@ public class ActivityManager { private final IInterface manager; private Method getContentProviderExternalMethod; - private boolean getContentProviderExternalMethodLegacy; + private boolean getContentProviderExternalMethodNewVersion = true; private Method removeContentProviderExternalMethod; public ActivityManager(IInterface manager) { @@ -29,7 +29,7 @@ public class ActivityManager { } catch (NoSuchMethodException e) { // old version getContentProviderExternalMethod = manager.getClass().getMethod("getContentProviderExternal", String.class, int.class, IBinder.class); - getContentProviderExternalMethodLegacy = true; + getContentProviderExternalMethodNewVersion = false; } } return getContentProviderExternalMethod; @@ -46,7 +46,7 @@ public class ActivityManager { try { Method method = getGetContentProviderExternalMethod(); Object[] args; - if (!getContentProviderExternalMethodLegacy) { + if (getContentProviderExternalMethodNewVersion) { // new version args = new Object[]{name, ServiceManager.USER_ID, token, null}; } else { From 66c581851fe968a12bf66c225dd76ff4690841f1 Mon Sep 17 00:00:00 2001 From: brunoais Date: Sat, 17 Apr 2021 13:26:54 +0100 Subject: [PATCH 0543/2244] Rename control message type to COLLAPSE_PANELS The collapsing action collapses any panels. By the way, the Android method is named collapsePanels(). PR #2260 Signed-off-by: Romain Vimont --- app/src/control_msg.c | 2 +- app/src/control_msg.h | 2 +- app/src/input_manager.c | 6 +++--- app/tests/test_control_msg_serialize.c | 8 ++++---- .../main/java/com/genymobile/scrcpy/ControlMessage.java | 2 +- .../java/com/genymobile/scrcpy/ControlMessageReader.java | 2 +- .../src/main/java/com/genymobile/scrcpy/Controller.java | 2 +- .../com/genymobile/scrcpy/ControlMessageReaderTest.java | 6 +++--- 8 files changed, 15 insertions(+), 15 deletions(-) diff --git a/app/src/control_msg.c b/app/src/control_msg.c index 69e75014..7d18fb6a 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -81,7 +81,7 @@ control_msg_serialize(const struct control_msg *msg, unsigned char *buf) { buf[1] = msg->set_screen_power_mode.mode; return 2; case CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL: - case CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL: + case CONTROL_MSG_TYPE_COLLAPSE_PANELS: case CONTROL_MSG_TYPE_GET_CLIPBOARD: case CONTROL_MSG_TYPE_ROTATE_DEVICE: // no additional data diff --git a/app/src/control_msg.h b/app/src/control_msg.h index 8d9ab7d4..1e9481d9 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -27,7 +27,7 @@ enum control_msg_type { CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT, CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON, CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL, - CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL, + CONTROL_MSG_TYPE_COLLAPSE_PANELS, CONTROL_MSG_TYPE_GET_CLIPBOARD, CONTROL_MSG_TYPE_SET_CLIPBOARD, CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE, diff --git a/app/src/input_manager.c b/app/src/input_manager.c index b8a8c846..574e02d4 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -179,9 +179,9 @@ expand_notification_panel(struct controller *controller) { } static void -collapse_notification_panel(struct controller *controller) { +collapse_panels(struct controller *controller) { struct control_msg msg; - msg.type = CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL; + msg.type = CONTROL_MSG_TYPE_COLLAPSE_PANELS; if (!controller_push_msg(controller, &msg)) { LOGW("Could not request 'collapse notification panel'"); @@ -498,7 +498,7 @@ input_manager_process_key(struct input_manager *im, case SDLK_n: if (control && !repeat && down) { if (shift) { - collapse_notification_panel(controller); + collapse_panels(controller); } else { expand_notification_panel(controller); } diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c index 4771ce1f..d7b89929 100644 --- a/app/tests/test_control_msg_serialize.c +++ b/app/tests/test_control_msg_serialize.c @@ -177,9 +177,9 @@ static void test_serialize_expand_notification_panel(void) { assert(!memcmp(buf, expected, sizeof(expected))); } -static void test_serialize_collapse_notification_panel(void) { +static void test_serialize_collapse_panels(void) { struct control_msg msg = { - .type = CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL, + .type = CONTROL_MSG_TYPE_COLLAPSE_PANELS, }; unsigned char buf[CONTROL_MSG_MAX_SIZE]; @@ -187,7 +187,7 @@ static void test_serialize_collapse_notification_panel(void) { assert(size == 1); const unsigned char expected[] = { - CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL, + CONTROL_MSG_TYPE_COLLAPSE_PANELS, }; assert(!memcmp(buf, expected, sizeof(expected))); } @@ -274,7 +274,7 @@ int main(int argc, char *argv[]) { test_serialize_inject_scroll_event(); test_serialize_back_or_screen_on(); test_serialize_expand_notification_panel(); - test_serialize_collapse_notification_panel(); + test_serialize_collapse_panels(); test_serialize_get_clipboard(); test_serialize_set_clipboard(); test_serialize_set_screen_power_mode(); diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java index 44cb1b59..34ce1941 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java @@ -11,7 +11,7 @@ public final class ControlMessage { public static final int TYPE_INJECT_SCROLL_EVENT = 3; public static final int TYPE_BACK_OR_SCREEN_ON = 4; public static final int TYPE_EXPAND_NOTIFICATION_PANEL = 5; - public static final int TYPE_COLLAPSE_NOTIFICATION_PANEL = 6; + public static final int TYPE_COLLAPSE_PANELS = 6; public static final int TYPE_GET_CLIPBOARD = 7; public static final int TYPE_SET_CLIPBOARD = 8; public static final int TYPE_SET_SCREEN_POWER_MODE = 9; diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java index 7ebecf76..97f027ff 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java @@ -77,7 +77,7 @@ public class ControlMessageReader { msg = parseSetScreenPowerMode(); break; case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL: - case ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL: + case ControlMessage.TYPE_COLLAPSE_PANELS: case ControlMessage.TYPE_GET_CLIPBOARD: case ControlMessage.TYPE_ROTATE_DEVICE: msg = ControlMessage.createEmpty(type); diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index 6af5ddf6..15c0584a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -107,7 +107,7 @@ public class Controller { case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL: Device.expandNotificationPanel(); break; - case ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL: + case ControlMessage.TYPE_COLLAPSE_PANELS: Device.collapsePanels(); break; case ControlMessage.TYPE_GET_CLIPBOARD: diff --git a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java index 6167ddeb..97429026 100644 --- a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java @@ -182,19 +182,19 @@ public class ControlMessageReaderTest { } @Test - public void testParseCollapseNotificationPanelEvent() throws IOException { + public void testParseCollapsePanelsEvent() throws IOException { ControlMessageReader reader = new ControlMessageReader(); ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); - dos.writeByte(ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL); + dos.writeByte(ControlMessage.TYPE_COLLAPSE_PANELS); byte[] packet = bos.toByteArray(); reader.readFrom(new ByteArrayInputStream(packet)); ControlMessage event = reader.next(); - Assert.assertEquals(ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL, event.getType()); + Assert.assertEquals(ControlMessage.TYPE_COLLAPSE_PANELS, event.getType()); } @Test From 9576283907fed9cf7baad1bb15613a811894f719 Mon Sep 17 00:00:00 2001 From: brunoais Date: Sat, 17 Apr 2021 13:15:31 +0100 Subject: [PATCH 0544/2244] Count repeated identical key events This will allow shortcuts such as MOD+n+n to open the settings panel. PR #2260 Signed-off-by: Romain Vimont --- app/src/input_manager.c | 19 +++++++++++++++++-- app/src/input_manager.h | 7 +++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 574e02d4..b6006285 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -72,6 +72,10 @@ input_manager_init(struct input_manager *im, im->sdl_shortcut_mods.count = shortcut_mods->count; im->vfinger_down = false; + + im->last_keycode = SDLK_UNKNOWN; + im->last_mod = 0; + im->key_repeat = 0; } static void @@ -384,16 +388,27 @@ input_manager_process_key(struct input_manager *im, // control: indicates the state of the command-line option --no-control bool control = im->control; - bool smod = is_shortcut_mod(im, event->keysym.mod); - struct controller *controller = im->controller; SDL_Keycode keycode = event->keysym.sym; + uint16_t mod = event->keysym.mod; bool down = event->type == SDL_KEYDOWN; bool ctrl = event->keysym.mod & KMOD_CTRL; bool shift = event->keysym.mod & KMOD_SHIFT; bool repeat = event->repeat; + bool smod = is_shortcut_mod(im, mod); + + if (down && !repeat) { + if (keycode == im->last_keycode && mod == im->last_mod) { + ++im->key_repeat; + } else { + im->key_repeat = 0; + im->last_keycode = keycode; + im->last_mod = mod; + } + } + // The shortcut modifier is pressed if (smod) { int action = down ? ACTION_DOWN : ACTION_UP; diff --git a/app/src/input_manager.h b/app/src/input_manager.h index 160977b3..5c7e2b91 100644 --- a/app/src/input_manager.h +++ b/app/src/input_manager.h @@ -33,6 +33,13 @@ struct input_manager { } sdl_shortcut_mods; bool vfinger_down; + + // Tracks the number of identical consecutive shortcut key down events. + // Not to be confused with event->repeat, which counts the number of + // system-generated repeated key presses. + unsigned key_repeat; + SDL_Keycode last_keycode; + uint16_t last_mod; }; void From 50eecdab285692ca1cb58b47552af6e1dde9c615 Mon Sep 17 00:00:00 2001 From: brunoais Date: Sat, 17 Apr 2021 13:32:18 +0100 Subject: [PATCH 0545/2244] Add control message to expand settings panel PR #2260 Signed-off-by: Romain Vimont --- app/src/control_msg.c | 1 + app/src/control_msg.h | 1 + app/src/input_manager.c | 10 ++++++ app/tests/test_control_msg_serialize.c | 16 ++++++++++ .../com/genymobile/scrcpy/ControlMessage.java | 11 ++++--- .../scrcpy/ControlMessageReader.java | 1 + .../com/genymobile/scrcpy/Controller.java | 3 ++ .../java/com/genymobile/scrcpy/Device.java | 4 +++ .../scrcpy/wrappers/StatusBarManager.java | 31 +++++++++++++++++++ .../scrcpy/ControlMessageReaderTest.java | 16 ++++++++++ 10 files changed, 89 insertions(+), 5 deletions(-) diff --git a/app/src/control_msg.c b/app/src/control_msg.c index 7d18fb6a..8908c546 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -81,6 +81,7 @@ control_msg_serialize(const struct control_msg *msg, unsigned char *buf) { buf[1] = msg->set_screen_power_mode.mode; return 2; case CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL: + case CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL: case CONTROL_MSG_TYPE_COLLAPSE_PANELS: case CONTROL_MSG_TYPE_GET_CLIPBOARD: case CONTROL_MSG_TYPE_ROTATE_DEVICE: diff --git a/app/src/control_msg.h b/app/src/control_msg.h index 1e9481d9..c1099c79 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -27,6 +27,7 @@ enum control_msg_type { CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT, CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON, CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL, + CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL, CONTROL_MSG_TYPE_COLLAPSE_PANELS, CONTROL_MSG_TYPE_GET_CLIPBOARD, CONTROL_MSG_TYPE_SET_CLIPBOARD, diff --git a/app/src/input_manager.c b/app/src/input_manager.c index b6006285..0d3175e6 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -182,6 +182,16 @@ expand_notification_panel(struct controller *controller) { } } +static void +expand_settings_panel(struct controller *controller) { + struct control_msg msg; + msg.type = CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL; + + if (!controller_push_msg(controller, &msg)) { + LOGW("Could not request 'expand settings panel'"); + } +} + static void collapse_panels(struct controller *controller) { struct control_msg msg; diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c index d7b89929..ef9247ca 100644 --- a/app/tests/test_control_msg_serialize.c +++ b/app/tests/test_control_msg_serialize.c @@ -177,6 +177,21 @@ static void test_serialize_expand_notification_panel(void) { assert(!memcmp(buf, expected, sizeof(expected))); } +static void test_serialize_expand_settings_panel(void) { + struct control_msg msg = { + .type = CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL, + }; + + unsigned char buf[CONTROL_MSG_MAX_SIZE]; + size_t size = control_msg_serialize(&msg, buf); + assert(size == 1); + + const unsigned char expected[] = { + CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL, + }; + assert(!memcmp(buf, expected, sizeof(expected))); +} + static void test_serialize_collapse_panels(void) { struct control_msg msg = { .type = CONTROL_MSG_TYPE_COLLAPSE_PANELS, @@ -274,6 +289,7 @@ int main(int argc, char *argv[]) { test_serialize_inject_scroll_event(); test_serialize_back_or_screen_on(); test_serialize_expand_notification_panel(); + test_serialize_expand_settings_panel(); test_serialize_collapse_panels(); test_serialize_get_clipboard(); test_serialize_set_clipboard(); diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java index 34ce1941..f8edd53c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java @@ -11,11 +11,12 @@ public final class ControlMessage { public static final int TYPE_INJECT_SCROLL_EVENT = 3; public static final int TYPE_BACK_OR_SCREEN_ON = 4; public static final int TYPE_EXPAND_NOTIFICATION_PANEL = 5; - public static final int TYPE_COLLAPSE_PANELS = 6; - public static final int TYPE_GET_CLIPBOARD = 7; - public static final int TYPE_SET_CLIPBOARD = 8; - public static final int TYPE_SET_SCREEN_POWER_MODE = 9; - public static final int TYPE_ROTATE_DEVICE = 10; + public static final int TYPE_EXPAND_SETTINGS_PANEL = 6; + public static final int TYPE_COLLAPSE_PANELS = 7; + public static final int TYPE_GET_CLIPBOARD = 8; + public static final int TYPE_SET_CLIPBOARD = 9; + public static final int TYPE_SET_SCREEN_POWER_MODE = 10; + public static final int TYPE_ROTATE_DEVICE = 11; private int type; private String text; diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java index 97f027ff..e4ab8402 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java @@ -77,6 +77,7 @@ public class ControlMessageReader { msg = parseSetScreenPowerMode(); break; case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL: + case ControlMessage.TYPE_EXPAND_SETTINGS_PANEL: case ControlMessage.TYPE_COLLAPSE_PANELS: case ControlMessage.TYPE_GET_CLIPBOARD: case ControlMessage.TYPE_ROTATE_DEVICE: diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index 15c0584a..3760bbd4 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -107,6 +107,9 @@ public class Controller { case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL: Device.expandNotificationPanel(); break; + case ControlMessage.TYPE_EXPAND_SETTINGS_PANEL: + Device.expandSettingsPanel(); + break; case ControlMessage.TYPE_COLLAPSE_PANELS: Device.collapsePanels(); break; diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index 624c9fa0..a63976fd 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -227,6 +227,10 @@ public final class Device { SERVICE_MANAGER.getStatusBarManager().expandNotificationsPanel(); } + public static void expandSettingsPanel() { + SERVICE_MANAGER.getStatusBarManager().expandSettingsPanel(); + } + public static void collapsePanels() { SERVICE_MANAGER.getStatusBarManager().collapsePanels(); } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java index 6f8941bd..5b1e5f5e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java @@ -11,6 +11,8 @@ public class StatusBarManager { private final IInterface manager; private Method expandNotificationsPanelMethod; + private Method expandSettingsPanelMethod; + private boolean expandSettingsPanelMethodNewVersion = true; private Method collapsePanelsMethod; public StatusBarManager(IInterface manager) { @@ -24,6 +26,20 @@ public class StatusBarManager { return expandNotificationsPanelMethod; } + private Method getExpandSettingsPanel() throws NoSuchMethodException { + if (expandSettingsPanelMethod == null) { + try { + // Since Android 7: https://android.googlesource.com/platform/frameworks/base.git/+/a9927325eda025504d59bb6594fee8e240d95b01%5E%21/ + expandSettingsPanelMethod = manager.getClass().getMethod("expandSettingsPanel", String.class); + } catch (NoSuchMethodException e) { + // old version + expandSettingsPanelMethod = manager.getClass().getMethod("expandSettingsPanel"); + expandSettingsPanelMethodNewVersion = false; + } + } + return expandSettingsPanelMethod; + } + private Method getCollapsePanelsMethod() throws NoSuchMethodException { if (collapsePanelsMethod == null) { collapsePanelsMethod = manager.getClass().getMethod("collapsePanels"); @@ -40,6 +56,21 @@ public class StatusBarManager { } } + public void expandSettingsPanel() { + try { + Method method = getExpandSettingsPanel(); + if (expandSettingsPanelMethodNewVersion) { + // new version + method.invoke(manager, (Object) null); + } else { + // old version + method.invoke(manager); + } + } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + Ln.e("Could not invoke method", e); + } + } + public void collapsePanels() { try { Method method = getCollapsePanelsMethod(); diff --git a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java index 97429026..da568486 100644 --- a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java @@ -181,6 +181,22 @@ public class ControlMessageReaderTest { Assert.assertEquals(ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL, event.getType()); } + @Test + public void testParseExpandSettingsPanelEvent() throws IOException { + ControlMessageReader reader = new ControlMessageReader(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(ControlMessage.TYPE_EXPAND_SETTINGS_PANEL); + + byte[] packet = bos.toByteArray(); + + reader.readFrom(new ByteArrayInputStream(packet)); + ControlMessage event = reader.next(); + + Assert.assertEquals(ControlMessage.TYPE_EXPAND_SETTINGS_PANEL, event.getType()); + } + @Test public void testParseCollapsePanelsEvent() throws IOException { ControlMessageReader reader = new ControlMessageReader(); From 6fa63cf6f86ff1f566587b0f0819d5dbf397a06e Mon Sep 17 00:00:00 2001 From: brunoais Date: Tue, 20 Apr 2021 18:31:39 +0200 Subject: [PATCH 0546/2244] Add keyboard shortcut to expand settings panel PR #2260 Signed-off-by: Romain Vimont --- app/src/input_manager.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 0d3175e6..d5b0c505 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -524,8 +524,10 @@ input_manager_process_key(struct input_manager *im, if (control && !repeat && down) { if (shift) { collapse_panels(controller); - } else { + } else if (im->key_repeat == 0) { expand_notification_panel(controller); + } else { + expand_settings_panel(controller); } } return; From b4ee9f27ce522710a52c1b688d1524bb1c904dff Mon Sep 17 00:00:00 2001 From: brunoais Date: Sat, 17 Apr 2021 13:56:53 +0100 Subject: [PATCH 0547/2244] Add mouse shortcut to expand settings panel Double-click on extra mouse button to open the settings panel (a single-click opens the notification panel). This is consistent with the keyboard shortcut MOD+n+n. PR #2264 Signed-off-by: Romain Vimont --- app/src/input_manager.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index d5b0c505..94086616 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -693,7 +693,11 @@ input_manager_process_mouse_button(struct input_manager *im, return; } if (control && event->button == SDL_BUTTON_X2 && down) { - expand_notification_panel(im->controller); + if (event->clicks < 2) { + expand_notification_panel(im->controller); + } else { + expand_settings_panel(im->controller); + } return; } if (control && event->button == SDL_BUTTON_RIGHT) { From d7e658967780bca3631ac630bdb57fc071c2cae3 Mon Sep 17 00:00:00 2001 From: brunoais Date: Thu, 22 Apr 2021 21:57:31 +0100 Subject: [PATCH 0548/2244] Document 4th+5th + 2xn shortcuts PR #2260 Signed-off-by: Romain Vimont --- README.md | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 6a987a73..747fb5e3 100644 --- a/README.md +++ b/README.md @@ -698,10 +698,10 @@ _[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+w \| _Double-click¹_ + | Resize window to remove black borders | MOD+w \| _Double-left-click¹_ | Click on `HOME` | MOD+h \| _Middle-click_ | Click on `BACK` | MOD+b \| _Right-click²_ - | Click on `APP_SWITCH` | MOD+s + | Click on `APP_SWITCH` | MOD+s \| _4th-click³_ | Click on `MENU` (unlock screen) | MOD+m | Click on `VOLUME_UP` | MOD+ _(up)_ | Click on `VOLUME_DOWN` | MOD+ _(down)_ @@ -710,18 +710,27 @@ _[Super] is typically the Windows or Cmd key._ | 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 + | Expand notification panel | MOD+n \| _5th-click³_ + | Expand settings panel | MOD+n+n \| _Double-5th-click³_ + | Collapse panels | 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 | 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._ -_³Only on Android >= 7._ +_³4th and 5th mouse buttons, if your mouse has them._ +_⁴Only on Android >= 7._ + +Shortcuts with repeated keys are executted by releasing and pressing the key a +second time. For example, to execute "Expand settings panel": + + 1. Press and keep pressing MOD. + 2. Then double-press n. + 3. Finally, release MOD. All Ctrl+_key_ shortcuts are forwarded to the device, so they are handled by the active application. From 21b590b766e1efa4ab75ed685449d1dc09ab9b20 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 11 Apr 2021 15:01:05 +0200 Subject: [PATCH 0549/2244] Write trailer from recorder thread The recorder thread wrote the whole content except the trailer, which was odd. --- app/src/recorder.c | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/app/src/recorder.c b/app/src/recorder.c index 2e3b0c28..3f5eb0d0 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -171,25 +171,8 @@ recorder_open(struct recorder *recorder, const AVCodec *input_codec) { void recorder_close(struct recorder *recorder) { - if (recorder->header_written) { - int ret = av_write_trailer(recorder->ctx); - if (ret < 0) { - LOGE("Failed to write trailer to %s", recorder->filename); - recorder->failed = true; - } - } else { - // the recorded file is empty - recorder->failed = true; - } avio_close(recorder->ctx->pb); avformat_free_context(recorder->ctx); - - if (recorder->failed) { - LOGE("Recording failed to %s", recorder->filename); - } else { - const char *format_name = recorder_get_format_name(recorder->format); - LOGI("Recording complete to %s file: %s", format_name, recorder->filename); - } } static bool @@ -317,7 +300,26 @@ run_recorder(void *data) { sc_mutex_unlock(&recorder->mutex); break; } + } + if (!recorder->failed) { + if (recorder->header_written) { + int ret = av_write_trailer(recorder->ctx); + if (ret < 0) { + LOGE("Failed to write trailer to %s", recorder->filename); + recorder->failed = true; + } + } else { + // the recorded file is empty + recorder->failed = true; + } + } + + if (recorder->failed) { + LOGE("Recording failed to %s", recorder->filename); + } else { + const char *format_name = recorder_get_format_name(recorder->format); + LOGI("Recording complete to %s file: %s", format_name, recorder->filename); } LOGD("Recorder thread ended"); From 55806e7d312f2acabd99d1a3c707f7e42fbf90e2 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 11 Apr 2021 15:01:05 +0200 Subject: [PATCH 0550/2244] Remove option --render-expired-frames This flag forced the decoder to wait for the previous frame to be consumed by the display. It was initially implemented as a compilation flag for testing, not intended to be exposed at runtime. But to remove ifdefs and to allow users to test this flag easily, it had finally been exposed by commit ebccb9f6cc111e8acfbe10d656cac5c1f1b744a0. In practice, it turned out to be useless: it had no practical impact, and it did not solve or mitigate any performance issues causing frame skipping. But that added some complexity to the codebase: it required an additional condition variable, and made video buffer calls possibly blocking, which in turn required code to interrupt it on exit. To prepare support for multiple sinks plugged to the decoder (display and v4l2 for example), the blocking call used for pacing the decoder output becomes unacceptable, so just remove this useless "feature". --- README.md | 12 ------------ app/scrcpy.1 | 4 ---- app/src/cli.c | 9 ++------- app/src/decoder.c | 5 ----- app/src/decoder.h | 3 --- app/src/scrcpy.c | 9 +++------ app/src/scrcpy.h | 2 -- app/src/stream.c | 7 ------- app/src/stream.h | 3 --- app/src/video_buffer.c | 38 +------------------------------------- app/src/video_buffer.h | 9 +-------- app/tests/test_cli.c | 2 -- 12 files changed, 7 insertions(+), 96 deletions(-) diff --git a/README.md b/README.md index 747fb5e3..d9aeb142 100644 --- a/README.md +++ b/README.md @@ -491,18 +491,6 @@ scrcpy -Sw ``` -#### Render expired frames - -By default, to minimize latency, _scrcpy_ always renders the last decoded frame -available, and drops any previous one. - -To force the rendering of all frames (at a cost of a possible increased -latency), use: - -```bash -scrcpy --render-expired-frames -``` - #### Show touches For presentations, it may be useful to show physical touches (on the physical diff --git a/app/scrcpy.1 b/app/scrcpy.1 index bf3bc9cd..ea9bce9b 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -155,10 +155,6 @@ Supported names are currently "direct3d", "opengl", "opengles2", "opengles", "me .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. - .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. diff --git a/app/src/cli.c b/app/src/cli.c index 042a1f4c..ec3c1294 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -143,12 +143,6 @@ scrcpy_print_usage(const char *arg0) { " \"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" - " 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" @@ -816,7 +810,8 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { opts->stay_awake = true; break; case OPT_RENDER_EXPIRED_FRAMES: - opts->render_expired_frames = true; + LOGW("Option --render-expired-frames has been removed. This " + "flag has been ignored."); break; case OPT_WINDOW_TITLE: opts->window_title = optarg; diff --git a/app/src/decoder.c b/app/src/decoder.c index a13cf75e..b7101194 100644 --- a/app/src/decoder.c +++ b/app/src/decoder.c @@ -69,8 +69,3 @@ decoder_push(struct decoder *decoder, const AVPacket *packet) { #endif return true; } - -void -decoder_interrupt(struct decoder *decoder) { - video_buffer_interrupt(decoder->video_buffer); -} diff --git a/app/src/decoder.h b/app/src/decoder.h index bbd7a9a7..ba06583e 100644 --- a/app/src/decoder.h +++ b/app/src/decoder.h @@ -26,7 +26,4 @@ decoder_close(struct decoder *decoder); bool decoder_push(struct decoder *decoder, const AVPacket *packet); -void -decoder_interrupt(struct decoder *decoder); - #endif diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 388bb73d..2f8abd64 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -305,7 +305,7 @@ scrcpy(const struct scrcpy_options *options) { } fps_counter_initialized = true; - if (!video_buffer_init(&video_buffer, options->render_expired_frames)) { + if (!video_buffer_init(&video_buffer)) { goto end; } video_buffer_initialized = true; @@ -398,11 +398,8 @@ scrcpy(const struct scrcpy_options *options) { LOGD("quit..."); end: - // stop stream and controller so that they don't continue once their socket - // is shutdown - if (stream_started) { - stream_stop(&stream); - } + // The stream is not stopped explicitly, because it will stop by itself on + // end-of-stream if (controller_started) { controller_stop(&controller); } diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index 8ee70f60..f91cb6b8 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -72,7 +72,6 @@ struct scrcpy_options { bool control; bool display; bool turn_screen_off; - bool render_expired_frames; bool prefer_text; bool window_borderless; bool mipmaps; @@ -120,7 +119,6 @@ struct scrcpy_options { .control = true, \ .display = true, \ .turn_screen_off = false, \ - .render_expired_frames = false, \ .prefer_text = false, \ .window_borderless = false, \ .mipmaps = true, \ diff --git a/app/src/stream.c b/app/src/stream.c index ba72f164..e0a223be 100644 --- a/app/src/stream.c +++ b/app/src/stream.c @@ -285,13 +285,6 @@ stream_start(struct stream *stream) { return true; } -void -stream_stop(struct stream *stream) { - if (stream->decoder) { - decoder_interrupt(stream->decoder); - } -} - void stream_join(struct stream *stream) { sc_thread_join(&stream->thread, NULL); diff --git a/app/src/stream.h b/app/src/stream.h index d4a9bd4a..421c1bf0 100644 --- a/app/src/stream.h +++ b/app/src/stream.h @@ -31,9 +31,6 @@ stream_init(struct stream *stream, socket_t socket, bool stream_start(struct stream *stream); -void -stream_stop(struct stream *stream); - void stream_join(struct stream *stream); diff --git a/app/src/video_buffer.c b/app/src/video_buffer.c index 94619840..d4954afc 100644 --- a/app/src/video_buffer.c +++ b/app/src/video_buffer.c @@ -7,7 +7,7 @@ #include "util/log.h" bool -video_buffer_init(struct video_buffer *vb, bool wait_consumer) { +video_buffer_init(struct video_buffer *vb) { vb->producer_frame = av_frame_alloc(); if (!vb->producer_frame) { goto error_0; @@ -28,18 +28,6 @@ video_buffer_init(struct video_buffer *vb, bool wait_consumer) { goto error_3; } - vb->wait_consumer = wait_consumer; - if (wait_consumer) { - ok = sc_cond_init(&vb->pending_frame_consumed_cond); - if (!ok) { - sc_mutex_destroy(&vb->mutex); - goto error_2; - } - // interrupted is not used if wait_consumer is disabled since offering - // a frame will never block - vb->interrupted = false; - } - // there is initially no frame, so consider it has already been consumed vb->pending_frame_consumed = true; @@ -61,9 +49,6 @@ error_0: void video_buffer_destroy(struct video_buffer *vb) { - if (vb->wait_consumer) { - sc_cond_destroy(&vb->pending_frame_consumed_cond); - } sc_mutex_destroy(&vb->mutex); av_frame_free(&vb->consumer_frame); av_frame_free(&vb->pending_frame); @@ -93,12 +78,6 @@ video_buffer_producer_offer_frame(struct video_buffer *vb) { assert(vb->cbs); sc_mutex_lock(&vb->mutex); - if (vb->wait_consumer) { - // wait for the current (expired) frame to be consumed - while (!vb->pending_frame_consumed && !vb->interrupted) { - sc_cond_wait(&vb->pending_frame_consumed_cond, &vb->mutex); - } - } av_frame_unref(vb->pending_frame); swap_frames(&vb->producer_frame, &vb->pending_frame); @@ -125,23 +104,8 @@ video_buffer_consumer_take_frame(struct video_buffer *vb) { swap_frames(&vb->consumer_frame, &vb->pending_frame); av_frame_unref(vb->pending_frame); - if (vb->wait_consumer) { - // unblock video_buffer_offer_decoded_frame() - sc_cond_signal(&vb->pending_frame_consumed_cond); - } sc_mutex_unlock(&vb->mutex); // consumer_frame is only written from this thread, no need to lock return vb->consumer_frame; } - -void -video_buffer_interrupt(struct video_buffer *vb) { - if (vb->wait_consumer) { - sc_mutex_lock(&vb->mutex); - vb->interrupted = true; - sc_mutex_unlock(&vb->mutex); - // wake up blocking wait - sc_cond_signal(&vb->pending_frame_consumed_cond); - } -} diff --git a/app/src/video_buffer.h b/app/src/video_buffer.h index 4d11e3ab..48e57ff4 100644 --- a/app/src/video_buffer.h +++ b/app/src/video_buffer.h @@ -34,10 +34,7 @@ struct video_buffer { AVFrame *consumer_frame; sc_mutex mutex; - bool wait_consumer; // never overwrite a pending frame if it is not consumed - bool interrupted; - sc_cond pending_frame_consumed_cond; bool pending_frame_consumed; const struct video_buffer_callbacks *cbs; @@ -56,7 +53,7 @@ struct video_buffer_callbacks { }; bool -video_buffer_init(struct video_buffer *vb, bool wait_consumer); +video_buffer_init(struct video_buffer *vb); void video_buffer_destroy(struct video_buffer *vb); @@ -75,8 +72,4 @@ video_buffer_producer_offer_frame(struct video_buffer *vb); const AVFrame * video_buffer_consumer_take_frame(struct video_buffer *vb); -// wake up and avoid any blocking call -void -video_buffer_interrupt(struct video_buffer *vb); - #endif diff --git a/app/tests/test_cli.c b/app/tests/test_cli.c index cd222d63..3fa9b3d7 100644 --- a/app/tests/test_cli.c +++ b/app/tests/test_cli.c @@ -58,7 +58,6 @@ static void test_options(void) { "--push-target", "/sdcard/Movies", "--record", "file", "--record-format", "mkv", - "--render-expired-frames", "--serial", "0123456789abcdef", "--show-touches", "--turn-screen-off", @@ -87,7 +86,6 @@ static void test_options(void) { assert(!strcmp(opts->push_target, "/sdcard/Movies")); assert(!strcmp(opts->record_filename, "file")); assert(opts->record_format == SC_RECORD_FORMAT_MKV); - assert(opts->render_expired_frames); assert(!strcmp(opts->serial, "0123456789abcdef")); assert(opts->show_touches); assert(opts->turn_screen_off); From de9b79ec2dd73e5442c6bf0161669bcd8ca7d5be Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 11 Apr 2021 15:01:05 +0200 Subject: [PATCH 0551/2244] Remove compat with old FFmpeg decoding API The new API has been introduced in 2016 in libavcodec 57.xx, it's very old. This will avoid to maintain two code paths for decoding. --- app/src/compat.h | 10 ---------- app/src/decoder.c | 17 ----------------- 2 files changed, 27 deletions(-) diff --git a/app/src/compat.h b/app/src/compat.h index 9a84a4c1..f3e7bd7a 100644 --- a/app/src/compat.h +++ b/app/src/compat.h @@ -8,7 +8,6 @@ # define _DARWIN_C_SOURCE #endif -#include #include #include @@ -33,15 +32,6 @@ # define SCRCPY_LAVF_REQUIRES_REGISTER_ALL #endif -// In ffmpeg/doc/APIchanges: -// 2016-04-21 - 7fc329e - lavc 57.37.100 - avcodec.h -// Add a new audio/video encoding and decoding API with decoupled input -// and output -- avcodec_send_packet(), avcodec_receive_frame(), -// avcodec_send_frame() and avcodec_receive_packet(). -#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(57, 37, 100) -# define SCRCPY_LAVF_HAS_NEW_ENCODING_DECODING_API -#endif - #if SDL_VERSION_ATLEAST(2, 0, 5) // # define SCRCPY_SDL_HAS_HINT_MOUSE_FOCUS_CLICKTHROUGH diff --git a/app/src/decoder.c b/app/src/decoder.c index b7101194..f05303a3 100644 --- a/app/src/decoder.c +++ b/app/src/decoder.c @@ -36,9 +36,6 @@ decoder_close(struct decoder *decoder) { bool decoder_push(struct decoder *decoder, const AVPacket *packet) { -// the new decoding/encoding API has been introduced by: -// -#ifdef SCRCPY_LAVF_HAS_NEW_ENCODING_DECODING_API int ret; if ((ret = avcodec_send_packet(decoder->codec_ctx, packet)) < 0) { LOGE("Could not send video packet: %d", ret); @@ -53,19 +50,5 @@ decoder_push(struct decoder *decoder, const AVPacket *packet) { LOGE("Could not receive video frame: %d", ret); return false; } -#else - int got_picture; - int len = avcodec_decode_video2(decoder->codec_ctx, - decoder->video_buffer->producer_frame, - &got_picture, - packet); - if (len < 0) { - LOGE("Could not decode video packet: %d", len); - return false; - } - if (got_picture) { - video_buffer_producer_offer_frame(decoder->video_buffer); - } -#endif return true; } From 5d9e96dc4eaa41b185c41e8a6df6191762764b1c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 11 Apr 2021 15:01:05 +0200 Subject: [PATCH 0552/2244] Remove compat with old FFmpeg codec params API The new API has been introduced in 2016 in libavformat 57.xx, it's very old. This will avoid to maintain two code paths for codec parameters. --- app/src/compat.h | 10 ---------- app/src/recorder.c | 13 ------------- 2 files changed, 23 deletions(-) diff --git a/app/src/compat.h b/app/src/compat.h index f3e7bd7a..9d9a7884 100644 --- a/app/src/compat.h +++ b/app/src/compat.h @@ -11,16 +11,6 @@ #include #include -// In ffmpeg/doc/APIchanges: -// 2016-04-11 - 6f69f7a / 9200514 - lavf 57.33.100 / 57.5.0 - avformat.h -// Add AVStream.codecpar, deprecate AVStream.codec. -#if (LIBAVFORMAT_VERSION_MICRO >= 100 /* FFmpeg */ && \ - LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(57, 33, 100)) \ - || (LIBAVFORMAT_VERSION_MICRO < 100 && /* Libav */ \ - LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(57, 5, 0)) -# define SCRCPY_LAVF_HAS_NEW_CODEC_PARAMS_API -#endif - // In ffmpeg/doc/APIchanges: // 2018-02-06 - 0694d87024 - lavf 58.9.100 - avformat.h // Deprecate use of av_register_input_format(), av_register_output_format(), diff --git a/app/src/recorder.c b/app/src/recorder.c index 3f5eb0d0..c0d7aed1 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -141,19 +141,11 @@ recorder_open(struct recorder *recorder, const AVCodec *input_codec) { return false; } -#ifdef SCRCPY_LAVF_HAS_NEW_CODEC_PARAMS_API ostream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO; ostream->codecpar->codec_id = input_codec->id; ostream->codecpar->format = AV_PIX_FMT_YUV420P; ostream->codecpar->width = recorder->declared_frame_size.width; ostream->codecpar->height = recorder->declared_frame_size.height; -#else - ostream->codec->codec_type = AVMEDIA_TYPE_VIDEO; - ostream->codec->codec_id = input_codec->id; - ostream->codec->pix_fmt = AV_PIX_FMT_YUV420P; - ostream->codec->width = recorder->declared_frame_size.width; - ostream->codec->height = recorder->declared_frame_size.height; -#endif int ret = avio_open(&recorder->ctx->pb, recorder->filename, AVIO_FLAG_WRITE); @@ -188,13 +180,8 @@ recorder_write_header(struct recorder *recorder, const AVPacket *packet) { // copy the first packet to the extra data memcpy(extradata, packet->data, packet->size); -#ifdef SCRCPY_LAVF_HAS_NEW_CODEC_PARAMS_API ostream->codecpar->extradata = extradata; ostream->codecpar->extradata_size = packet->size; -#else - ostream->codec->extradata = extradata; - ostream->codec->extradata_size = packet->size; -#endif int ret = avformat_write_header(recorder->ctx, NULL); if (ret < 0) { From 2ddf760c091ecaed6d6dcb4131dd2c468453a0da Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 11 Apr 2021 15:01:05 +0200 Subject: [PATCH 0553/2244] Make video_buffer more generic The video buffer took ownership of the producer frame (so that it could swap frames quickly). In order to support multiple sinks plugged to the decoder, the decoded frame must not be consumed by the display video buffer. Therefore, move the producer and consumer frames out of the video buffer, and use FFmpeg AVFrame refcounting to share ownership while avoiding copies. --- app/src/decoder.c | 17 ++++++++++-- app/src/decoder.h | 1 + app/src/screen.c | 14 +++++++++- app/src/screen.h | 2 ++ app/src/video_buffer.c | 62 ++++++++++++++++++++---------------------- app/src/video_buffer.h | 41 ++++++++++++---------------- 6 files changed, 77 insertions(+), 60 deletions(-) diff --git a/app/src/decoder.c b/app/src/decoder.c index f05303a3..247b459c 100644 --- a/app/src/decoder.c +++ b/app/src/decoder.c @@ -25,11 +25,20 @@ decoder_open(struct decoder *decoder, const AVCodec *codec) { return false; } + decoder->frame = av_frame_alloc(); + if (!decoder->frame) { + LOGE("Could not create decoder frame"); + avcodec_close(decoder->codec_ctx); + avcodec_free_context(&decoder->codec_ctx); + return false; + } + return true; } void decoder_close(struct decoder *decoder) { + av_frame_free(&decoder->frame); avcodec_close(decoder->codec_ctx); avcodec_free_context(&decoder->codec_ctx); } @@ -41,11 +50,13 @@ decoder_push(struct decoder *decoder, const AVPacket *packet) { LOGE("Could not send video packet: %d", ret); return false; } - ret = avcodec_receive_frame(decoder->codec_ctx, - decoder->video_buffer->producer_frame); + ret = avcodec_receive_frame(decoder->codec_ctx, decoder->frame); if (!ret) { // a frame was received - video_buffer_producer_offer_frame(decoder->video_buffer); + bool ok = video_buffer_push(decoder->video_buffer, decoder->frame); + // A frame lost should not make the whole pipeline fail. The error, if + // any, is already logged. + (void) ok; } else if (ret != AVERROR(EAGAIN)) { LOGE("Could not receive video frame: %d", ret); return false; diff --git a/app/src/decoder.h b/app/src/decoder.h index ba06583e..50dd7fe0 100644 --- a/app/src/decoder.h +++ b/app/src/decoder.h @@ -12,6 +12,7 @@ struct decoder { struct video_buffer *video_buffer; AVCodecContext *codec_ctx; + AVFrame *frame; }; void diff --git a/app/src/screen.c b/app/src/screen.c index 934e418f..de734554 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -378,6 +378,15 @@ screen_init(struct screen *screen, struct video_buffer *vb, return false; } + screen->frame = av_frame_alloc(); + if (!screen->frame) { + LOGC("Could not create screen frame"); + SDL_DestroyTexture(screen->texture); + SDL_DestroyRenderer(screen->renderer); + SDL_DestroyWindow(screen->window); + 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 @@ -403,6 +412,7 @@ screen_show_window(struct screen *screen) { void screen_destroy(struct screen *screen) { + av_frame_free(&screen->frame); SDL_DestroyTexture(screen->texture); SDL_DestroyRenderer(screen->renderer); SDL_DestroyWindow(screen->window); @@ -510,7 +520,9 @@ update_texture(struct screen *screen, const AVFrame *frame) { static bool screen_update_frame(struct screen *screen) { - const AVFrame *frame = video_buffer_consumer_take_frame(screen->vb); + av_frame_unref(screen->frame); + video_buffer_consume(screen->vb, screen->frame); + AVFrame *frame = screen->frame; fps_counter_add_rendered_frame(screen->fps_counter); diff --git a/app/src/screen.h b/app/src/screen.h index dca65d41..cd849779 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -36,6 +36,8 @@ struct screen { bool fullscreen; bool maximized; bool mipmaps; + + AVFrame *frame; }; struct screen_params { diff --git a/app/src/video_buffer.c b/app/src/video_buffer.c index d4954afc..18a180fa 100644 --- a/app/src/video_buffer.c +++ b/app/src/video_buffer.c @@ -8,24 +8,22 @@ bool video_buffer_init(struct video_buffer *vb) { - vb->producer_frame = av_frame_alloc(); - if (!vb->producer_frame) { - goto error_0; - } - vb->pending_frame = av_frame_alloc(); if (!vb->pending_frame) { - goto error_1; + return false; } - vb->consumer_frame = av_frame_alloc(); - if (!vb->consumer_frame) { - goto error_2; + vb->tmp_frame = av_frame_alloc(); + if (!vb->tmp_frame) { + av_frame_free(&vb->pending_frame); + return false; } bool ok = sc_mutex_init(&vb->mutex); if (!ok) { - goto error_3; + av_frame_free(&vb->pending_frame); + av_frame_free(&vb->tmp_frame); + return false; } // there is initially no frame, so consider it has already been consumed @@ -36,23 +34,13 @@ video_buffer_init(struct video_buffer *vb) { vb->cbs = NULL; return true; - -error_3: - av_frame_free(&vb->consumer_frame); -error_2: - av_frame_free(&vb->pending_frame); -error_1: - av_frame_free(&vb->producer_frame); -error_0: - return false; } void video_buffer_destroy(struct video_buffer *vb) { sc_mutex_destroy(&vb->mutex); - av_frame_free(&vb->consumer_frame); av_frame_free(&vb->pending_frame); - av_frame_free(&vb->producer_frame); + av_frame_free(&vb->tmp_frame); } static inline void @@ -73,14 +61,24 @@ video_buffer_set_consumer_callbacks(struct video_buffer *vb, vb->cbs_userdata = cbs_userdata; } -void -video_buffer_producer_offer_frame(struct video_buffer *vb) { +bool +video_buffer_push(struct video_buffer *vb, const AVFrame *frame) { assert(vb->cbs); sc_mutex_lock(&vb->mutex); - av_frame_unref(vb->pending_frame); - swap_frames(&vb->producer_frame, &vb->pending_frame); + // Use a temporary frame to preserve pending_frame in case of error. + // tmp_frame is an empty frame, no need to call av_frame_unref() beforehand. + int r = av_frame_ref(vb->tmp_frame, frame); + if (r) { + LOGE("Could not ref frame: %d", r); + return false; + } + + // Now that av_frame_ref() succeeded, we can replace the previous + // pending_frame + swap_frames(&vb->pending_frame, &vb->tmp_frame); + av_frame_unref(vb->tmp_frame); bool skipped = !vb->pending_frame_consumed; vb->pending_frame_consumed = false; @@ -93,19 +91,19 @@ video_buffer_producer_offer_frame(struct video_buffer *vb) { } else { vb->cbs->on_frame_available(vb, vb->cbs_userdata); } + + return true; } -const AVFrame * -video_buffer_consumer_take_frame(struct video_buffer *vb) { +void +video_buffer_consume(struct video_buffer *vb, AVFrame *dst) { sc_mutex_lock(&vb->mutex); assert(!vb->pending_frame_consumed); vb->pending_frame_consumed = true; - swap_frames(&vb->consumer_frame, &vb->pending_frame); - av_frame_unref(vb->pending_frame); + av_frame_move_ref(dst, vb->pending_frame); + // av_frame_move_ref() resets its source frame, so no need to call + // av_frame_unref() sc_mutex_unlock(&vb->mutex); - - // consumer_frame is only written from this thread, no need to lock - return vb->consumer_frame; } diff --git a/app/src/video_buffer.h b/app/src/video_buffer.h index 48e57ff4..cdecb259 100644 --- a/app/src/video_buffer.h +++ b/app/src/video_buffer.h @@ -12,26 +12,23 @@ typedef struct AVFrame AVFrame; /** - * There are 3 frames in memory: - * - one frame is held by the producer (producer_frame) - * - one frame is held by the consumer (consumer_frame) - * - one frame is shared between the producer and the consumer (pending_frame) + * A video buffer holds 1 pending frame, which is the last frame received from + * the producer (typically, the decoder). * - * The producer generates a frame into the producer_frame (it may takes time). + * If a pending frame has not been consumed when the producer pushes a new + * frame, then it is lost. The intent is to always provide access to the very + * last frame to minimize latency. * - * Once the frame is produced, it calls video_buffer_producer_offer_frame(), - * which swaps the producer and pending frames. - * - * When the consumer is notified that a new frame is available, it calls - * video_buffer_consumer_take_frame() to retrieve it, which swaps the pending - * and consumer frames. The frame is valid until the next call, without - * blocking the producer. + * The producer and the consumer typically do not live in the same thread. + * That's the reason why the callback on_frame_available() does not provide the + * frame as parameter: the consumer might post an event to its own thread to + * retrieve the pending frame from there, and that frame may have changed since + * the callback if producer pushed a new one in between. */ struct video_buffer { - AVFrame *producer_frame; AVFrame *pending_frame; - AVFrame *consumer_frame; + AVFrame *tmp_frame; // To preserve the pending frame on error sc_mutex mutex; @@ -42,12 +39,11 @@ struct video_buffer { }; struct video_buffer_callbacks { - // Called when a new frame can be consumed by - // video_buffer_consumer_take_frame(vb) + // Called when a new frame can be consumed. // This callback is mandatory (it must not be NULL). void (*on_frame_available)(struct video_buffer *vb, void *userdata); - // Called when a pending frame has been overwritten by the producer + // Called when a pending frame has been overwritten by the producer. // This callback is optional (it may be NULL). void (*on_frame_skipped)(struct video_buffer *vb, void *userdata); }; @@ -63,13 +59,10 @@ video_buffer_set_consumer_callbacks(struct video_buffer *vb, const struct video_buffer_callbacks *cbs, void *cbs_userdata); -// set the producer frame as ready for consuming -void -video_buffer_producer_offer_frame(struct video_buffer *vb); +bool +video_buffer_push(struct video_buffer *vb, const AVFrame *frame); -// mark the consumer frame as consumed and return it -// the frame is valid until the next call to this function -const AVFrame * -video_buffer_consumer_take_frame(struct video_buffer *vb); +void +video_buffer_consume(struct video_buffer *vb, AVFrame *dst); #endif From 08f1fd46c8f2ff70b1cc4464ec9dea5c6fd898c9 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 11 Apr 2021 15:01:05 +0200 Subject: [PATCH 0554/2244] Add container_of() macro This will allow to get the parent of an embedded struct. --- app/src/common.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/common.h b/app/src/common.h index 27c8d2fb..accbc615 100644 --- a/app/src/common.h +++ b/app/src/common.h @@ -8,4 +8,7 @@ #define MIN(X,Y) (X) < (Y) ? (X) : (Y) #define MAX(X,Y) (X) > (Y) ? (X) : (Y) +#define container_of(ptr, type, member) \ + ((type *) (((char *) (ptr)) - offsetof(type, member))) + #endif From 1b072a24c4e58361d353a0bfb5b5d63174e47524 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 11 Apr 2021 15:01:05 +0200 Subject: [PATCH 0555/2244] Add packet sink trait This trait will allow to abstract the concrete sink types from the packet producer (stream.c). --- app/src/trait/packet_sink.h | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 app/src/trait/packet_sink.h diff --git a/app/src/trait/packet_sink.h b/app/src/trait/packet_sink.h new file mode 100644 index 00000000..fe9c137d --- /dev/null +++ b/app/src/trait/packet_sink.h @@ -0,0 +1,27 @@ +#ifndef SC_PACKET_SINK +#define SC_PACKET_SINK + +#include "common.h" + +#include +#include + +typedef struct AVCodec AVCodec; +typedef struct AVPacket AVPacket; + +/** + * Packet sink trait. + * + * Component able to receive AVPackets should implement this trait. + */ +struct sc_packet_sink { + const struct sc_packet_sink_ops *ops; +}; + +struct sc_packet_sink_ops { + bool (*open)(struct sc_packet_sink *sink, const AVCodec *codec); + void (*close)(struct sc_packet_sink *sink); + bool (*push)(struct sc_packet_sink *sink, const AVPacket *packet); +}; + +#endif From a974483c1560042085a25412f8cf19329505447c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 11 Apr 2021 15:01:05 +0200 Subject: [PATCH 0556/2244] Reorder recorder functions This will make further commits more readable. --- app/src/recorder.c | 202 ++++++++++++++++++++++----------------------- 1 file changed, 101 insertions(+), 101 deletions(-) diff --git a/app/src/recorder.c b/app/src/recorder.c index c0d7aed1..c24393de 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -57,50 +57,6 @@ recorder_queue_clear(struct recorder_queue *queue) { } } -bool -recorder_init(struct recorder *recorder, - const char *filename, - enum sc_record_format format, - struct size declared_frame_size) { - recorder->filename = strdup(filename); - if (!recorder->filename) { - LOGE("Could not strdup filename"); - return false; - } - - bool ok = sc_mutex_init(&recorder->mutex); - if (!ok) { - LOGC("Could not create mutex"); - free(recorder->filename); - return false; - } - - ok = sc_cond_init(&recorder->queue_cond); - if (!ok) { - LOGC("Could not create cond"); - sc_mutex_destroy(&recorder->mutex); - free(recorder->filename); - return false; - } - - queue_init(&recorder->queue); - recorder->stopped = false; - recorder->failed = false; - recorder->format = format; - recorder->declared_frame_size = declared_frame_size; - recorder->header_written = false; - recorder->previous = NULL; - - return true; -} - -void -recorder_destroy(struct recorder *recorder) { - sc_cond_destroy(&recorder->queue_cond); - sc_mutex_destroy(&recorder->mutex); - free(recorder->filename); -} - static const char * recorder_get_format_name(enum sc_record_format format) { switch (format) { @@ -110,63 +66,6 @@ recorder_get_format_name(enum sc_record_format format) { } } -bool -recorder_open(struct recorder *recorder, const AVCodec *input_codec) { - const char *format_name = recorder_get_format_name(recorder->format); - assert(format_name); - const AVOutputFormat *format = find_muxer(format_name); - if (!format) { - LOGE("Could not find muxer"); - return false; - } - - recorder->ctx = avformat_alloc_context(); - if (!recorder->ctx) { - LOGE("Could not allocate output context"); - return false; - } - - // contrary to the deprecated API (av_oformat_next()), av_muxer_iterate() - // returns (on purpose) a pointer-to-const, but AVFormatContext.oformat - // still expects a pointer-to-non-const (it has not be updated accordingly) - // - recorder->ctx->oformat = (AVOutputFormat *) format; - - av_dict_set(&recorder->ctx->metadata, "comment", - "Recorded by scrcpy " SCRCPY_VERSION, 0); - - AVStream *ostream = avformat_new_stream(recorder->ctx, input_codec); - if (!ostream) { - avformat_free_context(recorder->ctx); - return false; - } - - ostream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO; - ostream->codecpar->codec_id = input_codec->id; - ostream->codecpar->format = AV_PIX_FMT_YUV420P; - ostream->codecpar->width = recorder->declared_frame_size.width; - ostream->codecpar->height = recorder->declared_frame_size.height; - - int ret = avio_open(&recorder->ctx->pb, recorder->filename, - AVIO_FLAG_WRITE); - if (ret < 0) { - LOGE("Failed to open output file: %s", recorder->filename); - // ostream will be cleaned up during context cleaning - avformat_free_context(recorder->ctx); - return false; - } - - LOGI("Recording started to %s file: %s", format_name, recorder->filename); - - return true; -} - -void -recorder_close(struct recorder *recorder) { - avio_close(recorder->ctx->pb); - avformat_free_context(recorder->ctx); -} - static bool recorder_write_header(struct recorder *recorder, const AVPacket *packet) { AVStream *ostream = recorder->ctx->streams[0]; @@ -314,6 +213,63 @@ run_recorder(void *data) { return 0; } +bool +recorder_open(struct recorder *recorder, const AVCodec *input_codec) { + const char *format_name = recorder_get_format_name(recorder->format); + assert(format_name); + const AVOutputFormat *format = find_muxer(format_name); + if (!format) { + LOGE("Could not find muxer"); + return false; + } + + recorder->ctx = avformat_alloc_context(); + if (!recorder->ctx) { + LOGE("Could not allocate output context"); + return false; + } + + // contrary to the deprecated API (av_oformat_next()), av_muxer_iterate() + // returns (on purpose) a pointer-to-const, but AVFormatContext.oformat + // still expects a pointer-to-non-const (it has not be updated accordingly) + // + recorder->ctx->oformat = (AVOutputFormat *) format; + + av_dict_set(&recorder->ctx->metadata, "comment", + "Recorded by scrcpy " SCRCPY_VERSION, 0); + + AVStream *ostream = avformat_new_stream(recorder->ctx, input_codec); + if (!ostream) { + avformat_free_context(recorder->ctx); + return false; + } + + ostream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO; + ostream->codecpar->codec_id = input_codec->id; + ostream->codecpar->format = AV_PIX_FMT_YUV420P; + ostream->codecpar->width = recorder->declared_frame_size.width; + ostream->codecpar->height = recorder->declared_frame_size.height; + + int ret = avio_open(&recorder->ctx->pb, recorder->filename, + AVIO_FLAG_WRITE); + if (ret < 0) { + LOGE("Failed to open output file: %s", recorder->filename); + // ostream will be cleaned up during context cleaning + avformat_free_context(recorder->ctx); + return false; + } + + LOGI("Recording started to %s file: %s", format_name, recorder->filename); + + return true; +} + +void +recorder_close(struct recorder *recorder) { + avio_close(recorder->ctx->pb); + avformat_free_context(recorder->ctx); +} + bool recorder_start(struct recorder *recorder) { LOGD("Starting recorder thread"); @@ -365,3 +321,47 @@ recorder_push(struct recorder *recorder, const AVPacket *packet) { sc_mutex_unlock(&recorder->mutex); return true; } + +bool +recorder_init(struct recorder *recorder, + const char *filename, + enum sc_record_format format, + struct size declared_frame_size) { + recorder->filename = strdup(filename); + if (!recorder->filename) { + LOGE("Could not strdup filename"); + return false; + } + + bool ok = sc_mutex_init(&recorder->mutex); + if (!ok) { + LOGC("Could not create mutex"); + free(recorder->filename); + return false; + } + + ok = sc_cond_init(&recorder->queue_cond); + if (!ok) { + LOGC("Could not create cond"); + sc_mutex_destroy(&recorder->mutex); + free(recorder->filename); + return false; + } + + queue_init(&recorder->queue); + recorder->stopped = false; + recorder->failed = false; + recorder->format = format; + recorder->declared_frame_size = declared_frame_size; + recorder->header_written = false; + recorder->previous = NULL; + + return true; +} + +void +recorder_destroy(struct recorder *recorder) { + sc_cond_destroy(&recorder->queue_cond); + sc_mutex_destroy(&recorder->mutex); + free(recorder->filename); +} From fe8de893ca58f3cce53819bc8409fc33d416387c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 11 Apr 2021 15:01:05 +0200 Subject: [PATCH 0557/2244] Privatize recorder threading The fact that the recorder uses a separate thread is an internal detail, so the functions _start(), _stop() and _join() should not be exposed. Instead, start the thread on _open() and _stop()+_join() on close(). This paves the way to expose the recorder as a packet sink trait. --- app/src/recorder.c | 36 +++++++++++++----------------------- app/src/recorder.h | 11 +---------- app/src/stream.c | 13 +------------ 3 files changed, 15 insertions(+), 45 deletions(-) diff --git a/app/src/recorder.c b/app/src/recorder.c index c24393de..387d8f79 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -259,6 +259,16 @@ recorder_open(struct recorder *recorder, const AVCodec *input_codec) { return false; } + LOGD("Starting recorder thread"); + bool ok = sc_thread_create(&recorder->thread, run_recorder, "recorder", + recorder); + if (!ok) { + LOGC("Could not start recorder thread"); + avio_close(recorder->ctx->pb); + avformat_free_context(recorder->ctx); + return false; + } + LOGI("Recording started to %s file: %s", format_name, recorder->filename); return true; @@ -266,35 +276,15 @@ recorder_open(struct recorder *recorder, const AVCodec *input_codec) { void recorder_close(struct recorder *recorder) { - avio_close(recorder->ctx->pb); - avformat_free_context(recorder->ctx); -} - -bool -recorder_start(struct recorder *recorder) { - LOGD("Starting recorder thread"); - - bool ok = sc_thread_create(&recorder->thread, run_recorder, "recorder", - recorder); - if (!ok) { - LOGC("Could not start recorder thread"); - return false; - } - - return true; -} - -void -recorder_stop(struct recorder *recorder) { sc_mutex_lock(&recorder->mutex); recorder->stopped = true; sc_cond_signal(&recorder->queue_cond); sc_mutex_unlock(&recorder->mutex); -} -void -recorder_join(struct recorder *recorder) { sc_thread_join(&recorder->thread, NULL); + + avio_close(recorder->ctx->pb); + avformat_free_context(recorder->ctx); } bool diff --git a/app/src/recorder.h b/app/src/recorder.h index be2b2dff..97e499bc 100644 --- a/app/src/recorder.h +++ b/app/src/recorder.h @@ -28,7 +28,7 @@ struct recorder { sc_thread thread; sc_mutex mutex; sc_cond queue_cond; - bool stopped; // set on recorder_stop() by the stream reader + bool stopped; // set on recorder_close() bool failed; // set on packet write failure struct recorder_queue queue; @@ -52,15 +52,6 @@ recorder_open(struct recorder *recorder, const AVCodec *input_codec); void recorder_close(struct recorder *recorder); -bool -recorder_start(struct recorder *recorder); - -void -recorder_stop(struct recorder *recorder); - -void -recorder_join(struct recorder *recorder); - bool recorder_push(struct recorder *recorder, const AVPacket *packet); diff --git a/app/src/stream.c b/app/src/stream.c index e0a223be..787e9515 100644 --- a/app/src/stream.c +++ b/app/src/stream.c @@ -203,17 +203,12 @@ run_stream(void *data) { LOGE("Could not open recorder"); goto finally_close_decoder; } - - if (!recorder_start(stream->recorder)) { - LOGE("Could not start recorder"); - goto finally_close_recorder; - } } stream->parser = av_parser_init(AV_CODEC_ID_H264); if (!stream->parser) { LOGE("Could not initialize parser"); - goto finally_stop_and_join_recorder; + goto finally_close_recorder; } // We must only pass complete frames to av_parser_parse2()! @@ -243,12 +238,6 @@ run_stream(void *data) { } av_parser_close(stream->parser); -finally_stop_and_join_recorder: - if (stream->recorder) { - recorder_stop(stream->recorder); - LOGI("Finishing recording..."); - recorder_join(stream->recorder); - } finally_close_recorder: if (stream->recorder) { recorder_close(stream->recorder); From 5980183a33bd75235c3dcffdf6e0027462493749 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 11 Apr 2021 15:01:05 +0200 Subject: [PATCH 0558/2244] Expose recorder as packet sink Make recorder implement the packet sink trait. This will allow the stream to push packets without depending on the concrete sink type. --- app/src/recorder.c | 29 +++++++++++++++++++++++++++++ app/src/recorder.h | 3 +++ 2 files changed, 32 insertions(+) diff --git a/app/src/recorder.c b/app/src/recorder.c index 387d8f79..fabbc95c 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -5,6 +5,9 @@ #include "util/log.h" +/** Downcast packet_sink to recorder */ +#define DOWNCAST(SINK) container_of(SINK, struct recorder, packet_sink) + static const AVRational SCRCPY_TIME_BASE = {1, 1000000}; // timestamps in us static const AVOutputFormat * @@ -312,6 +315,24 @@ recorder_push(struct recorder *recorder, const AVPacket *packet) { return true; } +static bool +recorder_packet_sink_open(struct sc_packet_sink *sink, const AVCodec *codec) { + struct recorder *recorder = DOWNCAST(sink); + return recorder_open(recorder, codec); +} + +static void +recorder_packet_sink_close(struct sc_packet_sink *sink) { + struct recorder *recorder = DOWNCAST(sink); + recorder_close(recorder); +} + +static bool +recorder_packet_sink_push(struct sc_packet_sink *sink, const AVPacket *packet) { + struct recorder *recorder = DOWNCAST(sink); + return recorder_push(recorder, packet); +} + bool recorder_init(struct recorder *recorder, const char *filename, @@ -346,6 +367,14 @@ recorder_init(struct recorder *recorder, recorder->header_written = false; recorder->previous = NULL; + static const struct sc_packet_sink_ops ops = { + .open = recorder_packet_sink_open, + .close = recorder_packet_sink_close, + .push = recorder_packet_sink_push, + }; + + recorder->packet_sink.ops = &ops; + return true; } diff --git a/app/src/recorder.h b/app/src/recorder.h index 97e499bc..4991f0cf 100644 --- a/app/src/recorder.h +++ b/app/src/recorder.h @@ -8,6 +8,7 @@ #include "coords.h" #include "scrcpy.h" +#include "trait/packet_sink.h" #include "util/queue.h" #include "util/thread.h" @@ -19,6 +20,8 @@ struct record_packet { struct recorder_queue QUEUE(struct record_packet); struct recorder { + struct sc_packet_sink packet_sink; // packet sink trait + char *filename; enum sc_record_format format; AVFormatContext *ctx; From 5beb7d6c0242bbb9995041fdd559982280a3499b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 11 Apr 2021 15:01:05 +0200 Subject: [PATCH 0559/2244] Reorder decoder functions This will make further commits more readable. --- app/src/decoder.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/decoder.c b/app/src/decoder.c index 247b459c..3b94afef 100644 --- a/app/src/decoder.c +++ b/app/src/decoder.c @@ -6,11 +6,6 @@ #include "video_buffer.h" #include "util/log.h" -void -decoder_init(struct decoder *decoder, struct video_buffer *vb) { - decoder->video_buffer = vb; -} - bool decoder_open(struct decoder *decoder, const AVCodec *codec) { decoder->codec_ctx = avcodec_alloc_context3(codec); @@ -63,3 +58,8 @@ decoder_push(struct decoder *decoder, const AVPacket *packet) { } return true; } + +void +decoder_init(struct decoder *decoder, struct video_buffer *vb) { + decoder->video_buffer = vb; +} From cbed38799e2e39bdf368bfd90ab028a896e307e3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 11 Apr 2021 15:01:05 +0200 Subject: [PATCH 0560/2244] Expose decoder as packet sink Make decoder implement the packet sink trait. This will allow the stream to push packets without depending on the concrete sink type. --- app/src/decoder.c | 29 +++++++++++++++++++++++++++++ app/src/decoder.h | 4 ++++ 2 files changed, 33 insertions(+) diff --git a/app/src/decoder.c b/app/src/decoder.c index 3b94afef..41e2fe85 100644 --- a/app/src/decoder.c +++ b/app/src/decoder.c @@ -6,6 +6,9 @@ #include "video_buffer.h" #include "util/log.h" +/** Downcast packet_sink to decoder */ +#define DOWNCAST(SINK) container_of(SINK, struct decoder, packet_sink) + bool decoder_open(struct decoder *decoder, const AVCodec *codec) { decoder->codec_ctx = avcodec_alloc_context3(codec); @@ -59,7 +62,33 @@ decoder_push(struct decoder *decoder, const AVPacket *packet) { return true; } +static bool +decoder_packet_sink_open(struct sc_packet_sink *sink, const AVCodec *codec) { + struct decoder *decoder = DOWNCAST(sink); + return decoder_open(decoder, codec); +} + +static void +decoder_packet_sink_close(struct sc_packet_sink *sink) { + struct decoder *decoder = DOWNCAST(sink); + decoder_close(decoder); +} + +static bool +decoder_packet_sink_push(struct sc_packet_sink *sink, const AVPacket *packet) { + struct decoder *decoder = DOWNCAST(sink); + return decoder_push(decoder, packet); +} + void decoder_init(struct decoder *decoder, struct video_buffer *vb) { decoder->video_buffer = vb; + + static const struct sc_packet_sink_ops ops = { + .open = decoder_packet_sink_open, + .close = decoder_packet_sink_close, + .push = decoder_packet_sink_push, + }; + + decoder->packet_sink.ops = &ops; } diff --git a/app/src/decoder.h b/app/src/decoder.h index 50dd7fe0..c3f7cb73 100644 --- a/app/src/decoder.h +++ b/app/src/decoder.h @@ -3,12 +3,16 @@ #include "common.h" +#include "trait/packet_sink.h" + #include #include struct video_buffer; struct decoder { + struct sc_packet_sink packet_sink; // packet sink trait + struct video_buffer *video_buffer; AVCodecContext *codec_ctx; From f7a1b67d66e8b6bd569a5ac608d85a595df3ec27 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 11 Apr 2021 15:01:05 +0200 Subject: [PATCH 0561/2244] Make stream push packets to sinks Now that decoder and recorder implement the packet sink trait, make stream push packets to the sinks without depending on the concrete sink types. --- app/src/decoder.c | 8 +++- app/src/decoder.h | 3 -- app/src/recorder.c | 6 +-- app/src/recorder.h | 9 ----- app/src/scrcpy.c | 10 ++++- app/src/stream.c | 96 +++++++++++++++++++++++++--------------------- app/src/stream.h | 15 ++++++-- 7 files changed, 82 insertions(+), 65 deletions(-) diff --git a/app/src/decoder.c b/app/src/decoder.c index 41e2fe85..134ffd3c 100644 --- a/app/src/decoder.c +++ b/app/src/decoder.c @@ -41,8 +41,14 @@ decoder_close(struct decoder *decoder) { avcodec_free_context(&decoder->codec_ctx); } -bool +static bool decoder_push(struct decoder *decoder, const AVPacket *packet) { + bool is_config = packet->pts == AV_NOPTS_VALUE; + if (is_config) { + // nothing to do + return true; + } + int ret; if ((ret = avcodec_send_packet(decoder->codec_ctx, packet)) < 0) { LOGE("Could not send video packet: %d", ret); diff --git a/app/src/decoder.h b/app/src/decoder.h index c3f7cb73..e3730db5 100644 --- a/app/src/decoder.h +++ b/app/src/decoder.h @@ -28,7 +28,4 @@ decoder_open(struct decoder *decoder, const AVCodec *codec); void decoder_close(struct decoder *decoder); -bool -decoder_push(struct decoder *decoder, const AVPacket *packet); - #endif diff --git a/app/src/recorder.c b/app/src/recorder.c index fabbc95c..f0ec86dc 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -216,7 +216,7 @@ run_recorder(void *data) { return 0; } -bool +static bool recorder_open(struct recorder *recorder, const AVCodec *input_codec) { const char *format_name = recorder_get_format_name(recorder->format); assert(format_name); @@ -277,7 +277,7 @@ recorder_open(struct recorder *recorder, const AVCodec *input_codec) { return true; } -void +static void recorder_close(struct recorder *recorder) { sc_mutex_lock(&recorder->mutex); recorder->stopped = true; @@ -290,7 +290,7 @@ recorder_close(struct recorder *recorder) { avformat_free_context(recorder->ctx); } -bool +static bool recorder_push(struct recorder *recorder, const AVPacket *packet) { sc_mutex_lock(&recorder->mutex); assert(!recorder->stopped); diff --git a/app/src/recorder.h b/app/src/recorder.h index 4991f0cf..1b2b9284 100644 --- a/app/src/recorder.h +++ b/app/src/recorder.h @@ -49,13 +49,4 @@ recorder_init(struct recorder *recorder, const char *filename, void recorder_destroy(struct recorder *recorder); -bool -recorder_open(struct recorder *recorder, const AVCodec *input_codec); - -void -recorder_close(struct recorder *recorder); - -bool -recorder_push(struct recorder *recorder, const AVPacket *packet); - #endif diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 2f8abd64..ad705c4a 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -336,7 +336,15 @@ scrcpy(const struct scrcpy_options *options) { av_log_set_callback(av_log_callback); - stream_init(&stream, server.video_socket, dec, rec); + stream_init(&stream, server.video_socket); + + if (dec) { + stream_add_sink(&stream, &dec->packet_sink); + } + + if (rec) { + stream_add_sink(&stream, &rec->packet_sink); + } if (options->display) { if (options->control) { diff --git a/app/src/stream.c b/app/src/stream.c index 787e9515..a11218e3 100644 --- a/app/src/stream.c +++ b/app/src/stream.c @@ -66,25 +66,11 @@ notify_stopped(void) { } static bool -process_config_packet(struct stream *stream, AVPacket *packet) { - if (stream->recorder && !recorder_push(stream->recorder, packet)) { - LOGE("Could not send config packet to recorder"); - return false; - } - return true; -} - -static bool -process_frame(struct stream *stream, AVPacket *packet) { - if (stream->decoder && !decoder_push(stream->decoder, packet)) { - return false; - } - - if (stream->recorder) { - packet->dts = packet->pts; - - if (!recorder_push(stream->recorder, packet)) { - LOGE("Could not send packet to recorder"); +push_packet_to_sinks(struct stream *stream, const AVPacket *packet) { + for (unsigned i = 0; i < stream->sink_count; ++i) { + struct sc_packet_sink *sink = stream->sinks[i]; + if (!sink->ops->push(sink, packet)) { + LOGE("Could not send config packet to sink %d", i); return false; } } @@ -111,9 +97,11 @@ stream_parse(struct stream *stream, AVPacket *packet) { packet->flags |= AV_PKT_FLAG_KEY; } - bool ok = process_frame(stream, packet); + packet->dts = packet->pts; + + bool ok = push_packet_to_sinks(stream, packet); if (!ok) { - LOGE("Could not process frame"); + LOGE("Could not process packet"); return false; } @@ -156,7 +144,7 @@ stream_push_packet(struct stream *stream, AVPacket *packet) { if (is_config) { // config packet - bool ok = process_config_packet(stream, packet); + bool ok = push_packet_to_sinks(stream, packet); if (!ok) { return false; } @@ -177,6 +165,33 @@ stream_push_packet(struct stream *stream, AVPacket *packet) { return true; } +static void +stream_close_first_sinks(struct stream *stream, unsigned count) { + while (count) { + struct sc_packet_sink *sink = stream->sinks[--count]; + sink->ops->close(sink); + } +} + +static inline void +stream_close_sinks(struct stream *stream) { + stream_close_first_sinks(stream, stream->sink_count); +} + +static bool +stream_open_sinks(struct stream *stream, const AVCodec *codec) { + for (unsigned i = 0; i < stream->sink_count; ++i) { + struct sc_packet_sink *sink = stream->sinks[i]; + if (!sink->ops->open(sink, codec)) { + LOGE("Could not open packet sink %d", i); + stream_close_first_sinks(stream, i); + return false; + } + } + + return true; +} + static int run_stream(void *data) { struct stream *stream = data; @@ -193,22 +208,15 @@ run_stream(void *data) { goto end; } - if (stream->decoder && !decoder_open(stream->decoder, codec)) { - LOGE("Could not open decoder"); + if (!stream_open_sinks(stream, codec)) { + LOGE("Could not open stream sinks"); goto finally_free_codec_ctx; } - if (stream->recorder) { - if (!recorder_open(stream->recorder, codec)) { - LOGE("Could not open recorder"); - goto finally_close_decoder; - } - } - stream->parser = av_parser_init(AV_CODEC_ID_H264); if (!stream->parser) { LOGE("Could not initialize parser"); - goto finally_close_recorder; + goto finally_close_sinks; } // We must only pass complete frames to av_parser_parse2()! @@ -238,14 +246,8 @@ run_stream(void *data) { } av_parser_close(stream->parser); -finally_close_recorder: - if (stream->recorder) { - recorder_close(stream->recorder); - } -finally_close_decoder: - if (stream->decoder) { - decoder_close(stream->decoder); - } +finally_close_sinks: + stream_close_sinks(stream); finally_free_codec_ctx: avcodec_free_context(&stream->codec_ctx); end: @@ -254,12 +256,18 @@ end: } void -stream_init(struct stream *stream, socket_t socket, - struct decoder *decoder, struct recorder *recorder) { +stream_init(struct stream *stream, socket_t socket) { stream->socket = socket; - stream->decoder = decoder, - stream->recorder = recorder; stream->has_pending = false; + stream->sink_count = 0; +} + +void +stream_add_sink(struct stream *stream, struct sc_packet_sink *sink) { + assert(stream->sink_count < STREAM_MAX_SINKS); + assert(sink); + assert(sink->ops); + stream->sinks[stream->sink_count++] = sink; } bool diff --git a/app/src/stream.h b/app/src/stream.h index 421c1bf0..81175420 100644 --- a/app/src/stream.h +++ b/app/src/stream.h @@ -8,14 +8,19 @@ #include #include +#include "trait/packet_sink.h" #include "util/net.h" #include "util/thread.h" +#define STREAM_MAX_SINKS 2 + struct stream { socket_t socket; sc_thread thread; - struct decoder *decoder; - struct recorder *recorder; + + struct sc_packet_sink *sinks[STREAM_MAX_SINKS]; + unsigned sink_count; + AVCodecContext *codec_ctx; AVCodecParserContext *parser; // successive packets may need to be concatenated, until a non-config @@ -25,8 +30,10 @@ struct stream { }; void -stream_init(struct stream *stream, socket_t socket, - struct decoder *decoder, struct recorder *recorder); +stream_init(struct stream *stream, socket_t socket); + +void +stream_add_sink(struct stream *stream, struct sc_packet_sink *sink); bool stream_start(struct stream *stream); From deab7da761d856de22e5880e5376d53cd2e8fb77 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 11 Apr 2021 15:39:00 +0200 Subject: [PATCH 0562/2244] Add frame sink trait This trait will allow to abstract the concrete sink types from the frame producer (decoder.c). --- app/src/trait/frame_sink.h | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 app/src/trait/frame_sink.h diff --git a/app/src/trait/frame_sink.h b/app/src/trait/frame_sink.h new file mode 100644 index 00000000..64ab0de9 --- /dev/null +++ b/app/src/trait/frame_sink.h @@ -0,0 +1,26 @@ +#ifndef SC_FRAME_SINK +#define SC_FRAME_SINK + +#include "common.h" + +#include +#include + +typedef struct AVFrame AVFrame; + +/** + * Frame sink trait. + * + * Component able to receive AVFrames should implement this trait. + */ +struct sc_frame_sink { + const struct sc_frame_sink_ops *ops; +}; + +struct sc_frame_sink_ops { + bool (*open)(struct sc_frame_sink *sink); + void (*close)(struct sc_frame_sink *sink); + bool (*push)(struct sc_frame_sink *sink, const AVFrame *frame); +}; + +#endif From 08b3086ffc95f846bc3a420f5d4296092020eb8c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 11 Apr 2021 15:01:05 +0200 Subject: [PATCH 0563/2244] Expose screen as frame sink Make screen implement the frame sink trait. This will allow the decoder to push frames without depending on the concrete sink type. --- app/src/screen.c | 33 +++++++++++++++++++++++++++++++++ app/src/screen.h | 3 +++ 2 files changed, 36 insertions(+) diff --git a/app/src/screen.c b/app/src/screen.c index de734554..0aa0b832 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -13,6 +13,8 @@ #define DISPLAY_MARGINS 96 +#define DOWNCAST(SINK) container_of(SINK, struct screen, frame_sink) + static inline struct size get_rotated_size(struct size size, int rotation) { struct size rotated_size; @@ -262,6 +264,29 @@ event_watcher(void *data, SDL_Event *event) { } #endif +static bool +screen_frame_sink_open(struct sc_frame_sink *sink) { + struct screen *screen = DOWNCAST(sink); + (void) screen; + + // nothing to do, the screen is already open on the main thread + return true; +} + +static void +screen_frame_sink_close(struct sc_frame_sink *sink) { + struct screen *screen = DOWNCAST(sink); + (void) screen; + + // nothing to do, the screen lifecycle is not managed by the frame producer +} + +static bool +screen_frame_sink_push(struct sc_frame_sink *sink, const AVFrame *frame) { + struct screen *screen = DOWNCAST(sink); + return video_buffer_push(screen->vb, frame); +} + bool screen_init(struct screen *screen, struct video_buffer *vb, struct fps_counter *fps_counter, @@ -402,6 +427,14 @@ screen_init(struct screen *screen, struct video_buffer *vb, SDL_AddEventWatch(event_watcher, screen); #endif + static const struct sc_frame_sink_ops ops = { + .open = screen_frame_sink_open, + .close = screen_frame_sink_close, + .push = screen_frame_sink_push, + }; + + screen->frame_sink.ops = &ops; + return true; } diff --git a/app/src/screen.h b/app/src/screen.h index cd849779..d57b7152 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -9,10 +9,13 @@ #include "coords.h" #include "opengl.h" +#include "trait/frame_sink.h" struct video_buffer; struct screen { + struct sc_frame_sink frame_sink; // frame sink trait + struct video_buffer *vb; struct fps_counter *fps_counter; From 6f5ad21f5757482b71a5d197f920ea39fb2f1167 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 11 Apr 2021 15:01:05 +0200 Subject: [PATCH 0564/2244] Make decoder push frames to sinks Now that screen implements the packet sink trait, make decoder push packets to the sinks without depending on the concrete sink types. --- app/src/decoder.c | 68 ++++++++++++++++++++++++++++++++++++++++++----- app/src/decoder.h | 12 ++++----- app/src/scrcpy.c | 4 ++- 3 files changed, 70 insertions(+), 14 deletions(-) diff --git a/app/src/decoder.c b/app/src/decoder.c index 134ffd3c..34f2a15f 100644 --- a/app/src/decoder.c +++ b/app/src/decoder.c @@ -4,12 +4,40 @@ #include "events.h" #include "video_buffer.h" +#include "trait/frame_sink.h" #include "util/log.h" /** Downcast packet_sink to decoder */ #define DOWNCAST(SINK) container_of(SINK, struct decoder, packet_sink) -bool +static void +decoder_close_first_sinks(struct decoder *decoder, unsigned count) { + while (count) { + struct sc_frame_sink *sink = decoder->sinks[--count]; + sink->ops->close(sink); + } +} + +static inline void +decoder_close_sinks(struct decoder *decoder) { + decoder_close_first_sinks(decoder, decoder->sink_count); +} + +static bool +decoder_open_sinks(struct decoder *decoder) { + for (unsigned i = 0; i < decoder->sink_count; ++i) { + struct sc_frame_sink *sink = decoder->sinks[i]; + if (!sink->ops->open(sink)) { + LOGE("Could not open frame sink %d", i); + decoder_close_first_sinks(decoder, i); + return false; + } + } + + return true; +} + +static bool decoder_open(struct decoder *decoder, const AVCodec *codec) { decoder->codec_ctx = avcodec_alloc_context3(codec); if (!decoder->codec_ctx) { @@ -31,16 +59,38 @@ decoder_open(struct decoder *decoder, const AVCodec *codec) { return false; } + if (!decoder_open_sinks(decoder)) { + LOGE("Could not open decoder sinks"); + av_frame_free(&decoder->frame); + avcodec_close(decoder->codec_ctx); + avcodec_free_context(&decoder->codec_ctx); + return false; + } + return true; } -void +static void decoder_close(struct decoder *decoder) { + decoder_close_sinks(decoder); av_frame_free(&decoder->frame); avcodec_close(decoder->codec_ctx); avcodec_free_context(&decoder->codec_ctx); } +static bool +push_frame_to_sinks(struct decoder *decoder, const AVFrame *frame) { + for (unsigned i = 0; i < decoder->sink_count; ++i) { + struct sc_frame_sink *sink = decoder->sinks[i]; + if (!sink->ops->push(sink, frame)) { + LOGE("Could not send frame to sink %d", i); + return false; + } + } + + return true; +} + static bool decoder_push(struct decoder *decoder, const AVPacket *packet) { bool is_config = packet->pts == AV_NOPTS_VALUE; @@ -57,7 +107,7 @@ decoder_push(struct decoder *decoder, const AVPacket *packet) { ret = avcodec_receive_frame(decoder->codec_ctx, decoder->frame); if (!ret) { // a frame was received - bool ok = video_buffer_push(decoder->video_buffer, decoder->frame); + bool ok = push_frame_to_sinks(decoder, decoder->frame); // A frame lost should not make the whole pipeline fail. The error, if // any, is already logged. (void) ok; @@ -87,9 +137,7 @@ decoder_packet_sink_push(struct sc_packet_sink *sink, const AVPacket *packet) { } void -decoder_init(struct decoder *decoder, struct video_buffer *vb) { - decoder->video_buffer = vb; - +decoder_init(struct decoder *decoder) { static const struct sc_packet_sink_ops ops = { .open = decoder_packet_sink_open, .close = decoder_packet_sink_close, @@ -98,3 +146,11 @@ decoder_init(struct decoder *decoder, struct video_buffer *vb) { decoder->packet_sink.ops = &ops; } + +void +decoder_add_sink(struct decoder *decoder, struct sc_frame_sink *sink) { + assert(decoder->sink_count < DECODER_MAX_SINKS); + assert(sink); + assert(sink->ops); + decoder->sinks[decoder->sink_count++] = sink; +} diff --git a/app/src/decoder.h b/app/src/decoder.h index e3730db5..bae97869 100644 --- a/app/src/decoder.h +++ b/app/src/decoder.h @@ -8,24 +8,22 @@ #include #include -struct video_buffer; +#define DECODER_MAX_SINKS 1 struct decoder { struct sc_packet_sink packet_sink; // packet sink trait - struct video_buffer *video_buffer; + struct sc_frame_sink *sinks[DECODER_MAX_SINKS]; + unsigned sink_count; AVCodecContext *codec_ctx; AVFrame *frame; }; void -decoder_init(struct decoder *decoder, struct video_buffer *vb); - -bool -decoder_open(struct decoder *decoder, const AVCodec *codec); +decoder_init(struct decoder *decoder); void -decoder_close(struct decoder *decoder); +decoder_add_sink(struct decoder *decoder, struct sc_frame_sink *sink); #endif diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index ad705c4a..e8715cbe 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -318,7 +318,7 @@ scrcpy(const struct scrcpy_options *options) { file_handler_initialized = true; } - decoder_init(&decoder, &video_buffer); + decoder_init(&decoder); dec = &decoder; } @@ -382,6 +382,8 @@ scrcpy(const struct scrcpy_options *options) { } screen_initialized = true; + decoder_add_sink(&decoder, &screen.frame_sink); + if (options->turn_screen_off) { struct control_msg msg; msg.type = CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE; From e91acdb0c43f2db43c211f43c4b9a46ce3e76084 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 11 Apr 2021 15:01:05 +0200 Subject: [PATCH 0565/2244] Move video_buffer to screen The video buffer is now an internal detail of the screen component. Since the screen is plugged to the decoder via the frame sink trait, the decoder does not access to the video buffer anymore. --- app/src/scrcpy.c | 15 +-------------- app/src/screen.c | 20 ++++++++++++++------ app/src/screen.h | 8 +++----- 3 files changed, 18 insertions(+), 25 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index e8715cbe..cab63522 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -25,14 +25,12 @@ #include "server.h" #include "stream.h" #include "tiny_xpm.h" -#include "video_buffer.h" #include "util/log.h" #include "util/net.h" static struct server server; static struct screen screen; static struct fps_counter fps_counter; -static struct video_buffer video_buffer; static struct stream stream; static struct decoder decoder; static struct recorder recorder; @@ -247,7 +245,6 @@ scrcpy(const struct scrcpy_options *options) { bool server_started = false; bool fps_counter_initialized = false; - bool video_buffer_initialized = false; bool file_handler_initialized = false; bool recorder_initialized = false; bool stream_started = false; @@ -305,11 +302,6 @@ scrcpy(const struct scrcpy_options *options) { } fps_counter_initialized = true; - if (!video_buffer_init(&video_buffer)) { - goto end; - } - video_buffer_initialized = true; - if (options->control) { if (!file_handler_init(&file_handler, server.serial, options->push_target)) { @@ -376,8 +368,7 @@ scrcpy(const struct scrcpy_options *options) { .fullscreen = options->fullscreen, }; - if (!screen_init(&screen, &video_buffer, &fps_counter, - &screen_params)) { + if (!screen_init(&screen, &fps_counter, &screen_params)) { goto end; } screen_initialized = true; @@ -453,10 +444,6 @@ end: file_handler_destroy(&file_handler); } - if (video_buffer_initialized) { - video_buffer_destroy(&video_buffer); - } - if (fps_counter_initialized) { fps_counter_join(&fps_counter); fps_counter_destroy(&fps_counter); diff --git a/app/src/screen.c b/app/src/screen.c index 0aa0b832..c067ab7a 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -284,14 +284,12 @@ screen_frame_sink_close(struct sc_frame_sink *sink) { static bool screen_frame_sink_push(struct sc_frame_sink *sink, const AVFrame *frame) { struct screen *screen = DOWNCAST(sink); - return video_buffer_push(screen->vb, frame); + return video_buffer_push(&screen->vb, frame); } bool -screen_init(struct screen *screen, struct video_buffer *vb, - struct fps_counter *fps_counter, +screen_init(struct screen *screen, struct fps_counter *fps_counter, const struct screen_params *params) { - screen->vb = vb; screen->fps_counter = fps_counter; screen->resize_pending = false; @@ -299,11 +297,17 @@ screen_init(struct screen *screen, struct video_buffer *vb, screen->fullscreen = false; screen->maximized = false; + bool ok = video_buffer_init(&screen->vb); + if (!ok) { + LOGE("Could not initialize video buffer"); + return false; + } + static const struct video_buffer_callbacks cbs = { .on_frame_available = on_frame_available, .on_frame_skipped = on_frame_skipped, }; - video_buffer_set_consumer_callbacks(vb, &cbs, screen); + video_buffer_set_consumer_callbacks(&screen->vb, &cbs, screen); screen->frame_size = params->frame_size; screen->rotation = params->rotation; @@ -349,6 +353,7 @@ screen_init(struct screen *screen, struct video_buffer *vb, if (!screen->renderer) { LOGC("Could not create renderer: %s", SDL_GetError()); SDL_DestroyWindow(screen->window); + video_buffer_destroy(&screen->vb); return false; } @@ -400,6 +405,7 @@ screen_init(struct screen *screen, struct video_buffer *vb, LOGC("Could not create texture: %s", SDL_GetError()); SDL_DestroyRenderer(screen->renderer); SDL_DestroyWindow(screen->window); + video_buffer_destroy(&screen->vb); return false; } @@ -409,6 +415,7 @@ screen_init(struct screen *screen, struct video_buffer *vb, SDL_DestroyTexture(screen->texture); SDL_DestroyRenderer(screen->renderer); SDL_DestroyWindow(screen->window); + video_buffer_destroy(&screen->vb); return false; } @@ -449,6 +456,7 @@ screen_destroy(struct screen *screen) { SDL_DestroyTexture(screen->texture); SDL_DestroyRenderer(screen->renderer); SDL_DestroyWindow(screen->window); + video_buffer_destroy(&screen->vb); } static void @@ -554,7 +562,7 @@ update_texture(struct screen *screen, const AVFrame *frame) { static bool screen_update_frame(struct screen *screen) { av_frame_unref(screen->frame); - video_buffer_consume(screen->vb, screen->frame); + video_buffer_consume(&screen->vb, screen->frame); AVFrame *frame = screen->frame; fps_counter_add_rendered_frame(screen->fps_counter); diff --git a/app/src/screen.h b/app/src/screen.h index d57b7152..3b4506e3 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -10,13 +10,12 @@ #include "coords.h" #include "opengl.h" #include "trait/frame_sink.h" - -struct video_buffer; +#include "video_buffer.h" struct screen { struct sc_frame_sink frame_sink; // frame sink trait - struct video_buffer *vb; + struct video_buffer vb; struct fps_counter *fps_counter; SDL_Window *window; @@ -63,8 +62,7 @@ struct screen_params { // initialize screen, create window, renderer and texture (window is hidden) bool -screen_init(struct screen *screen, struct video_buffer *vb, - struct fps_counter *fps_counter, +screen_init(struct screen *screen, struct fps_counter *fps_counter, const struct screen_params *params); // destroy window, renderer and texture (if any) From 2a94a2b119a9efd2cd06ded03280872c81abe6b4 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 11 Apr 2021 15:01:05 +0200 Subject: [PATCH 0566/2244] Remove video_buffer callbacks Now that screen is both the owner and the listener of the video buffer, execute the code directly without callbacks. --- app/src/screen.c | 49 ++++++++++++++++++------------------------ app/src/video_buffer.c | 31 +++++--------------------- app/src/video_buffer.h | 20 +---------------- 3 files changed, 27 insertions(+), 73 deletions(-) diff --git a/app/src/screen.c b/app/src/screen.c index c067ab7a..ddf81b08 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -193,27 +193,6 @@ screen_update_content_rect(struct screen *screen) { } } -static void -on_frame_available(struct video_buffer *vb, void *userdata) { - (void) vb; - (void) userdata; - - static SDL_Event new_frame_event = { - .type = EVENT_NEW_FRAME, - }; - - // Post the event on the UI thread - SDL_PushEvent(&new_frame_event); -} - -static void -on_frame_skipped(struct video_buffer *vb, void *userdata) { - (void) vb; - - struct screen *screen = userdata; - fps_counter_add_skipped_frame(screen->fps_counter); -} - static inline SDL_Texture * create_texture(struct screen *screen) { SDL_Renderer *renderer = screen->renderer; @@ -284,7 +263,27 @@ screen_frame_sink_close(struct sc_frame_sink *sink) { static bool screen_frame_sink_push(struct sc_frame_sink *sink, const AVFrame *frame) { struct screen *screen = DOWNCAST(sink); - return video_buffer_push(&screen->vb, frame); + + bool previous_frame_skipped; + bool ok = video_buffer_push(&screen->vb, frame, &previous_frame_skipped); + if (!ok) { + return false; + } + + if (previous_frame_skipped) { + fps_counter_add_skipped_frame(screen->fps_counter); + // The EVENT_NEW_FRAME triggered for the previous frame will consume + // this new frame instead + } else { + static SDL_Event new_frame_event = { + .type = EVENT_NEW_FRAME, + }; + + // Post the event on the UI thread + SDL_PushEvent(&new_frame_event); + } + + return true; } bool @@ -303,12 +302,6 @@ screen_init(struct screen *screen, struct fps_counter *fps_counter, return false; } - static const struct video_buffer_callbacks cbs = { - .on_frame_available = on_frame_available, - .on_frame_skipped = on_frame_skipped, - }; - video_buffer_set_consumer_callbacks(&screen->vb, &cbs, screen); - screen->frame_size = params->frame_size; screen->rotation = params->rotation; if (screen->rotation) { diff --git a/app/src/video_buffer.c b/app/src/video_buffer.c index 18a180fa..7adf098b 100644 --- a/app/src/video_buffer.c +++ b/app/src/video_buffer.c @@ -29,10 +29,6 @@ video_buffer_init(struct video_buffer *vb) { // there is initially no frame, so consider it has already been consumed vb->pending_frame_consumed = true; - // The callbacks must be set by the consumer via - // video_buffer_set_consumer_callbacks() - vb->cbs = NULL; - return true; } @@ -50,21 +46,9 @@ swap_frames(AVFrame **lhs, AVFrame **rhs) { *rhs = tmp; } -void -video_buffer_set_consumer_callbacks(struct video_buffer *vb, - const struct video_buffer_callbacks *cbs, - void *cbs_userdata) { - assert(!vb->cbs); // must be set only once - assert(cbs); - assert(cbs->on_frame_available); - vb->cbs = cbs; - vb->cbs_userdata = cbs_userdata; -} - bool -video_buffer_push(struct video_buffer *vb, const AVFrame *frame) { - assert(vb->cbs); - +video_buffer_push(struct video_buffer *vb, const AVFrame *frame, + bool *previous_frame_skipped) { sc_mutex_lock(&vb->mutex); // Use a temporary frame to preserve pending_frame in case of error. @@ -80,18 +64,13 @@ video_buffer_push(struct video_buffer *vb, const AVFrame *frame) { swap_frames(&vb->pending_frame, &vb->tmp_frame); av_frame_unref(vb->tmp_frame); - bool skipped = !vb->pending_frame_consumed; + if (previous_frame_skipped) { + *previous_frame_skipped = !vb->pending_frame_consumed; + } vb->pending_frame_consumed = false; sc_mutex_unlock(&vb->mutex); - if (skipped) { - if (vb->cbs->on_frame_skipped) - vb->cbs->on_frame_skipped(vb, vb->cbs_userdata); - } else { - vb->cbs->on_frame_available(vb, vb->cbs_userdata); - } - return true; } diff --git a/app/src/video_buffer.h b/app/src/video_buffer.h index cdecb259..b9478f4c 100644 --- a/app/src/video_buffer.h +++ b/app/src/video_buffer.h @@ -33,19 +33,6 @@ struct video_buffer { sc_mutex mutex; bool pending_frame_consumed; - - const struct video_buffer_callbacks *cbs; - void *cbs_userdata; -}; - -struct video_buffer_callbacks { - // Called when a new frame can be consumed. - // This callback is mandatory (it must not be NULL). - void (*on_frame_available)(struct video_buffer *vb, void *userdata); - - // Called when a pending frame has been overwritten by the producer. - // This callback is optional (it may be NULL). - void (*on_frame_skipped)(struct video_buffer *vb, void *userdata); }; bool @@ -54,13 +41,8 @@ video_buffer_init(struct video_buffer *vb); void video_buffer_destroy(struct video_buffer *vb); -void -video_buffer_set_consumer_callbacks(struct video_buffer *vb, - const struct video_buffer_callbacks *cbs, - void *cbs_userdata); - bool -video_buffer_push(struct video_buffer *vb, const AVFrame *frame); +video_buffer_push(struct video_buffer *vb, const AVFrame *frame, bool *skipped); void video_buffer_consume(struct video_buffer *vb, AVFrame *dst); From 0272e6dc772664bdbad21eab5c28919e63102bdd Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 11 Apr 2021 15:01:05 +0200 Subject: [PATCH 0567/2244] Assert screen closed on destroy The destruction order is important, but tricky, because the screen is open/close by the decoder, but destroyed by scrcpy.c on the main thread. Add assertions to guarantee that the screen is not destroyed before being closed. --- app/src/screen.c | 13 +++++++++++++ app/src/screen.h | 4 ++++ 2 files changed, 17 insertions(+) diff --git a/app/src/screen.c b/app/src/screen.c index ddf81b08..1b3c5179 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -247,6 +247,9 @@ static bool screen_frame_sink_open(struct sc_frame_sink *sink) { struct screen *screen = DOWNCAST(sink); (void) screen; +#ifndef NDEBUG + screen->open = true; +#endif // nothing to do, the screen is already open on the main thread return true; @@ -256,6 +259,9 @@ static void screen_frame_sink_close(struct sc_frame_sink *sink) { struct screen *screen = DOWNCAST(sink); (void) screen; +#ifndef NDEBUG + screen->open = false; +#endif // nothing to do, the screen lifecycle is not managed by the frame producer } @@ -435,6 +441,10 @@ screen_init(struct screen *screen, struct fps_counter *fps_counter, screen->frame_sink.ops = &ops; +#ifndef NDEBUG + screen->open = false; +#endif + return true; } @@ -445,6 +455,9 @@ screen_show_window(struct screen *screen) { void screen_destroy(struct screen *screen) { +#ifndef NDEBUG + assert(!screen->open); +#endif av_frame_free(&screen->frame); SDL_DestroyTexture(screen->texture); SDL_DestroyRenderer(screen->renderer); diff --git a/app/src/screen.h b/app/src/screen.h index 3b4506e3..4a0bad09 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -15,6 +15,10 @@ struct screen { struct sc_frame_sink frame_sink; // frame sink trait +#ifndef NDEBUG + bool open; // track the open/close state to assert correct behavior +#endif + struct video_buffer vb; struct fps_counter *fps_counter; From 0541f1bff2be8dcf4de0765ce357836c0b2bf4ca Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 13 Apr 2021 22:22:54 +0200 Subject: [PATCH 0568/2244] Hide the window immediately on close The screen may not be destroyed immediately on close to avoid undefined behavior, because it may still receive events from the decoder. But the visual window must still be closed immediately. --- app/src/scrcpy.c | 4 ++++ app/src/screen.c | 5 +++++ app/src/screen.h | 7 +++++++ 3 files changed, 16 insertions(+) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index cab63522..4de62389 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -398,6 +398,10 @@ scrcpy(const struct scrcpy_options *options) { ret = event_loop(options); LOGD("quit..."); + // Close the window immediately on closing, because screen_destroy() may + // only be called once the stream thread is joined (it may take time) + screen_hide_window(&screen); + end: // The stream is not stopped explicitly, because it will stop by itself on // end-of-stream diff --git a/app/src/screen.c b/app/src/screen.c index 1b3c5179..0598ccf4 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -453,6 +453,11 @@ screen_show_window(struct screen *screen) { SDL_ShowWindow(screen->window); } +void +screen_hide_window(struct screen *screen) { + SDL_HideWindow(screen->window); +} + void screen_destroy(struct screen *screen) { #ifndef NDEBUG diff --git a/app/src/screen.h b/app/src/screen.h index 4a0bad09..2921c701 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -73,6 +73,13 @@ screen_init(struct screen *screen, struct fps_counter *fps_counter, void screen_destroy(struct screen *screen); +// hide the window +// +// It is used to hide the window immediately on closing without waiting for +// screen_destroy() +void +screen_hide_window(struct screen *screen); + // render the texture to the renderer // // Set the update_content_rect flag if the window or content size may have From e3fccc5a5e697d55283a2a038a455af372ed6da6 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 18 Apr 2021 11:25:58 +0200 Subject: [PATCH 0569/2244] Initialize recorder fields on open Only initialize ops and parameters copy from recorder_init(). It was inconsistent to initialize some fields from _init() and some others from _open(). --- app/src/recorder.c | 55 +++++++++++++++++++++++++++------------------- 1 file changed, 32 insertions(+), 23 deletions(-) diff --git a/app/src/recorder.c b/app/src/recorder.c index f0ec86dc..e96c4a52 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -218,17 +218,40 @@ run_recorder(void *data) { static bool recorder_open(struct recorder *recorder, const AVCodec *input_codec) { + bool ok = sc_mutex_init(&recorder->mutex); + if (!ok) { + LOGC("Could not create mutex"); + return false; + } + + ok = sc_cond_init(&recorder->queue_cond); + if (!ok) { + LOGC("Could not create cond"); + sc_mutex_destroy(&recorder->mutex); + return false; + } + + queue_init(&recorder->queue); + recorder->stopped = false; + recorder->failed = false; + recorder->header_written = false; + recorder->previous = NULL; + const char *format_name = recorder_get_format_name(recorder->format); assert(format_name); const AVOutputFormat *format = find_muxer(format_name); if (!format) { LOGE("Could not find muxer"); + sc_cond_destroy(&recorder->queue_cond); + sc_mutex_destroy(&recorder->mutex); return false; } recorder->ctx = avformat_alloc_context(); if (!recorder->ctx) { LOGE("Could not allocate output context"); + sc_cond_destroy(&recorder->queue_cond); + sc_mutex_destroy(&recorder->mutex); return false; } @@ -244,6 +267,8 @@ recorder_open(struct recorder *recorder, const AVCodec *input_codec) { AVStream *ostream = avformat_new_stream(recorder->ctx, input_codec); if (!ostream) { avformat_free_context(recorder->ctx); + sc_cond_destroy(&recorder->queue_cond); + sc_mutex_destroy(&recorder->mutex); return false; } @@ -259,16 +284,20 @@ recorder_open(struct recorder *recorder, const AVCodec *input_codec) { LOGE("Failed to open output file: %s", recorder->filename); // ostream will be cleaned up during context cleaning avformat_free_context(recorder->ctx); + sc_cond_destroy(&recorder->queue_cond); + sc_mutex_destroy(&recorder->mutex); return false; } LOGD("Starting recorder thread"); - bool ok = sc_thread_create(&recorder->thread, run_recorder, "recorder", + ok = sc_thread_create(&recorder->thread, run_recorder, "recorder", recorder); if (!ok) { LOGC("Could not start recorder thread"); avio_close(recorder->ctx->pb); avformat_free_context(recorder->ctx); + sc_cond_destroy(&recorder->queue_cond); + sc_mutex_destroy(&recorder->mutex); return false; } @@ -288,6 +317,8 @@ recorder_close(struct recorder *recorder) { avio_close(recorder->ctx->pb); avformat_free_context(recorder->ctx); + sc_cond_destroy(&recorder->queue_cond); + sc_mutex_destroy(&recorder->mutex); } static bool @@ -344,28 +375,8 @@ recorder_init(struct recorder *recorder, return false; } - bool ok = sc_mutex_init(&recorder->mutex); - if (!ok) { - LOGC("Could not create mutex"); - free(recorder->filename); - return false; - } - - ok = sc_cond_init(&recorder->queue_cond); - if (!ok) { - LOGC("Could not create cond"); - sc_mutex_destroy(&recorder->mutex); - free(recorder->filename); - return false; - } - - queue_init(&recorder->queue); - recorder->stopped = false; - recorder->failed = false; recorder->format = format; recorder->declared_frame_size = declared_frame_size; - recorder->header_written = false; - recorder->previous = NULL; static const struct sc_packet_sink_ops ops = { .open = recorder_packet_sink_open, @@ -380,7 +391,5 @@ recorder_init(struct recorder *recorder, void recorder_destroy(struct recorder *recorder) { - sc_cond_destroy(&recorder->queue_cond); - sc_mutex_destroy(&recorder->mutex); free(recorder->filename); } From 2a5dfc1c177bfeaf034b29c3128bcab5eadff969 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 18 Apr 2021 11:32:21 +0200 Subject: [PATCH 0570/2244] Handle errors using gotos in recorder_open() There are many initializations in recorder_open(). Handle RAII-like deinitialization using gotos. --- app/src/recorder.c | 38 +++++++++++++++++--------------------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/app/src/recorder.c b/app/src/recorder.c index e96c4a52..479bdb39 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -227,8 +227,7 @@ recorder_open(struct recorder *recorder, const AVCodec *input_codec) { ok = sc_cond_init(&recorder->queue_cond); if (!ok) { LOGC("Could not create cond"); - sc_mutex_destroy(&recorder->mutex); - return false; + goto error_mutex_destroy; } queue_init(&recorder->queue); @@ -242,17 +241,13 @@ recorder_open(struct recorder *recorder, const AVCodec *input_codec) { const AVOutputFormat *format = find_muxer(format_name); if (!format) { LOGE("Could not find muxer"); - sc_cond_destroy(&recorder->queue_cond); - sc_mutex_destroy(&recorder->mutex); - return false; + goto error_cond_destroy; } recorder->ctx = avformat_alloc_context(); if (!recorder->ctx) { LOGE("Could not allocate output context"); - sc_cond_destroy(&recorder->queue_cond); - sc_mutex_destroy(&recorder->mutex); - return false; + goto error_cond_destroy; } // contrary to the deprecated API (av_oformat_next()), av_muxer_iterate() @@ -266,10 +261,7 @@ recorder_open(struct recorder *recorder, const AVCodec *input_codec) { AVStream *ostream = avformat_new_stream(recorder->ctx, input_codec); if (!ostream) { - avformat_free_context(recorder->ctx); - sc_cond_destroy(&recorder->queue_cond); - sc_mutex_destroy(&recorder->mutex); - return false; + goto error_avformat_free_context; } ostream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO; @@ -283,10 +275,7 @@ recorder_open(struct recorder *recorder, const AVCodec *input_codec) { if (ret < 0) { LOGE("Failed to open output file: %s", recorder->filename); // ostream will be cleaned up during context cleaning - avformat_free_context(recorder->ctx); - sc_cond_destroy(&recorder->queue_cond); - sc_mutex_destroy(&recorder->mutex); - return false; + goto error_avformat_free_context; } LOGD("Starting recorder thread"); @@ -294,16 +283,23 @@ recorder_open(struct recorder *recorder, const AVCodec *input_codec) { recorder); if (!ok) { LOGC("Could not start recorder thread"); - avio_close(recorder->ctx->pb); - avformat_free_context(recorder->ctx); - sc_cond_destroy(&recorder->queue_cond); - sc_mutex_destroy(&recorder->mutex); - return false; + goto error_avio_close; } LOGI("Recording started to %s file: %s", format_name, recorder->filename); return true; + +error_avio_close: + avio_close(recorder->ctx->pb); +error_avformat_free_context: + avformat_free_context(recorder->ctx); +error_cond_destroy: + sc_cond_destroy(&recorder->queue_cond); +error_mutex_destroy: + sc_mutex_destroy(&recorder->mutex); + + return false; } static void From 8b90dc61b95679eadc82b2e4c193faa6621fe24e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 18 Apr 2021 17:13:58 +0200 Subject: [PATCH 0571/2244] Handle EAGAIN on send_packet in decoder EAGAIN was only handled on receive_frame. In practice, it should not be necessary, since one packet always contains one frame. But just in case. --- app/src/decoder.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/decoder.c b/app/src/decoder.c index 34f2a15f..476158d9 100644 --- a/app/src/decoder.c +++ b/app/src/decoder.c @@ -99,8 +99,8 @@ decoder_push(struct decoder *decoder, const AVPacket *packet) { return true; } - int ret; - if ((ret = avcodec_send_packet(decoder->codec_ctx, packet)) < 0) { + int ret = avcodec_send_packet(decoder->codec_ctx, packet); + if (ret < 0 && ret != AVERROR(EAGAIN)) { LOGE("Could not send video packet: %d", ret); return false; } From 243854a408a79896dbe7ff566d258082b13160f6 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 18 Apr 2021 17:23:09 +0200 Subject: [PATCH 0572/2244] Fix recorder comment --- app/src/recorder.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/recorder.c b/app/src/recorder.c index 479bdb39..e33d1a3b 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -22,7 +22,7 @@ find_muxer(const char *name) { #else oformat = av_oformat_next(oformat); #endif - // until null or with name "mp4" + // until null or with having the requested name } while (oformat && strcmp(oformat->name, name)); return oformat; } From ffc00210e9deb8364bffffe1313fc123b9621bb9 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 19 Apr 2021 09:22:53 +0200 Subject: [PATCH 0573/2244] Add strlist_contains() Add a function to know if a string list, using some separator, contains a specific string. --- app/src/util/str_util.c | 18 ++++++++++++++++++ app/src/util/str_util.h | 5 +++++ app/tests/test_strutil.c | 13 +++++++++++++ 3 files changed, 36 insertions(+) diff --git a/app/src/util/str_util.c b/app/src/util/str_util.c index 352d1d2f..287c08de 100644 --- a/app/src/util/str_util.c +++ b/app/src/util/str_util.c @@ -140,6 +140,24 @@ parse_integer_with_suffix(const char *s, long *out) { return true; } +bool +strlist_contains(const char *list, char sep, const char *s) { + char *p; + do { + p = strchr(list, sep); + + size_t token_len = p ? (size_t) (p - list) : strlen(list); + if (!strncmp(list, s, token_len)) { + return true; + } + + if (p) { + list = p + 1; + } + } while (p); + return false; +} + size_t utf8_truncation_index(const char *utf8, size_t max_len) { size_t len = strlen(utf8); diff --git a/app/src/util/str_util.h b/app/src/util/str_util.h index 25bec444..c016a625 100644 --- a/app/src/util/str_util.h +++ b/app/src/util/str_util.h @@ -43,6 +43,11 @@ parse_integers(const char *s, const char sep, size_t max_items, long *out); bool parse_integer_with_suffix(const char *s, long *out); +// search s in the list separated by sep +// for example, strlist_contains("a,bc,def", ',', "bc") returns true +bool +strlist_contains(const char *list, char sep, const char *s); + // return the index to truncate a UTF-8 string at a valid position size_t utf8_truncation_index(const char *utf8, size_t max_len); diff --git a/app/tests/test_strutil.c b/app/tests/test_strutil.c index ce0d5d30..dfd99658 100644 --- a/app/tests/test_strutil.c +++ b/app/tests/test_strutil.c @@ -287,6 +287,18 @@ static void test_parse_integer_with_suffix(void) { assert(!ok); } +static void test_strlist_contains(void) { + assert(strlist_contains("a,bc,def", ',', "bc")); + assert(!strlist_contains("a,bc,def", ',', "b")); + assert(strlist_contains("", ',', "")); + assert(strlist_contains("abc,", ',', "")); + assert(strlist_contains(",abc", ',', "")); + assert(strlist_contains("abc,,def", ',', "")); + assert(!strlist_contains("abc", ',', "")); + assert(strlist_contains(",,|x", '|', ",,")); + assert(strlist_contains("xyz", '\0', "xyz")); +} + int main(int argc, char *argv[]) { (void) argc; (void) argv; @@ -304,5 +316,6 @@ int main(int argc, char *argv[]) { test_parse_integer(); test_parse_integers(); test_parse_integer_with_suffix(); + test_strlist_contains(); return 0; } From 151bc166444ba1a8aaaaa651532f42e3a3741061 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 19 Apr 2021 09:28:28 +0200 Subject: [PATCH 0574/2244] Use strlist_contains() to find a muxer The AVOutputFormat name is a string list: it contains names separated by ',' (possibly only one). --- app/src/recorder.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/recorder.c b/app/src/recorder.c index e33d1a3b..91390ff1 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -4,6 +4,7 @@ #include #include "util/log.h" +#include "util/str_util.h" /** Downcast packet_sink to recorder */ #define DOWNCAST(SINK) container_of(SINK, struct recorder, packet_sink) @@ -22,8 +23,8 @@ find_muxer(const char *name) { #else oformat = av_oformat_next(oformat); #endif - // until null or with having the requested name - } while (oformat && strcmp(oformat->name, name)); + // until null or containing the requested name + } while (oformat && !strlist_contains(oformat->name, ',', name)); return oformat; } From fd0dc6c0cdc22a283e56d1852bbccc57138fb12d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 19 Apr 2021 18:42:20 +0200 Subject: [PATCH 0575/2244] Add --lock-video-orientation=initial Add a new mode to the --lock-video-orientation option, to lock the initial orientation of the device. This avoids to pass an explicit value (0, 1, 2 or 3) and think about which is the right one. --- README.md | 1 + app/scrcpy.1 | 4 +-- app/src/cli.c | 26 ++++++++++++++----- app/src/scrcpy.h | 14 ++++++++-- .../java/com/genymobile/scrcpy/Device.java | 3 +++ .../com/genymobile/scrcpy/ScreenInfo.java | 6 +++++ 6 files changed, 44 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index d9aeb142..037c5828 100644 --- a/README.md +++ b/README.md @@ -198,6 +198,7 @@ If `--max-size` is also specified, resizing is applied after cropping. To lock the orientation of the mirroring: ```bash +scrcpy --lock-video-orientation initial # initial (current) orientation scrcpy --lock-video-orientation 0 # natural orientation scrcpy --lock-video-orientation 1 # 90° counterclockwise scrcpy --lock-video-orientation 2 # 180° diff --git a/app/scrcpy.1 b/app/scrcpy.1 index ea9bce9b..89b7ee75 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -84,9 +84,9 @@ This is a workaround for some devices not behaving as expected when setting the .TP .BI "\-\-lock\-video\-orientation " value -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. +Lock video orientation to \fIvalue\fR. Possible values are "unlocked", "initial" (locked to the initial orientation), 0, 1, 2 and 3. Natural device orientation is 0, and each increment adds a 90 degrees otation counterclockwise. -Default is -1 (unlocked). +Default is "unlocked". .TP .BI "\-\-max\-fps " value diff --git a/app/src/cli.c b/app/src/cli.c index ec3c1294..05e178e0 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -81,10 +81,11 @@ scrcpy_print_usage(const char *arg0) { "\n" " --lock-video-orientation value\n" " Lock video orientation to value.\n" - " Possible values are -1 (unlocked), 0, 1, 2 and 3.\n" + " Possible values are \"unlocked\", \"initial\" (locked to the\n" + " initial orientation), 0, 1, 2 and 3.\n" " Natural device orientation is 0, and each increment adds a\n" " 90 degrees rotation counterclockwise.\n" - " Default is -1 (unlocked).\n" + " Default is \"unlocked\".\n" "\n" " --max-fps value\n" " Limit the frame rate of screen capture (officially supported\n" @@ -383,15 +384,27 @@ parse_max_fps(const char *s, uint16_t *max_fps) { } static bool -parse_lock_video_orientation(const char *s, int8_t *lock_video_orientation) { +parse_lock_video_orientation(const char *s, + enum sc_lock_video_orientation *lock_mode) { + if (!strcmp(s, "initial")) { + // Without argument, lock the initial orientation + *lock_mode = SC_LOCK_VIDEO_ORIENTATION_INITIAL; + return true; + } + + if (!strcmp(s, "unlocked")) { + *lock_mode = SC_LOCK_VIDEO_ORIENTATION_UNLOCKED; + return true; + } + long value; - bool ok = parse_integer_arg(s, &value, false, -1, 3, + bool ok = parse_integer_arg(s, &value, false, 0, 3, "lock video orientation"); if (!ok) { return false; } - *lock_video_orientation = (int8_t) value; + *lock_mode = (enum sc_lock_video_orientation) value; return true; } @@ -765,7 +778,8 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { } break; case OPT_LOCK_VIDEO_ORIENTATION: - if (!parse_lock_video_orientation(optarg, &opts->lock_video_orientation)) { + if (!parse_lock_video_orientation(optarg, + &opts->lock_video_orientation)) { return false; } break; diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index f91cb6b8..7c2689ab 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -20,6 +20,16 @@ enum sc_record_format { SC_RECORD_FORMAT_MKV, }; +enum sc_lock_video_orientation { + SC_LOCK_VIDEO_ORIENTATION_UNLOCKED = -1, + // lock the current orientation when scrcpy starts + SC_LOCK_VIDEO_ORIENTATION_INITIAL = -2, + SC_LOCK_VIDEO_ORIENTATION_0 = 0, + SC_LOCK_VIDEO_ORIENTATION_1, + SC_LOCK_VIDEO_ORIENTATION_2, + SC_LOCK_VIDEO_ORIENTATION_3, +}; + #define SC_MAX_SHORTCUT_MODS 8 enum sc_shortcut_mod { @@ -59,7 +69,7 @@ struct scrcpy_options { uint16_t max_size; uint32_t bit_rate; uint16_t max_fps; - int8_t lock_video_orientation; + enum sc_lock_video_orientation lock_video_orientation; uint8_t rotation; int16_t window_x; // SC_WINDOW_POSITION_UNDEFINED for "auto" int16_t window_y; // SC_WINDOW_POSITION_UNDEFINED for "auto" @@ -106,7 +116,7 @@ struct scrcpy_options { .max_size = 0, \ .bit_rate = DEFAULT_BIT_RATE, \ .max_fps = 0, \ - .lock_video_orientation = -1, \ + .lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_UNLOCKED, \ .rotation = 0, \ .window_x = SC_WINDOW_POSITION_UNDEFINED, \ .window_y = SC_WINDOW_POSITION_UNDEFINED, \ diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index a63976fd..2d67ab6f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -25,6 +25,9 @@ public final class Device { public static final int POWER_MODE_OFF = SurfaceControl.POWER_MODE_OFF; public static final int POWER_MODE_NORMAL = SurfaceControl.POWER_MODE_NORMAL; + public static final int LOCK_VIDEO_ORIENTATION_UNLOCKED = -1; + public static final int LOCK_VIDEO_ORIENTATION_INITIAL = -2; + private static final ServiceManager SERVICE_MANAGER = new ServiceManager(); public interface RotationListener { diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java b/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java index 10acfb50..c27322ef 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java @@ -82,6 +82,12 @@ public final class ScreenInfo { public static ScreenInfo computeScreenInfo(DisplayInfo displayInfo, Rect crop, int maxSize, int lockedVideoOrientation) { int rotation = displayInfo.getRotation(); + + if (lockedVideoOrientation == Device.LOCK_VIDEO_ORIENTATION_INITIAL) { + // The user requested to lock the video orientation to the current orientation + lockedVideoOrientation = rotation; + } + Size deviceSize = displayInfo.getSize(); Rect contentRect = new Rect(0, 0, deviceSize.getWidth(), deviceSize.getHeight()); if (crop != null) { From 5af9d0ee0faddfca689f468c7d463c86233e8d93 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 19 Apr 2021 18:40:48 +0200 Subject: [PATCH 0576/2244] Make --lock-video-orientation argument optional If the option is set without argument, lock the initial device orientation (as if the value "initial" was passed). --- README.md | 2 +- app/scrcpy.1 | 4 +++- app/src/cli.c | 8 +++++--- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 037c5828..f162fb3d 100644 --- a/README.md +++ b/README.md @@ -198,7 +198,7 @@ If `--max-size` is also specified, resizing is applied after cropping. To lock the orientation of the mirroring: ```bash -scrcpy --lock-video-orientation initial # initial (current) orientation +scrcpy --lock-video-orientation # initial (current) orientation scrcpy --lock-video-orientation 0 # natural orientation scrcpy --lock-video-orientation 1 # 90° counterclockwise scrcpy --lock-video-orientation 2 # 180° diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 89b7ee75..51c618a8 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -83,11 +83,13 @@ Inject computer clipboard text as a sequence of key events on Ctrl+v (like MOD+S This is a workaround for some devices not behaving as expected when setting the device clipboard programmatically. .TP -.BI "\-\-lock\-video\-orientation " value +.BI "\-\-lock\-video\-orientation " [value] Lock video orientation to \fIvalue\fR. Possible values are "unlocked", "initial" (locked to the initial orientation), 0, 1, 2 and 3. Natural device orientation is 0, and each increment adds a 90 degrees otation counterclockwise. Default is "unlocked". +Passing the option without argument is equivalent to passing "initial". + .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 05e178e0..6d5fd6b8 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -79,13 +79,15 @@ scrcpy_print_usage(const char *arg0) { " This is a workaround for some devices not behaving as\n" " expected when setting the device clipboard programmatically.\n" "\n" - " --lock-video-orientation value\n" + " --lock-video-orientation [value]\n" " Lock video orientation to value.\n" " Possible values are \"unlocked\", \"initial\" (locked to the\n" " initial orientation), 0, 1, 2 and 3.\n" " Natural device orientation is 0, and each increment adds a\n" " 90 degrees rotation counterclockwise.\n" " Default is \"unlocked\".\n" + " Passing the option without argument is equivalent to passing\n" + " \"initial\".\n" "\n" " --max-fps value\n" " Limit the frame rate of screen capture (officially supported\n" @@ -386,7 +388,7 @@ parse_max_fps(const char *s, uint16_t *max_fps) { static bool parse_lock_video_orientation(const char *s, enum sc_lock_video_orientation *lock_mode) { - if (!strcmp(s, "initial")) { + if (!s || !strcmp(s, "initial")) { // Without argument, lock the initial orientation *lock_mode = SC_LOCK_VIDEO_ORIENTATION_INITIAL; return true; @@ -693,7 +695,7 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { {"fullscreen", no_argument, NULL, 'f'}, {"help", no_argument, NULL, 'h'}, {"legacy-paste", no_argument, NULL, OPT_LEGACY_PASTE}, - {"lock-video-orientation", required_argument, NULL, + {"lock-video-orientation", optional_argument, NULL, OPT_LOCK_VIDEO_ORIENTATION}, {"max-fps", required_argument, NULL, OPT_MAX_FPS}, {"max-size", required_argument, NULL, 'm'}, From d39161f7539b9d335e38385ffff5c3b4c21c7122 Mon Sep 17 00:00:00 2001 From: Marco Martinelli <6640057+martinellimarco@users.noreply.github.com> Date: Sun, 4 Apr 2021 00:10:44 +0200 Subject: [PATCH 0577/2244] Add support for v4l2loopback It allows to send the video stream to /dev/videoN, so that it can be captured (like a webcam) by any v4l2-capable tool. PR #2232 PR #2233 PR #2268 Co-authored-by: Romain Vimont --- app/meson.build | 12 ++ app/scrcpy.1 | 6 + app/src/cli.c | 31 ++++ app/src/decoder.h | 2 +- app/src/main.c | 14 ++ app/src/scrcpy.c | 35 ++++- app/src/scrcpy.h | 2 + app/src/v4l2_sink.c | 341 ++++++++++++++++++++++++++++++++++++++++++++ app/src/v4l2_sink.h | 39 +++++ 9 files changed, 480 insertions(+), 2 deletions(-) create mode 100644 app/src/v4l2_sink.c create mode 100644 app/src/v4l2_sink.h diff --git a/app/meson.build b/app/meson.build index c2a59741..c1dbf489 100644 --- a/app/meson.build +++ b/app/meson.build @@ -33,6 +33,11 @@ else src += [ 'src/sys/unix/process.c' ] endif +v4l2_support = host_machine.system() == 'linux' +if v4l2_support + src += [ 'src/v4l2_sink.c' ] +endif + check_functions = [ 'strdup' ] @@ -49,6 +54,10 @@ if not get_option('crossbuild_windows') dependency('sdl2'), ] + if v4l2_support + dependencies += dependency('libavdevice') + endif + else # cross-compile mingw32 build (from Linux to Windows) @@ -124,6 +133,9 @@ 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') +# enable V4L2 support (linux only) +conf.set('HAVE_V4L2', v4l2_support) + configure_file(configuration: conf, output: 'config.h') src_dir = include_directories('src') diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 51c618a8..644d9bcc 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -185,6 +185,12 @@ Enable "show touches" on start, restore the initial value on exit. It only shows physical touches (not clicks from scrcpy). +.TP +.BI "\-\-v4l2_sink " /dev/videoN +Output to v4l2loopback device. + +It requires to lock the video orientation (see --lock-video-orientation). + .TP .BI "\-V, \-\-verbosity " value Set the log level ("debug", "info", "warn" or "error"). diff --git a/app/src/cli.c b/app/src/cli.c index 6d5fd6b8..16c56c6a 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -176,6 +176,13 @@ scrcpy_print_usage(const char *arg0) { " on exit.\n" " It only shows physical touches (not clicks from scrcpy).\n" "\n" +#ifdef HAVE_V4L2 + " --v4l2_sink /dev/videoN\n" + " Output to v4l2loopback device.\n" + " It requires to lock the video orientation (see\n" + " --lock-video-orientation).\n" + "\n" +#endif " -V, --verbosity value\n" " Set the log level (debug, info, warn or error).\n" #ifndef NDEBUG @@ -676,6 +683,7 @@ guess_record_format(const char *filename) { #define OPT_LEGACY_PASTE 1024 #define OPT_ENCODER_NAME 1025 #define OPT_POWER_OFF_ON_CLOSE 1026 +#define OPT_V4L2_SINK 1027 bool scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { @@ -717,6 +725,9 @@ 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'}, +#ifdef HAVE_V4L2 + {"v4l2_sink", required_argument, NULL, OPT_V4L2_SINK}, +#endif {"verbosity", required_argument, NULL, 'V'}, {"version", no_argument, NULL, 'v'}, {"window-title", required_argument, NULL, OPT_WINDOW_TITLE}, @@ -901,16 +912,36 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { case OPT_POWER_OFF_ON_CLOSE: opts->power_off_on_close = true; break; +#ifdef HAVE_V4L2 + case OPT_V4L2_SINK: + opts->v4l2_device = optarg; + break; +#endif default: // getopt prints the error message on stderr return false; } } +#ifdef HAVE_V4L2 + if (!opts->display && !opts->record_filename && !opts->v4l2_device) { + LOGE("-N/--no-display requires either screen recording (-r/--record)" + " or sink to v4l2loopback device (--v4l2_sink)"); + return false; + } + + if (opts->v4l2_device && opts->lock_video_orientation + == SC_LOCK_VIDEO_ORIENTATION_UNLOCKED) { + LOGI("Video orientation is locked for v4l2 sink. " + "See --lock-video-orientation."); + opts->lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_INITIAL; + } +#else if (!opts->display && !opts->record_filename) { LOGE("-N/--no-display requires screen recording (-r/--record)"); return false; } +#endif int index = optind; if (index < argc) { diff --git a/app/src/decoder.h b/app/src/decoder.h index bae97869..257f751a 100644 --- a/app/src/decoder.h +++ b/app/src/decoder.h @@ -8,7 +8,7 @@ #include #include -#define DECODER_MAX_SINKS 1 +#define DECODER_MAX_SINKS 2 struct decoder { struct sc_packet_sink packet_sink; // packet sink trait diff --git a/app/src/main.c b/app/src/main.c index e1e44f68..a468aed7 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -6,6 +6,9 @@ #include #include #include +#ifdef HAVE_V4L2 +# include +#endif #define SDL_MAIN_HANDLED // avoid link error on Linux Windows Subsystem #include @@ -28,6 +31,11 @@ print_version(void) { fprintf(stderr, " - libavutil %d.%d.%d\n", LIBAVUTIL_VERSION_MAJOR, LIBAVUTIL_VERSION_MINOR, LIBAVUTIL_VERSION_MICRO); +#ifdef HAVE_V4L2 + fprintf(stderr, " - libavdevice %d.%d.%d\n", LIBAVDEVICE_VERSION_MAJOR, + LIBAVDEVICE_VERSION_MINOR, + LIBAVDEVICE_VERSION_MICRO); +#endif } static SDL_LogPriority @@ -90,6 +98,12 @@ main(int argc, char *argv[]) { av_register_all(); #endif +#ifdef HAVE_V4L2 + if (args.opts.v4l2_device) { + avdevice_register_all(); + } +#endif + if (avformat_network_init()) { return 1; } diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 4de62389..e847037e 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -27,6 +27,9 @@ #include "tiny_xpm.h" #include "util/log.h" #include "util/net.h" +#ifdef HAVE_V4L2 +# include "v4l2_sink.h" +#endif static struct server server; static struct screen screen; @@ -34,6 +37,9 @@ static struct fps_counter fps_counter; static struct stream stream; static struct decoder decoder; static struct recorder recorder; +#ifdef HAVE_V4L2 +static struct sc_v4l2_sink v4l2_sink; +#endif static struct controller controller; static struct file_handler file_handler; @@ -247,6 +253,9 @@ scrcpy(const struct scrcpy_options *options) { bool fps_counter_initialized = false; bool file_handler_initialized = false; bool recorder_initialized = false; +#ifdef HAVE_V4L2 + bool v4l2_sink_initialized = false; +#endif bool stream_started = false; bool controller_initialized = false; bool controller_started = false; @@ -295,7 +304,6 @@ scrcpy(const struct scrcpy_options *options) { goto end; } - struct decoder *dec = NULL; if (options->display) { if (!fps_counter_init(&fps_counter)) { goto end; @@ -309,7 +317,14 @@ scrcpy(const struct scrcpy_options *options) { } file_handler_initialized = true; } + } + struct decoder *dec = NULL; + bool needs_decoder = options->display; +#ifdef HAVE_V4L2 + needs_decoder |= !!options->v4l2_device; +#endif + if (needs_decoder) { decoder_init(&decoder); dec = &decoder; } @@ -386,6 +401,18 @@ scrcpy(const struct scrcpy_options *options) { } } +#ifdef HAVE_V4L2 + if (options->v4l2_device) { + if (!sc_v4l2_sink_init(&v4l2_sink, options->v4l2_device, frame_size)) { + goto end; + } + + decoder_add_sink(&decoder, &v4l2_sink.frame_sink); + + v4l2_sink_initialized = true; + } +#endif + // now we consumed the header values, the socket receives the video stream // start the stream if (!stream_start(&stream)) { @@ -426,6 +453,12 @@ end: stream_join(&stream); } +#ifdef HAVE_V4L2 + if (v4l2_sink_initialized) { + sc_v4l2_sink_destroy(&v4l2_sink); + } +#endif + // Destroy the screen only after the stream is guaranteed to be finished, // because otherwise the screen could receive new frames after destruction if (screen_initialized) { diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index 7c2689ab..405dc7f3 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -62,6 +62,7 @@ struct scrcpy_options { const char *render_driver; const char *codec_options; const char *encoder_name; + const char *v4l2_device; enum sc_log_level log_level; enum sc_record_format record_format; struct sc_port_range port_range; @@ -103,6 +104,7 @@ struct scrcpy_options { .render_driver = NULL, \ .codec_options = NULL, \ .encoder_name = NULL, \ + .v4l2_device = NULL, \ .log_level = SC_LOG_LEVEL_INFO, \ .record_format = SC_RECORD_FORMAT_AUTO, \ .port_range = { \ diff --git a/app/src/v4l2_sink.c b/app/src/v4l2_sink.c new file mode 100644 index 00000000..b7d06bb6 --- /dev/null +++ b/app/src/v4l2_sink.c @@ -0,0 +1,341 @@ +#include "v4l2_sink.h" + +#include "util/log.h" +#include "util/str_util.h" + +/** Downcast frame_sink to sc_v4l2_sink */ +#define DOWNCAST(SINK) container_of(SINK, struct sc_v4l2_sink, frame_sink) + +static const AVRational SCRCPY_TIME_BASE = {1, 1000000}; // timestamps in us + +static const AVOutputFormat * +find_muxer(const char *name) { +#ifdef SCRCPY_LAVF_HAS_NEW_MUXER_ITERATOR_API + void *opaque = NULL; +#endif + const AVOutputFormat *oformat = NULL; + do { +#ifdef SCRCPY_LAVF_HAS_NEW_MUXER_ITERATOR_API + oformat = av_muxer_iterate(&opaque); +#else + oformat = av_oformat_next(oformat); +#endif + // until null or containing the requested name + } while (oformat && !strlist_contains(oformat->name, ',', name)); + return oformat; +} + +static bool +write_header(struct sc_v4l2_sink *vs, const AVPacket *packet) { + AVStream *ostream = vs->format_ctx->streams[0]; + + uint8_t *extradata = av_malloc(packet->size * sizeof(uint8_t)); + if (!extradata) { + LOGC("Could not allocate extradata"); + return false; + } + + // copy the first packet to the extra data + memcpy(extradata, packet->data, packet->size); + + ostream->codecpar->extradata = extradata; + ostream->codecpar->extradata_size = packet->size; + + int ret = avformat_write_header(vs->format_ctx, NULL); + if (ret < 0) { + LOGE("Failed to write header to %s", vs->device_name); + return false; + } + + return true; +} + +static void +rescale_packet(struct sc_v4l2_sink *vs, AVPacket *packet) { + AVStream *ostream = vs->format_ctx->streams[0]; + av_packet_rescale_ts(packet, SCRCPY_TIME_BASE, ostream->time_base); +} + +static bool +write_packet(struct sc_v4l2_sink *vs, AVPacket *packet) { + if (!vs->header_written) { + bool ok = write_header(vs, packet); + if (!ok) { + return false; + } + vs->header_written = true; + return true; + } + + rescale_packet(vs, packet); + + bool ok = av_write_frame(vs->format_ctx, packet) >= 0; + + // Failing to write the last frame is not very serious, no future frame may + // depend on it, so the resulting file will still be valid + (void) ok; + + return true; +} + +static bool +encode_and_write_frame(struct sc_v4l2_sink *vs, const AVFrame *frame) { + int ret = avcodec_send_frame(vs->encoder_ctx, frame); + if (ret < 0 && ret != AVERROR(EAGAIN)) { + LOGE("Could not send v4l2 video frame: %d", ret); + return false; + } + + AVPacket *packet = &vs->packet; + ret = avcodec_receive_packet(vs->encoder_ctx, packet); + if (ret == 0) { + // A packet was received + + bool ok = write_packet(vs, packet); + if (!ok) { + LOGW("Could not send packet to v4l2 sink"); + return false; + } + av_packet_unref(packet); + } else if (ret != AVERROR(EAGAIN)) { + LOGE("Could not receive v4l2 video packet: %d", ret); + return false; + } + + return true; +} + +static int +run_v4l2_sink(void *data) { + struct sc_v4l2_sink *vs = data; + + for (;;) { + sc_mutex_lock(&vs->mutex); + + while (!vs->stopped && vs->vb.pending_frame_consumed) { + sc_cond_wait(&vs->cond, &vs->mutex); + } + + if (vs->stopped) { + sc_mutex_unlock(&vs->mutex); + break; + } + + sc_mutex_unlock(&vs->mutex); + + video_buffer_consume(&vs->vb, vs->frame); + bool ok = encode_and_write_frame(vs, vs->frame); + if (!ok) { + LOGE("Could not send frame to v4l2 sink"); + break; + } + } + + LOGD("V4l2 thread ended"); + + return 0; +} + +static bool +sc_v4l2_sink_open(struct sc_v4l2_sink *vs) { + bool ok = video_buffer_init(&vs->vb); + if (!ok) { + return false; + } + + ok = sc_mutex_init(&vs->mutex); + if (!ok) { + LOGC("Could not create mutex"); + goto error_video_buffer_destroy; + } + + ok = sc_cond_init(&vs->cond); + if (!ok) { + LOGC("Could not create cond"); + goto error_mutex_destroy; + } + + // FIXME + const AVOutputFormat *format = find_muxer("video4linux2,v4l2"); + if (!format) { + LOGE("Could not find v4l2 muxer"); + goto error_cond_destroy; + } + + const AVCodec *encoder = avcodec_find_encoder(AV_CODEC_ID_RAWVIDEO); + if (!encoder) { + LOGE("Raw video encoder not found"); + return false; + } + + vs->format_ctx = avformat_alloc_context(); + if (!vs->format_ctx) { + LOGE("Could not allocate v4l2 output context"); + return false; + } + + // contrary to the deprecated API (av_oformat_next()), av_muxer_iterate() + // returns (on purpose) a pointer-to-const, but AVFormatContext.oformat + // still expects a pointer-to-non-const (it has not be updated accordingly) + // + vs->format_ctx->oformat = (AVOutputFormat *) format; + vs->format_ctx->url = strdup(vs->device_name); + if (!vs->format_ctx->url) { + LOGE("Could not strdup v4l2 device name"); + goto error_avformat_free_context; + return false; + } + + AVStream *ostream = avformat_new_stream(vs->format_ctx, encoder); + if (!ostream) { + LOGE("Could not allocate new v4l2 stream"); + goto error_avformat_free_context; + return false; + } + + ostream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO; + ostream->codecpar->codec_id = encoder->id; + ostream->codecpar->format = AV_PIX_FMT_YUV420P; + ostream->codecpar->width = vs->frame_size.width; + ostream->codecpar->height = vs->frame_size.height; + + int ret = avio_open(&vs->format_ctx->pb, vs->device_name, AVIO_FLAG_WRITE); + if (ret < 0) { + LOGE("Failed to open output device: %s", vs->device_name); + // ostream will be cleaned up during context cleaning + goto error_avformat_free_context; + } + + vs->encoder_ctx = avcodec_alloc_context3(encoder); + if (!vs->encoder_ctx) { + LOGC("Could not allocate codec context for v4l2"); + goto error_avio_close; + } + + vs->encoder_ctx->width = vs->frame_size.width; + vs->encoder_ctx->height = vs->frame_size.height; + vs->encoder_ctx->pix_fmt = AV_PIX_FMT_YUV420P; + vs->encoder_ctx->time_base.num = 1; + vs->encoder_ctx->time_base.den = 1; + + if (avcodec_open2(vs->encoder_ctx, encoder, NULL) < 0) { + LOGE("Could not open codec for v4l2"); + goto error_avcodec_free_context; + } + + vs->frame = av_frame_alloc(); + if (!vs->frame) { + LOGE("Could not create v4l2 frame"); + goto error_avcodec_close; + } + + LOGD("Starting v4l2 thread"); + ok = sc_thread_create(&vs->thread, run_v4l2_sink, "v4l2", vs); + if (!ok) { + LOGC("Could not start v4l2 thread"); + goto error_av_frame_free; + } + + vs->header_written = false; + vs->stopped = false; + + LOGI("v4l2 sink started to device: %s", vs->device_name); + + return true; + +error_av_frame_free: + av_frame_free(&vs->frame); +error_avcodec_close: + avcodec_close(vs->encoder_ctx); +error_avcodec_free_context: + avcodec_free_context(&vs->encoder_ctx); +error_avio_close: + avio_close(vs->format_ctx->pb); +error_avformat_free_context: + avformat_free_context(vs->format_ctx); +error_cond_destroy: + sc_cond_destroy(&vs->cond); +error_mutex_destroy: + sc_mutex_destroy(&vs->mutex); +error_video_buffer_destroy: + video_buffer_destroy(&vs->vb); + + return false; +} + +static void +sc_v4l2_sink_close(struct sc_v4l2_sink *vs) { + sc_mutex_lock(&vs->mutex); + vs->stopped = true; + sc_cond_signal(&vs->cond); + sc_mutex_unlock(&vs->mutex); + + sc_thread_join(&vs->thread, NULL); + + av_frame_free(&vs->frame); + avcodec_close(vs->encoder_ctx); + avcodec_free_context(&vs->encoder_ctx); + avio_close(vs->format_ctx->pb); + avformat_free_context(vs->format_ctx); + sc_cond_destroy(&vs->cond); + sc_mutex_destroy(&vs->mutex); + video_buffer_destroy(&vs->vb); +} + +static bool +sc_v4l2_sink_push(struct sc_v4l2_sink *vs, const AVFrame *frame) { + bool ok = video_buffer_push(&vs->vb, frame, NULL); + if (!ok) { + return false; + } + + // signal possible change of vs->vb.pending_frame_consumed + sc_cond_signal(&vs->cond); + + return true; +} + +static bool +sc_v4l2_frame_sink_open(struct sc_frame_sink *sink) { + struct sc_v4l2_sink *vs = DOWNCAST(sink); + return sc_v4l2_sink_open(vs); +} + +static void +sc_v4l2_frame_sink_close(struct sc_frame_sink *sink) { + struct sc_v4l2_sink *vs = DOWNCAST(sink); + sc_v4l2_sink_close(vs); +} + +static bool +sc_v4l2_frame_sink_push(struct sc_frame_sink *sink, const AVFrame *frame) { + struct sc_v4l2_sink *vs = DOWNCAST(sink); + return sc_v4l2_sink_push(vs, frame); +} + +bool +sc_v4l2_sink_init(struct sc_v4l2_sink *vs, const char *device_name, + struct size frame_size) { + vs->device_name = strdup(device_name); + if (!vs->device_name) { + LOGE("Could not strdup v4l2 device name"); + return false; + } + + vs->frame_size = frame_size; + + static const struct sc_frame_sink_ops ops = { + .open = sc_v4l2_frame_sink_open, + .close = sc_v4l2_frame_sink_close, + .push = sc_v4l2_frame_sink_push, + }; + + vs->frame_sink.ops = &ops; + + return true; +} + +void +sc_v4l2_sink_destroy(struct sc_v4l2_sink *vs) { + free(vs->device_name); +} diff --git a/app/src/v4l2_sink.h b/app/src/v4l2_sink.h new file mode 100644 index 00000000..9d2ee149 --- /dev/null +++ b/app/src/v4l2_sink.h @@ -0,0 +1,39 @@ +#ifndef SC_V4L2_SINK_H +#define SC_V4L2_SINK_H + +#include "common.h" + +#include "coords.h" +#include "trait/frame_sink.h" +#include "video_buffer.h" + +#include + +struct sc_v4l2_sink { + struct sc_frame_sink frame_sink; // frame sink trait + + struct video_buffer vb; + AVFormatContext *format_ctx; + AVCodecContext *encoder_ctx; + + char *device_name; + struct size frame_size; + + sc_thread thread; + sc_mutex mutex; + sc_cond cond; + bool stopped; + bool header_written; + + AVFrame *frame; + AVPacket packet; +}; + +bool +sc_v4l2_sink_init(struct sc_v4l2_sink *vs, const char *device_name, + struct size frame_size); + +void +sc_v4l2_sink_destroy(struct sc_v4l2_sink *vs); + +#endif From 41a0383d7ce42de79158ecd2f6eca3fd42602760 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 19 Apr 2021 20:12:08 +0200 Subject: [PATCH 0578/2244] Document v4l2 sink in README --- README.md | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f162fb3d..71eec7e2 100644 --- a/README.md +++ b/README.md @@ -226,7 +226,9 @@ error will give the available encoders: scrcpy --encoder _ ``` -### Recording +### Capture + +#### Recording It is possible to record the screen while mirroring: @@ -250,6 +252,58 @@ variation] does not impact the recorded file. [packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation +#### v4l2loopback + +On Linux, it is possible to send the video stream to a v4l2 loopback device, so +that the Android device can be opened like a webcam by any v4l2-capable tool. + +The module `v4l2loopback` must be installed: + +```bash +sudo apt install v4l2loopback-dkms +``` + +To create a v4l2 device: + +```bash +sudo modprobe v4l2loopback +``` + +This will create a new video device in `/dev/videoN`, where `N` is an integer +(more [options](https://github.com/umlaeute/v4l2loopback#options) are available +to create several devices or devices with specific IDs). + +To list the enabled devices: + +```bash +# requires v4l-utils package +v4l2-ctl --list-devices + +# simple but might be sufficient +ls /dev/video* +``` + +To start scrcpy using a v4l2 sink: + +```bash +scrcpy --v4l2-sink=/dev/videoN +scrcpy --v4l2-sink=/dev/videoN -N # --no-display to disable mirroring window +``` + +(replace `N` by the device ID, check with `ls /dev/video*`) + +Once enabled, you can open your video stream with a v4l2-capable tool: + +```bash +ffplay -i /dev/videoN +vlc v4l2:///dev/videoN # VLC might add some buffering delay +``` + +For example, you could capture the video within [OBS]. + +[OBS]: https://obsproject.com/fr + + ### Connection #### Wireless From 45e72801484deae6c931a4a79595b606227e547a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 26 Apr 2021 17:59:35 +0200 Subject: [PATCH 0579/2244] Rename --v4l2_sink to --v4l2-sink This was a rebase issue, the previous version in #2268 was correct. Refs #2268 --- app/scrcpy.1 | 2 +- app/src/cli.c | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 644d9bcc..7ef69640 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -186,7 +186,7 @@ Enable "show touches" on start, restore the initial value on exit. It only shows physical touches (not clicks from scrcpy). .TP -.BI "\-\-v4l2_sink " /dev/videoN +.BI "\-\-v4l2-sink " /dev/videoN Output to v4l2loopback device. It requires to lock the video orientation (see --lock-video-orientation). diff --git a/app/src/cli.c b/app/src/cli.c index 16c56c6a..841b7c39 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -177,7 +177,7 @@ scrcpy_print_usage(const char *arg0) { " It only shows physical touches (not clicks from scrcpy).\n" "\n" #ifdef HAVE_V4L2 - " --v4l2_sink /dev/videoN\n" + " --v4l2-sink /dev/videoN\n" " Output to v4l2loopback device.\n" " It requires to lock the video orientation (see\n" " --lock-video-orientation).\n" @@ -726,7 +726,7 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { {"stay-awake", no_argument, NULL, 'w'}, {"turn-screen-off", no_argument, NULL, 'S'}, #ifdef HAVE_V4L2 - {"v4l2_sink", required_argument, NULL, OPT_V4L2_SINK}, + {"v4l2-sink", required_argument, NULL, OPT_V4L2_SINK}, #endif {"verbosity", required_argument, NULL, 'V'}, {"version", no_argument, NULL, 'v'}, @@ -926,7 +926,7 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { #ifdef HAVE_V4L2 if (!opts->display && !opts->record_filename && !opts->v4l2_device) { LOGE("-N/--no-display requires either screen recording (-r/--record)" - " or sink to v4l2loopback device (--v4l2_sink)"); + " or sink to v4l2loopback device (--v4l2-sink)"); return false; } From 1cde68a1fa01a9b37eeede3310edf163d4e79970 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 26 Apr 2021 18:00:47 +0200 Subject: [PATCH 0580/2244] Fix v4l2 AVFrame memory leak Unref frame immediately once encoded. Fixes #2279 --- app/src/v4l2_sink.c | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/v4l2_sink.c b/app/src/v4l2_sink.c index b7d06bb6..9b4a9c85 100644 --- a/app/src/v4l2_sink.c +++ b/app/src/v4l2_sink.c @@ -125,6 +125,7 @@ run_v4l2_sink(void *data) { video_buffer_consume(&vs->vb, vs->frame); bool ok = encode_and_write_frame(vs, vs->frame); + av_frame_unref(vs->frame); if (!ok) { LOGE("Could not send frame to v4l2 sink"); break; From 84f17fdeab518cbe9f9c1a5fdf7dc50981c01806 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 26 Apr 2021 18:05:11 +0200 Subject: [PATCH 0581/2244] Fix v4l2 AVPacket memory leak on error Unref v4l2 AVPacket even if writing failed. --- app/src/v4l2_sink.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/v4l2_sink.c b/app/src/v4l2_sink.c index 9b4a9c85..fd0bda12 100644 --- a/app/src/v4l2_sink.c +++ b/app/src/v4l2_sink.c @@ -92,11 +92,11 @@ encode_and_write_frame(struct sc_v4l2_sink *vs, const AVFrame *frame) { // A packet was received bool ok = write_packet(vs, packet); + av_packet_unref(packet); if (!ok) { LOGW("Could not send packet to v4l2 sink"); return false; } - av_packet_unref(packet); } else if (ret != AVERROR(EAGAIN)) { LOGE("Could not receive v4l2 video packet: %d", ret); return false; From ae6ec7a194257ddc72b8fa55f8a39486fb01b4c2 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 26 Apr 2021 18:05:43 +0200 Subject: [PATCH 0582/2244] Unref decoder AVFrame immediately The frame can be unref immediately after it is pushed to the frame sinks. It was not really a memory leak because the frame was unref every time by avcodec_receive_frame() (and freed on close), but a reference was unnecessarily kept for too long. --- app/src/decoder.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/decoder.c b/app/src/decoder.c index 476158d9..dbaa3d39 100644 --- a/app/src/decoder.c +++ b/app/src/decoder.c @@ -111,6 +111,8 @@ decoder_push(struct decoder *decoder, const AVPacket *packet) { // A frame lost should not make the whole pipeline fail. The error, if // any, is already logged. (void) ok; + + av_frame_unref(decoder->frame); } else if (ret != AVERROR(EAGAIN)) { LOGE("Could not receive video frame: %d", ret); return false; From d00ee640c0704412322aa4652cdccf5f3f3f9250 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 30 Apr 2021 22:57:42 +0200 Subject: [PATCH 0583/2244] Simplify Device.java Remove useless intermediate method with a "mode" parameter (it's always called with the same mode). This also avoids the need for a specific injectEventOnDisplay() method, since it does not conflict with another injectEvent() method anymore. --- .../java/com/genymobile/scrcpy/Device.java | 22 +++++-------------- 1 file changed, 5 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 2d67ab6f..7d1d3aa8 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -164,39 +164,27 @@ public final class Device { return supportsInputEvents; } - public static boolean injectEvent(InputEvent inputEvent, int mode, int displayId) { + public static boolean injectEvent(InputEvent inputEvent, int displayId) { if (!supportsInputEvents(displayId)) { - return false; + throw new AssertionError("Could not inject input event if !supportsInputEvents()"); } if (displayId != 0 && !InputManager.setDisplayId(inputEvent, displayId)) { return false; } - return SERVICE_MANAGER.getInputManager().injectInputEvent(inputEvent, mode); - } - - public boolean injectEvent(InputEvent inputEvent, int mode) { - if (!supportsInputEvents()) { - throw new AssertionError("Could not inject input event if !supportsInputEvents()"); - } - - return injectEvent(inputEvent, mode, displayId); - } - - public static boolean injectEventOnDisplay(InputEvent event, int displayId) { - return injectEvent(event, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC, displayId); + return SERVICE_MANAGER.getInputManager().injectInputEvent(inputEvent, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC); } public boolean injectEvent(InputEvent event) { - return injectEvent(event, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC); + return injectEvent(event, displayId); } public static boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState, int displayId) { long now = SystemClock.uptimeMillis(); KeyEvent event = new KeyEvent(now, now, action, keyCode, repeat, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0, InputDevice.SOURCE_KEYBOARD); - return injectEventOnDisplay(event, displayId); + return injectEvent(event, displayId); } public boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState) { From 9a7d351d67396363f0d78473842d321f6c764877 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 30 Apr 2021 23:01:58 +0200 Subject: [PATCH 0584/2244] Simplify non-static injectEvent() implementation Just call the static version (having a displayId) from the non-static version (using the displayId field). --- server/src/main/java/com/genymobile/scrcpy/Device.java | 7 ++----- 1 file changed, 2 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 7d1d3aa8..4374ffc5 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -188,10 +188,7 @@ public final class Device { } 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); + return injectKeyEvent(action, keyCode, repeat, metaState, displayId); } public static boolean injectKeycode(int keyCode, int displayId) { @@ -199,7 +196,7 @@ public final class Device { } public boolean injectKeycode(int keyCode) { - return injectKeyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0) && injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0); + return injectKeycode(keyCode, displayId); } public static boolean isScreenOn() { From 233f8e6cc4e84baaf3761ea61a60ba802aafeb88 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 30 Apr 2021 23:05:15 +0200 Subject: [PATCH 0585/2244] Rename keycode injection method Make it explicit that it injects both "press" and "release" events. --- .../src/main/java/com/genymobile/scrcpy/Controller.java | 6 +++--- server/src/main/java/com/genymobile/scrcpy/Device.java | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index 3760bbd4..92986241 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -55,7 +55,7 @@ public class Controller { public void control() throws IOException { // on start, power on the device if (!Device.isScreenOn()) { - device.injectKeycode(KeyEvent.KEYCODE_POWER); + device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER); // dirty hack // After POWER is injected, the device is powered on asynchronously. @@ -273,7 +273,7 @@ public class Controller { if (keepPowerModeOff) { schedulePowerModeOff(); } - return device.injectKeycode(KeyEvent.KEYCODE_POWER); + return device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER); } private boolean setClipboard(String text, boolean paste) { @@ -284,7 +284,7 @@ public class Controller { // 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); + device.pressReleaseKeycode(KeyEvent.KEYCODE_PASTE); } return ok; diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index 4374ffc5..3e71fe9c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -191,12 +191,12 @@ public final class Device { return injectKeyEvent(action, keyCode, repeat, metaState, displayId); } - public static boolean injectKeycode(int keyCode, int displayId) { + public static boolean pressReleaseKeycode(int keyCode, int displayId) { return injectKeyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0, displayId) && injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0, displayId); } - public boolean injectKeycode(int keyCode) { - return injectKeycode(keyCode, displayId); + public boolean pressReleaseKeycode(int keyCode) { + return pressReleaseKeycode(keyCode, displayId); } public static boolean isScreenOn() { @@ -272,7 +272,7 @@ public final class Device { if (!isScreenOn()) { return true; } - return injectKeycode(KeyEvent.KEYCODE_POWER, displayId); + return pressReleaseKeycode(KeyEvent.KEYCODE_POWER, displayId); } /** From 1b9dcce23c8f52aa1ba97fe30fad83930c60578d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 3 May 2021 20:39:49 +0200 Subject: [PATCH 0586/2244] Fix double-free on error On error, server->serial was freed twice: immediately and in server_destroy(). Refs #2292 --- app/src/server.c | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/app/src/server.c b/app/src/server.c index a4bba33c..276e0bd6 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -417,18 +417,19 @@ server_start(struct server *server, const char *serial, } if (!push_server(serial)) { - goto error1; + /* server->serial will be freed on server_destroy() */ + return false; } if (!enable_tunnel_any_port(server, params->port_range, params->force_adb_forward)) { - goto error1; + return false; } // server will connect to our server socket server->process = execute_server(server, params); if (server->process == PROCESS_NONE) { - goto error2; + goto error; } // If the server process dies before connecting to the server socket, then @@ -442,14 +443,14 @@ server_start(struct server *server, const char *serial, if (!ok) { process_terminate(server->process); process_wait(server->process, true); // ignore exit code - goto error2; + goto error; } server->tunnel_enabled = true; return true; -error2: +error: if (!server->tunnel_forward) { bool was_closed = atomic_flag_test_and_set(&server->server_socket_closed); @@ -459,8 +460,7 @@ error2: close_socket(server->server_socket); } disable_tunnel(server); -error1: - free(server->serial); + return false; } From 644a5ef14ae9559ca72365c37d8f6b63edbdbd6f Mon Sep 17 00:00:00 2001 From: Haren S Date: Thu, 6 May 2021 17:56:41 +0100 Subject: [PATCH 0587/2244] Add MacPorts documentation PR #2299 Signed-off-by: Romain Vimont --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index 7a4b7d41..21f789d9 100644 --- a/README.md +++ b/README.md @@ -130,6 +130,15 @@ You need `adb`, accessible from your `PATH`. If you don't have it yet: brew install android-platform-tools ``` +It's also available in [MacPorts], which sets up adb for you: + +```bash +sudo port install scrcpy +``` + +[MacPorts]: https://www.macports.org/ + + You can also [build the app manually][BUILD]. From 8fb5715740e52bcb2ba8d4bac1e53f4ef76ef688 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 9 May 2021 10:49:46 +0200 Subject: [PATCH 0588/2244] Add libavdevice-dev in BUILD instructions On Linux, scrcpy now depends on libavdevice for v4l2. --- BUILD.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/BUILD.md b/BUILD.md index 4dc6d64c..d5aead0f 100644 --- a/BUILD.md +++ b/BUILD.md @@ -13,8 +13,8 @@ First, you need to install the required packages: ```bash # for Debian/Ubuntu sudo apt install ffmpeg libsdl2-2.0-0 adb wget \ - gcc git pkg-config meson ninja-build \ - libavcodec-dev libavformat-dev libavutil-dev libsdl2-dev + gcc git pkg-config meson ninja-build libsdl2-dev \ + libavcodec-dev libavdevice-dev libavformat-dev libavutil-dev ``` Then clone the repo and execute the installation script @@ -91,8 +91,8 @@ Install the required packages from your package manager. sudo apt install ffmpeg libsdl2-2.0-0 adb # client build dependencies -sudo apt install gcc git pkg-config meson ninja-build \ - libavcodec-dev libavformat-dev libavutil-dev libsdl2-dev +sudo apt install gcc git pkg-config meson ninja-build libsdl2-dev \ + libavcodec-dev libavdevice-dev libavformat-dev libavutil-dev # server build dependencies sudo apt install openjdk-11-jdk From 1e64f0f931c2484ea6955205b83703776c03992b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 9 May 2021 11:06:02 +0200 Subject: [PATCH 0589/2244] Use ARRAY_LEN() macro --- 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 276e0bd6..4448ab95 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -306,7 +306,7 @@ execute_server(struct server *server, const struct server_params *params) { // Port: 5005 // Then click on "Debug" #endif - return adb_execute(server->serial, cmd, sizeof(cmd) / sizeof(cmd[0])); + return adb_execute(server->serial, cmd, ARRAY_LEN(cmd)); } static socket_t From 42b3d1e66e91aa4c07e8c9ab5e8bb71ba309abbe Mon Sep 17 00:00:00 2001 From: LYK Date: Fri, 14 May 2021 12:07:48 +0900 Subject: [PATCH 0590/2244] Reformat README.ko.md PR #2311 Signed-off-by: Romain Vimont --- README.ko.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.ko.md b/README.ko.md index 2da50562..31e38c6f 100644 --- a/README.ko.md +++ b/README.ko.md @@ -112,7 +112,7 @@ scrcpy --help ### 캡쳐 환경 설정 -###사이즈 재정의 +### 사이즈 재정의 가끔씩 성능을 향상시키기위해 안드로이드 디바이스를 낮은 해상도에서 미러링하는 것이 유용할 때도 있습니다. @@ -136,7 +136,7 @@ scrcpy --bit-rate 2M scrcpy -b 2M # 축약 버전 ``` -###프레임 비율 제한 +### 프레임 비율 제한 안드로이드 버전 10이상의 디바이스에서는, 다음의 명령어로 캡쳐 화면의 프레임 비율을 제한할 수 있습니다: From 72081a241b85edc64569ece05d2178018e4913ca Mon Sep 17 00:00:00 2001 From: Alberto Pasqualetto Date: Fri, 14 May 2021 14:57:38 +0200 Subject: [PATCH 0591/2244] Fix visualization of comment in code block PR #2315 Signed-off-by: Romain Vimont --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 21f789d9..e0d40984 100644 --- a/README.md +++ b/README.md @@ -460,7 +460,7 @@ scrcpy --display 1 The list of display ids can be retrieved by: -``` +```bash adb shell dumpsys display # search "mDisplayId=" in the output ``` From 83116fc19984bbed9da0200e341d140626fee3fd Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 16 May 2021 15:32:31 +0200 Subject: [PATCH 0592/2244] Notify end-of-stream via callback Do not send a SDL event directly, to make stream independent of SDL. --- app/src/scrcpy.c | 15 ++++++++++++++- app/src/stream.c | 19 +++++++++---------- app/src/stream.h | 11 +++++++++-- 3 files changed, 32 insertions(+), 13 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index e847037e..1cb66bc1 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -241,6 +241,16 @@ av_log_callback(void *avcl, int level, const char *fmt, va_list vl) { free(local_fmt); } +static void +stream_on_eos(struct stream *stream, void *userdata) { + (void) stream; + (void) userdata; + + SDL_Event stop_event; + stop_event.type = EVENT_STREAM_STOPPED; + SDL_PushEvent(&stop_event); +} + bool scrcpy(const struct scrcpy_options *options) { if (!server_init(&server)) { @@ -343,7 +353,10 @@ scrcpy(const struct scrcpy_options *options) { av_log_set_callback(av_log_callback); - stream_init(&stream, server.video_socket); + const struct stream_callbacks stream_cbs = { + .on_eos = stream_on_eos, + }; + stream_init(&stream, server.video_socket, &stream_cbs, NULL); if (dec) { stream_add_sink(&stream, &dec->packet_sink); diff --git a/app/src/stream.c b/app/src/stream.c index a11218e3..2d9a0ab4 100644 --- a/app/src/stream.c +++ b/app/src/stream.c @@ -3,7 +3,6 @@ #include #include #include -#include #include #include "decoder.h" @@ -58,13 +57,6 @@ stream_recv_packet(struct stream *stream, AVPacket *packet) { return true; } -static void -notify_stopped(void) { - SDL_Event stop_event; - stop_event.type = EVENT_STREAM_STOPPED; - SDL_PushEvent(&stop_event); -} - static bool push_packet_to_sinks(struct stream *stream, const AVPacket *packet) { for (unsigned i = 0; i < stream->sink_count; ++i) { @@ -251,15 +243,22 @@ finally_close_sinks: finally_free_codec_ctx: avcodec_free_context(&stream->codec_ctx); end: - notify_stopped(); + stream->cbs->on_eos(stream, stream->cbs_userdata); + return 0; } void -stream_init(struct stream *stream, socket_t socket) { +stream_init(struct stream *stream, socket_t socket, + const struct stream_callbacks *cbs, void *cbs_userdata) { stream->socket = socket; stream->has_pending = false; stream->sink_count = 0; + + assert(cbs && cbs->on_eos); + + stream->cbs = cbs; + stream->cbs_userdata = cbs_userdata; } void diff --git a/app/src/stream.h b/app/src/stream.h index 81175420..9fc4d1e1 100644 --- a/app/src/stream.h +++ b/app/src/stream.h @@ -6,7 +6,6 @@ #include #include #include -#include #include "trait/packet_sink.h" #include "util/net.h" @@ -27,10 +26,18 @@ struct stream { // packet is available bool has_pending; AVPacket pending; + + const struct stream_callbacks *cbs; + void *cbs_userdata; +}; + +struct stream_callbacks { + void (*on_eos)(struct stream *stream, void *userdata); }; void -stream_init(struct stream *stream, socket_t socket); +stream_init(struct stream *stream, socket_t socket, + const struct stream_callbacks *cbs, void *cbs_userdata); void stream_add_sink(struct stream *stream, struct sc_packet_sink *sink); From f19c455110fb7d11864692208458591ab06f622d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 16 May 2021 15:54:02 +0200 Subject: [PATCH 0593/2244] Fix leak on error Destroy video buffer if screen window creation failed. --- app/src/screen.c | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/screen.c b/app/src/screen.c index 0598ccf4..55a832ef 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -344,6 +344,7 @@ screen_init(struct screen *screen, struct fps_counter *fps_counter, window_flags); if (!screen->window) { LOGC("Could not create window: %s", SDL_GetError()); + video_buffer_destroy(&screen->vb); return false; } From e604e8a752f1a041bea2c782223e2b2d6769bfa7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 16 May 2021 18:26:20 +0200 Subject: [PATCH 0594/2244] Move fps_counter to screen The FPS counter specifically count frames from the screen video buffer, so it is specific to the screen. --- app/src/fps_counter.c | 4 ++++ app/src/input_manager.c | 2 +- app/src/input_manager.h | 1 - app/src/scrcpy.c | 31 ++++++++----------------------- app/src/screen.c | 30 ++++++++++++++++++++++++------ app/src/screen.h | 14 +++++++++++--- 6 files changed, 48 insertions(+), 34 deletions(-) diff --git a/app/src/fps_counter.c b/app/src/fps_counter.c index 281c58cf..bbf71887 100644 --- a/app/src/fps_counter.c +++ b/app/src/fps_counter.c @@ -150,6 +150,10 @@ fps_counter_interrupt(struct fps_counter *counter) { void fps_counter_join(struct fps_counter *counter) { if (counter->thread_started) { + // interrupted must be set by the thread calling join(), so no need to + // lock for the assertion + assert(counter->interrupted); + sc_thread_join(&counter->thread, NULL); } } diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 94086616..55b5fae2 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -517,7 +517,7 @@ input_manager_process_key(struct input_manager *im, return; case SDLK_i: if (!shift && !repeat && down) { - switch_fps_counter_state(im->fps_counter); + switch_fps_counter_state(&im->screen->fps_counter); } return; case SDLK_n: diff --git a/app/src/input_manager.h b/app/src/input_manager.h index 5c7e2b91..76690757 100644 --- a/app/src/input_manager.h +++ b/app/src/input_manager.h @@ -14,7 +14,6 @@ struct input_manager { struct controller *controller; - struct fps_counter *fps_counter; struct screen *screen; // SDL reports repeated events as a boolean, but Android expects the actual diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 1cb66bc1..e7a349d8 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -18,7 +18,6 @@ #include "device.h" #include "events.h" #include "file_handler.h" -#include "fps_counter.h" #include "input_manager.h" #include "recorder.h" #include "screen.h" @@ -33,7 +32,6 @@ static struct server server; static struct screen screen; -static struct fps_counter fps_counter; static struct stream stream; static struct decoder decoder; static struct recorder recorder; @@ -45,7 +43,6 @@ static struct file_handler file_handler; static struct input_manager input_manager = { .controller = &controller, - .fps_counter = &fps_counter, .screen = &screen, .repeat = 0, @@ -260,7 +257,6 @@ scrcpy(const struct scrcpy_options *options) { bool ret = false; bool server_started = false; - bool fps_counter_initialized = false; bool file_handler_initialized = false; bool recorder_initialized = false; #ifdef HAVE_V4L2 @@ -314,19 +310,12 @@ scrcpy(const struct scrcpy_options *options) { goto end; } - if (options->display) { - if (!fps_counter_init(&fps_counter)) { + if (options->display && options->control) { + if (!file_handler_init(&file_handler, server.serial, + options->push_target)) { goto end; } - fps_counter_initialized = true; - - if (options->control) { - if (!file_handler_init(&file_handler, server.serial, - options->push_target)) { - goto end; - } - file_handler_initialized = true; - } + file_handler_initialized = true; } struct decoder *dec = NULL; @@ -396,7 +385,7 @@ scrcpy(const struct scrcpy_options *options) { .fullscreen = options->fullscreen, }; - if (!screen_init(&screen, &fps_counter, &screen_params)) { + if (!screen_init(&screen, &screen_params)) { goto end; } screen_initialized = true; @@ -451,8 +440,8 @@ end: if (file_handler_initialized) { file_handler_stop(&file_handler); } - if (fps_counter_initialized) { - fps_counter_interrupt(&fps_counter); + if (screen_initialized) { + screen_interrupt(&screen); } if (server_started) { @@ -475,6 +464,7 @@ end: // Destroy the screen only after the stream is guaranteed to be finished, // because otherwise the screen could receive new frames after destruction if (screen_initialized) { + screen_join(&screen); screen_destroy(&screen); } @@ -494,11 +484,6 @@ end: file_handler_destroy(&file_handler); } - if (fps_counter_initialized) { - fps_counter_join(&fps_counter); - fps_counter_destroy(&fps_counter); - } - server_destroy(&server); return ret; diff --git a/app/src/screen.c b/app/src/screen.c index 55a832ef..12ad7d0a 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -277,7 +277,7 @@ screen_frame_sink_push(struct sc_frame_sink *sink, const AVFrame *frame) { } if (previous_frame_skipped) { - fps_counter_add_skipped_frame(screen->fps_counter); + fps_counter_add_skipped_frame(&screen->fps_counter); // The EVENT_NEW_FRAME triggered for the previous frame will consume // this new frame instead } else { @@ -293,10 +293,7 @@ screen_frame_sink_push(struct sc_frame_sink *sink, const AVFrame *frame) { } bool -screen_init(struct screen *screen, struct fps_counter *fps_counter, - const struct screen_params *params) { - screen->fps_counter = fps_counter; - +screen_init(struct screen *screen, const struct screen_params *params) { screen->resize_pending = false; screen->has_frame = false; screen->fullscreen = false; @@ -308,6 +305,12 @@ screen_init(struct screen *screen, struct fps_counter *fps_counter, return false; } + if (!fps_counter_init(&screen->fps_counter)) { + LOGE("Could not initialize FPS counter"); + video_buffer_destroy(&screen->vb); + return false; + } + screen->frame_size = params->frame_size; screen->rotation = params->rotation; if (screen->rotation) { @@ -344,6 +347,7 @@ screen_init(struct screen *screen, struct fps_counter *fps_counter, window_flags); if (!screen->window) { LOGC("Could not create window: %s", SDL_GetError()); + fps_counter_destroy(&screen->fps_counter); video_buffer_destroy(&screen->vb); return false; } @@ -353,6 +357,7 @@ screen_init(struct screen *screen, struct fps_counter *fps_counter, if (!screen->renderer) { LOGC("Could not create renderer: %s", SDL_GetError()); SDL_DestroyWindow(screen->window); + fps_counter_destroy(&screen->fps_counter); video_buffer_destroy(&screen->vb); return false; } @@ -405,6 +410,7 @@ screen_init(struct screen *screen, struct fps_counter *fps_counter, LOGC("Could not create texture: %s", SDL_GetError()); SDL_DestroyRenderer(screen->renderer); SDL_DestroyWindow(screen->window); + fps_counter_destroy(&screen->fps_counter); video_buffer_destroy(&screen->vb); return false; } @@ -415,6 +421,7 @@ screen_init(struct screen *screen, struct fps_counter *fps_counter, SDL_DestroyTexture(screen->texture); SDL_DestroyRenderer(screen->renderer); SDL_DestroyWindow(screen->window); + fps_counter_destroy(&screen->fps_counter); video_buffer_destroy(&screen->vb); return false; } @@ -459,6 +466,16 @@ screen_hide_window(struct screen *screen) { SDL_HideWindow(screen->window); } +void +screen_interrupt(struct screen *screen) { + fps_counter_interrupt(&screen->fps_counter); +} + +void +screen_join(struct screen *screen) { + fps_counter_join(&screen->fps_counter); +} + void screen_destroy(struct screen *screen) { #ifndef NDEBUG @@ -468,6 +485,7 @@ screen_destroy(struct screen *screen) { SDL_DestroyTexture(screen->texture); SDL_DestroyRenderer(screen->renderer); SDL_DestroyWindow(screen->window); + fps_counter_destroy(&screen->fps_counter); video_buffer_destroy(&screen->vb); } @@ -577,7 +595,7 @@ screen_update_frame(struct screen *screen) { video_buffer_consume(&screen->vb, screen->frame); AVFrame *frame = screen->frame; - fps_counter_add_rendered_frame(screen->fps_counter); + fps_counter_add_rendered_frame(&screen->fps_counter); struct size new_frame_size = {frame->width, frame->height}; if (!prepare_for_frame(screen, new_frame_size)) { diff --git a/app/src/screen.h b/app/src/screen.h index 2921c701..e2a43da7 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -20,7 +20,7 @@ struct screen { #endif struct video_buffer vb; - struct fps_counter *fps_counter; + struct fps_counter fps_counter; SDL_Window *window; SDL_Renderer *renderer; @@ -66,8 +66,16 @@ struct screen_params { // initialize screen, create window, renderer and texture (window is hidden) bool -screen_init(struct screen *screen, struct fps_counter *fps_counter, - const struct screen_params *params); +screen_init(struct screen *screen, const struct screen_params *params); + +// request to interrupt any inner thread +// must be called before screen_join() +void +screen_interrupt(struct screen *screen); + +// join any inner thread +void +screen_join(struct screen *screen); // destroy window, renderer and texture (if any) void From dcee7c0f7f2fe5709dc947fa6bc4ff4cdc07654e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 16 May 2021 18:36:07 +0200 Subject: [PATCH 0595/2244] Factorize screen_init() error management --- app/src/screen.c | 38 ++++++++++++++++++-------------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/app/src/screen.c b/app/src/screen.c index 12ad7d0a..a09831a3 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -307,8 +307,7 @@ screen_init(struct screen *screen, const struct screen_params *params) { if (!fps_counter_init(&screen->fps_counter)) { LOGE("Could not initialize FPS counter"); - video_buffer_destroy(&screen->vb); - return false; + goto error_destroy_video_buffer; } screen->frame_size = params->frame_size; @@ -347,19 +346,14 @@ screen_init(struct screen *screen, const struct screen_params *params) { window_flags); if (!screen->window) { LOGC("Could not create window: %s", SDL_GetError()); - fps_counter_destroy(&screen->fps_counter); - video_buffer_destroy(&screen->vb); - return false; + goto error_destroy_fps_counter; } screen->renderer = SDL_CreateRenderer(screen->window, -1, SDL_RENDERER_ACCELERATED); if (!screen->renderer) { LOGC("Could not create renderer: %s", SDL_GetError()); - SDL_DestroyWindow(screen->window); - fps_counter_destroy(&screen->fps_counter); - video_buffer_destroy(&screen->vb); - return false; + goto error_destroy_window; } SDL_RendererInfo renderer_info; @@ -408,22 +402,13 @@ screen_init(struct screen *screen, const struct screen_params *params) { screen->texture = create_texture(screen); if (!screen->texture) { LOGC("Could not create texture: %s", SDL_GetError()); - SDL_DestroyRenderer(screen->renderer); - SDL_DestroyWindow(screen->window); - fps_counter_destroy(&screen->fps_counter); - video_buffer_destroy(&screen->vb); - return false; + goto error_destroy_renderer; } screen->frame = av_frame_alloc(); if (!screen->frame) { LOGC("Could not create screen frame"); - SDL_DestroyTexture(screen->texture); - SDL_DestroyRenderer(screen->renderer); - SDL_DestroyWindow(screen->window); - fps_counter_destroy(&screen->fps_counter); - video_buffer_destroy(&screen->vb); - return false; + goto error_destroy_texture; } // Reset the window size to trigger a SIZE_CHANGED event, to workaround @@ -454,6 +439,19 @@ screen_init(struct screen *screen, const struct screen_params *params) { #endif return true; + +error_destroy_texture: + SDL_DestroyTexture(screen->texture); +error_destroy_renderer: + SDL_DestroyRenderer(screen->renderer); +error_destroy_window: + SDL_DestroyWindow(screen->window); +error_destroy_fps_counter: + fps_counter_destroy(&screen->fps_counter); +error_destroy_video_buffer: + video_buffer_destroy(&screen->vb); + + return false; } static void From 6a2cd089a176bc907c151f7245df66f347c0ed28 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 17 May 2021 08:46:38 +0200 Subject: [PATCH 0596/2244] Initialize input manager dynamically The input manager was partially initialized statically, but a call to input_manager_init() was needed anyway, so initialize all the fields from the "constructor". This is consistent with the initialization of the other structs. --- app/src/input_manager.c | 10 +++++++--- app/src/input_manager.h | 4 ++-- app/src/scrcpy.c | 16 ++-------------- 3 files changed, 11 insertions(+), 19 deletions(-) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 55b5fae2..b008c4db 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -52,9 +52,13 @@ is_shortcut_mod(struct input_manager *im, uint16_t sdl_mod) { } void -input_manager_init(struct input_manager *im, - const struct scrcpy_options *options) -{ +input_manager_init(struct input_manager *im, struct controller *controller, + struct screen *screen, + const struct scrcpy_options *options) { + im->controller = controller; + im->screen = screen; + im->repeat = 0; + im->control = options->control; im->forward_key_repeat = options->forward_key_repeat; im->prefer_text = options->prefer_text; diff --git a/app/src/input_manager.h b/app/src/input_manager.h index 76690757..1dd7825f 100644 --- a/app/src/input_manager.h +++ b/app/src/input_manager.h @@ -42,8 +42,8 @@ struct input_manager { }; void -input_manager_init(struct input_manager *im, - const struct scrcpy_options *options); +input_manager_init(struct input_manager *im, struct controller *controller, + struct screen *screen, const struct scrcpy_options *options); bool input_manager_handle_event(struct input_manager *im, SDL_Event *event); diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index e7a349d8..8d14a21b 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -40,19 +40,7 @@ static struct sc_v4l2_sink v4l2_sink; #endif static struct controller controller; static struct file_handler file_handler; - -static struct input_manager input_manager = { - .controller = &controller, - .screen = &screen, - .repeat = 0, - - // initialized later - .prefer_text = false, - .sdl_shortcut_mods = { - .data = {0}, - .count = 0, - }, -}; +static struct input_manager input_manager; #ifdef _WIN32 BOOL WINAPI windows_ctrl_handler(DWORD ctrl_type) { @@ -422,7 +410,7 @@ scrcpy(const struct scrcpy_options *options) { } stream_started = true; - input_manager_init(&input_manager, options); + input_manager_init(&input_manager, &controller, &screen, options); ret = event_loop(options); LOGD("quit..."); From 6adc97198bd6fc9122c9cd9cef319420219a384d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 9 May 2021 16:52:22 +0200 Subject: [PATCH 0597/2244] Provide device info directly on server connection This avoids to retrieve them in a separate step. --- app/meson.build | 1 - app/src/device.c | 23 ----------------------- app/src/device.h | 17 ----------------- app/src/scrcpy.c | 10 +--------- app/src/server.c | 25 +++++++++++++++++++++++-- app/src/server.h | 5 ++++- 6 files changed, 28 insertions(+), 53 deletions(-) delete mode 100644 app/src/device.c delete mode 100644 app/src/device.h diff --git a/app/meson.build b/app/meson.build index c1dbf489..6b6991aa 100644 --- a/app/meson.build +++ b/app/meson.build @@ -6,7 +6,6 @@ src = [ 'src/control_msg.c', 'src/controller.c', 'src/decoder.c', - 'src/device.c', 'src/device_msg.c', 'src/event_converter.c', 'src/file_handler.c', diff --git a/app/src/device.c b/app/src/device.c deleted file mode 100644 index 094dc616..00000000 --- a/app/src/device.c +++ /dev/null @@ -1,23 +0,0 @@ -#include "device.h" - -#include "util/log.h" - -bool -device_read_info(socket_t device_socket, char *device_name, struct size *size) { - unsigned char buf[DEVICE_NAME_FIELD_LENGTH + 4]; - int r = net_recv_all(device_socket, buf, sizeof(buf)); - if (r < DEVICE_NAME_FIELD_LENGTH + 4) { - LOGE("Could not retrieve device information"); - return false; - } - // in case the client sends garbage - buf[DEVICE_NAME_FIELD_LENGTH - 1] = '\0'; - // strcpy is safe here, since name contains at least - // DEVICE_NAME_FIELD_LENGTH bytes and strlen(buf) < DEVICE_NAME_FIELD_LENGTH - strcpy(device_name, (char *) buf); - size->width = (buf[DEVICE_NAME_FIELD_LENGTH] << 8) - | buf[DEVICE_NAME_FIELD_LENGTH + 1]; - size->height = (buf[DEVICE_NAME_FIELD_LENGTH + 2] << 8) - | buf[DEVICE_NAME_FIELD_LENGTH + 3]; - return true; -} diff --git a/app/src/device.h b/app/src/device.h deleted file mode 100644 index 376e3d4b..00000000 --- a/app/src/device.h +++ /dev/null @@ -1,17 +0,0 @@ -#ifndef DEVICE_H -#define DEVICE_H - -#include "common.h" - -#include - -#include "coords.h" -#include "util/net.h" - -#define DEVICE_NAME_FIELD_LENGTH 64 - -// name must be at least DEVICE_NAME_FIELD_LENGTH bytes -bool -device_read_info(socket_t device_socket, char *device_name, struct size *size); - -#endif diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 8d14a21b..8418353d 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -15,7 +15,6 @@ #include "controller.h" #include "decoder.h" -#include "device.h" #include "events.h" #include "file_handler.h" #include "input_manager.h" @@ -284,17 +283,10 @@ scrcpy(const struct scrcpy_options *options) { goto end; } - if (!server_connect_to(&server)) { - goto end; - } - char device_name[DEVICE_NAME_FIELD_LENGTH]; struct size frame_size; - // screenrecord does not send frames when the screen content does not - // change therefore, we transmit the screen size before the video stream, - // to be able to init the window immediately - if (!device_read_info(server.video_socket, device_name, &frame_size)) { + if (!server_connect_to(&server, device_name, &frame_size)) { goto end; } diff --git a/app/src/server.c b/app/src/server.c index 4448ab95..646006d6 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -464,8 +464,28 @@ error: return false; } +static bool +device_read_info(socket_t device_socket, char *device_name, struct size *size) { + unsigned char buf[DEVICE_NAME_FIELD_LENGTH + 4]; + int r = net_recv_all(device_socket, buf, sizeof(buf)); + if (r < DEVICE_NAME_FIELD_LENGTH + 4) { + LOGE("Could not retrieve device information"); + return false; + } + // in case the client sends garbage + buf[DEVICE_NAME_FIELD_LENGTH - 1] = '\0'; + // strcpy is safe here, since name contains at least + // DEVICE_NAME_FIELD_LENGTH bytes and strlen(buf) < DEVICE_NAME_FIELD_LENGTH + strcpy(device_name, (char *) buf); + size->width = (buf[DEVICE_NAME_FIELD_LENGTH] << 8) + | buf[DEVICE_NAME_FIELD_LENGTH + 1]; + size->height = (buf[DEVICE_NAME_FIELD_LENGTH + 2] << 8) + | buf[DEVICE_NAME_FIELD_LENGTH + 3]; + return true; +} + bool -server_connect_to(struct server *server) { +server_connect_to(struct server *server, char *device_name, struct size *size) { if (!server->tunnel_forward) { server->video_socket = net_accept(server->server_socket); if (server->video_socket == INVALID_SOCKET) { @@ -505,7 +525,8 @@ server_connect_to(struct server *server) { disable_tunnel(server); // ignore failure server->tunnel_enabled = false; - return true; + // The sockets will be closed on stop if device_read_info() fails + return device_read_info(server->video_socket, device_name, size); } void diff --git a/app/src/server.h b/app/src/server.h index 15306e4f..ad49a982 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -8,6 +8,7 @@ #include #include "adb.h" +#include "coords.h" #include "scrcpy.h" #include "util/log.h" #include "util/net.h" @@ -58,9 +59,11 @@ bool server_start(struct server *server, const char *serial, const struct server_params *params); +#define DEVICE_NAME_FIELD_LENGTH 64 // block until the communication with the server is established +// device_name must point to a buffer of at least DEVICE_NAME_FIELD_LENGTH bytes bool -server_connect_to(struct server *server); +server_connect_to(struct server *server, char *device_name, struct size *size); // disconnect and kill the server process void From 9bed8cf3f44b551cbb9337b67581ee4f6fb5a23a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 28 May 2021 10:05:34 +0200 Subject: [PATCH 0598/2244] Fix syntax highlighting in README --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e0d40984..ae0c6b03 100644 --- a/README.md +++ b/README.md @@ -748,7 +748,9 @@ handled by the active application. To use a specific _adb_ binary, configure its path in the environment variable `ADB`: - ADB=/path/to/adb scrcpy +```bash +ADB=/path/to/adb scrcpy +``` To override the path of the `scrcpy-server` file, configure its path in `SCRCPY_SERVER_PATH`. From 1b7c9b3e1c4a8f934268bd5d0b3db6a558695c7d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 28 May 2021 10:18:36 +0200 Subject: [PATCH 0599/2244] Reference translations from FAQ.md --- FAQ.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/FAQ.md b/FAQ.md index a84aade8..2fd209b6 100644 --- a/FAQ.md +++ b/FAQ.md @@ -1,5 +1,7 @@ # Frequently Asked Questions +[Read in another language](#translations) + Here are the common reported problems and their status. @@ -230,3 +232,10 @@ You could also edit (a copy of) `scrcpy-console.bat` or `scrcpy-noconsole.vbs` to add some arguments. [show file extensions]: https://www.howtogeek.com/205086/beginner-how-to-make-windows-show-file-extensions/ + + +## Translations + +This FAQ is available in other languages: + + - [한국어 (Korean, `ko`) - v1.11](FAQ.ko.md) From 2e9d52008084696c2ad8809a03ff8e92988591df Mon Sep 17 00:00:00 2001 From: Alberto Pasqualetto Date: Sat, 15 May 2021 00:13:56 +0200 Subject: [PATCH 0600/2244] README in Italian PR #2316 Signed-off-by: Romain Vimont --- README.it.md | 742 +++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 1 + 2 files changed, 743 insertions(+) create mode 100644 README.it.md diff --git a/README.it.md b/README.it.md new file mode 100644 index 00000000..37416f1d --- /dev/null +++ b/README.it.md @@ -0,0 +1,742 @@ +_Apri il [README](README.md) originale e sempre aggiornato._ + +# scrcpy (v1.17) + +Questa applicazione fornisce la visualizzazione e il controllo dei dispositivi Android collegati via USB (o [via TCP/IP][article-tcpip]). Non richiede alcun accesso _root_. +Funziona su _GNU/Linux_, _Windows_ e _macOS_. + +![screenshot](assets/screenshot-debian-600.jpg) + +Si concentra su: + + - **leggerezza** (nativo, mostra solo lo schermo del dispositivo) + - **prestazioni** (30~60fps) + - **qualità** (1920×1080 o superiore) + - **bassa latenza** ([35~70ms][lowlatency]) + - **tempo di avvio basso** (~ 1secondo per visualizzare la prima immagine) + - **non invadenza** (nulla viene lasciato installato sul dispositivo) + +[lowlatency]: https://github.com/Genymobile/scrcpy/pull/646 + + +## Requisiti + +Il dispositivo Android richiede almeno le API 21 (Android 5.0). + +Assiucurati di aver [attivato il debug usb][enable-adb] sul(/i) tuo(i) dispositivo(/i). + +[enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling + +In alcuni dispositivi, devi anche abilitare [un'opzione aggiuntiva][control] per controllarli con tastiera e mouse. + +[control]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323 + +## Ottieni l'app + +Packaging status + +### Sommario + + - Linux: `apt install scrcpy` + - Windows: [download](README.md#windows) + - macOS: `brew install scrcpy` + +Compila dai sorgenti: [BUILD] (in inglese) ([procedimento semplificato][BUILD_simple] (in inglese)) + +[BUILD]: BUILD.md +[BUILD_simple]: BUILD.md#simple + + +### Linux + +Su Debian (_testing_ e _sid_ per ora) e Ubuntu (20.04): + +``` +apt install scrcpy +``` + +È disponibile anche un pacchetto [Snap]: [`scrcpy`][snap-link]. + +[snap-link]: https://snapstats.org/snaps/scrcpy + +[snap]: https://it.wikipedia.org/wiki/Snappy_(gestore_pacchetti) + +Per Fedora, è disponibile un pacchetto [COPR]: [`scrcpy`][copr-link]. + +[COPR]: https://fedoraproject.org/wiki/Category:Copr +[copr-link]: https://copr.fedorainfracloud.org/coprs/zeno/scrcpy/ + +Per Arch Linux, è disponibile un pacchetto [AUR]: [`scrcpy`][aur-link]. + +[AUR]: https://wiki.archlinux.org/index.php/Arch_User_Repository +[aur-link]: https://aur.archlinux.org/packages/scrcpy/ + +Per Gentoo, è disponibile una [Ebuild]: [`scrcpy/`][ebuild-link]. + +[Ebuild]: https://wiki.gentoo.org/wiki/Ebuild +[ebuild-link]: https://github.com/maggu2810/maggu2810-overlay/tree/master/app-mobilephone/scrcpy + +Puoi anche [compilare l'app manualmente][BUILD] (in inglese) ([procedimento semplificato][BUILD_simple] (in inglese)). + + +### Windows + +Per Windows, per semplicità è disponibile un archivio precompilato con tutte le dipendenze (incluso `adb`): + + - [README](README.md#windows) (Link al README originale per l'ultima versione) + +È anche disponibile in [Chocolatey]: + +[Chocolatey]: https://chocolatey.org/ + +```bash +choco install scrcpy +choco install adb # se non lo hai già +``` + +E in [Scoop]: + +```bash +scoop install scrcpy +scoop install adb # se non lo hai già +``` + +[Scoop]: https://scoop.sh + +Puoi anche [compilare l'app manualmente][BUILD] (in inglese). + + +### macOS + +L'applicazione è disponibile in [Homebrew]. Basta installarlo: + +[Homebrew]: https://brew.sh/ + +```bash +brew install scrcpy +``` + +Serve che `adb` sia accessibile dal tuo `PATH`. Se non lo hai già: + +```bash +brew install android-platform-tools +``` + +È anche disponibile in [MacPorts], che imposta adb per te: + +```bash +sudo port install scrcpy +``` + +[MacPorts]: https://www.macports.org/ + + +Puoi anche [compilare l'app manualmente][BUILD] (in inglese). + + +## Esecuzione + +Collega un dispositivo Android ed esegui: + +```bash +scrcpy +``` + +Scrcpy accetta argomenti da riga di comando, essi sono listati con: + +```bash +scrcpy --help +``` + +## Funzionalità + +### Configurazione di acquisizione + +#### Riduci dimensione + +Qualche volta è utile trasmettere un dispositvo Android ad una definizione inferiore per aumentare le prestazioni. + +Per limitare sia larghezza che altezza ad un certo valore (ad es. 1024): + +```bash +scrcpy --max-size 1024 +scrcpy -m 1024 # versione breve +``` + +L'altra dimensione è calcolata in modo tale che il rapporto di forma del dispositivo sia preservato. +In questo esempio un dispositivo in 1920x1080 viene trasmesso a 1024x576. + + +#### Cambia bit-rate (velocità di trasmissione) + +Il bit-rate predefinito è 8 Mbps. Per cambiare il bitrate video (ad es. a 2 Mbps): + +```bash +scrcpy --bit-rate 2M +scrcpy -b 2M # versione breve +``` + +#### Limitare il frame rate (frequenza di fotogrammi) + +Il frame rate di acquisizione può essere limitato: + +```bash +scrcpy --max-fps 15 +``` + +Questo è supportato ufficialmente a partire da Android 10, ma potrebbe funzionare in versioni precedenti. + +#### Ritaglio + +Lo schermo del dispositivo può essere ritagliato per visualizzare solo parte di esso. + +Questo può essere utile, per esempio, per trasmettere solo un occhio dell'Oculus Go: + +```bash +scrcpy --crop 1224:1440:0:0 # 1224x1440 at offset (0,0) +``` + +Se anche `--max-size` è specificata, il ridimensionamento è applicato dopo il ritaglio. + + +#### Blocca orientamento del video + + +Per bloccare l'orientamento della trasmissione: + +```bash +scrcpy --lock-video-orientation 0 # orientamento naturale +scrcpy --lock-video-orientation 1 # 90° antiorario +scrcpy --lock-video-orientation 2 # 180° +scrcpy --lock-video-orientation 3 # 90° orario +``` + +Questo influisce sull'orientamento della registrazione. + + +La [finestra può anche essere ruotata](#rotazione) indipendentemente. + + +#### Codificatore + +Alcuni dispositivi hanno più di un codificatore e alcuni di questi possono provocare problemi o crash. È possibile selezionare un encoder diverso: + +```bash +scrcpy --encoder OMX.qcom.video.encoder.avc +``` + +Per elencare i codificatori disponibili puoi immettere un nome di codificatore non valido e l'errore mostrerà i codificatori disponibili: + +```bash +scrcpy --encoder _ +``` + +### Registrazione + +È possibile registrare lo schermo durante la trasmissione: + +```bash +scrcpy --record file.mp4 +scrcpy -r file.mkv +``` + +Per disabilitare la trasmissione durante la registrazione: + +```bash +scrcpy --no-display --record file.mp4 +scrcpy -Nr file.mkv +# interrompere la registrazione con Ctrl+C +``` + +I "fotogrammi saltati" sono registrati nonostante non siano mostrati in tempo reale (per motivi di prestazioni). I fotogrammi sono _datati_ sul dispositivo, così una [variazione di latenza dei pacchetti][packet delay variation] non impatta il file registrato. + +[packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation + + +### Connessione + +#### Wireless + + +_Scrcpy_ usa `adb` per comunicare col dispositivo e `adb` può [connettersi][connect] al dispositivo mediante TCP/IP: + +1. Connetti il dispositivo alla stessa rete Wi-Fi del tuo computer. +2. Trova l'indirizzo IP del tuo dispositivo in Impostazioni → Informazioni sul telefono → Stato, oppure eseguendo questo comando: + + ```bash + adb shell ip route | awk '{print $9}' + ``` + +3. Abilita adb via TCP/IP sul tuo dispositivo: `adb tcpip 5555`. +4. Scollega il tuo dispositivo. +5. Connetti il tuo dispositivo: `adb connect IP_DISPOSITVO:5555` _(rimpiazza `IP_DISPOSITIVO`)_. +6. Esegui `scrcpy` come al solito. + +Potrebbe essere utile diminuire il bit-rate e la definizione + +```bash +scrcpy --bit-rate 2M --max-size 800 +scrcpy -b2M -m800 # versione breve +``` + +[connect]: https://developer.android.com/studio/command-line/adb.html#wireless + + +#### Multi dispositivo + +Se in `adb devices` sono listati più dispositivi, è necessario specificare il _seriale_: + +```bash +scrcpy --serial 0123456789abcdef +scrcpy -s 0123456789abcdef # versione breve +``` + +Se il dispositivo è collegato mediante TCP/IP: + +```bash +scrcpy --serial 192.168.0.1:5555 +scrcpy -s 192.168.0.1:5555 # versione breve +``` + +Puoi avviare più istanze di _scrcpy_ per diversi dispositivi. + + +#### Avvio automativo alla connessione del dispositivo + +Potresti usare [AutoAdb]: + +```bash +autoadb scrcpy -s '{}' +``` + +[AutoAdb]: https://github.com/rom1v/autoadb + +#### Tunnel SSH + +Per connettersi a un dispositivo remoto è possibile collegare un client `adb` locale ad un server `adb` remoto (assunto che entrambi stiano usando la stessa versione del protocollo _adb_): + +```bash +adb kill-server # termina il server adb locale su 5037 +ssh -CN -L5037:localhost:5037 -R27183:localhost:27183 your_remote_computer +# tieni questo aperto +``` + +Da un altro terminale: + +```bash +scrcpy +``` + +Per evitare l'abilitazione dell'apertura porte remota potresti invece forzare una "forward connection" (notare il `-L` invece di `-R`) + +```bash +adb kill-server # termina il server adb locale su 5037 +ssh -CN -L5037:localhost:5037 -L27183:localhost:27183 your_remote_computer +# tieni questo aperto +``` + +Da un altro terminale: + +```bash +scrcpy --force-adb-forward +``` + + +Come per le connessioni wireless potrebbe essere utile ridurre la qualità: + +``` +scrcpy -b2M -m800 --max-fps 15 +``` + +### Configurazione della finestra + +#### Titolo + +Il titolo della finestra è il modello del dispositivo per impostazione predefinita. Esso può essere cambiato: + +```bash +scrcpy --window-title 'My device' +``` + +#### Posizione e dimensione + +La posizione e la dimensione iniziale della finestra può essere specificata: + +```bash +scrcpy --window-x 100 --window-y 100 --window-width 800 --window-height 600 +``` + +#### Senza bordi + +Per disabilitare le decorazioni della finestra: + +```bash +scrcpy --window-borderless +``` + +#### Sempre in primo piano + +Per tenere scrcpy sempre in primo piano: + +```bash +scrcpy --always-on-top +``` + +#### Schermo intero + +L'app può essere avviata direttamente a schermo intero: + +```bash +scrcpy --fullscreen +scrcpy -f # versione breve +``` + +Lo schermo intero può anche essere attivato/disattivato con MOD+f. + +#### Rotazione + +La finestra può essere ruotata: + +```bash +scrcpy --rotation 1 +``` + +I valori possibili sono: + - `0`: nessuna rotazione + - `1`: 90 gradi antiorari + - `2`: 180 gradi + - `3`: 90 gradi orari + +La rotazione può anche essere cambiata dinamicamente con MOD+ +_(sinistra)_ e MOD+ _(destra)_. + +Notare che _scrcpy_ gestisce 3 diversi tipi di rotazione: + - MOD+r richiede al dispositvo di cambiare tra orientamento verticale (portrait) e orizzontale (landscape) (l'app in uso potrebbe rifiutarsi se non supporta l'orientamento richiesto). + - [`--lock-video-orientation`](#blocca-orientamento-del-video) cambia l'orientamento della trasmissione (l'orientamento del video inviato dal dispositivo al computer). Questo influenza la registrazione. + - `--rotation` (o MOD+/MOD+) ruota solo il contenuto della finestra. Questo influenza solo la visualizzazione, non la registrazione. + + +### Altre opzioni di trasmissione + +#### "Sola lettura" + +Per disabilitare i controlli (tutto ciò che può interagire col dispositivo: tasti di input, eventi del mouse, trascina e rilascia (drag&drop) file): + +```bash +scrcpy --no-control +scrcpy -n +``` + +#### Schermo + +Se sono disponibili più schermi, è possibile selezionare lo schermo da trasmettere: + +```bash +scrcpy --display 1 +``` + +La lista degli id schermo può essere ricavata da: + +```bash +adb shell dumpsys display # cerca "mDisplayId=" nell'output +``` + +Lo schermo secondario potrebbe essere possibile controllarlo solo se il dispositivo esegue almeno Android 10 (in caso contrario è trasmesso in modalità sola lettura). + + +#### Mantenere sbloccato + +Per evitare che il dispositivo si blocchi dopo un po' che il dispositivo è collegato: + +```bash +scrcpy --stay-awake +scrcpy -w +``` + +Lo stato iniziale è ripristinato quando scrcpy viene chiuso. + + +#### Spegnere lo schermo + +È possibile spegnere lo schermo del dispositivo durante la trasmissione con un'opzione da riga di comando: + +```bash +scrcpy --turn-screen-off +scrcpy -S +``` + +Oppure premendo MOD+o in qualsiasi momento. + +Per riaccenderlo premere MOD+Shift+o. + +In Android il pulsante `POWER` (tasto di accensione) accende sempre lo schermo. Per comodità, se `POWER` è inviato via scrcpy (con click destro o con MOD+p), si forza il dispositivo a spegnere lo schermo dopo un piccolo ritardo (appena possibile). +Il pulsante fisico `POWER` continuerà ad accendere lo schermo normalmente. + +Può anche essere utile evitare il blocco del dispositivo: + +```bash +scrcpy --turn-screen-off --stay-awake +scrcpy -Sw +``` + +#### Renderizzare i fotogrammi scaduti + +Per minimizzare la latenza _scrcpy_ renderizza sempre l'ultimo fotogramma decodificato disponibile in maniera predefinita e tralascia quelli precedenti. + +Per forzare la renderizzazione di tutti i fotogrammi (a costo di una possibile latenza superiore), utilizzare: + +```bash +scrcpy --render-expired-frames +``` + +#### Mostrare i tocchi + +Per le presentazioni può essere utile mostrare i tocchi fisici (sul dispositivo fisico). + +Android fornisce questa funzionalità nelle _Opzioni sviluppatore_. + +_Scrcpy_ fornisce un'opzione per abilitare questa funzionalità all'avvio e ripristinare il valore iniziale alla chiusura: + +```bash +scrcpy --show-touches +scrcpy -t +``` + +Notare che mostra solo i tocchi _fisici_ (con le dita sul dispositivo). + + +#### Disabilitare il salvaschermo + +In maniera predefinita scrcpy non previene l'attivazione del salvaschermo del computer. + +Per disabilitarlo: + +```bash +scrcpy --disable-screensaver +``` + + +### Input di controlli + +#### Rotazione dello schermo del dispostivo + +Premere MOD+r per cambiare tra le modalità verticale (portrait) e orizzontale (landscape). + +Notare che la rotazione avviene solo se l'applicazione in primo piano supporta l'orientamento richiesto. + +#### Copia-incolla + +Quando gli appunti di Android cambiano, essi vengono automaticamente sincronizzati con gli appunti del computer. + +Qualsiasi scorciatoia Ctrl viene inoltrata al dispositivo. In particolare: + - Ctrl+c copia + - Ctrl+x taglia + - Ctrl+v incolla (dopo la sincronizzazione degli appunti da computer a dispositivo) + +Questo solitamente funziona nella maniera più comune. + +Il comportamento reale, però, dipende dall'applicazione attiva. Per esempio _Termux_ invia SIGINT con Ctrl+c, e _K-9 Mail_ compone un nuovo messaggio. + +Per copiare, tagliare e incollare in questi casi (ma è solo supportato in Android >= 7): + - MOD+c inietta `COPY` + - MOD+x inietta `CUT` + - MOD+v inietta `PASTE` (dopo la sincronizzazione degli appunti da computer a dispositivo) + +In aggiunta, MOD+Shift+v permette l'iniezione del testo degli appunti del computer come una sequenza di eventi pressione dei tasti. Questo è utile quando il componente non accetta l'incollaggio di testo (per esempio in _Termux_), ma questo può rompere il contenuto non ASCII. + +**AVVISO:** Incollare gli appunti del computer nel dispositivo (sia con Ctrl+v che con MOD+v) copia il contenuto negli appunti del dispositivo. Come conseguenza, qualsiasi applicazione Android potrebbe leggere il suo contenuto. Dovresti evitare di incollare contenuti sensibili (come password) in questa maniera. + +Alcuni dispositivi non si comportano come aspettato quando si modificano gli appunti del dispositivo a livello di codice. L'opzione `--legacy-paste` è fornita per cambiare il comportamento di Ctrl+v and MOD+v in modo tale che anch'essi iniettino il testo gli appunti del computer come una sequenza di eventi pressione dei tasti (nella stessa maniera di MOD+Shift+v). + +#### Pizzica per zoomare (pinch-to-zoom) + +Per simulare il "pizzica per zoomare": Ctrl+_click e trascina_. + +Più precisamente, tieni premuto Ctrl mentre premi il pulsante sinistro. Finchè il pulsante non sarà rilasciato, tutti i movimenti del mouse ridimensioneranno e ruoteranno il contenuto (se supportato dall'applicazione) relativamente al centro dello schermo. + +Concretamente scrcpy genera degli eventi di tocco addizionali di un "dito virtuale" nella posizione simmetricamente opposta rispetto al centro dello schermo. + + +#### Preferenze di iniezione del testo + +Ci sono due tipi di [eventi][textevents] generati quando si scrive testo: + - _eventi di pressione_, segnalano che tasto è stato premuto o rilasciato; + - _eventi di testo_, segnalano che del testo è stato inserito. + +In maniera predefinita le lettere sono "iniettate" usando gli eventi di pressione, in maniera tale che la tastiera si comporti come aspettato nei giochi (come accade solitamente per i tasti WASD). + +Questo, però, può [causare problemi][prefertext]. Se incontri un problema del genere, puoi evitarlo con: + +```bash +scrcpy --prefer-text +``` + +(ma questo romperà il normale funzionamento della tastiera nei giochi) + +[textevents]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-text-input +[prefertext]: https://github.com/Genymobile/scrcpy/issues/650#issuecomment-512945343 + + +#### Ripetizione di tasti + +In maniera predefinita tenere premuto un tasto genera una ripetizione degli eventi di pressione di tale tasto. Questo può creare problemi di performance in alcuni giochi, dove questi eventi sono inutilizzati. + +Per prevenire l'inoltro ripetuto degli eventi di pressione: + +```bash +scrcpy --no-key-repeat +``` + +#### Click destro e click centrale + +In maniera predefinita, click destro aziona BACK (indietro) e il click centrale aziona HOME. Per disabilitare queste scorciatoie e, invece, inviare i click al dispositivo: + +```bash +scrcpy --forward-all-clicks +``` + + +### Rilascio di file + +#### Installare APK + +Per installare un APK, trascina e rilascia un file APK (finisce con `.apk`) nella finestra di _scrcpy_. + +Non c'è alcuna risposta visiva, un log è stampato nella console. + + +#### Trasferimento di file verso il dispositivo + +Per trasferire un file in `/sdcard/` del dispositivo trascina e rilascia un file (non APK) nella finestra di _scrcpy_. + +Non c'è alcuna risposta visiva, un log è stampato nella console. + +La cartella di destinazione può essere cambiata all'avvio: + +```bash +scrcpy --push-target=/sdcard/Download/ +``` + + +### Inoltro dell'audio + +L'audio non è inoltrato da _scrcpy_. Usa [sndcpy]. + +Vedi anche la [issue #14]. + +[sndcpy]: https://github.com/rom1v/sndcpy +[issue #14]: https://github.com/Genymobile/scrcpy/issues/14 + + +## Scociatoie + +Nella lista seguente, MOD è il modificatore delle scorciatoie. In maniera predefinita è Alt (sinistro) o Super (sinistro). + +Può essere cambiato usando `--shortcut-mod`. I tasti possibili sono `lctrl`, `rctrl`, `lalt`, `ralt`, `lsuper` and `rsuper` (`l` significa sinistro e `r` significa destro). Per esempio: + +```bash +# usa ctrl destro per le scorciatoie +scrcpy --shortcut-mod=rctrl + +# use sia "ctrl sinistro"+"alt sinistro" che "super sinistro" per le scorciatoie +scrcpy --shortcut-mod=lctrl+lalt,lsuper +``` + +_[Super] è il pulsante Windows o Cmd._ + +[Super]: https://it.wikipedia.org/wiki/Tasto_Windows + + + | Azione | Scorciatoia + | ------------------------------------------- |:----------------------------- + | Schermo intero | MOD+f + | Rotazione schermo a sinistra | MOD+ _(sinistra)_ + | Rotazione schermo a destra | MOD+ _(destra)_ + | Ridimensiona finestra a 1:1 (pixel-perfect) | MOD+g + | Ridimensiona la finestra per rimuovere i bordi neri | MOD+w \| _Doppio click¹_ + | Premi il tasto `HOME` | MOD+h \| _Click centrale_ + | Premi il tasto `BACK` | MOD+b \| _Click destro²_ + | Premi il tasto `APP_SWITCH` | MOD+s + | Premi il tasto `MENU` (sblocca lo schermo) | MOD+m + | Premi il tasto `VOLUME_UP` | MOD+ _(su)_ + | Premi il tasto `VOLUME_DOWN` | MOD+ _(giù)_ + | Premi il tasto `POWER` | MOD+p + | Accendi | _Click destro²_ + | Spegni lo schermo del dispositivo (continua a trasmettere) | MOD+o + | Accendi lo schermo del dispositivo | MOD+Shift+o + | Ruota lo schermo del dispositivo | MOD+r + | Espandi il pannello delle notifiche | MOD+n + | Chiudi il pannello delle notifiche | MOD+Shift+n + | Copia negli appunti³ | MOD+c + | Taglia negli appunti³ | MOD+x + | Sincronizza gli appunti e incolla³ | MOD+v + | Inietta il testo degli appunti del computer | MOD+Shift+v + | Abilita/Disabilita il contatore FPS (su stdout) | MOD+i + | Pizzica per zoomare | Ctrl+_click e trascina_ + +_¹Doppio click sui bordi neri per rimuoverli._ +_²Il tasto destro accende lo schermo se era spento, preme BACK in caso contrario._ +_³Solo in Android >= 7._ + +Tutte le scorciatoie Ctrl+_tasto_ sono inoltrate al dispositivo, così sono gestite dall'applicazione attiva. + +## Path personalizzati + +Per utilizzare dei binari _adb_ specifici, configura il suo path nella variabile d'ambente `ADB`: + +```bash +ADB=/percorso/per/adb scrcpy +``` + +Per sovrascrivere il percorso del file `scrcpy-server`, configura il percorso in `SCRCPY_SERVER_PATH`. + +## Perchè _scrcpy_? + +Un collega mi ha sfidato a trovare un nome tanto impronunciabile quanto [gnirehtet]. + +[`strcpy`] copia una **str**ing (stringa); `scrcpy` copia uno **scr**een (schermo). + +[gnirehtet]: https://github.com/Genymobile/gnirehtet +[`strcpy`]: http://man7.org/linux/man-pages/man3/strcpy.3.html + +## Come compilare? + +Vedi [BUILD] (in inglese). + + +## Problemi comuni + +Vedi le [FAQ](FAQ.it.md). + + +## Sviluppatori + +Leggi la [pagina per sviluppatori]. + +[pagina per sviluppatori]: DEVELOP.md + + +## Licenza (in inglese) + + Copyright (C) 2018 Genymobile + Copyright (C) 2018-2021 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. + +## Articoli (in inglese) + +- [Introducendo scrcpy][article-intro] +- [Scrcpy ora funziona wireless][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 ae0c6b03..835dad1e 100644 --- a/README.md +++ b/README.md @@ -815,6 +815,7 @@ Read the [developers page]. This README is available in other languages: - [Indonesian (Indonesia, `id`) - v1.16](README.id.md) +- [Italiano (Italiano, `it`) - v1.17](README.it.md) - [日本語 (Japanese, `jp`) - v1.17](README.jp.md) - [한국어 (Korean, `ko`) - v1.11](README.ko.md) - [português brasileiro (Brazilian Portuguese, `pt-BR`) - v1.17](README.pt-br.md) From 8121b0b7e72b393818f592a7858b5003a8de9c59 Mon Sep 17 00:00:00 2001 From: Alberto Pasqualetto Date: Sat, 15 May 2021 00:14:08 +0200 Subject: [PATCH 0601/2244] FAQ in Italian PR #2316 Signed-off-by: Romain Vimont --- FAQ.it.md | 217 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ FAQ.md | 1 + 2 files changed, 218 insertions(+) create mode 100644 FAQ.it.md diff --git a/FAQ.it.md b/FAQ.it.md new file mode 100644 index 00000000..5c5830ce --- /dev/null +++ b/FAQ.it.md @@ -0,0 +1,217 @@ +_Apri le [FAQ](FAQ.md) originali e sempre aggiornate._ + +# Domande Frequenti (FAQ) + +Questi sono i problemi più comuni riportati e i loro stati. + + +## Problemi di `adb` + +`scrcpy` esegue comandi `adb` per inizializzare la connessione con il dispositivo. Se `adb` fallisce, scrcpy non funzionerà. + +In questo caso sarà stampato questo errore: + +> ERROR: "adb push" returned with value 1 + +Questo solitamente non è un bug di _scrcpy_, ma un problema del tuo ambiente. + +Per trovare la causa, esegui: + +```bash +adb devices +``` + +### `adb` not found (`adb` non trovato) + +È necessario che `adb` sia accessibile dal tuo `PATH`. + +In Windows, la cartella corrente è nel tuo `PATH` e `adb.exe` è incluso nella release, perciò dovrebbe già essere pronto all'uso. + + +### Device unauthorized (Dispositivo non autorizzato) + +Controlla [stackoverflow][device-unauthorized] (in inglese). + +[device-unauthorized]: https://stackoverflow.com/questions/23081263/adb-android-device-unauthorized + + +### Device not detected (Dispositivo non rilevato) + +> adb: error: failed to get feature set: no devices/emulators found + +Controlla di aver abilitato correttamente il [debug con adb][enable-adb] (link in inglese). + +Se il tuo dispositivo non è rilevato, potresti avere bisogno dei [driver][drivers] (link in inglese) (in Windows). + +[enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling +[drivers]: https://developer.android.com/studio/run/oem-usb.html + + +### Più dispositivi connessi + +Se più dispositivi sono connessi, riscontrerai questo errore: + +> adb: error: failed to get feature set: more than one device/emulator + +l'identificatore del tuo dispositivo deve essere fornito: + +```bash +scrcpy -s 01234567890abcdef +``` + +Notare che se il tuo dispositivo è connesso mediante TCP/IP, riscontrerai questo messaggio: + +> adb: error: more than one device/emulator +> ERROR: "adb reverse" returned with value 1 +> WARN: 'adb reverse' failed, fallback to 'adb forward' + +Questo è un problema atteso (a causa di un bug di una vecchia versione di Android, vedi [#5] (link in inglese)), ma in quel caso scrcpy ripiega su un metodo differente, il quale dovrebbe funzionare. + +[#5]: https://github.com/Genymobile/scrcpy/issues/5 + + +### Conflitti tra versioni di adb + +> adb server version (41) doesn't match this client (39); killing... + +L'errore compare quando usi più versioni di `adb` simultaneamente. Devi trovare il programma che sta utilizzando una versione differente di `adb` e utilizzare la stessa versione dappertutto. + +Puoi sovrascrivere i binari di `adb` nell'altro programma, oppure chiedere a _scrcpy_ di usare un binario specifico di `adb`, impostando la variabile d'ambiente `ADB`: + +```bash +set ADB=/path/to/your/adb +scrcpy +``` + + +### Device disconnected (Dispositivo disconnesso) + +Se _scrcpy_ si interrompe con l'avviso "Device disconnected", allora la connessione `adb` è stata chiusa. + +Prova con un altro cavo USB o inseriscilo in un'altra porta USB. Vedi [#281] (in inglese) e [#283] (in inglese). + +[#281]: https://github.com/Genymobile/scrcpy/issues/281 +[#283]: https://github.com/Genymobile/scrcpy/issues/283 + + + +## Problemi di controllo + +### Mouse e tastiera non funzionano + +Su alcuni dispositivi potresti dover abilitare un opzione che permette l'[input simulato][simulating input] (link in inglese). Nelle opzioni sviluppatore, abilita: + +> **Debug USB (Impostazioni di sicurezza)** +> _Permetti la concessione dei permessi e la simulazione degli input mediante il debug USB_ + + +[simulating input]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323 + + +### I caratteri speciali non funzionano + +Iniettare del testo in input è [limitato ai caratteri ASCII][text-input] (link in inglese). Un trucco permette di iniettare dei [caratteri accentati][accented-characters] (link in inglese), ma questo è tutto. Vedi [#37] (link in inglese). + +[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 + + +## Problemi del client + +### La qualità è bassa + +Se la definizione della finestra del tuo client è minore di quella del tuo dispositivo, allora potresti avere una bassa qualità di visualizzazione, specialmente individuabile nei testi (vedi [#40] (link in inglese)). + +[#40]: https://github.com/Genymobile/scrcpy/issues/40 + +Per migliorare la qualità di ridimensionamento (downscaling), il filtro trilineare è applicato automaticamente se il renderizzatore è OpenGL e se supporta la creazione di mipmap. + +In Windows, potresti voler forzare OpenGL: + +``` +scrcpy --render-driver=opengl +``` + +Potresti anche dover configurare il [comportamento di ridimensionamento][scaling behavior] (link in inglese): + +> `scrcpy.exe` > Propietà > Compatibilità > Modifica impostazioni DPI elevati > Esegui l'override del comportamento di ridimensionamento DPI elevati > Ridimensionamento eseguito per: _Applicazione_. + +[scaling behavior]: https://github.com/Genymobile/scrcpy/issues/40#issuecomment-424466723 + + + +### Crash del compositore KWin + +In Plasma Desktop, il compositore è disabilitato mentre _scrcpy_ è in esecuzione. + +Come soluzione alternativa, [disattiva la "composizione dei blocchi"][kwin] (link in inglese). + + +[kwin]: https://github.com/Genymobile/scrcpy/issues/114#issuecomment-378778613 + + +## Crash + +### Eccezione + +Ci potrebbero essere molte ragioni. Una causa comune è che il codificatore hardware del tuo dispositivo non riesce a codificare alla definizione selezionata: + +> ``` +> 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 +> ``` + +o + +> ``` +> ERROR: Exception on thread Thread[main,5,main] +> java.lang.IllegalStateException +> at android.media.MediaCodec.native_dequeueOutputBuffer(Native Method) +> ``` + +Prova con una definizione inferiore: + +``` +scrcpy -m 1920 +scrcpy -m 1024 +scrcpy -m 800 +``` + +Potresti anche provare un altro [codificatore](README.it.md#codificatore). + + +## Linea di comando in Windows + +Alcuni utenti Windows non sono familiari con la riga di comando. Qui è descritto come aprire un terminale ed eseguire `scrcpy` con gli argomenti: + + 1. Premi Windows+r, questo apre una finestra di dialogo. + 2. Scrivi `cmd` e premi Enter, questo apre un terminale. + 3. Vai nella tua cartella di _scrcpy_ scrivendo (adatta il percorso): + + ```bat + cd C:\Users\user\Downloads\scrcpy-win64-xxx + ``` + + e premi Enter + 4. Scrivi il tuo comando. Per esempio: + + ```bat + scrcpy --record file.mkv + ``` + +Se pianifichi di utilizzare sempre gli stessi argomenti, crea un file `myscrcpy.bat` (abilita mostra [estensioni nomi file][show file extensions] per evitare di far confusione) contenente il tuo comando nella cartella di `scrcpy`. Per esempio: + +```bat +scrcpy --prefer-text --turn-screen-off --stay-awake +``` + +Poi fai doppio click su quel file. + +Potresti anche modificare (una copia di) `scrcpy-console.bat` o `scrcpy-noconsole.vbs` per aggiungere alcuni argomenti. + +[show file extensions]: https://www.techpedia.it/14-windows/windows-10/171-visualizzare-le-estensioni-nomi-file-con-windows-10 diff --git a/FAQ.md b/FAQ.md index 2fd209b6..c1e39a39 100644 --- a/FAQ.md +++ b/FAQ.md @@ -238,4 +238,5 @@ to add some arguments. This FAQ is available in other languages: + - [Italiano (Italiano, `it`) - v1.17](FAQ.it.md) - [한국어 (Korean, `ko`) - v1.11](FAQ.ko.md) From d3d955f67b92d08bbee21bebce40fa072eea38f6 Mon Sep 17 00:00:00 2001 From: Secreto31126 <46955459+Secreto31126@users.noreply.github.com> Date: Sat, 15 May 2021 04:08:28 -0300 Subject: [PATCH 0602/2244] Translate README.md into Spanish PR #2318 Signed-off-by: Romain Vimont --- README.md | 1 + README.sp.md | 743 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 744 insertions(+) create mode 100644 README.sp.md diff --git a/README.md b/README.md index 835dad1e..477c6d25 100644 --- a/README.md +++ b/README.md @@ -819,6 +819,7 @@ This README is available in other languages: - [日本語 (Japanese, `jp`) - v1.17](README.jp.md) - [한국어 (Korean, `ko`) - v1.11](README.ko.md) - [português brasileiro (Brazilian Portuguese, `pt-BR`) - v1.17](README.pt-br.md) +- [Español (Spanish, `sp`) - v1.17](README.sp.md) - [简体中文 (Simplified Chinese, `zh-Hans`) - v1.17](README.zh-Hans.md) - [繁體中文 (Traditional Chinese, `zh-Hant`) - v1.15](README.zh-Hant.md) diff --git a/README.sp.md b/README.sp.md new file mode 100644 index 00000000..6f76a7be --- /dev/null +++ b/README.sp.md @@ -0,0 +1,743 @@ +Solo se garantiza que el archivo [README](README.md) original esté actualizado. + +# scrcpy (v1.17) + +Esta aplicación proporciona imagen y control de un dispositivo Android conectado +por USB (o [por TCP/IP][article-tcpip]). No requiere acceso _root_. +Compatible con _GNU/Linux_, _Windows_ y _macOS_. + +![screenshot](assets/screenshot-debian-600.jpg) + +Sus características principales son: + + - **ligero** (nativo, solo muestra la imagen del dispositivo) + - **desempeño** (30~60fps) + - **calidad** (1920×1080 o superior) + - **baja latencia** ([35~70ms][lowlatency]) + - **corto tiempo de inicio** (~1 segundo para mostrar la primera imagen) + - **no intrusivo** (no se deja nada instalado en el dispositivo) + +[lowlatency]: https://github.com/Genymobile/scrcpy/pull/646 + + +## Requisitos + +El dispositivo Android requiere como mínimo API 21 (Android 5.0). + +Asegurate de [habilitar el adb debugging][enable-adb] en tu(s) dispositivo(s). + +[enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling + +En algunos dispositivos, también necesitas habilitar [una opción adicional][control] para controlarlo con el teclado y ratón. + +[control]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323 + + +## Consigue la app + +Packaging status + +### Resumen + + - Linux: `apt install scrcpy` + - Windows: [download](README.md#windows) + - macOS: `brew install scrcpy` + +Construir desde la fuente: [BUILD] ([proceso simplificado][BUILD_simple]) + +[BUILD]: BUILD.md +[BUILD_simple]: BUILD.md#simple + + +### Linux + +En Debian (_test_ y _sid_ por ahora) y Ubuntu (20.04): + +``` +apt install scrcpy +``` + +Hay un paquete [Snap]: [`scrcpy`][snap-link]. + +[snap-link]: https://snapstats.org/snaps/scrcpy + +[snap]: https://en.wikipedia.org/wiki/Snappy_(package_manager) + +Para Fedora, hay un paquete [COPR]: [`scrcpy`][copr-link]. + +[COPR]: https://fedoraproject.org/wiki/Category:Copr +[copr-link]: https://copr.fedorainfracloud.org/coprs/zeno/scrcpy/ + +Para Arch Linux, hay un paquete [AUR]: [`scrcpy`][aur-link]. + +[AUR]: https://wiki.archlinux.org/index.php/Arch_User_Repository +[aur-link]: https://aur.archlinux.org/packages/scrcpy/ + +Para Gentoo, hay un paquete [Ebuild]: [`scrcpy/`][ebuild-link]. + +[Ebuild]: https://wiki.gentoo.org/wiki/Ebuild +[ebuild-link]: https://github.com/maggu2810/maggu2810-overlay/tree/master/app-mobilephone/scrcpy + +También puedes [construir la aplicación manualmente][BUILD] ([proceso simplificado][BUILD_simple]). + + +### Windows + +Para Windows, por simplicidad, hay un pre-compilado con todas las dependencias +(incluyendo `adb`): + + - [README](README.md#windows) + +También está disponible en [Chocolatey]: + +[Chocolatey]: https://chocolatey.org/ + +```bash +choco install scrcpy +choco install adb # si aún no está instalado +``` + +Y en [Scoop]: + +```bash +scoop install scrcpy +scoop install adb # si aún no está instalado +``` + +[Scoop]: https://scoop.sh + +También puedes [construir la aplicación manualmente][BUILD]. + + +### macOS + +La aplicación está disponible en [Homebrew]. Solo instalala: + +[Homebrew]: https://brew.sh/ + +```bash +brew install scrcpy +``` + +Necesitarás `adb`, accesible desde `PATH`. Si aún no lo tienes: + +```bash +brew install android-platform-tools +``` + +También está disponible en [MacPorts], que configurará el adb automáticamente: + +```bash +sudo port install scrcpy +``` + +[MacPorts]: https://www.macports.org/ + + +También puedes [construir la aplicación manualmente][BUILD]. + + +## Ejecutar + +Enchufa el dispositivo Android, y ejecuta: + +```bash +scrcpy +``` + +Acepta argumentos desde la línea de comandos, listados en: + +```bash +scrcpy --help +``` + +## Características + +### Capturar configuración + +#### Reducir la definición + +A veces es útil reducir la definición de la imagen del dispositivo Android para aumentar el desempeño. + +Para limitar el ancho y la altura a un valor específico (ej. 1024): + +```bash +scrcpy --max-size 1024 +scrcpy -m 1024 # versión breve +``` + +La otra dimensión es calculada para conservar el aspect ratio del dispositivo. +De esta forma, un dispositivo en 1920×1080 será transmitido a 1024×576. + + +#### Cambiar el bit-rate + +El bit-rate por defecto es 8 Mbps. Para cambiar el bit-rate del video (ej. a 2 Mbps): + +```bash +scrcpy --bit-rate 2M +scrcpy -b 2M # versión breve +``` + +#### Limitar los fps + +El fps puede ser limitado: + +```bash +scrcpy --max-fps 15 +``` + +Es oficialmente soportado desde Android 10, pero puede funcionar en versiones anteriores. + +#### Recortar + +La imagen del dispositivo puede ser recortada para transmitir solo una parte de la pantalla. + +Por ejemplo, puede ser útil para transmitir la imagen de un solo ojo del Oculus Go: + +```bash +scrcpy --crop 1224:1440:0:0 # 1224x1440 con coordenadas de origen en (0,0) +``` + +Si `--max-size` también está especificado, el cambio de tamaño es aplicado después de cortar. + + +#### Fijar la rotación del video + + +Para fijar la rotación de la transmisión: + +```bash +scrcpy --lock-video-orientation 0 # orientación normal +scrcpy --lock-video-orientation 1 # 90° contrarreloj +scrcpy --lock-video-orientation 2 # 180° +scrcpy --lock-video-orientation 3 # 90° sentido de las agujas del reloj +``` + +Esto afecta la rotación de la grabación. + +La [ventana también puede ser rotada](#rotación) independientemente. + + +#### Codificador + +Algunos dispositivos pueden tener más de una rotación, y algunos pueden causar problemas o errores. Es posible seleccionar un codificador diferente: + +```bash +scrcpy --encoder OMX.qcom.video.encoder.avc +``` + +Para listar los codificadores disponibles, puedes pasar un nombre de codificador inválido, el error te dará los codificadores disponibles: + +```bash +scrcpy --encoder _ +``` + +### Grabación + +Es posible grabar la pantalla mientras se transmite: + +```bash +scrcpy --record file.mp4 +scrcpy -r file.mkv +``` + +Para grabar sin transmitir la pantalla: + +```bash +scrcpy --no-display --record file.mp4 +scrcpy -Nr file.mkv +# interrumpe la grabación con Ctrl+C +``` + +"Skipped frames" son grabados, incluso si no son mostrados en tiempo real (por razones de desempeño). Los frames tienen _marcas de tiempo_ en el dispositivo, por lo que el "[packet delay +variation]" no impacta el archivo grabado. + +[packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation + + +### Conexión + +#### Inalámbrica + +_Scrcpy_ usa `adb` para comunicarse con el dispositivo, y `adb` puede [conectarse] vía TCP/IP: + +1. Conecta el dispositivo al mismo Wi-Fi que tu computadora. +2. Obtén la dirección IP del dispositivo, en Ajustes → Acerca del dispositivo → Estado, o ejecutando este comando: + + ```bash + adb shell ip route | awk '{print $9}' + ``` + +3. Habilita adb vía TCP/IP en el dispositivo: `adb tcpip 5555`. +4. Desenchufa el dispositivo. +5. Conéctate a tu dispositivo: `adb connect IP_DEL_DISPOSITIVO:5555` _(reemplaza `IP_DEL_DISPOSITIVO`)_. +6. Ejecuta `scrcpy` con normalidad. + +Podría resultar útil reducir el bit-rate y la definición: + +```bash +scrcpy --bit-rate 2M --max-size 800 +scrcpy -b2M -m800 # versión breve +``` + +[conectarse]: https://developer.android.com/studio/command-line/adb.html#wireless + + +#### Múltiples dispositivos + +Si hay muchos dispositivos listados en `adb devices`, será necesario especificar el _número de serie_: + +```bash +scrcpy --serial 0123456789abcdef +scrcpy -s 0123456789abcdef # versión breve +``` + +Si el dispositivo está conectado por TCP/IP: + +```bash +scrcpy --serial 192.168.0.1:5555 +scrcpy -s 192.168.0.1:5555 # versión breve +``` + +Puedes iniciar múltiples instancias de _scrcpy_ para múltiples dispositivos. + +#### Autoiniciar al detectar dispositivo + +Puedes utilizar [AutoAdb]: + +```bash +autoadb scrcpy -s '{}' +``` + +[AutoAdb]: https://github.com/rom1v/autoadb + +#### Túnel SSH + +Para conectarse a un dispositivo remoto, es posible conectar un cliente local de `adb` a un servidor remoto `adb` (siempre y cuando utilicen la misma versión de protocolos _adb_): + +```bash +adb kill-server # cierra el servidor local adb en 5037 +ssh -CN -L5037:localhost:5037 -R27183:localhost:27183 your_remote_computer +# conserva este servidor abierto +``` + +Desde otra terminal: + +```bash +scrcpy +``` + +Para evitar habilitar "remote port forwarding", puedes forzar una "forward connection" (nótese el argumento `-L` en vez de `-R`): + +```bash +adb kill-server # cierra el servidor local adb en 5037 +ssh -CN -L5037:localhost:5037 -L27183:localhost:27183 your_remote_computer +# conserva este servidor abierto +``` + +Desde otra terminal: + +```bash +scrcpy --force-adb-forward +``` + + +Al igual que las conexiones inalámbricas, puede resultar útil reducir la calidad: + +``` +scrcpy -b2M -m800 --max-fps 15 +``` + +### Configuración de la ventana + +#### Título + +Por defecto, el título de la ventana es el modelo del dispositivo. Puede ser modificado: + +```bash +scrcpy --window-title 'My device' +``` + +#### Posición y tamaño + +La posición y tamaño inicial de la ventana puede ser especificado: + +```bash +scrcpy --window-x 100 --window-y 100 --window-width 800 --window-height 600 +``` + +#### Sin bordes + +Para deshabilitar el diseño de la ventana: + +```bash +scrcpy --window-borderless +``` + +#### Siempre adelante + +Para mantener la ventana de scrcpy siempre adelante: + +```bash +scrcpy --always-on-top +``` + +#### Pantalla completa + +La aplicación puede ser iniciada en pantalla completa: + +```bash +scrcpy --fullscreen +scrcpy -f # versión breve +``` + +Puede entrar y salir de la pantalla completa con la combinación MOD+f. + +#### Rotación + +Se puede rotar la ventana: + +```bash +scrcpy --rotation 1 +``` + +Los valores posibles son: + - `0`: sin rotación + - `1`: 90 grados contrarreloj + - `2`: 180 grados + - `3`: 90 grados en sentido de las agujas del reloj + +La rotación también puede ser modificada con la combinación de teclas MOD+ _(izquierda)_ y MOD+ _(derecha)_. + +Nótese que _scrcpy_ maneja 3 diferentes rotaciones: + - MOD+r solicita al dispositivo cambiar entre vertical y horizontal (la aplicación en uso puede rechazarlo si no soporta la orientación solicitada). + - [`--lock-video-orientation`](#fijar-la-rotación-del-video) cambia la rotación de la transmisión (la orientación del video enviado a la PC). Esto afecta a la grabación. + - `--rotation` (o MOD+/MOD+) rota solo el contenido de la imagen. Esto solo afecta a la imagen mostrada, no a la grabación. + + +### Otras opciones menores + +#### Solo lectura ("Read-only") + +Para deshabilitar los controles (todo lo que interactúe con el dispositivo: eventos del teclado, eventos del mouse, arrastrar y soltar archivos): + +```bash +scrcpy --no-control +scrcpy -n # versión breve +``` + +#### Pantalla + +Si múltiples pantallas están disponibles, es posible elegir cual transmitir: + +```bash +scrcpy --display 1 +``` + +Los ids de las pantallas se pueden obtener con el siguiente comando: + +```bash +adb shell dumpsys display # busque "mDisplayId=" en la respuesta +``` + +La segunda pantalla solo puede ser manejada si el dispositivo cuenta con Android 10 (en caso contrario será transmitida en el modo solo lectura). + + +#### Permanecer activo + +Para evitar que el dispositivo descanse después de un tiempo mientras está conectado: + +```bash +scrcpy --stay-awake +scrcpy -w # versión breve +``` + +La configuración original se restaura al cerrar scrcpy. + + +#### Apagar la pantalla + +Es posible apagar la pantalla mientras se transmite al iniciar con el siguiente comando: + +```bash +scrcpy --turn-screen-off +scrcpy -S # versión breve +``` + +O presionando MOD+o en cualquier momento. + +Para volver a prenderla, presione MOD+Shift+o. + +En Android, el botón de `POWER` siempre prende la pantalla. Por conveniencia, si `POWER` es enviado vía scrcpy (con click-derecho o MOD+p), esto forzará a apagar la pantalla con un poco de atraso (en la mejor de las situaciones). El botón físico `POWER` seguirá prendiendo la pantalla. + +También puede resultar útil para evitar que el dispositivo entre en inactividad: + +```bash +scrcpy --turn-screen-off --stay-awake +scrcpy -Sw # versión breve +``` + + +#### Renderizar frames vencidos + +Por defecto, para minimizar la latencia, _scrcpy_ siempre renderiza el último frame disponible decodificado, e ignora cualquier frame anterior. + +Para forzar el renderizado de todos los frames (a costo de posible aumento de latencia), use: + +```bash +scrcpy --render-expired-frames +``` + +#### Mostrar clicks + +Para presentaciones, puede resultar útil mostrar los clicks físicos (en el dispositivo físicamente). + +Android provee esta opción en _Opciones para desarrolladores_. + +_Scrcpy_ provee una opción para habilitar esta función al iniciar la aplicación y restaurar el valor original al salir: + +```bash +scrcpy --show-touches +scrcpy -t # versión breve +``` + +Nótese que solo muestra los clicks _físicos_ (con el dedo en el dispositivo). + + +#### Desactivar protector de pantalla + +Por defecto, scrcpy no evita que el protector de pantalla se active en la computadora. + +Para deshabilitarlo: + +```bash +scrcpy --disable-screensaver +``` + + +### Control + +#### Rotar pantalla del dispositivo + +Presione MOD+r para cambiar entre posición vertical y horizontal. + +Nótese que solo rotará si la aplicación activa soporta la orientación solicitada. + +#### Copiar y pegar + +Cuando que el portapapeles de Android cambia, automáticamente se sincroniza al portapapeles de la computadora. + +Cualquier shortcut con Ctrl es enviado al dispositivo. En particular: + - Ctrl+c normalmente copia + - Ctrl+x normalmente corta + - Ctrl+v normalmente pega (después de la sincronización de portapapeles entre la computadora y el dispositivo) + +Esto normalmente funciona como es esperado. + +Sin embargo, este comportamiento depende de la aplicación en uso. Por ejemplo, _Termux_ envía SIGINT con Ctrl+c, y _K-9 Mail_ crea un nuevo mensaje. + +Para copiar, cortar y pegar, en tales casos (solo soportado en Android >= 7): + - MOD+c inyecta `COPY` + - MOD+x inyecta `CUT` + - MOD+v inyecta `PASTE` (después de la sincronización de portapapeles entre la computadora y el dispositivo) + +Además, MOD+Shift+v permite inyectar el texto en el portapapeles de la computadora como una secuencia de teclas. Esto es útil cuando el componente no acepta pegado de texto (por ejemplo en _Termux_), pero puede romper caracteres no pertenecientes a ASCII. + +**AVISO:** Pegar de la computadora al dispositivo (tanto con Ctrl+v o MOD+v) copia el contenido al portapapeles del dispositivo. Como consecuencia, cualquier aplicación de Android puede leer su contenido. Debería evitar pegar contenido sensible (como contraseñas) de esta forma. + +Algunos dispositivos no se comportan como es esperado al establecer el portapapeles programáticamente. La opción `--legacy-paste` está disponible para cambiar el comportamiento de Ctrl+v y MOD+v para que también inyecten el texto del portapapeles de la computadora como una secuencia de teclas (de la misma forma que MOD+Shift+v). + +#### Pellizcar para zoom + +Para simular "pinch-to-zoom": Ctrl+_click-y-mover_. + +Más precisamente, mantén Ctrl mientras presionas botón izquierdo. Hasta que no se suelte el botón, todos los movimientos del mouse cambiarán el tamaño y rotación del contenido (si es soportado por la app en uso) respecto al centro de la pantalla. + +Concretamente, scrcpy genera clicks adicionales con un "dedo virtual" en la posición invertida respecto al centro de la pantalla. + + +#### Preferencias de inyección de texto + +Existen dos tipos de [eventos][textevents] generados al escribir texto: + - _key events_, marcando si la tecla es presionada o soltada; + - _text events_, marcando si un texto fue introducido. + +Por defecto, las letras son inyectadas usando _key events_, para que el teclado funcione como es esperado en juegos (típicamente las teclas WASD). + +Pero esto puede [causar problemas][prefertext]. Si encuentras tales problemas, los puedes evitar con: + +```bash +scrcpy --prefer-text +``` + +(Pero esto romperá el comportamiento del teclado en los juegos) + +[textevents]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-text-input +[prefertext]: https://github.com/Genymobile/scrcpy/issues/650#issuecomment-512945343 + + +#### Repetir tecla + +Por defecto, mantener una tecla presionada genera múltiples _key events_. Esto puede causar problemas de desempeño en algunos juegos, donde estos eventos no tienen sentido de todos modos. + +Para evitar enviar _key events_ repetidos: + +```bash +scrcpy --no-key-repeat +``` + + +#### Botón derecho y botón del medio + +Por defecto, botón derecho ejecuta RETROCEDER (o ENCENDIDO) y botón del medio INICIO. Para inhabilitar estos atajos y enviar los clicks al dispositivo: + +```bash +scrcpy --forward-all-clicks +``` + + +### Arrastrar y soltar archivos + +#### Instalar APKs + +Para instalar un APK, arrastre y suelte el archivo APK (terminado en `.apk`) a la ventana de _scrcpy_. + +No hay respuesta visual, un mensaje se escribirá en la consola. + + +#### Enviar archivos al dispositivo + +Para enviar un archivo a `/sdcard/` en el dispositivo, arrastre y suelte un archivo (no APK) a la ventana de _scrcpy_. + +No hay respuesta visual, un mensaje se escribirá en la consola. + +El directorio de destino puede ser modificado al iniciar: + +```bash +scrcpy --push-target=/sdcard/Download/ +``` + + +### Envío de Audio + +_Scrcpy_ no envía el audio. Use [sndcpy]. + +También lea [issue #14]. + +[sndcpy]: https://github.com/rom1v/sndcpy +[issue #14]: https://github.com/Genymobile/scrcpy/issues/14 + + +## Atajos + +En la siguiente lista, MOD es el atajo modificador. Por defecto es Alt (izquierdo) o Super (izquierdo). + +Se puede modificar usando `--shortcut-mod`. Las posibles teclas son `lctrl` (izquierdo), `rctrl` (derecho), `lalt` (izquierdo), `ralt` (derecho), `lsuper` (izquierdo) y `rsuper` (derecho). Por ejemplo: + +```bash +# use RCtrl para los atajos +scrcpy --shortcut-mod=rctrl + +# use tanto LCtrl+LAlt o LSuper para los atajos +scrcpy --shortcut-mod=lctrl+lalt,lsuper +``` + +_[Super] es generalmente la tecla Windows o Cmd._ + +[Super]: https://en.wikipedia.org/wiki/Super_key_(keyboard_button) + + | Acción | Atajo + | ------------------------------------------- |:----------------------------- + | Alterne entre pantalla compelta | MOD+f + | Rotar pantalla hacia la izquierda | MOD+ _(izquierda)_ + | Rotar pantalla hacia la derecha | MOD+ _(derecha)_ + | Ajustar ventana a 1:1 ("pixel-perfect") | MOD+g + | Ajustar ventana para quitar los bordes negros| MOD+w \| _Doble click¹_ + | Click en `INICIO` | MOD+h \| _Botón del medio_ + | Click en `RETROCEDER` | MOD+b \| _Botón derecho²_ + | Click en `CAMBIAR APLICACIÓN` | MOD+s + | Click en `MENÚ` (desbloquear pantalla) | MOD+m + | Click en `SUBIR VOLUMEN` | MOD+ _(arriba)_ + | Click en `BAJAR VOLUME` | MOD+ _(abajo)_ + | Click en `ENCENDIDO` | MOD+p + | Encendido | _Botón derecho²_ + | Apagar pantalla (manteniendo la transmisión)| MOD+o + | Encender pantalla | MOD+Shift+o + | Rotar pantalla del dispositivo | MOD+r + | Abrir panel de notificaciones | MOD+n + | Cerrar panel de notificaciones | MOD+Shift+n + | Copiar al portapapeles³ | MOD+c + | Cortar al portapapeles³ | MOD+x + | Synchronizar portapapeles y pegar³ | MOD+v + | inyectar texto del portapapeles de la PC | MOD+Shift+v + | Habilitar/Deshabilitar contador de FPS (en stdout) | MOD+i + | Pellizcar para zoom | Ctrl+_click-y-mover_ + +_¹Doble click en los bordes negros para eliminarlos._ +_²Botón derecho enciende la pantalla si estaba apagada, sino ejecuta RETROCEDER._ +_³Solo en Android >= 7._ + +Todos los atajos Ctrl+_tecla_ son enviados al dispositivo para que sean manejados por la aplicación activa. + + +## Path personalizado + +Para usar un binario de _adb_ en particular, configure el path `ADB` en las variables de entorno: + +```bash +ADB=/path/to/adb scrcpy +``` + +Para sobreescribir el path del archivo `scrcpy-server`, configure el path en `SCRCPY_SERVER_PATH`. + + +## ¿Por qué _scrcpy_? + +Un colega me retó a encontrar un nombre tan impronunciable como [gnirehtet]. + +[`strcpy`] copia un **str**ing; `scrcpy` copia un **scr**een. + +[gnirehtet]: https://github.com/Genymobile/gnirehtet +[`strcpy`]: http://man7.org/linux/man-pages/man3/strcpy.3.html + + +## ¿Cómo construir (BUILD)? + +Véase [BUILD] (en inglés). + + +## Problemas generales + +Vea las [preguntas frecuentes (en inglés)](FAQ.md). + + +## Desarrolladores + +Lea la [hoja de desarrolladores (en inglés)](DEVELOP.md). + + +## Licencia + + Copyright (C) 2018 Genymobile + Copyright (C) 2018-2021 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. + +## Artículos + +- [Introducing scrcpy][article-intro] (en inglés) +- [Scrcpy now works wirelessly][article-tcpip] (en inglés) + +[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 4c31911df2cfb5ae7d918894df394f8a7a317e23 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 17 May 2021 09:41:22 +0200 Subject: [PATCH 0603/2244] Pass serial within struct server_params This was the only option passed separately. --- app/src/scrcpy.c | 3 ++- app/src/server.c | 9 ++++----- app/src/server.h | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 8418353d..29ebe129 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -256,6 +256,7 @@ scrcpy(const struct scrcpy_options *options) { bool record = !!options->record_filename; struct server_params params = { + .serial = options->serial, .log_level = options->log_level, .crop = options->crop, .port_range = options->port_range, @@ -272,7 +273,7 @@ scrcpy(const struct scrcpy_options *options) { .force_adb_forward = options->force_adb_forward, .power_off_on_close = options->power_off_on_close, }; - if (!server_start(&server, options->serial, ¶ms)) { + if (!server_start(&server, ¶ms)) { goto end; } diff --git a/app/src/server.c b/app/src/server.c index 646006d6..41e8166c 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -407,16 +407,15 @@ run_wait_server(void *data) { } bool -server_start(struct server *server, const char *serial, - const struct server_params *params) { - if (serial) { - server->serial = strdup(serial); +server_start(struct server *server, const struct server_params *params) { + if (params->serial) { + server->serial = strdup(params->serial); if (!server->serial) { return false; } } - if (!push_server(serial)) { + if (!push_server(params->serial)) { /* server->serial will be freed on server_destroy() */ return false; } diff --git a/app/src/server.h b/app/src/server.h index ad49a982..c249b374 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -33,6 +33,7 @@ struct server { }; struct server_params { + const char *serial; enum sc_log_level log_level; const char *crop; const char *codec_options; @@ -56,8 +57,7 @@ server_init(struct server *server); // push, enable tunnel et start the server bool -server_start(struct server *server, const char *serial, - const struct server_params *params); +server_start(struct server *server, const struct server_params *params); #define DEVICE_NAME_FIELD_LENGTH 64 // block until the communication with the server is established From 6fd7e89da1d3982697af9c581b322a1ca4398621 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 28 May 2021 21:23:10 +0200 Subject: [PATCH 0604/2244] Explicitly initialize decoder sink_count The zero-initialization relied on the fact that the decoder instance is static. --- app/src/decoder.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/decoder.c b/app/src/decoder.c index dbaa3d39..aa5018b3 100644 --- a/app/src/decoder.c +++ b/app/src/decoder.c @@ -140,6 +140,8 @@ decoder_packet_sink_push(struct sc_packet_sink *sink, const AVPacket *packet) { void decoder_init(struct decoder *decoder) { + decoder->sink_count = 0; + static const struct sc_packet_sink_ops ops = { .open = decoder_packet_sink_open, .close = decoder_packet_sink_close, From 506f918fb7cc944e08b108ff357c70d26bed90d2 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 28 May 2021 21:29:14 +0200 Subject: [PATCH 0605/2244] Group components into struct scrcpy This avoids to refer to many structs globally. --- app/src/scrcpy.c | 108 +++++++++++++++++++++++++---------------------- 1 file changed, 57 insertions(+), 51 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 29ebe129..17902156 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -29,17 +29,19 @@ # include "v4l2_sink.h" #endif -static struct server server; -static struct screen screen; -static struct stream stream; -static struct decoder decoder; -static struct recorder recorder; +struct scrcpy { + struct server server; + struct screen screen; + struct stream stream; + struct decoder decoder; + struct recorder recorder; #ifdef HAVE_V4L2 -static struct sc_v4l2_sink v4l2_sink; + struct sc_v4l2_sink v4l2_sink; #endif -static struct controller controller; -static struct file_handler file_handler; -static struct input_manager input_manager; + struct controller controller; + struct file_handler file_handler; + struct input_manager input_manager; +}; #ifdef _WIN32 BOOL WINAPI windows_ctrl_handler(DWORD ctrl_type) { @@ -129,7 +131,8 @@ enum event_result { }; static enum event_result -handle_event(SDL_Event *event, const struct scrcpy_options *options) { +handle_event(struct scrcpy *s, const struct scrcpy_options *options, + SDL_Event *event) { switch (event->type) { case EVENT_STREAM_STOPPED: LOGD("Video stream stopped"); @@ -154,17 +157,17 @@ handle_event(SDL_Event *event, const struct scrcpy_options *options) { } else { action = ACTION_PUSH_FILE; } - file_handler_request(&file_handler, action, file); + file_handler_request(&s->file_handler, action, file); goto end; } } - bool consumed = screen_handle_event(&screen, event); + bool consumed = screen_handle_event(&s->screen, event); if (consumed) { goto end; } - consumed = input_manager_handle_event(&input_manager, event); + consumed = input_manager_handle_event(&s->input_manager, event); (void) consumed; end: @@ -172,10 +175,10 @@ end: } static bool -event_loop(const struct scrcpy_options *options) { +event_loop(struct scrcpy *s, const struct scrcpy_options *options) { SDL_Event event; while (SDL_WaitEvent(&event)) { - enum event_result result = handle_event(&event, options); + enum event_result result = handle_event(s, options, &event); switch (result) { case EVENT_RESULT_STOPPED_BY_USER: return true; @@ -237,7 +240,10 @@ stream_on_eos(struct stream *stream, void *userdata) { bool scrcpy(const struct scrcpy_options *options) { - if (!server_init(&server)) { + static struct scrcpy scrcpy; + struct scrcpy *s = &scrcpy; + + if (!server_init(&s->server)) { return false; } @@ -273,7 +279,7 @@ scrcpy(const struct scrcpy_options *options) { .force_adb_forward = options->force_adb_forward, .power_off_on_close = options->power_off_on_close, }; - if (!server_start(&server, ¶ms)) { + if (!server_start(&s->server, ¶ms)) { goto end; } @@ -287,12 +293,12 @@ scrcpy(const struct scrcpy_options *options) { char device_name[DEVICE_NAME_FIELD_LENGTH]; struct size frame_size; - if (!server_connect_to(&server, device_name, &frame_size)) { + if (!server_connect_to(&s->server, device_name, &frame_size)) { goto end; } if (options->display && options->control) { - if (!file_handler_init(&file_handler, server.serial, + if (!file_handler_init(&s->file_handler, s->server.serial, options->push_target)) { goto end; } @@ -305,19 +311,19 @@ scrcpy(const struct scrcpy_options *options) { needs_decoder |= !!options->v4l2_device; #endif if (needs_decoder) { - decoder_init(&decoder); - dec = &decoder; + decoder_init(&s->decoder); + dec = &s->decoder; } struct recorder *rec = NULL; if (record) { - if (!recorder_init(&recorder, + if (!recorder_init(&s->recorder, options->record_filename, options->record_format, frame_size)) { goto end; } - rec = &recorder; + rec = &s->recorder; recorder_initialized = true; } @@ -326,24 +332,24 @@ scrcpy(const struct scrcpy_options *options) { const struct stream_callbacks stream_cbs = { .on_eos = stream_on_eos, }; - stream_init(&stream, server.video_socket, &stream_cbs, NULL); + stream_init(&s->stream, s->server.video_socket, &stream_cbs, NULL); if (dec) { - stream_add_sink(&stream, &dec->packet_sink); + stream_add_sink(&s->stream, &dec->packet_sink); } if (rec) { - stream_add_sink(&stream, &rec->packet_sink); + stream_add_sink(&s->stream, &rec->packet_sink); } if (options->display) { if (options->control) { - if (!controller_init(&controller, server.control_socket)) { + if (!controller_init(&s->controller, s->server.control_socket)) { goto end; } controller_initialized = true; - if (!controller_start(&controller)) { + if (!controller_start(&s->controller)) { goto end; } controller_started = true; @@ -366,19 +372,19 @@ scrcpy(const struct scrcpy_options *options) { .fullscreen = options->fullscreen, }; - if (!screen_init(&screen, &screen_params)) { + if (!screen_init(&s->screen, &screen_params)) { goto end; } screen_initialized = true; - decoder_add_sink(&decoder, &screen.frame_sink); + decoder_add_sink(&s->decoder, &s->screen.frame_sink); if (options->turn_screen_off) { struct control_msg msg; msg.type = CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE; msg.set_screen_power_mode.mode = SCREEN_POWER_MODE_OFF; - if (!controller_push_msg(&controller, &msg)) { + if (!controller_push_msg(&s->controller, &msg)) { LOGW("Could not request 'set screen power mode'"); } } @@ -386,11 +392,11 @@ scrcpy(const struct scrcpy_options *options) { #ifdef HAVE_V4L2 if (options->v4l2_device) { - if (!sc_v4l2_sink_init(&v4l2_sink, options->v4l2_device, frame_size)) { + if (!sc_v4l2_sink_init(&s->v4l2_sink, options->v4l2_device, frame_size)) { goto end; } - decoder_add_sink(&decoder, &v4l2_sink.frame_sink); + decoder_add_sink(&s->decoder, &s->v4l2_sink.frame_sink); v4l2_sink_initialized = true; } @@ -398,74 +404,74 @@ scrcpy(const struct scrcpy_options *options) { // now we consumed the header values, the socket receives the video stream // start the stream - if (!stream_start(&stream)) { + if (!stream_start(&s->stream)) { goto end; } stream_started = true; - input_manager_init(&input_manager, &controller, &screen, options); + input_manager_init(&s->input_manager, &s->controller, &s->screen, options); - ret = event_loop(options); + ret = event_loop(s, options); LOGD("quit..."); // Close the window immediately on closing, because screen_destroy() may // only be called once the stream thread is joined (it may take time) - screen_hide_window(&screen); + screen_hide_window(&s->screen); end: // The stream is not stopped explicitly, because it will stop by itself on // end-of-stream if (controller_started) { - controller_stop(&controller); + controller_stop(&s->controller); } if (file_handler_initialized) { - file_handler_stop(&file_handler); + file_handler_stop(&s->file_handler); } if (screen_initialized) { - screen_interrupt(&screen); + screen_interrupt(&s->screen); } if (server_started) { // shutdown the sockets and kill the server - server_stop(&server); + server_stop(&s->server); } // now that the sockets are shutdown, the stream and controller are // interrupted, we can join them if (stream_started) { - stream_join(&stream); + stream_join(&s->stream); } #ifdef HAVE_V4L2 if (v4l2_sink_initialized) { - sc_v4l2_sink_destroy(&v4l2_sink); + sc_v4l2_sink_destroy(&s->v4l2_sink); } #endif // Destroy the screen only after the stream is guaranteed to be finished, // because otherwise the screen could receive new frames after destruction if (screen_initialized) { - screen_join(&screen); - screen_destroy(&screen); + screen_join(&s->screen); + screen_destroy(&s->screen); } if (controller_started) { - controller_join(&controller); + controller_join(&s->controller); } if (controller_initialized) { - controller_destroy(&controller); + controller_destroy(&s->controller); } if (recorder_initialized) { - recorder_destroy(&recorder); + recorder_destroy(&s->recorder); } if (file_handler_initialized) { - file_handler_join(&file_handler); - file_handler_destroy(&file_handler); + file_handler_join(&s->file_handler); + file_handler_destroy(&s->file_handler); } - server_destroy(&server); + server_destroy(&s->server); return ret; } From 969bfd43744a88109d369089f9ee1313211ac9e1 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 1 May 2021 00:18:55 +0200 Subject: [PATCH 0606/2244] Serialize clean-up configuration This avoids to pass each option as individual parameter and parse them manually (it's still "manual" in the Parcelable implementation). Refs #824 Reviewed-by: Yu-Chen Lin --- .../java/com/genymobile/scrcpy/CleanUp.java | 138 +++++++++++++++--- .../java/com/genymobile/scrcpy/Server.java | 2 +- 2 files changed, 119 insertions(+), 21 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java index ccdc9fd8..ec61a1c0 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java +++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java @@ -3,6 +3,10 @@ package com.genymobile.scrcpy; import com.genymobile.scrcpy.wrappers.ContentProvider; import com.genymobile.scrcpy.wrappers.ServiceManager; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Base64; + import java.io.File; import java.io.IOException; @@ -15,25 +19,123 @@ public final class CleanUp { public static final String SERVER_PATH = "/data/local/tmp/scrcpy-server.jar"; + // A simple struct to be passed from the main process to the cleanup process + public static class Config implements Parcelable { + + public static final Creator CREATOR = new Creator() { + @Override + public Config createFromParcel(Parcel in) { + return new Config(in); + } + + @Override + public Config[] newArray(int size) { + return new Config[size]; + } + }; + + private static final int FLAG_DISABLE_SHOW_TOUCHES = 1; + private static final int FLAG_RESTORE_NORMAL_POWER_MODE = 2; + private static final int FLAG_POWER_OFF_SCREEN = 4; + + private int displayId; + + // Restore the value (between 0 and 7), -1 to not restore + // + private int restoreStayOn = -1; + + private boolean disableShowTouches; + private boolean restoreNormalPowerMode; + private boolean powerOffScreen; + + public Config() { + // Default constructor, the fields are initialized by CleanUp.configure() + } + + protected Config(Parcel in) { + displayId = in.readInt(); + restoreStayOn = in.readInt(); + byte options = in.readByte(); + disableShowTouches = (options & FLAG_DISABLE_SHOW_TOUCHES) != 0; + restoreNormalPowerMode = (options & FLAG_RESTORE_NORMAL_POWER_MODE) != 0; + powerOffScreen = (options & FLAG_POWER_OFF_SCREEN) != 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(displayId); + dest.writeInt(restoreStayOn); + byte options = 0; + if (disableShowTouches) { + options |= FLAG_DISABLE_SHOW_TOUCHES; + } + if (restoreNormalPowerMode) { + options |= FLAG_RESTORE_NORMAL_POWER_MODE; + } + if (powerOffScreen) { + options |= FLAG_POWER_OFF_SCREEN; + } + dest.writeByte(options); + } + + private boolean hasWork() { + return disableShowTouches || restoreStayOn != -1 || restoreNormalPowerMode || powerOffScreen; + } + + @Override + public int describeContents() { + return 0; + } + + byte[] serialize() { + Parcel parcel = Parcel.obtain(); + writeToParcel(parcel, 0); + byte[] bytes = parcel.marshall(); + parcel.recycle(); + return bytes; + } + + static Config deserialize(byte[] bytes) { + Parcel parcel = Parcel.obtain(); + parcel.unmarshall(bytes, 0, bytes.length); + parcel.setDataPosition(0); + return CREATOR.createFromParcel(parcel); + } + + static Config fromBase64(String base64) { + byte[] bytes = Base64.decode(base64, Base64.NO_WRAP); + return deserialize(bytes); + } + + String toBase64() { + byte[] bytes = serialize(); + return Base64.encodeToString(bytes, Base64.NO_WRAP); + } + } + private CleanUp() { // not instantiable } - public static void configure(boolean disableShowTouches, int restoreStayOn, boolean restoreNormalPowerMode, boolean powerOffScreen, int displayId) + public static void configure(int displayId, int restoreStayOn, boolean disableShowTouches, boolean restoreNormalPowerMode, boolean powerOffScreen) throws IOException { - boolean needProcess = disableShowTouches || restoreStayOn != -1 || restoreNormalPowerMode || powerOffScreen; - if (needProcess) { - startProcess(disableShowTouches, restoreStayOn, restoreNormalPowerMode, powerOffScreen, displayId); + Config config = new Config(); + config.displayId = displayId; + config.disableShowTouches = disableShowTouches; + config.restoreStayOn = restoreStayOn; + config.restoreNormalPowerMode = restoreNormalPowerMode; + config.powerOffScreen = powerOffScreen; + + if (config.hasWork()) { + startProcess(config); } else { // There is no additional clean up to do when scrcpy dies unlinkSelf(); } } - private static void startProcess(boolean disableShowTouches, int restoreStayOn, boolean restoreNormalPowerMode, boolean powerOffScreen, - int displayId) throws IOException { - String[] cmd = {"app_process", "/", CleanUp.class.getName(), String.valueOf(disableShowTouches), String.valueOf( - restoreStayOn), String.valueOf(restoreNormalPowerMode), String.valueOf(powerOffScreen), String.valueOf(displayId)}; + private static void startProcess(Config config) throws IOException { + String[] cmd = {"app_process", "/", CleanUp.class.getName(), config.toBase64()}; ProcessBuilder builder = new ProcessBuilder(cmd); builder.environment().put("CLASSPATH", SERVER_PATH); @@ -60,31 +162,27 @@ public final class CleanUp { Ln.i("Cleaning up"); - boolean disableShowTouches = Boolean.parseBoolean(args[0]); - int restoreStayOn = Integer.parseInt(args[1]); - boolean restoreNormalPowerMode = Boolean.parseBoolean(args[2]); - boolean powerOffScreen = Boolean.parseBoolean(args[3]); - int displayId = Integer.parseInt(args[4]); + Config config = Config.fromBase64(args[0]); - if (disableShowTouches || restoreStayOn != -1) { + if (config.disableShowTouches || config.restoreStayOn != -1) { ServiceManager serviceManager = new ServiceManager(); try (ContentProvider settings = serviceManager.getActivityManager().createSettingsProvider()) { - if (disableShowTouches) { + if (config.disableShowTouches) { Ln.i("Disabling \"show touches\""); settings.putValue(ContentProvider.TABLE_SYSTEM, "show_touches", "0"); } - if (restoreStayOn != -1) { + if (config.restoreStayOn != -1) { Ln.i("Restoring \"stay awake\""); - settings.putValue(ContentProvider.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(restoreStayOn)); + settings.putValue(ContentProvider.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(config.restoreStayOn)); } } } if (Device.isScreenOn()) { - if (powerOffScreen) { + if (config.powerOffScreen) { Ln.i("Power off screen"); - Device.powerOffScreen(displayId); - } else if (restoreNormalPowerMode) { + Device.powerOffScreen(config.displayId); + } else if (config.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 674f314b..fdd9db88 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -50,7 +50,7 @@ public final class Server { } } - CleanUp.configure(mustDisableShowTouchesOnCleanUp, restoreStayOn, true, options.getPowerOffScreenOnClose(), options.getDisplayId()); + CleanUp.configure(options.getDisplayId(), restoreStayOn, mustDisableShowTouchesOnCleanUp, true, options.getPowerOffScreenOnClose()); boolean tunnelForward = options.isTunnelForward(); From f7533e8896c5e362af9a2f31b2b8d849d3207cc2 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 19 Feb 2021 17:11:44 +0100 Subject: [PATCH 0607/2244] Use non-secure display for Android >= 12 Since Android 12, secure displays could not be created with shell permissions anymore. Refs commit 1fdde490fd2a0b89680a2b5da5e5274192398023 Fixes #2129 --- .../src/main/java/com/genymobile/scrcpy/ScreenEncoder.java | 5 ++++- 1 file changed, 4 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 c7c104e4..9a149241 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -7,6 +7,7 @@ import android.media.MediaCodec; import android.media.MediaCodecInfo; import android.media.MediaCodecList; import android.media.MediaFormat; +import android.os.Build; import android.os.IBinder; import android.view.Surface; @@ -225,7 +226,9 @@ public class ScreenEncoder implements Device.RotationListener { } private static IBinder createDisplay() { - return SurfaceControl.createDisplay("scrcpy", true); + // Since Android 12, secure displays could not be created with shell permissions anymore + boolean secure = Build.VERSION.SDK_INT <= Build.VERSION_CODES.R; + return SurfaceControl.createDisplay("scrcpy", secure); } private static void configure(MediaCodec codec, MediaFormat format) { From 16a63e0917e423fb417eaad3ae0ca05f16227db6 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 11 Jun 2021 10:02:52 +0200 Subject: [PATCH 0608/2244] Use non-secure display for Android 12 preview Android 12 preview identifies as Android 11, but its codename is "S". Refs #2129 --- .../src/main/java/com/genymobile/scrcpy/ScreenEncoder.java | 6 ++++-- 1 file changed, 4 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 9a149241..2f7109c5 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -226,8 +226,10 @@ public class ScreenEncoder implements Device.RotationListener { } private static IBinder createDisplay() { - // Since Android 12, secure displays could not be created with shell permissions anymore - boolean secure = Build.VERSION.SDK_INT <= Build.VERSION_CODES.R; + // Since Android 12 (preview), secure displays could not be created with shell permissions anymore. + // On Android 12 preview, SDK_INT is still R (not S), but CODENAME is "S". + boolean secure = Build.VERSION.SDK_INT < Build.VERSION_CODES.R || (Build.VERSION.SDK_INT == Build.VERSION_CODES.R && !"S" + .equals(Build.VERSION.CODENAME)); return SurfaceControl.createDisplay("scrcpy", secure); } From f76fe2c0d4847d0e40d8708f97591abf3fa22ea5 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 11 Jun 2021 18:39:23 +0200 Subject: [PATCH 0609/2244] Fix --lock-video-orientation syntax The argument for option --lock-video-orientation has been made optional by 5af9d0ee0faddfca689f468c7d463c86233e8d93. With getopt_long(), contrary to mandatory arguments, optional arguments must be given with a '=': --lock-video-orientation 2 # wrong, parse error --lock-video-orientation=2 # correct --- README.md | 8 ++++---- app/tests/test_cli.c | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index c3bab060..503ef87d 100644 --- a/README.md +++ b/README.md @@ -215,10 +215,10 @@ To lock the orientation of the mirroring: ```bash scrcpy --lock-video-orientation # initial (current) orientation -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 +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. diff --git a/app/tests/test_cli.c b/app/tests/test_cli.c index 3fa9b3d7..94740a9a 100644 --- a/app/tests/test_cli.c +++ b/app/tests/test_cli.c @@ -51,7 +51,7 @@ static void test_options(void) { "--fullscreen", "--max-fps", "30", "--max-size", "1024", - "--lock-video-orientation", "2", + "--lock-video-orientation=2", // optional arguments require '=' // "--no-control" is not compatible with "--turn-screen-off" // "--no-display" is not compatible with "--fulscreen" "--port", "1234:1236", From af228706f14696e818f4004691d4de0006ac4910 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 13 Jun 2021 18:31:48 +0200 Subject: [PATCH 0610/2244] Fix compatibility with old FFmpeg V4L2 sink used a "url" field format AVFormatContext which has been introduced in lavf 58.7.100. Fixes #2382 Refs Refs --- app/src/compat.h | 12 ++++++++++++ app/src/v4l2_sink.c | 5 +++++ 2 files changed, 17 insertions(+) diff --git a/app/src/compat.h b/app/src/compat.h index 9d9a7884..8e2d18f4 100644 --- a/app/src/compat.h +++ b/app/src/compat.h @@ -22,6 +22,18 @@ # define SCRCPY_LAVF_REQUIRES_REGISTER_ALL #endif + +// In ffmpeg/doc/APIchanges: +// 2018-01-28 - ea3672b7d6 - lavf 58.7.100 - avformat.h +// Deprecate AVFormatContext filename field which had limited length, use the +// new dynamically allocated url field instead. +// +// 2018-01-28 - ea3672b7d6 - lavf 58.7.100 - avformat.h +// Add url field to AVFormatContext and add ff_format_set_url helper function. +#if LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(58, 7, 100) +# define SCRCPY_LAVF_HAS_AVFORMATCONTEXT_URL +#endif + #if SDL_VERSION_ATLEAST(2, 0, 5) // # define SCRCPY_SDL_HAS_HINT_MOUSE_FOCUS_CLICKTHROUGH diff --git a/app/src/v4l2_sink.c b/app/src/v4l2_sink.c index fd0bda12..d7c1a667 100644 --- a/app/src/v4l2_sink.c +++ b/app/src/v4l2_sink.c @@ -180,12 +180,17 @@ sc_v4l2_sink_open(struct sc_v4l2_sink *vs) { // still expects a pointer-to-non-const (it has not be updated accordingly) // vs->format_ctx->oformat = (AVOutputFormat *) format; +#ifdef SCRCPY_LAVF_HAS_AVFORMATCONTEXT_URL vs->format_ctx->url = strdup(vs->device_name); if (!vs->format_ctx->url) { LOGE("Could not strdup v4l2 device name"); goto error_avformat_free_context; return false; } +#else + strncpy(vs->format_ctx->filename, vs->device_name, + sizeof(vs->format_ctx->filename)); +#endif AVStream *ostream = avformat_new_stream(vs->format_ctx, encoder); if (!ostream) { From a5d71eee450b3c91c942d15b598959021126f31d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 14 Jun 2021 08:16:05 +0200 Subject: [PATCH 0611/2244] Clarify --no-display usage with v4l2 --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 503ef87d..ba68dfb1 100644 --- a/README.md +++ b/README.md @@ -303,7 +303,8 @@ To start scrcpy using a v4l2 sink: ```bash scrcpy --v4l2-sink=/dev/videoN -scrcpy --v4l2-sink=/dev/videoN -N # --no-display to disable mirroring window +scrcpy --v4l2-sink=/dev/videoN --no-display # disable mirroring window +scrcpy --v4l2-sink=/dev/videoN -N # short version ``` (replace `N` by the device ID, check with `ls /dev/video*`) From e8b053ad2f1da25f2b0e06d2874cabb12ff44e01 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 14 Jun 2021 09:07:49 +0200 Subject: [PATCH 0612/2244] Allocate AVPacket for stream->pending From FFmpeg/doc/APIchanges: 2021-03-17 - f7db77bd87 - lavc 58.133.100 - codec.h Deprecated av_init_packet(). Once removed, sizeof(AVPacket) will no longer be a part of the public ABI. Remove the has_pending boolean, which can be replaced by: stream->pending != NULL Refs #2302 --- app/src/stream.c | 44 +++++++++++++++++++++++++------------------- app/src/stream.h | 3 +-- 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/app/src/stream.c b/app/src/stream.c index 2d9a0ab4..86f5d87a 100644 --- a/app/src/stream.c +++ b/app/src/stream.c @@ -104,33 +104,38 @@ static bool stream_push_packet(struct stream *stream, AVPacket *packet) { bool is_config = packet->pts == AV_NOPTS_VALUE; - // A config packet must not be decoded immetiately (it contains no + // A config packet must not be decoded immediately (it contains no // frame); instead, it must be concatenated with the future data packet. - if (stream->has_pending || is_config) { + if (stream->pending || is_config) { size_t offset; - if (stream->has_pending) { - offset = stream->pending.size; - if (av_grow_packet(&stream->pending, packet->size)) { + if (stream->pending) { + offset = stream->pending->size; + if (av_grow_packet(stream->pending, packet->size)) { LOGE("Could not grow packet"); return false; } } else { offset = 0; - if (av_new_packet(&stream->pending, packet->size)) { - LOGE("Could not create packet"); + stream->pending = av_packet_alloc(); + if (!stream->pending) { + LOGE("Could not allocate packet"); + return false; + } + if (av_new_packet(stream->pending, packet->size)) { + LOGE("Could not create packet"); + av_packet_free(&stream->pending); return false; } - stream->has_pending = true; } - memcpy(stream->pending.data + offset, packet->data, packet->size); + memcpy(stream->pending->data + offset, packet->data, packet->size); if (!is_config) { // prepare the concat packet to send to the decoder - stream->pending.pts = packet->pts; - stream->pending.dts = packet->dts; - stream->pending.flags = packet->flags; - packet = &stream->pending; + stream->pending->pts = packet->pts; + stream->pending->dts = packet->dts; + stream->pending->flags = packet->flags; + packet = stream->pending; } } @@ -144,10 +149,10 @@ stream_push_packet(struct stream *stream, AVPacket *packet) { // data packet bool ok = stream_parse(stream, packet); - if (stream->has_pending) { + if (stream->pending) { // the pending packet must be discarded (consumed or error) - stream->has_pending = false; - av_packet_unref(&stream->pending); + av_packet_unref(stream->pending); + av_packet_free(&stream->pending); } if (!ok) { @@ -233,8 +238,9 @@ run_stream(void *data) { LOGD("End of frames"); - if (stream->has_pending) { - av_packet_unref(&stream->pending); + if (stream->pending) { + av_packet_unref(stream->pending); + av_packet_free(&stream->pending); } av_parser_close(stream->parser); @@ -252,7 +258,7 @@ void stream_init(struct stream *stream, socket_t socket, const struct stream_callbacks *cbs, void *cbs_userdata) { stream->socket = socket; - stream->has_pending = false; + stream->pending = NULL; stream->sink_count = 0; assert(cbs && cbs->on_eos); diff --git a/app/src/stream.h b/app/src/stream.h index 9fc4d1e1..d7047c95 100644 --- a/app/src/stream.h +++ b/app/src/stream.h @@ -24,8 +24,7 @@ struct stream { AVCodecParserContext *parser; // successive packets may need to be concatenated, until a non-config // packet is available - bool has_pending; - AVPacket pending; + AVPacket *pending; const struct stream_callbacks *cbs; void *cbs_userdata; From 318b6a572e0e866554c5aa5a2ed8c4d0d774a5dc Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 14 Jun 2021 09:07:49 +0200 Subject: [PATCH 0613/2244] Allocate AVPacket for local stream packet From FFmpeg/doc/APIchanges: 2021-03-17 - f7db77bd87 - lavc 58.133.100 - codec.h Deprecated av_init_packet(). Once removed, sizeof(AVPacket) will no longer be a part of the public ABI. Refs #2302 --- app/src/stream.c | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/app/src/stream.c b/app/src/stream.c index 86f5d87a..d1b8b9f3 100644 --- a/app/src/stream.c +++ b/app/src/stream.c @@ -220,16 +220,21 @@ run_stream(void *data) { // It's more complicated, but this allows to reduce the latency by 1 frame! stream->parser->flags |= PARSER_FLAG_COMPLETE_FRAMES; + AVPacket *packet = av_packet_alloc(); + if (!packet) { + LOGE("Could not allocate packet"); + goto finally_close_parser; + } + for (;;) { - AVPacket packet; - bool ok = stream_recv_packet(stream, &packet); + bool ok = stream_recv_packet(stream, packet); if (!ok) { // end of stream break; } - ok = stream_push_packet(stream, &packet); - av_packet_unref(&packet); + ok = stream_push_packet(stream, packet); + av_packet_unref(packet); if (!ok) { // cannot process packet (error already logged) break; @@ -243,6 +248,8 @@ run_stream(void *data) { av_packet_free(&stream->pending); } + av_packet_free(&packet); +finally_close_parser: av_parser_close(stream->parser); finally_close_sinks: stream_close_sinks(stream); From 4af317d40da70d63dee44bf645694ed5c4f05d65 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 14 Jun 2021 09:07:49 +0200 Subject: [PATCH 0614/2244] Allocate AVPacket for recorder From FFmpeg/doc/APIchanges: 2021-03-17 - f7db77bd87 - lavc 58.133.100 - codec.h Deprecated av_init_packet(). Once removed, sizeof(AVPacket) will no longer be a part of the public ABI. Refs #2302 --- app/src/recorder.c | 27 ++++++++++++++++----------- app/src/recorder.h | 2 +- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/app/src/recorder.c b/app/src/recorder.c index 91390ff1..85570324 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -35,11 +35,14 @@ record_packet_new(const AVPacket *packet) { return NULL; } - // av_packet_ref() does not initialize all fields in old FFmpeg versions - // See - av_init_packet(&rec->packet); + rec->packet = av_packet_alloc(); + if (!rec->packet) { + free(rec); + return NULL; + } - if (av_packet_ref(&rec->packet, packet)) { + if (av_packet_ref(rec->packet, packet)) { + av_packet_free(&rec->packet); free(rec); return NULL; } @@ -48,7 +51,8 @@ record_packet_new(const AVPacket *packet) { static void record_packet_delete(struct record_packet *rec) { - av_packet_unref(&rec->packet); + av_packet_unref(rec->packet); + av_packet_free(&rec->packet); free(rec); } @@ -144,8 +148,8 @@ run_recorder(void *data) { struct record_packet *last = recorder->previous; if (last) { // assign an arbitrary duration to the last packet - last->packet.duration = 100000; - bool ok = recorder_write(recorder, &last->packet); + last->packet->duration = 100000; + bool ok = recorder_write(recorder, last->packet); if (!ok) { // failing to write the last frame is not very serious, no // future frame may depend on it, so the resulting file @@ -172,13 +176,14 @@ run_recorder(void *data) { } // config packets have no PTS, we must ignore them - if (rec->packet.pts != AV_NOPTS_VALUE - && previous->packet.pts != AV_NOPTS_VALUE) { + if (rec->packet->pts != AV_NOPTS_VALUE + && previous->packet->pts != AV_NOPTS_VALUE) { // we now know the duration of the previous packet - previous->packet.duration = rec->packet.pts - previous->packet.pts; + previous->packet->duration = + rec->packet->pts - previous->packet->pts; } - bool ok = recorder_write(recorder, &previous->packet); + bool ok = recorder_write(recorder, previous->packet); record_packet_delete(previous); if (!ok) { LOGE("Could not record packet"); diff --git a/app/src/recorder.h b/app/src/recorder.h index 1b2b9284..0c376cd1 100644 --- a/app/src/recorder.h +++ b/app/src/recorder.h @@ -13,7 +13,7 @@ #include "util/thread.h" struct record_packet { - AVPacket packet; + AVPacket *packet; struct record_packet *next; }; From cd2894570d224d854d6e27abb401434fa6ec2840 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 14 Jun 2021 09:07:49 +0200 Subject: [PATCH 0615/2244] Allocate AVPacket for v4l2_sink From FFmpeg/doc/APIchanges: 2021-03-17 - f7db77bd87 - lavc 58.133.100 - codec.h Deprecated av_init_packet(). Once removed, sizeof(AVPacket) will no longer be a part of the public ABI. Refs #2302 --- app/src/v4l2_sink.c | 13 +++++++++++-- app/src/v4l2_sink.h | 2 +- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/app/src/v4l2_sink.c b/app/src/v4l2_sink.c index d7c1a667..bd184b4d 100644 --- a/app/src/v4l2_sink.c +++ b/app/src/v4l2_sink.c @@ -86,7 +86,7 @@ encode_and_write_frame(struct sc_v4l2_sink *vs, const AVFrame *frame) { return false; } - AVPacket *packet = &vs->packet; + AVPacket *packet = vs->packet; ret = avcodec_receive_packet(vs->encoder_ctx, packet); if (ret == 0) { // A packet was received @@ -235,11 +235,17 @@ sc_v4l2_sink_open(struct sc_v4l2_sink *vs) { goto error_avcodec_close; } + vs->packet = av_packet_alloc(); + if (!vs->packet) { + LOGE("Could not allocate packet"); + goto error_av_frame_free; + } + LOGD("Starting v4l2 thread"); ok = sc_thread_create(&vs->thread, run_v4l2_sink, "v4l2", vs); if (!ok) { LOGC("Could not start v4l2 thread"); - goto error_av_frame_free; + goto error_av_packet_free; } vs->header_written = false; @@ -249,6 +255,8 @@ sc_v4l2_sink_open(struct sc_v4l2_sink *vs) { return true; +error_av_packet_free: + av_packet_free(&vs->packet); error_av_frame_free: av_frame_free(&vs->frame); error_avcodec_close: @@ -278,6 +286,7 @@ sc_v4l2_sink_close(struct sc_v4l2_sink *vs) { sc_thread_join(&vs->thread, NULL); + av_packet_free(&vs->packet); av_frame_free(&vs->frame); avcodec_close(vs->encoder_ctx); avcodec_free_context(&vs->encoder_ctx); diff --git a/app/src/v4l2_sink.h b/app/src/v4l2_sink.h index 9d2ee149..81bcdd1e 100644 --- a/app/src/v4l2_sink.h +++ b/app/src/v4l2_sink.h @@ -26,7 +26,7 @@ struct sc_v4l2_sink { bool header_written; AVFrame *frame; - AVPacket packet; + AVPacket *packet; }; bool From 7343b233e457076d9419fb0f6e587ba13a7e79c8 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 14 Jun 2021 09:33:10 +0200 Subject: [PATCH 0616/2244] Render screen on window restored It should not be necessary, since screen_render() is called just after on SDL_WINDOWEVENT_EXPOSED, but in practice the window content might not be correctly displayed on restored if a rotation occurred while minimized. Note that calling screen_render() twice in a row on SDL_WINDOWEVENT_EXPOSED also "fixes" the issue. --- app/src/screen.c | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/screen.c b/app/src/screen.c index a09831a3..c70a95e3 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -726,6 +726,7 @@ screen_handle_event(struct screen *screen, SDL_Event *event) { } screen->maximized = false; apply_pending_resize(screen); + screen_render(screen, true); break; } return true; From 9b89b7ab7208cac0d7da91bb9a1811d6738c82a7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 14 Jun 2021 21:24:51 +0200 Subject: [PATCH 0617/2244] Center the window on resize-to-fit When removing the black borders (by double-clicking on them, or by pressing MOD+w), the window is resized to fit the device screen, but its top-left position was left unchanged. Instead, move the window so that the new window area is at the center of the old window area. Refs #2387 --- app/src/screen.c | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/app/src/screen.c b/app/src/screen.c index c70a95e3..99327b3b 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -41,6 +41,18 @@ get_window_size(const struct screen *screen) { return size; } +static struct point +get_window_position(const struct screen *screen) { + int x; + int y; + SDL_GetWindowPosition(screen->window, &x, &y); + + struct point point; + point.x = x; + point.y = y; + return point; +} + // set the window size to be applied when fullscreen is disabled static void set_window_size(struct screen *screen, struct size new_size) { @@ -122,13 +134,6 @@ get_optimal_size(struct size current_size, struct size content_size) { return window_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 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 // req_width and req_height, if not 0, are the sizes requested by the user static inline struct size @@ -662,9 +667,20 @@ screen_resize_to_fit(struct screen *screen) { return; } + struct point point = get_window_position(screen); + struct size window_size = get_window_size(screen); + struct size optimal_size = - get_optimal_window_size(screen, screen->content_size); + get_optimal_size(window_size, screen->content_size); + + // Center the window related to the device screen + assert(optimal_size.width <= window_size.width); + assert(optimal_size.height <= window_size.height); + uint32_t new_x = point.x + (window_size.width - optimal_size.width) / 2; + uint32_t new_y = point.y + (window_size.height - optimal_size.height) / 2; + SDL_SetWindowSize(screen->window, optimal_size.width, optimal_size.height); + SDL_SetWindowPosition(screen->window, new_x, new_y); LOGD("Resized to optimal size: %ux%u", optimal_size.width, optimal_size.height); } From a1f2094787cf6267d9913dcf32e1782002a76729 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 13 Jun 2021 22:47:16 +0200 Subject: [PATCH 0618/2244] Push to /sdcard/Download/ by default Change the default push target from /sdcard/ to /sdcard/Download/. Pushing to the root of /sdcard/ is not very convenient, many apps do not expose its content directly. It can still be changed by --push-target. PR #2384 --- README.md | 6 +++--- app/scrcpy.1 | 2 +- app/src/cli.c | 2 +- app/src/file_handler.c | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index ba68dfb1..150319d2 100644 --- a/README.md +++ b/README.md @@ -710,15 +710,15 @@ There is no visual feedback, a log is printed to the console. #### Push file to device -To push a file to `/sdcard/` on the device, drag & drop a (non-APK) file to the -_scrcpy_ window. +To push a file to `/sdcard/Download/` on the device, drag & drop a (non-APK) +file to the _scrcpy_ window. There is no visual feedback, a log is printed to the console. The target directory can be changed on start: ```bash -scrcpy --push-target=/sdcard/Download/ +scrcpy --push-target=/sdcard/Movies/ ``` diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 80467d10..7b6f1faa 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -133,7 +133,7 @@ but breaks the expected behavior of alpha keys in games (typically WASD). .BI "\-\-push\-target " path Set the target directory for pushing files to the device by drag & drop. It is passed as\-is to "adb push". -Default is "/sdcard/". +Default is "/sdcard/Download/". .TP .BI "\-r, \-\-record " file diff --git a/app/src/cli.c b/app/src/cli.c index 3e5d613d..6e1180bc 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -129,7 +129,7 @@ scrcpy_print_usage(const char *arg0) { " --push-target path\n" " Set the target directory for pushing files to the device by\n" " drag & drop. It is passed as-is to \"adb push\".\n" - " Default is \"/sdcard/\".\n" + " Default is \"/sdcard/Download/\".\n" "\n" " -r, --record file.mp4\n" " Record screen to file.\n" diff --git a/app/src/file_handler.c b/app/src/file_handler.c index 2b08240c..27fe6fa3 100644 --- a/app/src/file_handler.c +++ b/app/src/file_handler.c @@ -6,7 +6,7 @@ #include "adb.h" #include "util/log.h" -#define DEFAULT_PUSH_TARGET "/sdcard/" +#define DEFAULT_PUSH_TARGET "/sdcard/Download/" static void file_handler_request_destroy(struct file_handler_request *req) { From b846d3a085cc4a55a5a5751c2ab6114310997e30 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 19 Jun 2021 08:43:49 +0200 Subject: [PATCH 0619/2244] Adapt call() on ContentProvider for Android 12 Android 12 changed one of the call() overloads with a new parameter AttributionSource. Adapt the wrapper. Fixes #2402 --- .../scrcpy/wrappers/ContentProvider.java | 45 ++++++++++++++----- 1 file changed, 35 insertions(+), 10 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java index f8393e59..387c7a60 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java @@ -2,6 +2,7 @@ package com.genymobile.scrcpy.wrappers; import com.genymobile.scrcpy.Ln; +import android.annotation.SuppressLint; import android.os.Bundle; import android.os.IBinder; @@ -37,6 +38,8 @@ public class ContentProvider implements Closeable { private Method callMethod; private int callMethodVersion; + private Object attributionSource; + ContentProvider(ActivityManager manager, Object provider, String name, IBinder token) { this.manager = manager; this.provider = provider; @@ -44,36 +47,58 @@ public class ContentProvider implements Closeable { this.token = token; } + @SuppressLint("PrivateApi") private Method getCallMethod() throws NoSuchMethodException { if (callMethod == null) { - try { - callMethod = provider.getClass() - .getMethod("call", String.class, String.class, String.class, String.class, String.class, Bundle.class); + Class attributionSourceClass = Class.forName("android.content.AttributionSource"); + callMethod = provider.getClass().getMethod("call", attributionSourceClass, String.class, String.class, String.class, Bundle.class); callMethodVersion = 0; - } catch (NoSuchMethodException e) { + } catch (NoSuchMethodException | ClassNotFoundException e0) { // old versions try { - callMethod = provider.getClass().getMethod("call", String.class, String.class, String.class, String.class, Bundle.class); + callMethod = provider.getClass() + .getMethod("call", String.class, String.class, String.class, String.class, String.class, Bundle.class); callMethodVersion = 1; - } catch (NoSuchMethodException e2) { - callMethod = provider.getClass().getMethod("call", String.class, String.class, String.class, Bundle.class); - callMethodVersion = 2; + } catch (NoSuchMethodException e1) { + try { + callMethod = provider.getClass().getMethod("call", String.class, String.class, String.class, String.class, Bundle.class); + callMethodVersion = 2; + } catch (NoSuchMethodException e2) { + callMethod = provider.getClass().getMethod("call", String.class, String.class, String.class, Bundle.class); + callMethodVersion = 3; + } } } } return callMethod; } + @SuppressLint("PrivateApi") + private Object getAttributionSource() + throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { + if (attributionSource == null) { + Class cl = Class.forName("android.content.AttributionSource$Builder"); + Object builder = cl.getConstructor(int.class).newInstance(ServiceManager.USER_ID); + cl.getDeclaredMethod("setPackageName", String.class).invoke(builder, ServiceManager.PACKAGE_NAME); + attributionSource = cl.getDeclaredMethod("build").invoke(builder); + } + + return attributionSource; + } + private Bundle call(String callMethod, String arg, Bundle extras) { try { Method method = getCallMethod(); Object[] args; switch (callMethodVersion) { case 0: - args = new Object[]{ServiceManager.PACKAGE_NAME, null, "settings", callMethod, arg, extras}; + args = new Object[]{getAttributionSource(), "settings", callMethod, arg, extras}; break; case 1: + args = new Object[]{ServiceManager.PACKAGE_NAME, null, "settings", callMethod, arg, extras}; + break; + case 2: args = new Object[]{ServiceManager.PACKAGE_NAME, "settings", callMethod, arg, extras}; break; default: @@ -81,7 +106,7 @@ public class ContentProvider implements Closeable { break; } return (Bundle) method.invoke(provider, args); - } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException | ClassNotFoundException | InstantiationException e) { Ln.e("Could not invoke method", e); return null; } From df017160ed9a9856c3a8eb08de29d77c14df70f0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 20 Jun 2021 12:33:05 +0200 Subject: [PATCH 0620/2244] Replace strcpy() by memcpy() It was safe to call strcpy() since the input length was checked, but then it is more straightforward to call memcpy() directly. --- app/src/scrcpy.c | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 17902156..4dcb412f 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -216,14 +216,15 @@ av_log_callback(void *avcl, int level, const char *fmt, va_list vl) { if (priority == 0) { return; } - char *local_fmt = malloc(strlen(fmt) + 10); + + size_t fmt_len = strlen(fmt); + char *local_fmt = malloc(fmt_len + 10); if (!local_fmt) { LOGC("Could not allocate string"); return; } - // strcpy is safe here, the destination is large enough - strcpy(local_fmt, "[FFmpeg] "); - strcpy(local_fmt + 9, fmt); + memcpy(local_fmt, "[FFmpeg] ", 9); // do not write the final '\0' + memcpy(local_fmt + 9, fmt, fmt_len + 1); // include '\0' SDL_LogMessageV(SDL_LOG_CATEGORY_VIDEO, priority, local_fmt, vl); free(local_fmt); } From 8b90e1d3f46b77e162caa29a89952182edaa465c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 20 Jun 2021 00:32:55 +0200 Subject: [PATCH 0621/2244] Remove extra ';' in #define --- app/src/control_msg.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/control_msg.h b/app/src/control_msg.h index c1099c79..a259d0db 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -17,8 +17,8 @@ // 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); -#define POINTER_ID_VIRTUAL_FINGER UINT64_C(-2); +#define POINTER_ID_MOUSE UINT64_C(-1) +#define POINTER_ID_VIRTUAL_FINGER UINT64_C(-2) enum control_msg_type { CONTROL_MSG_TYPE_INJECT_KEYCODE, From 7db0189f23bc3dd3802c751ed44a1dd367523a5a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 20 Jun 2021 00:42:55 +0200 Subject: [PATCH 0622/2244] Forward mouse motion only on main clicks Mouse motion events were forwarded as soon as any mouse button was pressed. Instead, only consider left-click (and also middle-click and right-click if --forward-all-clicks is enabled). --- app/src/input_manager.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index b008c4db..a5d0ad07 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -595,8 +595,12 @@ convert_mouse_motion(const SDL_MouseMotionEvent *from, struct screen *screen, static void input_manager_process_mouse_motion(struct input_manager *im, const SDL_MouseMotionEvent *event) { - if (!event->state) { - // do not send motion events when no button is pressed + uint32_t mask = SDL_BUTTON_LMASK; + if (im->forward_all_clicks) { + mask |= SDL_BUTTON_MMASK | SDL_BUTTON_RMASK; + } + if (!(event->state & mask)) { + // do not send motion events when no click is pressed return; } if (event->which == SDL_TOUCH_MOUSEID) { From 937fa704a6bb07a264e247bb41de08b5378de39e Mon Sep 17 00:00:00 2001 From: Marti Raudsepp Date: Thu, 17 Jun 2021 22:40:30 +0300 Subject: [PATCH 0623/2244] Add --verbosity=verbose log level PR #2371 Signed-off-by: Romain Vimont --- app/scrcpy.1 | 2 +- app/src/cli.c | 7 ++++++- app/src/main.c | 2 ++ app/src/scrcpy.h | 1 + app/src/server.c | 2 ++ server/src/main/java/com/genymobile/scrcpy/Ln.java | 9 ++++++++- 6 files changed, 20 insertions(+), 3 deletions(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 7b6f1faa..62dc9677 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -193,7 +193,7 @@ It requires to lock the video orientation (see --lock-video-orientation). .TP .BI "\-V, \-\-verbosity " value -Set the log level ("debug", "info", "warn" or "error"). +Set the log level ("verbose", "debug", "info", "warn" or "error"). Default is "info" for release builds, "debug" for debug builds. diff --git a/app/src/cli.c b/app/src/cli.c index 6e1180bc..4c1fbb2f 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -184,7 +184,7 @@ scrcpy_print_usage(const char *arg0) { "\n" #endif " -V, --verbosity value\n" - " Set the log level (debug, info, warn or error).\n" + " Set the log level (verbose, debug, info, warn or error).\n" #ifndef NDEBUG " Default is debug.\n" #else @@ -505,6 +505,11 @@ parse_display_id(const char *s, uint32_t *display_id) { static bool parse_log_level(const char *s, enum sc_log_level *log_level) { + if (!strcmp(s, "verbose")) { + *log_level = SC_LOG_LEVEL_VERBOSE; + return true; + } + if (!strcmp(s, "debug")) { *log_level = SC_LOG_LEVEL_DEBUG; return true; diff --git a/app/src/main.c b/app/src/main.c index a468aed7..c3a7ad2e 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -41,6 +41,8 @@ print_version(void) { static SDL_LogPriority convert_log_level_to_sdl(enum sc_log_level level) { switch (level) { + case SC_LOG_LEVEL_VERBOSE: + return SDL_LOG_PRIORITY_VERBOSE; case SC_LOG_LEVEL_DEBUG: return SDL_LOG_PRIORITY_DEBUG; case SC_LOG_LEVEL_INFO: diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index 405dc7f3..0a2deb71 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -8,6 +8,7 @@ #include enum sc_log_level { + SC_LOG_LEVEL_VERBOSE, SC_LOG_LEVEL_DEBUG, SC_LOG_LEVEL_INFO, SC_LOG_LEVEL_WARN, diff --git a/app/src/server.c b/app/src/server.c index 41e8166c..a4cdb0c9 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -235,6 +235,8 @@ enable_tunnel_any_port(struct server *server, struct sc_port_range port_range, static const char * log_level_to_server_string(enum sc_log_level level) { switch (level) { + case SC_LOG_LEVEL_VERBOSE: + return "verbose"; case SC_LOG_LEVEL_DEBUG: return "debug"; case SC_LOG_LEVEL_INFO: diff --git a/server/src/main/java/com/genymobile/scrcpy/Ln.java b/server/src/main/java/com/genymobile/scrcpy/Ln.java index c218fa0f..061cda95 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Ln.java +++ b/server/src/main/java/com/genymobile/scrcpy/Ln.java @@ -12,7 +12,7 @@ public final class Ln { private static final String PREFIX = "[server] "; enum Level { - DEBUG, INFO, WARN, ERROR + VERBOSE, DEBUG, INFO, WARN, ERROR } private static Level threshold = Level.INFO; @@ -36,6 +36,13 @@ public final class Ln { return level.ordinal() >= threshold.ordinal(); } + public static void v(String message) { + if (isEnabled(Level.VERBOSE)) { + Log.v(TAG, message); + System.out.println(PREFIX + "VERBOSE: " + message); + } + } + public static void d(String message) { if (isEnabled(Level.DEBUG)) { Log.d(TAG, message); From 19ca02cd8f1a7554cd7c895482dca87134378baf Mon Sep 17 00:00:00 2001 From: Marti Raudsepp Date: Tue, 8 Jun 2021 19:14:20 +0300 Subject: [PATCH 0624/2244] Log control messages in verbose mode PR #2371 Signed-off-by: Romain Vimont --- app/src/control_msg.c | 130 ++++++++++++++++++++++++++++++++++++++++++ app/src/control_msg.h | 3 + app/src/controller.c | 1 + 3 files changed, 134 insertions(+) diff --git a/app/src/control_msg.c b/app/src/control_msg.c index 8908c546..aba4bd16 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -1,6 +1,7 @@ #include "control_msg.h" #include +#include #include #include @@ -8,6 +9,52 @@ #include "util/log.h" #include "util/str_util.h" +/** + * Map an enum value to a string based on an array, without crashing on an + * out-of-bounds index. + */ +#define ENUM_TO_LABEL(labels, value) \ + ((size_t) (value) < ARRAY_LEN(labels) ? labels[value] : "???") + +#define KEYEVENT_ACTION_LABEL(value) \ + ENUM_TO_LABEL(android_keyevent_action_labels, value) + +#define MOTIONEVENT_ACTION_LABEL(value) \ + ENUM_TO_LABEL(android_motionevent_action_labels, value) + +#define SCREEN_POWER_MODE_LABEL(value) \ + ENUM_TO_LABEL(screen_power_mode_labels, value) + +static const char *const android_keyevent_action_labels[] = { + "down", + "up", + "multi", +}; + +static const char *const android_motionevent_action_labels[] = { + "down", + "up", + "move", + "cancel", + "outside", + "ponter-down", + "pointer-up", + "hover-move", + "scroll", + "hover-enter" + "hover-exit", + "btn-press", + "btn-release", +}; + +static const char *const screen_power_mode_labels[] = { + "off", + "doze", + "normal", + "doze-suspend", + "suspend", +}; + static void write_position(uint8_t *buf, const struct position *position) { buffer_write32be(&buf[0], position->point.x); @@ -93,6 +140,89 @@ control_msg_serialize(const struct control_msg *msg, unsigned char *buf) { } } +void +control_msg_log(const struct control_msg *msg) { +#define LOG_CMSG(fmt, ...) LOGV("input: " fmt, ## __VA_ARGS__) + switch (msg->type) { + case CONTROL_MSG_TYPE_INJECT_KEYCODE: + LOG_CMSG("key %-4s code=%d repeat=%" PRIu32 " meta=%06lx", + KEYEVENT_ACTION_LABEL(msg->inject_keycode.action), + (int) msg->inject_keycode.keycode, + msg->inject_keycode.repeat, + (long) msg->inject_keycode.metastate); + break; + case CONTROL_MSG_TYPE_INJECT_TEXT: + LOG_CMSG("text \"%s\"", msg->inject_text.text); + break; + case CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT: { + int action = msg->inject_touch_event.action + & AMOTION_EVENT_ACTION_MASK; + uint64_t id = msg->inject_touch_event.pointer_id; + if (id == POINTER_ID_MOUSE || id == POINTER_ID_VIRTUAL_FINGER) { + // string pointer id + LOG_CMSG("touch [id=%s] %-4s position=%" PRIi32 ",%" PRIi32 + " pressure=%g buttons=%06lx", + id == POINTER_ID_MOUSE ? "mouse" : "vfinger", + MOTIONEVENT_ACTION_LABEL(action), + msg->inject_touch_event.position.point.x, + msg->inject_touch_event.position.point.y, + msg->inject_touch_event.pressure, + (long) msg->inject_touch_event.buttons); + } else { + // numeric pointer id + LOG_CMSG("touch [id=%" PRIu64 "] %-4s position=%" PRIi32 ",%" + PRIi32 " pressure=%g buttons=%06lx", + id, + MOTIONEVENT_ACTION_LABEL(action), + msg->inject_touch_event.position.point.x, + msg->inject_touch_event.position.point.y, + msg->inject_touch_event.pressure, + (long) msg->inject_touch_event.buttons); + } + break; + } + case CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT: + LOG_CMSG("scroll position=%" PRIi32 ",%" PRIi32 " hscroll=%" PRIi32 + " vscroll=%" PRIi32, + msg->inject_scroll_event.position.point.x, + msg->inject_scroll_event.position.point.y, + msg->inject_scroll_event.hscroll, + msg->inject_scroll_event.vscroll); + break; + case CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON: + LOG_CMSG("back-or-screen-on %s", + KEYEVENT_ACTION_LABEL(msg->inject_keycode.action)); + break; + case CONTROL_MSG_TYPE_SET_CLIPBOARD: + LOG_CMSG("clipboard %s \"%s\"", + msg->set_clipboard.paste ? "paste" : "copy", + msg->set_clipboard.text); + break; + case CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE: + LOG_CMSG("power mode %s", + SCREEN_POWER_MODE_LABEL(msg->set_screen_power_mode.mode)); + break; + case CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL: + LOG_CMSG("expand notification panel"); + break; + case CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL: + LOG_CMSG("expand settings panel"); + break; + case CONTROL_MSG_TYPE_COLLAPSE_PANELS: + LOG_CMSG("collapse panels"); + break; + case CONTROL_MSG_TYPE_GET_CLIPBOARD: + LOG_CMSG("get clipboard"); + break; + case CONTROL_MSG_TYPE_ROTATE_DEVICE: + LOG_CMSG("rotate device"); + break; + default: + LOG_CMSG("unknown type: %u", (unsigned) msg->type); + break; + } +} + void control_msg_destroy(struct control_msg *msg) { switch (msg->type) { diff --git a/app/src/control_msg.h b/app/src/control_msg.h index a259d0db..920a493a 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -84,6 +84,9 @@ struct control_msg { size_t control_msg_serialize(const struct control_msg *msg, unsigned char *buf); +void +control_msg_log(const struct control_msg *msg); + void control_msg_destroy(struct control_msg *msg); diff --git a/app/src/controller.c b/app/src/controller.c index 38b5e702..9284ae52 100644 --- a/app/src/controller.c +++ b/app/src/controller.c @@ -48,6 +48,7 @@ controller_destroy(struct controller *controller) { bool controller_push_msg(struct controller *controller, const struct control_msg *msg) { + control_msg_log(msg); sc_mutex_lock(&controller->mutex); bool was_empty = cbuf_is_empty(&controller->queue); bool res = cbuf_push(&controller->queue, *msg); From 1039f9b531e57f710432073b2fd525d8ecb857c5 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 20 Jun 2021 01:30:06 +0200 Subject: [PATCH 0625/2244] Workaround PRIu64 on Windows On Windows, PRIu64 is defined to "llu", which is not supported: error: unknown conversion type character 'l' in format --- app/src/control_msg.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/src/control_msg.c b/app/src/control_msg.c index aba4bd16..1257010e 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -170,7 +170,12 @@ control_msg_log(const struct control_msg *msg) { (long) msg->inject_touch_event.buttons); } else { // numeric pointer id - LOG_CMSG("touch [id=%" PRIu64 "] %-4s position=%" PRIi32 ",%" +#ifndef __WIN32 +# define PRIu64_ PRIu64 +#else +# define PRIu64_ "I64u" // Windows... +#endif + LOG_CMSG("touch [id=%" PRIu64_ "] %-4s position=%" PRIi32 ",%" PRIi32 " pressure=%g buttons=%06lx", id, MOTIONEVENT_ACTION_LABEL(action), From 5c95d18beb55e9dc24bf8b650042fcabeccce63f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 20 Jun 2021 12:46:41 +0200 Subject: [PATCH 0626/2244] Move log level conversion to log API --- app/meson.build | 1 + app/src/main.c | 23 +---------------------- app/src/util/log.c | 28 ++++++++++++++++++++++++++++ app/src/util/log.h | 11 +++++++++-- 4 files changed, 39 insertions(+), 24 deletions(-) create mode 100644 app/src/util/log.c diff --git a/app/meson.build b/app/meson.build index 6b6991aa..0663c641 100644 --- a/app/meson.build +++ b/app/meson.build @@ -20,6 +20,7 @@ src = [ 'src/stream.c', 'src/tiny_xpm.c', 'src/video_buffer.c', + 'src/util/log.c', 'src/util/net.c', 'src/util/process.c', 'src/util/str_util.c', diff --git a/app/src/main.c b/app/src/main.c index c3a7ad2e..2afa3c4e 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -38,26 +38,6 @@ print_version(void) { #endif } -static SDL_LogPriority -convert_log_level_to_sdl(enum sc_log_level level) { - switch (level) { - case SC_LOG_LEVEL_VERBOSE: - return SDL_LOG_PRIORITY_VERBOSE; - 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 SDL_LOG_PRIORITY_INFO; - } -} - - int main(int argc, char *argv[]) { #ifdef __WINDOWS__ @@ -81,8 +61,7 @@ main(int argc, char *argv[]) { return 1; } - SDL_LogPriority sdl_log = convert_log_level_to_sdl(args.opts.log_level); - SDL_LogSetPriority(SDL_LOG_CATEGORY_APPLICATION, sdl_log); + sc_set_log_level(args.opts.log_level); if (args.help) { scrcpy_print_usage(argv[0]); diff --git a/app/src/util/log.c b/app/src/util/log.c new file mode 100644 index 00000000..ac26de1c --- /dev/null +++ b/app/src/util/log.c @@ -0,0 +1,28 @@ +#include "log.h" + +#include + +static SDL_LogPriority +log_level_sc_to_sdl(enum sc_log_level level) { + switch (level) { + case SC_LOG_LEVEL_VERBOSE: + return SDL_LOG_PRIORITY_VERBOSE; + 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 SDL_LOG_PRIORITY_INFO; + } +} + +void +sc_set_log_level(enum sc_log_level level) { + SDL_LogPriority sdl_log = log_level_sc_to_sdl(level); + SDL_LogSetPriority(SDL_LOG_CATEGORY_APPLICATION, sdl_log); +} diff --git a/app/src/util/log.h b/app/src/util/log.h index 5955c7fb..8b6c175e 100644 --- a/app/src/util/log.h +++ b/app/src/util/log.h @@ -1,8 +1,12 @@ -#ifndef LOG_H -#define LOG_H +#ifndef SC_LOG_H +#define SC_LOG_H + +#include "common.h" #include +#include "scrcpy.h" + #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__) @@ -10,4 +14,7 @@ #define LOGE(...) SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__) #define LOGC(...) SDL_LogCritical(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__) +void +sc_set_log_level(enum sc_log_level level); + #endif From 488991116b9c56112a26daa1bb1069b57b9832ec Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 20 Jun 2021 12:49:45 +0200 Subject: [PATCH 0627/2244] Expose function to get the current log level This will allow to avoid unnecessary processing for creating logs which will be discarded anyway. --- app/src/util/log.c | 25 +++++++++++++++++++++++++ app/src/util/log.h | 3 +++ 2 files changed, 28 insertions(+) diff --git a/app/src/util/log.c b/app/src/util/log.c index ac26de1c..a285fffb 100644 --- a/app/src/util/log.c +++ b/app/src/util/log.c @@ -21,8 +21,33 @@ log_level_sc_to_sdl(enum sc_log_level level) { } } +static enum sc_log_level +log_level_sdl_to_sc(SDL_LogPriority priority) { + switch (priority) { + case SDL_LOG_PRIORITY_VERBOSE: + return SC_LOG_LEVEL_VERBOSE; + case SDL_LOG_PRIORITY_DEBUG: + return SC_LOG_LEVEL_DEBUG; + case SDL_LOG_PRIORITY_INFO: + return SC_LOG_LEVEL_INFO; + case SDL_LOG_PRIORITY_WARN: + return SC_LOG_LEVEL_WARN; + case SDL_LOG_PRIORITY_ERROR: + return SC_LOG_LEVEL_ERROR; + default: + assert(!"unexpected log level"); + return SC_LOG_LEVEL_INFO; + } +} + void sc_set_log_level(enum sc_log_level level) { SDL_LogPriority sdl_log = log_level_sc_to_sdl(level); SDL_LogSetPriority(SDL_LOG_CATEGORY_APPLICATION, sdl_log); } + +enum sc_log_level +sc_get_log_level(void) { + SDL_LogPriority sdl_log = SDL_LogGetPriority(SDL_LOG_CATEGORY_APPLICATION); + return log_level_sdl_to_sc(sdl_log); +} diff --git a/app/src/util/log.h b/app/src/util/log.h index 8b6c175e..30934b5c 100644 --- a/app/src/util/log.h +++ b/app/src/util/log.h @@ -17,4 +17,7 @@ void sc_set_log_level(enum sc_log_level level); +enum sc_log_level +sc_get_log_level(void); + #endif From 1c950434784823d13e6bb9397bed1d6bfdad54ef Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 20 Jun 2021 12:54:09 +0200 Subject: [PATCH 0628/2244] Attempt to log message only in verbose mode If the log level is not verbose, there is no need to attempt to log control messages at all. --- app/src/controller.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/controller.c b/app/src/controller.c index 9284ae52..3a428aa8 100644 --- a/app/src/controller.c +++ b/app/src/controller.c @@ -48,7 +48,10 @@ controller_destroy(struct controller *controller) { bool controller_push_msg(struct controller *controller, const struct control_msg *msg) { - control_msg_log(msg); + if (sc_get_log_level() <= SC_LOG_LEVEL_VERBOSE) { + control_msg_log(msg); + } + sc_mutex_lock(&controller->mutex); bool was_empty = cbuf_is_empty(&controller->queue); bool res = cbuf_push(&controller->queue, *msg); From 77e96d745b0243323fc4cacec0081da062d2506f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 20 Jun 2021 20:57:52 +0200 Subject: [PATCH 0629/2244] Suggest --record-format instead of -F on error The short option -F has been deprecated by ff061b4f30c54dedc5073a588c6c697477b805db. On error, suggest the long option --record-format instead. --- app/src/cli.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/cli.c b/app/src/cli.c index 4c1fbb2f..3eab8d27 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -962,7 +962,8 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { if (opts->record_filename && !opts->record_format) { opts->record_format = guess_record_format(opts->record_filename); if (!opts->record_format) { - LOGE("No format specified for \"%s\" (try with -F mkv)", + LOGE("No format specified for \"%s\" " + "(try with --record-format=mkv)", opts->record_filename); return false; } From 710e80aa0dcef5e35f0a40fd0b3668cef8e9b5c9 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 19 Jun 2021 22:53:40 +0200 Subject: [PATCH 0630/2244] Return build_cmd() success via a boolean For consistency with other functions in the codebase. --- app/src/sys/win/process.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/sys/win/process.c b/app/src/sys/win/process.c index f170e40d..7f5da6af 100644 --- a/app/src/sys/win/process.c +++ b/app/src/sys/win/process.c @@ -6,7 +6,7 @@ #include "util/log.h" #include "util/str_util.h" -static int +static bool build_cmd(char *cmd, size_t len, const char *const argv[]) { // Windows command-line parsing is WTF: // @@ -15,9 +15,9 @@ build_cmd(char *cmd, size_t len, const char *const argv[]) { size_t ret = xstrjoin(cmd, argv, ' ', len); if (ret >= len) { LOGE("Command too long (%" PRIsizet " chars)", len - 1); - return -1; + return false; } - return 0; + return true; } enum process_result @@ -28,7 +28,7 @@ process_execute(const char *const argv[], HANDLE *handle) { si.cb = sizeof(si); char cmd[256]; - if (build_cmd(cmd, sizeof(cmd), argv)) { + if (!build_cmd(cmd, sizeof(cmd), argv)) { *handle = NULL; return PROCESS_ERROR_GENERIC; } From fda32928c1bc02afb2c1068084c228565305ce87 Mon Sep 17 00:00:00 2001 From: Wirtos_new Date: Sat, 19 Jun 2021 18:47:57 +0300 Subject: [PATCH 0631/2244] Rename cmd to argv This is more explicit. PR #2405 Signed-off-by: Romain Vimont --- app/src/adb.c | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/src/adb.c b/app/src/adb.c index be973c41..44c80be6 100644 --- a/app/src/adb.c +++ b/app/src/adb.c @@ -102,23 +102,23 @@ show_adb_err_msg(enum process_result err, const char *const argv[]) { process_t adb_execute(const char *serial, const char *const adb_cmd[], size_t len) { - const char *cmd[len + 4]; + const char *argv[len + 4]; int i; process_t process; - cmd[0] = get_adb_command(); + argv[0] = get_adb_command(); if (serial) { - cmd[1] = "-s"; - cmd[2] = serial; + argv[1] = "-s"; + argv[2] = serial; i = 3; } else { i = 1; } - memcpy(&cmd[i], adb_cmd, len * sizeof(const char *)); - cmd[len + i] = NULL; - enum process_result r = process_execute(cmd, &process); + memcpy(&argv[i], adb_cmd, len * sizeof(const char *)); + argv[len + i] = NULL; + enum process_result r = process_execute(argv, &process); if (r != PROCESS_SUCCESS) { - show_adb_err_msg(r, cmd); + show_adb_err_msg(r, argv); return PROCESS_NONE; } return process; From a9d9cbf8b502cb537a1c7caaf2689be9eca3af60 Mon Sep 17 00:00:00 2001 From: Wirtos_new Date: Sat, 19 Jun 2021 18:47:57 +0300 Subject: [PATCH 0632/2244] Replace VLA by dynamic allocation And increase the command buffer size. Refs #1358 PR #2405 Signed-off-by: Romain Vimont --- app/src/adb.c | 25 ++++++++++++++++++++----- app/src/sys/win/process.c | 7 +++++-- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/app/src/adb.c b/app/src/adb.c index 44c80be6..5bb9df30 100644 --- a/app/src/adb.c +++ b/app/src/adb.c @@ -81,14 +81,20 @@ show_adb_installation_msg() { static void show_adb_err_msg(enum process_result err, const char *const argv[]) { - char buf[512]; +#define MAX_COMMAND_STRING_LEN 1024 + char *buf = malloc(MAX_COMMAND_STRING_LEN); + if (!buf) { + LOGE("Failed to execute (could not allocate error message)"); + return; + } + switch (err) { case PROCESS_ERROR_GENERIC: - argv_to_string(argv, buf, sizeof(buf)); + argv_to_string(argv, buf, MAX_COMMAND_STRING_LEN); LOGE("Failed to execute: %s", buf); break; case PROCESS_ERROR_MISSING_BINARY: - argv_to_string(argv, buf, sizeof(buf)); + argv_to_string(argv, buf, MAX_COMMAND_STRING_LEN); LOGE("Command not found: %s", buf); LOGE("(make 'adb' accessible from your PATH or define its full" "path in the ADB environment variable)"); @@ -98,13 +104,20 @@ show_adb_err_msg(enum process_result err, const char *const argv[]) { // do nothing break; } + + free(buf); } process_t adb_execute(const char *serial, const char *const adb_cmd[], size_t len) { - const char *argv[len + 4]; int i; process_t process; + + const char **argv = malloc((len + 4) * sizeof(*argv)); + if (!argv) { + return PROCESS_NONE; + } + argv[0] = get_adb_command(); if (serial) { argv[1] = "-s"; @@ -119,8 +132,10 @@ adb_execute(const char *serial, const char *const adb_cmd[], size_t len) { enum process_result r = process_execute(argv, &process); if (r != PROCESS_SUCCESS) { show_adb_err_msg(r, argv); - return PROCESS_NONE; + process = PROCESS_NONE; } + + free(argv); return process; } diff --git a/app/src/sys/win/process.c b/app/src/sys/win/process.c index 7f5da6af..aafd5d34 100644 --- a/app/src/sys/win/process.c +++ b/app/src/sys/win/process.c @@ -6,6 +6,8 @@ #include "util/log.h" #include "util/str_util.h" +#define CMD_MAX_LEN 8192 + static bool build_cmd(char *cmd, size_t len, const char *const argv[]) { // Windows command-line parsing is WTF: @@ -27,13 +29,14 @@ process_execute(const char *const argv[], HANDLE *handle) { memset(&si, 0, sizeof(si)); si.cb = sizeof(si); - char cmd[256]; - if (!build_cmd(cmd, sizeof(cmd), argv)) { + char *cmd = malloc(CMD_MAX_LEN); + if (!cmd || !build_cmd(cmd, CMD_MAX_LEN, argv)) { *handle = NULL; return PROCESS_ERROR_GENERIC; } wchar_t *wide = utf8_to_wide_char(cmd); + free(cmd); if (!wide) { LOGC("Could not allocate wide char string"); return PROCESS_ERROR_GENERIC; From ff7baad7093f4574eb04e82cebd7c1829310b663 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 11 Jun 2021 19:31:13 +0200 Subject: [PATCH 0633/2244] Upgrade platform-tools (31.0.2) 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 356a944e..d75d0a5c 100644 --- a/prebuilt-deps/Makefile +++ b/prebuilt-deps/Makefile @@ -35,6 +35,6 @@ prepare-sdl2: SDL2-2.0.14 prepare-adb: - @./prepare-dep https://dl.google.com/android/repository/platform-tools_r30.0.5-windows.zip \ - 549ba2bdc31f335eb8a504f005f77606a479cc216d6b64a3e8b64c780003661f \ + @./prepare-dep https://dl.google.com/android/repository/platform-tools_r31.0.2-windows.zip \ + d560cb8ded83ae04763b94632673481f14843a5969256569623cfeac82db4ba5 \ platform-tools From 60c4e886d409ec8a0688e2fd43d8d12c06ebd8df Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 20 Jun 2021 22:01:07 +0200 Subject: [PATCH 0634/2244] Bump version to 1.18 Make the versionCode a decimal representation of the scrcpy version. This will for example allow to correctly number the versionCode of v1.17.1 after a v1.18 is released: - v1.18 -> 11800 - v1.17.1 -> 11701 - v1.18.1 -> 11801 --- 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 c2989ec7..2d76f1e9 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('scrcpy', 'c', - version: '1.17', + version: '1.18', meson_version: '>= 0.48', default_options: [ 'c_std=c11', diff --git a/server/build.gradle b/server/build.gradle index 08ce2d27..f088ba9d 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -6,8 +6,8 @@ android { applicationId "com.genymobile.scrcpy" minSdkVersion 21 targetSdkVersion 30 - versionCode 20 - versionName "1.17" + versionCode 11800 + versionName "1.18" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index 9afc5ba7..302d3aaa 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.17 +SCRCPY_VERSION_NAME=1.18 PLATFORM=${ANDROID_PLATFORM:-30} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-30.0.0} From 376201a83cfd3960027e471c9383d1a1f554ac20 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 20 Jun 2021 22:04:52 +0200 Subject: [PATCH 0635/2244] Update links to v1.18 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 d5aead0f..87078b71 100644 --- a/BUILD.md +++ b/BUILD.md @@ -268,10 +268,10 @@ install` must be run as root)._ #### Option 2: Use prebuilt server - - [`scrcpy-server-v1.17`][direct-scrcpy-server] - _(SHA-256: 11b5ad2d1bc9b9730fb7254a78efd71a8ff46b1938ff468e47a21b653a1b6725)_ + - [`scrcpy-server-v1.18`][direct-scrcpy-server] + _(SHA-256: 641c5c6beda9399dfae72d116f5ff43b5ed1059d871c9ebc3f47610fd33c51a3)_ -[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.17/scrcpy-server-v1.17 +[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.18/scrcpy-server-v1.18 Download the prebuilt server somewhere, and specify its path during the Meson configuration: diff --git a/README.md b/README.md index 36426afa..0dfa068c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# scrcpy (v1.17) +# scrcpy (v1.18) [Read in another language](#translations) @@ -88,10 +88,10 @@ process][BUILD_simple]). For Windows, for simplicity, a prebuilt archive with all the dependencies (including `adb`) is available: - - [`scrcpy-win64-v1.17.zip`][direct-win64] - _(SHA-256: 8b9e57993c707367ed10ebfe0e1ef563c7a29d9af4a355cd8b6a52a317c73eea)_ + - [`scrcpy-win64-v1.18.zip`][direct-win64] + _(SHA-256: 37212f5087fe6f3e258f1d44fa5c02207496b30e1d7ec442cbcf8358910a5c63)_ -[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.17/scrcpy-win64-v1.17.zip +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.18/scrcpy-win64-v1.18.zip It is also available in [Chocolatey]: From ab12b6c981d62aac2c4c86c5436378970723cc38 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 21 Jun 2021 09:47:15 +0200 Subject: [PATCH 0636/2244] Update scrcpy-server in install-release.sh Make the install script download the new prebuilt server (v1.18). Fixes #2409 --- install_release.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/install_release.sh b/install_release.sh index 5179c447..9158bdd4 100755 --- a/install_release.sh +++ b/install_release.sh @@ -2,8 +2,8 @@ set -e BUILDDIR=build-auto -PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v1.17/scrcpy-server-v1.17 -PREBUILT_SERVER_SHA256=11b5ad2d1bc9b9730fb7254a78efd71a8ff46b1938ff468e47a21b653a1b6725 +PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v1.18/scrcpy-server-v1.18 +PREBUILT_SERVER_SHA256=641c5c6beda9399dfae72d116f5ff43b5ed1059d871c9ebc3f47610fd33c51a3 echo "[scrcpy] Downloading prebuilt server..." wget "$PREBUILT_SERVER_URL" -O scrcpy-server From 7dca5078e75a913b76ecde794033791eaf1843f6 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 25 Jun 2021 21:43:44 +0200 Subject: [PATCH 0637/2244] Initialize controller even if there is no display The options --no-display and --no-control are independent. The controller was not initialized when no display was requested, because it was assumed that no control could occur without display. But that's not true (anymore): for example, it is possible to pass --turn-screen-off. Fixes #2426 --- app/src/scrcpy.c | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 4dcb412f..d0a22e77 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -343,19 +343,29 @@ scrcpy(const struct scrcpy_options *options) { stream_add_sink(&s->stream, &rec->packet_sink); } - if (options->display) { - if (options->control) { - if (!controller_init(&s->controller, s->server.control_socket)) { - goto end; - } - controller_initialized = true; - - if (!controller_start(&s->controller)) { - goto end; - } - controller_started = true; + if (options->control) { + if (!controller_init(&s->controller, s->server.control_socket)) { + goto end; } + controller_initialized = true; + if (!controller_start(&s->controller)) { + goto end; + } + controller_started = true; + + if (options->turn_screen_off) { + struct control_msg msg; + msg.type = CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE; + msg.set_screen_power_mode.mode = SCREEN_POWER_MODE_OFF; + + if (!controller_push_msg(&s->controller, &msg)) { + LOGW("Could not request 'set screen power mode'"); + } + } + } + + if (options->display) { const char *window_title = options->window_title ? options->window_title : device_name; @@ -379,16 +389,6 @@ scrcpy(const struct scrcpy_options *options) { screen_initialized = true; decoder_add_sink(&s->decoder, &s->screen.frame_sink); - - if (options->turn_screen_off) { - struct control_msg msg; - msg.type = CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE; - msg.set_screen_power_mode.mode = SCREEN_POWER_MODE_OFF; - - if (!controller_push_msg(&s->controller, &msg)) { - LOGW("Could not request 'set screen power mode'"); - } - } } #ifdef HAVE_V4L2 From f33d37976cab056e7fe3d86f275095782a5234e9 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 26 Jun 2021 15:29:08 +0200 Subject: [PATCH 0638/2244] Fix assertion race condition in debug mode Commit 21d206f360535047e16cc4ea7de889a55bd9444d added mutex assertions. However, the "locker" variable to trace the locker thread id was read and written by several threads without any protection, so it was racy. Reported by TSAN. --- app/src/util/thread.c | 17 +++++++++++------ app/src/util/thread.h | 6 ++++-- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/app/src/util/thread.c b/app/src/util/thread.c index a0a99f20..de05365d 100644 --- a/app/src/util/thread.c +++ b/app/src/util/thread.c @@ -31,7 +31,7 @@ sc_mutex_init(sc_mutex *mutex) { mutex->mutex = sdl_mutex; #ifndef NDEBUG - mutex->locker = 0; + atomic_init(&mutex->locker, 0); #endif return true; } @@ -52,7 +52,8 @@ sc_mutex_lock(sc_mutex *mutex) { abort(); } - mutex->locker = sc_thread_get_id(); + atomic_store_explicit(&mutex->locker, sc_thread_get_id(), + memory_order_relaxed); #else (void) r; #endif @@ -62,7 +63,7 @@ void sc_mutex_unlock(sc_mutex *mutex) { #ifndef NDEBUG assert(sc_mutex_held(mutex)); - mutex->locker = 0; + atomic_store_explicit(&mutex->locker, 0, memory_order_relaxed); #endif int r = SDL_UnlockMutex(mutex->mutex); #ifndef NDEBUG @@ -83,7 +84,9 @@ sc_thread_get_id(void) { #ifndef NDEBUG bool sc_mutex_held(struct sc_mutex *mutex) { - return mutex->locker == sc_thread_get_id(); + sc_thread_id locker_id = + atomic_load_explicit(&mutex->locker, memory_order_relaxed); + return locker_id == sc_thread_get_id(); } #endif @@ -112,7 +115,8 @@ sc_cond_wait(sc_cond *cond, sc_mutex *mutex) { abort(); } - mutex->locker = sc_thread_get_id(); + atomic_store_explicit(&mutex->locker, sc_thread_get_id(), + memory_order_relaxed); #else (void) r; #endif @@ -127,7 +131,8 @@ sc_cond_timedwait(sc_cond *cond, sc_mutex *mutex, uint32_t ms) { abort(); } - mutex->locker = sc_thread_get_id(); + atomic_store_explicit(&mutex->locker, sc_thread_get_id(), + memory_order_relaxed); #endif assert(r == 0 || r == SDL_MUTEX_TIMEDOUT); return r == 0; diff --git a/app/src/util/thread.h b/app/src/util/thread.h index d23e1432..dd3a630e 100644 --- a/app/src/util/thread.h +++ b/app/src/util/thread.h @@ -3,6 +3,7 @@ #include "common.h" +#include #include #include @@ -12,7 +13,8 @@ typedef struct SDL_mutex SDL_mutex; typedef struct SDL_cond SDL_cond; typedef int sc_thread_fn(void *); -typedef unsigned int sc_thread_id; +typedef unsigned sc_thread_id; +typedef atomic_uint sc_atomic_thread_id; typedef struct sc_thread { SDL_Thread *thread; @@ -21,7 +23,7 @@ typedef struct sc_thread { typedef struct sc_mutex { SDL_mutex *mutex; #ifndef NDEBUG - sc_thread_id locker; + sc_atomic_thread_id locker; #endif } sc_mutex; From 33fbdc86c7c61d34219c702b66419d476e028392 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 26 Jun 2021 15:54:27 +0200 Subject: [PATCH 0639/2244] Initialize fields before starting a thread To avoid data races. Reported by TSAN. --- app/src/v4l2_sink.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/v4l2_sink.c b/app/src/v4l2_sink.c index bd184b4d..a276b79c 100644 --- a/app/src/v4l2_sink.c +++ b/app/src/v4l2_sink.c @@ -241,6 +241,9 @@ sc_v4l2_sink_open(struct sc_v4l2_sink *vs) { goto error_av_frame_free; } + vs->header_written = false; + vs->stopped = false; + LOGD("Starting v4l2 thread"); ok = sc_thread_create(&vs->thread, run_v4l2_sink, "v4l2", vs); if (!ok) { @@ -248,9 +251,6 @@ sc_v4l2_sink_open(struct sc_v4l2_sink *vs) { goto error_av_packet_free; } - vs->header_written = false; - vs->stopped = false; - LOGI("v4l2 sink started to device: %s", vs->device_name); return true; From 5caeab5f6d12fe0fc69e708a829cfdbec3a401b4 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 26 Jun 2021 15:53:05 +0200 Subject: [PATCH 0640/2244] Fix v4l2 data race The v4l2_sink implementation directly read the internal video_buffer field "pending_frame_consumed", which is protected by the internal video_buffer mutex. But this mutex was not locked, so reads were racy. Lock using the v4l2_sink mutex in addition, and use a separate field to avoid depending on the video_buffer internal data. --- app/src/v4l2_sink.c | 13 ++++++++++--- app/src/v4l2_sink.h | 1 + 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/app/src/v4l2_sink.c b/app/src/v4l2_sink.c index a276b79c..1f6c264c 100644 --- a/app/src/v4l2_sink.c +++ b/app/src/v4l2_sink.c @@ -112,7 +112,7 @@ run_v4l2_sink(void *data) { for (;;) { sc_mutex_lock(&vs->mutex); - while (!vs->stopped && vs->vb.pending_frame_consumed) { + while (!vs->stopped && !vs->has_frame) { sc_cond_wait(&vs->cond, &vs->mutex); } @@ -121,9 +121,11 @@ run_v4l2_sink(void *data) { break; } + video_buffer_consume(&vs->vb, vs->frame); + vs->has_frame = false; + sc_mutex_unlock(&vs->mutex); - video_buffer_consume(&vs->vb, vs->frame); bool ok = encode_and_write_frame(vs, vs->frame); av_frame_unref(vs->frame); if (!ok) { @@ -241,6 +243,7 @@ sc_v4l2_sink_open(struct sc_v4l2_sink *vs) { goto error_av_frame_free; } + vs->has_frame = false; vs->header_written = false; vs->stopped = false; @@ -299,14 +302,18 @@ sc_v4l2_sink_close(struct sc_v4l2_sink *vs) { static bool sc_v4l2_sink_push(struct sc_v4l2_sink *vs, const AVFrame *frame) { + sc_mutex_lock(&vs->mutex); + bool ok = video_buffer_push(&vs->vb, frame, NULL); if (!ok) { return false; } - // signal possible change of vs->vb.pending_frame_consumed + vs->has_frame = true; sc_cond_signal(&vs->cond); + sc_mutex_unlock(&vs->mutex); + return true; } diff --git a/app/src/v4l2_sink.h b/app/src/v4l2_sink.h index 81bcdd1e..aad25926 100644 --- a/app/src/v4l2_sink.h +++ b/app/src/v4l2_sink.h @@ -22,6 +22,7 @@ struct sc_v4l2_sink { sc_thread thread; sc_mutex mutex; sc_cond cond; + bool has_frame; bool stopped; bool header_written; From e9096e3e342a1b510573be5b9c009c651db829b1 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 4 Jul 2021 12:19:52 +0200 Subject: [PATCH 0641/2244] Remove unnecessary calls to av_packet_unref() av_packet_free() already calls av_packet_unref(). --- app/src/recorder.c | 1 - app/src/stream.c | 2 -- 2 files changed, 3 deletions(-) diff --git a/app/src/recorder.c b/app/src/recorder.c index 85570324..0cf87934 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -51,7 +51,6 @@ record_packet_new(const AVPacket *packet) { static void record_packet_delete(struct record_packet *rec) { - av_packet_unref(rec->packet); av_packet_free(&rec->packet); free(rec); } diff --git a/app/src/stream.c b/app/src/stream.c index d1b8b9f3..adc6277f 100644 --- a/app/src/stream.c +++ b/app/src/stream.c @@ -151,7 +151,6 @@ stream_push_packet(struct stream *stream, AVPacket *packet) { if (stream->pending) { // the pending packet must be discarded (consumed or error) - av_packet_unref(stream->pending); av_packet_free(&stream->pending); } @@ -244,7 +243,6 @@ run_stream(void *data) { LOGD("End of frames"); if (stream->pending) { - av_packet_unref(stream->pending); av_packet_free(&stream->pending); } From 5938e862a1c9fe09554c5238684272be4be54945 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 6 Jul 2021 18:32:40 +0200 Subject: [PATCH 0642/2244] Fix --lock-video-orientation syntax in help Commit f76fe2c0d4847d0e40d8708f97591abf3fa22ea5 fixed README and tests. Fix command line help and manpage. --- 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 62dc9677..08adfc27 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -83,7 +83,7 @@ Inject computer clipboard text as a sequence of key events on Ctrl+v (like MOD+S This is a workaround for some devices not behaving as expected when setting the device clipboard programmatically. .TP -.BI "\-\-lock\-video\-orientation " [value] +.BI "\-\-lock\-video\-orientation[=value] Lock video orientation to \fIvalue\fR. Possible values are "unlocked", "initial" (locked to the initial orientation), 0, 1, 2 and 3. Natural device orientation is 0, and each increment adds a 90 degrees otation counterclockwise. Default is "unlocked". diff --git a/app/src/cli.c b/app/src/cli.c index 3eab8d27..ab35745d 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -79,7 +79,7 @@ scrcpy_print_usage(const char *arg0) { " This is a workaround for some devices not behaving as\n" " expected when setting the device clipboard programmatically.\n" "\n" - " --lock-video-orientation [value]\n" + " --lock-video-orientation[=value]\n" " Lock video orientation to value.\n" " Possible values are \"unlocked\", \"initial\" (locked to the\n" " initial orientation), 0, 1, 2 and 3.\n" From af8a21ed7c4e242f8c7d4cb5ba208622b85ad512 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 6 Jul 2021 18:33:04 +0200 Subject: [PATCH 0643/2244] Fix manpage formatting --- app/scrcpy.1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 08adfc27..253dd04f 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -189,7 +189,7 @@ It only shows physical touches (not clicks from scrcpy). .BI "\-\-v4l2-sink " /dev/videoN Output to v4l2loopback device. -It requires to lock the video orientation (see --lock-video-orientation). +It requires to lock the video orientation (see \fB\-\-lock\-video\-orientation\fR). .TP .BI "\-V, \-\-verbosity " value @@ -240,7 +240,7 @@ Default is 0 (automatic). .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 (see above). +Alt or (left) Super, but it can be configured by \fB\-\-shortcut\-mod\fR (see above). .TP .B MOD+f From 099cba07f0349898938ef5a77753e65b3caa3e46 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 4 Jul 2021 15:05:08 +0200 Subject: [PATCH 0644/2244] Rename queue to sc_queue Add a scrcpy-specific prefix. --- app/src/recorder.c | 14 +++++++------- app/src/recorder.h | 2 +- app/src/util/queue.h | 32 ++++++++++++++++---------------- app/tests/test_queue.c | 20 ++++++++++---------- 4 files changed, 34 insertions(+), 34 deletions(-) diff --git a/app/src/recorder.c b/app/src/recorder.c index 0cf87934..f2775c3a 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -57,9 +57,9 @@ record_packet_delete(struct record_packet *rec) { static void recorder_queue_clear(struct recorder_queue *queue) { - while (!queue_is_empty(queue)) { + while (!sc_queue_is_empty(queue)) { struct record_packet *rec; - queue_take(queue, next, &rec); + sc_queue_take(queue, next, &rec); record_packet_delete(rec); } } @@ -135,14 +135,14 @@ run_recorder(void *data) { for (;;) { sc_mutex_lock(&recorder->mutex); - while (!recorder->stopped && queue_is_empty(&recorder->queue)) { + while (!recorder->stopped && sc_queue_is_empty(&recorder->queue)) { sc_cond_wait(&recorder->queue_cond, &recorder->mutex); } // if stopped is set, continue to process the remaining events (to // finish the recording) before actually stopping - if (recorder->stopped && queue_is_empty(&recorder->queue)) { + if (recorder->stopped && sc_queue_is_empty(&recorder->queue)) { sc_mutex_unlock(&recorder->mutex); struct record_packet *last = recorder->previous; if (last) { @@ -161,7 +161,7 @@ run_recorder(void *data) { } struct record_packet *rec; - queue_take(&recorder->queue, next, &rec); + sc_queue_take(&recorder->queue, next, &rec); sc_mutex_unlock(&recorder->mutex); @@ -235,7 +235,7 @@ recorder_open(struct recorder *recorder, const AVCodec *input_codec) { goto error_mutex_destroy; } - queue_init(&recorder->queue); + sc_queue_init(&recorder->queue); recorder->stopped = false; recorder->failed = false; recorder->header_written = false; @@ -340,7 +340,7 @@ recorder_push(struct recorder *recorder, const AVPacket *packet) { return false; } - queue_push(&recorder->queue, next, rec); + sc_queue_push(&recorder->queue, next, rec); sc_cond_signal(&recorder->queue_cond); sc_mutex_unlock(&recorder->mutex); diff --git a/app/src/recorder.h b/app/src/recorder.h index 0c376cd1..96caaf5f 100644 --- a/app/src/recorder.h +++ b/app/src/recorder.h @@ -17,7 +17,7 @@ struct record_packet { struct record_packet *next; }; -struct recorder_queue QUEUE(struct record_packet); +struct recorder_queue SC_QUEUE(struct record_packet); struct recorder { struct sc_packet_sink packet_sink; // packet sink trait diff --git a/app/src/util/queue.h b/app/src/util/queue.h index 0681070c..2233eca0 100644 --- a/app/src/util/queue.h +++ b/app/src/util/queue.h @@ -1,6 +1,6 @@ // generic intrusive FIFO queue -#ifndef QUEUE_H -#define QUEUE_H +#ifndef SC_QUEUE_H +#define SC_QUEUE_H #include "common.h" @@ -10,15 +10,15 @@ // To define a queue type of "struct foo": // struct queue_foo QUEUE(struct foo); -#define QUEUE(TYPE) { \ +#define SC_QUEUE(TYPE) { \ TYPE *first; \ TYPE *last; \ } -#define queue_init(PQ) \ +#define sc_queue_init(PQ) \ (void) ((PQ)->first = (PQ)->last = NULL) -#define queue_is_empty(PQ) \ +#define sc_queue_is_empty(PQ) \ !(PQ)->first // NEXTFIELD is the field in the ITEM type used for intrusive linked-list @@ -30,30 +30,30 @@ // }; // // // define the type "struct my_queue" -// struct my_queue QUEUE(struct foo); +// struct my_queue SC_QUEUE(struct foo); // // struct my_queue queue; -// queue_init(&queue); +// sc_queue_init(&queue); // // struct foo v1 = { .value = 42 }; // struct foo v2 = { .value = 27 }; // -// queue_push(&queue, next, v1); -// queue_push(&queue, next, v2); +// sc_queue_push(&queue, next, v1); +// sc_queue_push(&queue, next, v2); // // struct foo *foo; -// queue_take(&queue, next, &foo); +// sc_queue_take(&queue, next, &foo); // assert(foo->value == 42); -// queue_take(&queue, next, &foo); +// sc_queue_take(&queue, next, &foo); // assert(foo->value == 27); -// assert(queue_is_empty(&queue)); +// assert(sc_queue_is_empty(&queue)); // // push a new item into the queue -#define queue_push(PQ, NEXTFIELD, ITEM) \ +#define sc_queue_push(PQ, NEXTFIELD, ITEM) \ (void) ({ \ (ITEM)->NEXTFIELD = NULL; \ - if (queue_is_empty(PQ)) { \ + if (sc_queue_is_empty(PQ)) { \ (PQ)->first = (PQ)->last = (ITEM); \ } else { \ (PQ)->last->NEXTFIELD = (ITEM); \ @@ -65,9 +65,9 @@ // the result is stored in *(PITEM) // (without typeof(), we could not store a local variable having the correct // type so that we can "return" it) -#define queue_take(PQ, NEXTFIELD, PITEM) \ +#define sc_queue_take(PQ, NEXTFIELD, PITEM) \ (void) ({ \ - assert(!queue_is_empty(PQ)); \ + assert(!sc_queue_is_empty(PQ)); \ *(PITEM) = (PQ)->first; \ (PQ)->first = (PQ)->first->NEXTFIELD; \ }) diff --git a/app/tests/test_queue.c b/app/tests/test_queue.c index fcbafc62..d8b2b4ec 100644 --- a/app/tests/test_queue.c +++ b/app/tests/test_queue.c @@ -10,28 +10,28 @@ struct foo { }; static void test_queue(void) { - struct my_queue QUEUE(struct foo) queue; - queue_init(&queue); + struct my_queue SC_QUEUE(struct foo) queue; + sc_queue_init(&queue); - assert(queue_is_empty(&queue)); + assert(sc_queue_is_empty(&queue)); struct foo v1 = { .value = 42 }; struct foo v2 = { .value = 27 }; - queue_push(&queue, next, &v1); - queue_push(&queue, next, &v2); + sc_queue_push(&queue, next, &v1); + sc_queue_push(&queue, next, &v2); struct foo *foo; - assert(!queue_is_empty(&queue)); - queue_take(&queue, next, &foo); + assert(!sc_queue_is_empty(&queue)); + sc_queue_take(&queue, next, &foo); assert(foo->value == 42); - assert(!queue_is_empty(&queue)); - queue_take(&queue, next, &foo); + assert(!sc_queue_is_empty(&queue)); + sc_queue_take(&queue, next, &foo); assert(foo->value == 27); - assert(queue_is_empty(&queue)); + assert(sc_queue_is_empty(&queue)); } int main(int argc, char *argv[]) { From 40cea1f677d35c6f7fe8c86d1d2bef1a313da9f5 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 26 Jun 2021 14:34:07 +0200 Subject: [PATCH 0645/2244] Remove obsolete comment Commit 2a94a2b119a9efd2cd06ded03280872c81abe6b4 removed video_buffer callbacks, the comment is now meaningless. --- app/src/video_buffer.h | 6 ------ 1 file changed, 6 deletions(-) diff --git a/app/src/video_buffer.h b/app/src/video_buffer.h index b9478f4c..b957bf1e 100644 --- a/app/src/video_buffer.h +++ b/app/src/video_buffer.h @@ -18,12 +18,6 @@ typedef struct AVFrame AVFrame; * If a pending frame has not been consumed when the producer pushes a new * frame, then it is lost. The intent is to always provide access to the very * last frame to minimize latency. - * - * The producer and the consumer typically do not live in the same thread. - * That's the reason why the callback on_frame_available() does not provide the - * frame as parameter: the consumer might post an event to its own thread to - * retrieve the pending frame from there, and that frame may have changed since - * the callback if producer pushed a new one in between. */ struct video_buffer { From 4ed3aa3604d0e78ef8b134bb94258efa672eb330 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 26 Jun 2021 14:38:36 +0200 Subject: [PATCH 0646/2244] Move include fps_counter The fps_counter is not used from video_buffer. --- app/src/screen.h | 1 + app/src/video_buffer.h | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/screen.h b/app/src/screen.h index e2a43da7..8acabebe 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -8,6 +8,7 @@ #include #include "coords.h" +#include "fps_counter.h" #include "opengl.h" #include "trait/frame_sink.h" #include "video_buffer.h" diff --git a/app/src/video_buffer.h b/app/src/video_buffer.h index b957bf1e..c4d1d1a4 100644 --- a/app/src/video_buffer.h +++ b/app/src/video_buffer.h @@ -5,7 +5,6 @@ #include -#include "fps_counter.h" #include "util/thread.h" // forward declarations From 5524f378c8907355ee6df17e435d0bc1f5f467cb Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 4 Jul 2021 15:47:39 +0200 Subject: [PATCH 0647/2244] Add missing error log Log video buffer initialization failure in v4l2_sink. --- app/src/v4l2_sink.c | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/v4l2_sink.c b/app/src/v4l2_sink.c index 1f6c264c..5ee9c8eb 100644 --- a/app/src/v4l2_sink.c +++ b/app/src/v4l2_sink.c @@ -143,6 +143,7 @@ static bool sc_v4l2_sink_open(struct sc_v4l2_sink *vs) { bool ok = video_buffer_init(&vs->vb); if (!ok) { + LOGE("Could not initialize video buffer"); return false; } From ec871dd3f596a8183e37982821645ac5a5791fe0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 4 Jul 2021 16:50:19 +0200 Subject: [PATCH 0648/2244] Wrap tick API This avoids to use the SDL timer API directly, and will allow to handle generic ticks (possibly negative). --- app/meson.build | 1 + app/src/fps_counter.c | 19 +++++++++---------- app/src/fps_counter.h | 2 +- app/src/server.c | 4 ++-- app/src/util/thread.c | 7 ++++++- app/src/util/thread.h | 5 +++-- app/src/util/tick.c | 16 ++++++++++++++++ app/src/util/tick.h | 20 ++++++++++++++++++++ 8 files changed, 58 insertions(+), 16 deletions(-) create mode 100644 app/src/util/tick.c create mode 100644 app/src/util/tick.h diff --git a/app/meson.build b/app/meson.build index 0663c641..6c94f33e 100644 --- a/app/meson.build +++ b/app/meson.build @@ -25,6 +25,7 @@ src = [ 'src/util/process.c', 'src/util/str_util.c', 'src/util/thread.c', + 'src/util/tick.c', ] if host_machine.system() == 'windows' diff --git a/app/src/fps_counter.c b/app/src/fps_counter.c index bbf71887..4fad6550 100644 --- a/app/src/fps_counter.c +++ b/app/src/fps_counter.c @@ -1,11 +1,10 @@ #include "fps_counter.h" #include -#include #include "util/log.h" -#define FPS_COUNTER_INTERVAL_MS 1000 +#define FPS_COUNTER_INTERVAL SC_TICK_FROM_SEC(1) bool fps_counter_init(struct fps_counter *counter) { @@ -47,7 +46,7 @@ set_started(struct fps_counter *counter, bool started) { static void display_fps(struct fps_counter *counter) { unsigned rendered_per_second = - counter->nr_rendered * 1000 / FPS_COUNTER_INTERVAL_MS; + counter->nr_rendered * SC_TICK_FREQ / FPS_COUNTER_INTERVAL; if (counter->nr_skipped) { LOGI("%u fps (+%u frames skipped)", rendered_per_second, counter->nr_skipped); @@ -68,8 +67,8 @@ check_interval_expired(struct fps_counter *counter, uint32_t now) { counter->nr_skipped = 0; // add a multiple of the interval uint32_t elapsed_slices = - (now - counter->next_timestamp) / FPS_COUNTER_INTERVAL_MS + 1; - counter->next_timestamp += FPS_COUNTER_INTERVAL_MS * elapsed_slices; + (now - counter->next_timestamp) / FPS_COUNTER_INTERVAL + 1; + counter->next_timestamp += FPS_COUNTER_INTERVAL * elapsed_slices; } static int @@ -82,11 +81,11 @@ run_fps_counter(void *data) { sc_cond_wait(&counter->state_cond, &counter->mutex); } while (!counter->interrupted && is_started(counter)) { - uint32_t now = SDL_GetTicks(); + sc_tick now = sc_tick_now(); check_interval_expired(counter, now); assert(counter->next_timestamp > now); - uint32_t remaining = counter->next_timestamp - now; + sc_tick remaining = counter->next_timestamp - now; // ignore the reason (timeout or signaled), we just loop anyway sc_cond_timedwait(&counter->state_cond, &counter->mutex, remaining); @@ -99,7 +98,7 @@ run_fps_counter(void *data) { bool fps_counter_start(struct fps_counter *counter) { sc_mutex_lock(&counter->mutex); - counter->next_timestamp = SDL_GetTicks() + FPS_COUNTER_INTERVAL_MS; + counter->next_timestamp = sc_tick_now() + FPS_COUNTER_INTERVAL; counter->nr_rendered = 0; counter->nr_skipped = 0; sc_mutex_unlock(&counter->mutex); @@ -165,7 +164,7 @@ fps_counter_add_rendered_frame(struct fps_counter *counter) { } sc_mutex_lock(&counter->mutex); - uint32_t now = SDL_GetTicks(); + sc_tick now = sc_tick_now(); check_interval_expired(counter, now); ++counter->nr_rendered; sc_mutex_unlock(&counter->mutex); @@ -178,7 +177,7 @@ fps_counter_add_skipped_frame(struct fps_counter *counter) { } sc_mutex_lock(&counter->mutex); - uint32_t now = SDL_GetTicks(); + sc_tick now = sc_tick_now(); check_interval_expired(counter, now); ++counter->nr_skipped; sc_mutex_unlock(&counter->mutex); diff --git a/app/src/fps_counter.h b/app/src/fps_counter.h index de252586..9609c814 100644 --- a/app/src/fps_counter.h +++ b/app/src/fps_counter.h @@ -24,7 +24,7 @@ struct fps_counter { bool interrupted; unsigned nr_rendered; unsigned nr_skipped; - uint32_t next_timestamp; + sc_tick next_timestamp; }; bool diff --git a/app/src/server.c b/app/src/server.c index a4cdb0c9..ca609e25 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -554,10 +554,10 @@ server_stop(struct server *server) { sc_mutex_lock(&server->mutex); bool signaled = false; if (!server->process_terminated) { -#define WATCHDOG_DELAY_MS 1000 +#define WATCHDOG_DELAY SC_TICK_FROM_SEC(1) signaled = sc_cond_timedwait(&server->process_terminated_cond, &server->mutex, - WATCHDOG_DELAY_MS); + WATCHDOG_DELAY); } sc_mutex_unlock(&server->mutex); diff --git a/app/src/util/thread.c b/app/src/util/thread.c index de05365d..07b1f6a6 100644 --- a/app/src/util/thread.c +++ b/app/src/util/thread.c @@ -123,7 +123,12 @@ sc_cond_wait(sc_cond *cond, sc_mutex *mutex) { } bool -sc_cond_timedwait(sc_cond *cond, sc_mutex *mutex, uint32_t ms) { +sc_cond_timedwait(sc_cond *cond, sc_mutex *mutex, sc_tick delay) { + if (delay < 0) { + return false; // timeout + } + + uint32_t ms = SC_TICK_TO_MS(delay); int r = SDL_CondWaitTimeout(cond->cond, mutex->mutex, ms); #ifndef NDEBUG if (r < 0) { diff --git a/app/src/util/thread.h b/app/src/util/thread.h index dd3a630e..a59c09a1 100644 --- a/app/src/util/thread.h +++ b/app/src/util/thread.h @@ -5,7 +5,8 @@ #include #include -#include + +#include "tick.h" /* Forward declarations */ typedef struct SDL_Thread SDL_Thread; @@ -72,7 +73,7 @@ sc_cond_wait(sc_cond *cond, sc_mutex *mutex); // return true on signaled, false on timeout bool -sc_cond_timedwait(sc_cond *cond, sc_mutex *mutex, uint32_t ms); +sc_cond_timedwait(sc_cond *cond, sc_mutex *mutex, sc_tick ms); void sc_cond_signal(sc_cond *cond); diff --git a/app/src/util/tick.c b/app/src/util/tick.c new file mode 100644 index 00000000..b85ce971 --- /dev/null +++ b/app/src/util/tick.c @@ -0,0 +1,16 @@ +#include "tick.h" + +#include + +sc_tick +sc_tick_now(void) { + // SDL_GetTicks() resolution is in milliseconds, but sc_tick are expressed + // in microseconds to store PTS without precision loss. + // + // As an alternative, SDL_GetPerformanceCounter() and + // SDL_GetPerformanceFrequency() could be used, but: + // - the conversions (avoiding overflow) are expansive, since the + // frequency is not known at compile time; + // - in practice, we don't need more precision for now. + return (sc_tick) SDL_GetTicks() * 1000; +} diff --git a/app/src/util/tick.h b/app/src/util/tick.h new file mode 100644 index 00000000..a7494458 --- /dev/null +++ b/app/src/util/tick.h @@ -0,0 +1,20 @@ +#ifndef SC_TICK_H +#define SC_TICK_H + +#include + +typedef int64_t sc_tick; +#define SC_TICK_FREQ 1000000 // microsecond + +// To be adapted if SC_TICK_FREQ changes +#define SC_TICK_TO_US(tick) (tick) +#define SC_TICK_TO_MS(tick) ((tick) / 1000) +#define SC_TICK_TO_SEC(tick) ((tick) / 1000000) +#define SC_TICK_FROM_US(us) (us) +#define SC_TICK_FROM_MS(ms) ((ms) * 1000) +#define SC_TICK_FROM_SEC(sec) ((sec) * 1000000) + +sc_tick +sc_tick_now(void); + +#endif From 32e692d5d2284cc0d890e43fc2d370fc277643dc Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 4 Jul 2021 17:04:20 +0200 Subject: [PATCH 0649/2244] Replace delay by deadline in timedwait() The function sc_cond_timedwait() accepted a parameter representing the max duration to wait, because it internally uses SDL_CondWaitTimeout(). Instead, accept a deadline, to be consistent with pthread_cond_timedwait(). --- app/src/fps_counter.c | 6 ++---- app/src/server.c | 2 +- app/src/util/thread.c | 7 ++++--- app/src/util/thread.h | 2 +- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/app/src/fps_counter.c b/app/src/fps_counter.c index 4fad6550..c92d4140 100644 --- a/app/src/fps_counter.c +++ b/app/src/fps_counter.c @@ -84,11 +84,9 @@ run_fps_counter(void *data) { sc_tick now = sc_tick_now(); check_interval_expired(counter, now); - assert(counter->next_timestamp > now); - sc_tick remaining = counter->next_timestamp - now; - // ignore the reason (timeout or signaled), we just loop anyway - sc_cond_timedwait(&counter->state_cond, &counter->mutex, remaining); + sc_cond_timedwait(&counter->state_cond, &counter->mutex, + counter->next_timestamp); } } sc_mutex_unlock(&counter->mutex); diff --git a/app/src/server.c b/app/src/server.c index ca609e25..4e0f2b23 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -557,7 +557,7 @@ server_stop(struct server *server) { #define WATCHDOG_DELAY SC_TICK_FROM_SEC(1) signaled = sc_cond_timedwait(&server->process_terminated_cond, &server->mutex, - WATCHDOG_DELAY); + sc_tick_now() + WATCHDOG_DELAY); } sc_mutex_unlock(&server->mutex); diff --git a/app/src/util/thread.c b/app/src/util/thread.c index 07b1f6a6..2c376e97 100644 --- a/app/src/util/thread.c +++ b/app/src/util/thread.c @@ -123,12 +123,13 @@ sc_cond_wait(sc_cond *cond, sc_mutex *mutex) { } bool -sc_cond_timedwait(sc_cond *cond, sc_mutex *mutex, sc_tick delay) { - if (delay < 0) { +sc_cond_timedwait(sc_cond *cond, sc_mutex *mutex, sc_tick deadline) { + sc_tick now = sc_tick_now(); + if (deadline <= now) { return false; // timeout } - uint32_t ms = SC_TICK_TO_MS(delay); + uint32_t ms = SC_TICK_TO_MS(deadline - now); int r = SDL_CondWaitTimeout(cond->cond, mutex->mutex, ms); #ifndef NDEBUG if (r < 0) { diff --git a/app/src/util/thread.h b/app/src/util/thread.h index a59c09a1..7add6f1c 100644 --- a/app/src/util/thread.h +++ b/app/src/util/thread.h @@ -73,7 +73,7 @@ sc_cond_wait(sc_cond *cond, sc_mutex *mutex); // return true on signaled, false on timeout bool -sc_cond_timedwait(sc_cond *cond, sc_mutex *mutex, sc_tick ms); +sc_cond_timedwait(sc_cond *cond, sc_mutex *mutex, sc_tick deadline); void sc_cond_signal(sc_cond *cond); From 28bce48d479be87ae72295fba0670d10d7eab697 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 4 Jul 2021 12:30:49 +0200 Subject: [PATCH 0650/2244] Relax v4l2_sink lock constraints To fix a data race, commit 5caeab5f6d12fe0fc69e708a829cfdbec3a401b4 called video_buffer_push() and video_buffer_consume() under the v4l2_sink lock. Instead, use the previous_skipped indication (initialized with video buffer locked) to lock only for protecting the has_frame flag. This enables the possibility for the video_buffer to notify new frames via callbacks without lock inversion issues. --- app/src/v4l2_sink.c | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/app/src/v4l2_sink.c b/app/src/v4l2_sink.c index 5ee9c8eb..7904b450 100644 --- a/app/src/v4l2_sink.c +++ b/app/src/v4l2_sink.c @@ -121,11 +121,11 @@ run_v4l2_sink(void *data) { break; } - video_buffer_consume(&vs->vb, vs->frame); vs->has_frame = false; - sc_mutex_unlock(&vs->mutex); + video_buffer_consume(&vs->vb, vs->frame); + bool ok = encode_and_write_frame(vs, vs->frame); av_frame_unref(vs->frame); if (!ok) { @@ -303,17 +303,18 @@ sc_v4l2_sink_close(struct sc_v4l2_sink *vs) { static bool sc_v4l2_sink_push(struct sc_v4l2_sink *vs, const AVFrame *frame) { - sc_mutex_lock(&vs->mutex); - - bool ok = video_buffer_push(&vs->vb, frame, NULL); + bool previous_skipped; + bool ok = video_buffer_push(&vs->vb, frame, &previous_skipped); if (!ok) { return false; } - vs->has_frame = true; - sc_cond_signal(&vs->cond); - - sc_mutex_unlock(&vs->mutex); + if (!previous_skipped) { + sc_mutex_lock(&vs->mutex); + vs->has_frame = true; + sc_cond_signal(&vs->cond); + sc_mutex_unlock(&vs->mutex); + } return true; } From 336248df08873e904574f6246ff636822bf6bb69 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 26 Jun 2021 15:02:18 +0200 Subject: [PATCH 0651/2244] Rename video_buffer to sc_video_buffer Add a scrcpy-specific prefix. --- app/src/screen.c | 10 +++++----- app/src/screen.h | 2 +- app/src/v4l2_sink.c | 10 +++++----- app/src/v4l2_sink.h | 2 +- app/src/video_buffer.c | 8 ++++---- app/src/video_buffer.h | 15 ++++++++------- 6 files changed, 24 insertions(+), 23 deletions(-) diff --git a/app/src/screen.c b/app/src/screen.c index 99327b3b..a11235c9 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -276,7 +276,7 @@ screen_frame_sink_push(struct sc_frame_sink *sink, const AVFrame *frame) { struct screen *screen = DOWNCAST(sink); bool previous_frame_skipped; - bool ok = video_buffer_push(&screen->vb, frame, &previous_frame_skipped); + bool ok = sc_video_buffer_push(&screen->vb, frame, &previous_frame_skipped); if (!ok) { return false; } @@ -304,7 +304,7 @@ screen_init(struct screen *screen, const struct screen_params *params) { screen->fullscreen = false; screen->maximized = false; - bool ok = video_buffer_init(&screen->vb); + bool ok = sc_video_buffer_init(&screen->vb); if (!ok) { LOGE("Could not initialize video buffer"); return false; @@ -454,7 +454,7 @@ error_destroy_window: error_destroy_fps_counter: fps_counter_destroy(&screen->fps_counter); error_destroy_video_buffer: - video_buffer_destroy(&screen->vb); + sc_video_buffer_destroy(&screen->vb); return false; } @@ -489,7 +489,7 @@ screen_destroy(struct screen *screen) { SDL_DestroyRenderer(screen->renderer); SDL_DestroyWindow(screen->window); fps_counter_destroy(&screen->fps_counter); - video_buffer_destroy(&screen->vb); + sc_video_buffer_destroy(&screen->vb); } static void @@ -595,7 +595,7 @@ update_texture(struct screen *screen, const AVFrame *frame) { static bool screen_update_frame(struct screen *screen) { av_frame_unref(screen->frame); - video_buffer_consume(&screen->vb, screen->frame); + sc_video_buffer_consume(&screen->vb, screen->frame); AVFrame *frame = screen->frame; fps_counter_add_rendered_frame(&screen->fps_counter); diff --git a/app/src/screen.h b/app/src/screen.h index 8acabebe..e38d65bc 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -20,7 +20,7 @@ struct screen { bool open; // track the open/close state to assert correct behavior #endif - struct video_buffer vb; + struct sc_video_buffer vb; struct fps_counter fps_counter; SDL_Window *window; diff --git a/app/src/v4l2_sink.c b/app/src/v4l2_sink.c index 7904b450..4b8ea043 100644 --- a/app/src/v4l2_sink.c +++ b/app/src/v4l2_sink.c @@ -124,7 +124,7 @@ run_v4l2_sink(void *data) { vs->has_frame = false; sc_mutex_unlock(&vs->mutex); - video_buffer_consume(&vs->vb, vs->frame); + sc_video_buffer_consume(&vs->vb, vs->frame); bool ok = encode_and_write_frame(vs, vs->frame); av_frame_unref(vs->frame); @@ -141,7 +141,7 @@ run_v4l2_sink(void *data) { static bool sc_v4l2_sink_open(struct sc_v4l2_sink *vs) { - bool ok = video_buffer_init(&vs->vb); + bool ok = sc_video_buffer_init(&vs->vb); if (!ok) { LOGE("Could not initialize video buffer"); return false; @@ -276,7 +276,7 @@ error_cond_destroy: error_mutex_destroy: sc_mutex_destroy(&vs->mutex); error_video_buffer_destroy: - video_buffer_destroy(&vs->vb); + sc_video_buffer_destroy(&vs->vb); return false; } @@ -298,13 +298,13 @@ sc_v4l2_sink_close(struct sc_v4l2_sink *vs) { avformat_free_context(vs->format_ctx); sc_cond_destroy(&vs->cond); sc_mutex_destroy(&vs->mutex); - video_buffer_destroy(&vs->vb); + sc_video_buffer_destroy(&vs->vb); } static bool sc_v4l2_sink_push(struct sc_v4l2_sink *vs, const AVFrame *frame) { bool previous_skipped; - bool ok = video_buffer_push(&vs->vb, frame, &previous_skipped); + bool ok = sc_video_buffer_push(&vs->vb, frame, &previous_skipped); if (!ok) { return false; } diff --git a/app/src/v4l2_sink.h b/app/src/v4l2_sink.h index aad25926..cf3fdddc 100644 --- a/app/src/v4l2_sink.h +++ b/app/src/v4l2_sink.h @@ -12,7 +12,7 @@ struct sc_v4l2_sink { struct sc_frame_sink frame_sink; // frame sink trait - struct video_buffer vb; + struct sc_video_buffer vb; AVFormatContext *format_ctx; AVCodecContext *encoder_ctx; diff --git a/app/src/video_buffer.c b/app/src/video_buffer.c index 7adf098b..a1d09cb8 100644 --- a/app/src/video_buffer.c +++ b/app/src/video_buffer.c @@ -7,7 +7,7 @@ #include "util/log.h" bool -video_buffer_init(struct video_buffer *vb) { +sc_video_buffer_init(struct sc_video_buffer *vb) { vb->pending_frame = av_frame_alloc(); if (!vb->pending_frame) { return false; @@ -33,7 +33,7 @@ video_buffer_init(struct video_buffer *vb) { } void -video_buffer_destroy(struct video_buffer *vb) { +sc_video_buffer_destroy(struct sc_video_buffer *vb) { sc_mutex_destroy(&vb->mutex); av_frame_free(&vb->pending_frame); av_frame_free(&vb->tmp_frame); @@ -47,7 +47,7 @@ swap_frames(AVFrame **lhs, AVFrame **rhs) { } bool -video_buffer_push(struct video_buffer *vb, const AVFrame *frame, +sc_video_buffer_push(struct sc_video_buffer *vb, const AVFrame *frame, bool *previous_frame_skipped) { sc_mutex_lock(&vb->mutex); @@ -75,7 +75,7 @@ video_buffer_push(struct video_buffer *vb, const AVFrame *frame, } void -video_buffer_consume(struct video_buffer *vb, AVFrame *dst) { +sc_video_buffer_consume(struct sc_video_buffer *vb, AVFrame *dst) { sc_mutex_lock(&vb->mutex); assert(!vb->pending_frame_consumed); vb->pending_frame_consumed = true; diff --git a/app/src/video_buffer.h b/app/src/video_buffer.h index c4d1d1a4..f4364e08 100644 --- a/app/src/video_buffer.h +++ b/app/src/video_buffer.h @@ -1,5 +1,5 @@ -#ifndef VIDEO_BUFFER_H -#define VIDEO_BUFFER_H +#ifndef SC_VIDEO_BUFFER_H +#define SC_VIDEO_BUFFER_H #include "common.h" @@ -19,7 +19,7 @@ typedef struct AVFrame AVFrame; * last frame to minimize latency. */ -struct video_buffer { +struct sc_video_buffer { AVFrame *pending_frame; AVFrame *tmp_frame; // To preserve the pending frame on error @@ -29,15 +29,16 @@ struct video_buffer { }; bool -video_buffer_init(struct video_buffer *vb); +sc_video_buffer_init(struct sc_video_buffer *vb); void -video_buffer_destroy(struct video_buffer *vb); +sc_video_buffer_destroy(struct sc_video_buffer *vb); bool -video_buffer_push(struct video_buffer *vb, const AVFrame *frame, bool *skipped); +sc_video_buffer_push(struct sc_video_buffer *vb, const AVFrame *frame, + bool *skipped); void -video_buffer_consume(struct video_buffer *vb, AVFrame *dst); +sc_video_buffer_consume(struct sc_video_buffer *vb, AVFrame *dst); #endif From 4d8bcfc68af15444ec587692c4393014598ea12d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 4 Jul 2021 12:39:03 +0200 Subject: [PATCH 0652/2244] Extract current video_buffer to frame_buffer The current video buffer only stores one pending frame. In order to add a new buffering feature, move this part to a separate "frame buffer". Keep the video_buffer, which currently delegates all its calls to the frame_buffer. --- app/meson.build | 1 + app/src/frame_buffer.c | 88 ++++++++++++++++++++++++++++++++++++++++++ app/src/frame_buffer.h | 44 +++++++++++++++++++++ app/src/video_buffer.c | 68 ++------------------------------ app/src/video_buffer.h | 18 +-------- 5 files changed, 139 insertions(+), 80 deletions(-) create mode 100644 app/src/frame_buffer.c create mode 100644 app/src/frame_buffer.h diff --git a/app/meson.build b/app/meson.build index 6c94f33e..7efd94a1 100644 --- a/app/meson.build +++ b/app/meson.build @@ -10,6 +10,7 @@ src = [ 'src/event_converter.c', 'src/file_handler.c', 'src/fps_counter.c', + 'src/frame_buffer.c', 'src/input_manager.c', 'src/opengl.c', 'src/receiver.c', diff --git a/app/src/frame_buffer.c b/app/src/frame_buffer.c new file mode 100644 index 00000000..33ca6227 --- /dev/null +++ b/app/src/frame_buffer.c @@ -0,0 +1,88 @@ +#include "frame_buffer.h" + +#include +#include +#include + +#include "util/log.h" + +bool +sc_frame_buffer_init(struct sc_frame_buffer *fb) { + fb->pending_frame = av_frame_alloc(); + if (!fb->pending_frame) { + return false; + } + + fb->tmp_frame = av_frame_alloc(); + if (!fb->tmp_frame) { + av_frame_free(&fb->pending_frame); + return false; + } + + bool ok = sc_mutex_init(&fb->mutex); + if (!ok) { + av_frame_free(&fb->pending_frame); + av_frame_free(&fb->tmp_frame); + return false; + } + + // there is initially no frame, so consider it has already been consumed + fb->pending_frame_consumed = true; + + return true; +} + +void +sc_frame_buffer_destroy(struct sc_frame_buffer *fb) { + sc_mutex_destroy(&fb->mutex); + av_frame_free(&fb->pending_frame); + av_frame_free(&fb->tmp_frame); +} + +static inline void +swap_frames(AVFrame **lhs, AVFrame **rhs) { + AVFrame *tmp = *lhs; + *lhs = *rhs; + *rhs = tmp; +} + +bool +sc_frame_buffer_push(struct sc_frame_buffer *fb, const AVFrame *frame, + bool *previous_frame_skipped) { + sc_mutex_lock(&fb->mutex); + + // Use a temporary frame to preserve pending_frame in case of error. + // tmp_frame is an empty frame, no need to call av_frame_unref() beforehand. + int r = av_frame_ref(fb->tmp_frame, frame); + if (r) { + LOGE("Could not ref frame: %d", r); + return false; + } + + // Now that av_frame_ref() succeeded, we can replace the previous + // pending_frame + swap_frames(&fb->pending_frame, &fb->tmp_frame); + av_frame_unref(fb->tmp_frame); + + if (previous_frame_skipped) { + *previous_frame_skipped = !fb->pending_frame_consumed; + } + fb->pending_frame_consumed = false; + + sc_mutex_unlock(&fb->mutex); + + return true; +} + +void +sc_frame_buffer_consume(struct sc_frame_buffer *fb, AVFrame *dst) { + sc_mutex_lock(&fb->mutex); + assert(!fb->pending_frame_consumed); + fb->pending_frame_consumed = true; + + av_frame_move_ref(dst, fb->pending_frame); + // av_frame_move_ref() resets its source frame, so no need to call + // av_frame_unref() + + sc_mutex_unlock(&fb->mutex); +} diff --git a/app/src/frame_buffer.h b/app/src/frame_buffer.h new file mode 100644 index 00000000..f97261cd --- /dev/null +++ b/app/src/frame_buffer.h @@ -0,0 +1,44 @@ +#ifndef SC_FRAME_BUFFER_H +#define SC_FRAME_BUFFER_H + +#include "common.h" + +#include + +#include "util/thread.h" + +// forward declarations +typedef struct AVFrame AVFrame; + +/** + * A frame buffer holds 1 pending frame, which is the last frame received from + * the producer (typically, the decoder). + * + * If a pending frame has not been consumed when the producer pushes a new + * frame, then it is lost. The intent is to always provide access to the very + * last frame to minimize latency. + */ + +struct sc_frame_buffer { + AVFrame *pending_frame; + AVFrame *tmp_frame; // To preserve the pending frame on error + + sc_mutex mutex; + + bool pending_frame_consumed; +}; + +bool +sc_frame_buffer_init(struct sc_frame_buffer *fb); + +void +sc_frame_buffer_destroy(struct sc_frame_buffer *fb); + +bool +sc_frame_buffer_push(struct sc_frame_buffer *fb, const AVFrame *frame, + bool *skipped); + +void +sc_frame_buffer_consume(struct sc_frame_buffer *fb, AVFrame *dst); + +#endif diff --git a/app/src/video_buffer.c b/app/src/video_buffer.c index a1d09cb8..9a5fed43 100644 --- a/app/src/video_buffer.c +++ b/app/src/video_buffer.c @@ -8,81 +8,21 @@ bool sc_video_buffer_init(struct sc_video_buffer *vb) { - vb->pending_frame = av_frame_alloc(); - if (!vb->pending_frame) { - return false; - } - - vb->tmp_frame = av_frame_alloc(); - if (!vb->tmp_frame) { - av_frame_free(&vb->pending_frame); - return false; - } - - bool ok = sc_mutex_init(&vb->mutex); - if (!ok) { - av_frame_free(&vb->pending_frame); - av_frame_free(&vb->tmp_frame); - return false; - } - - // there is initially no frame, so consider it has already been consumed - vb->pending_frame_consumed = true; - - return true; + return sc_frame_buffer_init(&vb->fb); } void sc_video_buffer_destroy(struct sc_video_buffer *vb) { - sc_mutex_destroy(&vb->mutex); - av_frame_free(&vb->pending_frame); - av_frame_free(&vb->tmp_frame); -} - -static inline void -swap_frames(AVFrame **lhs, AVFrame **rhs) { - AVFrame *tmp = *lhs; - *lhs = *rhs; - *rhs = tmp; + sc_frame_buffer_destroy(&vb->fb); } bool sc_video_buffer_push(struct sc_video_buffer *vb, const AVFrame *frame, bool *previous_frame_skipped) { - sc_mutex_lock(&vb->mutex); - - // Use a temporary frame to preserve pending_frame in case of error. - // tmp_frame is an empty frame, no need to call av_frame_unref() beforehand. - int r = av_frame_ref(vb->tmp_frame, frame); - if (r) { - LOGE("Could not ref frame: %d", r); - return false; - } - - // Now that av_frame_ref() succeeded, we can replace the previous - // pending_frame - swap_frames(&vb->pending_frame, &vb->tmp_frame); - av_frame_unref(vb->tmp_frame); - - if (previous_frame_skipped) { - *previous_frame_skipped = !vb->pending_frame_consumed; - } - vb->pending_frame_consumed = false; - - sc_mutex_unlock(&vb->mutex); - - return true; + return sc_frame_buffer_push(&vb->fb, frame, previous_frame_skipped); } void sc_video_buffer_consume(struct sc_video_buffer *vb, AVFrame *dst) { - sc_mutex_lock(&vb->mutex); - assert(!vb->pending_frame_consumed); - vb->pending_frame_consumed = true; - - av_frame_move_ref(dst, vb->pending_frame); - // av_frame_move_ref() resets its source frame, so no need to call - // av_frame_unref() - - sc_mutex_unlock(&vb->mutex); + sc_frame_buffer_consume(&vb->fb, dst); } diff --git a/app/src/video_buffer.h b/app/src/video_buffer.h index f4364e08..9befd26d 100644 --- a/app/src/video_buffer.h +++ b/app/src/video_buffer.h @@ -5,27 +5,13 @@ #include -#include "util/thread.h" +#include "frame_buffer.h" // forward declarations typedef struct AVFrame AVFrame; -/** - * A video buffer holds 1 pending frame, which is the last frame received from - * the producer (typically, the decoder). - * - * If a pending frame has not been consumed when the producer pushes a new - * frame, then it is lost. The intent is to always provide access to the very - * last frame to minimize latency. - */ - struct sc_video_buffer { - AVFrame *pending_frame; - AVFrame *tmp_frame; // To preserve the pending frame on error - - sc_mutex mutex; - - bool pending_frame_consumed; + struct sc_frame_buffer fb; }; bool From 408a301201fb170c1541028235e9d63cf88e53c7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 4 Jul 2021 12:42:22 +0200 Subject: [PATCH 0653/2244] Notify new frames via callbacks Currently, a frame is available to the consumer as soon as it is pushed by the producer (which can detect if the previous frame is skipped). Notify the new frames (and frame skipped) via callbacks instead. This paves the way to add (optional) buffering, which will introduce a delay between the time when the frame is produced and the time it is available to be consumed. --- app/src/screen.c | 22 +++++++++++++--------- app/src/v4l2_sink.c | 35 ++++++++++++++++++++--------------- app/src/video_buffer.c | 28 +++++++++++++++++++++++----- app/src/video_buffer.h | 15 ++++++++++++--- 4 files changed, 68 insertions(+), 32 deletions(-) diff --git a/app/src/screen.c b/app/src/screen.c index a11235c9..a9ee1fb0 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -274,14 +274,16 @@ screen_frame_sink_close(struct sc_frame_sink *sink) { static bool screen_frame_sink_push(struct sc_frame_sink *sink, const AVFrame *frame) { struct screen *screen = DOWNCAST(sink); + return sc_video_buffer_push(&screen->vb, frame); +} - bool previous_frame_skipped; - bool ok = sc_video_buffer_push(&screen->vb, frame, &previous_frame_skipped); - if (!ok) { - return false; - } +static void +sc_video_buffer_on_new_frame(struct sc_video_buffer *vb, bool previous_skipped, + void *userdata) { + (void) vb; + struct screen *screen = userdata; - if (previous_frame_skipped) { + if (previous_skipped) { fps_counter_add_skipped_frame(&screen->fps_counter); // The EVENT_NEW_FRAME triggered for the previous frame will consume // this new frame instead @@ -293,8 +295,6 @@ screen_frame_sink_push(struct sc_frame_sink *sink, const AVFrame *frame) { // Post the event on the UI thread SDL_PushEvent(&new_frame_event); } - - return true; } bool @@ -304,7 +304,11 @@ screen_init(struct screen *screen, const struct screen_params *params) { screen->fullscreen = false; screen->maximized = false; - bool ok = sc_video_buffer_init(&screen->vb); + static const struct sc_video_buffer_callbacks cbs = { + .on_new_frame = sc_video_buffer_on_new_frame, + }; + + bool ok = sc_video_buffer_init(&screen->vb, &cbs, screen); if (!ok) { LOGE("Could not initialize video buffer"); return false; diff --git a/app/src/v4l2_sink.c b/app/src/v4l2_sink.c index 4b8ea043..21bbe404 100644 --- a/app/src/v4l2_sink.c +++ b/app/src/v4l2_sink.c @@ -139,9 +139,27 @@ run_v4l2_sink(void *data) { return 0; } +static void +sc_video_buffer_on_new_frame(struct sc_video_buffer *vb, bool previous_skipped, + void *userdata) { + (void) vb; + struct sc_v4l2_sink *vs = userdata; + + if (!previous_skipped) { + sc_mutex_lock(&vs->mutex); + vs->has_frame = true; + sc_cond_signal(&vs->cond); + sc_mutex_unlock(&vs->mutex); + } +} + static bool sc_v4l2_sink_open(struct sc_v4l2_sink *vs) { - bool ok = sc_video_buffer_init(&vs->vb); + static const struct sc_video_buffer_callbacks cbs = { + .on_new_frame = sc_video_buffer_on_new_frame, + }; + + bool ok = sc_video_buffer_init(&vs->vb, &cbs, vs); if (!ok) { LOGE("Could not initialize video buffer"); return false; @@ -303,20 +321,7 @@ sc_v4l2_sink_close(struct sc_v4l2_sink *vs) { static bool sc_v4l2_sink_push(struct sc_v4l2_sink *vs, const AVFrame *frame) { - bool previous_skipped; - bool ok = sc_video_buffer_push(&vs->vb, frame, &previous_skipped); - if (!ok) { - return false; - } - - if (!previous_skipped) { - sc_mutex_lock(&vs->mutex); - vs->has_frame = true; - sc_cond_signal(&vs->cond); - sc_mutex_unlock(&vs->mutex); - } - - return true; + return sc_video_buffer_push(&vs->vb, frame); } static bool diff --git a/app/src/video_buffer.c b/app/src/video_buffer.c index 9a5fed43..664eb6c1 100644 --- a/app/src/video_buffer.c +++ b/app/src/video_buffer.c @@ -7,8 +7,20 @@ #include "util/log.h" bool -sc_video_buffer_init(struct sc_video_buffer *vb) { - return sc_frame_buffer_init(&vb->fb); +sc_video_buffer_init(struct sc_video_buffer *vb, + const struct sc_video_buffer_callbacks *cbs, + void *cbs_userdata) { + bool ok = sc_frame_buffer_init(&vb->fb); + if (!ok) { + return false; + } + + assert(cbs); + assert(cbs->on_new_frame); + + vb->cbs = cbs; + vb->cbs_userdata = cbs_userdata; + return true; } void @@ -17,9 +29,15 @@ sc_video_buffer_destroy(struct sc_video_buffer *vb) { } bool -sc_video_buffer_push(struct sc_video_buffer *vb, const AVFrame *frame, - bool *previous_frame_skipped) { - return sc_frame_buffer_push(&vb->fb, frame, previous_frame_skipped); +sc_video_buffer_push(struct sc_video_buffer *vb, const AVFrame *frame) { + bool previous_skipped; + bool ok = sc_frame_buffer_push(&vb->fb, frame, &previous_skipped); + if (!ok) { + return false; + } + + vb->cbs->on_new_frame(vb, previous_skipped, vb->cbs_userdata); + return true; } void diff --git a/app/src/video_buffer.h b/app/src/video_buffer.h index 9befd26d..6f258980 100644 --- a/app/src/video_buffer.h +++ b/app/src/video_buffer.h @@ -12,17 +12,26 @@ typedef struct AVFrame AVFrame; struct sc_video_buffer { struct sc_frame_buffer fb; + + const struct sc_video_buffer_callbacks *cbs; + void *cbs_userdata; +}; + +struct sc_video_buffer_callbacks { + void (*on_new_frame)(struct sc_video_buffer *vb, bool previous_skipped, + void *userdata); }; bool -sc_video_buffer_init(struct sc_video_buffer *vb); +sc_video_buffer_init(struct sc_video_buffer *vb, + const struct sc_video_buffer_callbacks *cbs, + void *cbs_userdata); void sc_video_buffer_destroy(struct sc_video_buffer *vb); bool -sc_video_buffer_push(struct sc_video_buffer *vb, const AVFrame *frame, - bool *skipped); +sc_video_buffer_push(struct sc_video_buffer *vb, const AVFrame *frame); void sc_video_buffer_consume(struct sc_video_buffer *vb, AVFrame *dst); From 79278961b9955173573f2af884fc468d1ec67380 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 4 Jul 2021 15:24:51 +0200 Subject: [PATCH 0654/2244] Implement buffering To minimize latency (at the cost of jitter), scrcpy always displays a frame as soon as it available, without waiting. However, when recording (--record), it still writes the captured timestamps to the output file, so that the recorded file can be played correctly without jitter. Some real-time use cases might benefit from adding a small latency to compensate for jitter too. For example, few tens of seconds of latency for live-streaming are not important, but jitter is noticeable. Therefore, implement a buffering mechanism (disabled by default) to add a configurable latency delay. PR #2417 --- app/meson.build | 1 + app/src/clock.c | 90 ++++++++++++++++ app/src/clock.h | 70 +++++++++++++ app/src/screen.c | 15 ++- app/src/v4l2_sink.c | 16 ++- app/src/video_buffer.c | 233 ++++++++++++++++++++++++++++++++++++++--- app/src/video_buffer.h | 36 ++++++- 7 files changed, 439 insertions(+), 22 deletions(-) create mode 100644 app/src/clock.c create mode 100644 app/src/clock.h diff --git a/app/meson.build b/app/meson.build index 7efd94a1..d7ab0416 100644 --- a/app/meson.build +++ b/app/meson.build @@ -2,6 +2,7 @@ src = [ 'src/main.c', 'src/adb.c', 'src/cli.c', + 'src/clock.c', 'src/compat.c', 'src/control_msg.c', 'src/controller.c', diff --git a/app/src/clock.c b/app/src/clock.c new file mode 100644 index 00000000..7a1e0940 --- /dev/null +++ b/app/src/clock.c @@ -0,0 +1,90 @@ +#include "clock.h" + +void +sc_clock_init(struct sc_clock *clock) { + clock->count = 0; + clock->head = 0; + clock->left_sum.system = 0; + clock->left_sum.stream = 0; + clock->right_sum.system = 0; + clock->right_sum.stream = 0; +} + +// Estimate the affine function f(stream) = slope * stream + offset +static void +sc_clock_estimate(struct sc_clock *clock, + double *out_slope, sc_tick *out_offset) { + assert(clock->count > 1); // two points are necessary + struct sc_clock_point left_avg = { + .system = clock->left_sum.system / (clock->count / 2), + .stream = clock->left_sum.stream / (clock->count / 2), + }; + struct sc_clock_point right_avg = { + .system = clock->right_sum.system / ((clock->count + 1) / 2), + .stream = clock->right_sum.stream / ((clock->count + 1) / 2), + }; + struct sc_clock_point global_avg = { + .system = (clock->left_sum.system + clock->right_sum.system) + / clock->count, + .stream = (clock->left_sum.stream + clock->right_sum.stream) + / clock->count, + }; + + double slope = (double) (right_avg.system - left_avg.system) + / (right_avg.stream - left_avg.stream); + sc_tick offset = global_avg.system - (sc_tick) (global_avg.stream * slope); + + *out_slope = slope; + *out_offset = offset; +} + +void +sc_clock_update(struct sc_clock *clock, sc_tick system, sc_tick stream) { + struct sc_clock_point *point = &clock->points[clock->head]; + + if (clock->count == SC_CLOCK_RANGE || clock->count & 1) { + // One point passes from the right sum to the left sum + + unsigned mid; + if (clock->count == SC_CLOCK_RANGE) { + mid = (clock->head + SC_CLOCK_RANGE / 2) % SC_CLOCK_RANGE; + } else { + // Only for the first frames + mid = clock->count / 2; + } + + struct sc_clock_point *mid_point = &clock->points[mid]; + clock->left_sum.system += mid_point->system; + clock->left_sum.stream += mid_point->stream; + clock->right_sum.system -= mid_point->system; + clock->right_sum.stream -= mid_point->stream; + } + + if (clock->count == SC_CLOCK_RANGE) { + // The current point overwrites the previous value in the circular + // array, update the left sum accordingly + clock->left_sum.system -= point->system; + clock->left_sum.stream -= point->stream; + } else { + ++clock->count; + } + + point->system = system; + point->stream = stream; + + clock->right_sum.system += system; + clock->right_sum.stream += stream; + + clock->head = (clock->head + 1) % SC_CLOCK_RANGE; + + if (clock->count > 1) { + // Update estimation + sc_clock_estimate(clock, &clock->slope, &clock->offset); + } +} + +sc_tick +sc_clock_to_system_time(struct sc_clock *clock, sc_tick stream) { + assert(clock->count > 1); // sc_clock_update() must have been called + return (sc_tick) (stream * clock->slope) + clock->offset; +} diff --git a/app/src/clock.h b/app/src/clock.h new file mode 100644 index 00000000..eb7fa594 --- /dev/null +++ b/app/src/clock.h @@ -0,0 +1,70 @@ +#ifndef SC_CLOCK_H +#define SC_CLOCK_H + +#include "common.h" + +#include + +#include "util/tick.h" + +#define SC_CLOCK_RANGE 32 +static_assert(!(SC_CLOCK_RANGE & 1), "SC_CLOCK_RANGE must be even"); + +struct sc_clock_point { + sc_tick system; + sc_tick stream; +}; + +/** + * The clock aims to estimate the affine relation between the stream (device) + * time and the system time: + * + * f(stream) = slope * stream + offset + * + * To that end, it stores the SC_CLOCK_RANGE last clock points (the timestamps + * of a frame expressed both in stream time and system time) in a circular + * array. + * + * To estimate the slope, it splits the last SC_CLOCK_RANGE points into two + * sets of SC_CLOCK_RANGE/2 points, and compute their centroid ("average + * point"). The slope of the estimated affine function is that of the line + * passing through these two points. + * + * To estimate the offset, it computes the centroid of all the SC_CLOCK_RANGE + * points. The resulting affine function passes by this centroid. + * + * With a circular array, the rolling sums (and average) are quick to compute. + * In practice, the estimation is stable and the evolution is smooth. + */ +struct sc_clock { + // Circular array + struct sc_clock_point points[SC_CLOCK_RANGE]; + + // Number of points in the array (count <= SC_CLOCK_RANGE) + unsigned count; + + // Index of the next point to write + unsigned head; + + // Sum of the first count/2 points + struct sc_clock_point left_sum; + + // Sum of the last (count+1)/2 points + struct sc_clock_point right_sum; + + // Estimated slope and offset + // (computed on sc_clock_update(), used by sc_clock_to_system_time()) + double slope; + sc_tick offset; +}; + +void +sc_clock_init(struct sc_clock *clock); + +void +sc_clock_update(struct sc_clock *clock, sc_tick system, sc_tick stream); + +sc_tick +sc_clock_to_system_time(struct sc_clock *clock, sc_tick stream); + +#endif diff --git a/app/src/screen.c b/app/src/screen.c index a9ee1fb0..126caf9b 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -308,15 +308,21 @@ screen_init(struct screen *screen, const struct screen_params *params) { .on_new_frame = sc_video_buffer_on_new_frame, }; - bool ok = sc_video_buffer_init(&screen->vb, &cbs, screen); + bool ok = sc_video_buffer_init(&screen->vb, 0, &cbs, screen); if (!ok) { LOGE("Could not initialize video buffer"); return false; } + ok = sc_video_buffer_start(&screen->vb); + if (!ok) { + LOGE("Could not start video_buffer"); + goto error_destroy_video_buffer; + } + if (!fps_counter_init(&screen->fps_counter)) { LOGE("Could not initialize FPS counter"); - goto error_destroy_video_buffer; + goto error_stop_and_join_video_buffer; } screen->frame_size = params->frame_size; @@ -457,6 +463,9 @@ error_destroy_window: SDL_DestroyWindow(screen->window); error_destroy_fps_counter: fps_counter_destroy(&screen->fps_counter); +error_stop_and_join_video_buffer: + sc_video_buffer_stop(&screen->vb); + sc_video_buffer_join(&screen->vb); error_destroy_video_buffer: sc_video_buffer_destroy(&screen->vb); @@ -475,11 +484,13 @@ screen_hide_window(struct screen *screen) { void screen_interrupt(struct screen *screen) { + sc_video_buffer_stop(&screen->vb); fps_counter_interrupt(&screen->fps_counter); } void screen_join(struct screen *screen) { + sc_video_buffer_join(&screen->vb); fps_counter_join(&screen->fps_counter); } diff --git a/app/src/v4l2_sink.c b/app/src/v4l2_sink.c index 21bbe404..8f8b98ee 100644 --- a/app/src/v4l2_sink.c +++ b/app/src/v4l2_sink.c @@ -159,16 +159,22 @@ sc_v4l2_sink_open(struct sc_v4l2_sink *vs) { .on_new_frame = sc_video_buffer_on_new_frame, }; - bool ok = sc_video_buffer_init(&vs->vb, &cbs, vs); + bool ok = sc_video_buffer_init(&vs->vb, 0, &cbs, vs); if (!ok) { LOGE("Could not initialize video buffer"); return false; } + ok = sc_video_buffer_start(&vs->vb); + if (!ok) { + LOGE("Could not start video buffer"); + goto error_video_buffer_destroy; + } + ok = sc_mutex_init(&vs->mutex); if (!ok) { LOGC("Could not create mutex"); - goto error_video_buffer_destroy; + goto error_video_buffer_stop_and_join; } ok = sc_cond_init(&vs->cond); @@ -293,6 +299,9 @@ error_cond_destroy: sc_cond_destroy(&vs->cond); error_mutex_destroy: sc_mutex_destroy(&vs->mutex); +error_video_buffer_stop_and_join: + sc_video_buffer_stop(&vs->vb); + sc_video_buffer_join(&vs->vb); error_video_buffer_destroy: sc_video_buffer_destroy(&vs->vb); @@ -306,7 +315,10 @@ sc_v4l2_sink_close(struct sc_v4l2_sink *vs) { sc_cond_signal(&vs->cond); sc_mutex_unlock(&vs->mutex); + sc_video_buffer_stop(&vs->vb); + sc_thread_join(&vs->thread, NULL); + sc_video_buffer_join(&vs->vb); av_packet_free(&vs->packet); av_frame_free(&vs->frame); diff --git a/app/src/video_buffer.c b/app/src/video_buffer.c index 664eb6c1..e75c8873 100644 --- a/app/src/video_buffer.c +++ b/app/src/video_buffer.c @@ -1,35 +1,44 @@ #include "video_buffer.h" #include +#include + #include #include #include "util/log.h" -bool -sc_video_buffer_init(struct sc_video_buffer *vb, - const struct sc_video_buffer_callbacks *cbs, - void *cbs_userdata) { - bool ok = sc_frame_buffer_init(&vb->fb); - if (!ok) { - return false; +static struct sc_video_buffer_frame * +sc_video_buffer_frame_new(const AVFrame *frame) { + struct sc_video_buffer_frame *vb_frame = malloc(sizeof(*vb_frame)); + if (!vb_frame) { + return NULL; } - assert(cbs); - assert(cbs->on_new_frame); + vb_frame->frame = av_frame_alloc(); + if (!vb_frame->frame) { + free(vb_frame); + return NULL; + } - vb->cbs = cbs; - vb->cbs_userdata = cbs_userdata; - return true; + if (av_frame_ref(vb_frame->frame, frame)) { + av_frame_free(&vb_frame->frame); + free(vb_frame); + return NULL; + } + + return vb_frame; } -void -sc_video_buffer_destroy(struct sc_video_buffer *vb) { - sc_frame_buffer_destroy(&vb->fb); +static void +sc_video_buffer_frame_delete(struct sc_video_buffer_frame *vb_frame) { + av_frame_unref(vb_frame->frame); + av_frame_free(&vb_frame->frame); + free(vb_frame); } -bool -sc_video_buffer_push(struct sc_video_buffer *vb, const AVFrame *frame) { +static bool +sc_video_buffer_offer(struct sc_video_buffer *vb, const AVFrame *frame) { bool previous_skipped; bool ok = sc_frame_buffer_push(&vb->fb, frame, &previous_skipped); if (!ok) { @@ -40,6 +49,196 @@ sc_video_buffer_push(struct sc_video_buffer *vb, const AVFrame *frame) { return true; } +static int +run_buffering(void *data) { + struct sc_video_buffer *vb = data; + + assert(vb->buffering_time > 0); + + for (;;) { + sc_mutex_lock(&vb->b.mutex); + + while (!vb->b.stopped && sc_queue_is_empty(&vb->b.queue)) { + sc_cond_wait(&vb->b.queue_cond, &vb->b.mutex); + } + + if (vb->b.stopped) { + sc_mutex_unlock(&vb->b.mutex); + goto stopped; + } + + struct sc_video_buffer_frame *vb_frame; + sc_queue_take(&vb->b.queue, next, &vb_frame); + + sc_tick max_deadline = sc_tick_now() + vb->buffering_time; + // PTS (written by the server) are expressed in microseconds + sc_tick pts = SC_TICK_TO_US(vb_frame->frame->pts); + + bool timed_out = false; + while (!vb->b.stopped && !timed_out) { + sc_tick deadline = sc_clock_to_system_time(&vb->b.clock, pts) + + vb->buffering_time; + if (deadline > max_deadline) { + deadline = max_deadline; + } + + timed_out = + !sc_cond_timedwait(&vb->b.wait_cond, &vb->b.mutex, deadline); + } + + if (vb->b.stopped) { + sc_video_buffer_frame_delete(vb_frame); + sc_mutex_unlock(&vb->b.mutex); + goto stopped; + } + + sc_mutex_unlock(&vb->b.mutex); + + sc_video_buffer_offer(vb, vb_frame->frame); + + sc_video_buffer_frame_delete(vb_frame); + } + +stopped: + // Flush queue + while (!sc_queue_is_empty(&vb->b.queue)) { + struct sc_video_buffer_frame *vb_frame; + sc_queue_take(&vb->b.queue, next, &vb_frame); + sc_video_buffer_frame_delete(vb_frame); + } + + LOGD("Buffering thread ended"); + + return 0; +} + +bool +sc_video_buffer_init(struct sc_video_buffer *vb, sc_tick buffering_time, + const struct sc_video_buffer_callbacks *cbs, + void *cbs_userdata) { + bool ok = sc_frame_buffer_init(&vb->fb); + if (!ok) { + return false; + } + + assert(buffering_time >= 0); + if (buffering_time) { + ok = sc_mutex_init(&vb->b.mutex); + if (!ok) { + LOGC("Could not create mutex"); + sc_frame_buffer_destroy(&vb->fb); + return false; + } + + ok = sc_cond_init(&vb->b.queue_cond); + if (!ok) { + LOGC("Could not create cond"); + sc_mutex_destroy(&vb->b.mutex); + sc_frame_buffer_destroy(&vb->fb); + return false; + } + + ok = sc_cond_init(&vb->b.wait_cond); + if (!ok) { + LOGC("Could not create wait cond"); + sc_cond_destroy(&vb->b.queue_cond); + sc_mutex_destroy(&vb->b.mutex); + sc_frame_buffer_destroy(&vb->fb); + return false; + } + + sc_clock_init(&vb->b.clock); + sc_queue_init(&vb->b.queue); + } + + assert(cbs); + assert(cbs->on_new_frame); + + vb->buffering_time = buffering_time; + vb->cbs = cbs; + vb->cbs_userdata = cbs_userdata; + return true; +} + +bool +sc_video_buffer_start(struct sc_video_buffer *vb) { + if (vb->buffering_time) { + bool ok = + sc_thread_create(&vb->b.thread, run_buffering, "buffering", vb); + if (!ok) { + LOGE("Could not start buffering thread"); + return false; + } + } + + return true; +} + +void +sc_video_buffer_stop(struct sc_video_buffer *vb) { + if (vb->buffering_time) { + sc_mutex_lock(&vb->b.mutex); + vb->b.stopped = true; + sc_cond_signal(&vb->b.queue_cond); + sc_cond_signal(&vb->b.wait_cond); + sc_mutex_unlock(&vb->b.mutex); + } +} + +void +sc_video_buffer_join(struct sc_video_buffer *vb) { + if (vb->buffering_time) { + sc_thread_join(&vb->b.thread, NULL); + } +} + +void +sc_video_buffer_destroy(struct sc_video_buffer *vb) { + sc_frame_buffer_destroy(&vb->fb); + if (vb->buffering_time) { + sc_cond_destroy(&vb->b.wait_cond); + sc_cond_destroy(&vb->b.queue_cond); + sc_mutex_destroy(&vb->b.mutex); + } +} + +bool +sc_video_buffer_push(struct sc_video_buffer *vb, const AVFrame *frame) { + if (!vb->buffering_time) { + // No buffering + return sc_video_buffer_offer(vb, frame); + } + + sc_mutex_lock(&vb->b.mutex); + + sc_tick pts = SC_TICK_FROM_US(frame->pts); + sc_clock_update(&vb->b.clock, sc_tick_now(), pts); + sc_cond_signal(&vb->b.wait_cond); + + if (vb->b.clock.count == 1) { + sc_mutex_unlock(&vb->b.mutex); + // First frame, offer it immediately, for two reasons: + // - not to delay the opening of the scrcpy window + // - the buffering estimation needs at least two clock points, so it + // could not handle the first frame + return sc_video_buffer_offer(vb, frame); + } + + struct sc_video_buffer_frame *vb_frame = sc_video_buffer_frame_new(frame); + if (!vb_frame) { + sc_mutex_unlock(&vb->b.mutex); + LOGE("Could not allocate frame"); + return false; + } + + sc_queue_push(&vb->b.queue, next, vb_frame); + sc_cond_signal(&vb->b.queue_cond); + + sc_mutex_unlock(&vb->b.mutex); + + return true; +} + void sc_video_buffer_consume(struct sc_video_buffer *vb, AVFrame *dst) { sc_frame_buffer_consume(&vb->fb, dst); diff --git a/app/src/video_buffer.h b/app/src/video_buffer.h index 6f258980..bfdafab5 100644 --- a/app/src/video_buffer.h +++ b/app/src/video_buffer.h @@ -5,14 +5,39 @@ #include +#include "clock.h" #include "frame_buffer.h" +#include "util/queue.h" +#include "util/thread.h" +#include "util/tick.h" // forward declarations typedef struct AVFrame AVFrame; +struct sc_video_buffer_frame { + AVFrame *frame; + struct sc_video_buffer_frame *next; +}; + +struct sc_video_buffer_frame_queue SC_QUEUE(struct sc_video_buffer_frame); + struct sc_video_buffer { struct sc_frame_buffer fb; + sc_tick buffering_time; + + // only if buffering_time > 0 + struct { + sc_thread thread; + sc_mutex mutex; + sc_cond queue_cond; + sc_cond wait_cond; + + struct sc_clock clock; + struct sc_video_buffer_frame_queue queue; + bool stopped; + } b; // buffering + const struct sc_video_buffer_callbacks *cbs; void *cbs_userdata; }; @@ -23,10 +48,19 @@ struct sc_video_buffer_callbacks { }; bool -sc_video_buffer_init(struct sc_video_buffer *vb, +sc_video_buffer_init(struct sc_video_buffer *vb, sc_tick buffering_time, const struct sc_video_buffer_callbacks *cbs, void *cbs_userdata); +bool +sc_video_buffer_start(struct sc_video_buffer *vb); + +void +sc_video_buffer_stop(struct sc_video_buffer *vb); + +void +sc_video_buffer_join(struct sc_video_buffer *vb); + void sc_video_buffer_destroy(struct sc_video_buffer *vb); From 33977203305d76f72bab9bcbb61a1d8ab5643c67 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 6 Jul 2021 19:04:05 +0200 Subject: [PATCH 0655/2244] Add buffering command line options Add --display-buffer and --v4l2-buffer options to configure buffering time. --- README.md | 18 +++++++++++++++++ app/scrcpy.1 | 14 ++++++++++++++ app/src/cli.c | 47 +++++++++++++++++++++++++++++++++++++++++++++ app/src/scrcpy.c | 4 +++- app/src/scrcpy.h | 6 ++++++ app/src/screen.c | 3 ++- app/src/screen.h | 2 ++ app/src/v4l2_sink.c | 5 +++-- app/src/v4l2_sink.h | 4 +++- 9 files changed, 98 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 0dfa068c..7b1d2e78 100644 --- a/README.md +++ b/README.md @@ -321,6 +321,24 @@ For example, you could capture the video within [OBS]. [OBS]: https://obsproject.com/fr +#### Buffering + +It is possible to add buffering. This increases latency but reduces jitter (see +#2464). + +The option is available for display buffering: + +```bash +scrcpy --display-buffer=50 # add 50 ms buffering for display +``` + +and V4L2 sink: + +```bash +scrcpy --v4l2-buffer=500 # add 500 ms buffering for v4l2 sink +``` + + ### Connection #### Wireless diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 253dd04f..00589a43 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -56,6 +56,12 @@ The list of possible display ids can be listed by "adb shell dumpsys display" Default is 0. +.TP +.BI "\-\-display\-buffer ms +Add a buffering delay (in milliseconds) before displaying. This increases latency to compensate for jitter. + +Default is 0 (no buffering). + .TP .BI "\-\-encoder " name Use a specific MediaCodec encoder (must be a H.264 encoder). @@ -191,6 +197,14 @@ Output to v4l2loopback device. It requires to lock the video orientation (see \fB\-\-lock\-video\-orientation\fR). +.TP +.BI "\-\-v4l2-buffer " ms +Add a buffering delay (in milliseconds) before pushing frames. This increases latency to compensate for jitter. + +This option is similar to \fB\-\-display\-buffer\fR, but specific to V4L2 sink. + +Default is 0 (no buffering). + .TP .BI "\-V, \-\-verbosity " value Set the log level ("verbose", "debug", "info", "warn" or "error"). diff --git a/app/src/cli.c b/app/src/cli.c index ab35745d..d22096ca 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -55,6 +55,12 @@ scrcpy_print_usage(const char *arg0) { "\n" " Default is 0.\n" "\n" + " --display-buffer ms\n" + " Add a buffering delay (in milliseconds) before displaying.\n" + " This increases latency to compensate for jitter.\n" + "\n" + " Default is 0 (no buffering).\n" + "\n" " --encoder name\n" " Use a specific MediaCodec encoder (must be a H.264 encoder).\n" "\n" @@ -182,6 +188,15 @@ scrcpy_print_usage(const char *arg0) { " It requires to lock the video orientation (see\n" " --lock-video-orientation).\n" "\n" + " --v4l2-buffer ms\n" + " Add a buffering delay (in milliseconds) before pushing\n" + " frames. This increases latency to compensate for jitter.\n" + "\n" + " This option is similar to --display-buffer, but specific to\n" + " V4L2 sink.\n" + "\n" + " Default is 0 (no buffering).\n" + "\n" #endif " -V, --verbosity value\n" " Set the log level (verbose, debug, info, warn or error).\n" @@ -392,6 +407,19 @@ parse_max_fps(const char *s, uint16_t *max_fps) { return true; } +static bool +parse_buffering_time(const char *s, sc_tick *tick) { + long value; + bool ok = parse_integer_arg(s, &value, false, 0, 0x7FFFFFFF, + "buffering time"); + if (!ok) { + return false; + } + + *tick = SC_TICK_FROM_MS(value); + return true; +} + static bool parse_lock_video_orientation(const char *s, enum sc_lock_video_orientation *lock_mode) { @@ -689,6 +717,8 @@ guess_record_format(const char *filename) { #define OPT_ENCODER_NAME 1025 #define OPT_POWER_OFF_ON_CLOSE 1026 #define OPT_V4L2_SINK 1027 +#define OPT_DISPLAY_BUFFER 1028 +#define OPT_V4L2_BUFFER 1029 bool scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { @@ -700,6 +730,7 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { {"disable-screensaver", no_argument, NULL, OPT_DISABLE_SCREENSAVER}, {"display", required_argument, NULL, OPT_DISPLAY_ID}, + {"display-buffer", required_argument, NULL, OPT_DISPLAY_BUFFER}, {"encoder", required_argument, NULL, OPT_ENCODER_NAME}, {"force-adb-forward", no_argument, NULL, OPT_FORCE_ADB_FORWARD}, @@ -732,6 +763,7 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { {"turn-screen-off", no_argument, NULL, 'S'}, #ifdef HAVE_V4L2 {"v4l2-sink", required_argument, NULL, OPT_V4L2_SINK}, + {"v4l2-buffer", required_argument, NULL, OPT_V4L2_BUFFER}, #endif {"verbosity", required_argument, NULL, 'V'}, {"version", no_argument, NULL, 'v'}, @@ -917,10 +949,20 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { case OPT_POWER_OFF_ON_CLOSE: opts->power_off_on_close = true; break; + case OPT_DISPLAY_BUFFER: + if (!parse_buffering_time(optarg, &opts->display_buffer)) { + return false; + } + break; #ifdef HAVE_V4L2 case OPT_V4L2_SINK: opts->v4l2_device = optarg; break; + case OPT_V4L2_BUFFER: + if (!parse_buffering_time(optarg, &opts->v4l2_buffer)) { + return false; + } + break; #endif default: // getopt prints the error message on stderr @@ -941,6 +983,11 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { "See --lock-video-orientation."); opts->lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_INITIAL; } + + if (opts->v4l2_buffer && !opts->v4l2_device) { + LOGE("V4L2 buffer value without V4L2 sink\n"); + return false; + } #else if (!opts->display && !opts->record_filename) { LOGE("-N/--no-display requires screen recording (-r/--record)"); diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index d0a22e77..25822526 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -381,6 +381,7 @@ scrcpy(const struct scrcpy_options *options) { .rotation = options->rotation, .mipmaps = options->mipmaps, .fullscreen = options->fullscreen, + .buffering_time = options->display_buffer, }; if (!screen_init(&s->screen, &screen_params)) { @@ -393,7 +394,8 @@ scrcpy(const struct scrcpy_options *options) { #ifdef HAVE_V4L2 if (options->v4l2_device) { - if (!sc_v4l2_sink_init(&s->v4l2_sink, options->v4l2_device, frame_size)) { + if (!sc_v4l2_sink_init(&s->v4l2_sink, options->v4l2_device, frame_size, + options->v4l2_buffer)) { goto end; } diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index 0a2deb71..8b76fb25 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -7,6 +7,8 @@ #include #include +#include "util/tick.h" + enum sc_log_level { SC_LOG_LEVEL_VERBOSE, SC_LOG_LEVEL_DEBUG, @@ -78,6 +80,8 @@ struct scrcpy_options { uint16_t window_width; uint16_t window_height; uint32_t display_id; + sc_tick display_buffer; + sc_tick v4l2_buffer; bool show_touches; bool fullscreen; bool always_on_top; @@ -126,6 +130,8 @@ struct scrcpy_options { .window_width = 0, \ .window_height = 0, \ .display_id = 0, \ + .display_buffer = 0, \ + .v4l2_buffer = 0, \ .show_touches = false, \ .fullscreen = false, \ .always_on_top = false, \ diff --git a/app/src/screen.c b/app/src/screen.c index 126caf9b..3cd4329f 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -308,7 +308,8 @@ screen_init(struct screen *screen, const struct screen_params *params) { .on_new_frame = sc_video_buffer_on_new_frame, }; - bool ok = sc_video_buffer_init(&screen->vb, 0, &cbs, screen); + bool ok = sc_video_buffer_init(&screen->vb, params->buffering_time, &cbs, + screen); if (!ok) { LOGE("Could not initialize video buffer"); return false; diff --git a/app/src/screen.h b/app/src/screen.h index e38d65bc..86aa1183 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -63,6 +63,8 @@ struct screen_params { bool mipmaps; bool fullscreen; + + sc_tick buffering_time; }; // initialize screen, create window, renderer and texture (window is hidden) diff --git a/app/src/v4l2_sink.c b/app/src/v4l2_sink.c index 8f8b98ee..cae3eee9 100644 --- a/app/src/v4l2_sink.c +++ b/app/src/v4l2_sink.c @@ -159,7 +159,7 @@ sc_v4l2_sink_open(struct sc_v4l2_sink *vs) { .on_new_frame = sc_video_buffer_on_new_frame, }; - bool ok = sc_video_buffer_init(&vs->vb, 0, &cbs, vs); + bool ok = sc_video_buffer_init(&vs->vb, vs->buffering_time, &cbs, vs); if (!ok) { LOGE("Could not initialize video buffer"); return false; @@ -356,7 +356,7 @@ sc_v4l2_frame_sink_push(struct sc_frame_sink *sink, const AVFrame *frame) { bool sc_v4l2_sink_init(struct sc_v4l2_sink *vs, const char *device_name, - struct size frame_size) { + struct size frame_size, sc_tick buffering_time) { vs->device_name = strdup(device_name); if (!vs->device_name) { LOGE("Could not strdup v4l2 device name"); @@ -364,6 +364,7 @@ sc_v4l2_sink_init(struct sc_v4l2_sink *vs, const char *device_name, } vs->frame_size = frame_size; + vs->buffering_time = buffering_time; static const struct sc_frame_sink_ops ops = { .open = sc_v4l2_frame_sink_open, diff --git a/app/src/v4l2_sink.h b/app/src/v4l2_sink.h index cf3fdddc..6773cd26 100644 --- a/app/src/v4l2_sink.h +++ b/app/src/v4l2_sink.h @@ -6,6 +6,7 @@ #include "coords.h" #include "trait/frame_sink.h" #include "video_buffer.h" +#include "util/tick.h" #include @@ -18,6 +19,7 @@ struct sc_v4l2_sink { char *device_name; struct size frame_size; + sc_tick buffering_time; sc_thread thread; sc_mutex mutex; @@ -32,7 +34,7 @@ struct sc_v4l2_sink { bool sc_v4l2_sink_init(struct sc_v4l2_sink *vs, const char *device_name, - struct size frame_size); + struct size frame_size, sc_tick buffering_time); void sc_v4l2_sink_destroy(struct sc_v4l2_sink *vs); From 2f03141e9fed348ae3053e1d185df3e1f8028e4a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 6 Jul 2021 23:57:01 +0200 Subject: [PATCH 0656/2244] Add clock tests The clock rolling sum is not trivial. Test it. --- app/meson.build | 4 +++ app/tests/test_clock.c | 79 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 app/tests/test_clock.c diff --git a/app/meson.build b/app/meson.build index d7ab0416..f5345803 100644 --- a/app/meson.build +++ b/app/meson.build @@ -168,6 +168,10 @@ if get_option('buildtype') == 'debug' 'src/cli.c', 'src/util/str_util.c', ]], + ['test_clock', [ + 'tests/test_clock.c', + 'src/clock.c', + ]], ['test_control_msg_serialize', [ 'tests/test_control_msg_serialize.c', 'src/control_msg.c', diff --git a/app/tests/test_clock.c b/app/tests/test_clock.c new file mode 100644 index 00000000..a88d5800 --- /dev/null +++ b/app/tests/test_clock.c @@ -0,0 +1,79 @@ +#include "common.h" + +#include + +#include "clock.h" + +void test_small_rolling_sum(void) { + struct sc_clock clock; + sc_clock_init(&clock); + + assert(clock.count == 0); + assert(clock.left_sum.system == 0); + assert(clock.left_sum.stream == 0); + assert(clock.right_sum.system == 0); + assert(clock.right_sum.stream == 0); + + sc_clock_update(&clock, 2, 3); + assert(clock.count == 1); + assert(clock.left_sum.system == 0); + assert(clock.left_sum.stream == 0); + assert(clock.right_sum.system == 2); + assert(clock.right_sum.stream == 3); + + sc_clock_update(&clock, 10, 20); + assert(clock.count == 2); + assert(clock.left_sum.system == 2); + assert(clock.left_sum.stream == 3); + assert(clock.right_sum.system == 10); + assert(clock.right_sum.stream == 20); + + sc_clock_update(&clock, 40, 80); + assert(clock.count == 3); + assert(clock.left_sum.system == 2); + assert(clock.left_sum.stream == 3); + assert(clock.right_sum.system == 50); + assert(clock.right_sum.stream == 100); + + sc_clock_update(&clock, 400, 800); + assert(clock.count == 4); + assert(clock.left_sum.system == 12); + assert(clock.left_sum.stream == 23); + assert(clock.right_sum.system == 440); + assert(clock.right_sum.stream == 880); +} + +void test_large_rolling_sum(void) { + const unsigned half_range = SC_CLOCK_RANGE / 2; + + struct sc_clock clock1; + sc_clock_init(&clock1); + for (unsigned i = 0; i < 5 * half_range; ++i) { + sc_clock_update(&clock1, i, 2 * i + 1); + } + + struct sc_clock clock2; + sc_clock_init(&clock2); + for (unsigned i = 3 * half_range; i < 5 * half_range; ++i) { + sc_clock_update(&clock2, i, 2 * i + 1); + } + + assert(clock1.count == SC_CLOCK_RANGE); + assert(clock2.count == SC_CLOCK_RANGE); + + // The values before the last SC_CLOCK_RANGE points in clock1 should have + // no impact + assert(clock1.left_sum.system == clock2.left_sum.system); + assert(clock1.left_sum.stream == clock2.left_sum.stream); + assert(clock1.right_sum.system == clock2.right_sum.system); + assert(clock1.right_sum.stream == clock2.right_sum.stream); +} + +int main(int argc, char *argv[]) { + (void) argc; + (void) argv; + + test_small_rolling_sum(); + test_large_rolling_sum(); + return 0; +}; From 4c4d02295ca5551a5d3c2bada4ce455c516ec55b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 13 Jul 2021 22:32:05 +0200 Subject: [PATCH 0657/2244] Add buffering debugging tools Output buffering and clock logs by disabling a compilation flag. --- app/src/clock.c | 9 +++++++++ app/src/util/tick.h | 1 + app/src/video_buffer.c | 10 ++++++++++ app/src/video_buffer.h | 3 +++ 4 files changed, 23 insertions(+) diff --git a/app/src/clock.c b/app/src/clock.c index 7a1e0940..1b353347 100644 --- a/app/src/clock.c +++ b/app/src/clock.c @@ -1,5 +1,9 @@ #include "clock.h" +#include "util/log.h" + +#define SC_CLOCK_NDEBUG // comment to debug + void sc_clock_init(struct sc_clock *clock) { clock->count = 0; @@ -80,6 +84,11 @@ sc_clock_update(struct sc_clock *clock, sc_tick system, sc_tick stream) { if (clock->count > 1) { // Update estimation sc_clock_estimate(clock, &clock->slope, &clock->offset); + +#ifndef SC_CLOCK_NDEBUG + LOGD("Clock estimation: %g * pts + %" PRItick, + clock->slope, clock->offset); +#endif } } diff --git a/app/src/util/tick.h b/app/src/util/tick.h index a7494458..472a18a7 100644 --- a/app/src/util/tick.h +++ b/app/src/util/tick.h @@ -4,6 +4,7 @@ #include typedef int64_t sc_tick; +#define PRItick PRIi64 #define SC_TICK_FREQ 1000000 // microsecond // To be adapted if SC_TICK_FREQ changes diff --git a/app/src/video_buffer.c b/app/src/video_buffer.c index e75c8873..f71a4e78 100644 --- a/app/src/video_buffer.c +++ b/app/src/video_buffer.c @@ -8,6 +8,8 @@ #include "util/log.h" +#define SC_BUFFERING_NDEBUG // comment to debug + static struct sc_video_buffer_frame * sc_video_buffer_frame_new(const AVFrame *frame) { struct sc_video_buffer_frame *vb_frame = malloc(sizeof(*vb_frame)); @@ -94,6 +96,11 @@ run_buffering(void *data) { sc_mutex_unlock(&vb->b.mutex); +#ifndef SC_BUFFERING_NDEBUG + LOGD("Buffering: %" PRItick ";%" PRItick ";%" PRItick, + pts, vb_frame->push_date, sc_tick_now()); +#endif + sc_video_buffer_offer(vb, vb_frame->frame); sc_video_buffer_frame_delete(vb_frame); @@ -231,6 +238,9 @@ sc_video_buffer_push(struct sc_video_buffer *vb, const AVFrame *frame) { return false; } +#ifndef SC_BUFFERING_NDEBUG + vb_frame->push_date = sc_tick_now(); +#endif sc_queue_push(&vb->b.queue, next, vb_frame); sc_cond_signal(&vb->b.queue_cond); diff --git a/app/src/video_buffer.h b/app/src/video_buffer.h index bfdafab5..48777703 100644 --- a/app/src/video_buffer.h +++ b/app/src/video_buffer.h @@ -17,6 +17,9 @@ typedef struct AVFrame AVFrame; struct sc_video_buffer_frame { AVFrame *frame; struct sc_video_buffer_frame *next; +#ifndef NDEBUG + sc_tick push_date; +#endif }; struct sc_video_buffer_frame_queue SC_QUEUE(struct sc_video_buffer_frame); From 0ae10f2b398868cb326a7adbe43574a4ac053e9a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 13 Jul 2021 23:14:43 +0200 Subject: [PATCH 0658/2244] Improve slope estimation on start The first frames are typically received and decoded with more delay than the others, causing a wrong slope estimation on start. To compensate, assume an initial slope of 1, then progressively use the estimated slope. --- app/src/clock.c | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/app/src/clock.c b/app/src/clock.c index 1b353347..fe072f01 100644 --- a/app/src/clock.c +++ b/app/src/clock.c @@ -19,6 +19,7 @@ static void sc_clock_estimate(struct sc_clock *clock, double *out_slope, sc_tick *out_offset) { assert(clock->count > 1); // two points are necessary + struct sc_clock_point left_avg = { .system = clock->left_sum.system / (clock->count / 2), .stream = clock->left_sum.stream / (clock->count / 2), @@ -27,6 +28,19 @@ sc_clock_estimate(struct sc_clock *clock, .system = clock->right_sum.system / ((clock->count + 1) / 2), .stream = clock->right_sum.stream / ((clock->count + 1) / 2), }; + + double slope = (double) (right_avg.system - left_avg.system) + / (right_avg.stream - left_avg.stream); + + if (clock->count < SC_CLOCK_RANGE) { + /* The first frames are typically received and decoded with more delay + * than the others, causing a wrong slope estimation on start. To + * compensate, assume an initial slope of 1, then progressively use the + * estimated slope. */ + slope = (clock->count * slope + (SC_CLOCK_RANGE - clock->count)) + / SC_CLOCK_RANGE; + } + struct sc_clock_point global_avg = { .system = (clock->left_sum.system + clock->right_sum.system) / clock->count, @@ -34,8 +48,6 @@ sc_clock_estimate(struct sc_clock *clock, / clock->count, }; - double slope = (double) (right_avg.system - left_avg.system) - / (right_avg.stream - left_avg.stream); sc_tick offset = global_avg.system - (sc_tick) (global_avg.stream * slope); *out_slope = slope; From daf90d33d5a86d39352681c78734820710fd4cc7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 15 Jul 2021 18:07:39 +0200 Subject: [PATCH 0659/2244] Fix code style Make the code fit into 80 columns. --- app/src/recorder.c | 3 ++- app/src/server.c | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/src/recorder.c b/app/src/recorder.c index f2775c3a..c98b6b8c 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -213,7 +213,8 @@ run_recorder(void *data) { LOGE("Recording failed to %s", recorder->filename); } else { const char *format_name = recorder_get_format_name(recorder->format); - LOGI("Recording complete to %s file: %s", format_name, recorder->filename); + LOGI("Recording complete to %s file: %s", format_name, + recorder->filename); } LOGD("Recorder thread ended"); diff --git a/app/src/server.c b/app/src/server.c index 4e0f2b23..b861ed3b 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -261,7 +261,8 @@ execute_server(struct server *server, const struct server_params *params) { 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(lock_video_orientation_string, "%"PRIi8, + params->lock_video_orientation); sprintf(display_id_string, "%"PRIu32, params->display_id); const char *const cmd[] = { "shell", @@ -271,7 +272,8 @@ execute_server(struct server *server, const struct server_params *params) { # 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=" + "-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=" From 6f03022646bc400ae02ba9d4ec863646cf1ae063 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 15 Jul 2021 18:16:26 +0200 Subject: [PATCH 0660/2244] Fix net_send_all() On partial writes, the final result was the number of bytes written by the last send() rather than the total. --- app/src/util/net.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/util/net.c b/app/src/util/net.c index bbf57bbc..75daef8b 100644 --- a/app/src/util/net.c +++ b/app/src/util/net.c @@ -99,16 +99,18 @@ net_send(socket_t socket, const void *buf, size_t len) { ssize_t net_send_all(socket_t socket, const void *buf, size_t len) { + size_t copied = 0; ssize_t w = 0; while (len > 0) { w = send(socket, buf, len, 0); if (w == -1) { - return -1; + return copied ? (ssize_t) copied : -1; } len -= w; buf = (char *) buf + w; + copied += w; } - return w; + return copied; } bool From f78608ab298b1a9cd17e11e0b1219da9f6b5b88e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 15 Jul 2021 18:16:56 +0200 Subject: [PATCH 0661/2244] Fix type for assignment The functions net_send_all() and net_recv_all() return ssize_t, not int. --- app/src/controller.c | 2 +- app/src/server.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/controller.c b/app/src/controller.c index 3a428aa8..17844c98 100644 --- a/app/src/controller.c +++ b/app/src/controller.c @@ -70,7 +70,7 @@ process_msg(struct controller *controller, if (!length) { return false; } - int w = net_send_all(controller->control_socket, serialized_msg, length); + ssize_t w = net_send_all(controller->control_socket, serialized_msg, length); return (size_t) w == length; } diff --git a/app/src/server.c b/app/src/server.c index b861ed3b..e1db2f0c 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -470,7 +470,7 @@ error: static bool device_read_info(socket_t device_socket, char *device_name, struct size *size) { unsigned char buf[DEVICE_NAME_FIELD_LENGTH + 4]; - int r = net_recv_all(device_socket, buf, sizeof(buf)); + ssize_t r = net_recv_all(device_socket, buf, sizeof(buf)); if (r < DEVICE_NAME_FIELD_LENGTH + 4) { LOGE("Could not retrieve device information"); return false; From ea233d811d20827212dc6344004a9b0d71e530d7 Mon Sep 17 00:00:00 2001 From: Stefan Huber Date: Mon, 2 Aug 2021 14:32:35 +0200 Subject: [PATCH 0662/2244] Fix typo in DEVELOP.md PR #2540 Signed-off-by: Romain Vimont --- DEVELOP.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DEVELOP.md b/DEVELOP.md index d11f139e..d200c3fd 100644 --- a/DEVELOP.md +++ b/DEVELOP.md @@ -76,7 +76,7 @@ The server uses 3 threads: - the **main** thread, encoding and streaming the video to the client; - the **controller** thread, listening for _control messages_ (typically, keyboard and mouse events) from the client; - - the **receiver** thread (managed by the controller), sending _device messges_ + - the **receiver** thread (managed by the controller), sending _device messages_ to the clients (currently, it is only used to send the device clipboard content). From 4bc78244b97e6b1d4ded6dd844e6b6ea5d4bd260 Mon Sep 17 00:00:00 2001 From: Hyperterminal Byte Date: Thu, 5 Aug 2021 09:25:11 +0530 Subject: [PATCH 0663/2244] Fix OBS project ref URL PR #2545 Signed-off-by: Romain Vimont --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0dfa068c..7dc45642 100644 --- a/README.md +++ b/README.md @@ -318,7 +318,7 @@ vlc v4l2:///dev/videoN # VLC might add some buffering delay For example, you could capture the video within [OBS]. -[OBS]: https://obsproject.com/fr +[OBS]: https://obsproject.com/ ### Connection From 4ab3e89c29bc990c1361e8578f36f5f8ada45d81 Mon Sep 17 00:00:00 2001 From: kosantosbik <312roadrunner@gmail.com> Date: Sat, 24 Jul 2021 19:21:06 +0300 Subject: [PATCH 0664/2244] Add README file in Turkish PR #2514 Signed-off-by: Romain Vimont --- README.md | 1 + README.tr.md | 824 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 825 insertions(+) create mode 100644 README.tr.md diff --git a/README.md b/README.md index 7dc45642..77c92ef2 100644 --- a/README.md +++ b/README.md @@ -875,5 +875,6 @@ This README is available in other languages: - [Español (Spanish, `sp`) - v1.17](README.sp.md) - [简体中文 (Simplified Chinese, `zh-Hans`) - v1.17](README.zh-Hans.md) - [繁體中文 (Traditional Chinese, `zh-Hant`) - v1.15](README.zh-Hant.md) +- [Turkish (Turkish, `tr`) - v1.18](README.tr.md) Only this README file is guaranteed to be up-to-date. diff --git a/README.tr.md b/README.tr.md new file mode 100644 index 00000000..15c56b27 --- /dev/null +++ b/README.tr.md @@ -0,0 +1,824 @@ +# scrcpy (v1.18) + +Bu uygulama Android cihazların USB (ya da [TCP/IP][article-tcpip]) üzerinden +görüntülenmesini ve kontrol edilmesini sağlar. _root_ erişimine ihtiyaç duymaz. +_GNU/Linux_, _Windows_ ve _macOS_ sistemlerinde çalışabilir. + +![screenshot](assets/screenshot-debian-600.jpg) + +Öne çıkan özellikler: + +- **hafiflik** (doğal, sadece cihazın ekranını gösterir) +- **performans** (30~60fps) +- **kalite** (1920×1080 ya da üzeri) +- **düşük gecikme süresi** ([35~70ms][lowlatency]) +- **düşük başlangıç süresi** (~1 saniye ilk kareyi gösterme süresi) +- **müdaheleci olmama** (cihazda kurulu yazılım kalmaz) + +[lowlatency]: https://github.com/Genymobile/scrcpy/pull/646 + +## Gereksinimler + +Android cihaz en düşük API 21 (Android 5.0) olmalıdır. + +[Adb hata ayıklamasının][enable-adb] cihazınızda aktif olduğundan emin olun. + +[enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling + +Bazı cihazlarda klavye ve fare ile kontrol için [ilave bir seçenek][control] daha +etkinleştirmeniz gerekebilir. + +[control]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323 + +## Uygulamayı indirin + +Packaging status + +### Özet + +- Linux: `apt install scrcpy` +- Windows: [indir][direct-win64] +- macOS: `brew install scrcpy` + +Kaynak kodu derle: [BUILD] ([basitleştirilmiş süreç][build_simple]) + +[build]: BUILD.md +[build_simple]: BUILD.md#simple + +### Linux + +Debian (şimdilik _testing_ ve _sid_) ve Ubuntu (20.04) için: + +``` +apt install scrcpy +``` + +[Snap] paketi: [`scrcpy`][snap-link]. + +[snap-link]: https://snapstats.org/snaps/scrcpy +[snap]: https://en.wikipedia.org/wiki/Snappy_(package_manager) + +Fedora için, [COPR] paketi: [`scrcpy`][copr-link]. + +[copr]: https://fedoraproject.org/wiki/Category:Copr +[copr-link]: https://copr.fedorainfracloud.org/coprs/zeno/scrcpy/ + +Arch Linux için, [AUR] paketi: [`scrcpy`][aur-link]. + +[aur]: https://wiki.archlinux.org/index.php/Arch_User_Repository +[aur-link]: https://aur.archlinux.org/packages/scrcpy/ + +Gentoo için, [Ebuild] mevcut: [`scrcpy/`][ebuild-link]. + +[ebuild]: https://wiki.gentoo.org/wiki/Ebuild +[ebuild-link]: https://github.com/maggu2810/maggu2810-overlay/tree/master/app-mobilephone/scrcpy + +Ayrıca [uygulamayı el ile de derleyebilirsiniz][build] ([basitleştirilmiş süreç][build_simple]). + +### Windows + +Windows için (`adb` dahil) tüm gereksinimleri ile derlenmiş bir arşiv mevcut: + + - [README](README.md#windows) + +[Chocolatey] ile kurulum: + +[chocolatey]: https://chocolatey.org/ + +```bash +choco install scrcpy +choco install adb # if you don't have it yet +``` + +[Scoop] ile kurulum: + +```bash +scoop install scrcpy +scoop install adb # if you don't have it yet +``` + +[scoop]: https://scoop.sh + +Ayrıca [uygulamayı el ile de derleyebilirsiniz][build]. + +### macOS + +Uygulama [Homebrew] içerisinde mevcut. Sadece kurun: + +[homebrew]: https://brew.sh/ + +```bash +brew install scrcpy +``` + +`adb`, `PATH` içerisinden erişilebilir olmalıdır. Eğer değilse: + +```bash +brew install android-platform-tools +``` + +[MacPorts] kullanılarak adb ve uygulamanın birlikte kurulumu yapılabilir: + +```bash +sudo port install scrcpy +``` + +[macports]: https://www.macports.org/ + +Ayrıca [uygulamayı el ile de derleyebilirsiniz][build]. + +## Çalıştırma + +Android cihazınızı bağlayın ve aşağıdaki komutu çalıştırın: + +```bash +scrcpy +``` + +Komut satırı argümanları aşağıdaki komut ile listelenebilir: + +```bash +scrcpy --help +``` + +## Özellikler + +### Ekran yakalama ayarları + +#### Boyut azaltma + +Bazen, Android cihaz ekranını daha düşük seviyede göstermek performansı artırabilir. + +Hem genişliği hem de yüksekliği bir değere sabitlemek için (ör. 1024): + +```bash +scrcpy --max-size 1024 +scrcpy -m 1024 # kısa versiyon +``` + +Diğer boyut en-boy oranı korunacak şekilde hesaplanır. +Bu şekilde ekran boyutu 1920x1080 olan bir cihaz 1024x576 olarak görünür. + +#### Bit-oranı değiştirme + +Varsayılan bit-oranı 8 Mbps'dir. Değiştirmek için (ör. 2 Mbps): + +```bash +scrcpy --bit-rate 2M +scrcpy -b 2M # kısa versiyon +``` + +#### Çerçeve oranı sınırlama + +Ekran yakalama için maksimum çerçeve oranı için sınır koyulabilir: + +```bash +scrcpy --max-fps 15 +``` + +Bu özellik Android 10 ve sonrası sürümlerde resmi olarak desteklenmektedir, +ancak daha önceki sürümlerde çalışmayabilir. + +#### Kesme + +Cihaz ekranının sadece bir kısmı görünecek şekilde kesilebilir. + +Bu özellik Oculus Go'nun bir gözünü yakalamak gibi durumlarda kullanışlı olur: + +```bash +scrcpy --crop 1224:1440:0:0 # (0,0) noktasından 1224x1440 +``` + +Eğer `--max-size` belirtilmişse yeniden boyutlandırma kesme işleminden sonra yapılır. + +#### Video yönünü kilitleme + +Videonun yönünü kilitlemek için: + +```bash +scrcpy --lock-video-orientation # başlangıç yönü +scrcpy --lock-video-orientation=0 # doğal yön +scrcpy --lock-video-orientation=1 # 90° saatin tersi yönü +scrcpy --lock-video-orientation=2 # 180° +scrcpy --lock-video-orientation=3 # 90° saat yönü +``` + +Bu özellik kaydetme yönünü de etkiler. + +[Pencere ayrı olarak döndürülmüş](#rotation) olabilir. + +#### Kodlayıcı + +Bazı cihazlar birden fazla kodlayıcıya sahiptir, ve bunların bazıları programın +kapanmasına sebep olabilir. Bu durumda farklı bir kodlayıcı seçilebilir: + +```bash +scrcpy --encoder OMX.qcom.video.encoder.avc +``` + +Mevcut kodlayıcıları listelemek için geçerli olmayan bir kodlayıcı ismi girebilirsiniz, +hata mesajı mevcut kodlayıcıları listeleyecektir: + +```bash +scrcpy --encoder _ +``` + +### Yakalama + +#### Kaydetme + +Ekran yakalama sırasında kaydedilebilir: + +```bash +scrcpy --record file.mp4 +scrcpy -r file.mkv +``` + +Yakalama olmadan kayıt için: + +```bash +scrcpy --no-display --record file.mp4 +scrcpy -Nr file.mkv +# Ctrl+C ile kayıt kesilebilir +``` + +"Atlanan kareler" gerçek zamanlı olarak gösterilmese (performans sebeplerinden ötürü) dahi kaydedilir. +Kareler cihazda _zamandamgası_ ile saklanır, bu sayede [paket gecikme varyasyonu] +kayıt edilen dosyayı etkilemez. + +[paket gecikme varyasyonu]: https://en.wikipedia.org/wiki/Packet_delay_variation + +#### v4l2loopback + +Linux'ta video akışı bir v4l2 loopback cihazına gönderilebilir. Bu sayede Android +cihaz bir web kamerası gibi davranabilir. + +Bu işlem için `v4l2loopback` modülü kurulu olmalıdır: + +```bash +sudo apt install v4l2loopback-dkms +``` + +v4l2 cihazı oluşturmak için: + +```bash +sudo modprobe v4l2loopback +``` + +Bu komut `/dev/videoN` adresinde `N` yerine bir tamsayı koyarak yeni bir video +cihazı oluşturacaktır. +(birden fazla cihaz oluşturmak veya spesifik ID'ye sahip cihazlar için +diğer [seçenekleri](https://github.com/umlaeute/v4l2loopback#options) inceleyebilirsiniz.) + +Aktif cihazları listelemek için: + +```bash +# v4l-utils paketi ile +v4l2-ctl --list-devices + +# daha basit ama yeterli olabilecek şekilde +ls /dev/video* +``` + +v4l2 kullanarak scrpy kullanmaya başlamak için: + +```bash +scrcpy --v4l2-sink=/dev/videoN +scrcpy --v4l2-sink=/dev/videoN --no-display # ayna penceresini kapatarak +scrcpy --v4l2-sink=/dev/videoN -N # kısa versiyon +``` + +(`N` harfini oluşturulan cihaz ID numarası ile değiştirin. `ls /dev/video*` cihaz ID'lerini görebilirsiniz.) + +Aktifleştirildikten sonra video akışını herhangi bir v4l2 özellikli araçla açabilirsiniz: + +```bash +ffplay -i /dev/videoN +vlc v4l2:///dev/videoN # VLC kullanırken yükleme gecikmesi olabilir +``` + +Örneğin, [OBS] ile video akışını kullanabilirsiniz. + +[obs]: https://obsproject.com/ + +### Bağlantı + +#### Kablosuz + +_Scrcpy_ cihazla iletişim kurmak için `adb`'yi kullanır, Ve `adb` +bir cihaza TCP/IP kullanarak [bağlanabilir]. + +1. Cihazınızı bilgisayarınızla aynı Wi-Fi ağına bağlayın. +2. Cihazınızın IP adresini bulun. Ayarlar → Telefon Hakkında → Durum sekmesinden veya + aşağıdaki komutu çalıştırarak öğrenebilirsiniz: + + ```bash + adb shell ip route | awk '{print $9}' + ``` + +3. Cihazınızda TCP/IP üzerinden adb kullanımını etkinleştirin: `adb tcpip 5555`. +4. Cihazınızı bilgisayarınızdan sökün. +5. Cihazınıza bağlanın: `adb connect DEVICE_IP:5555` _(`DEVICE_IP` değerini değiştirin)_. +6. `scrcpy` komutunu normal olarak çalıştırın. + +Bit-oranını ve büyüklüğü azaltmak yararlı olabilir: + +```bash +scrcpy --bit-rate 2M --max-size 800 +scrcpy -b2M -m800 # kısa version +``` + +[bağlanabilir]: https://developer.android.com/studio/command-line/adb.html#wireless + +#### Birden fazla cihaz + +Eğer `adb devices` komutu birden fazla cihaz listeliyorsa _serial_ değerini belirtmeniz gerekir: + +```bash +scrcpy --serial 0123456789abcdef +scrcpy -s 0123456789abcdef # kısa versiyon +``` + +Eğer cihaz TCP/IP üzerinden bağlanmışsa: + +```bash +scrcpy --serial 192.168.0.1:5555 +scrcpy -s 192.168.0.1:5555 # kısa version +``` + +Birden fazla cihaz için birden fazla _scrcpy_ uygulaması çalıştırabilirsiniz. + +#### Cihaz bağlantısı ile otomatik başlatma + +[AutoAdb] ile yapılabilir: + +```bash +autoadb scrcpy -s '{}' +``` + +[autoadb]: https://github.com/rom1v/autoadb + +#### SSH Tünel + +Uzaktaki bir cihaza erişmek için lokal `adb` istemcisi, uzaktaki bir `adb` sunucusuna +(aynı _adb_ sürümünü kullanmak şartı ile) bağlanabilir : + +```bash +adb kill-server # 5037 portunda çalışan lokal adb sunucusunu kapat +ssh -CN -L5037:localhost:5037 -R27183:localhost:27183 your_remote_computer +# bunu açık tutun +``` + +Başka bir terminalde: + +```bash +scrcpy +``` + +Uzaktan port yönlendirme ileri yönlü bağlantı kullanabilirsiniz +(`-R` yerine `-L` olduğuna dikkat edin): + +```bash +adb kill-server # 5037 portunda çalışan lokal adb sunucusunu kapat +ssh -CN -L5037:localhost:5037 -L27183:localhost:27183 your_remote_computer +# bunu açık tutun +``` + +Başka bir terminalde: + +```bash +scrcpy --force-adb-forward +``` + +Kablosuz bağlantı gibi burada da kalite düşürmek faydalı olabilir: + +``` +scrcpy -b2M -m800 --max-fps 15 +``` + +### Pencere ayarları + +#### İsim + +Cihaz modeli varsayılan pencere ismidir. Değiştirmek için: + +```bash +scrcpy --window-title 'Benim cihazım' +``` + +#### Konum ve + +Pencerenin başlangıç konumu ve boyutu belirtilebilir: + +```bash +scrcpy --window-x 100 --window-y 100 --window-width 800 --window-height 600 +``` + +#### Kenarlıklar + +Pencere dekorasyonunu kapatmak için: + +```bash +scrcpy --window-borderless +``` + +#### Her zaman üstte + +Scrcpy penceresini her zaman üstte tutmak için: + +```bash +scrcpy --always-on-top +``` + +#### Tam ekran + +Uygulamayı tam ekran başlatmak için: + +```bash +scrcpy --fullscreen +scrcpy -f # kısa versiyon +``` + +Tam ekran MOD+f ile dinamik olarak değiştirilebilir. + +#### Döndürme + +Pencere döndürülebilir: + +```bash +scrcpy --rotation 1 +``` + +Seçilebilecek değerler: + +- `0`: döndürme yok +- `1`: 90 derece saat yönünün tersi +- `2`: 180 derece +- `3`: 90 derece saat yönü + +Döndürme MOD+_(sol)_ ve +MOD+ _(sağ)_ ile dinamik olarak değiştirilebilir. + +_scrcpy_'de 3 farklı döndürme olduğuna dikkat edin: + +- MOD+r cihazın yatay veya dikey modda çalışmasını sağlar. + (çalışan uygulama istenilen oryantasyonda çalışmayı desteklemiyorsa döndürme + işlemini reddedebilir.) +- [`--lock-video-orientation`](#lock-video-orientation) görüntü yakalama oryantasyonunu + (cihazdan bilgisayara gelen video akışının oryantasyonu) değiştirir. Bu kayıt işlemini + etkiler. +- `--rotation` (or MOD+/MOD+) + pencere içeriğini dönderir. Bu sadece canlı görüntüyü etkiler, kayıt işlemini etkilemez. + +### Diğer ekran yakalama seçenekleri + +#### Yazma korumalı + +Kontrolleri devre dışı bırakmak için (cihazla etkileşime geçebilecek her şey: klavye ve +fare girdileri, dosya sürükleyip bırakma): + +```bash +scrcpy --no-control +scrcpy -n +``` + +#### Ekran + +Eğer cihazın birden fazla ekranı varsa hangi ekranın kullanılacağını seçebilirsiniz: + +```bash +scrcpy --display 1 +``` + +Kullanılabilecek ekranları listelemek için: + +```bash +adb shell dumpsys display # çıktı içerisinde "mDisplayId=" terimini arayın +``` + +İkinci ekran ancak cihaz Android sürümü 10 veya üzeri olmalıdır (değilse yazma korumalı +olarak görüntülenir). + +#### Uyanık kalma + +Cihazın uyku moduna girmesini engellemek için: + +```bash +scrcpy --stay-awake +scrcpy -w +``` + +scrcpy kapandığında cihaz başlangıç durumuna geri döner. + +#### Ekranı kapatma + +Ekran yakalama sırasında cihazın ekranı kapatılabilir: + +```bash +scrcpy --turn-screen-off +scrcpy -S +``` + +Ya da MOD+o kısayolunu kullanabilirsiniz. + +Tekrar açmak için ise MOD+Shift+o tuşlarına basın. + +Android'de, `GÜÇ` tuşu her zaman ekranı açar. Eğer `GÜÇ` sinyali scrcpy ile +gönderilsiyse (sağ tık veya MOD+p), ekran kısa bir gecikme +ile kapanacaktır. Fiziksel `GÜÇ` tuşuna basmak hala ekranın açılmasına sebep olacaktır. + +Bu cihazın uykuya geçmesini engellemek için kullanılabilir: + +```bash +scrcpy --turn-screen-off --stay-awake +scrcpy -Sw +``` + +#### Dokunuşları gösterme + +Sunumlar sırasında fiziksel dokunuşları (fiziksel cihazdaki) göstermek +faydalı olabilir. + +Android'de bu özellik _Geliştici seçenekleri_ içerisinde bulunur. + +_Scrcpy_ bu özelliği çalışırken etkinleştirebilir ve kapanırken eski +haline geri getirebilir: + +```bash +scrcpy --show-touches +scrcpy -t +``` + +Bu opsiyon sadece _fiziksel_ dokunuşları (cihaz ekranındaki) gösterir. + +#### Ekran koruyucuyu devre dışı bırakma + +Scrcpy varsayılan ayarlarında ekran koruyucuyu devre dışı bırakmaz. + +Bırakmak için: + +```bash +scrcpy --disable-screensaver +``` + +### Girdi kontrolü + +#### Cihaz ekranını dönderme + +MOD+r tuşları ile yatay ve dikey modlar arasında +geçiş yapabilirsiniz. + +Bu kısayol ancak çalışan uygulama desteklediği takdirde ekranı döndürecektir. + +#### Kopyala yapıştır + +Ne zaman Android cihazdaki pano değişse bilgisayardaki pano otomatik olarak +senkronize edilir. + +Tüm Ctrl kısayolları cihaza iletilir: + +- Ctrl+c genelde kopyalar +- Ctrl+x genelde keser +- Ctrl+v genelde yapıştırır (bilgisayar ve cihaz arasındaki + pano senkronizasyonundan sonra) + +Bu kısayollar genelde beklediğiniz gibi çalışır. + +Ancak kısayolun gerçekten yaptığı eylemi açık olan uygulama belirler. +Örneğin, _Termux_ Ctrl+c ile kopyalama yerine +SIGINT sinyali gönderir, _K-9 Mail_ ise yeni mesaj oluşturur. + +Bu tip durumlarda kopyalama, kesme ve yapıştırma için (Android versiyon 7 ve +üstü): + +- MOD+c `KOPYALA` +- MOD+x `KES` +- MOD+v `YAPIŞTIR` (bilgisayar ve cihaz arasındaki + pano senkronizasyonundan sonra) + +Bunlara ek olarak, MOD+Shift+v tuşları +bilgisayar pano içeriğini tuş basma eylemleri şeklinde gönderir. Bu metin +yapıştırmayı desteklemeyen (_Termux_ gibi) uygulamar için kullanışlıdır, +ancak ASCII olmayan içerikleri bozabilir. + +**UYARI:** Bilgisayar pano içeriğini cihaza yapıştırmak +(Ctrl+v ya da MOD+v tuşları ile) +içeriği cihaz panosuna kopyalar. Sonuç olarak, herhangi bir Android uygulaması +içeriğe erişebilir. Hassas içerikler (parolalar gibi) için bu özelliği kullanmaktan +kaçının. + +Bazı cihazlar pano değişikleri konusunda beklenilen şekilde çalışmayabilir. +Bu durumlarda `--legacy-paste` argümanı kullanılabilir. Bu sayede +Ctrl+v ve MOD+v tuşları da +pano içeriğini tuş basma eylemleri şeklinde gönderir +(MOD+Shift+v ile aynı şekilde). + +#### İki parmak ile yakınlaştırma + +"İki parmak ile yakınlaştırma" için: Ctrl+_tıkla-ve-sürükle_. + +Daha açıklayıcı şekilde, Ctrl tuşuna sol-tık ile birlikte basılı +tutun. Sol-tık serbest bırakılıncaya kadar yapılan tüm fare hareketleri +ekran içeriğini ekranın merkezini baz alarak dönderir, büyütür veya küçültür +(eğer uygulama destekliyorsa). + +Scrcpy ekranın merkezinde bir "sanal parmak" varmış gibi davranır. + +#### Metin gönderme tercihi + +Metin girilirken ili çeşit [eylem][textevents] gerçekleştirilir: + +- _tuş eylemleri_, bir tuşa basıldığı sinyalini verir; +- _metin eylemleri_, bir metin girildiği sinyalini verir. + +Varsayılan olarak, harfler tuş eylemleri kullanılarak gönderilir. Bu sayede +klavye oyunlarda beklenilene uygun olarak çalışır (Genelde WASD tuşları). + +Ancak bu [bazı problemlere][prefertext] yol açabilir. Eğer bu problemler ile +karşılaşırsanız metin eylemlerini tercih edebilirsiniz: + +```bash +scrcpy --prefer-text +``` + +(Ama bu oyunlardaki klavye davranışlarını bozacaktır) + +[textevents]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-text-input +[prefertext]: https://github.com/Genymobile/scrcpy/issues/650#issuecomment-512945343 + +#### Tuş tekrarı + +Varsayılan olarak, bir tuşa basılı tutmak tuş eylemini tekrarlar. Bu durum +bazı oyunlarda problemlere yol açabilir. + +Tuş eylemlerinin tekrarını kapatmak için: + +```bash +scrcpy --no-key-repeat +``` + +#### Sağ-tık ve Orta-tık + +Varsayılan olarak, sağ-tık GERİ (ya da GÜÇ açma) eylemlerini, orta-tık ise +ANA EKRAN eylemini tetikler. Bu kısayolları devre dışı bırakmak için: + +```bash +scrcpy --forward-all-clicks +``` + +### Dosya bırakma + +#### APK kurulumu + +APK kurmak için, bilgisayarınızdaki APK dosyasını (`.apk` ile biten) _scrcpy_ +penceresine sürükleyip bırakın. + +Bu eylem görsel bir geri dönüt oluşturmaz, konsola log yazılır. + +#### Dosyayı cihaza gönderme + +Bir dosyayı cihazdaki `/sdcard/Download/` dizinine atmak için, (APK olmayan) +bir dosyayı _scrcpy_ penceresine sürükleyip bırakın. + +Bu eylem görsel bir geri dönüt oluşturmaz, konsola log yazılır. + +Hedef dizin uygulama başlatılırken değiştirilebilir: + +```bash +scrcpy --push-target=/sdcard/Movies/ +``` + +### Ses iletimi + +_Scrcpy_ ses iletimi yapmaz. Yerine [sndcpy] kullanabilirsiniz. + +Ayrıca bakınız [issue #14]. + +[sndcpy]: https://github.com/rom1v/sndcpy +[issue #14]: https://github.com/Genymobile/scrcpy/issues/14 + +## Kısayollar + +Aşağıdaki listede, MOD kısayol tamamlayıcısıdır. Varsayılan olarak +(sol) Alt veya (sol) Super tuşudur. + +Bu tuş `--shortcut-mod` argümanı kullanılarak `lctrl`, `rctrl`, +`lalt`, `ralt`, `lsuper` ve `rsuper` tuşlarından biri ile değiştirilebilir. +Örneğin: + +```bash +# Sağ Ctrl kullanmak için +scrcpy --shortcut-mod=rctrl + +# Sol Ctrl, Sol Alt veya Sol Super tuşlarından birini kullanmak için +scrcpy --shortcut-mod=lctrl+lalt,lsuper +``` + +_[Super] tuşu genelde Windows veya Cmd tuşudur._ + +[super]: https://en.wikipedia.org/wiki/Super_key_(keyboard_button) + +| Action | Shortcut | +| ------------------------------------------------ | :-------------------------------------------------------- | +| Tam ekran modunu değiştirme | MOD+f | +| Ekranı sola çevirme | MOD+ _(sol)_ | +| Ekranı sağa çevirme | MOD+ _(sağ)_ | +| Pencereyi 1:1 oranına çevirme (pixel-perfect) | MOD+g | +| Penceredeki siyah kenarlıkları kaldırma | MOD+w \| _Çift-sol-tık¹_ | +| `ANA EKRAN` tuşu | MOD+h \| _Orta-tık_ | +| `GERİ` tuşu | MOD+b \| _Sağ-tık²_ | +| `UYGULAMA_DEĞİŞTİR` tuşu | MOD+s \| _4.tık³_ | +| `MENÜ` tuşu (ekran kilidini açma) | MOD+m | +| `SES_AÇ` tuşu | MOD+ _(yukarı)_ | +| `SES_KIS` tuşu | MOD+ _(aşağı)_ | +| `GÜÇ` tuşu | MOD+p | +| Gücü açma | _Sağ-tık²_ | +| Cihaz ekranını kapatma (ekran yakalama durmadan) | MOD+o | +| Cihaz ekranını açma | MOD+Shift+o | +| Cihaz ekranını dönderme | MOD+r | +| Bildirim panelini genişletme | MOD+n \| _5.tık³_ | +| Ayarlar panelini genişletme | MOD+n+n \| _Çift-5.tık³_ | +| Panelleri kapatma | MOD+Shift+n | +| Panoya kopyalama⁴ | MOD+c | +| Panoya kesme⁴ | MOD+x | +| Panoları senkronize ederek yapıştırma⁴ | MOD+v | +| Bilgisayar panosundaki metini girme | MOD+Shift+v | +| FPS sayacını açma/kapatma (terminalde) | MOD+i | +| İki parmakla yakınlaştırma | Ctrl+_tıkla-ve-sürükle_ | + +_¹Siyah kenarlıkları silmek için üzerine çift tıklayın._ +_²Sağ-tık ekran kapalıysa açar, değilse GERİ sinyali gönderir._ +_³4. ve 5. fare tuşları (eğer varsa)._ +_⁴Sadece Android 7 ve üzeri versiyonlarda._ + +Tekrarlı tuşu olan kısayollar tuş bırakılıp tekrar basılarak tekrar çalıştırılır. +Örneğin, "Ayarlar panelini genişletmek" için: + +1. MOD tuşuna basın ve basılı tutun. +2. n tuşuna iki defa basın. +3. MOD tuşuna basmayı bırakın. + +Tüm Ctrl+_tuş_ kısayolları cihaza gönderilir. Bu sayede istenilen komut +uygulama tarafından çalıştırılır. + +## Özel dizinler + +Varsayılandan farklı bir _adb_ programı çalıştırmak için `ADB` ortam değişkenini +ayarlayın: + +```bash +ADB=/path/to/adb scrcpy +``` + +`scrcpy-server` programının dizinini değiştirmek için `SCRCPY_SERVER_PATH` +değişkenini ayarlayın. + +[useful]: https://github.com/Genymobile/scrcpy/issues/278#issuecomment-429330345 + +## Neden _scrcpy_? + +Bir meslektaşım [gnirehtet] gibi söylenmesi zor bir isim bulmam için bana meydan okudu. + +[`strcpy`] **str**ing kopyalıyor; `scrcpy` **scr**een kopyalıyor. + +[gnirehtet]: https://github.com/Genymobile/gnirehtet +[`strcpy`]: http://man7.org/linux/man-pages/man3/strcpy.3.html + +## Nasıl derlenir? + +Bakınız [BUILD]. + +## Yaygın problemler + +Bakınız [FAQ](FAQ.md). + +## Geliştiriciler + +[Geliştiriciler sayfası]nı okuyun. + +[geliştiriciler sayfası]: DEVELOP.md + +## Lisans + + Copyright (C) 2018 Genymobile + Copyright (C) 2018-2021 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. + +## Makaleler + +- [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 d6aaa5bf9aa3710660c683b6e3e0ed971ee44af5 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 13 Aug 2021 12:37:29 +0200 Subject: [PATCH 0665/2244] Add a FAQ section for Wayland support The video driver might need to be explicitly set to wayland. Refs #2554 Refs #2559 --- FAQ.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/FAQ.md b/FAQ.md index c1e39a39..b11896db 100644 --- a/FAQ.md +++ b/FAQ.md @@ -153,6 +153,26 @@ You may also need to configure the [scaling behavior]: [scaling behavior]: https://github.com/Genymobile/scrcpy/issues/40#issuecomment-424466723 +### Issue with Wayland + +By default, SDL uses x11 on Linux. The [video driver] can be changed via the +`SDL_VIDEODRIVER` environment variable: + +[video driver]: https://wiki.libsdl.org/FAQUsingSDL#how_do_i_choose_a_specific_video_driver + +```bash +export SDL_VIDEODRIVER=wayland +scrcpy +``` + +On some distributions (at least Fedora), the package `libdecor` must be +installed manually. + +See issues [#2554] and [#2559]. + +[#2554]: https://github.com/Genymobile/scrcpy/issues/2554 +[#2559]: https://github.com/Genymobile/scrcpy/issues/2559 + ### KWin compositor crashes From c96f5c70e9c6fe82da059c7d24e77d9730aa7f86 Mon Sep 17 00:00:00 2001 From: horizon86 Date: Tue, 17 Aug 2021 22:12:01 +0800 Subject: [PATCH 0666/2244] Add Simplified Chinese translation of FAQ PR #2568 Signed-off-by: Romain Vimont --- FAQ.md | 1 + FAQ.zh-Hans.md | 240 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 241 insertions(+) create mode 100644 FAQ.zh-Hans.md diff --git a/FAQ.md b/FAQ.md index b11896db..f74845a5 100644 --- a/FAQ.md +++ b/FAQ.md @@ -260,3 +260,4 @@ This FAQ is available in other languages: - [Italiano (Italiano, `it`) - v1.17](FAQ.it.md) - [한국어 (Korean, `ko`) - v1.11](FAQ.ko.md) + - [简体中文 (Simplified Chinese, `zh-Hans`) - v1.18](FAQ.zh-Hans.md) diff --git a/FAQ.zh-Hans.md b/FAQ.zh-Hans.md new file mode 100644 index 00000000..136b5f2e --- /dev/null +++ b/FAQ.zh-Hans.md @@ -0,0 +1,240 @@ +只有原版的[FAQ](FAQ.md)会保持更新。 +本文根据[d6aaa5]翻译。 + +[d6aaa5]:https://github.com/Genymobile/scrcpy/blob/d6aaa5bf9aa3710660c683b6e3e0ed971ee44af5/FAQ.md + +# 常见问题 + +这里是一些常见的问题以及他们的状态。 + +## `adb` 相关问题 + +`scrcpy` 执行 `adb` 命令来初始化和设备之间的连接。如果`adb` 执行失败了, scrcpy 就无法工作。 + +在这种情况中,将会输出这个错误: + +> ERROR: "adb push" returned with value 1 + +这通常不是 _scrcpy_ 的bug,而是你的环境的问题。 + +要找出原因,请执行以下操作: + +```bash +adb devices +``` + +### 找不到`adb` + + +你的`PATH`中需要能访问到`adb`。 + +在Windows上,当前目录会包含在`PATH`中,并且`adb.exe`也包含在发行版中,因此它应该是开箱即用(直接解压就可以)的。 + + +### 设备未授权 + +参见这里 [stackoverflow][device-unauthorized]. + +[device-unauthorized]: https://stackoverflow.com/questions/23081263/adb-android-device-unauthorized + + +### 未检测到设备 + +> adb: error: failed to get feature set: no devices/emulators found + +确认已经正确启用 [adb debugging][enable-adb]. + +如果你的设备没有被检测到,你可能需要一些[驱动][drivers] (在 Windows上). + +[enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling +[drivers]: https://developer.android.com/studio/run/oem-usb.html + + +### 已连接多个设备 + +如果连接了多个设备,您将遇到以下错误: + +> adb: error: failed to get feature set: more than one device/emulator + +必须提供要镜像的设备的标识符: + +```bash +scrcpy -s 01234567890abcdef +``` + +注意,如果你的设备是通过 TCP/IP 连接的, 你将会收到以下消息: + +> adb: error: more than one device/emulator +> ERROR: "adb reverse" returned with value 1 +> WARN: 'adb reverse' failed, fallback to 'adb forward' + +这是意料之中的 (由于旧版安卓的一个bug, 请参见 [#5]),但是在这种情况下,scrcpy会退回到另一种方法,这种方法应该可以起作用。 + +[#5]: https://github.com/Genymobile/scrcpy/issues/5 + + +### adb版本之间冲突 + +> adb server version (41) doesn't match this client (39); killing... + +同时使用多个版本的`adb`时会发生此错误。你必须查找使用不同`adb`版本的程序,并在所有地方使用相同版本的`adb`。 + +你可以覆盖另一个程序中的`adb`二进制文件,或者通过设置`ADB`环境变量来让 _scrcpy_ 使用特定的`adb`二进制文件。 + +```bash +set ADB=/path/to/your/adb +scrcpy +``` + + +### 设备断开连接 + +如果 _scrcpy_ 在警告“设备连接断开”的情况下自动中止,那就意味着`adb`连接已经断开了。 +请尝试使用另一条USB线或者电脑上的另一个USB接口。 +请参看 [#281] 和 [#283]。 + +[#281]: https://github.com/Genymobile/scrcpy/issues/281 +[#283]: https://github.com/Genymobile/scrcpy/issues/283 + +## 控制相关问题 + +### 鼠标和键盘不起作用 + + +在某些设备上,您可能需要启用一个选项以允许 [模拟输入][simulating input]。 + +在开发者选项中,打开: + +> **USB调试 (安全设置)** +> _允许通过USB调试修改权限或模拟点击_ + +[simulating input]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323 + + +### 特殊字符不起作用 + +可输入的文本[被限制为ASCII字符][text-input]。也可以用一些小技巧输入一些[带重音符号的字符][accented-characters],但是仅此而已。参见[#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 + + +## 客户端相关问题 + +### 效果很差 + +如果你的客户端窗口分辨率比你的设备屏幕小,则可能出现效果差的问题,尤其是在文本上(参见 [#40])。 + +[#40]: https://github.com/Genymobile/scrcpy/issues/40 + + +为了提升降尺度的质量,如果渲染器是OpenGL并且支持mip映射,就会自动开启三线性过滤。 + +在Windows上,你可能希望强制使用OpenGL: + +``` +scrcpy --render-driver=opengl +``` + +你可能还需要配置[缩放行为][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 + + +### Wayland相关的问题 + +在Linux上,SDL默认使用x11。可以通过`SDL_VIDEODRIVER`环境变量来更改[视频驱动][video driver]: + +[video driver]: https://wiki.libsdl.org/FAQUsingSDL#how_do_i_choose_a_specific_video_driver + +```bash +export SDL_VIDEODRIVER=wayland +scrcpy +``` + +在一些发行版上 (至少包括 Fedora), `libdecor` 包必须手动安装。 + +参见 [#2554] 和 [#2559]。 + +[#2554]: https://github.com/Genymobile/scrcpy/issues/2554 +[#2559]: https://github.com/Genymobile/scrcpy/issues/2559 + + +### KWin compositor 崩溃 + +在Plasma桌面中,当 _scrcpy_ 运行时,会禁用compositor。 + +一种解决方法是, [禁用 "Block compositing"][kwin]. + +[kwin]: https://github.com/Genymobile/scrcpy/issues/114#issuecomment-378778613 + + +## 崩溃 + +### 异常 +可能有很多原因。一个常见的原因是您的设备无法按给定清晰度进行编码: + +> ``` +> 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] +> java.lang.IllegalStateException +> at android.media.MediaCodec.native_dequeueOutputBuffer(Native Method) +> ``` + +请尝试使用更低的清晰度: + +``` +scrcpy -m 1920 +scrcpy -m 1024 +scrcpy -m 800 +``` + +你也可以尝试另一种 [编码器](README.md#encoder)。 + + +## Windows命令行 + +一些Windows用户不熟悉命令行。以下是如何打开终端并带参数执行`scrcpy`: + + 1. 按下 Windows+r,打开一个对话框。 + 2. 输入 `cmd` 并按 Enter,这样就打开了一个终端。 + 3. 通过输入以下命令,切换到你的 _scrcpy_ 所在的目录 (根据你的实际位置修改路径): + + ```bat + cd C:\Users\user\Downloads\scrcpy-win64-xxx + ``` + + 然后按 Enter + 4. 输入你的命令。比如: + + ```bat + scrcpy --record file.mkv + ``` + +如果你打算总是使用相同的参数,在`scrcpy`目录创建一个文件 `myscrcpy.bat` +(启用 [显示文件拓展名][show file extensions] 避免混淆),文件中包含你的命令。例如: + +```bat +scrcpy --prefer-text --turn-screen-off --stay-awake +``` + +然后双击刚刚创建的文件。 + +你也可以编辑 `scrcpy-console.bat` 或者 `scrcpy-noconsole.vbs`(的副本)来添加参数。 + +[show file extensions]: https://www.howtogeek.com/205086/beginner-how-to-make-windows-show-file-extensions/ From 3761f56c28511dd62fc3c7116ccb2dfa88ffec7c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 26 Aug 2021 12:26:44 +0200 Subject: [PATCH 0667/2244] Declare callbacks static It was a typo, "static" was missing. --- 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 25822526..6a285788 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -330,7 +330,7 @@ scrcpy(const struct scrcpy_options *options) { av_log_set_callback(av_log_callback); - const struct stream_callbacks stream_cbs = { + static const struct stream_callbacks stream_cbs = { .on_eos = stream_on_eos, }; stream_init(&s->stream, s->server.video_socket, &stream_cbs, NULL); From 3fdc89ad42f6543b2946567276e5bd0d42d6956d Mon Sep 17 00:00:00 2001 From: nkh0472 Date: Wed, 25 Aug 2021 11:09:58 +0800 Subject: [PATCH 0668/2244] Upgrade platform-tools (31.0.3) for Windows Include the latest version of adb in Windows releases. PR #2588 Signed-off-by: Romain Vimont --- prebuilt-deps/Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/prebuilt-deps/Makefile b/prebuilt-deps/Makefile index d75d0a5c..75736f8c 100644 --- a/prebuilt-deps/Makefile +++ b/prebuilt-deps/Makefile @@ -35,6 +35,6 @@ prepare-sdl2: SDL2-2.0.14 prepare-adb: - @./prepare-dep https://dl.google.com/android/repository/platform-tools_r31.0.2-windows.zip \ - d560cb8ded83ae04763b94632673481f14843a5969256569623cfeac82db4ba5 \ + @./prepare-dep https://dl.google.com/android/repository/platform-tools_r31.0.3-windows.zip \ + 0f4b8fdd26af2c3733539d6eebb3c2ed499ea1d4bb1f4e0ecc2d6016961a6e24 \ platform-tools From 3a39bacb76003fec3dcfd14200f12d52114c3289 Mon Sep 17 00:00:00 2001 From: nkh0472 Date: Wed, 25 Aug 2021 11:11:41 +0800 Subject: [PATCH 0669/2244] Upgrade SDL (2.0.16) for Windows Include the latest version of SDL in Windows releases. PR #2589 Signed-off-by: Romain Vimont --- cross_win32.txt | 2 +- cross_win64.txt | 2 +- prebuilt-deps/Makefile | 6 +++--- release.mk | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cross_win32.txt b/cross_win32.txt index 0d8a843a..4db17be7 100644 --- a/cross_win32.txt +++ b/cross_win32.txt @@ -17,4 +17,4 @@ endian = 'little' [properties] prebuilt_ffmpeg_shared = 'ffmpeg-4.3.1-win32-shared' prebuilt_ffmpeg_dev = 'ffmpeg-4.3.1-win32-dev' -prebuilt_sdl2 = 'SDL2-2.0.14/i686-w64-mingw32' +prebuilt_sdl2 = 'SDL2-2.0.16/i686-w64-mingw32' diff --git a/cross_win64.txt b/cross_win64.txt index 6a39c391..d03f0272 100644 --- a/cross_win64.txt +++ b/cross_win64.txt @@ -17,4 +17,4 @@ endian = 'little' [properties] prebuilt_ffmpeg_shared = 'ffmpeg-4.3.1-win64-shared' prebuilt_ffmpeg_dev = 'ffmpeg-4.3.1-win64-dev' -prebuilt_sdl2 = 'SDL2-2.0.14/x86_64-w64-mingw32' +prebuilt_sdl2 = 'SDL2-2.0.16/x86_64-w64-mingw32' diff --git a/prebuilt-deps/Makefile b/prebuilt-deps/Makefile index 75736f8c..dced047c 100644 --- a/prebuilt-deps/Makefile +++ b/prebuilt-deps/Makefile @@ -30,9 +30,9 @@ prepare-ffmpeg-dev-win64: ffmpeg-4.3.1-win64-dev prepare-sdl2: - @./prepare-dep https://libsdl.org/release/SDL2-devel-2.0.14-mingw.tar.gz \ - 405eaff3eb18f2e08fe669ef9e63bc9a8710b7d343756f238619761e9b60407d \ - SDL2-2.0.14 + @./prepare-dep https://libsdl.org/release/SDL2-devel-2.0.16-mingw.tar.gz \ + 2bfe48628aa9635c12eac7d421907e291525de1d0b04b3bca4a5bd6e6c881a6f \ + SDL2-2.0.16 prepare-adb: @./prepare-dep https://dl.google.com/android/repository/platform-tools_r31.0.3-windows.zip \ diff --git a/release.mk b/release.mk index 2a026135..e327654c 100644 --- a/release.mk +++ b/release.mk @@ -102,7 +102,7 @@ dist-win32: build-server build-win32 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.14/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/SDL2-2.0.16/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/" dist-win64: build-server build-win64 mkdir -p "$(DIST)/$(WIN64_TARGET_DIR)" @@ -118,7 +118,7 @@ dist-win64: build-server build-win64 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.14/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/SDL2-2.0.16/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/" zip-win32: dist-win32 cd "$(DIST)/$(WIN32_TARGET_DIR)"; \ From 116acc8d25bc8c20efd88ace43baceaf20c66c20 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 28 Aug 2021 13:56:39 +0200 Subject: [PATCH 0670/2244] Use SOURCE_MOUSE for scroll events This has no practical impact (AFAIK), but a scroll events should come from a mouse. Refs #2602 --- 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 92986241..45882bb9 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -241,7 +241,7 @@ public class Controller { MotionEvent event = MotionEvent .obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, 0, 1f, 1f, DEFAULT_DEVICE_ID, 0, - InputDevice.SOURCE_TOUCHSCREEN, 0); + InputDevice.SOURCE_MOUSE, 0); return device.injectEvent(event); } From b5e98db6352737ce82e89538ada264ccfc3a96dc Mon Sep 17 00:00:00 2001 From: a1346054 <36859588+a1346054@users.noreply.github.com> Date: Thu, 2 Sep 2021 14:46:27 +0000 Subject: [PATCH 0671/2244] Fix typo in manpage PR #2606 Signed-off-by: Romain Vimont --- app/scrcpy.1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 00589a43..1b69a065 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -90,7 +90,7 @@ This is a workaround for some devices not behaving as expected when setting the .TP .BI "\-\-lock\-video\-orientation[=value] -Lock video orientation to \fIvalue\fR. Possible values are "unlocked", "initial" (locked to the initial orientation), 0, 1, 2 and 3. Natural device orientation is 0, and each increment adds a 90 degrees otation counterclockwise. +Lock video orientation to \fIvalue\fR. Possible values are "unlocked", "initial" (locked to the initial orientation), 0, 1, 2 and 3. Natural device orientation is 0, and each increment adds a 90 degrees rotation counterclockwise. Default is "unlocked". From 4d6dd9d28130105ec6c0814364435ad36b6386ca Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 9 Sep 2021 10:24:08 +0200 Subject: [PATCH 0672/2244] Compute scrcpy directory manually The function dirname() does not work correctly everywhere with non-ASCII characters. Fixes #2619 --- app/src/server.c | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/app/src/server.c b/app/src/server.c index e1db2f0c..e3c8c344 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -60,7 +60,20 @@ get_server_path(void) { // not found, use current directory return strdup(SERVER_FILENAME); } - char *dir = dirname(executable_path); + + // dirname() does not work correctly everywhere, so get the parent + // directory manually. + // See + char *p = strrchr(executable_path, PATH_SEPARATOR); + if (!p) { + LOGE("Unexpected executable path: \"%s\" (it should contain a '%c')", + executable_path, PATH_SEPARATOR); + free(executable_path); + return strdup(SERVER_FILENAME); + } + + *p = '\0'; // modify executable_path in place + char *dir = executable_path; size_t dirlen = strlen(dir); // sizeof(SERVER_FILENAME) gives statically the size including the null byte From 1d1c9f36f43314523187b8e1fb868d03718dcc79 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 9 Sep 2021 18:07:38 +0200 Subject: [PATCH 0673/2244] Retrieve correct error messages on Windows For sockets functions, Windows does not store error codes in errno, so perror() does not print any error. Use WSAGetLastError() instead. Refs #2624 --- app/src/util/net.c | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/app/src/util/net.c b/app/src/util/net.c index 75daef8b..17299424 100644 --- a/app/src/util/net.c +++ b/app/src/util/net.c @@ -19,11 +19,27 @@ typedef struct in_addr IN_ADDR; #endif +static void +net_perror(const char *s) { +#ifdef _WIN32 + int error = WSAGetLastError(); + char *wsa_message; + FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, + NULL, error, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + (char *) &wsa_message, 0, NULL); + // no explicit '\n', wsa_message already contains a trailing '\n' + fprintf(stderr, "%s: [%d] %s", s, error, wsa_message); + LocalFree(wsa_message); +#else + perror(s); +#endif +} + socket_t net_connect(uint32_t addr, uint16_t port) { socket_t sock = socket(AF_INET, SOCK_STREAM, 0); if (sock == INVALID_SOCKET) { - perror("socket"); + net_perror("socket"); return INVALID_SOCKET; } @@ -33,7 +49,7 @@ net_connect(uint32_t addr, uint16_t port) { sin.sin_port = htons(port); if (connect(sock, (SOCKADDR *) &sin, sizeof(sin)) == SOCKET_ERROR) { - perror("connect"); + net_perror("connect"); net_close(sock); return INVALID_SOCKET; } @@ -45,14 +61,14 @@ socket_t net_listen(uint32_t addr, uint16_t port, int backlog) { socket_t sock = socket(AF_INET, SOCK_STREAM, 0); if (sock == INVALID_SOCKET) { - perror("socket"); + net_perror("socket"); return INVALID_SOCKET; } int reuse = 1; if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (const void *) &reuse, sizeof(reuse)) == -1) { - perror("setsockopt(SO_REUSEADDR)"); + net_perror("setsockopt(SO_REUSEADDR)"); } SOCKADDR_IN sin; @@ -61,13 +77,13 @@ net_listen(uint32_t addr, uint16_t port, int backlog) { sin.sin_port = htons(port); if (bind(sock, (SOCKADDR *) &sin, sizeof(sin)) == SOCKET_ERROR) { - perror("bind"); + net_perror("bind"); net_close(sock); return INVALID_SOCKET; } if (listen(sock, backlog) == SOCKET_ERROR) { - perror("listen"); + net_perror("listen"); net_close(sock); return INVALID_SOCKET; } From 228e2c15f44849032e9006dce1e0c47ae6e108ee Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 10 Sep 2021 21:40:08 +0200 Subject: [PATCH 0674/2244] Bump version to 1.19 --- 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 2d76f1e9..fc61bcea 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('scrcpy', 'c', - version: '1.18', + version: '1.19', meson_version: '>= 0.48', default_options: [ 'c_std=c11', diff --git a/server/build.gradle b/server/build.gradle index f088ba9d..7cd7dbd7 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -6,8 +6,8 @@ android { applicationId "com.genymobile.scrcpy" minSdkVersion 21 targetSdkVersion 30 - versionCode 11800 - versionName "1.18" + versionCode 11900 + versionName "1.19" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index 302d3aaa..05b822ba 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.18 +SCRCPY_VERSION_NAME=1.19 PLATFORM=${ANDROID_PLATFORM:-30} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-30.0.0} From 069fe93f74b77edd75e2ddda0ba30d768d938248 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 10 Sep 2021 21:50:32 +0200 Subject: [PATCH 0675/2244] Update links to v1.19 --- BUILD.md | 6 +++--- README.md | 8 ++++---- install_release.sh | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/BUILD.md b/BUILD.md index 87078b71..69475f2c 100644 --- a/BUILD.md +++ b/BUILD.md @@ -268,10 +268,10 @@ install` must be run as root)._ #### Option 2: Use prebuilt server - - [`scrcpy-server-v1.18`][direct-scrcpy-server] - _(SHA-256: 641c5c6beda9399dfae72d116f5ff43b5ed1059d871c9ebc3f47610fd33c51a3)_ + - [`scrcpy-server-v1.19`][direct-scrcpy-server] + _(SHA-256: 876f9322182e6aac6a58db1334f4225855ef3a17eaebc80aab6601d9d1ecb867)_ -[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.18/scrcpy-server-v1.18 +[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.19/scrcpy-server-v1.19 Download the prebuilt server somewhere, and specify its path during the Meson configuration: diff --git a/README.md b/README.md index 1cd70a83..b9da20b5 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# scrcpy (v1.18) +# scrcpy (v1.19) [Read in another language](#translations) @@ -88,10 +88,10 @@ process][BUILD_simple]). For Windows, for simplicity, a prebuilt archive with all the dependencies (including `adb`) is available: - - [`scrcpy-win64-v1.18.zip`][direct-win64] - _(SHA-256: 37212f5087fe6f3e258f1d44fa5c02207496b30e1d7ec442cbcf8358910a5c63)_ + - [`scrcpy-win64-v1.19.zip`][direct-win64] + _(SHA-256: 383d6483f25ac0092d4bb9fef6c967351ecd50fc248e0c82932db97d6d32f11b)_ -[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.18/scrcpy-win64-v1.18.zip +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.19/scrcpy-win64-v1.19.zip It is also available in [Chocolatey]: diff --git a/install_release.sh b/install_release.sh index 9158bdd4..dcb254f9 100755 --- a/install_release.sh +++ b/install_release.sh @@ -2,8 +2,8 @@ set -e BUILDDIR=build-auto -PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v1.18/scrcpy-server-v1.18 -PREBUILT_SERVER_SHA256=641c5c6beda9399dfae72d116f5ff43b5ed1059d871c9ebc3f47610fd33c51a3 +PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v1.19/scrcpy-server-v1.19 +PREBUILT_SERVER_SHA256=876f9322182e6aac6a58db1334f4225855ef3a17eaebc80aab6601d9d1ecb867 echo "[scrcpy] Downloading prebuilt server..." wget "$PREBUILT_SERVER_URL" -O scrcpy-server From fa100b814b0d1866dbf30efbcfd24334dc8ddce2 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 11 Sep 2021 10:46:25 +0200 Subject: [PATCH 0676/2244] Add support for expandNotificationsPanel() variant Some custom vendor ROM added an int as a parameter. Fixes #2551 --- .../scrcpy/wrappers/StatusBarManager.java | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java index 5b1e5f5e..7a19e6e5 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java @@ -11,6 +11,7 @@ public class StatusBarManager { private final IInterface manager; private Method expandNotificationsPanelMethod; + private boolean expandNotificationPanelMethodCustomVersion; private Method expandSettingsPanelMethod; private boolean expandSettingsPanelMethodNewVersion = true; private Method collapsePanelsMethod; @@ -21,7 +22,13 @@ public class StatusBarManager { private Method getExpandNotificationsPanelMethod() throws NoSuchMethodException { if (expandNotificationsPanelMethod == null) { - expandNotificationsPanelMethod = manager.getClass().getMethod("expandNotificationsPanel"); + try { + expandNotificationsPanelMethod = manager.getClass().getMethod("expandNotificationsPanel"); + } catch (NoSuchMethodException e) { + // Custom version for custom vendor ROM: + expandNotificationsPanelMethod = manager.getClass().getMethod("expandNotificationsPanel", int.class); + expandNotificationPanelMethodCustomVersion = true; + } } return expandNotificationsPanelMethod; } @@ -50,7 +57,11 @@ public class StatusBarManager { public void expandNotificationsPanel() { try { Method method = getExpandNotificationsPanelMethod(); - method.invoke(manager); + if (expandNotificationPanelMethodCustomVersion) { + method.invoke(manager, 0); + } else { + method.invoke(manager); + } } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { Ln.e("Could not invoke method", e); } From a846c01883e15cbf04ec2c76c07c7e0f074306b2 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 11 Sep 2021 21:36:21 +0200 Subject: [PATCH 0677/2244] Fix link in README --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b9da20b5..1a964e41 100644 --- a/README.md +++ b/README.md @@ -324,7 +324,9 @@ For example, you could capture the video within [OBS]. #### Buffering It is possible to add buffering. This increases latency but reduces jitter (see -#2464). +[#2464]). + +[#2464]: https://github.com/Genymobile/scrcpy/issues/2464 The option is available for display buffering: From 31131039bbb0d1840c64a0be79edfc3d5531bfb1 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 20 Sep 2021 18:27:37 +0200 Subject: [PATCH 0678/2244] Add missing includes Refs #2616 --- app/src/decoder.c | 1 + app/src/recorder.c | 2 ++ 2 files changed, 3 insertions(+) diff --git a/app/src/decoder.c b/app/src/decoder.c index aa5018b3..7c67e836 100644 --- a/app/src/decoder.c +++ b/app/src/decoder.c @@ -1,5 +1,6 @@ #include "decoder.h" +#include #include #include "events.h" diff --git a/app/src/recorder.c b/app/src/recorder.c index c98b6b8c..3b5fe070 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -1,6 +1,8 @@ #include "recorder.h" #include +#include +#include #include #include "util/log.h" From 8df42cec82cb5856e6c10a4089e6b2953310da6e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 22 Sep 2021 15:33:17 +0200 Subject: [PATCH 0679/2244] Fix workarounds for Meizu Workarounds.fillAppInfo() is necessary for Meizu devices even before the first call to internalStreamScreen(), but it is harmful on other devices (#940). Therefore, simplify the workaround, by calling fillAppInfo() only if Build.BRAND equals "meizu" (case insensitive). Fixes #240 (again) Fixes #2656 --- .../java/com/genymobile/scrcpy/ScreenEncoder.java | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index 2f7109c5..f98c53d0 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -56,17 +56,13 @@ public class ScreenEncoder implements Device.RotationListener { public void streamScreen(Device device, FileDescriptor fd) throws IOException { Workarounds.prepareMainLooper(); - - try { - internalStreamScreen(device, fd); - } catch (NullPointerException e) { - // Retry with workarounds enabled: - // - // - Ln.d("Applying workarounds to avoid NullPointerException"); + if (Build.BRAND.equalsIgnoreCase("meizu")) { + // + // Workarounds.fillAppInfo(); - internalStreamScreen(device, fd); } + + internalStreamScreen(device, fd); } private void internalStreamScreen(Device device, FileDescriptor fd) throws IOException { From 63b2b5ca4e2cca5165e2aede0ad8a8de659f0942 Mon Sep 17 00:00:00 2001 From: Alberto Pasqualetto <39854348+albertopasqualetto@users.noreply.github.com> Date: Tue, 21 Sep 2021 01:35:55 +0200 Subject: [PATCH 0680/2244] Update italian translation to v1.19 PR #2650 Signed-off-by: Romain Vimont --- FAQ.it.md | 18 ++++++++ FAQ.md | 2 +- README.it.md | 121 ++++++++++++++++++++++++++++++++++++++++----------- README.md | 2 +- 4 files changed, 116 insertions(+), 27 deletions(-) diff --git a/FAQ.it.md b/FAQ.it.md index 5c5830ce..0da656c0 100644 --- a/FAQ.it.md +++ b/FAQ.it.md @@ -140,6 +140,24 @@ Potresti anche dover configurare il [comportamento di ridimensionamento][scaling [scaling behavior]: https://github.com/Genymobile/scrcpy/issues/40#issuecomment-424466723 +### Problema con Wayland + +Per impostazione predefinita, SDL utilizza x11 su Linux. Il [video driver] può essere cambiato attraversio la variabile d'ambiente `SDL_VIDEODRIVER`: + +[video driver]: https://wiki.libsdl.org/FAQUsingSDL#how_do_i_choose_a_specific_video_driver + +```bash +export SDL_VIDEODRIVER=wayland +scrcpy +``` + +Su alcune distribuzioni (almeno Fedora), il pacchetto `libdecor` deve essere installato manualmente. + +Vedi le issues [#2554] e [#2559]. + +[#2554]: https://github.com/Genymobile/scrcpy/issues/2554 +[#2559]: https://github.com/Genymobile/scrcpy/issues/2559 + ### Crash del compositore KWin diff --git a/FAQ.md b/FAQ.md index f74845a5..6a0aabae 100644 --- a/FAQ.md +++ b/FAQ.md @@ -258,6 +258,6 @@ to add some arguments. This FAQ is available in other languages: - - [Italiano (Italiano, `it`) - v1.17](FAQ.it.md) + - [Italiano (Italiano, `it`) - v1.19](FAQ.it.md) - [한국어 (Korean, `ko`) - v1.11](FAQ.ko.md) - [简体中文 (Simplified Chinese, `zh-Hans`) - v1.18](FAQ.zh-Hans.md) diff --git a/README.it.md b/README.it.md index 37416f1d..6b5d6884 100644 --- a/README.it.md +++ b/README.it.md @@ -1,6 +1,6 @@ _Apri il [README](README.md) originale e sempre aggiornato._ -# scrcpy (v1.17) +# scrcpy (v1.19) Questa applicazione fornisce la visualizzazione e il controllo dei dispositivi Android collegati via USB (o [via TCP/IP][article-tcpip]). Non richiede alcun accesso _root_. Funziona su _GNU/Linux_, _Windows_ e _macOS_. @@ -205,10 +205,11 @@ Se anche `--max-size` è specificata, il ridimensionamento è applicato dopo il Per bloccare l'orientamento della trasmissione: ```bash -scrcpy --lock-video-orientation 0 # orientamento naturale -scrcpy --lock-video-orientation 1 # 90° antiorario -scrcpy --lock-video-orientation 2 # 180° -scrcpy --lock-video-orientation 3 # 90° orario +scrcpy --lock-video-orientation # orientamento iniziale (corrente) +scrcpy --lock-video-orientation=0 # orientamento naturale +scrcpy --lock-video-orientation=1 # 90° antiorario +scrcpy --lock-video-orientation=2 # 180° +scrcpy --lock-video-orientation=3 # 90° orario ``` Questo influisce sull'orientamento della registrazione. @@ -231,7 +232,9 @@ Per elencare i codificatori disponibili puoi immettere un nome di codificatore n scrcpy --encoder _ ``` -### Registrazione +### Cattura + +#### Registrazione È possibile registrare lo schermo durante la trasmissione: @@ -253,6 +256,75 @@ I "fotogrammi saltati" sono registrati nonostante non siano mostrati in tempo re [packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation +#### v4l2loopback + +Su Linux è possibile inviare il flusso video ad un dispositivo v4l2 loopback, cosicchè un dispositivo Android possa essere aperto come una webcam da qualsiasi strumento compatibile con v4l2. + +Il modulo `v4l2loopback` deve essere installato: + +```bash +sudo apt install v4l2loopback-dkms +``` + +Per creare un dispositvo v4l2: + +```bash +sudo modprobe v4l2loopback +``` + +Questo creerà un nuovo dispositivo video in `/dev/videoN` dove `N` è un intero (più [opzioni](https://github.com/umlaeute/v4l2loopback#options) sono disponibili per crere più dispositivi o dispositivi con ID specifici). + +Per elencare i dispositvi attivati: + +```bash +# necessita del pacchetto v4l-utils +v4l2-ctl --list-devices + +# semplice ma potrebbe essere sufficiente +ls /dev/video* +``` + +Per avviare scrcpy utilizzando un v4l2 sink: + +```bash +scrcpy --v4l2-sink=/dev/videoN +scrcpy --v4l2-sink=/dev/videoN --no-display # disabilita la finestra di trasmissione +scrcpy --v4l2-sink=/dev/videoN -N # versione corta +``` + +(sostituisci `N` con l'ID del dispositivo, controlla con `ls /dev/video*`) + +Una volta abilitato, puoi aprire il tuo flusso video con uno strumento compatibile con v4l2: + +```bash +ffplay -i /dev/videoN +vlc v4l2:///dev/videoN # VLC potrebbe aggiungere del ritardo per il buffer +``` + +Per esempio potresti catturare il video in [OBS]. + +[OBS]: https://obsproject.com/ + + +#### Buffering + +È possibile aggiungere del buffer. Questo aumenta la latenza ma riduce il jitter (vedi [#2464]). + +[#2464]: https://github.com/Genymobile/scrcpy/issues/2464 + +L'opzione è disponibile per il buffer della visualizzazione: + +```bash +scrcpy --display-buffer=50 # aggiungi 50 ms di buffer per la visualizzazione +``` + +e per il V4L2 sink: + +```bash +scrcpy --v4l2-buffer=500 # aggiungi 50 ms di buffer per il v4l2 sink +``` + + ### Connessione #### Wireless @@ -479,15 +551,6 @@ scrcpy --turn-screen-off --stay-awake scrcpy -Sw ``` -#### Renderizzare i fotogrammi scaduti - -Per minimizzare la latenza _scrcpy_ renderizza sempre l'ultimo fotogramma decodificato disponibile in maniera predefinita e tralascia quelli precedenti. - -Per forzare la renderizzazione di tutti i fotogrammi (a costo di una possibile latenza superiore), utilizzare: - -```bash -scrcpy --render-expired-frames -``` #### Mostrare i tocchi @@ -607,14 +670,14 @@ Non c'è alcuna risposta visiva, un log è stampato nella console. #### Trasferimento di file verso il dispositivo -Per trasferire un file in `/sdcard/` del dispositivo trascina e rilascia un file (non APK) nella finestra di _scrcpy_. +Per trasferire un file in `/sdcard/Download` del dispositivo trascina e rilascia un file (non APK) nella finestra di _scrcpy_. Non c'è alcuna risposta visiva, un log è stampato nella console. La cartella di destinazione può essere cambiata all'avvio: ```bash -scrcpy --push-target=/sdcard/Download/ +scrcpy --push-target=/sdcard/Movies/ ``` @@ -653,10 +716,10 @@ _[Super] è il pulsante Windows o Cmd._ | Rotazione schermo a sinistra | MOD+ _(sinistra)_ | Rotazione schermo a destra | MOD+ _(destra)_ | Ridimensiona finestra a 1:1 (pixel-perfect) | MOD+g - | Ridimensiona la finestra per rimuovere i bordi neri | MOD+w \| _Doppio click¹_ + | Ridimensiona la finestra per rimuovere i bordi neri | MOD+w \| _Doppio click sinistro¹_ | Premi il tasto `HOME` | MOD+h \| _Click centrale_ | Premi il tasto `BACK` | MOD+b \| _Click destro²_ - | Premi il tasto `APP_SWITCH` | MOD+s + | Premi il tasto `APP_SWITCH` | MOD+s \| _4° click³_ | Premi il tasto `MENU` (sblocca lo schermo) | MOD+m | Premi il tasto `VOLUME_UP` | MOD+ _(su)_ | Premi il tasto `VOLUME_DOWN` | MOD+ _(giù)_ @@ -665,18 +728,26 @@ _[Super] è il pulsante Windows o Cmd._ | Spegni lo schermo del dispositivo (continua a trasmettere) | MOD+o | Accendi lo schermo del dispositivo | MOD+Shift+o | Ruota lo schermo del dispositivo | MOD+r - | Espandi il pannello delle notifiche | MOD+n - | Chiudi il pannello delle notifiche | MOD+Shift+n - | Copia negli appunti³ | MOD+c - | Taglia negli appunti³ | MOD+x - | Sincronizza gli appunti e incolla³ | MOD+v + | Espandi il pannello delle notifiche | MOD+n \| _5° click³_ + | Espandi il pannello delle impostazioni | MOD+n+n \| _Doppio 5° click³_ + | Chiudi pannelli | MOD+Shift+n + | Copia negli appunti⁴ | MOD+c + | Taglia negli appunti⁴ | MOD+x + | Sincronizza gli appunti e incolla⁴ | MOD+v | Inietta il testo degli appunti del computer | MOD+Shift+v | Abilita/Disabilita il contatore FPS (su stdout) | MOD+i | Pizzica per zoomare | Ctrl+_click e trascina_ _¹Doppio click sui bordi neri per rimuoverli._ _²Il tasto destro accende lo schermo se era spento, preme BACK in caso contrario._ -_³Solo in Android >= 7._ +_³4° e 5° pulsante del mouse, se il tuo mouse ne dispone._ +_⁴Solo in Android >= 7._ + +Le scorciatoie con pulsanti ripetuti sono eseguite rilasciando e premendo il pulsante una seconda volta. Per esempio, per eseguire "Espandi il pannello delle impostazioni": + +1. Premi e tieni premuto MOD. +2. Poi premi due volte n. +3. Infine rilascia MOD. Tutte le scorciatoie Ctrl+_tasto_ sono inoltrate al dispositivo, così sono gestite dall'applicazione attiva. diff --git a/README.md b/README.md index 1a964e41..ce8effd3 100644 --- a/README.md +++ b/README.md @@ -888,7 +888,7 @@ Read the [developers page]. This README is available in other languages: - [Indonesian (Indonesia, `id`) - v1.16](README.id.md) -- [Italiano (Italiano, `it`) - v1.17](README.it.md) +- [Italiano (Italiano, `it`) - v1.19](README.it.md) - [日本語 (Japanese, `jp`) - v1.17](README.jp.md) - [한국어 (Korean, `ko`) - v1.11](README.ko.md) - [português brasileiro (Brazilian Portuguese, `pt-BR`) - v1.17](README.pt-br.md) From 96e3963afceb526cef8c099060ef32856180b0b2 Mon Sep 17 00:00:00 2001 From: Pedro Miguel A Carraro Date: Wed, 6 Oct 2021 13:19:40 -0300 Subject: [PATCH 0681/2244] Update Readme.pt-br.md to v1.19 PR #2686 Signed-off-by: Romain Vimont --- README.md | 2 +- README.pt-br.md | 174 ++++++++++++++++++++++++++++++++++++------------ 2 files changed, 132 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index ce8effd3..81410b80 100644 --- a/README.md +++ b/README.md @@ -891,7 +891,7 @@ This README is available in other languages: - [Italiano (Italiano, `it`) - v1.19](README.it.md) - [日本語 (Japanese, `jp`) - v1.17](README.jp.md) - [한국어 (Korean, `ko`) - v1.11](README.ko.md) -- [português brasileiro (Brazilian Portuguese, `pt-BR`) - v1.17](README.pt-br.md) +- [Português Brasileiro (Brazilian Portuguese, `pt-BR`) - v1.19](README.pt-br.md) - [Español (Spanish, `sp`) - v1.17](README.sp.md) - [简体中文 (Simplified Chinese, `zh-Hans`) - v1.17](README.zh-Hans.md) - [繁體中文 (Traditional Chinese, `zh-Hant`) - v1.15](README.zh-Hant.md) diff --git a/README.pt-br.md b/README.pt-br.md index 3549f0fb..cdfeafeb 100644 --- a/README.pt-br.md +++ b/README.pt-br.md @@ -1,6 +1,6 @@ _Apenas o [README](README.md) original é garantido estar atualizado._ -# scrcpy (v1.17) +# scrcpy (v1.19) Esta aplicação fornece exibição e controle de dispositivos Android conectados via USB (ou [via TCP/IP][article-tcpip]). Não requer nenhum acesso _root_. @@ -38,6 +38,18 @@ controlá-lo usando teclado e mouse. Packaging status +### Sumário + + - Linux: `apt install scrcpy` + - Windows: [baixar][direct-win64] + - macOS: `brew install scrcpy` + + Compilar pelos arquivos fontes: [BUILD] ([processo simplificado][BUILD_simple]) + +[BUILD]: BUILD.md +[BUILD_simple]: BUILD.md#simple + + ### Linux No Debian (_testing_ e _sid_ por enquanto) e Ubuntu (20.04): @@ -67,9 +79,7 @@ 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 o app manualmente][BUILD] (não se preocupe, não é tão -difícil). - +Você também pode [compilar o app manualmente][BUILD] ([processo simplificado][BUILD_simple]). ### Windows @@ -113,13 +123,18 @@ brew install scrcpy Você precisa do `adb`, acessível pelo seu `PATH`. Se você ainda não o tem: ```bash -# Homebrew >= 2.6.0 -brew install --cask android-platform-tools - -# Homebrew < 2.6.0 -brew cask install android-platform-tools +brew install android-platform-tools ``` +Está também disponivel em [MacPorts], que prepara o adb para você: + +```bash +sudo port install scrcpy +``` + +[MacPorts]: https://www.macports.org/ + + Você também pode [compilar o app manualmente][BUILD]. @@ -195,10 +210,11 @@ Se `--max-size` também for especificado, o redimensionamento é aplicado após Para travar a orientação do espelhamento: ```bash -scrcpy --lock-video-orientation 0 # orientação natural -scrcpy --lock-video-orientation 1 # 90° sentido anti-horário -scrcpy --lock-video-orientation 2 # 180° -scrcpy --lock-video-orientation 3 # 90° sentido horário +scrcpy --lock-video-orientation # orientação inicial (Atual) +scrcpy --lock-video-orientation=0 # orientação natural +scrcpy --lock-video-orientation=1 # 90° sentido anti-horário +scrcpy --lock-video-orientation=2 # 180° +scrcpy --lock-video-orientation=3 # 90° sentido horário ``` Isso afeta a orientação de gravação. @@ -222,7 +238,9 @@ erro dará os encoders disponíveis: scrcpy --encoder _ ``` -### Gravando +### Captura + +#### Gravando É possível gravar a tela enquanto ocorre o espelhamento: @@ -246,6 +264,79 @@ pacotes][packet delay variation] não impacta o arquivo gravado. [packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation +#### v4l2loopback + +Em Linux, é possível enviar a transmissão do video para um disposiivo v4l2 loopback, assim +o dispositivo Android pode ser aberto como uma webcam por qualquer ferramneta capaz de v4l2 + +The module `v4l2loopback` must be installed: + +```bash +sudo apt install v4l2loopback-dkms +``` + +Para criar um dispositivo v4l2: + +```bash +sudo modprobe v4l2loopback +``` + +Isso criara um novo dispositivo de vídeo em `/dev/videoN`, onde `N` é uma integer +(mais [opções](https://github.com/umlaeute/v4l2loopback#options) estão disponiveis +para criar varios dispositivos ou dispositivos com IDs específicas). + +Para listar os dispositivos disponíveis: + +```bash +# requer o pacote v4l-utils +v4l2-ctl --list-devices + +# simples, mas pode ser suficiente +ls /dev/video* +``` + +Para iniciar o scrcpy usando o coletor v4l2 (sink): + +```bash +scrcpy --v4l2-sink=/dev/videoN +scrcpy --v4l2-sink=/dev/videoN --no-display # desativa a janela espelhada +scrcpy --v4l2-sink=/dev/videoN -N # versão curta +``` + +(troque `N` pelo ID do dipositivo, verifique com `ls /dev/video*`) + +Uma vez ativado, você pode abrir suas trasmissões de videos com uma ferramenta capaz de v4l2: + +```bash +ffplay -i /dev/videoN +vlc v4l2:///dev/videoN # VLC pode adicionar um pouco de atraso de buffering +``` + +Por exemplo, você pode capturar o video dentro do [OBS]. + +[OBS]: https://obsproject.com/ + + +#### Buffering + +É possivel adicionar buffering. Isso aumenta a latência, mas reduz a tenção (jitter) (veja +[#2464]). + +[#2464]: https://github.com/Genymobile/scrcpy/issues/2464 + +A opção éta disponivel para buffering de exibição: + +```bash +scrcpy --display-buffer=50 # adiciona 50 ms de buffering para a exibição +``` + +e coletor V4L2: + +```bash +scrcpy --v4l2-buffer=500 # adiciona 500 ms de buffering para coletor V4L2 +``` + +, ### Conexão #### Sem fio @@ -488,18 +579,6 @@ scrcpy -Sw ``` -#### Renderizar frames expirados - -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 um possível aumento de -latência), use: - -```bash -scrcpy --render-expired-frames -``` - #### Mostrar toques Para apresentações, pode ser útil mostrar toques físicos (no dispositivo @@ -647,7 +726,7 @@ Não existe feedback visual, um log é imprimido no console. #### Enviar arquivo para dispositivo -Para enviar um arquivo para `/sdcard/` no dispositivo, arraste e solte um arquivo (não-APK) para a +Para enviar um arquivo para `/sdcard/Download/` 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. @@ -694,12 +773,12 @@ _[Super] é tipicamente a tecla Windows ou Cmd. | Mudar modo de tela cheia | MOD+f | Rotacionar display para esquerda | MOD+ _(esquerda)_ | Rotacionar display para direita | MOD+ _(direita)_ - | Redimensionar janela para 1:1 (pixel-perfect) | MOD+g - | Redimensionar janela para remover bordas pretas | MOD+w \| _Clique-duplo¹_ + | Redimensionar janela para 1:1 (pixel-perfeito) | MOD+g + | Redimensionar janela para remover bordas pretas | MOD+w \| _Clique-duplo-esquerdo¹_ | Clicar em `HOME` | MOD+h \| _Clique-do-meio_ | Clicar em `BACK` | MOD+b \| _Clique-direito²_ - | Clicar em `APP_SWITCH` | MOD+s - | Clicar em `MENU` (desbloquear tela | MOD+m + | Clicar em `APP_SWITCH` | MOD+s \| _Clique-do-4.°³_ + | Clicar em `MENU` (desbloquear tela) | MOD+m | Clicar em `VOLUME_UP` | MOD+ _(cima)_ | Clicar em `VOLUME_DOWN` | MOD+ _(baixo)_ | Clicar em `POWER` | MOD+p @@ -707,18 +786,27 @@ _[Super] é tipicamente a tecla Windows ou Cmd. | Desligar tela do dispositivo (continuar espelhando) | MOD+o | Ligar tela do dispositivo | MOD+Shift+o | Rotacionar tela do dispositivo | MOD+r - | Expandir painel de notificação | MOD+n - | Colapsar painel de notificação | MOD+Shift+n - | Copiar para área de transferência³ | MOD+c - | Recortar para área de transferência³ | MOD+x - | Sincronizar áreas de transferência e colar³ | MOD+v + | Expandir painel de notificação | MOD+n \| _Clique-do-5.°³_ + | Expandir painel de configurção | MOD+n+n \| _Clique-duplo-do-5.°³_ + | Colapsar paineis | MOD+Shift+n + | Copiar para área de transferência⁴ | MOD+c + | Recortar para área de transferência⁴ | MOD+x + | Sincronizar áreas de transferência e colar⁴ | MOD+v | Injetar texto da área de transferência do computador | MOD+Shift+v | Ativar/desativar contador de FPS (em stdout) | MOD+i - | Pinçar para dar zoom | Ctrl+_clicar-e-mover_ + | Pinçar para dar zoom | Ctrl+_Clicar-e-mover_ -_¹Clique-duplo em bordas pretas para removê-las._ -_²Clique-direito liga a tela se ela estiver desligada, pressiona BACK caso contrário._ -_³Apenas em Android >= 7._ +_¹Clique-duplo-esquerdo na borda preta para remove-la._ +_²Clique-direito liga a tela caso esteja desligada, pressione BACK caso contrário._ +_³4.° and 5.° botões do mouse, caso o mouse possua._ +_⁴Apenas em Android >= 7._ + +Atalhos com teclas reptidas são executados soltando e precionando a tecla +uma segunda vez. Por exemplo, para executar "Expandir painel de Configurção": + + 1. Mantenha pressionado MOD. + 2. Depois click duas vezes n. + 3. Finalmente, solte MOD. Todos os atalhos Ctrl+_tecla_ são encaminhados para o dispositivo, para que eles sejam tratados pela aplicação ativa. @@ -729,7 +817,9 @@ tratados pela aplicação ativa. Para usar um binário _adb_ específico, configure seu caminho na variável de ambiente `ADB`: - ADB=/caminho/para/adb scrcpy +```bash +ADB=/caminho/para/adb scrcpy +``` Para sobrepor o caminho do arquivo `scrcpy-server`, configure seu caminho em `SCRCPY_SERVER_PATH`. @@ -751,8 +841,6 @@ Um colega me desafiou a encontrar um nome tão impronunciável quanto [gnirehtet Veja [BUILD]. -[BUILD]: BUILD.md - ## Problemas comuns From 07d75eb336264cab4eb4cabfde482ce0104eba2b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 17 Oct 2021 16:21:01 +0200 Subject: [PATCH 0682/2244] Simplify net_send_all() There is no need to declare the variable before the loop. --- app/src/util/net.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/util/net.c b/app/src/util/net.c index 17299424..2b5a0e5e 100644 --- a/app/src/util/net.c +++ b/app/src/util/net.c @@ -116,9 +116,8 @@ net_send(socket_t socket, const void *buf, size_t len) { ssize_t net_send_all(socket_t socket, const void *buf, size_t len) { size_t copied = 0; - ssize_t w = 0; while (len > 0) { - w = send(socket, buf, len, 0); + ssize_t w = send(socket, buf, len, 0); if (w == -1) { return copied ? (ssize_t) copied : -1; } From 46d3e35c3079ede0ec4f92db9e35939d45aa3898 Mon Sep 17 00:00:00 2001 From: zhongkaizhu Date: Wed, 20 Oct 2021 19:35:58 +0800 Subject: [PATCH 0683/2244] Fix "Could not find v4l2 muxer" The AVOutputFormat name is a comma-separated list. In theory, possible names for V4L2 are: - "video4linux2,v4l2" - "v4l2,video4linux2" - "v4l2" - "video4linux2" To find the muxer in all cases, we must request exactly one muxer name at a time. PR #2718 Co-authored-by: Romain Vimont Signed-off-by: Romain Vimont --- app/src/v4l2_sink.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/src/v4l2_sink.c b/app/src/v4l2_sink.c index cae3eee9..95e20541 100644 --- a/app/src/v4l2_sink.c +++ b/app/src/v4l2_sink.c @@ -183,8 +183,11 @@ sc_v4l2_sink_open(struct sc_v4l2_sink *vs) { goto error_mutex_destroy; } - // FIXME - const AVOutputFormat *format = find_muxer("video4linux2,v4l2"); + const AVOutputFormat *format = find_muxer("v4l2"); + if (!format) { + // Alternative name + format = find_muxer("video4linux2"); + } if (!format) { LOGE("Could not find v4l2 muxer"); goto error_cond_destroy; From a7e41b0f85f3babb8916f9728d1d2c3e60aa40c8 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 21 Oct 2021 18:36:16 +0200 Subject: [PATCH 0684/2244] Fix code style --- app/src/controller.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/controller.c b/app/src/controller.c index 17844c98..b85ac02d 100644 --- a/app/src/controller.c +++ b/app/src/controller.c @@ -47,7 +47,7 @@ controller_destroy(struct controller *controller) { bool controller_push_msg(struct controller *controller, - const struct control_msg *msg) { + const struct control_msg *msg) { if (sc_get_log_level() <= SC_LOG_LEVEL_VERBOSE) { control_msg_log(msg); } @@ -63,14 +63,14 @@ controller_push_msg(struct controller *controller, } static bool -process_msg(struct controller *controller, - const struct control_msg *msg) { +process_msg(struct controller *controller, const struct control_msg *msg) { static unsigned char serialized_msg[CONTROL_MSG_MAX_SIZE]; size_t length = control_msg_serialize(msg, serialized_msg); if (!length) { return false; } - ssize_t w = net_send_all(controller->control_socket, serialized_msg, length); + ssize_t w = + net_send_all(controller->control_socket, serialized_msg, length); return (size_t) w == length; } From 7229e3cce033591127148523197c9fd56a1ab78d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 22 Oct 2021 18:51:20 +0200 Subject: [PATCH 0685/2244] Extract util function to build a local file path Finding a local file in the scrcpy directory may be useful for files other than scrcpy-server in the future. --- app/src/server.c | 40 ++-------------------------------------- app/src/util/process.c | 42 ++++++++++++++++++++++++++++++++++++++++++ app/src/util/process.h | 5 +++++ 3 files changed, 49 insertions(+), 38 deletions(-) diff --git a/app/src/server.c b/app/src/server.c index e3c8c344..7c0d3d3f 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -3,7 +3,6 @@ #include #include #include -#include #include #include #include @@ -51,48 +50,13 @@ get_server_path(void) { // the absolute path is hardcoded return server_path; #else - - // use scrcpy-server in the same directory as the executable - char *executable_path = get_executable_path(); - if (!executable_path) { - LOGE("Could not get executable path, " - "using " SERVER_FILENAME " from current directory"); - // not found, use current directory - return strdup(SERVER_FILENAME); - } - - // dirname() does not work correctly everywhere, so get the parent - // directory manually. - // See - char *p = strrchr(executable_path, PATH_SEPARATOR); - if (!p) { - LOGE("Unexpected executable path: \"%s\" (it should contain a '%c')", - executable_path, PATH_SEPARATOR); - free(executable_path); - return strdup(SERVER_FILENAME); - } - - *p = '\0'; // modify executable_path in place - char *dir = executable_path; - size_t dirlen = strlen(dir); - - // sizeof(SERVER_FILENAME) gives statically the size including the null byte - size_t len = dirlen + 1 + sizeof(SERVER_FILENAME); - char *server_path = malloc(len); + char *server_path = get_local_file_path(SERVER_FILENAME); if (!server_path) { - LOGE("Could not alloc server path string, " + LOGE("Could not get local file path, " "using " SERVER_FILENAME " from current directory"); - free(executable_path); return strdup(SERVER_FILENAME); } - memcpy(server_path, dir, dirlen); - server_path[dirlen] = PATH_SEPARATOR; - memcpy(&server_path[dirlen + 1], SERVER_FILENAME, sizeof(SERVER_FILENAME)); - // the final null byte has been copied with SERVER_FILENAME - - free(executable_path); - LOGD("Using server (portable): %s", server_path); return server_path; #endif diff --git a/app/src/util/process.c b/app/src/util/process.c index 5edeeee6..a9af4d67 100644 --- a/app/src/util/process.c +++ b/app/src/util/process.c @@ -1,5 +1,6 @@ #include "process.h" +#include #include "log.h" bool @@ -19,3 +20,44 @@ process_check_success(process_t proc, const char *name, bool close) { } return true; } + +char * +get_local_file_path(const char *name) { + char *executable_path = get_executable_path(); + if (!executable_path) { + return NULL; + } + + // dirname() does not work correctly everywhere, so get the parent + // directory manually. + // See + char *p = strrchr(executable_path, PATH_SEPARATOR); + if (!p) { + LOGE("Unexpected executable path: \"%s\" (it should contain a '%c')", + executable_path, PATH_SEPARATOR); + free(executable_path); + return NULL; + } + + *p = '\0'; // modify executable_path in place + char *dir = executable_path; + size_t dirlen = strlen(dir); + size_t namelen = strlen(name); + + size_t len = dirlen + namelen + 2; // +2: '/' and '\0' + char *file_path = malloc(len); + if (!file_path) { + LOGE("Could not alloc path"); + free(executable_path); + return NULL; + } + + memcpy(file_path, dir, dirlen); + file_path[dirlen] = PATH_SEPARATOR; + // namelen + 1 to copy the final '\0' + memcpy(&file_path[dirlen + 1], name, namelen + 1); + + free(executable_path); + + return file_path; +} diff --git a/app/src/util/process.h b/app/src/util/process.h index 7838a848..6aca6bf5 100644 --- a/app/src/util/process.h +++ b/app/src/util/process.h @@ -74,6 +74,11 @@ search_executable(const char *file); char * get_executable_path(void); +// Return the absolute path of a file in the same directory as he executable. +// May be NULL on error. To be freed by free(). +char * +get_local_file_path(const char *name); + // returns true if the file exists and is not a directory bool is_regular_file(const char *path); From 156d958e77763428eee48e3433af4d7f5344f00c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 25 Oct 2021 18:08:31 +0200 Subject: [PATCH 0686/2244] Move common instruction out of ifdef Both ifdef-branches return server_path. --- app/src/server.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/src/server.c b/app/src/server.c index 7c0d3d3f..4c1a43f5 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -47,8 +47,6 @@ get_server_path(void) { LOGE("Could not allocate memory"); return NULL; } - // the absolute path is hardcoded - return server_path; #else char *server_path = get_local_file_path(SERVER_FILENAME); if (!server_path) { @@ -58,8 +56,9 @@ get_server_path(void) { } LOGD("Using server (portable): %s", server_path); - return server_path; #endif + + return server_path; } static bool From 0e4564da03df723dfe799cbcf15d26b3bdaaf6a5 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 22 Oct 2021 18:51:20 +0200 Subject: [PATCH 0687/2244] Add icon loader Add helper to load icons from image files via FFmpeg. --- app/meson.build | 1 + app/src/icon.c | 247 ++++++++++++++++++++++++++++++++++++++++++++++++ app/src/icon.h | 16 ++++ 3 files changed, 264 insertions(+) create mode 100644 app/src/icon.c create mode 100644 app/src/icon.h diff --git a/app/meson.build b/app/meson.build index f5345803..698120a5 100644 --- a/app/meson.build +++ b/app/meson.build @@ -9,6 +9,7 @@ src = [ 'src/decoder.c', 'src/device_msg.c', 'src/event_converter.c', + 'src/icon.c', 'src/file_handler.c', 'src/fps_counter.c', 'src/frame_buffer.c', diff --git a/app/src/icon.c b/app/src/icon.c new file mode 100644 index 00000000..119c5eeb --- /dev/null +++ b/app/src/icon.c @@ -0,0 +1,247 @@ +#include "icon.h" + +#include +#include +#include +#include +#include + +#include "config.h" +#include "compat.h" +#include "util/log.h" +#include "util/process.h" +#include "util/str_util.h" + +#define SCRCPY_PORTABLE_ICON_FILENAME "icon.png" +#define SCRCPY_DEFAULT_ICON_PATH \ + PREFIX "/share/icons/hicolor/256x256/apps/scrcpy.png" + +static char * +get_icon_path(void) { +#ifdef __WINDOWS__ + const wchar_t *icon_path_env = _wgetenv(L"SCRCPY_ICON_PATH"); +#else + const char *icon_path_env = getenv("SCRCPY_ICON_PATH"); +#endif + if (icon_path_env) { + // if the envvar is set, use it +#ifdef __WINDOWS__ + char *icon_path = utf8_from_wide_char(icon_path_env); +#else + char *icon_path = strdup(icon_path_env); +#endif + if (!icon_path) { + LOGE("Could not allocate memory"); + return NULL; + } + LOGD("Using SCRCPY_ICON_PATH: %s", icon_path); + return icon_path; + } + +#ifndef PORTABLE + LOGD("Using icon: " SCRCPY_DEFAULT_ICON_PATH); + char *icon_path = strdup(SCRCPY_DEFAULT_ICON_PATH); + if (!icon_path) { + LOGE("Could not allocate memory"); + return NULL; + } +#else + char *icon_path = get_local_file_path(SCRCPY_PORTABLE_ICON_FILENAME); + if (!icon_path) { + LOGE("Could not get icon path"); + return NULL; + } + LOGD("Using icon (portable): %s", icon_path); +#endif + + return icon_path; +} + +static AVFrame * +decode_image(const char *path) { + AVFrame *result = NULL; + + AVFormatContext *ctx = avformat_alloc_context(); + if (!ctx) { + LOGE("Could not allocate image decoder context"); + return NULL; + } + + if (avformat_open_input(&ctx, path, NULL, NULL) < 0) { + LOGE("Could not open image codec: %s", path); + goto free_ctx; + } + + if (avformat_find_stream_info(ctx, NULL) < 0) { + LOGE("Could not find image stream info"); + goto close_input; + } + + int stream = av_find_best_stream(ctx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0); + if (stream < 0 ) { + LOGE("Could not find best image stream"); + goto close_input; + } + + AVCodecParameters *params = ctx->streams[stream]->codecpar; + + AVCodec *codec = avcodec_find_decoder(params->codec_id); + if (!codec) { + LOGE("Could not find image decoder"); + goto close_input; + } + + AVCodecContext *codec_ctx = avcodec_alloc_context3(codec); + if (!codec_ctx) { + LOGE("Could not allocate codec context"); + goto close_input; + } + + if (avcodec_parameters_to_context(codec_ctx, params) < 0) { + LOGE("Could not fill codec context"); + goto free_codec_ctx; + } + + if (avcodec_open2(codec_ctx, codec, NULL) < 0) { + LOGE("Could not open image codec"); + goto free_codec_ctx; + } + + AVFrame *frame = av_frame_alloc(); + if (!frame) { + LOGE("Could not allocate frame"); + goto close_codec; + } + + AVPacket *packet = av_packet_alloc(); + if (!packet) { + LOGE("Could not allocate packet"); + av_frame_free(&frame); + goto close_codec; + } + + if (av_read_frame(ctx, packet) < 0) { + LOGE("Could not read frame"); + av_packet_free(&packet); + av_frame_free(&frame); + goto close_codec; + } + + int ret; + if ((ret = avcodec_send_packet(codec_ctx, packet)) < 0) { + LOGE("Could not send icon packet: %d", ret); + av_packet_free(&packet); + av_frame_free(&frame); + goto close_codec; + } + + if ((ret = avcodec_receive_frame(codec_ctx, frame)) != 0) { + LOGE("Could not receive icon frame: %d", ret); + av_packet_free(&packet); + av_frame_free(&frame); + goto close_codec; + } + + av_packet_free(&packet); + + result = frame; + +close_codec: + avcodec_close(codec_ctx); +free_codec_ctx: + avcodec_free_context(&codec_ctx); +close_input: + avformat_close_input(&ctx); +free_ctx: + avformat_free_context(ctx); + + return result; +} + +static SDL_PixelFormatEnum +to_sdl_pixel_format(enum AVPixelFormat fmt) { + switch (fmt) { + case AV_PIX_FMT_RGB24: return SDL_PIXELFORMAT_RGB24; + case AV_PIX_FMT_BGR24: return SDL_PIXELFORMAT_BGR24; + case AV_PIX_FMT_ARGB: return SDL_PIXELFORMAT_ARGB32; + case AV_PIX_FMT_RGBA: return SDL_PIXELFORMAT_RGBA32; + case AV_PIX_FMT_ABGR: return SDL_PIXELFORMAT_ABGR32; + case AV_PIX_FMT_BGRA: return SDL_PIXELFORMAT_BGRA32; + case AV_PIX_FMT_RGB565BE: return SDL_PIXELFORMAT_RGB565; + case AV_PIX_FMT_RGB555BE: return SDL_PIXELFORMAT_RGB555; + case AV_PIX_FMT_BGR565BE: return SDL_PIXELFORMAT_BGR565; + case AV_PIX_FMT_BGR555BE: return SDL_PIXELFORMAT_BGR555; + case AV_PIX_FMT_RGB444BE: return SDL_PIXELFORMAT_RGB444; + case AV_PIX_FMT_BGR444BE: return SDL_PIXELFORMAT_BGR444; + default: return SDL_PIXELFORMAT_UNKNOWN; + } +} + +static SDL_Surface * +load_from_path(const char *path) { + AVFrame *frame = decode_image(path); + if (!frame) { + return NULL; + } + + const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(frame->format); + if (!desc) { + LOGE("Could not get icon format descriptor"); + goto error; + } + + bool is_packed_rgb = desc->flags & AV_PIX_FMT_FLAG_RGB + && !(desc->flags & AV_PIX_FMT_FLAG_PLANAR); + if (!is_packed_rgb) { + LOGE("Could not load non-RGB icon"); + goto error; + } + + SDL_PixelFormatEnum format = to_sdl_pixel_format(frame->format); + if (format == SDL_PIXELFORMAT_UNKNOWN) { + LOGE("Unsupported icon pixel format: %s (%d)", desc->name, + frame->format); + goto error; + } + + int bits_per_pixel = av_get_bits_per_pixel(desc); + SDL_Surface *surface = + SDL_CreateRGBSurfaceWithFormatFrom(frame->data[0], + frame->width, frame->height, + bits_per_pixel, + frame->linesize[0], + format); + + if (!surface) { + LOGE("Could not create icon surface"); + goto error; + } + + surface->userdata = frame; // frame owns the data + + return surface; + +error: + av_frame_free(&frame); + return NULL; +} + +SDL_Surface * +scrcpy_icon_load() { + char *icon_path = get_icon_path(); + if (!icon_path) { + return NULL; + } + + SDL_Surface *icon = load_from_path(icon_path); + free(icon_path); + return icon; +} + +void +scrcpy_icon_destroy(SDL_Surface *icon) { + AVFrame *frame = icon->userdata; + assert(frame); + av_frame_free(&frame); + SDL_FreeSurface(icon); +} diff --git a/app/src/icon.h b/app/src/icon.h new file mode 100644 index 00000000..8df53671 --- /dev/null +++ b/app/src/icon.h @@ -0,0 +1,16 @@ +#ifndef ICON_H +#define ICON_H + +#include "common.h" + +#include +#include +#include + +SDL_Surface * +scrcpy_icon_load(void); + +void +scrcpy_icon_destroy(SDL_Surface *icon); + +#endif From 12ed2f24020b95e0699f48864962da819414fd44 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 22 Oct 2021 18:51:20 +0200 Subject: [PATCH 0688/2244] Add support for palette icon formats To support more icon formats. --- app/src/icon.c | 43 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/app/src/icon.c b/app/src/icon.c index 119c5eeb..607c7162 100644 --- a/app/src/icon.c +++ b/app/src/icon.c @@ -173,6 +173,7 @@ to_sdl_pixel_format(enum AVPixelFormat fmt) { case AV_PIX_FMT_BGR555BE: return SDL_PIXELFORMAT_BGR555; case AV_PIX_FMT_RGB444BE: return SDL_PIXELFORMAT_RGB444; case AV_PIX_FMT_BGR444BE: return SDL_PIXELFORMAT_BGR444; + case AV_PIX_FMT_PAL8: return SDL_PIXELFORMAT_INDEX8; default: return SDL_PIXELFORMAT_UNKNOWN; } } @@ -190,10 +191,9 @@ load_from_path(const char *path) { goto error; } - bool is_packed_rgb = desc->flags & AV_PIX_FMT_FLAG_RGB - && !(desc->flags & AV_PIX_FMT_FLAG_PLANAR); - if (!is_packed_rgb) { - LOGE("Could not load non-RGB icon"); + bool is_packed = !(desc->flags & AV_PIX_FMT_FLAG_PLANAR); + if (!is_packed) { + LOGE("Could not load non-packed icon"); goto error; } @@ -217,6 +217,41 @@ load_from_path(const char *path) { goto error; } + if (frame->format == AV_PIX_FMT_PAL8) { + // Initialize the SDL palette + uint8_t *data = frame->data[1]; + SDL_Color colors[256]; + for (int i = 0; i < 256; ++i) { + SDL_Color *color = &colors[i]; + + // The palette is transported in AVFrame.data[1], is 1024 bytes + // long (256 4-byte entries) and is formatted the same as in + // AV_PIX_FMT_RGB32 described above (i.e., it is also + // endian-specific). + // +#if SDL_BYTEORDER == SDL_BIG_ENDIAN + color->a = data[i * 4]; + color->r = data[i * 4 + 1]; + color->g = data[i * 4 + 2]; + color->b = data[i * 4 + 3]; +#else + color->a = data[i * 4 + 3]; + color->r = data[i * 4 + 2]; + color->g = data[i * 4 + 1]; + color->b = data[i * 4]; +#endif + } + + SDL_Palette *palette = surface->format->palette; + assert(palette); + int ret = SDL_SetPaletteColors(palette, colors, 0, 256); + if (ret) { + LOGE("Could not set palette colors"); + SDL_FreeSurface(surface); + goto error; + } + } + surface->userdata = frame; // frame owns the data return surface; From 6004f0b6b069ef8c97e00a7b6f240413e24b28d7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 22 Oct 2021 18:51:20 +0200 Subject: [PATCH 0689/2244] Use a new scrcpy icon Use the new icon designed by @varlesh: Load it from a PNG file (SDL only supports bitmap icons). --- app/meson.build | 3 +++ app/src/screen.c | 7 +++---- data/icon.png | Bin 0 -> 6530 bytes release.mk | 2 ++ run | 4 +++- 5 files changed, 11 insertions(+), 5 deletions(-) create mode 100644 data/icon.png diff --git a/app/meson.build b/app/meson.build index 698120a5..b842bd04 100644 --- a/app/meson.build +++ b/app/meson.build @@ -151,6 +151,9 @@ executable('scrcpy', src, c_args: []) install_man('scrcpy.1') +install_data('../data/icon.png', + rename: 'scrcpy.png', + install_dir: 'share/icons/hicolor/256x256/apps') ### TESTS diff --git a/app/src/screen.c b/app/src/screen.c index 3cd4329f..8a2748a9 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -5,9 +5,8 @@ #include #include "events.h" -#include "icon.xpm" +#include "icon.h" #include "scrcpy.h" -#include "tiny_xpm.h" #include "video_buffer.h" #include "util/log.h" @@ -405,10 +404,10 @@ screen_init(struct screen *screen, const struct screen_params *params) { LOGD("Trilinear filtering disabled (not an OpenGL renderer)"); } - SDL_Surface *icon = read_xpm(icon_xpm); + SDL_Surface *icon = scrcpy_icon_load(); if (icon) { SDL_SetWindowIcon(screen->window, icon); - SDL_FreeSurface(icon); + scrcpy_icon_destroy(icon); } else { LOGW("Could not load icon"); } diff --git a/data/icon.png b/data/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..b96a1aff9b842976bae652c6fc22af7daed8bbf2 GIT binary patch literal 6530 zcmeAS@N?(olHy`uVBq!ia0y~yU}OMc4mJh`hM1xiX$%aKo}Mm_Ar-gY&gCo-N!@nb z_;E6eQpYAPCniCGn?g(+N*zjF3p%`5WWQF*O<-~2xcR-+HYNVVp|XO+BZZPr1yclh zXKS!HF|`ONSg-i(Rv;-VG2!_|8Kd!*oMjTd-?mn*SBB&ZfW5l zz~V?Xw4|uCH@YBv{f?vS_AdUj?7sZl|5^-Dt=SFPjoF9pz5eH4oAfuntdH-;QxVyM ztD0&J3=CCF>n)D6ym=!m{2;M>>V0*+4~h@H#S~63Z0Y&1z~;>sos*yl{I``ui6Klt zI^eGHxy^fj9s6s@SlD~a>>}e8+dI?W@qJP|-cWhvTD{)gofUixvmg#^%5KhHGLiEL ztJ})ezie&Z3Gh6a^la}|Lq#*JE0zqoH4 zke_wjltYDK+F{cruUXdw_cB_zv)2{HA5mfvVBmZZ^=MX5KjRUWnp1bhS@;<;+n8d4 zR~a_U6YREgeZVk*Z=L2<2?zE6J8uh^Ff3`#_A+IA@ZxuxToYSENrCUmV+^-`?b#^n z$n0=Pw%G~9e;4z?iid$gQ!tB*je&uOVW0>{W{}klj0_A33=9ks3=9lx3CUKj=hc^= zaVUzEcCrl?o1A>YMrGQuhcXXh9?W^6{Dkc}2hV|XJPZE+5|~wG(;K&Q!?}*gt2<2+ z<84Z#HwqkGU}*77rutlCTjj@|3EKQ?_}ds)++~n)wt2bn?`mn*9g3T?uU&GUtsE#J zmH6SAhT${ar>xKUo(u3ew6a~eR?p}AOKsy(1!*SX4fPhso;^A8B!AM1*aI;V4Yzm_ zZnR%aV~x5m{9@t;j@G2*1v$QzpUjVZJuD&7FpFnF?FpNzl}fkYI2fOw=dt`-;|!TC z=C?Qv=c*mKHfhd>6=yR4awbbOy!;X^pCWQ#dUo;4m9IpqmK2HhYQ%fxJ@w8AIa)L; zVCSrPYfol~6ntM>s3B$DoP6uVo7Nk}dTw|+g`mhPV#uv>wLHRS$R@uU+QuN+Y|}^$v8=YEO>X{@%Gm z_xO#gXZFkp+}w8TT^u8GKsO`LpJ)l&D9QY?1v8JY5_)yK&PK!|w#O+b`?~$j^BLFX zCue*5eJDsgQ5|T&uvNn0ctn1Y_5LG^5{%DZO}VCewpexM6NxvSO+mNC;uX_mcJH#_ zGTdq+AJZE1XigL3iVS&&{c{(JvDYjyaC zY>`~1XMT&p>{5^F{)Y zVnO{+j$YBe8(;JOuBCOyuY3;ChyS1bKVE;VzWsmRagO|_D*HR{%$G{I^7Z+RmHS@v zGfzm+n4tYNz#(!qlkDO>E6d#&c<%|hCfh~33G=I5n6fCO>fs%!S+TRTFa9epbbr#i z_0jnU^G!cK$lAzm;_^fG&u%NXYV9?z-)rVxw+~O$Z#~Gc=5T8Hj1=d_jk}}rgHPPO z`1$DNY`5pSOHZ<%{1Q~T?I>%W?w{R1jX$IpMvD8CAKx-hv*&K^ov-EF7rp*}V4lGo zj+A`13wALQTyr+nXc*kf(AY3-&L*bZ&n5j|mN7~ndNJ{Q_=PW*dcw5Jf@jvLIsaMn z=abKI5B=F&0_XQI7^+Pw`Rko@!tjtVTS9+2Q`zZnTUSMA{3trnoqj!_y`WNP&*`pK zlP`xgX2r}%nsQ^6^N%;deak1kKEClv?$Y%-xmOj>bf?WZx$5`a%V#S7FFv`hewx$y z<_1QOEshz}OPv|{n-?C~aaUXQcg%OauL}j|AGx?CSn`$bk7?(kG)@Zaf4tYbMkRZD z;r#V$>htfPTDQMjX!kz@hR$}T#LpQoPD*69F)`I{*0s0URJ%c)w?f-T#@+kllII)s z_wP6)obX5N_L5a8g~DfcoCU*DaUGxgd1lly1e zGuMlG?>~N~OGavQ^-ha7hTC8NPr5m4*L>rvr`FY9*NIQnFFwSuWa;~J0^O5a=TDPk zWAEtse`w1>hwVpPzq~jY>Xv|Nr>raYI7dZP` zbj@PVc-2d_CztK5wfuKF)Gf9?`__{8eF0D37dJ3^*ou5uq8P@*xvlhnSDJ?|m(KZ6eMcnh&9VdYXWCoWvpa7#d8rbY=yApR^Ec^rzn)3R z|4e(i`hL>&{~@kKKDivVZMsUEa6!LhZ_9?wd<_z6-U-mQJtWFx>F0xb$Sm$)yGc3^T)6 zw|Se3%$Oj*e)h$92jOqWXPaz19-BHZK|1|tuutv>6=m^tD-XAHvJ0>HyeV3nUHIEE z@9SUh%PpF>_M8F3&prY9pq!rp2{FwJ8SblHzET?O>-)%ejaSH%ihI*Ov)y4^=A(3O z)ybuu?LFZMYYfjSswW*0Rg`Er#_Ld%wEV|}*FP<~Cue$<&U$J6G|OnM)aF~o>kkx^ zE>&F?ar5i@Ij@|vP6bLd+*!?3d5xDfZJWWxqbL6s9OV+(&B?y`)4t|sZR|F0Hm!QS zUw6ipY4Z&jq(1P@ zFMk#1gjU}YDGev*aTx$*Hu4J+&BE{}a&45|VkyMNz&ykG?=^OFtvb65ru=s|r*oQA zMF$%+{+oR$fg#GQAuh99TK3+-&Zh0(-|*X=-0?6Vq3mqbtIcWxYzthOA0)4kzPI}L z^Q$*MRpcf_-xvfhHrTci_3MgO&*#{CD-|$<22c`KH4V}pxDLa+tT0Xg1=_m3ZQ${$X!Pda+D=s=yGj-gG%l;S0? zi{vF5Y$Y9{6Yu;lNnG_YRXDU$gF(S);lbYXm;IJ?nwuYHcyol|&URhHeMPIJ^nW+B z>2oiYczR;R9#sQ|YJlif#o<$oQp3!kj{Gp_i2{{4lz_VNoiW#7NdZ(g_b|D8RK z_v8Q1c0Csyh9lK4aX*1ldr&VOGY+yC+_ zw{*aZ?GL|lOSdlnv;S;XaG%4wr+X*WvemI4DF`Y^xfdkSAjP}jwpaY(b^n*FQaL#( z<#0dyiJ;iMm9lCVSn^};cwBGUwlU;&{T=6Lr5HvX$+Ow_nwTPMilkjwVr$#qXQ z^N;H$>R3Ac|2c8y`M=AmnI;>^7`*y6`}dRlRq6tt{D(`w{_5`UjjLEr3YosW^ju|X#lMF1xrVprXMT(ED)tghQaS#wLHyLF6B|vxvmIdY zNxpG^@vHPg)i=M4mxk|G)|{%AxM|hY9`AUMTg&!nDPO;|;I+G<0fVaaf!Kxfz64cX zTYGXgceZNn$#!+Y{7G9L)iOnyHzbFzQppYOyC^K=_v6!t!fEdoMBG!mV|a+0Z9xcg zfpNsQ$yJkjr>|IYZTt25OM4h=%KxUHy#3X*Gd%LfpV#FadaaBa7a6QtDkr+T`rZBg zH|V|4=ihnVPdcmKKI1(r&9J@k%A*L+y|;t+JHB{&>dv*7tOpoo9sm0HhShYr_?=Oo zT%N|(2X%*wY{_Gop8rRya<|t`>wxo%r+zH``Se?+43C2++XIP5k>&gD^K`2J`{J|y zwdYsr!RP+zm*)O6Xb3ZBu)7}nZn?ej z7L7S-wQ`>Se?H!&(iv5=bCTR0v-rcJ4`L@`Fhw`V^cwPAsEZ=GW z`rur<7mOUa2XZF{eA*bA8&&brhWBRK>-3YBECzdWXWSQgBVsJib|H$*;LG2P$n<}` z7UJqQYxf*wsjhHWkQEj>_a`>XTywRK{~yUkGvCCnsbqd&c451F+0FDM>6xn+Pe0db z_;CI~!=kB~LKg(?rJdVp{-;?&!XaDoz?MfPH?RGjopfP(UFpeF{?c-MA9h?8`TO}) z|C4H!?eFe)$6ifNoMqmic-*P?){*rIvVZSSo$n*}xp;g1hIc2=^PKv=`Q-C!@3||! zg|P7~Fy(3B|8qh9^V77&DNj2c4yUB5z3*K1UF&<-X@6hakN*E6Tvxa1{r_MSzqsDq zAb+3lyb!PSA2%a5>hj!9tGK6B=y4$TdPsl6?OEx+D-W$tw6Sp8_Up|r>GtY1-hiRQPdi}v@Wzc_eU7tvnG+bk-r0S`{adg7*>xOqIs~S# z*YUiWN8<{Wn~Ea{ui# zR{1**lfxEuZT=_m-F)|@q#arblNNFrPN=@Q-tyRMKVGSC-z7kr9(-Icbd=fths^hD zem4}`n=>DLV?C?>GB5t7`SKgWcf#(ZowfVgYW93vey;OV&I~*2f=dfsBpP&i7Q`Ni z`Is^-^Xkhksl$BMlAg``IhP$|XFjt0#)I`tM?3AJ`@P?0MJQ~M-LANO>5XMhw~M5U zqqSca+Xc^6nbVN1GXpo$OOs*0cHi(h>v-e)#z&Xx=Zb1`(e%UQ1>^z4_^` z0t@pD=ebGZ9e3;N)TKSyr?BLEJiha0(;0R)h3Sl6&T=aC--=O@2~hbH*LC_HwEpYiEck1}JwQYI^!Deg5O^8 zh38Et@o3D{yt?v=@kM?%g`YwRVG}C6wI}gtNX}dGZn`uN^MqZDF)eOSnfbQ1bfu>pHF`C10|%QzDRW83hhv#m4nNyxPC0g}SdU>E5xB^6qj6%- zfw0)Wjq&b5^ZS`sUiP}fcgJJ7|NMJ(4-c_Un)T>Wl9rARi^Ik_HkFf}oTy!=c~<(s z&f@12QXDRZgoGSmcw)q`Y{?RqPBxK~GmX&w>>zUc|ESGQ6~?`}5=WD#M2L`~O)U;h^x8aiFjf*U(kc$ zjQfhY_nVrV(+hQ9W_g+Ja{s>YuCbBPv+(fn|2+*%3DgxRgQBhGX2bdBrEt~J;?|)vB?Wny|c<`#A_-1{DO_`UMH3+Nwy%FAJ7A}~<(6j4z zcz1XA_uLyB4(?C9%VA-?BB0*z4XcDv!Gi;Zj*gD{&!0W}weR=4-3z_aKR-L0XkGT^ zN7?mtu`E;0r_ZnbW_#R+cjwf(bKjocdcgEz$XbpaFS9(SzN(q6zf`M*ftCB9(gHb) zv~LrtnPO!^*2QJq6b$w*}Ct{XcFlPiCJd+WY(&yQ-$8=brEMS*zY(TN}-}V)~LLD)Fk%XRKqZdX@EvkKIF3X_39WZPk|+2FB3fLmOS(oYPh= zSdd`Wb==OP?9C0G6qA%qc0V38Telps?0vLR`jo}86FDbTI5|ISbsgI$P*t*CVY6cJ z{C`U|q;@~fsjK@}uXuQ2nre@Dyz|BzZ(|O`Pkb2Q?f>6cL+lHaLhiXg`x})u{@&Se zed0@jdH-%bm8oJ32u}WQ9w;;G;r@vq1LpqdR8TouDbki^(I3aqGP6GSvzC#(kgAHa zB9~LKh~UNf6aH5GvTsbf@TTj4O9;cff3E|0W~s7^GB6*wG{4NUm8*d<#!)e2+Wi1V zgND@_ED{V$0!2kcrYsaNvF42E;<~Wo)926TizT+4ytlXdz8h;s3@0aN1*6Ae6*aYe zZCnj8t?G_~Tu#w%Zf}3@GTSWo&V+v#IO5~)hxf@?HeF@tRXFmiTwZ?uO}_2t9=4mT zOJsVtI(&T`i|_@vkH?Q6{~XI$BX~SuiT#sB-fZRXIAtsf7@jlU(v*q5J2}2yxS#uy zywR?MGuBV?^7ZAtwK@I#zow?9Yz>*K`d?!oM}YFNS)p%4`V#vu>3tTIl#~{xuLlet-za@|=VxO3viA?Amv!5+NPcH(P&;s0-Y7_3q&4kDZ%O;CAbxhy z*mypjU3I(JFUe&2y=D1zh~v$=50=YvC*GT|^3iFPpXa{C@oj%s@SZ)Sb+caC_ZLZ= glub!o`Sq**y}}%h+e)eV3=9kmp00i_>zopr04#ywj{pDw literal 0 HcmV?d00001 diff --git a/release.mk b/release.mk index e327654c..05ddbe46 100644 --- a/release.mk +++ b/release.mk @@ -94,6 +94,7 @@ dist-win32: build-server build-win32 cp "$(WIN32_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/" cp data/scrcpy-console.bat "$(DIST)/$(WIN32_TARGET_DIR)" cp data/scrcpy-noconsole.vbs "$(DIST)/$(WIN32_TARGET_DIR)" + cp data/icon.png "$(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)/" @@ -110,6 +111,7 @@ dist-win64: build-server build-win64 cp "$(WIN64_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/" cp data/scrcpy-console.bat "$(DIST)/$(WIN64_TARGET_DIR)" cp data/scrcpy-noconsole.vbs "$(DIST)/$(WIN64_TARGET_DIR)" + cp data/icon.png "$(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)/" diff --git a/run b/run index 628c5c7e..fda3ea57 100755 --- a/run +++ b/run @@ -20,4 +20,6 @@ then exit 1 fi -SCRCPY_SERVER_PATH="$BUILDDIR/server/scrcpy-server" "$BUILDDIR/app/scrcpy" "$@" +SCRCPY_ICON_PATH="data/icon.png" \ +SCRCPY_SERVER_PATH="$BUILDDIR/server/scrcpy-server" \ +"$BUILDDIR/app/scrcpy" "$@" From 3d5b31e0cbd0025f5072bc530cc0840ae301379a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 23 Oct 2021 09:25:59 +0200 Subject: [PATCH 0690/2244] Add icon source in SVG format Scrcpy only uses the PNG format (because SDL only supports bitmap icons), but keep the SVG source in the repo. --- data/icon.svg | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 data/icon.svg diff --git a/data/icon.svg b/data/icon.svg new file mode 100644 index 00000000..0ab92c2a --- /dev/null +++ b/data/icon.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + From 1e340caf76ef0ffa3c9f3d0ecd7d9a42472ae73a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 22 Oct 2021 18:51:20 +0200 Subject: [PATCH 0691/2244] Remove legacy scrcpy icon Remove the old icon in XPM format and the code to load it. --- app/meson.build | 1 - app/src/icon.xpm | 53 -------------------- app/src/scrcpy.c | 1 - app/src/tiny_xpm.c | 119 --------------------------------------------- app/src/tiny_xpm.h | 11 ----- 5 files changed, 185 deletions(-) delete mode 100644 app/src/icon.xpm delete mode 100644 app/src/tiny_xpm.c delete mode 100644 app/src/tiny_xpm.h diff --git a/app/meson.build b/app/meson.build index b842bd04..13978e91 100644 --- a/app/meson.build +++ b/app/meson.build @@ -21,7 +21,6 @@ src = [ 'src/screen.c', 'src/server.c', 'src/stream.c', - 'src/tiny_xpm.c', 'src/video_buffer.c', 'src/util/log.c', 'src/util/net.c', diff --git a/app/src/icon.xpm b/app/src/icon.xpm deleted file mode 100644 index 73b29da9..00000000 --- a/app/src/icon.xpm +++ /dev/null @@ -1,53 +0,0 @@ -/* XPM */ -static char * icon_xpm[] = { -"48 48 2 1", -" c None", -". c #96C13E", -" .. .. ", -" ... ... ", -" ... ...... ... ", -" ................ ", -" .............. ", -" ................ ", -" .................. ", -" .................... ", -" ..... ........ ..... ", -" ..... ........ ..... ", -" ...................... ", -" ........................ ", -" ........................ ", -" ........................ ", -" ", -" ", -" .... ........................ .... ", -" ...... ........................ ...... ", -" ...... ........................ ...... ", -" ...... ........................ ...... ", -" ...... ........................ ...... ", -" ...... ........................ ...... ", -" ...... ........................ ...... ", -" ...... ........................ ...... ", -" ...... ........................ ...... ", -" ...... ........................ ...... ", -" ...... ........................ ...... ", -" ...... ........................ ...... ", -" ...... ........................ ...... ", -" ...... ........................ ...... ", -" ...... ........................ ...... ", -" ...... ........................ ...... ", -" ...... ........................ ...... ", -" ...... ........................ ...... ", -" ...... ........................ ...... ", -" .... ........................ .... ", -" ........................ ", -" ...................... ", -" ...... ...... ", -" ...... ...... ", -" ...... ...... ", -" ...... ...... ", -" ...... ...... ", -" ...... ...... ", -" ...... ...... ", -" ...... ...... ", -" ...... ...... ", -" .... .... "}; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 6a285788..ce402ecc 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -22,7 +22,6 @@ #include "screen.h" #include "server.h" #include "stream.h" -#include "tiny_xpm.h" #include "util/log.h" #include "util/net.h" #ifdef HAVE_V4L2 diff --git a/app/src/tiny_xpm.c b/app/src/tiny_xpm.c deleted file mode 100644 index df1f9e53..00000000 --- a/app/src/tiny_xpm.c +++ /dev/null @@ -1,119 +0,0 @@ -#include "tiny_xpm.h" - -#include -#include -#include -#include -#include - -#include "util/log.h" - -struct index { - char c; - uint32_t color; -}; - -static bool -find_color(struct index *index, int len, char c, uint32_t *color) { - // there are typically very few color, so it's ok to iterate over the array - for (int i = 0; i < len; ++i) { - if (index[i].c == c) { - *color = index[i].color; - return true; - } - } - *color = 0; - return false; -} - -// We encounter some problems with SDL2_image on MSYS2 (Windows), -// so here is our own XPM parsing not to depend on SDL_image. -// -// We do not hardcode the binary image to keep some flexibility to replace the -// icon easily (just by replacing icon.xpm). -// -// Parameter is not "const char *" because XPM formats are generally stored in a -// (non-const) "char *" -SDL_Surface * -read_xpm(char *xpm[]) { -#ifndef NDEBUG - // patch the XPM to change the icon color in debug mode - xpm[2] = ". c #CC00CC"; -#endif - - char *endptr; - // *** No error handling, assume the XPM source is valid *** - // (it's in our source repo) - // Assertions are only checked in debug - int width = strtol(xpm[0], &endptr, 10); - int height = strtol(endptr + 1, &endptr, 10); - int colors = strtol(endptr + 1, &endptr, 10); - int chars = strtol(endptr + 1, &endptr, 10); - - // sanity checks - assert(0 <= width && width < 256); - assert(0 <= height && height < 256); - assert(0 <= colors && colors < 256); - assert(chars == 1); // this implementation does not support more - - (void) chars; - - // init index - struct index index[colors]; - for (int i = 0; i < colors; ++i) { - const char *line = xpm[1+i]; - index[i].c = line[0]; - assert(line[1] == '\t'); - assert(line[2] == 'c'); - assert(line[3] == ' '); - if (line[4] == '#') { - index[i].color = 0xff000000 | strtol(&line[5], &endptr, 0x10); - assert(*endptr == '\0'); - } else { - assert(!strcmp("None", &line[4])); - index[i].color = 0; - } - } - - // parse image - uint32_t *pixels = SDL_malloc(4 * width * height); - if (!pixels) { - LOGE("Could not allocate icon memory"); - return NULL; - } - for (int y = 0; y < height; ++y) { - const char *line = xpm[1 + colors + y]; - for (int x = 0; x < width; ++x) { - char c = line[x]; - uint32_t color; - bool color_found = find_color(index, colors, c, &color); - assert(color_found); - (void) color_found; - pixels[y * width + x] = color; - } - } - -#if SDL_BYTEORDER == SDL_BIG_ENDIAN - uint32_t amask = 0x000000ff; - uint32_t rmask = 0x0000ff00; - uint32_t gmask = 0x00ff0000; - uint32_t bmask = 0xff000000; -#else // little endian, like x86 - uint32_t amask = 0xff000000; - uint32_t rmask = 0x00ff0000; - uint32_t gmask = 0x0000ff00; - uint32_t bmask = 0x000000ff; -#endif - - SDL_Surface *surface = SDL_CreateRGBSurfaceFrom(pixels, - width, height, - 32, 4 * width, - rmask, gmask, bmask, amask); - if (!surface) { - LOGE("Could not create icon surface"); - return NULL; - } - // make the surface own the raw pixels - surface->flags &= ~SDL_PREALLOC; - return surface; -} diff --git a/app/src/tiny_xpm.h b/app/src/tiny_xpm.h deleted file mode 100644 index 29b42d14..00000000 --- a/app/src/tiny_xpm.h +++ /dev/null @@ -1,11 +0,0 @@ -#ifndef TINYXPM_H -#define TINYXPM_H - -#include "common.h" - -#include - -SDL_Surface * -read_xpm(char *xpm[]); - -#endif From 580f5d8bb929777cf101ebbc09a7235d177356ec Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 23 Oct 2021 09:29:30 +0200 Subject: [PATCH 0692/2244] Add scrcpy icon to README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index b9da20b5..98b54f54 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # scrcpy (v1.19) +![icon](data/icon.png) + [Read in another language](#translations) This application provides display and control of Android devices connected on From d6568f64af8bbb76b800fd083b270de0e9cb7e17 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 25 Oct 2021 18:21:31 +0200 Subject: [PATCH 0693/2244] Mention SCRCPY_ICON_PATH envvar in README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 98b54f54..41fddb97 100644 --- a/README.md +++ b/README.md @@ -830,6 +830,8 @@ To override the path of the `scrcpy-server` file, configure its path in [useful]: https://github.com/Genymobile/scrcpy/issues/278#issuecomment-429330345 +To override the icon, configure its path in `SCRCPY_ICON_PATH`. + ## Why _scrcpy_? From bea3197c3f3e906992486c28a604b10b22726bb0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 25 Oct 2021 18:22:48 +0200 Subject: [PATCH 0694/2244] Remove unused markdown link in README --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 41fddb97..84dd5ff5 100644 --- a/README.md +++ b/README.md @@ -828,8 +828,6 @@ ADB=/path/to/adb scrcpy To override the path of the `scrcpy-server` file, configure its path in `SCRCPY_SERVER_PATH`. -[useful]: https://github.com/Genymobile/scrcpy/issues/278#issuecomment-429330345 - To override the icon, configure its path in `SCRCPY_ICON_PATH`. From 056d36ce4aee1a496bb90fb470b9b41f79d2df24 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 3 Oct 2021 17:13:12 +0200 Subject: [PATCH 0695/2244] Fix trait header guards --- app/src/trait/frame_sink.h | 4 ++-- app/src/trait/packet_sink.h | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/trait/frame_sink.h b/app/src/trait/frame_sink.h index 64ab0de9..0214ab3e 100644 --- a/app/src/trait/frame_sink.h +++ b/app/src/trait/frame_sink.h @@ -1,5 +1,5 @@ -#ifndef SC_FRAME_SINK -#define SC_FRAME_SINK +#ifndef SC_FRAME_SINK_H +#define SC_FRAME_SINK_H #include "common.h" diff --git a/app/src/trait/packet_sink.h b/app/src/trait/packet_sink.h index fe9c137d..1fef765f 100644 --- a/app/src/trait/packet_sink.h +++ b/app/src/trait/packet_sink.h @@ -1,5 +1,5 @@ -#ifndef SC_PACKET_SINK -#define SC_PACKET_SINK +#ifndef SC_PACKET_SINK_H +#define SC_PACKET_SINK_H #include "common.h" From bcf5a9750f65e63e3ab2abd263c7393cb2748b8e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 3 Oct 2021 17:11:20 +0200 Subject: [PATCH 0696/2244] Extract keyboard processor trait This will allow to provide alternative key processors. --- app/meson.build | 1 + app/src/event_converter.c | 151 -------------------- app/src/event_converter.h | 10 -- app/src/input_manager.c | 65 +-------- app/src/input_manager.h | 10 +- app/src/keyboard_inject.c | 254 ++++++++++++++++++++++++++++++++++ app/src/keyboard_inject.h | 30 ++++ app/src/scrcpy.c | 12 +- app/src/trait/key_processor.h | 29 ++++ 9 files changed, 335 insertions(+), 227 deletions(-) create mode 100644 app/src/keyboard_inject.c create mode 100644 app/src/keyboard_inject.h create mode 100644 app/src/trait/key_processor.h diff --git a/app/meson.build b/app/meson.build index 13978e91..78060cff 100644 --- a/app/meson.build +++ b/app/meson.build @@ -14,6 +14,7 @@ src = [ 'src/fps_counter.c', 'src/frame_buffer.c', 'src/input_manager.c', + 'src/keyboard_inject.c', 'src/opengl.c', 'src/receiver.c', 'src/recorder.c', diff --git a/app/src/event_converter.c b/app/src/event_converter.c index a3c2da89..71f9de16 100644 --- a/app/src/event_converter.c +++ b/app/src/event_converter.c @@ -3,157 +3,6 @@ #define MAP(FROM, TO) case FROM: *to = TO; return true #define FAIL default: return false -bool -convert_keycode_action(SDL_EventType from, enum android_keyevent_action *to) { - switch (from) { - MAP(SDL_KEYDOWN, AKEY_EVENT_ACTION_DOWN); - MAP(SDL_KEYUP, AKEY_EVENT_ACTION_UP); - FAIL; - } -} - -static enum android_metastate -autocomplete_metastate(enum android_metastate metastate) { - // fill dependent flags - if (metastate & (AMETA_SHIFT_LEFT_ON | AMETA_SHIFT_RIGHT_ON)) { - metastate |= AMETA_SHIFT_ON; - } - if (metastate & (AMETA_CTRL_LEFT_ON | AMETA_CTRL_RIGHT_ON)) { - metastate |= AMETA_CTRL_ON; - } - if (metastate & (AMETA_ALT_LEFT_ON | AMETA_ALT_RIGHT_ON)) { - metastate |= AMETA_ALT_ON; - } - if (metastate & (AMETA_META_LEFT_ON | AMETA_META_RIGHT_ON)) { - metastate |= AMETA_META_ON; - } - - return metastate; -} - -enum android_metastate -convert_meta_state(SDL_Keymod mod) { - enum android_metastate metastate = 0; - if (mod & KMOD_LSHIFT) { - metastate |= AMETA_SHIFT_LEFT_ON; - } - if (mod & KMOD_RSHIFT) { - metastate |= AMETA_SHIFT_RIGHT_ON; - } - if (mod & KMOD_LCTRL) { - metastate |= AMETA_CTRL_LEFT_ON; - } - if (mod & KMOD_RCTRL) { - metastate |= AMETA_CTRL_RIGHT_ON; - } - if (mod & KMOD_LALT) { - metastate |= AMETA_ALT_LEFT_ON; - } - if (mod & KMOD_RALT) { - metastate |= AMETA_ALT_RIGHT_ON; - } - if (mod & KMOD_LGUI) { // Windows key - metastate |= AMETA_META_LEFT_ON; - } - if (mod & KMOD_RGUI) { // Windows key - metastate |= AMETA_META_RIGHT_ON; - } - if (mod & KMOD_NUM) { - metastate |= AMETA_NUM_LOCK_ON; - } - if (mod & KMOD_CAPS) { - metastate |= AMETA_CAPS_LOCK_ON; - } - if (mod & KMOD_MODE) { // Alt Gr - // no mapping? - } - - // fill the dependent fields - return autocomplete_metastate(metastate); -} - -bool -convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod, - bool prefer_text) { - switch (from) { - MAP(SDLK_RETURN, AKEYCODE_ENTER); - MAP(SDLK_KP_ENTER, AKEYCODE_NUMPAD_ENTER); - MAP(SDLK_ESCAPE, AKEYCODE_ESCAPE); - MAP(SDLK_BACKSPACE, AKEYCODE_DEL); - MAP(SDLK_TAB, AKEYCODE_TAB); - MAP(SDLK_PAGEUP, AKEYCODE_PAGE_UP); - MAP(SDLK_DELETE, AKEYCODE_FORWARD_DEL); - MAP(SDLK_HOME, AKEYCODE_MOVE_HOME); - MAP(SDLK_END, AKEYCODE_MOVE_END); - MAP(SDLK_PAGEDOWN, AKEYCODE_PAGE_DOWN); - MAP(SDLK_RIGHT, AKEYCODE_DPAD_RIGHT); - 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); - MAP(SDLK_LSHIFT, AKEYCODE_SHIFT_LEFT); - MAP(SDLK_RSHIFT, AKEYCODE_SHIFT_RIGHT); - } - - 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 && !(mod & KMOD_CTRL)) { - // do not forward alpha and space key events (unless Ctrl is pressed) - return false; - } - - if (mod & (KMOD_LALT | KMOD_RALT | KMOD_LGUI | KMOD_RGUI)) { - return false; - } - // if ALT and META are not pressed, also handle letters and space - switch (from) { - MAP(SDLK_a, AKEYCODE_A); - MAP(SDLK_b, AKEYCODE_B); - MAP(SDLK_c, AKEYCODE_C); - MAP(SDLK_d, AKEYCODE_D); - MAP(SDLK_e, AKEYCODE_E); - MAP(SDLK_f, AKEYCODE_F); - MAP(SDLK_g, AKEYCODE_G); - MAP(SDLK_h, AKEYCODE_H); - MAP(SDLK_i, AKEYCODE_I); - MAP(SDLK_j, AKEYCODE_J); - MAP(SDLK_k, AKEYCODE_K); - MAP(SDLK_l, AKEYCODE_L); - MAP(SDLK_m, AKEYCODE_M); - MAP(SDLK_n, AKEYCODE_N); - MAP(SDLK_o, AKEYCODE_O); - MAP(SDLK_p, AKEYCODE_P); - MAP(SDLK_q, AKEYCODE_Q); - MAP(SDLK_r, AKEYCODE_R); - MAP(SDLK_s, AKEYCODE_S); - MAP(SDLK_t, AKEYCODE_T); - MAP(SDLK_u, AKEYCODE_U); - MAP(SDLK_v, AKEYCODE_V); - MAP(SDLK_w, AKEYCODE_W); - MAP(SDLK_x, AKEYCODE_X); - MAP(SDLK_y, AKEYCODE_Y); - MAP(SDLK_z, AKEYCODE_Z); - MAP(SDLK_SPACE, AKEYCODE_SPACE); - FAIL; - } -} - enum android_motionevent_buttons convert_mouse_buttons(uint32_t state) { enum android_motionevent_buttons buttons = 0; diff --git a/app/src/event_converter.h b/app/src/event_converter.h index d28e9fdc..71e8edec 100644 --- a/app/src/event_converter.h +++ b/app/src/event_converter.h @@ -8,16 +8,6 @@ #include "control_msg.h" -bool -convert_keycode_action(SDL_EventType from, enum android_keyevent_action *to); - -enum android_metastate -convert_meta_state(SDL_Keymod mod); - -bool -convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod, - bool prefer_text); - enum android_motionevent_buttons convert_mouse_buttons(uint32_t state); diff --git a/app/src/input_manager.c b/app/src/input_manager.c index a5d0ad07..764760cf 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -53,15 +53,15 @@ is_shortcut_mod(struct input_manager *im, uint16_t sdl_mod) { void input_manager_init(struct input_manager *im, struct controller *controller, - struct screen *screen, + struct screen *screen, struct sc_key_processor *kp, const struct scrcpy_options *options) { + assert(!options->control || (kp && kp->ops)); + im->controller = controller; im->screen = screen; - im->repeat = 0; + im->kp = kp; im->control = options->control; - im->forward_key_repeat = options->forward_key_repeat; - im->prefer_text = options->prefer_text; im->forward_all_clicks = options->forward_all_clicks; im->legacy_paste = options->legacy_paste; @@ -323,26 +323,8 @@ input_manager_process_text_input(struct input_manager *im, // A shortcut must never generate text events return; } - if (!im->prefer_text) { - char c = event->text[0]; - if (isalpha(c) || c == ' ') { - assert(event->text[1] == '\0'); - // letters and space are handled as raw key event - return; - } - } - struct control_msg msg; - msg.type = CONTROL_MSG_TYPE_INJECT_TEXT; - msg.inject_text.text = strdup(event->text); - if (!msg.inject_text.text) { - LOGW("Could not strdup input text"); - return; - } - if (!controller_push_msg(im->controller, &msg)) { - free(msg.inject_text.text); - LOGW("Could not request 'inject text'"); - } + im->kp->ops->process_text(im->kp, event); } static bool @@ -375,27 +357,6 @@ inverse_point(struct point point, struct size size) { return point; } -static bool -convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to, - bool prefer_text, uint32_t repeat) { - to->type = CONTROL_MSG_TYPE_INJECT_KEYCODE; - - if (!convert_keycode_action(from->type, &to->inject_keycode.action)) { - return false; - } - - uint16_t mod = from->keysym.mod; - if (!convert_keycode(from->keysym.sym, &to->inject_keycode.keycode, mod, - prefer_text)) { - return false; - } - - to->inject_keycode.repeat = repeat; - to->inject_keycode.metastate = convert_meta_state(mod); - - return true; -} - static void input_manager_process_key(struct input_manager *im, const SDL_KeyboardEvent *event) { @@ -549,15 +510,6 @@ input_manager_process_key(struct input_manager *im, return; } - if (event->repeat) { - if (!im->forward_key_repeat) { - return; - } - ++im->repeat; - } else { - im->repeat = 0; - } - if (ctrl && !shift && keycode == SDLK_v && down && !repeat) { if (im->legacy_paste) { // inject the text as input events @@ -569,12 +521,7 @@ input_manager_process_key(struct input_manager *im, 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)) { - LOGW("Could not request 'inject keycode'"); - } - } + im->kp->ops->process_key(im->kp, event); } static bool diff --git a/app/src/input_manager.h b/app/src/input_manager.h index 1dd7825f..cdd32295 100644 --- a/app/src/input_manager.h +++ b/app/src/input_manager.h @@ -11,18 +11,15 @@ #include "fps_counter.h" #include "scrcpy.h" #include "screen.h" +#include "trait/key_processor.h" struct input_manager { struct controller *controller; 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; + struct sc_key_processor *kp; bool control; - bool forward_key_repeat; - bool prefer_text; bool forward_all_clicks; bool legacy_paste; @@ -43,7 +40,8 @@ struct input_manager { void input_manager_init(struct input_manager *im, struct controller *controller, - struct screen *screen, const struct scrcpy_options *options); + struct screen *screen, struct sc_key_processor *kp, + const struct scrcpy_options *options); bool input_manager_handle_event(struct input_manager *im, SDL_Event *event); diff --git a/app/src/keyboard_inject.c b/app/src/keyboard_inject.c new file mode 100644 index 00000000..bcc85da8 --- /dev/null +++ b/app/src/keyboard_inject.c @@ -0,0 +1,254 @@ +#include "keyboard_inject.h" + +#include +#include + +#include "android/input.h" +#include "control_msg.h" +#include "controller.h" +#include "util/log.h" + +/** Downcast key processor to sc_keyboard_inject */ +#define DOWNCAST(KP) \ + container_of(KP, struct sc_keyboard_inject, key_processor) + +#define MAP(FROM, TO) case FROM: *to = TO; return true +#define FAIL default: return false +static bool +convert_keycode_action(SDL_EventType from, enum android_keyevent_action *to) { + switch (from) { + MAP(SDL_KEYDOWN, AKEY_EVENT_ACTION_DOWN); + MAP(SDL_KEYUP, AKEY_EVENT_ACTION_UP); + FAIL; + } +} + +static bool +convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod, + bool prefer_text) { + switch (from) { + MAP(SDLK_RETURN, AKEYCODE_ENTER); + MAP(SDLK_KP_ENTER, AKEYCODE_NUMPAD_ENTER); + MAP(SDLK_ESCAPE, AKEYCODE_ESCAPE); + MAP(SDLK_BACKSPACE, AKEYCODE_DEL); + MAP(SDLK_TAB, AKEYCODE_TAB); + MAP(SDLK_PAGEUP, AKEYCODE_PAGE_UP); + MAP(SDLK_DELETE, AKEYCODE_FORWARD_DEL); + MAP(SDLK_HOME, AKEYCODE_MOVE_HOME); + MAP(SDLK_END, AKEYCODE_MOVE_END); + MAP(SDLK_PAGEDOWN, AKEYCODE_PAGE_DOWN); + MAP(SDLK_RIGHT, AKEYCODE_DPAD_RIGHT); + 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); + MAP(SDLK_LSHIFT, AKEYCODE_SHIFT_LEFT); + MAP(SDLK_RSHIFT, AKEYCODE_SHIFT_RIGHT); + } + + 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 && !(mod & KMOD_CTRL)) { + // do not forward alpha and space key events (unless Ctrl is pressed) + return false; + } + + if (mod & (KMOD_LALT | KMOD_RALT | KMOD_LGUI | KMOD_RGUI)) { + return false; + } + // if ALT and META are not pressed, also handle letters and space + switch (from) { + MAP(SDLK_a, AKEYCODE_A); + MAP(SDLK_b, AKEYCODE_B); + MAP(SDLK_c, AKEYCODE_C); + MAP(SDLK_d, AKEYCODE_D); + MAP(SDLK_e, AKEYCODE_E); + MAP(SDLK_f, AKEYCODE_F); + MAP(SDLK_g, AKEYCODE_G); + MAP(SDLK_h, AKEYCODE_H); + MAP(SDLK_i, AKEYCODE_I); + MAP(SDLK_j, AKEYCODE_J); + MAP(SDLK_k, AKEYCODE_K); + MAP(SDLK_l, AKEYCODE_L); + MAP(SDLK_m, AKEYCODE_M); + MAP(SDLK_n, AKEYCODE_N); + MAP(SDLK_o, AKEYCODE_O); + MAP(SDLK_p, AKEYCODE_P); + MAP(SDLK_q, AKEYCODE_Q); + MAP(SDLK_r, AKEYCODE_R); + MAP(SDLK_s, AKEYCODE_S); + MAP(SDLK_t, AKEYCODE_T); + MAP(SDLK_u, AKEYCODE_U); + MAP(SDLK_v, AKEYCODE_V); + MAP(SDLK_w, AKEYCODE_W); + MAP(SDLK_x, AKEYCODE_X); + MAP(SDLK_y, AKEYCODE_Y); + MAP(SDLK_z, AKEYCODE_Z); + MAP(SDLK_SPACE, AKEYCODE_SPACE); + FAIL; + } +} + +static enum android_metastate +autocomplete_metastate(enum android_metastate metastate) { + // fill dependent flags + if (metastate & (AMETA_SHIFT_LEFT_ON | AMETA_SHIFT_RIGHT_ON)) { + metastate |= AMETA_SHIFT_ON; + } + if (metastate & (AMETA_CTRL_LEFT_ON | AMETA_CTRL_RIGHT_ON)) { + metastate |= AMETA_CTRL_ON; + } + if (metastate & (AMETA_ALT_LEFT_ON | AMETA_ALT_RIGHT_ON)) { + metastate |= AMETA_ALT_ON; + } + if (metastate & (AMETA_META_LEFT_ON | AMETA_META_RIGHT_ON)) { + metastate |= AMETA_META_ON; + } + + return metastate; +} + +static enum android_metastate +convert_meta_state(SDL_Keymod mod) { + enum android_metastate metastate = 0; + if (mod & KMOD_LSHIFT) { + metastate |= AMETA_SHIFT_LEFT_ON; + } + if (mod & KMOD_RSHIFT) { + metastate |= AMETA_SHIFT_RIGHT_ON; + } + if (mod & KMOD_LCTRL) { + metastate |= AMETA_CTRL_LEFT_ON; + } + if (mod & KMOD_RCTRL) { + metastate |= AMETA_CTRL_RIGHT_ON; + } + if (mod & KMOD_LALT) { + metastate |= AMETA_ALT_LEFT_ON; + } + if (mod & KMOD_RALT) { + metastate |= AMETA_ALT_RIGHT_ON; + } + if (mod & KMOD_LGUI) { // Windows key + metastate |= AMETA_META_LEFT_ON; + } + if (mod & KMOD_RGUI) { // Windows key + metastate |= AMETA_META_RIGHT_ON; + } + if (mod & KMOD_NUM) { + metastate |= AMETA_NUM_LOCK_ON; + } + if (mod & KMOD_CAPS) { + metastate |= AMETA_CAPS_LOCK_ON; + } + if (mod & KMOD_MODE) { // Alt Gr + // no mapping? + } + + // fill the dependent fields + return autocomplete_metastate(metastate); +} + +static bool +convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to, + bool prefer_text, uint32_t repeat) { + to->type = CONTROL_MSG_TYPE_INJECT_KEYCODE; + + if (!convert_keycode_action(from->type, &to->inject_keycode.action)) { + return false; + } + + uint16_t mod = from->keysym.mod; + if (!convert_keycode(from->keysym.sym, &to->inject_keycode.keycode, mod, + prefer_text)) { + return false; + } + + to->inject_keycode.repeat = repeat; + to->inject_keycode.metastate = convert_meta_state(mod); + + return true; +} + +static void +sc_key_processor_process_key(struct sc_key_processor *kp, + const SDL_KeyboardEvent *event) { + struct sc_keyboard_inject *ki = DOWNCAST(kp); + + if (event->repeat) { + if (!ki->forward_key_repeat) { + return; + } + ++ki->repeat; + } else { + ki->repeat = 0; + } + + struct control_msg msg; + if (convert_input_key(event, &msg, ki->prefer_text, ki->repeat)) { + if (!controller_push_msg(ki->controller, &msg)) { + LOGW("Could not request 'inject keycode'"); + } + } +} + +static void +sc_key_processor_process_text(struct sc_key_processor *kp, + const SDL_TextInputEvent *event) { + struct sc_keyboard_inject *ki = DOWNCAST(kp); + + if (!ki->prefer_text) { + char c = event->text[0]; + if (isalpha(c) || c == ' ') { + assert(event->text[1] == '\0'); + // letters and space are handled as raw key event + return; + } + } + + struct control_msg msg; + msg.type = CONTROL_MSG_TYPE_INJECT_TEXT; + msg.inject_text.text = strdup(event->text); + if (!msg.inject_text.text) { + LOGW("Could not strdup input text"); + return; + } + if (!controller_push_msg(ki->controller, &msg)) { + free(msg.inject_text.text); + LOGW("Could not request 'inject text'"); + } +} + +void +sc_keyboard_inject_init(struct sc_keyboard_inject *ki, + struct controller *controller, + const struct scrcpy_options *options) { + ki->controller = controller; + ki->prefer_text = options->prefer_text; + ki->forward_key_repeat = options->forward_key_repeat; + + ki->repeat = 0; + + static const struct sc_key_processor_ops ops = { + .process_key = sc_key_processor_process_key, + .process_text = sc_key_processor_process_text, + }; + + ki->key_processor.ops = &ops; +} diff --git a/app/src/keyboard_inject.h b/app/src/keyboard_inject.h new file mode 100644 index 00000000..e59de46d --- /dev/null +++ b/app/src/keyboard_inject.h @@ -0,0 +1,30 @@ +#ifndef SC_KEYBOARD_INJECT_H +#define SC_KEYBOARD_INJECT_H + +#include "common.h" + +#include + +#include "controller.h" +#include "scrcpy.h" +#include "trait/key_processor.h" + +struct sc_keyboard_inject { + struct sc_key_processor key_processor; // key processor trait + + struct controller *controller; + + // 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; + bool forward_key_repeat; +}; + +void +sc_keyboard_inject_init(struct sc_keyboard_inject *ki, + struct controller *controller, + const struct scrcpy_options *options); + +#endif diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index ce402ecc..def96bb7 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -18,6 +18,7 @@ #include "events.h" #include "file_handler.h" #include "input_manager.h" +#include "keyboard_inject.h" #include "recorder.h" #include "screen.h" #include "server.h" @@ -39,6 +40,7 @@ struct scrcpy { #endif struct controller controller; struct file_handler file_handler; + struct sc_keyboard_inject keyboard_inject; struct input_manager input_manager; }; @@ -411,7 +413,15 @@ scrcpy(const struct scrcpy_options *options) { } stream_started = true; - input_manager_init(&s->input_manager, &s->controller, &s->screen, options); + struct sc_key_processor *kp = NULL; + + if (options->control) { + sc_keyboard_inject_init(&s->keyboard_inject, &s->controller, options); + kp = &s->keyboard_inject.key_processor; + } + + input_manager_init(&s->input_manager, &s->controller, &s->screen, kp, + options); ret = event_loop(s, options); LOGD("quit..."); diff --git a/app/src/trait/key_processor.h b/app/src/trait/key_processor.h new file mode 100644 index 00000000..5790310b --- /dev/null +++ b/app/src/trait/key_processor.h @@ -0,0 +1,29 @@ +#ifndef SC_KEY_PROCESSOR_H +#define SC_KEY_PROCESSOR_H + +#include "common.h" + +#include +#include + +#include + +/** + * Key processor trait. + * + * Component able to process and inject keys should implement this trait. + */ +struct sc_key_processor { + const struct sc_key_processor_ops *ops; +}; + +struct sc_key_processor_ops { + void + (*process_key)(struct sc_key_processor *kp, const SDL_KeyboardEvent *event); + + void + (*process_text)(struct sc_key_processor *kp, + const SDL_TextInputEvent *event); +}; + +#endif From f7d1efdf1dc8d35a5de7e7654e71a96be2a8bd7c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 3 Oct 2021 17:44:14 +0200 Subject: [PATCH 0697/2244] Extract mouse processor trait Mainly for consistency with the keyboard processor trait. This could allow to provide alternative mouse processors. --- app/meson.build | 2 +- app/src/event_converter.c | 44 ------- app/src/event_converter.h | 20 --- app/src/input_manager.c | 131 ++------------------ app/src/input_manager.h | 3 + app/src/mouse_inject.c | 211 ++++++++++++++++++++++++++++++++ app/src/mouse_inject.h | 24 ++++ app/src/scrcpy.c | 8 +- app/src/trait/mouse_processor.h | 39 ++++++ 9 files changed, 298 insertions(+), 184 deletions(-) delete mode 100644 app/src/event_converter.c delete mode 100644 app/src/event_converter.h create mode 100644 app/src/mouse_inject.c create mode 100644 app/src/mouse_inject.h create mode 100644 app/src/trait/mouse_processor.h diff --git a/app/meson.build b/app/meson.build index 78060cff..46d3994f 100644 --- a/app/meson.build +++ b/app/meson.build @@ -8,13 +8,13 @@ src = [ 'src/controller.c', 'src/decoder.c', 'src/device_msg.c', - 'src/event_converter.c', 'src/icon.c', 'src/file_handler.c', 'src/fps_counter.c', 'src/frame_buffer.c', 'src/input_manager.c', 'src/keyboard_inject.c', + 'src/mouse_inject.c', 'src/opengl.c', 'src/receiver.c', 'src/recorder.c', diff --git a/app/src/event_converter.c b/app/src/event_converter.c deleted file mode 100644 index 71f9de16..00000000 --- a/app/src/event_converter.c +++ /dev/null @@ -1,44 +0,0 @@ -#include "event_converter.h" - -#define MAP(FROM, TO) case FROM: *to = TO; return true -#define FAIL default: return false - -enum android_motionevent_buttons -convert_mouse_buttons(uint32_t state) { - enum android_motionevent_buttons buttons = 0; - if (state & SDL_BUTTON_LMASK) { - buttons |= AMOTION_EVENT_BUTTON_PRIMARY; - } - if (state & SDL_BUTTON_RMASK) { - buttons |= AMOTION_EVENT_BUTTON_SECONDARY; - } - if (state & SDL_BUTTON_MMASK) { - buttons |= AMOTION_EVENT_BUTTON_TERTIARY; - } - if (state & SDL_BUTTON_X1MASK) { - buttons |= AMOTION_EVENT_BUTTON_BACK; - } - if (state & SDL_BUTTON_X2MASK) { - buttons |= AMOTION_EVENT_BUTTON_FORWARD; - } - return buttons; -} - -bool -convert_mouse_action(SDL_EventType from, enum android_motionevent_action *to) { - switch (from) { - MAP(SDL_MOUSEBUTTONDOWN, AMOTION_EVENT_ACTION_DOWN); - MAP(SDL_MOUSEBUTTONUP, AMOTION_EVENT_ACTION_UP); - FAIL; - } -} - -bool -convert_touch_action(SDL_EventType from, enum android_motionevent_action *to) { - switch (from) { - MAP(SDL_FINGERMOTION, AMOTION_EVENT_ACTION_MOVE); - MAP(SDL_FINGERDOWN, AMOTION_EVENT_ACTION_DOWN); - MAP(SDL_FINGERUP, AMOTION_EVENT_ACTION_UP); - FAIL; - } -} diff --git a/app/src/event_converter.h b/app/src/event_converter.h deleted file mode 100644 index 71e8edec..00000000 --- a/app/src/event_converter.h +++ /dev/null @@ -1,20 +0,0 @@ -#ifndef CONVERT_H -#define CONVERT_H - -#include "common.h" - -#include -#include - -#include "control_msg.h" - -enum android_motionevent_buttons -convert_mouse_buttons(uint32_t state); - -bool -convert_mouse_action(SDL_EventType from, enum android_motionevent_action *to); - -bool -convert_touch_action(SDL_EventType from, enum android_motionevent_action *to); - -#endif diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 764760cf..bba3c998 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -3,7 +3,6 @@ #include #include -#include "event_converter.h" #include "util/log.h" static const int ACTION_DOWN = 1; @@ -54,12 +53,15 @@ is_shortcut_mod(struct input_manager *im, uint16_t sdl_mod) { void input_manager_init(struct input_manager *im, struct controller *controller, struct screen *screen, struct sc_key_processor *kp, + struct sc_mouse_processor *mp, const struct scrcpy_options *options) { assert(!options->control || (kp && kp->ops)); + assert(!options->control || (mp && mp->ops)); im->controller = controller; im->screen = screen; im->kp = kp; + im->mp = mp; im->control = options->control; im->forward_all_clicks = options->forward_all_clicks; @@ -524,21 +526,6 @@ input_manager_process_key(struct input_manager *im, im->kp->ops->process_key(im->kp, event); } -static bool -convert_mouse_motion(const SDL_MouseMotionEvent *from, struct screen *screen, - struct control_msg *to) { - to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT; - 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 = - 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); - - return true; -} - static void input_manager_process_mouse_motion(struct input_manager *im, const SDL_MouseMotionEvent *event) { @@ -554,79 +541,22 @@ input_manager_process_mouse_motion(struct input_manager *im, // simulated from touch events, so it's a duplicate return; } - struct control_msg msg; - if (!convert_mouse_motion(event, im->screen, &msg)) { - return; - } - if (!controller_push_msg(im->controller, &msg)) { - LOGW("Could not request 'inject mouse motion event'"); - } + im->mp->ops->process_mouse_motion(im->mp, event); if (im->vfinger_down) { - struct point mouse = msg.inject_touch_event.position.point; + struct point mouse = + screen_convert_window_to_frame_coords(im->screen, event->x, + event->y); struct point vfinger = inverse_point(mouse, im->screen->frame_size); simulate_virtual_finger(im, AMOTION_EVENT_ACTION_MOVE, vfinger); } } -static bool -convert_touch(const SDL_TouchFingerEvent *from, struct screen *screen, - struct control_msg *to) { - to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT; - - if (!convert_touch_action(from->type, &to->inject_touch_event.action)) { - return false; - } - - to->inject_touch_event.pointer_id = from->fingerId; - to->inject_touch_event.position.screen_size = screen->frame_size; - - 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 * dw; - int32_t y = from->y * dh; - to->inject_touch_event.position.point = - screen_convert_drawable_to_frame_coords(screen, x, y); - - to->inject_touch_event.pressure = from->pressure; - to->inject_touch_event.buttons = 0; - return true; -} - static void input_manager_process_touch(struct input_manager *im, const SDL_TouchFingerEvent *event) { - struct control_msg msg; - if (convert_touch(event, im->screen, &msg)) { - if (!controller_push_msg(im->controller, &msg)) { - LOGW("Could not request 'inject touch event'"); - } - } -} - -static bool -convert_mouse_button(const SDL_MouseButtonEvent *from, struct screen *screen, - struct control_msg *to) { - to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT; - - if (!convert_mouse_action(from->type, &to->inject_touch_event.action)) { - return false; - } - - 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_window_to_frame_coords(screen, from->x, from->y); - 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)); - - return true; + im->mp->ops->process_touch(im->mp, event); } static void @@ -686,15 +616,7 @@ input_manager_process_mouse_button(struct input_manager *im, return; } - struct control_msg msg; - 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; - } + im->mp->ops->process_mouse_button(im->mp, event); // Pinch-to-zoom simulation. // @@ -708,7 +630,9 @@ input_manager_process_mouse_button(struct input_manager *im, #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 mouse = + screen_convert_window_to_frame_coords(im->screen, event->x, + event->y); struct point vfinger = inverse_point(mouse, im->screen->frame_size); enum android_motionevent_action action = down ? AMOTION_EVENT_ACTION_DOWN @@ -720,39 +644,10 @@ 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 = screen_convert_window_to_frame_coords(screen, - mouse_x, mouse_y), - }; - - to->type = CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT; - - to->inject_scroll_event.position = position; - to->inject_scroll_event.hscroll = from->x; - to->inject_scroll_event.vscroll = from->y; - - return true; -} - static void input_manager_process_mouse_wheel(struct input_manager *im, const SDL_MouseWheelEvent *event) { - struct control_msg msg; - if (convert_mouse_wheel(event, im->screen, &msg)) { - if (!controller_push_msg(im->controller, &msg)) { - LOGW("Could not request 'inject mouse wheel event'"); - } - } + im->mp->ops->process_mouse_wheel(im->mp, event); } bool diff --git a/app/src/input_manager.h b/app/src/input_manager.h index cdd32295..bd9d7a1b 100644 --- a/app/src/input_manager.h +++ b/app/src/input_manager.h @@ -12,12 +12,14 @@ #include "scrcpy.h" #include "screen.h" #include "trait/key_processor.h" +#include "trait/mouse_processor.h" struct input_manager { struct controller *controller; struct screen *screen; struct sc_key_processor *kp; + struct sc_mouse_processor *mp; bool control; bool forward_all_clicks; @@ -41,6 +43,7 @@ struct input_manager { void input_manager_init(struct input_manager *im, struct controller *controller, struct screen *screen, struct sc_key_processor *kp, + struct sc_mouse_processor *mp, const struct scrcpy_options *options); bool diff --git a/app/src/mouse_inject.c b/app/src/mouse_inject.c new file mode 100644 index 00000000..008da267 --- /dev/null +++ b/app/src/mouse_inject.c @@ -0,0 +1,211 @@ +#include "mouse_inject.h" + +#include +#include + +#include "android/input.h" +#include "control_msg.h" +#include "controller.h" +#include "util/log.h" + +/** Downcast mouse processor to sc_mouse_inject */ +#define DOWNCAST(MP) container_of(MP, struct sc_mouse_inject, mouse_processor) + +static enum android_motionevent_buttons +convert_mouse_buttons(uint32_t state) { + enum android_motionevent_buttons buttons = 0; + if (state & SDL_BUTTON_LMASK) { + buttons |= AMOTION_EVENT_BUTTON_PRIMARY; + } + if (state & SDL_BUTTON_RMASK) { + buttons |= AMOTION_EVENT_BUTTON_SECONDARY; + } + if (state & SDL_BUTTON_MMASK) { + buttons |= AMOTION_EVENT_BUTTON_TERTIARY; + } + if (state & SDL_BUTTON_X1MASK) { + buttons |= AMOTION_EVENT_BUTTON_BACK; + } + if (state & SDL_BUTTON_X2MASK) { + buttons |= AMOTION_EVENT_BUTTON_FORWARD; + } + return buttons; +} + +#define MAP(FROM, TO) case FROM: *to = TO; return true +#define FAIL default: return false +static bool +convert_mouse_action(SDL_EventType from, enum android_motionevent_action *to) { + switch (from) { + MAP(SDL_MOUSEBUTTONDOWN, AMOTION_EVENT_ACTION_DOWN); + MAP(SDL_MOUSEBUTTONUP, AMOTION_EVENT_ACTION_UP); + FAIL; + } +} + +static bool +convert_touch_action(SDL_EventType from, enum android_motionevent_action *to) { + switch (from) { + MAP(SDL_FINGERMOTION, AMOTION_EVENT_ACTION_MOVE); + MAP(SDL_FINGERDOWN, AMOTION_EVENT_ACTION_DOWN); + MAP(SDL_FINGERUP, AMOTION_EVENT_ACTION_UP); + FAIL; + } +} + +static bool +convert_mouse_motion(const SDL_MouseMotionEvent *from, struct screen *screen, + struct control_msg *to) { + to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT; + 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 = + 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); + + return true; +} + +static bool +convert_touch(const SDL_TouchFingerEvent *from, struct screen *screen, + struct control_msg *to) { + to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT; + + if (!convert_touch_action(from->type, &to->inject_touch_event.action)) { + return false; + } + + to->inject_touch_event.pointer_id = from->fingerId; + to->inject_touch_event.position.screen_size = screen->frame_size; + + 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 * dw; + int32_t y = from->y * dh; + to->inject_touch_event.position.point = + screen_convert_drawable_to_frame_coords(screen, x, y); + + to->inject_touch_event.pressure = from->pressure; + to->inject_touch_event.buttons = 0; + return true; +} + +static bool +convert_mouse_button(const SDL_MouseButtonEvent *from, struct screen *screen, + struct control_msg *to) { + to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT; + + if (!convert_mouse_action(from->type, &to->inject_touch_event.action)) { + return false; + } + + 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_window_to_frame_coords(screen, from->x, from->y); + 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)); + + return true; +} + +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 = screen_convert_window_to_frame_coords(screen, + mouse_x, mouse_y), + }; + + to->type = CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT; + + to->inject_scroll_event.position = position; + to->inject_scroll_event.hscroll = from->x; + to->inject_scroll_event.vscroll = from->y; + + return true; +} + +static void +sc_mouse_processor_process_mouse_motion(struct sc_mouse_processor *mp, + const SDL_MouseMotionEvent *event) { + struct sc_mouse_inject *mi = DOWNCAST(mp); + + struct control_msg msg; + if (!convert_mouse_motion(event, mi->screen, &msg)) { + return; + } + + if (!controller_push_msg(mi->controller, &msg)) { + LOGW("Could not request 'inject mouse motion event'"); + } +} + +static void +sc_mouse_processor_process_touch(struct sc_mouse_processor *mp, + const SDL_TouchFingerEvent *event) { + struct sc_mouse_inject *mi = DOWNCAST(mp); + + struct control_msg msg; + if (convert_touch(event, mi->screen, &msg)) { + if (!controller_push_msg(mi->controller, &msg)) { + LOGW("Could not request 'inject touch event'"); + } + } +} + +static void +sc_mouse_processor_process_mouse_button(struct sc_mouse_processor *mp, + const SDL_MouseButtonEvent *event) { + struct sc_mouse_inject *mi = DOWNCAST(mp); + + struct control_msg msg; + if (convert_mouse_button(event, mi->screen, &msg)) { + if (!controller_push_msg(mi->controller, &msg)) { + LOGW("Could not request 'inject mouse button event'"); + } + } +} + +static void +sc_mouse_processor_process_mouse_wheel(struct sc_mouse_processor *mp, + const SDL_MouseWheelEvent *event) { + struct sc_mouse_inject *mi = DOWNCAST(mp); + + struct control_msg msg; + if (convert_mouse_wheel(event, mi->screen, &msg)) { + if (!controller_push_msg(mi->controller, &msg)) { + LOGW("Could not request 'inject mouse wheel event'"); + } + } +} + +void +sc_mouse_inject_init(struct sc_mouse_inject *mi, struct controller *controller, + struct screen *screen) { + mi->controller = controller; + mi->screen = screen; + + static const struct sc_mouse_processor_ops ops = { + .process_mouse_motion = sc_mouse_processor_process_mouse_motion, + .process_touch = sc_mouse_processor_process_touch, + .process_mouse_button = sc_mouse_processor_process_mouse_button, + .process_mouse_wheel = sc_mouse_processor_process_mouse_wheel, + }; + + mi->mouse_processor.ops = &ops; +} diff --git a/app/src/mouse_inject.h b/app/src/mouse_inject.h new file mode 100644 index 00000000..d5220db9 --- /dev/null +++ b/app/src/mouse_inject.h @@ -0,0 +1,24 @@ +#ifndef SC_MOUSE_INJECT_H +#define SC_MOUSE_INJECT_H + +#include "common.h" + +#include + +#include "controller.h" +#include "scrcpy.h" +#include "screen.h" +#include "trait/mouse_processor.h" + +struct sc_mouse_inject { + struct sc_mouse_processor mouse_processor; // mouse processor trait + + struct controller *controller; + struct screen *screen; +}; + +void +sc_mouse_inject_init(struct sc_mouse_inject *mi, struct controller *controller, + struct screen *screen); + +#endif diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index def96bb7..50250e2c 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -19,6 +19,7 @@ #include "file_handler.h" #include "input_manager.h" #include "keyboard_inject.h" +#include "mouse_inject.h" #include "recorder.h" #include "screen.h" #include "server.h" @@ -41,6 +42,7 @@ struct scrcpy { struct controller controller; struct file_handler file_handler; struct sc_keyboard_inject keyboard_inject; + struct sc_mouse_inject mouse_inject; struct input_manager input_manager; }; @@ -414,13 +416,17 @@ scrcpy(const struct scrcpy_options *options) { stream_started = true; struct sc_key_processor *kp = NULL; + struct sc_mouse_processor *mp = NULL; if (options->control) { sc_keyboard_inject_init(&s->keyboard_inject, &s->controller, options); kp = &s->keyboard_inject.key_processor; + + sc_mouse_inject_init(&s->mouse_inject, &s->controller, &s->screen); + mp = &s->mouse_inject.mouse_processor; } - input_manager_init(&s->input_manager, &s->controller, &s->screen, kp, + input_manager_init(&s->input_manager, &s->controller, &s->screen, kp, mp, options); ret = event_loop(s, options); diff --git a/app/src/trait/mouse_processor.h b/app/src/trait/mouse_processor.h new file mode 100644 index 00000000..f3548574 --- /dev/null +++ b/app/src/trait/mouse_processor.h @@ -0,0 +1,39 @@ +#ifndef SC_MOUSE_PROCESSOR_H +#define SC_MOUSE_PROCESSOR_H + +#include "common.h" + +#include +#include + +#include + +/** + * Mouse processor trait. + * + * Component able to process and inject mouse events should implement this + * trait. + */ +struct sc_mouse_processor { + const struct sc_mouse_processor_ops *ops; +}; + +struct sc_mouse_processor_ops { + void + (*process_mouse_motion)(struct sc_mouse_processor *mp, + const SDL_MouseMotionEvent *event); + + void + (*process_touch)(struct sc_mouse_processor *mp, + const SDL_TouchFingerEvent *event); + + void + (*process_mouse_button)(struct sc_mouse_processor *mp, + const SDL_MouseButtonEvent *event); + + void + (*process_mouse_wheel)(struct sc_mouse_processor *mp, + const SDL_MouseWheelEvent *event); +}; + +#endif From 207082977a01b931c4451c1954c8197878c3ba10 Mon Sep 17 00:00:00 2001 From: Alynx Zhou Date: Fri, 10 Sep 2021 18:57:35 +0800 Subject: [PATCH 0698/2244] Add support for USB HID keyboard over AOAv2 This provides a better input experience, by simulating a physical keyboard. It converts SDL keyboard events to proper HID events, and send them over AOAv2. This is a rewriting and bugfix of the origin code from @amosbird: The feature is enabled the command line option -K or --hid-keyboard, and is only available on Linux, over USB. Refs Refs PR #2632 Signed-off-by: Romain Vimont --- BUILD.md | 10 +- README.md | 39 +++++ app/meson.build | 16 +- app/scrcpy.1 | 8 + app/src/aoa_hid.c | 368 +++++++++++++++++++++++++++++++++++++++++ app/src/aoa_hid.h | 65 ++++++++ app/src/cli.c | 14 +- app/src/hid_keyboard.c | 302 +++++++++++++++++++++++++++++++++ app/src/hid_keyboard.h | 42 +++++ app/src/scrcpy.c | 77 ++++++++- app/src/scrcpy.h | 9 +- 11 files changed, 939 insertions(+), 11 deletions(-) create mode 100644 app/src/aoa_hid.c create mode 100644 app/src/aoa_hid.h create mode 100644 app/src/hid_keyboard.c create mode 100644 app/src/hid_keyboard.h diff --git a/BUILD.md b/BUILD.md index 69475f2c..5edb5a21 100644 --- a/BUILD.md +++ b/BUILD.md @@ -14,7 +14,8 @@ First, you need to install the required packages: # for Debian/Ubuntu sudo apt install ffmpeg libsdl2-2.0-0 adb wget \ gcc git pkg-config meson ninja-build libsdl2-dev \ - libavcodec-dev libavdevice-dev libavformat-dev libavutil-dev + libavcodec-dev libavdevice-dev libavformat-dev libavutil-dev \ + libusb-1.0-0 libusb-dev ``` Then clone the repo and execute the installation script @@ -88,11 +89,12 @@ Install the required packages from your package manager. ```bash # runtime dependencies -sudo apt install ffmpeg libsdl2-2.0-0 adb +sudo apt install ffmpeg libsdl2-2.0-0 adb libusb-1.0-0 # client build dependencies sudo apt install gcc git pkg-config meson ninja-build libsdl2-dev \ - libavcodec-dev libavdevice-dev libavformat-dev libavutil-dev + libavcodec-dev libavdevice-dev libavformat-dev libavutil-dev \ + libusb-dev # server build dependencies sudo apt install openjdk-11-jdk @@ -114,7 +116,7 @@ pip3 install meson sudo dnf install https://download1.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm # client build dependencies -sudo dnf install SDL2-devel ffms2-devel meson gcc make +sudo dnf install SDL2-devel ffms2-devel libusb-devel meson gcc make # server build dependencies sudo dnf install java-devel diff --git a/README.md b/README.md index 84dd5ff5..fdd74087 100644 --- a/README.md +++ b/README.md @@ -673,6 +673,39 @@ 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. +#### Physical keyboard simulation (HID) + +By default, scrcpy uses Android key or text injection: it works everywhere, but +is limited to ASCII. + +On Linux, scrcpy can simulate a USB physical keyboard on Android to provide a +better input experience (using [USB HID over AOAv2][hid-aoav2]): the virtual +keyboard is disabled and it works for all characters and IME. + +[hid-aoav2]: https://source.android.com/devices/accessories/aoa2#hid-support + +However, it only works if the device is connected by USB, and is currently only +supported on Linux. + +To enable this mode: + +```bash +scrcpy --hid-keyboard +scrcpy -K # short version +``` + +If it fails for some reason (for example because the device is not connected via +USB), it automatically fallbacks to the default mode (with a log in the +console). This allows to use the same command line options when connected over +USB and TCP/IP. + +In this mode, raw key events (scancodes) are sent to the device, independently +of the host key mapping. Therefore, if your keyboard layout does not match, it +must be configured on the Android device, in Settings → System → Languages and +input → [Physical keyboard]. + +[Physical keyboard]: https://github.com/Genymobile/scrcpy/pull/2632#issuecomment-923756915 + #### Text injection preference @@ -692,6 +725,9 @@ scrcpy --prefer-text (but this will break keyboard behavior in games) +This option has no effect on HID keyboard (all key events are sent as +scancodes in this mode). + [textevents]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-text-input [prefertext]: https://github.com/Genymobile/scrcpy/issues/650#issuecomment-512945343 @@ -707,6 +743,9 @@ To avoid forwarding repeated key events: scrcpy --no-key-repeat ``` +This option has no effect on HID keyboard (key repeat is handled by Android +directly in this mode). + #### Right-click and middle-click diff --git a/app/meson.build b/app/meson.build index 46d3994f..f82d37b3 100644 --- a/app/meson.build +++ b/app/meson.build @@ -42,6 +42,14 @@ if v4l2_support src += [ 'src/v4l2_sink.c' ] endif +aoa_hid_support = host_machine.system() == 'linux' +if aoa_hid_support + src += [ + 'src/aoa_hid.c', + 'src/hid_keyboard.c', + ] +endif + check_functions = [ 'strdup' ] @@ -62,8 +70,11 @@ if not get_option('crossbuild_windows') dependencies += dependency('libavdevice') endif -else + if aoa_hid_support + dependencies += dependency('libusb-1.0') + endif +else # cross-compile mingw32 build (from Linux to Windows) prebuilt_sdl2 = meson.get_cross_property('prebuilt_sdl2') sdl2_bin_dir = meson.current_source_dir() + '/../prebuilt-deps/' + prebuilt_sdl2 + '/bin' @@ -140,6 +151,9 @@ conf.set('SERVER_DEBUGGER_METHOD_NEW', get_option('server_debugger_method') == ' # enable V4L2 support (linux only) conf.set('HAVE_V4L2', v4l2_support) +# enable HID over AOA support (linux only) +conf.set('HAVE_AOA_HID', aoa_hid_support) + configure_file(configuration: conf, output: 'config.h') src_dir = include_directories('src') diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 1b69a065..46db6e1d 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -82,6 +82,14 @@ Start in fullscreen. .B \-h, \-\-help Print this help. +.TP +.B \-K, \-\-hid\-keyboard +Simulate a physical keyboard by using HID over AOAv2. + +This provides a better experience for IME users, and allows to generate non-ASCII characters, contrary to the default injection method. + +It may only work over USB, and is currently only supported on Linux. + .TP .B \-\-legacy\-paste Inject computer clipboard text as a sequence of key events on Ctrl+v (like MOD+Shift+v). diff --git a/app/src/aoa_hid.c b/app/src/aoa_hid.c new file mode 100644 index 00000000..7fc0f34c --- /dev/null +++ b/app/src/aoa_hid.c @@ -0,0 +1,368 @@ +#include "util/log.h" + +#include +#include + +#include "aoa_hid.h" + +// See . +#define ACCESSORY_REGISTER_HID 54 +#define ACCESSORY_SET_HID_REPORT_DESC 56 +#define ACCESSORY_SEND_HID_EVENT 57 +#define ACCESSORY_UNREGISTER_HID 55 + +#define DEFAULT_TIMEOUT 1000 + +static void +sc_hid_event_log(const struct sc_hid_event *event) { + // HID Event: [00] FF FF FF FF... + assert(event->size); + unsigned buffer_size = event->size * 3 + 1; + char *buffer = malloc(buffer_size); + if (!buffer) { + return; + } + for (unsigned i = 0; i < event->size; ++i) { + snprintf(buffer + i * 3, 4, " %02x", event->buffer[i]); + } + LOGV("HID Event: [%d]%s", event->accessory_id, buffer); + free(buffer); +} + +void +sc_hid_event_init(struct sc_hid_event *hid_event, uint16_t accessory_id, + unsigned char *buffer, uint16_t buffer_size) { + hid_event->accessory_id = accessory_id; + hid_event->buffer = buffer; + hid_event->size = buffer_size; +} + +void +sc_hid_event_destroy(struct sc_hid_event *hid_event) { + free(hid_event->buffer); +} + +static inline void +log_libusb_error(enum libusb_error errcode) { + LOGW("libusb error: %s", libusb_strerror(errcode)); +} + +static bool +accept_device(libusb_device *device, const char *serial) { + // do not log any USB error in this function, it is expected that many USB + // devices available on the computer have permission restrictions + + struct libusb_device_descriptor desc; + libusb_get_device_descriptor(device, &desc); + + if (!desc.iSerialNumber) { + return false; + } + + libusb_device_handle *handle; + int result = libusb_open(device, &handle); + if (result < 0) { + return false; + } + + char buffer[128]; + result = libusb_get_string_descriptor_ascii(handle, desc.iSerialNumber, + (unsigned char *) buffer, + sizeof(buffer)); + libusb_close(handle); + if (result < 0) { + return false; + } + + buffer[sizeof(buffer) - 1] = '\0'; // just in case + + // accept the device if its serial matches + return !strcmp(buffer, serial); +} + +static libusb_device * +sc_aoa_find_usb_device(const char *serial) { + if (!serial) { + return NULL; + } + + libusb_device **list; + libusb_device *result = NULL; + ssize_t count = libusb_get_device_list(NULL, &list); + if (count < 0) { + log_libusb_error((enum libusb_error) count); + return NULL; + } + + for (size_t i = 0; i < (size_t) count; ++i) { + libusb_device *device = list[i]; + + if (accept_device(device, serial)) { + result = libusb_ref_device(device); + break; + } + } + libusb_free_device_list(list, 1); + return result; +} + +static int +sc_aoa_open_usb_handle(libusb_device *device, libusb_device_handle **handle) { + int result = libusb_open(device, handle); + if (result < 0) { + log_libusb_error((enum libusb_error) result); + return result; + } + return 0; +} + +bool +sc_aoa_init(struct sc_aoa *aoa, const char *serial) { + cbuf_init(&aoa->queue); + + if (!sc_mutex_init(&aoa->mutex)) { + return false; + } + + if (!sc_cond_init(&aoa->event_cond)) { + sc_mutex_destroy(&aoa->mutex); + return false; + } + + if (libusb_init(&aoa->usb_context) != LIBUSB_SUCCESS) { + sc_cond_destroy(&aoa->event_cond); + sc_mutex_destroy(&aoa->mutex); + return false; + } + + aoa->usb_device = sc_aoa_find_usb_device(serial); + if (!aoa->usb_device) { + LOGW("USB device of serial %s not found", serial); + libusb_exit(aoa->usb_context); + sc_mutex_destroy(&aoa->mutex); + sc_cond_destroy(&aoa->event_cond); + return false; + } + + if (sc_aoa_open_usb_handle(aoa->usb_device, &aoa->usb_handle) < 0) { + LOGW("Open USB handle failed"); + libusb_unref_device(aoa->usb_device); + libusb_exit(aoa->usb_context); + sc_cond_destroy(&aoa->event_cond); + sc_mutex_destroy(&aoa->mutex); + return false; + } + + aoa->stopped = false; + + return true; +} + +void +sc_aoa_destroy(struct sc_aoa *aoa) { + // Destroy remaining events + struct sc_hid_event event; + while (cbuf_take(&aoa->queue, &event)) { + sc_hid_event_destroy(&event); + } + + libusb_close(aoa->usb_handle); + libusb_unref_device(aoa->usb_device); + libusb_exit(aoa->usb_context); + sc_cond_destroy(&aoa->event_cond); + sc_mutex_destroy(&aoa->mutex); +} + +static bool +sc_aoa_register_hid(struct sc_aoa *aoa, uint16_t accessory_id, + uint16_t report_desc_size) { + uint8_t request_type = LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR; + uint8_t request = ACCESSORY_REGISTER_HID; + // + // value (arg0): accessory assigned ID for the HID device + // index (arg1): total length of the HID report descriptor + uint16_t value = accessory_id; + uint16_t index = report_desc_size; + unsigned char *buffer = NULL; + uint16_t length = 0; + int result = libusb_control_transfer(aoa->usb_handle, request_type, request, + value, index, buffer, length, + DEFAULT_TIMEOUT); + if (result < 0) { + log_libusb_error((enum libusb_error) result); + return false; + } + + return true; +} + +static bool +sc_aoa_set_hid_report_desc(struct sc_aoa *aoa, uint16_t accessory_id, + const unsigned char *report_desc, + uint16_t report_desc_size) { + uint8_t request_type = LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR; + uint8_t request = ACCESSORY_SET_HID_REPORT_DESC; + /** + * If the HID descriptor is longer than the endpoint zero max packet size, + * the descriptor will be sent in multiple ACCESSORY_SET_HID_REPORT_DESC + * commands. The data for the descriptor must be sent sequentially + * if multiple packets are needed. + * + * + * libusb handles packet abstraction internally, so we don't need to care + * about bMaxPacketSize0 here. + * + * See + */ + // value (arg0): accessory assigned ID for the HID device + // index (arg1): offset of data (buffer) in descriptor + uint16_t value = accessory_id; + uint16_t index = 0; + // libusb_control_transfer expects a pointer to non-const + unsigned char *buffer = (unsigned char *) report_desc; + uint16_t length = report_desc_size; + int result = libusb_control_transfer(aoa->usb_handle, request_type, request, + value, index, buffer, length, + DEFAULT_TIMEOUT); + if (result < 0) { + log_libusb_error((enum libusb_error) result); + return false; + } + + return true; +} + +bool +sc_aoa_setup_hid(struct sc_aoa *aoa, uint16_t accessory_id, + const unsigned char *report_desc, uint16_t report_desc_size) { + bool ok = sc_aoa_register_hid(aoa, accessory_id, report_desc_size); + if (!ok) { + return false; + } + + ok = sc_aoa_set_hid_report_desc(aoa, accessory_id, report_desc, + report_desc_size); + if (!ok) { + if (!sc_aoa_unregister_hid(aoa, accessory_id)) { + LOGW("Could not unregister HID"); + } + return false; + } + + return true; +} + +static bool +sc_aoa_send_hid_event(struct sc_aoa *aoa, const struct sc_hid_event *event) { + uint8_t request_type = LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR; + uint8_t request = ACCESSORY_SEND_HID_EVENT; + // + // value (arg0): accessory assigned ID for the HID device + // index (arg1): 0 (unused) + uint16_t value = event->accessory_id; + uint16_t index = 0; + unsigned char *buffer = event->buffer; + uint16_t length = event->size; + int result = libusb_control_transfer(aoa->usb_handle, request_type, request, + value, index, buffer, length, + DEFAULT_TIMEOUT); + if (result < 0) { + log_libusb_error((enum libusb_error) result); + return false; + } + + return true; +} + +bool +sc_aoa_unregister_hid(struct sc_aoa *aoa, const uint16_t accessory_id) { + uint8_t request_type = LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR; + uint8_t request = ACCESSORY_UNREGISTER_HID; + // + // value (arg0): accessory assigned ID for the HID device + // index (arg1): 0 + uint16_t value = accessory_id; + uint16_t index = 0; + unsigned char *buffer = NULL; + uint16_t length = 0; + int result = libusb_control_transfer(aoa->usb_handle, request_type, request, + value, index, buffer, length, + DEFAULT_TIMEOUT); + if (result < 0) { + log_libusb_error((enum libusb_error) result); + return false; + } + + return true; +} + +bool +sc_aoa_push_hid_event(struct sc_aoa *aoa, const struct sc_hid_event *event) { + if (sc_get_log_level() <= SC_LOG_LEVEL_VERBOSE) { + sc_hid_event_log(event); + } + + sc_mutex_lock(&aoa->mutex); + bool was_empty = cbuf_is_empty(&aoa->queue); + bool res = cbuf_push(&aoa->queue, *event); + if (was_empty) { + sc_cond_signal(&aoa->event_cond); + } + sc_mutex_unlock(&aoa->mutex); + return res; +} + +static int +run_aoa_thread(void *data) { + struct sc_aoa *aoa = data; + + for (;;) { + sc_mutex_lock(&aoa->mutex); + while (!aoa->stopped && cbuf_is_empty(&aoa->queue)) { + sc_cond_wait(&aoa->event_cond, &aoa->mutex); + } + if (aoa->stopped) { + // Stop immediately, do not process further events + sc_mutex_unlock(&aoa->mutex); + break; + } + struct sc_hid_event event; + bool non_empty = cbuf_take(&aoa->queue, &event); + assert(non_empty); + (void) non_empty; + sc_mutex_unlock(&aoa->mutex); + + bool ok = sc_aoa_send_hid_event(aoa, &event); + sc_hid_event_destroy(&event); + if (!ok) { + LOGW("Could not send HID event to USB device"); + } + } + return 0; +} + +bool +sc_aoa_start(struct sc_aoa *aoa) { + LOGD("Starting AOA thread"); + + bool ok = sc_thread_create(&aoa->thread, run_aoa_thread, "aoa_thread", aoa); + if (!ok) { + LOGC("Could not start AOA thread"); + return false; + } + + return true; +} + +void +sc_aoa_stop(struct sc_aoa *aoa) { + sc_mutex_lock(&aoa->mutex); + aoa->stopped = true; + sc_cond_signal(&aoa->event_cond); + sc_mutex_unlock(&aoa->mutex); +} + +void +sc_aoa_join(struct sc_aoa *aoa) { + sc_thread_join(&aoa->thread, NULL); +} diff --git a/app/src/aoa_hid.h b/app/src/aoa_hid.h new file mode 100644 index 00000000..11b879ce --- /dev/null +++ b/app/src/aoa_hid.h @@ -0,0 +1,65 @@ +#ifndef SC_AOA_HID_H +#define SC_AOA_HID_H + +#include +#include + +#include + +#include "scrcpy.h" +#include "util/cbuf.h" +#include "util/thread.h" + +struct sc_hid_event { + uint16_t accessory_id; + unsigned char *buffer; + uint16_t size; +}; + +// Takes ownership of buffer +void +sc_hid_event_init(struct sc_hid_event *hid_event, uint16_t accessory_id, + unsigned char *buffer, uint16_t buffer_size); + +void +sc_hid_event_destroy(struct sc_hid_event *hid_event); + +struct sc_hid_event_queue CBUF(struct sc_hid_event, 64); + +struct sc_aoa { + libusb_context *usb_context; + libusb_device *usb_device; + libusb_device_handle *usb_handle; + sc_thread thread; + sc_mutex mutex; + sc_cond event_cond; + bool stopped; + struct sc_hid_event_queue queue; +}; + +bool +sc_aoa_init(struct sc_aoa *aoa, const char *serial); + +void +sc_aoa_destroy(struct sc_aoa *aoa); + +bool +sc_aoa_start(struct sc_aoa *aoa); + +void +sc_aoa_stop(struct sc_aoa *aoa); + +void +sc_aoa_join(struct sc_aoa *aoa); + +bool +sc_aoa_setup_hid(struct sc_aoa *aoa, uint16_t accessory_id, + const unsigned char *report_desc, uint16_t report_desc_size); + +bool +sc_aoa_unregister_hid(struct sc_aoa *aoa, uint16_t accessory_id); + +bool +sc_aoa_push_hid_event(struct sc_aoa *aoa, const struct sc_hid_event *event); + +#endif diff --git a/app/src/cli.c b/app/src/cli.c index d22096ca..a0a508af 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -76,6 +76,14 @@ scrcpy_print_usage(const char *arg0) { " -f, --fullscreen\n" " Start in fullscreen.\n" "\n" + " -K, --hid-keyboard\n" + " Simulate a physical keyboard by using HID over AOAv2.\n" + " It provides a better experience for IME users, and allows to\n" + " generate non-ASCII characters, contrary to the default\n" + " injection method.\n" + " It may only work over USB, and is currently only supported\n" + " on Linux.\n" + "\n" " -h, --help\n" " Print this help.\n" "\n" @@ -738,6 +746,7 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { OPT_FORWARD_ALL_CLICKS}, {"fullscreen", no_argument, NULL, 'f'}, {"help", no_argument, NULL, 'h'}, + {"hid-keyboard", no_argument, NULL, 'K'}, {"legacy-paste", no_argument, NULL, OPT_LEGACY_PASTE}, {"lock-video-orientation", optional_argument, NULL, OPT_LOCK_VIDEO_ORIENTATION}, @@ -784,7 +793,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:StTvV:w", + while ((c = getopt_long(argc, argv, "b:c:fF:hKm:nNp:r:s:StTvV:w", long_options, NULL)) != -1) { switch (c) { case 'b': @@ -817,6 +826,9 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { case 'h': args->help = true; break; + case 'K': + opts->keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_HID; + break; case OPT_MAX_FPS: if (!parse_max_fps(optarg, &opts->max_fps)) { return false; diff --git a/app/src/hid_keyboard.c b/app/src/hid_keyboard.c new file mode 100644 index 00000000..2599e1d2 --- /dev/null +++ b/app/src/hid_keyboard.c @@ -0,0 +1,302 @@ +#include "hid_keyboard.h" + +#include +#include + +#include "util/log.h" + +/** Downcast key processor to hid_keyboard */ +#define DOWNCAST(KP) container_of(KP, struct sc_hid_keyboard, key_processor) + +#define HID_KEYBOARD_ACCESSORY_ID 1 + +#define HID_MODIFIER_NONE 0x00 +#define HID_MODIFIER_LEFT_CONTROL (1 << 0) +#define HID_MODIFIER_LEFT_SHIFT (1 << 1) +#define HID_MODIFIER_LEFT_ALT (1 << 2) +#define HID_MODIFIER_LEFT_GUI (1 << 3) +#define HID_MODIFIER_RIGHT_CONTROL (1 << 4) +#define HID_MODIFIER_RIGHT_SHIFT (1 << 5) +#define HID_MODIFIER_RIGHT_ALT (1 << 6) +#define HID_MODIFIER_RIGHT_GUI (1 << 7) + +#define HID_KEYBOARD_INDEX_MODIFIER 0 +#define HID_KEYBOARD_INDEX_KEYS 2 + +// USB HID protocol says 6 keys in an event is the requirement for BIOS +// keyboard support, though OS could support more keys via modifying the report +// desc. 6 should be enough for scrcpy. +#define HID_KEYBOARD_MAX_KEYS 6 +#define HID_KEYBOARD_EVENT_SIZE (2 + HID_KEYBOARD_MAX_KEYS) + +#define HID_RESERVED 0x00 +#define HID_ERROR_ROLL_OVER 0x01 + +/** + * For HID over AOAv2, only report descriptor is needed. + * + * The specification is available here: + * + * + * In particular, read: + * - 6.2.2 Report Descriptor + * - Appendix B.1 Protocol 1 (Keyboard) + * - Appendix C: Keyboard Implementation + * + * Normally a basic HID keyboard uses 8 bytes: + * Modifier Reserved Key Key Key Key Key Key + * + * You can dump your device's report descriptor with: + * + * sudo usbhid-dump -m vid:pid -e descriptor + * + * (change vid:pid' to your device's vendor ID and product ID). + */ +static const unsigned char keyboard_report_desc[] = { + // Usage Page (Generic Desktop) + 0x05, 0x01, + // Usage (Keyboard) + 0x09, 0x06, + + // Collection (Application) + 0xA1, 0x01, + + // Usage Page (Key Codes) + 0x05, 0x07, + // Usage Minimum (224) + 0x19, 0xE0, + // Usage Maximum (231) + 0x29, 0xE7, + // Logical Minimum (0) + 0x15, 0x00, + // Logical Maximum (1) + 0x25, 0x01, + // Report Size (1) + 0x75, 0x01, + // Report Count (8) + 0x95, 0x08, + // Input (Data, Variable, Absolute): Modifier byte + 0x81, 0x02, + + // Report Size (8) + 0x75, 0x08, + // Report Count (1) + 0x95, 0x01, + // Input (Constant): Reserved byte + 0x81, 0x01, + + // Usage Page (LEDs) + 0x05, 0x08, + // Usage Minimum (1) + 0x19, 0x01, + // Usage Maximum (5) + 0x29, 0x05, + // Report Size (1) + 0x75, 0x01, + // Report Count (5) + 0x95, 0x05, + // Output (Data, Variable, Absolute): LED report + 0x91, 0x02, + + // Report Size (3) + 0x75, 0x03, + // Report Count (1) + 0x95, 0x01, + // Output (Constant): LED report padding + 0x91, 0x01, + + // Usage Page (Key Codes) + 0x05, 0x07, + // Usage Minimum (0) + 0x19, 0x00, + // Usage Maximum (101) + 0x29, SC_HID_KEYBOARD_KEYS - 1, + // Logical Minimum (0) + 0x15, 0x00, + // Logical Maximum(101) + 0x25, SC_HID_KEYBOARD_KEYS - 1, + // Report Size (8) + 0x75, 0x08, + // Report Count (6) + 0x95, HID_KEYBOARD_MAX_KEYS, + // Input (Data, Array): Keys + 0x81, 0x00, + + // End Collection + 0xC0 +}; + +static unsigned char +sdl_keymod_to_hid_modifiers(SDL_Keymod mod) { + unsigned char modifiers = HID_MODIFIER_NONE; + if (mod & KMOD_LCTRL) { + modifiers |= HID_MODIFIER_LEFT_CONTROL; + } + if (mod & KMOD_LSHIFT) { + modifiers |= HID_MODIFIER_LEFT_SHIFT; + } + if (mod & KMOD_LALT) { + modifiers |= HID_MODIFIER_LEFT_ALT; + } + if (mod & KMOD_LGUI) { + modifiers |= HID_MODIFIER_LEFT_GUI; + } + if (mod & KMOD_RCTRL) { + modifiers |= HID_MODIFIER_RIGHT_CONTROL; + } + if (mod & KMOD_RSHIFT) { + modifiers |= HID_MODIFIER_RIGHT_SHIFT; + } + if (mod & KMOD_RALT) { + modifiers |= HID_MODIFIER_RIGHT_ALT; + } + if (mod & KMOD_RGUI) { + modifiers |= HID_MODIFIER_RIGHT_GUI; + } + return modifiers; +} + +static bool +sc_hid_keyboard_event_init(struct sc_hid_event *hid_event) { + unsigned char *buffer = malloc(HID_KEYBOARD_EVENT_SIZE); + if (!buffer) { + return false; + } + + buffer[HID_KEYBOARD_INDEX_MODIFIER] = HID_MODIFIER_NONE; + buffer[1] = HID_RESERVED; + memset(&buffer[HID_KEYBOARD_INDEX_KEYS], 0, HID_KEYBOARD_MAX_KEYS); + + sc_hid_event_init(hid_event, HID_KEYBOARD_ACCESSORY_ID, buffer, + HID_KEYBOARD_EVENT_SIZE); + return true; +} + +static inline bool +scancode_is_modifier(SDL_Scancode scancode) { + return scancode >= SDL_SCANCODE_LCTRL && scancode <= SDL_SCANCODE_RGUI; +} + +static bool +convert_hid_keyboard_event(struct sc_hid_keyboard *kb, + struct sc_hid_event *hid_event, + const SDL_KeyboardEvent *event) { + SDL_Scancode scancode = event->keysym.scancode; + assert(scancode >= 0); + + // SDL also generates events when only modifiers are pressed, we cannot + // ignore them totally, for example press 'a' first then press 'Control', + // if we ignore 'Control' event, only 'a' is sent. + if (scancode >= SC_HID_KEYBOARD_KEYS && !scancode_is_modifier(scancode)) { + // Scancode to ignore + return false; + } + + if (!sc_hid_keyboard_event_init(hid_event)) { + LOGW("Could not initialize HID keyboard event"); + return false; + } + + unsigned char modifiers = sdl_keymod_to_hid_modifiers(event->keysym.mod); + + if (scancode < SC_HID_KEYBOARD_KEYS) { + // Pressed is true and released is false + kb->keys[scancode] = (event->type == SDL_KEYDOWN); + LOGV("keys[%02x] = %s", scancode, + kb->keys[scancode] ? "true" : "false"); + } + + hid_event->buffer[HID_KEYBOARD_INDEX_MODIFIER] = modifiers; + + unsigned char *keys_buffer = &hid_event->buffer[HID_KEYBOARD_INDEX_KEYS]; + // Re-calculate pressed keys every time + int keys_pressed_count = 0; + for (int i = 0; i < SC_HID_KEYBOARD_KEYS; ++i) { + if (kb->keys[i]) { + // USB HID protocol says that if keys exceeds report count, a + // phantom state should be reported + if (keys_pressed_count >= HID_KEYBOARD_MAX_KEYS) { + // Pantom state: + // - Modifiers + // - Reserved + // - ErrorRollOver * HID_MAX_KEYS + memset(keys_buffer, HID_ERROR_ROLL_OVER, HID_KEYBOARD_MAX_KEYS); + goto end; + } + + keys_buffer[keys_pressed_count] = i; + ++keys_pressed_count; + } + } + +end: + LOGV("hid keyboard: key %-4s scancode=%02x (%u) mod=%02x", + event->type == SDL_KEYDOWN ? "down" : "up", event->keysym.scancode, + event->keysym.scancode, modifiers); + + return true; +} + +static void +sc_key_processor_process_key(struct sc_key_processor *kp, + const SDL_KeyboardEvent *event) { + if (event->repeat) { + // In USB HID protocol, key repeat is handled by the host (Android), so + // just ignore key repeat here. + return; + } + + struct sc_hid_keyboard *kb = DOWNCAST(kp); + + struct sc_hid_event hid_event; + // Not all keys are supported, just ignore unsupported keys + if (convert_hid_keyboard_event(kb, &hid_event, event)) { + if (!sc_aoa_push_hid_event(kb->aoa, &hid_event)) { + sc_hid_event_destroy(&hid_event); + LOGW("Could request HID event"); + } + } +} + +static void +sc_key_processor_process_text(struct sc_key_processor *kp, + const SDL_TextInputEvent *event) { + (void) kp; + (void) event; + + // Never forward text input via HID (all the keys are injected separately) +} + +bool +sc_hid_keyboard_init(struct sc_hid_keyboard *kb, struct sc_aoa *aoa) { + kb->aoa = aoa; + + bool ok = sc_aoa_setup_hid(aoa, HID_KEYBOARD_ACCESSORY_ID, + keyboard_report_desc, + ARRAY_LEN(keyboard_report_desc)); + if (!ok) { + LOGW("Register HID keyboard failed"); + return false; + } + + // Reset all states + memset(kb->keys, false, SC_HID_KEYBOARD_KEYS); + + static const struct sc_key_processor_ops ops = { + .process_key = sc_key_processor_process_key, + .process_text = sc_key_processor_process_text, + }; + + kb->key_processor.ops = &ops; + + return true; +} + +void +sc_hid_keyboard_destroy(struct sc_hid_keyboard *kb) { + // Unregister HID keyboard so the soft keyboard shows again on Android + bool ok = sc_aoa_unregister_hid(kb->aoa, HID_KEYBOARD_ACCESSORY_ID); + if (!ok) { + LOGW("Could not unregister HID keyboard"); + } +} diff --git a/app/src/hid_keyboard.h b/app/src/hid_keyboard.h new file mode 100644 index 00000000..d8276cad --- /dev/null +++ b/app/src/hid_keyboard.h @@ -0,0 +1,42 @@ +#ifndef SC_HID_KEYBOARD_H +#define SC_HID_KEYBOARD_H + +#include "common.h" + +#include + +#include "aoa_hid.h" +#include "trait/key_processor.h" + +// See "SDL2/SDL_scancode.h". +// Maybe SDL_Keycode is used by most people, but SDL_Scancode is taken from USB +// HID protocol. +// 0x65 is Application, typically AT-101 Keyboard ends here. +#define SC_HID_KEYBOARD_KEYS 0x66 + +/** + * HID keyboard events are sequence-based, every time keyboard state changes + * it sends an array of currently pressed keys, the host is responsible for + * compare events and determine which key becomes pressed and which key becomes + * released. In order to convert SDL_KeyboardEvent to HID events, we first use + * an array of keys to save each keys' state. And when a SDL_KeyboardEvent was + * emitted, we updated our state, and then we use a loop to generate HID + * events. The sequence of array elements is unimportant and when too much keys + * pressed at the same time (more than report count), we should generate + * phantom state. Don't forget that modifiers should be updated too, even for + * phantom state. + */ +struct sc_hid_keyboard { + struct sc_key_processor key_processor; // key processor trait + + struct sc_aoa *aoa; + bool keys[SC_HID_KEYBOARD_KEYS]; +}; + +bool +sc_hid_keyboard_init(struct sc_hid_keyboard *kb, struct sc_aoa *aoa); + +void +sc_hid_keyboard_destroy(struct sc_hid_keyboard *kb); + +#endif diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 50250e2c..c4041562 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -18,6 +18,9 @@ #include "events.h" #include "file_handler.h" #include "input_manager.h" +#ifdef HAVE_AOA_HID +# include "hid_keyboard.h" +#endif #include "keyboard_inject.h" #include "mouse_inject.h" #include "recorder.h" @@ -41,7 +44,15 @@ struct scrcpy { #endif struct controller controller; struct file_handler file_handler; - struct sc_keyboard_inject keyboard_inject; +#ifdef HAVE_AOA_HID + struct sc_aoa aoa; +#endif + union { + struct sc_keyboard_inject keyboard_inject; +#ifdef HAVE_AOA_HID + struct sc_hid_keyboard keyboard_hid; +#endif + }; struct sc_mouse_inject mouse_inject; struct input_manager input_manager; }; @@ -243,7 +254,7 @@ stream_on_eos(struct stream *stream, void *userdata) { } bool -scrcpy(const struct scrcpy_options *options) { +scrcpy(struct scrcpy_options *options) { static struct scrcpy scrcpy; struct scrcpy *s = &scrcpy; @@ -260,6 +271,9 @@ scrcpy(const struct scrcpy_options *options) { bool v4l2_sink_initialized = false; #endif bool stream_started = false; +#ifdef HAVE_AOA_HID + bool aoa_hid_initialized = false; +#endif bool controller_initialized = false; bool controller_started = false; bool screen_initialized = false; @@ -419,8 +433,50 @@ scrcpy(const struct scrcpy_options *options) { struct sc_mouse_processor *mp = NULL; if (options->control) { - sc_keyboard_inject_init(&s->keyboard_inject, &s->controller, options); - kp = &s->keyboard_inject.key_processor; + if (options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_HID) { +#ifdef HAVE_AOA_HID + bool aoa_hid_ok = false; + if (!sc_aoa_init(&s->aoa, options->serial)) { + goto aoa_hid_end; + } + + if (!sc_hid_keyboard_init(&s->keyboard_hid, &s->aoa)) { + sc_aoa_destroy(&s->aoa); + goto aoa_hid_end; + } + + if (!sc_aoa_start(&s->aoa)) { + sc_hid_keyboard_destroy(&s->keyboard_hid); + sc_aoa_destroy(&s->aoa); + goto aoa_hid_end; + } + + aoa_hid_ok = true; + kp = &s->keyboard_hid.key_processor; + + aoa_hid_initialized = true; + +aoa_hid_end: + if (!aoa_hid_ok) { + LOGE("Failed to enable HID over AOA, " + "fallback to default keyboard injection method " + "(-K/--hid-keyboard ignored)"); + options->keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_INJECT; + } +#else + LOGE("HID over AOA is not supported on this platform, " + "fallback to default keyboard injection method " + "(-K/--hid-keyboard ignored)"); + options->keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_INJECT; +#endif + } + + // keyboard_input_mode may have been reset if HID mode failed + if (options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_INJECT) { + sc_keyboard_inject_init(&s->keyboard_inject, &s->controller, + options); + kp = &s->keyboard_inject.key_processor; + } sc_mouse_inject_init(&s->mouse_inject, &s->controller, &s->screen); mp = &s->mouse_inject.mouse_processor; @@ -439,6 +495,12 @@ scrcpy(const struct scrcpy_options *options) { end: // The stream is not stopped explicitly, because it will stop by itself on // end-of-stream +#ifdef HAVE_AOA_HID + if (aoa_hid_initialized) { + sc_hid_keyboard_destroy(&s->keyboard_hid); + sc_aoa_stop(&s->aoa); + } +#endif if (controller_started) { controller_stop(&s->controller); } @@ -466,6 +528,13 @@ end: } #endif +#ifdef HAVE_AOA_HID + if (aoa_hid_initialized) { + sc_aoa_join(&s->aoa); + sc_aoa_destroy(&s->aoa); + } +#endif + // Destroy the screen only after the stream is guaranteed to be finished, // because otherwise the screen could receive new frames after destruction if (screen_initialized) { diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index 8b76fb25..8cf4a917 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -33,6 +33,11 @@ enum sc_lock_video_orientation { SC_LOCK_VIDEO_ORIENTATION_3, }; +enum sc_keyboard_input_mode { + SC_KEYBOARD_INPUT_MODE_INJECT, + SC_KEYBOARD_INPUT_MODE_HID, +}; + #define SC_MAX_SHORTCUT_MODS 8 enum sc_shortcut_mod { @@ -68,6 +73,7 @@ struct scrcpy_options { const char *v4l2_device; enum sc_log_level log_level; enum sc_record_format record_format; + enum sc_keyboard_input_mode keyboard_input_mode; struct sc_port_range port_range; struct sc_shortcut_mods shortcut_mods; uint16_t max_size; @@ -112,6 +118,7 @@ struct scrcpy_options { .v4l2_device = NULL, \ .log_level = SC_LOG_LEVEL_INFO, \ .record_format = SC_RECORD_FORMAT_AUTO, \ + .keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_INJECT, \ .port_range = { \ .first = DEFAULT_LOCAL_PORT_RANGE_FIRST, \ .last = DEFAULT_LOCAL_PORT_RANGE_LAST, \ @@ -151,6 +158,6 @@ struct scrcpy_options { } bool -scrcpy(const struct scrcpy_options *options); +scrcpy(struct scrcpy_options *options); #endif From eaf4afaad9c4217c04f053860d7e587ead8b49ca Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 14 Nov 2020 22:12:07 +0100 Subject: [PATCH 0699/2244] Add command execution with redirection Expose command execution with pipes to stdin, stdout and stderr. This will allow to read the result of adb commands. --- app/src/sys/unix/process.c | 173 +++++++++++++++++++++++++++++-------- app/src/sys/win/process.c | 119 +++++++++++++++++++++++-- app/src/util/process.h | 16 ++++ 3 files changed, 265 insertions(+), 43 deletions(-) diff --git a/app/src/sys/unix/process.c b/app/src/sys/unix/process.c index 8683a2da..451b6491 100644 --- a/app/src/sys/unix/process.c +++ b/app/src/sys/unix/process.c @@ -1,5 +1,6 @@ #include "util/process.h" +#include #include #include #include @@ -50,65 +51,151 @@ search_executable(const char *file) { } enum process_result -process_execute(const char *const argv[], pid_t *pid) { - int fd[2]; +process_execute_redirect(const char *const argv[], pid_t *pid, int *pipe_stdin, + int *pipe_stdout, int *pipe_stderr) { + int in[2]; + int out[2]; + int err[2]; + int internal[2]; // communication between parent and children - if (pipe(fd) == -1) { + if (pipe(internal) == -1) { perror("pipe"); return PROCESS_ERROR_GENERIC; } - - enum process_result ret = PROCESS_SUCCESS; + if (pipe_stdin) { + if (pipe(in) == -1) { + perror("pipe"); + close(internal[0]); + close(internal[1]); + return PROCESS_ERROR_GENERIC; + } + } + if (pipe_stdout) { + if (pipe(out) == -1) { + perror("pipe"); + // clean up + if (pipe_stdin) { + close(in[0]); + close(in[1]); + } + close(internal[0]); + close(internal[1]); + return PROCESS_ERROR_GENERIC; + } + } + if (pipe_stderr) { + if (pipe(err) == -1) { + perror("pipe"); + // clean up + if (pipe_stdout) { + close(out[0]); + close(out[1]); + } + if (pipe_stdin) { + close(in[0]); + close(in[1]); + } + close(internal[0]); + close(internal[1]); + return PROCESS_ERROR_GENERIC; + } + } *pid = fork(); if (*pid == -1) { perror("fork"); - ret = PROCESS_ERROR_GENERIC; - goto end; + // clean up + if (pipe_stderr) { + close(err[0]); + close(err[1]); + } + if (pipe_stdout) { + close(out[0]); + close(out[1]); + } + if (pipe_stdin) { + close(in[0]); + close(in[1]); + } + close(internal[0]); + close(internal[1]); + return PROCESS_ERROR_GENERIC; } - if (*pid > 0) { - // parent close write side - close(fd[1]); - fd[1] = -1; - // wait for EOF or receive errno from child - if (read(fd[0], &ret, sizeof(ret)) == -1) { - perror("read"); - ret = PROCESS_ERROR_GENERIC; - goto end; - } - } else if (*pid == 0) { - // child close read side - close(fd[0]); - if (fcntl(fd[1], F_SETFD, FD_CLOEXEC) == 0) { - execvp(argv[0], (char *const *)argv); - if (errno == ENOENT) { - ret = PROCESS_ERROR_MISSING_BINARY; - } else { - ret = PROCESS_ERROR_GENERIC; + if (*pid == 0) { + if (pipe_stdin) { + if (in[0] != STDIN_FILENO) { + dup2(in[0], STDIN_FILENO); + close(in[0]); } + close(in[1]); + } + if (pipe_stdout) { + if (out[1] != STDOUT_FILENO) { + dup2(out[1], STDOUT_FILENO); + close(out[1]); + } + close(out[0]); + } + if (pipe_stderr) { + if (err[1] != STDERR_FILENO) { + dup2(err[1], STDERR_FILENO); + close(err[1]); + } + close(err[0]); + } + close(internal[0]); + enum process_result err; + if (fcntl(internal[1], F_SETFD, FD_CLOEXEC) == 0) { + execvp(argv[0], (char *const *) argv); perror("exec"); + err = errno == ENOENT ? PROCESS_ERROR_MISSING_BINARY + : PROCESS_ERROR_GENERIC; } else { perror("fcntl"); - ret = PROCESS_ERROR_GENERIC; + err = PROCESS_ERROR_GENERIC; } - // send ret to the parent - if (write(fd[1], &ret, sizeof(ret)) == -1) { + // send err to the parent + if (write(internal[1], &err, sizeof(err)) == -1) { perror("write"); } - // close write side before exiting - close(fd[1]); + close(internal[1]); _exit(1); } -end: - if (fd[0] != -1) { - close(fd[0]); + // parent + assert(*pid > 0); + + close(internal[1]); + + enum process_result res = PROCESS_SUCCESS; + // wait for EOF or receive err from child + if (read(internal[0], &res, sizeof(res)) == -1) { + perror("read"); + res = PROCESS_ERROR_GENERIC; } - if (fd[1] != -1) { - close(fd[1]); + + close(internal[0]); + + if (pipe_stdin) { + close(in[0]); + *pipe_stdin = in[1]; } - return ret; + if (pipe_stdout) { + *pipe_stdout = out[0]; + close(out[1]); + } + if (pipe_stderr) { + *pipe_stderr = err[0]; + close(err[1]); + } + + return res; +} + +enum process_result +process_execute(const char *const argv[], pid_t *pid) { + return process_execute_redirect(argv, pid, NULL, NULL, NULL); } bool @@ -175,3 +262,15 @@ is_regular_file(const char *path) { } return S_ISREG(path_stat.st_mode); } + +ssize_t +read_pipe(int pipe, char *data, size_t len) { + return read(pipe, data, len); +} + +void +close_pipe(int pipe) { + if (close(pipe)) { + perror("close pipe"); + } +} diff --git a/app/src/sys/win/process.c b/app/src/sys/win/process.c index aafd5d34..9a846fad 100644 --- a/app/src/sys/win/process.c +++ b/app/src/sys/win/process.c @@ -23,38 +23,129 @@ build_cmd(char *cmd, size_t len, const char *const argv[]) { } enum process_result -process_execute(const char *const argv[], HANDLE *handle) { +process_execute_redirect(const char *const argv[], HANDLE *handle, + HANDLE *pipe_stdin, HANDLE *pipe_stdout, + HANDLE *pipe_stderr) { + enum process_result ret = PROCESS_ERROR_GENERIC; + + SECURITY_ATTRIBUTES sa; + sa.nLength = sizeof(SECURITY_ATTRIBUTES); + sa.lpSecurityDescriptor = NULL; + sa.bInheritHandle = TRUE; + + HANDLE stdin_read_handle; + HANDLE stdout_write_handle; + HANDLE stderr_write_handle; + if (pipe_stdin) { + if (!CreatePipe(&stdin_read_handle, pipe_stdin, &sa, 0)) { + perror("pipe"); + return PROCESS_ERROR_GENERIC; + } + if (!SetHandleInformation(*pipe_stdin, HANDLE_FLAG_INHERIT, 0)) { + LOGE("SetHandleInformation stdin failed"); + goto error_close_stdin; + } + } + if (pipe_stdout) { + if (!CreatePipe(pipe_stdout, &stdout_write_handle, &sa, 0)) { + perror("pipe"); + goto error_close_stdin; + } + if (!SetHandleInformation(*pipe_stdout, HANDLE_FLAG_INHERIT, 0)) { + LOGE("SetHandleInformation stdout failed"); + goto error_close_stdout; + } + } + if (pipe_stderr) { + if (!CreatePipe(pipe_stderr, &stderr_write_handle, &sa, 0)) { + perror("pipe"); + goto error_close_stdout; + } + if (!SetHandleInformation(*pipe_stderr, HANDLE_FLAG_INHERIT, 0)) { + LOGE("SetHandleInformation stderr failed"); + goto error_close_stderr; + } + } + STARTUPINFOW si; PROCESS_INFORMATION pi; memset(&si, 0, sizeof(si)); si.cb = sizeof(si); + if (pipe_stdin || pipe_stdout || pipe_stderr) { + si.dwFlags = STARTF_USESTDHANDLES; + if (pipe_stdin) { + si.hStdInput = stdin_read_handle; + } + if (pipe_stdout) { + si.hStdOutput = stdout_write_handle; + } + if (pipe_stderr) { + si.hStdError = stderr_write_handle; + } + } char *cmd = malloc(CMD_MAX_LEN); if (!cmd || !build_cmd(cmd, CMD_MAX_LEN, argv)) { *handle = NULL; - return PROCESS_ERROR_GENERIC; + goto error_close_stderr; } wchar_t *wide = utf8_to_wide_char(cmd); free(cmd); if (!wide) { LOGC("Could not allocate wide char string"); - return PROCESS_ERROR_GENERIC; + goto error_close_stderr; } - if (!CreateProcessW(NULL, wide, NULL, NULL, FALSE, 0, NULL, NULL, &si, + if (!CreateProcessW(NULL, wide, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi)) { free(wide); *handle = NULL; + if (GetLastError() == ERROR_FILE_NOT_FOUND) { - return PROCESS_ERROR_MISSING_BINARY; + ret = PROCESS_ERROR_MISSING_BINARY; } - return PROCESS_ERROR_GENERIC; + goto error_close_stderr; + } + + // These handles are used by the child process, close them for this process + if (pipe_stdin) { + CloseHandle(stdin_read_handle); + } + if (pipe_stdout) { + CloseHandle(stdout_write_handle); + } + if (pipe_stderr) { + CloseHandle(stderr_write_handle); } free(wide); *handle = pi.hProcess; + return PROCESS_SUCCESS; + +error_close_stderr: + if (pipe_stderr) { + CloseHandle(*pipe_stderr); + CloseHandle(stderr_write_handle); + } +error_close_stdout: + if (pipe_stdout) { + CloseHandle(*pipe_stdout); + CloseHandle(stdout_write_handle); + } +error_close_stdin: + if (pipe_stdin) { + CloseHandle(*pipe_stdin); + CloseHandle(stdin_read_handle); + } + + return ret; +} + +enum process_result +process_execute(const char *const argv[], HANDLE *handle) { + return process_execute_redirect(argv, handle, NULL, NULL, NULL); } bool @@ -116,3 +207,19 @@ is_regular_file(const char *path) { } return S_ISREG(path_stat.st_mode); } + +ssize_t +read_pipe(HANDLE pipe, char *data, size_t len) { + DWORD r; + if (!ReadFile(pipe, data, len, &r, NULL)) { + return -1; + } + return r; +} + +void +close_pipe(HANDLE pipe) { + if (!CloseHandle(pipe)) { + LOGW("Cannot close pipe"); + } +} diff --git a/app/src/util/process.h b/app/src/util/process.h index 6aca6bf5..a21374b6 100644 --- a/app/src/util/process.h +++ b/app/src/util/process.h @@ -18,6 +18,7 @@ # define NO_EXIT_CODE -1u // max value as unsigned typedef HANDLE process_t; typedef DWORD exit_code_t; + typedef HANDLE pipe_t; #else @@ -29,6 +30,7 @@ # define NO_EXIT_CODE -1 typedef pid_t process_t; typedef int exit_code_t; + typedef int pipe_t; #endif @@ -42,6 +44,14 @@ enum process_result { enum process_result process_execute(const char *const argv[], process_t *process); +enum process_result +process_execute_redirect(const char *const argv[], process_t *process, + pipe_t *pipe_stdin, pipe_t *pipe_stdout, + pipe_t *pipe_stderr); + +bool +process_terminate(process_t pid); + // kill the process bool process_terminate(process_t pid); @@ -83,4 +93,10 @@ get_local_file_path(const char *name); bool is_regular_file(const char *path); +ssize_t +read_pipe(pipe_t pipe, char *data, size_t len); + +void +close_pipe(pipe_t pipe); + #endif From 96b18dabaad06b7fe417ad4fbf52031d11dd6080 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 17 Oct 2021 16:14:33 +0200 Subject: [PATCH 0700/2244] Expose adb execution with redirection Expose the redirection feature to the adb API. --- app/src/adb.c | 13 +++++++++++-- app/src/adb.h | 5 +++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/app/src/adb.c b/app/src/adb.c index 5bb9df30..6b4d6fc0 100644 --- a/app/src/adb.c +++ b/app/src/adb.c @@ -109,7 +109,9 @@ show_adb_err_msg(enum process_result err, const char *const argv[]) { } process_t -adb_execute(const char *serial, const char *const adb_cmd[], size_t len) { +adb_execute_redirect(const char *serial, const char *const adb_cmd[], + size_t len, pipe_t *pipe_stdin, pipe_t *pipe_stdout, + pipe_t *pipe_stderr) { int i; process_t process; @@ -129,7 +131,9 @@ adb_execute(const char *serial, const char *const adb_cmd[], size_t len) { memcpy(&argv[i], adb_cmd, len * sizeof(const char *)); argv[len + i] = NULL; - enum process_result r = process_execute(argv, &process); + enum process_result r = + process_execute_redirect(argv, &process, pipe_stdin, pipe_stdout, + pipe_stderr); if (r != PROCESS_SUCCESS) { show_adb_err_msg(r, argv); process = PROCESS_NONE; @@ -139,6 +143,11 @@ adb_execute(const char *serial, const char *const adb_cmd[], size_t len) { return process; } +process_t +adb_execute(const char *serial, const char *const adb_cmd[], size_t len) { + return adb_execute_redirect(serial, adb_cmd, len, NULL, NULL, NULL); +} + process_t adb_forward(const char *serial, uint16_t local_port, const char *device_socket_name) { diff --git a/app/src/adb.h b/app/src/adb.h index e27f34fa..d2f703a7 100644 --- a/app/src/adb.h +++ b/app/src/adb.h @@ -11,6 +11,11 @@ process_t adb_execute(const char *serial, const char *const adb_cmd[], size_t len); +process_t +adb_execute_redirect(const char *serial, const char *const adb_cmd[], + size_t len, pipe_t *pipe_stdin, pipe_t *pipe_stdout, + pipe_t *pipe_stderr); + process_t adb_forward(const char *serial, uint16_t local_port, const char *device_socket_name); From 068148080975126ceac222c656ae65ee46553ef2 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 17 Oct 2021 16:20:46 +0200 Subject: [PATCH 0701/2244] Add read_pipe_all() Add a convenience function to read from a pipe until all requested data has been read. --- app/src/util/process.c | 15 +++++++++++++++ app/src/util/process.h | 3 +++ 2 files changed, 18 insertions(+) diff --git a/app/src/util/process.c b/app/src/util/process.c index a9af4d67..5d572c26 100644 --- a/app/src/util/process.c +++ b/app/src/util/process.c @@ -61,3 +61,18 @@ get_local_file_path(const char *name) { return file_path; } + +ssize_t +read_pipe_all(pipe_t pipe, char *data, size_t len) { + size_t copied = 0; + while (len > 0) { + ssize_t r = read_pipe(pipe, data, len); + if (r <= 0) { + return copied ? (ssize_t) copied : r; + } + len -= r; + data += r; + copied += r; + } + return copied; +} diff --git a/app/src/util/process.h b/app/src/util/process.h index a21374b6..d6471a16 100644 --- a/app/src/util/process.h +++ b/app/src/util/process.h @@ -96,6 +96,9 @@ is_regular_file(const char *path); ssize_t read_pipe(pipe_t pipe, char *data, size_t len); +ssize_t +read_pipe_all(pipe_t pipe, char *data, size_t len); + void close_pipe(pipe_t pipe); From d55015e4cfd419582a4c7710125571241a377480 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 17 Oct 2021 16:30:07 +0200 Subject: [PATCH 0702/2244] Expose function to get the device serial Expose adb_get_serialno() to retrieve the device serial via the command "adb getserialno". --- app/src/adb.c | 44 ++++++++++++++++++++++++++++++++++++++++++++ app/src/adb.h | 4 ++++ 2 files changed, 48 insertions(+) diff --git a/app/src/adb.c b/app/src/adb.c index 6b4d6fc0..7f7e28d8 100644 --- a/app/src/adb.c +++ b/app/src/adb.c @@ -233,3 +233,47 @@ adb_install(const char *serial, const char *local) { return proc; } + +static ssize_t +adb_execute_for_output(const char *serial, const char *const adb_cmd[], + size_t adb_cmd_len, char *buf, size_t buf_len, + const char *name) { + pipe_t pipe_stdout; + process_t proc = adb_execute_redirect(serial, adb_cmd, adb_cmd_len, NULL, + &pipe_stdout, NULL); + + ssize_t r = read_pipe_all(pipe_stdout, buf, buf_len); + close_pipe(pipe_stdout); + + if (!process_check_success(proc, name, true)) { + return -1; + } + + return r; +} + +static size_t +truncate_first_line(char *data, size_t len) { + data[len - 1] = '\0'; + char *eol = strpbrk(data, "\r\n"); + if (eol) { + *eol = '\0'; + len = eol - data; + } + return len; +} + +char * +adb_get_serialno(void) { + char buf[128]; + + const char *const adb_cmd[] = {"get-serialno"}; + ssize_t r = adb_execute_for_output(NULL, adb_cmd, ARRAY_LEN(adb_cmd), + buf, sizeof(buf), "get-serialno"); + if (r <= 0) { + return NULL; + } + + truncate_first_line(buf, r); + return strdup(buf); +} diff --git a/app/src/adb.h b/app/src/adb.h index d2f703a7..34182fd3 100644 --- a/app/src/adb.h +++ b/app/src/adb.h @@ -36,4 +36,8 @@ adb_push(const char *serial, const char *local, const char *remote); process_t adb_install(const char *serial, const char *local); +// Return the result of "adb get-serialno". +char * +adb_get_serialno(void); + #endif From 511356710d7aced5f363d6a56b7beacba59e140f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 17 Oct 2021 16:58:31 +0200 Subject: [PATCH 0703/2244] Retrieve device serial for AOA The serial is necessary to find the correct Android device for AOA. If it is not explicitly provided by the user via -s, then execute "adb getserialno" to retrieve it. --- app/src/scrcpy.c | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index c4041562..bfea6642 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -436,7 +436,23 @@ scrcpy(struct scrcpy_options *options) { if (options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_HID) { #ifdef HAVE_AOA_HID bool aoa_hid_ok = false; - if (!sc_aoa_init(&s->aoa, options->serial)) { + + char *serialno = NULL; + + const char *serial = options->serial; + if (!serial) { + serialno = adb_get_serialno(); + if (!serialno) { + LOGE("Could not get device serial"); + goto aoa_hid_end; + } + serial = serialno; + LOGI("Device serial: %s", serial); + } + + bool ok = sc_aoa_init(&s->aoa, serial); + free(serialno); + if (!ok) { goto aoa_hid_end; } From c96874b257db964c0172051f568534d99bf553b8 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 21 Oct 2021 20:44:19 +0200 Subject: [PATCH 0704/2244] Synchronize HID keyboard state on first event When an AOA HID keyboard is registered, CAPSLOCK and NUMLOCK are both disabled, regardless of the state of the computer keyboard. To synchronize the state, on first key event, inject CAPSLOCK and/or NUMLOCK if necessary. --- app/src/hid_keyboard.c | 48 ++++++++++++++++++++++++++++++++++++++++++ app/src/hid_keyboard.h | 2 ++ 2 files changed, 50 insertions(+) diff --git a/app/src/hid_keyboard.c b/app/src/hid_keyboard.c index 2599e1d2..425516af 100644 --- a/app/src/hid_keyboard.c +++ b/app/src/hid_keyboard.c @@ -237,6 +237,45 @@ end: return true; } + +static bool +push_mod_lock_state(struct sc_hid_keyboard *kb, uint16_t sdl_mod) { + bool capslock = sdl_mod & KMOD_CAPS; + bool numlock = sdl_mod & KMOD_NUM; + if (!capslock && !numlock) { + // Nothing to do + return true; + } + + struct sc_hid_event hid_event; + if (!sc_hid_keyboard_event_init(&hid_event)) { + LOGW("Could not initialize HID keyboard event"); + return false; + } + +#define SC_SCANCODE_CAPSLOCK SDL_SCANCODE_CAPSLOCK +#define SC_SCANCODE_NUMLOCK SDL_SCANCODE_NUMLOCKCLEAR + unsigned i = 0; + if (capslock) { + hid_event.buffer[HID_KEYBOARD_INDEX_KEYS + i] = SC_SCANCODE_CAPSLOCK; + ++i; + } + if (numlock) { + hid_event.buffer[HID_KEYBOARD_INDEX_KEYS + i] = SC_SCANCODE_NUMLOCK; + ++i; + } + + if (!sc_aoa_push_hid_event(kb->aoa, &hid_event)) { + sc_hid_event_destroy(&hid_event); + LOGW("Could request HID event"); + return false; + } + + LOGD("HID keyboard state synchronized"); + + return true; +} + static void sc_key_processor_process_key(struct sc_key_processor *kp, const SDL_KeyboardEvent *event) { @@ -251,6 +290,13 @@ sc_key_processor_process_key(struct sc_key_processor *kp, struct sc_hid_event hid_event; // Not all keys are supported, just ignore unsupported keys if (convert_hid_keyboard_event(kb, &hid_event, event)) { + if (!kb->mod_lock_synchronized) { + // Inject CAPSLOCK and/or NUMLOCK if necessary to synchronize + // keyboard state + if (push_mod_lock_state(kb, event->keysym.mod)) { + kb->mod_lock_synchronized = true; + } + } if (!sc_aoa_push_hid_event(kb->aoa, &hid_event)) { sc_hid_event_destroy(&hid_event); LOGW("Could request HID event"); @@ -282,6 +328,8 @@ sc_hid_keyboard_init(struct sc_hid_keyboard *kb, struct sc_aoa *aoa) { // Reset all states memset(kb->keys, false, SC_HID_KEYBOARD_KEYS); + kb->mod_lock_synchronized = false; + static const struct sc_key_processor_ops ops = { .process_key = sc_key_processor_process_key, .process_text = sc_key_processor_process_text, diff --git a/app/src/hid_keyboard.h b/app/src/hid_keyboard.h index d8276cad..7173a898 100644 --- a/app/src/hid_keyboard.h +++ b/app/src/hid_keyboard.h @@ -31,6 +31,8 @@ struct sc_hid_keyboard { struct sc_aoa *aoa; bool keys[SC_HID_KEYBOARD_KEYS]; + + bool mod_lock_synchronized; }; bool From e4163321f00bb3830c6049bdb6c1515e7cc668a0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 21 Oct 2021 21:33:15 +0200 Subject: [PATCH 0705/2244] Delay HID events on Ctrl+v When Ctrl+v is pressed, a control is sent to the device to set the device clipboard before injecting Ctrl+v. With the InputManager method, it is guaranteed that the device synchronization is executed before handling Ctrl+v, since the commands are executed on the device in sequence. However, HID are injected from the computer, so there is no such guarantee. As a consequence, on Android, Ctrl+v triggers a paste with the old clipboard content. To workaround the issue, wait a bit (2 milliseconds) from the AOA thread before injecting the event, to leave enough time for the clipboard to be set before injecting Ctrl+v. --- app/src/aoa_hid.c | 17 +++++++++++++++++ app/src/aoa_hid.h | 2 ++ app/src/hid_keyboard.c | 13 +++++++++++++ 3 files changed, 32 insertions(+) diff --git a/app/src/aoa_hid.c b/app/src/aoa_hid.c index 7fc0f34c..4c0b2bda 100644 --- a/app/src/aoa_hid.c +++ b/app/src/aoa_hid.c @@ -35,6 +35,7 @@ sc_hid_event_init(struct sc_hid_event *hid_event, uint16_t accessory_id, hid_event->accessory_id = accessory_id; hid_event->buffer = buffer; hid_event->size = buffer_size; + hid_event->delay = 0; } void @@ -330,6 +331,22 @@ run_aoa_thread(void *data) { bool non_empty = cbuf_take(&aoa->queue, &event); assert(non_empty); (void) non_empty; + + assert(event.delay >= 0); + if (event.delay) { + // Wait during the specified delay before injecting the HID event + sc_tick deadline = sc_tick_now() + event.delay; + bool timed_out = false; + while (!aoa->stopped && !timed_out) { + timed_out = !sc_cond_timedwait(&aoa->event_cond, &aoa->mutex, + deadline); + } + if (aoa->stopped) { + sc_mutex_unlock(&aoa->mutex); + break; + } + } + sc_mutex_unlock(&aoa->mutex); bool ok = sc_aoa_send_hid_event(aoa, &event); diff --git a/app/src/aoa_hid.h b/app/src/aoa_hid.h index 11b879ce..11cc57b8 100644 --- a/app/src/aoa_hid.h +++ b/app/src/aoa_hid.h @@ -9,11 +9,13 @@ #include "scrcpy.h" #include "util/cbuf.h" #include "util/thread.h" +#include "util/tick.h" struct sc_hid_event { uint16_t accessory_id; unsigned char *buffer; uint16_t size; + sc_tick delay; }; // Takes ownership of buffer diff --git a/app/src/hid_keyboard.c b/app/src/hid_keyboard.c index 425516af..c6fba21c 100644 --- a/app/src/hid_keyboard.c +++ b/app/src/hid_keyboard.c @@ -297,6 +297,19 @@ sc_key_processor_process_key(struct sc_key_processor *kp, kb->mod_lock_synchronized = true; } } + + 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; + if (ctrl && !shift && keycode == SDLK_v && down) { + // Ctrl+v is pressed, so clipboard synchronization has been + // requested. Wait a bit so that the clipboard is set before + // injecting Ctrl+v via HID, otherwise it would paste the old + // clipboard content. + hid_event.delay = SC_TICK_FROM_MS(2); + } + if (!sc_aoa_push_hid_event(kb->aoa, &hid_event)) { sc_hid_event_destroy(&hid_event); LOGW("Could request HID event"); From 5222f213f4a588658a07441b5f91df152e671139 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 21 Oct 2021 21:38:01 +0200 Subject: [PATCH 0706/2244] Update FAQ to mention HID keyboard --- FAQ.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/FAQ.md b/FAQ.md index f74845a5..a1c2fb76 100644 --- a/FAQ.md +++ b/FAQ.md @@ -118,13 +118,17 @@ In developer options, enable: ### 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]. +The default text injection method is [limited to ASCII characters][text-input]. +A trick allows to also inject some [accented characters][accented-characters], +but that's all. See [#37]. + +Since scrcpy v1.20 on Linux, it is possible to simulate a [physical +keyboard][hid] (HID). [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 +[hid]: README.md#physical-keyboard-simulation-hid ## Client issues From eb6afe76698e3d974bae4615c0cba2e2744183de Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 26 Oct 2021 22:49:45 +0200 Subject: [PATCH 0707/2244] Move net_init() and net_cleanup() upwards These two functions are global, define them at the top of the implementation file. This is consistent with the header file. --- app/src/util/net.c | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/app/src/util/net.c b/app/src/util/net.c index 2b5a0e5e..4b5a4874 100644 --- a/app/src/util/net.c +++ b/app/src/util/net.c @@ -19,6 +19,26 @@ typedef struct in_addr IN_ADDR; #endif +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 +} + static void net_perror(const char *s) { #ifdef _WIN32 @@ -133,26 +153,6 @@ 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__ From 3adff37c2d33670937a4f3d94995a04b89d64f27 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 26 Oct 2021 22:49:45 +0200 Subject: [PATCH 0708/2244] Use sc_ prefix for sockets Rename: - socket_t to sc_socket - INVALID_SOCKET to SC_INVALID_SOCKET --- app/src/controller.c | 2 +- app/src/controller.h | 4 ++-- app/src/receiver.c | 2 +- app/src/receiver.h | 4 ++-- app/src/server.c | 51 ++++++++++++++++++++++---------------------- app/src/server.h | 6 +++--- app/src/stream.c | 2 +- app/src/stream.h | 4 ++-- app/src/util/net.c | 38 ++++++++++++++++----------------- app/src/util/net.h | 38 ++++++++++++++++++--------------- 10 files changed, 78 insertions(+), 73 deletions(-) diff --git a/app/src/controller.c b/app/src/controller.c index b85ac02d..e486ea72 100644 --- a/app/src/controller.c +++ b/app/src/controller.c @@ -5,7 +5,7 @@ #include "util/log.h" bool -controller_init(struct controller *controller, socket_t control_socket) { +controller_init(struct controller *controller, sc_socket control_socket) { cbuf_init(&controller->queue); bool ok = receiver_init(&controller->receiver, control_socket); diff --git a/app/src/controller.h b/app/src/controller.h index c53d0a61..e7004131 100644 --- a/app/src/controller.h +++ b/app/src/controller.h @@ -14,7 +14,7 @@ struct control_msg_queue CBUF(struct control_msg, 64); struct controller { - socket_t control_socket; + sc_socket control_socket; sc_thread thread; sc_mutex mutex; sc_cond msg_cond; @@ -24,7 +24,7 @@ struct controller { }; bool -controller_init(struct controller *controller, socket_t control_socket); +controller_init(struct controller *controller, sc_socket control_socket); void controller_destroy(struct controller *controller); diff --git a/app/src/receiver.c b/app/src/receiver.c index 337d2a17..b5cf9b39 100644 --- a/app/src/receiver.c +++ b/app/src/receiver.c @@ -7,7 +7,7 @@ #include "util/log.h" bool -receiver_init(struct receiver *receiver, socket_t control_socket) { +receiver_init(struct receiver *receiver, sc_socket control_socket) { bool ok = sc_mutex_init(&receiver->mutex); if (!ok) { return false; diff --git a/app/src/receiver.h b/app/src/receiver.h index 36523b62..99f128a4 100644 --- a/app/src/receiver.h +++ b/app/src/receiver.h @@ -11,13 +11,13 @@ // receive events from the device // managed by the controller struct receiver { - socket_t control_socket; + sc_socket control_socket; sc_thread thread; sc_mutex mutex; }; bool -receiver_init(struct receiver *receiver, socket_t control_socket); +receiver_init(struct receiver *receiver, sc_socket control_socket); void receiver_destroy(struct receiver *receiver); diff --git a/app/src/server.c b/app/src/server.c index 4c1a43f5..f786f62d 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -109,7 +109,7 @@ disable_tunnel(struct server *server) { return disable_tunnel_reverse(server->serial); } -static socket_t +static sc_socket listen_on_port(uint16_t port) { #define IPV4_LOCALHOST 0x7F000001 return net_listen(IPV4_LOCALHOST, port, 1); @@ -132,7 +132,7 @@ enable_tunnel_reverse_any_port(struct server *server, // need to try to connect until the server socket is listening on the // device. server->server_socket = listen_on_port(port); - if (server->server_socket != INVALID_SOCKET) { + if (server->server_socket != SC_INVALID_SOCKET) { // success server->local_port = port; return true; @@ -289,11 +289,11 @@ execute_server(struct server *server, const struct server_params *params) { return adb_execute(server->serial, cmd, ARRAY_LEN(cmd)); } -static socket_t +static sc_socket connect_and_read_byte(uint16_t port) { - socket_t socket = net_connect(IPV4_LOCALHOST, port); - if (socket == INVALID_SOCKET) { - return INVALID_SOCKET; + sc_socket socket = net_connect(IPV4_LOCALHOST, port); + if (socket == SC_INVALID_SOCKET) { + return SC_INVALID_SOCKET; } char byte; @@ -302,17 +302,17 @@ connect_and_read_byte(uint16_t port) { if (net_recv(socket, &byte, 1) != 1) { // the server is not listening yet behind the adb tunnel net_close(socket); - return INVALID_SOCKET; + return SC_INVALID_SOCKET; } return socket; } -static socket_t +static sc_socket connect_to_server(uint16_t port, uint32_t attempts, uint32_t delay) { do { LOGD("Remaining connection attempts: %d", (int) attempts); - socket_t socket = connect_and_read_byte(port); - if (socket != INVALID_SOCKET) { + sc_socket socket = connect_and_read_byte(port); + if (socket != SC_INVALID_SOCKET) { // it worked! return socket; } @@ -320,12 +320,12 @@ connect_to_server(uint16_t port, uint32_t attempts, uint32_t delay) { SDL_Delay(delay); } } while (--attempts > 0); - return INVALID_SOCKET; + return SC_INVALID_SOCKET; } static void -close_socket(socket_t socket) { - assert(socket != INVALID_SOCKET); +close_socket(sc_socket socket) { + assert(socket != SC_INVALID_SOCKET); net_shutdown(socket, SHUT_RDWR); if (!net_close(socket)) { LOGW("Could not close socket"); @@ -352,9 +352,9 @@ server_init(struct server *server) { server->process_terminated = false; - server->server_socket = INVALID_SOCKET; - server->video_socket = INVALID_SOCKET; - server->control_socket = INVALID_SOCKET; + server->server_socket = SC_INVALID_SOCKET; + server->video_socket = SC_INVALID_SOCKET; + server->control_socket = SC_INVALID_SOCKET; server->local_port = 0; @@ -376,7 +376,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 + if (server->server_socket != SC_INVALID_SOCKET && !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()). @@ -444,7 +444,8 @@ error: } static bool -device_read_info(socket_t device_socket, char *device_name, struct size *size) { +device_read_info(sc_socket device_socket, char *device_name, + struct size *size) { unsigned char buf[DEVICE_NAME_FIELD_LENGTH + 4]; ssize_t r = net_recv_all(device_socket, buf, sizeof(buf)); if (r < DEVICE_NAME_FIELD_LENGTH + 4) { @@ -467,12 +468,12 @@ bool server_connect_to(struct server *server, char *device_name, struct size *size) { if (!server->tunnel_forward) { server->video_socket = net_accept(server->server_socket); - if (server->video_socket == INVALID_SOCKET) { + if (server->video_socket == SC_INVALID_SOCKET) { return false; } server->control_socket = net_accept(server->server_socket); - if (server->control_socket == INVALID_SOCKET) { + if (server->control_socket == SC_INVALID_SOCKET) { // the video_socket will be cleaned up on destroy return false; } @@ -488,14 +489,14 @@ server_connect_to(struct server *server, char *device_name, struct size *size) { uint32_t delay = 100; // ms server->video_socket = connect_to_server(server->local_port, attempts, delay); - if (server->video_socket == INVALID_SOCKET) { + if (server->video_socket == SC_INVALID_SOCKET) { return false; } // we know that the device is listening, we don't need several attempts server->control_socket = net_connect(IPV4_LOCALHOST, server->local_port); - if (server->control_socket == INVALID_SOCKET) { + if (server->control_socket == SC_INVALID_SOCKET) { return false; } } @@ -510,14 +511,14 @@ server_connect_to(struct server *server, char *device_name, struct size *size) { void server_stop(struct server *server) { - if (server->server_socket != INVALID_SOCKET + if (server->server_socket != SC_INVALID_SOCKET && !atomic_flag_test_and_set(&server->server_socket_closed)) { close_socket(server->server_socket); } - if (server->video_socket != INVALID_SOCKET) { + if (server->video_socket != SC_INVALID_SOCKET) { close_socket(server->video_socket); } - if (server->control_socket != INVALID_SOCKET) { + if (server->control_socket != SC_INVALID_SOCKET) { close_socket(server->control_socket); } diff --git a/app/src/server.h b/app/src/server.h index c249b374..b6d2255a 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -24,9 +24,9 @@ struct server { sc_cond process_terminated_cond; bool process_terminated; - socket_t server_socket; // only used if !tunnel_forward - socket_t video_socket; - socket_t control_socket; + sc_socket server_socket; // only used if !tunnel_forward + sc_socket video_socket; + sc_socket control_socket; uint16_t local_port; // selected from port_range bool tunnel_enabled; bool tunnel_forward; // use "adb forward" instead of "adb reverse" diff --git a/app/src/stream.c b/app/src/stream.c index adc6277f..4c770250 100644 --- a/app/src/stream.c +++ b/app/src/stream.c @@ -260,7 +260,7 @@ end: } void -stream_init(struct stream *stream, socket_t socket, +stream_init(struct stream *stream, sc_socket socket, const struct stream_callbacks *cbs, void *cbs_userdata) { stream->socket = socket; stream->pending = NULL; diff --git a/app/src/stream.h b/app/src/stream.h index d7047c95..362bc4a7 100644 --- a/app/src/stream.h +++ b/app/src/stream.h @@ -14,7 +14,7 @@ #define STREAM_MAX_SINKS 2 struct stream { - socket_t socket; + sc_socket socket; sc_thread thread; struct sc_packet_sink *sinks[STREAM_MAX_SINKS]; @@ -35,7 +35,7 @@ struct stream_callbacks { }; void -stream_init(struct stream *stream, socket_t socket, +stream_init(struct stream *stream, sc_socket socket, const struct stream_callbacks *cbs, void *cbs_userdata); void diff --git a/app/src/util/net.c b/app/src/util/net.c index 4b5a4874..eb98ea59 100644 --- a/app/src/util/net.c +++ b/app/src/util/net.c @@ -55,12 +55,12 @@ net_perror(const char *s) { #endif } -socket_t +sc_socket net_connect(uint32_t addr, uint16_t port) { - socket_t sock = socket(AF_INET, SOCK_STREAM, 0); - if (sock == INVALID_SOCKET) { + sc_socket sock = socket(AF_INET, SOCK_STREAM, 0); + if (sock == SC_INVALID_SOCKET) { net_perror("socket"); - return INVALID_SOCKET; + return SC_INVALID_SOCKET; } SOCKADDR_IN sin; @@ -71,18 +71,18 @@ net_connect(uint32_t addr, uint16_t port) { if (connect(sock, (SOCKADDR *) &sin, sizeof(sin)) == SOCKET_ERROR) { net_perror("connect"); net_close(sock); - return INVALID_SOCKET; + return SC_INVALID_SOCKET; } return sock; } -socket_t +sc_socket net_listen(uint32_t addr, uint16_t port, int backlog) { - socket_t sock = socket(AF_INET, SOCK_STREAM, 0); - if (sock == INVALID_SOCKET) { + sc_socket sock = socket(AF_INET, SOCK_STREAM, 0); + if (sock == SC_INVALID_SOCKET) { net_perror("socket"); - return INVALID_SOCKET; + return SC_INVALID_SOCKET; } int reuse = 1; @@ -99,42 +99,42 @@ net_listen(uint32_t addr, uint16_t port, int backlog) { if (bind(sock, (SOCKADDR *) &sin, sizeof(sin)) == SOCKET_ERROR) { net_perror("bind"); net_close(sock); - return INVALID_SOCKET; + return SC_INVALID_SOCKET; } if (listen(sock, backlog) == SOCKET_ERROR) { net_perror("listen"); net_close(sock); - return INVALID_SOCKET; + return SC_INVALID_SOCKET; } return sock; } -socket_t -net_accept(socket_t server_socket) { +sc_socket +net_accept(sc_socket server_socket) { SOCKADDR_IN csin; socklen_t sinsize = sizeof(csin); return accept(server_socket, (SOCKADDR *) &csin, &sinsize); } ssize_t -net_recv(socket_t socket, void *buf, size_t len) { +net_recv(sc_socket socket, void *buf, size_t len) { return recv(socket, buf, len, 0); } ssize_t -net_recv_all(socket_t socket, void *buf, size_t len) { +net_recv_all(sc_socket socket, void *buf, size_t len) { return recv(socket, buf, len, MSG_WAITALL); } ssize_t -net_send(socket_t socket, const void *buf, size_t len) { +net_send(sc_socket socket, const void *buf, size_t len) { return send(socket, buf, len, 0); } ssize_t -net_send_all(socket_t socket, const void *buf, size_t len) { +net_send_all(sc_socket socket, const void *buf, size_t len) { size_t copied = 0; while (len > 0) { ssize_t w = send(socket, buf, len, 0); @@ -149,12 +149,12 @@ net_send_all(socket_t socket, const void *buf, size_t len) { } bool -net_shutdown(socket_t socket, int how) { +net_shutdown(sc_socket socket, int how) { return !shutdown(socket, how); } bool -net_close(socket_t socket) { +net_close(sc_socket socket) { #ifdef __WINDOWS__ return !closesocket(socket); #else diff --git a/app/src/util/net.h b/app/src/util/net.h index d3b1f941..31920fd6 100644 --- a/app/src/util/net.h +++ b/app/src/util/net.h @@ -8,15 +8,19 @@ #include #ifdef __WINDOWS__ + # include - #define SHUT_RD SD_RECEIVE - #define SHUT_WR SD_SEND - #define SHUT_RDWR SD_BOTH - typedef SOCKET socket_t; -#else +# define SHUT_RD SD_RECEIVE +# define SHUT_WR SD_SEND +# define SHUT_RDWR SD_BOTH +# define SC_INVALID_SOCKET INVALID_SOCKET + typedef SOCKET sc_socket; + +#else // not __WINDOWS__ + # include -# define INVALID_SOCKET -1 - typedef int socket_t; +# define SC_INVALID_SOCKET -1 + typedef int sc_socket; #endif bool @@ -25,33 +29,33 @@ net_init(void); void net_cleanup(void); -socket_t +sc_socket net_connect(uint32_t addr, uint16_t port); -socket_t +sc_socket net_listen(uint32_t addr, uint16_t port, int backlog); -socket_t -net_accept(socket_t server_socket); +sc_socket +net_accept(sc_socket server_socket); // the _all versions wait/retry until len bytes have been written/read ssize_t -net_recv(socket_t socket, void *buf, size_t len); +net_recv(sc_socket socket, void *buf, size_t len); ssize_t -net_recv_all(socket_t socket, void *buf, size_t len); +net_recv_all(sc_socket socket, void *buf, size_t len); ssize_t -net_send(socket_t socket, const void *buf, size_t len); +net_send(sc_socket socket, const void *buf, size_t len); ssize_t -net_send_all(socket_t socket, const void *buf, size_t len); +net_send_all(sc_socket socket, const void *buf, size_t len); // how is SHUT_RD (read), SHUT_WR (write) or SHUT_RDWR (both) bool -net_shutdown(socket_t socket, int how); +net_shutdown(sc_socket socket, int how); bool -net_close(socket_t socket); +net_close(sc_socket socket); #endif From 3eac212af1206ae798436ff018f3d2d77840e759 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 26 Oct 2021 22:49:45 +0200 Subject: [PATCH 0709/2244] Use net_send() from net_send_all() This will make net_send_all() continue to work even if net_send() behavior is changed. --- app/src/util/net.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/util/net.c b/app/src/util/net.c index eb98ea59..678d6e67 100644 --- a/app/src/util/net.c +++ b/app/src/util/net.c @@ -137,7 +137,7 @@ ssize_t net_send_all(sc_socket socket, const void *buf, size_t len) { size_t copied = 0; while (len > 0) { - ssize_t w = send(socket, buf, len, 0); + ssize_t w = net_send(socket, buf, len); if (w == -1) { return copied ? (ssize_t) copied : -1; } From e5ea13770b9ddefbbb147b8cabcff9387f677f83 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 26 Oct 2021 22:49:45 +0200 Subject: [PATCH 0710/2244] Add socket wrapper This paves the way to store an additional "closed" flag on Windows to interrupt and close properly. --- app/src/util/net.c | 76 ++++++++++++++++++++++++++++++++++++++-------- app/src/util/net.h | 6 ++-- 2 files changed, 67 insertions(+), 15 deletions(-) diff --git a/app/src/util/net.c b/app/src/util/net.c index 678d6e67..1e74214b 100644 --- a/app/src/util/net.c +++ b/app/src/util/net.c @@ -7,6 +7,7 @@ #ifdef __WINDOWS__ typedef int socklen_t; + typedef SOCKET sc_raw_socket; #else # include # include @@ -17,6 +18,7 @@ typedef struct sockaddr_in SOCKADDR_IN; typedef struct sockaddr SOCKADDR; typedef struct in_addr IN_ADDR; + typedef int sc_raw_socket; #endif bool @@ -39,6 +41,40 @@ net_cleanup(void) { #endif } +static inline sc_socket +wrap(sc_raw_socket sock) { +#ifdef __WINDOWS__ + if (sock == INVALID_SOCKET) { + return SC_INVALID_SOCKET; + } + + struct sc_socket_windows *socket = malloc(sizeof(*socket)); + if (!socket) { + closesocket(sock); + return SC_INVALID_SOCKET; + } + + socket->socket = sock; + + return socket; +#else + return sock; +#endif +} + +static inline sc_raw_socket +unwrap(sc_socket socket) { +#ifdef __WINDOWS__ + if (socket == SC_INVALID_SOCKET) { + return INVALID_SOCKET; + } + + return socket->socket; +#else + return socket; +#endif +} + static void net_perror(const char *s) { #ifdef _WIN32 @@ -57,7 +93,8 @@ net_perror(const char *s) { sc_socket net_connect(uint32_t addr, uint16_t port) { - sc_socket sock = socket(AF_INET, SOCK_STREAM, 0); + sc_raw_socket raw_sock = socket(AF_INET, SOCK_STREAM, 0); + sc_socket sock = wrap(raw_sock); if (sock == SC_INVALID_SOCKET) { net_perror("socket"); return SC_INVALID_SOCKET; @@ -68,7 +105,7 @@ net_connect(uint32_t addr, uint16_t port) { sin.sin_addr.s_addr = htonl(addr); sin.sin_port = htons(port); - if (connect(sock, (SOCKADDR *) &sin, sizeof(sin)) == SOCKET_ERROR) { + if (connect(raw_sock, (SOCKADDR *) &sin, sizeof(sin)) == SOCKET_ERROR) { net_perror("connect"); net_close(sock); return SC_INVALID_SOCKET; @@ -79,14 +116,15 @@ net_connect(uint32_t addr, uint16_t port) { sc_socket net_listen(uint32_t addr, uint16_t port, int backlog) { - sc_socket sock = socket(AF_INET, SOCK_STREAM, 0); + sc_raw_socket raw_sock = socket(AF_INET, SOCK_STREAM, 0); + sc_socket sock = wrap(raw_sock); if (sock == SC_INVALID_SOCKET) { net_perror("socket"); return SC_INVALID_SOCKET; } int reuse = 1; - if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (const void *) &reuse, + if (setsockopt(raw_sock, SOL_SOCKET, SO_REUSEADDR, (const void *) &reuse, sizeof(reuse)) == -1) { net_perror("setsockopt(SO_REUSEADDR)"); } @@ -96,13 +134,13 @@ net_listen(uint32_t addr, uint16_t port, int backlog) { sin.sin_addr.s_addr = htonl(addr); // htonl() harmless on INADDR_ANY sin.sin_port = htons(port); - if (bind(sock, (SOCKADDR *) &sin, sizeof(sin)) == SOCKET_ERROR) { + if (bind(raw_sock, (SOCKADDR *) &sin, sizeof(sin)) == SOCKET_ERROR) { net_perror("bind"); net_close(sock); return SC_INVALID_SOCKET; } - if (listen(sock, backlog) == SOCKET_ERROR) { + if (listen(raw_sock, backlog) == SOCKET_ERROR) { net_perror("listen"); net_close(sock); return SC_INVALID_SOCKET; @@ -113,24 +151,32 @@ net_listen(uint32_t addr, uint16_t port, int backlog) { sc_socket net_accept(sc_socket server_socket) { + sc_raw_socket raw_server_socket = unwrap(server_socket); + SOCKADDR_IN csin; socklen_t sinsize = sizeof(csin); - return accept(server_socket, (SOCKADDR *) &csin, &sinsize); + sc_raw_socket raw_sock = + accept(raw_server_socket, (SOCKADDR *) &csin, &sinsize); + + return wrap(raw_sock); } ssize_t net_recv(sc_socket socket, void *buf, size_t len) { - return recv(socket, buf, len, 0); + sc_raw_socket raw_sock = unwrap(socket); + return recv(raw_sock, buf, len, 0); } ssize_t net_recv_all(sc_socket socket, void *buf, size_t len) { - return recv(socket, buf, len, MSG_WAITALL); + sc_raw_socket raw_sock = unwrap(socket); + return recv(raw_sock, buf, len, MSG_WAITALL); } ssize_t net_send(sc_socket socket, const void *buf, size_t len) { - return send(socket, buf, len, 0); + sc_raw_socket raw_sock = unwrap(socket); + return send(raw_sock, buf, len, 0); } ssize_t @@ -150,14 +196,18 @@ net_send_all(sc_socket socket, const void *buf, size_t len) { bool net_shutdown(sc_socket socket, int how) { - return !shutdown(socket, how); + sc_raw_socket raw_sock = unwrap(socket); + return !shutdown(raw_sock, how); } bool net_close(sc_socket socket) { + sc_raw_socket raw_sock = unwrap(socket); + #ifdef __WINDOWS__ - return !closesocket(socket); + free(socket); + return !closesocket(raw_sock); #else - return !close(socket); + return !close(raw_sock); #endif } diff --git a/app/src/util/net.h b/app/src/util/net.h index 31920fd6..f40f0bb5 100644 --- a/app/src/util/net.h +++ b/app/src/util/net.h @@ -13,8 +13,10 @@ # define SHUT_RD SD_RECEIVE # define SHUT_WR SD_SEND # define SHUT_RDWR SD_BOTH -# define SC_INVALID_SOCKET INVALID_SOCKET - typedef SOCKET sc_socket; +# define SC_INVALID_SOCKET NULL + typedef struct sc_socket_windows { + SOCKET socket; + } *sc_socket; #else // not __WINDOWS__ From ac23bec14450a8f38ead09860564e42737ac047e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 26 Oct 2021 22:49:45 +0200 Subject: [PATCH 0711/2244] Expose socket interruption On Linux, socket functions are unblocked by shutdown(), but on Windows they are unblocked by closesocket(). Expose net_interrupt() and net_close() to abstract these differences: - net_interrupt() calls shutdown() on Linux and closesocket() on Windows (if not already called); - net_close() calls close() on Linux and closesocket() on Windows (if not already called). This simplifies the server code, and prevents a data race on close (reported by TSAN) on Linux (but does not fix it on Windows): WARNING: ThreadSanitizer: data race (pid=836124) Write of size 8 at 0x7ba0000000d0 by main thread: #0 close ../../../../src/libsanitizer/tsan/tsan_interceptors_posix.cpp:1690 (libtsan.so.0+0x359d8) #1 net_close ../app/src/util/net.c:211 (scrcpy+0x1c76b) #2 close_socket ../app/src/server.c:330 (scrcpy+0x19442) #3 server_stop ../app/src/server.c:522 (scrcpy+0x19e33) #4 scrcpy ../app/src/scrcpy.c:532 (scrcpy+0x156fc) #5 main ../app/src/main.c:92 (scrcpy+0x622a) Previous read of size 8 at 0x7ba0000000d0 by thread T6: #0 recv ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:6603 (libtsan.so.0+0x4f4a6) #1 net_recv ../app/src/util/net.c:167 (scrcpy+0x1c5a7) #2 run_receiver ../app/src/receiver.c:76 (scrcpy+0x12819) #3 (libSDL2-2.0.so.0+0x84f40) --- app/src/server.c | 68 ++++++++++++++++++++++++---------------------- app/src/server.h | 1 - app/src/util/net.c | 23 ++++++++++++++-- app/src/util/net.h | 12 ++++---- 4 files changed, 62 insertions(+), 42 deletions(-) diff --git a/app/src/server.c b/app/src/server.c index f786f62d..76d2910b 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -323,21 +323,10 @@ connect_to_server(uint16_t port, uint32_t attempts, uint32_t delay) { return SC_INVALID_SOCKET; } -static void -close_socket(sc_socket socket) { - assert(socket != SC_INVALID_SOCKET); - net_shutdown(socket, SHUT_RDWR); - if (!net_close(socket)) { - LOGW("Could not close socket"); - } -} - bool server_init(struct server *server) { server->serial = NULL; server->process = PROCESS_NONE; - atomic_flag_clear_explicit(&server->server_socket_closed, - memory_order_relaxed); bool ok = sc_mutex_init(&server->mutex); if (!ok) { @@ -376,12 +365,11 @@ run_wait_server(void *data) { // no need for synchronization, server_socket is initialized before this // thread was created - if (server->server_socket != SC_INVALID_SOCKET - && !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); + if (server->server_socket != SC_INVALID_SOCKET) { + // Unblock any accept() + net_interrupt(server->server_socket); } + LOGD("Server terminated"); return 0; } @@ -430,14 +418,8 @@ server_start(struct server *server, const struct server_params *params) { return true; error: - if (!server->tunnel_forward) { - 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); - } + // The server socket (if any) will be closed on server_destroy() + disable_tunnel(server); return false; @@ -479,11 +461,11 @@ server_connect_to(struct server *server, char *device_name, struct size *size) { } // we don't need the server socket anymore - 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() + if (!net_close(server->server_socket)) { + LOGW("Could not close server socket on connect"); } + // Do not attempt to close it again on server_destroy() + server->server_socket = SC_INVALID_SOCKET; } else { uint32_t attempts = 100; uint32_t delay = 100; // ms @@ -511,15 +493,20 @@ server_connect_to(struct server *server, char *device_name, struct size *size) { void server_stop(struct server *server) { - if (server->server_socket != SC_INVALID_SOCKET - && !atomic_flag_test_and_set(&server->server_socket_closed)) { - close_socket(server->server_socket); + if (server->server_socket != SC_INVALID_SOCKET) { + if (!net_interrupt(server->server_socket)) { + LOGW("Could not interrupt server socket"); + } } if (server->video_socket != SC_INVALID_SOCKET) { - close_socket(server->video_socket); + if (!net_interrupt(server->video_socket)) { + LOGW("Could not interrupt video socket"); + } } if (server->control_socket != SC_INVALID_SOCKET) { - close_socket(server->control_socket); + if (!net_interrupt(server->control_socket)) { + LOGW("Could not interrupt control socket"); + } } assert(server->process != PROCESS_NONE); @@ -556,6 +543,21 @@ server_stop(struct server *server) { void server_destroy(struct server *server) { + if (server->server_socket != SC_INVALID_SOCKET) { + if (!net_close(server->server_socket)) { + LOGW("Could not close server socket"); + } + } + if (server->video_socket != SC_INVALID_SOCKET) { + if (!net_close(server->video_socket)) { + LOGW("Could not close video socket"); + } + } + if (server->control_socket != SC_INVALID_SOCKET) { + if (!net_close(server->control_socket)) { + LOGW("Could not close control socket"); + } + } free(server->serial); sc_cond_destroy(&server->process_terminated_cond); sc_mutex_destroy(&server->mutex); diff --git a/app/src/server.h b/app/src/server.h index b6d2255a..141075f7 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -18,7 +18,6 @@ struct server { char *serial; process_t process; sc_thread wait_server_thread; - atomic_flag server_socket_closed; sc_mutex mutex; sc_cond process_terminated_cond; diff --git a/app/src/util/net.c b/app/src/util/net.c index 1e74214b..cfc60433 100644 --- a/app/src/util/net.c +++ b/app/src/util/net.c @@ -1,5 +1,6 @@ #include "net.h" +#include #include #include @@ -55,6 +56,7 @@ wrap(sc_raw_socket sock) { } socket->socket = sock; + socket->closed = (atomic_flag) ATOMIC_FLAG_INIT; return socket; #else @@ -195,18 +197,33 @@ net_send_all(sc_socket socket, const void *buf, size_t len) { } bool -net_shutdown(sc_socket socket, int how) { +net_interrupt(sc_socket socket) { + assert(socket != SC_INVALID_SOCKET); + sc_raw_socket raw_sock = unwrap(socket); - return !shutdown(raw_sock, how); + +#ifdef __WINDOWS__ + if (!atomic_flag_test_and_set(&socket->closed)) { + return !closesocket(raw_sock); + } + return true; +#else + return !shutdown(raw_sock, SHUT_RDWR); +#endif } +#include bool net_close(sc_socket socket) { sc_raw_socket raw_sock = unwrap(socket); #ifdef __WINDOWS__ + bool ret = true; + if (!atomic_flag_test_and_set(&socket->closed)) { + ret = !closesocket(raw_sock); + } free(socket); - return !closesocket(raw_sock); + return ret; #else return !close(raw_sock); #endif diff --git a/app/src/util/net.h b/app/src/util/net.h index f40f0bb5..c742c00e 100644 --- a/app/src/util/net.h +++ b/app/src/util/net.h @@ -10,12 +10,11 @@ #ifdef __WINDOWS__ # include -# define SHUT_RD SD_RECEIVE -# define SHUT_WR SD_SEND -# define SHUT_RDWR SD_BOTH +# include # define SC_INVALID_SOCKET NULL typedef struct sc_socket_windows { SOCKET socket; + atomic_flag closed; } *sc_socket; #else // not __WINDOWS__ @@ -53,10 +52,13 @@ net_send(sc_socket socket, const void *buf, size_t len); ssize_t net_send_all(sc_socket socket, const void *buf, size_t len); -// how is SHUT_RD (read), SHUT_WR (write) or SHUT_RDWR (both) +// Shutdown the socket (or close on Windows) so that any blocking send() or +// recv() are interrupted. bool -net_shutdown(sc_socket socket, int how); +net_interrupt(sc_socket socket); +// Close the socket. +// A socket must always be closed, even if net_interrupt() has been called. bool net_close(sc_socket socket); From e4d5c1ce363919e399958cf6342f63724ce4dd51 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 27 Oct 2021 18:43:47 +0200 Subject: [PATCH 0712/2244] Move scrcpy option structs to options.h This will allow to define symbols in options.c without all the dependencies of scrcpy.c. --- app/src/aoa_hid.h | 1 - app/src/cli.c | 2 +- app/src/cli.h | 2 +- app/src/input_manager.h | 2 +- app/src/keyboard_inject.h | 2 +- app/src/main.c | 4 +- app/src/mouse_inject.h | 1 - app/src/options.h | 160 ++++++++++++++++++++++++++++++++++++++ app/src/recorder.h | 2 +- app/src/scrcpy.h | 153 +----------------------------------- app/src/screen.c | 2 +- app/src/server.h | 2 +- app/src/util/log.h | 2 +- app/tests/test_cli.c | 2 +- 14 files changed, 172 insertions(+), 165 deletions(-) create mode 100644 app/src/options.h diff --git a/app/src/aoa_hid.h b/app/src/aoa_hid.h index 11cc57b8..24cef502 100644 --- a/app/src/aoa_hid.h +++ b/app/src/aoa_hid.h @@ -6,7 +6,6 @@ #include -#include "scrcpy.h" #include "util/cbuf.h" #include "util/thread.h" #include "util/tick.h" diff --git a/app/src/cli.c b/app/src/cli.c index a0a508af..cab25772 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -6,7 +6,7 @@ #include #include -#include "scrcpy.h" +#include "options.h" #include "util/log.h" #include "util/str_util.h" diff --git a/app/src/cli.h b/app/src/cli.h index 419f156f..b9361a9c 100644 --- a/app/src/cli.h +++ b/app/src/cli.h @@ -5,7 +5,7 @@ #include -#include "scrcpy.h" +#include "options.h" struct scrcpy_cli_args { struct scrcpy_options opts; diff --git a/app/src/input_manager.h b/app/src/input_manager.h index bd9d7a1b..f018f98a 100644 --- a/app/src/input_manager.h +++ b/app/src/input_manager.h @@ -9,7 +9,7 @@ #include "controller.h" #include "fps_counter.h" -#include "scrcpy.h" +#include "options.h" #include "screen.h" #include "trait/key_processor.h" #include "trait/mouse_processor.h" diff --git a/app/src/keyboard_inject.h b/app/src/keyboard_inject.h index e59de46d..f4ebe40e 100644 --- a/app/src/keyboard_inject.h +++ b/app/src/keyboard_inject.h @@ -6,7 +6,7 @@ #include #include "controller.h" -#include "scrcpy.h" +#include "options.h" #include "trait/key_processor.h" struct sc_keyboard_inject { diff --git a/app/src/main.c b/app/src/main.c index 2afa3c4e..51c13fd5 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -1,5 +1,3 @@ -#include "scrcpy.h" - #include "common.h" #include @@ -13,6 +11,8 @@ #include #include "cli.h" +#include "options.h" +#include "scrcpy.h" #include "util/log.h" static void diff --git a/app/src/mouse_inject.h b/app/src/mouse_inject.h index d5220db9..7dcf7e83 100644 --- a/app/src/mouse_inject.h +++ b/app/src/mouse_inject.h @@ -6,7 +6,6 @@ #include #include "controller.h" -#include "scrcpy.h" #include "screen.h" #include "trait/mouse_processor.h" diff --git a/app/src/options.h b/app/src/options.h new file mode 100644 index 00000000..04b8f6d2 --- /dev/null +++ b/app/src/options.h @@ -0,0 +1,160 @@ +#ifndef SCRCPY_OPTIONS_H +#define SCRCPY_OPTIONS_H + +#include "common.h" + +#include +#include +#include + +#include "util/tick.h" + +enum sc_log_level { + SC_LOG_LEVEL_VERBOSE, + 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, +}; + +enum sc_lock_video_orientation { + SC_LOCK_VIDEO_ORIENTATION_UNLOCKED = -1, + // lock the current orientation when scrcpy starts + SC_LOCK_VIDEO_ORIENTATION_INITIAL = -2, + SC_LOCK_VIDEO_ORIENTATION_0 = 0, + SC_LOCK_VIDEO_ORIENTATION_1, + SC_LOCK_VIDEO_ORIENTATION_2, + SC_LOCK_VIDEO_ORIENTATION_3, +}; + +enum sc_keyboard_input_mode { + SC_KEYBOARD_INPUT_MODE_INJECT, + SC_KEYBOARD_INPUT_MODE_HID, +}; + +#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; +}; + +#define SC_WINDOW_POSITION_UNDEFINED (-0x8000) + +struct scrcpy_options { + const char *serial; + const char *crop; + const char *record_filename; + const char *window_title; + const char *push_target; + const char *render_driver; + const char *codec_options; + const char *encoder_name; + const char *v4l2_device; + enum sc_log_level log_level; + enum sc_record_format record_format; + enum sc_keyboard_input_mode keyboard_input_mode; + struct sc_port_range port_range; + struct sc_shortcut_mods shortcut_mods; + uint16_t max_size; + uint32_t bit_rate; + uint16_t max_fps; + enum sc_lock_video_orientation lock_video_orientation; + uint8_t rotation; + 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; + uint32_t display_id; + sc_tick display_buffer; + sc_tick v4l2_buffer; + bool show_touches; + bool fullscreen; + bool always_on_top; + bool control; + bool display; + bool turn_screen_off; + bool prefer_text; + bool window_borderless; + bool mipmaps; + bool stay_awake; + bool force_adb_forward; + bool disable_screensaver; + bool forward_key_repeat; + bool forward_all_clicks; + bool legacy_paste; + bool power_off_on_close; +}; + +#define SCRCPY_OPTIONS_DEFAULT { \ + .serial = NULL, \ + .crop = NULL, \ + .record_filename = NULL, \ + .window_title = NULL, \ + .push_target = NULL, \ + .render_driver = NULL, \ + .codec_options = NULL, \ + .encoder_name = NULL, \ + .v4l2_device = NULL, \ + .log_level = SC_LOG_LEVEL_INFO, \ + .record_format = SC_RECORD_FORMAT_AUTO, \ + .keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_INJECT, \ + .port_range = { \ + .first = DEFAULT_LOCAL_PORT_RANGE_FIRST, \ + .last = DEFAULT_LOCAL_PORT_RANGE_LAST, \ + }, \ + .shortcut_mods = { \ + .data = {SC_MOD_LALT, SC_MOD_LSUPER}, \ + .count = 2, \ + }, \ + .max_size = 0, \ + .bit_rate = DEFAULT_BIT_RATE, \ + .max_fps = 0, \ + .lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_UNLOCKED, \ + .rotation = 0, \ + .window_x = SC_WINDOW_POSITION_UNDEFINED, \ + .window_y = SC_WINDOW_POSITION_UNDEFINED, \ + .window_width = 0, \ + .window_height = 0, \ + .display_id = 0, \ + .display_buffer = 0, \ + .v4l2_buffer = 0, \ + .show_touches = false, \ + .fullscreen = false, \ + .always_on_top = false, \ + .control = true, \ + .display = true, \ + .turn_screen_off = false, \ + .prefer_text = false, \ + .window_borderless = false, \ + .mipmaps = true, \ + .stay_awake = false, \ + .force_adb_forward = false, \ + .disable_screensaver = false, \ + .forward_key_repeat = true, \ + .forward_all_clicks = false, \ + .legacy_paste = false, \ + .power_off_on_close = false, \ +} + +#endif diff --git a/app/src/recorder.h b/app/src/recorder.h index 96caaf5f..f14523ff 100644 --- a/app/src/recorder.h +++ b/app/src/recorder.h @@ -7,7 +7,7 @@ #include #include "coords.h" -#include "scrcpy.h" +#include "options.h" #include "trait/packet_sink.h" #include "util/queue.h" #include "util/thread.h" diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index 8cf4a917..cdcecda7 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -4,158 +4,7 @@ #include "common.h" #include -#include -#include - -#include "util/tick.h" - -enum sc_log_level { - SC_LOG_LEVEL_VERBOSE, - 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, -}; - -enum sc_lock_video_orientation { - SC_LOCK_VIDEO_ORIENTATION_UNLOCKED = -1, - // lock the current orientation when scrcpy starts - SC_LOCK_VIDEO_ORIENTATION_INITIAL = -2, - SC_LOCK_VIDEO_ORIENTATION_0 = 0, - SC_LOCK_VIDEO_ORIENTATION_1, - SC_LOCK_VIDEO_ORIENTATION_2, - SC_LOCK_VIDEO_ORIENTATION_3, -}; - -enum sc_keyboard_input_mode { - SC_KEYBOARD_INPUT_MODE_INJECT, - SC_KEYBOARD_INPUT_MODE_HID, -}; - -#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; -}; - -#define SC_WINDOW_POSITION_UNDEFINED (-0x8000) - -struct scrcpy_options { - const char *serial; - const char *crop; - const char *record_filename; - const char *window_title; - const char *push_target; - const char *render_driver; - const char *codec_options; - const char *encoder_name; - const char *v4l2_device; - enum sc_log_level log_level; - enum sc_record_format record_format; - enum sc_keyboard_input_mode keyboard_input_mode; - struct sc_port_range port_range; - struct sc_shortcut_mods shortcut_mods; - uint16_t max_size; - uint32_t bit_rate; - uint16_t max_fps; - enum sc_lock_video_orientation lock_video_orientation; - uint8_t rotation; - 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; - uint32_t display_id; - sc_tick display_buffer; - sc_tick v4l2_buffer; - bool show_touches; - bool fullscreen; - bool always_on_top; - bool control; - bool display; - bool turn_screen_off; - bool prefer_text; - bool window_borderless; - bool mipmaps; - bool stay_awake; - bool force_adb_forward; - bool disable_screensaver; - bool forward_key_repeat; - bool forward_all_clicks; - bool legacy_paste; - bool power_off_on_close; -}; - -#define SCRCPY_OPTIONS_DEFAULT { \ - .serial = NULL, \ - .crop = NULL, \ - .record_filename = NULL, \ - .window_title = NULL, \ - .push_target = NULL, \ - .render_driver = NULL, \ - .codec_options = NULL, \ - .encoder_name = NULL, \ - .v4l2_device = NULL, \ - .log_level = SC_LOG_LEVEL_INFO, \ - .record_format = SC_RECORD_FORMAT_AUTO, \ - .keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_INJECT, \ - .port_range = { \ - .first = DEFAULT_LOCAL_PORT_RANGE_FIRST, \ - .last = DEFAULT_LOCAL_PORT_RANGE_LAST, \ - }, \ - .shortcut_mods = { \ - .data = {SC_MOD_LALT, SC_MOD_LSUPER}, \ - .count = 2, \ - }, \ - .max_size = 0, \ - .bit_rate = DEFAULT_BIT_RATE, \ - .max_fps = 0, \ - .lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_UNLOCKED, \ - .rotation = 0, \ - .window_x = SC_WINDOW_POSITION_UNDEFINED, \ - .window_y = SC_WINDOW_POSITION_UNDEFINED, \ - .window_width = 0, \ - .window_height = 0, \ - .display_id = 0, \ - .display_buffer = 0, \ - .v4l2_buffer = 0, \ - .show_touches = false, \ - .fullscreen = false, \ - .always_on_top = false, \ - .control = true, \ - .display = true, \ - .turn_screen_off = false, \ - .prefer_text = false, \ - .window_borderless = false, \ - .mipmaps = true, \ - .stay_awake = false, \ - .force_adb_forward = false, \ - .disable_screensaver = false, \ - .forward_key_repeat = true, \ - .forward_all_clicks = false, \ - .legacy_paste = false, \ - .power_off_on_close = false, \ -} +#include "options.h" bool scrcpy(struct scrcpy_options *options); diff --git a/app/src/screen.c b/app/src/screen.c index 8a2748a9..7f442143 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -6,7 +6,7 @@ #include "events.h" #include "icon.h" -#include "scrcpy.h" +#include "options.h" #include "video_buffer.h" #include "util/log.h" diff --git a/app/src/server.h b/app/src/server.h index 141075f7..75594522 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -9,7 +9,7 @@ #include "adb.h" #include "coords.h" -#include "scrcpy.h" +#include "options.h" #include "util/log.h" #include "util/net.h" #include "util/thread.h" diff --git a/app/src/util/log.h b/app/src/util/log.h index 30934b5c..4157d6e5 100644 --- a/app/src/util/log.h +++ b/app/src/util/log.h @@ -5,7 +5,7 @@ #include -#include "scrcpy.h" +#include "options.h" #define LOGV(...) SDL_LogVerbose(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__) #define LOGD(...) SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__) diff --git a/app/tests/test_cli.c b/app/tests/test_cli.c index 94740a9a..1682a72d 100644 --- a/app/tests/test_cli.c +++ b/app/tests/test_cli.c @@ -4,7 +4,7 @@ #include #include "cli.h" -#include "scrcpy.h" +#include "options.h" static void test_flag_version(void) { struct scrcpy_cli_args args = { From 27fa23846d5e82d0709001e045f35ca6572bcb5d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 27 Oct 2021 18:43:47 +0200 Subject: [PATCH 0713/2244] Define default options as const struct This is more readable than a macro, and we could ifdef some fields. --- app/meson.build | 2 ++ app/src/main.c | 2 +- app/src/options.c | 52 ++++++++++++++++++++++++++++++++++++++++++++ app/src/options.h | 51 +------------------------------------------ app/tests/test_cli.c | 8 +++---- 5 files changed, 60 insertions(+), 55 deletions(-) create mode 100644 app/src/options.c diff --git a/app/meson.build b/app/meson.build index f82d37b3..2ced151b 100644 --- a/app/meson.build +++ b/app/meson.build @@ -16,6 +16,7 @@ src = [ 'src/keyboard_inject.c', 'src/mouse_inject.c', 'src/opengl.c', + 'src/options.c', 'src/receiver.c', 'src/recorder.c', 'src/scrcpy.c', @@ -184,6 +185,7 @@ if get_option('buildtype') == 'debug' ['test_cli', [ 'tests/test_cli.c', 'src/cli.c', + 'src/options.c', 'src/util/str_util.c', ]], ['test_clock', [ diff --git a/app/src/main.c b/app/src/main.c index 51c13fd5..831b98fa 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -48,7 +48,7 @@ main(int argc, char *argv[]) { #endif struct scrcpy_cli_args args = { - .opts = SCRCPY_OPTIONS_DEFAULT, + .opts = scrcpy_options_default, .help = false, .version = false, }; diff --git a/app/src/options.c b/app/src/options.c new file mode 100644 index 00000000..a1722158 --- /dev/null +++ b/app/src/options.c @@ -0,0 +1,52 @@ +#include "options.h" + +const struct scrcpy_options scrcpy_options_default = { + .serial = NULL, + .crop = NULL, + .record_filename = NULL, + .window_title = NULL, + .push_target = NULL, + .render_driver = NULL, + .codec_options = NULL, + .encoder_name = NULL, + .v4l2_device = NULL, + .log_level = SC_LOG_LEVEL_INFO, + .record_format = SC_RECORD_FORMAT_AUTO, + .keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_INJECT, + .port_range = { + .first = DEFAULT_LOCAL_PORT_RANGE_FIRST, + .last = DEFAULT_LOCAL_PORT_RANGE_LAST, + }, + .shortcut_mods = { + .data = {SC_MOD_LALT, SC_MOD_LSUPER}, + .count = 2, + }, + .max_size = 0, + .bit_rate = DEFAULT_BIT_RATE, + .max_fps = 0, + .lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_UNLOCKED, + .rotation = 0, + .window_x = SC_WINDOW_POSITION_UNDEFINED, + .window_y = SC_WINDOW_POSITION_UNDEFINED, + .window_width = 0, + .window_height = 0, + .display_id = 0, + .display_buffer = 0, + .v4l2_buffer = 0, + .show_touches = false, + .fullscreen = false, + .always_on_top = false, + .control = true, + .display = true, + .turn_screen_off = false, + .prefer_text = false, + .window_borderless = false, + .mipmaps = true, + .stay_awake = false, + .force_adb_forward = false, + .disable_screensaver = false, + .forward_key_repeat = true, + .forward_all_clicks = false, + .legacy_paste = false, + .power_off_on_close = false, +}; diff --git a/app/src/options.h b/app/src/options.h index 04b8f6d2..23583f37 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -106,55 +106,6 @@ struct scrcpy_options { bool power_off_on_close; }; -#define SCRCPY_OPTIONS_DEFAULT { \ - .serial = NULL, \ - .crop = NULL, \ - .record_filename = NULL, \ - .window_title = NULL, \ - .push_target = NULL, \ - .render_driver = NULL, \ - .codec_options = NULL, \ - .encoder_name = NULL, \ - .v4l2_device = NULL, \ - .log_level = SC_LOG_LEVEL_INFO, \ - .record_format = SC_RECORD_FORMAT_AUTO, \ - .keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_INJECT, \ - .port_range = { \ - .first = DEFAULT_LOCAL_PORT_RANGE_FIRST, \ - .last = DEFAULT_LOCAL_PORT_RANGE_LAST, \ - }, \ - .shortcut_mods = { \ - .data = {SC_MOD_LALT, SC_MOD_LSUPER}, \ - .count = 2, \ - }, \ - .max_size = 0, \ - .bit_rate = DEFAULT_BIT_RATE, \ - .max_fps = 0, \ - .lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_UNLOCKED, \ - .rotation = 0, \ - .window_x = SC_WINDOW_POSITION_UNDEFINED, \ - .window_y = SC_WINDOW_POSITION_UNDEFINED, \ - .window_width = 0, \ - .window_height = 0, \ - .display_id = 0, \ - .display_buffer = 0, \ - .v4l2_buffer = 0, \ - .show_touches = false, \ - .fullscreen = false, \ - .always_on_top = false, \ - .control = true, \ - .display = true, \ - .turn_screen_off = false, \ - .prefer_text = false, \ - .window_borderless = false, \ - .mipmaps = true, \ - .stay_awake = false, \ - .force_adb_forward = false, \ - .disable_screensaver = false, \ - .forward_key_repeat = true, \ - .forward_all_clicks = false, \ - .legacy_paste = false, \ - .power_off_on_close = false, \ -} +extern const struct scrcpy_options scrcpy_options_default; #endif diff --git a/app/tests/test_cli.c b/app/tests/test_cli.c index 1682a72d..05bacbf8 100644 --- a/app/tests/test_cli.c +++ b/app/tests/test_cli.c @@ -8,7 +8,7 @@ static void test_flag_version(void) { struct scrcpy_cli_args args = { - .opts = SCRCPY_OPTIONS_DEFAULT, + .opts = scrcpy_options_default, .help = false, .version = false, }; @@ -23,7 +23,7 @@ static void test_flag_version(void) { static void test_flag_help(void) { struct scrcpy_cli_args args = { - .opts = SCRCPY_OPTIONS_DEFAULT, + .opts = scrcpy_options_default, .help = false, .version = false, }; @@ -38,7 +38,7 @@ static void test_flag_help(void) { static void test_options(void) { struct scrcpy_cli_args args = { - .opts = SCRCPY_OPTIONS_DEFAULT, + .opts = scrcpy_options_default, .help = false, .version = false, }; @@ -100,7 +100,7 @@ static void test_options(void) { static void test_options2(void) { struct scrcpy_cli_args args = { - .opts = SCRCPY_OPTIONS_DEFAULT, + .opts = scrcpy_options_default, .help = false, .version = false, }; From 34eb10ea0b736ff9e66716bc70d5f8e87411a0ec Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 27 Oct 2021 18:43:47 +0200 Subject: [PATCH 0714/2244] Define v4l2 option field only if HAVE_V4L2 --- app/src/options.c | 2 ++ app/src/options.h | 2 ++ 2 files changed, 4 insertions(+) diff --git a/app/src/options.c b/app/src/options.c index a1722158..82f25342 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -9,7 +9,9 @@ const struct scrcpy_options scrcpy_options_default = { .render_driver = NULL, .codec_options = NULL, .encoder_name = NULL, +#ifdef HAVE_V4L2 .v4l2_device = NULL, +#endif .log_level = SC_LOG_LEVEL_INFO, .record_format = SC_RECORD_FORMAT_AUTO, .keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_INJECT, diff --git a/app/src/options.h b/app/src/options.h index 23583f37..434225b9 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -70,7 +70,9 @@ struct scrcpy_options { const char *render_driver; const char *codec_options; const char *encoder_name; +#ifdef HAVE_V4L2 const char *v4l2_device; +#endif enum sc_log_level log_level; enum sc_record_format record_format; enum sc_keyboard_input_mode keyboard_input_mode; From 06131ef634a1204da6c4f663cc5f9d71f2b456f1 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 29 Oct 2021 12:21:34 +0200 Subject: [PATCH 0715/2244] Fix typo in clock comments --- app/src/clock.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/clock.h b/app/src/clock.h index eb7fa594..886d1f4d 100644 --- a/app/src/clock.h +++ b/app/src/clock.h @@ -26,7 +26,7 @@ struct sc_clock_point { * array. * * To estimate the slope, it splits the last SC_CLOCK_RANGE points into two - * sets of SC_CLOCK_RANGE/2 points, and compute their centroid ("average + * sets of SC_CLOCK_RANGE/2 points, and computes their centroid ("average * point"). The slope of the estimated affine function is that of the line * passing through these two points. * From 8bf28e9f5340d2a54dbc9b2c9d924b7ee4fa8d31 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 30 Oct 2021 11:23:51 +0200 Subject: [PATCH 0716/2244] Upgrade Android SDK to 31 --- server/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/build.gradle b/server/build.gradle index 7cd7dbd7..ef936d1b 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -1,11 +1,11 @@ apply plugin: 'com.android.application' android { - compileSdkVersion 30 + compileSdkVersion 31 defaultConfig { applicationId "com.genymobile.scrcpy" minSdkVersion 21 - targetSdkVersion 30 + targetSdkVersion 31 versionCode 11900 versionName "1.19" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" From 0c9666b733ef16e1c250d59773120d2635318e5f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 30 Oct 2021 11:23:51 +0200 Subject: [PATCH 0717/2244] Upgrade Android checkstyle to 9.0.1 Adapt checkstyle.xml to match the latest version, and remove a line break between imports which trigger a checkstyle volation. --- config/android-checkstyle.gradle | 2 +- config/checkstyle/checkstyle.xml | 35 +++++-------------- .../scrcpy/ControlMessageReaderTest.java | 1 - 3 files changed, 9 insertions(+), 29 deletions(-) diff --git a/config/android-checkstyle.gradle b/config/android-checkstyle.gradle index f998530e..29c67b19 100644 --- a/config/android-checkstyle.gradle +++ b/config/android-checkstyle.gradle @@ -2,7 +2,7 @@ apply plugin: 'checkstyle' check.dependsOn 'checkstyle' checkstyle { - toolVersion = '6.19' + toolVersion = '9.0.1' } task checkstyle(type: Checkstyle) { diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index 812d060b..edda3919 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -37,6 +37,14 @@ page at http://checkstyle.sourceforge.net/config.html --> + + + + + + + + @@ -72,13 +80,6 @@ page at http://checkstyle.sourceforge.net/config.html --> - - - - - - - @@ -152,26 +153,6 @@ page at http://checkstyle.sourceforge.net/config.html --> - - - - - - - - - - - - - - - - - - - - diff --git a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java index da568486..7f3d3f61 100644 --- a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java @@ -2,7 +2,6 @@ package com.genymobile.scrcpy; import android.view.KeyEvent; import android.view.MotionEvent; - import org.junit.Assert; import org.junit.Test; From db484d82dbb41f3aeca6a02e4b818ad54af03a47 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 30 Oct 2021 11:32:48 +0200 Subject: [PATCH 0718/2244] Upgrade gradle build tools to 7.0.2 --- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index c977c398..6ed7e4c6 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:4.0.1' + classpath 'com.android.tools.build:gradle:7.0.2' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a4b44297..29e41345 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-6.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 4c4381de4cf2677376d1727d6fa040937c2c5d03 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 30 Oct 2021 15:20:39 +0200 Subject: [PATCH 0719/2244] Use sc_ prefix for size, position and point --- app/src/control_msg.c | 2 +- app/src/control_msg.h | 4 +- app/src/coords.h | 10 ++--- app/src/input_manager.c | 14 +++---- app/src/mouse_inject.c | 2 +- app/src/recorder.c | 2 +- app/src/recorder.h | 4 +- app/src/scrcpy.c | 2 +- app/src/screen.c | 82 ++++++++++++++++++++--------------------- app/src/screen.h | 12 +++--- app/src/server.c | 5 ++- app/src/server.h | 3 +- app/src/v4l2_sink.c | 2 +- app/src/v4l2_sink.h | 4 +- 14 files changed, 75 insertions(+), 73 deletions(-) diff --git a/app/src/control_msg.c b/app/src/control_msg.c index 1257010e..a6a020bd 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -56,7 +56,7 @@ static const char *const screen_power_mode_labels[] = { }; static void -write_position(uint8_t *buf, const struct position *position) { +write_position(uint8_t *buf, const struct sc_position *position) { buffer_write32be(&buf[0], position->point.x); buffer_write32be(&buf[4], position->point.y); buffer_write16be(&buf[8], position->screen_size.width); diff --git a/app/src/control_msg.h b/app/src/control_msg.h index 920a493a..16492849 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -57,11 +57,11 @@ struct control_msg { enum android_motionevent_action action; enum android_motionevent_buttons buttons; uint64_t pointer_id; - struct position position; + struct sc_position position; float pressure; } inject_touch_event; struct { - struct position position; + struct sc_position position; int32_t hscroll; int32_t vscroll; } inject_scroll_event; diff --git a/app/src/coords.h b/app/src/coords.h index 7be6836d..cdabb782 100644 --- a/app/src/coords.h +++ b/app/src/coords.h @@ -3,22 +3,22 @@ #include -struct size { +struct sc_size { uint16_t width; uint16_t height; }; -struct point { +struct sc_point { int32_t x; int32_t y; }; -struct position { +struct sc_position { // The video screen size may be different from the real device screen size, // so store to which size the absolute position apply, to scale it // accordingly. - struct size screen_size; - struct point point; + struct sc_size screen_size; + struct sc_point point; }; #endif diff --git a/app/src/input_manager.c b/app/src/input_manager.c index bba3c998..b84f3bea 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -332,7 +332,7 @@ 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) { + struct sc_point point) { bool up = action == AMOTION_EVENT_ACTION_UP; struct control_msg msg; @@ -352,8 +352,8 @@ simulate_virtual_finger(struct input_manager *im, return true; } -static struct point -inverse_point(struct point point, struct size size) { +static struct sc_point +inverse_point(struct sc_point point, struct sc_size size) { point.x = size.width - point.x; point.y = size.height - point.y; return point; @@ -545,10 +545,10 @@ input_manager_process_mouse_motion(struct input_manager *im, im->mp->ops->process_mouse_motion(im->mp, event); if (im->vfinger_down) { - struct point mouse = + struct sc_point mouse = screen_convert_window_to_frame_coords(im->screen, event->x, event->y); - struct point vfinger = inverse_point(mouse, im->screen->frame_size); + struct sc_point vfinger = inverse_point(mouse, im->screen->frame_size); simulate_virtual_finger(im, AMOTION_EVENT_ACTION_MOVE, vfinger); } } @@ -630,10 +630,10 @@ input_manager_process_mouse_button(struct input_manager *im, #define CTRL_PRESSED (SDL_GetModState() & (KMOD_LCTRL | KMOD_RCTRL)) if ((down && !im->vfinger_down && CTRL_PRESSED) || (!down && im->vfinger_down)) { - struct point mouse = + struct sc_point mouse = screen_convert_window_to_frame_coords(im->screen, event->x, event->y); - struct point vfinger = inverse_point(mouse, im->screen->frame_size); + struct sc_point vfinger = inverse_point(mouse, im->screen->frame_size); enum android_motionevent_action action = down ? AMOTION_EVENT_ACTION_DOWN : AMOTION_EVENT_ACTION_UP; diff --git a/app/src/mouse_inject.c b/app/src/mouse_inject.c index 008da267..1d5fe230 100644 --- a/app/src/mouse_inject.c +++ b/app/src/mouse_inject.c @@ -125,7 +125,7 @@ convert_mouse_wheel(const SDL_MouseWheelEvent *from, struct screen *screen, int mouse_y; SDL_GetMouseState(&mouse_x, &mouse_y); - struct position position = { + struct sc_position position = { .screen_size = screen->frame_size, .point = screen_convert_window_to_frame_coords(screen, mouse_x, mouse_y), diff --git a/app/src/recorder.c b/app/src/recorder.c index 3b5fe070..a690e2de 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -372,7 +372,7 @@ bool recorder_init(struct recorder *recorder, const char *filename, enum sc_record_format format, - struct size declared_frame_size) { + struct sc_size declared_frame_size) { recorder->filename = strdup(filename); if (!recorder->filename) { LOGE("Could not strdup filename"); diff --git a/app/src/recorder.h b/app/src/recorder.h index f14523ff..27ea5526 100644 --- a/app/src/recorder.h +++ b/app/src/recorder.h @@ -25,7 +25,7 @@ struct recorder { char *filename; enum sc_record_format format; AVFormatContext *ctx; - struct size declared_frame_size; + struct sc_size declared_frame_size; bool header_written; sc_thread thread; @@ -44,7 +44,7 @@ struct recorder { bool recorder_init(struct recorder *recorder, const char *filename, - enum sc_record_format format, struct size declared_frame_size); + enum sc_record_format format, struct sc_size declared_frame_size); void recorder_destroy(struct recorder *recorder); diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index bfea6642..ea71d4a1 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -309,7 +309,7 @@ scrcpy(struct scrcpy_options *options) { } char device_name[DEVICE_NAME_FIELD_LENGTH]; - struct size frame_size; + struct sc_size frame_size; if (!server_connect_to(&s->server, device_name, &frame_size)) { goto end; diff --git a/app/src/screen.c b/app/src/screen.c index 7f442143..f82f5003 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -14,9 +14,9 @@ #define DOWNCAST(SINK) container_of(SINK, struct screen, frame_sink) -static inline struct size -get_rotated_size(struct size size, int rotation) { - struct size rotated_size; +static inline struct sc_size +get_rotated_size(struct sc_size size, int rotation) { + struct sc_size rotated_size; if (rotation & 1) { rotated_size.width = size.height; rotated_size.height = size.width; @@ -27,26 +27,26 @@ get_rotated_size(struct size size, int rotation) { return rotated_size; } -// get the window size in a struct size -static struct size +// get the window size in a struct sc_size +static struct sc_size get_window_size(const struct screen *screen) { int width; int height; SDL_GetWindowSize(screen->window, &width, &height); - struct size size; + struct sc_size size; size.width = width; size.height = height; return size; } -static struct point +static struct sc_point get_window_position(const struct screen *screen) { int x; int y; SDL_GetWindowPosition(screen->window, &x, &y); - struct point point; + struct sc_point point; point.x = x; point.y = y; return point; @@ -54,7 +54,7 @@ get_window_position(const struct screen *screen) { // set the window size to be applied when fullscreen is disabled static void -set_window_size(struct screen *screen, struct size new_size) { +set_window_size(struct screen *screen, struct sc_size new_size) { assert(!screen->fullscreen); assert(!screen->maximized); SDL_SetWindowSize(screen->window, new_size.width, new_size.height); @@ -62,7 +62,7 @@ set_window_size(struct screen *screen, struct size new_size) { // get the preferred display bounds (i.e. the screen bounds with some margins) static bool -get_preferred_display_bounds(struct size *bounds) { +get_preferred_display_bounds(struct sc_size *bounds) { SDL_Rect rect; #ifdef SCRCPY_SDL_HAS_GET_DISPLAY_USABLE_BOUNDS # define GET_DISPLAY_BOUNDS(i, r) SDL_GetDisplayUsableBounds((i), (r)) @@ -80,7 +80,7 @@ get_preferred_display_bounds(struct size *bounds) { } static bool -is_optimal_size(struct size current_size, struct size content_size) { +is_optimal_size(struct sc_size current_size, struct sc_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 @@ -94,16 +94,16 @@ is_optimal_size(struct size current_size, struct size content_size) { // crops the black borders) // - 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 content_size) { +static struct sc_size +get_optimal_size(struct sc_size current_size, struct sc_size content_size) { if (content_size.width == 0 || content_size.height == 0) { // avoid division by 0 return current_size; } - struct size window_size; + struct sc_size window_size; - struct size display_size; + struct sc_size display_size; if (!get_preferred_display_bounds(&display_size)) { // could not get display bounds, do not constraint the size window_size.width = current_size.width; @@ -135,10 +135,10 @@ get_optimal_size(struct size current_size, struct 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 content_size, uint16_t req_width, +static inline struct sc_size +get_initial_optimal_size(struct sc_size content_size, uint16_t req_width, uint16_t req_height) { - struct size window_size; + struct sc_size window_size; if (!req_width && !req_height) { window_size = get_optimal_size(content_size, content_size); } else { @@ -166,9 +166,9 @@ screen_update_content_rect(struct screen *screen) { int dh; SDL_GL_GetDrawableSize(screen->window, &dw, &dh); - struct size content_size = screen->content_size; + struct sc_size content_size = screen->content_size; // The drawable size is the window size * the HiDPI scale - struct size drawable_size = {dw, dh}; + struct sc_size drawable_size = {dw, dh}; SDL_Rect *rect = &screen->rect; @@ -200,7 +200,7 @@ screen_update_content_rect(struct screen *screen) { static inline SDL_Texture * create_texture(struct screen *screen) { SDL_Renderer *renderer = screen->renderer; - struct size size = screen->frame_size; + struct sc_size size = screen->frame_size; SDL_Texture *texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_YV12, SDL_TEXTUREACCESS_STREAMING, size.width, size.height); @@ -330,13 +330,13 @@ screen_init(struct screen *screen, const struct screen_params *params) { if (screen->rotation) { LOGI("Initial display rotation set to %u", screen->rotation); } - struct size content_size = + struct sc_size content_size = get_rotated_size(screen->frame_size, screen->rotation); screen->content_size = content_size; - struct size window_size = get_initial_optimal_size(content_size, - params->window_width, - params->window_height); + struct sc_size window_size = + get_initial_optimal_size(content_size,params->window_width, + params->window_height); uint32_t window_flags = SDL_WINDOW_HIDDEN | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI; @@ -508,10 +508,10 @@ screen_destroy(struct screen *screen) { } static void -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 = { +resize_for_content(struct screen *screen, struct sc_size old_content_size, + struct sc_size new_content_size) { + struct sc_size window_size = get_window_size(screen); + struct sc_size target_size = { .width = (uint32_t) window_size.width * new_content_size.width / old_content_size.width, .height = (uint32_t) window_size.height * new_content_size.height @@ -522,7 +522,7 @@ resize_for_content(struct screen *screen, struct size old_content_size, } static void -set_content_size(struct screen *screen, struct size new_content_size) { +set_content_size(struct screen *screen, struct sc_size new_content_size) { if (!screen->fullscreen && !screen->maximized) { resize_for_content(screen, screen->content_size, new_content_size); } else if (!screen->resize_pending) { @@ -553,7 +553,7 @@ screen_set_rotation(struct screen *screen, unsigned rotation) { return; } - struct size new_content_size = + struct sc_size new_content_size = get_rotated_size(screen->frame_size, rotation); set_content_size(screen, new_content_size); @@ -566,7 +566,7 @@ screen_set_rotation(struct screen *screen, unsigned rotation) { // 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) { +prepare_for_frame(struct screen *screen, struct sc_size new_frame_size) { if (screen->frame_size.width != new_frame_size.width || screen->frame_size.height != new_frame_size.height) { // frame dimension changed, destroy texture @@ -574,7 +574,7 @@ prepare_for_frame(struct screen *screen, struct size new_frame_size) { screen->frame_size = new_frame_size; - struct size new_content_size = + struct sc_size new_content_size = get_rotated_size(new_frame_size, screen->rotation); set_content_size(screen, new_content_size); @@ -615,7 +615,7 @@ screen_update_frame(struct screen *screen) { fps_counter_add_rendered_frame(&screen->fps_counter); - struct size new_frame_size = {frame->width, frame->height}; + struct sc_size new_frame_size = {frame->width, frame->height}; if (!prepare_for_frame(screen, new_frame_size)) { return false; } @@ -682,10 +682,10 @@ screen_resize_to_fit(struct screen *screen) { return; } - struct point point = get_window_position(screen); - struct size window_size = get_window_size(screen); + struct sc_point point = get_window_position(screen); + struct sc_size window_size = get_window_size(screen); - struct size optimal_size = + struct sc_size optimal_size = get_optimal_size(window_size, screen->content_size); // Center the window related to the device screen @@ -711,7 +711,7 @@ screen_resize_to_pixel_perfect(struct screen *screen) { screen->maximized = false; } - struct size content_size = screen->content_size; + struct sc_size content_size = screen->content_size; SDL_SetWindowSize(screen->window, content_size.width, content_size.height); LOGD("Resized to pixel-perfect: %ux%u", content_size.width, content_size.height); @@ -766,7 +766,7 @@ screen_handle_event(struct screen *screen, SDL_Event *event) { return false; } -struct point +struct sc_point screen_convert_drawable_to_frame_coords(struct screen *screen, int32_t x, int32_t y) { unsigned rotation = screen->rotation; @@ -780,7 +780,7 @@ screen_convert_drawable_to_frame_coords(struct screen *screen, y = (int64_t) (y - screen->rect.y) * h / screen->rect.h; // rotate - struct point result; + struct sc_point result; switch (rotation) { case 0: result.x = x; @@ -803,7 +803,7 @@ screen_convert_drawable_to_frame_coords(struct screen *screen, return result; } -struct point +struct sc_point screen_convert_window_to_frame_coords(struct screen *screen, int32_t x, int32_t y) { screen_hidpi_scale_coords(screen, &x, &y); diff --git a/app/src/screen.h b/app/src/screen.h index 86aa1183..0804e50e 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -27,13 +27,13 @@ struct screen { SDL_Renderer *renderer; SDL_Texture *texture; struct sc_opengl gl; - struct size frame_size; - struct size content_size; // rotated frame_size + struct sc_size frame_size; + struct sc_size content_size; // rotated frame_size 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; + struct sc_size windowed_content_size; // client rotation: 0, 1, 2 or 3 (x90 degrees counterclockwise) unsigned rotation; @@ -49,7 +49,7 @@ struct screen { struct screen_params { const char *window_title; - struct size frame_size; + struct sc_size frame_size; bool always_on_top; int16_t window_x; @@ -120,13 +120,13 @@ screen_handle_event(struct screen *screen, SDL_Event *event); // convert point from window coordinates to frame coordinates // x and y are expressed in pixels -struct point +struct sc_point 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 +struct sc_point screen_convert_drawable_to_frame_coords(struct screen *screen, int32_t x, int32_t y); diff --git a/app/src/server.c b/app/src/server.c index 76d2910b..50d7f836 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -427,7 +427,7 @@ error: static bool device_read_info(sc_socket device_socket, char *device_name, - struct size *size) { + struct sc_size *size) { unsigned char buf[DEVICE_NAME_FIELD_LENGTH + 4]; ssize_t r = net_recv_all(device_socket, buf, sizeof(buf)); if (r < DEVICE_NAME_FIELD_LENGTH + 4) { @@ -447,7 +447,8 @@ device_read_info(sc_socket device_socket, char *device_name, } bool -server_connect_to(struct server *server, char *device_name, struct size *size) { +server_connect_to(struct server *server, char *device_name, + struct sc_size *size) { if (!server->tunnel_forward) { server->video_socket = net_accept(server->server_socket); if (server->video_socket == SC_INVALID_SOCKET) { diff --git a/app/src/server.h b/app/src/server.h index 75594522..b291c0a5 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -62,7 +62,8 @@ server_start(struct server *server, const struct server_params *params); // block until the communication with the server is established // device_name must point to a buffer of at least DEVICE_NAME_FIELD_LENGTH bytes bool -server_connect_to(struct server *server, char *device_name, struct size *size); +server_connect_to(struct server *server, char *device_name, + struct sc_size *size); // disconnect and kill the server process void diff --git a/app/src/v4l2_sink.c b/app/src/v4l2_sink.c index 95e20541..48d90364 100644 --- a/app/src/v4l2_sink.c +++ b/app/src/v4l2_sink.c @@ -359,7 +359,7 @@ sc_v4l2_frame_sink_push(struct sc_frame_sink *sink, const AVFrame *frame) { bool sc_v4l2_sink_init(struct sc_v4l2_sink *vs, const char *device_name, - struct size frame_size, sc_tick buffering_time) { + struct sc_size frame_size, sc_tick buffering_time) { vs->device_name = strdup(device_name); if (!vs->device_name) { LOGE("Could not strdup v4l2 device name"); diff --git a/app/src/v4l2_sink.h b/app/src/v4l2_sink.h index 6773cd26..8737a607 100644 --- a/app/src/v4l2_sink.h +++ b/app/src/v4l2_sink.h @@ -18,7 +18,7 @@ struct sc_v4l2_sink { AVCodecContext *encoder_ctx; char *device_name; - struct size frame_size; + struct sc_size frame_size; sc_tick buffering_time; sc_thread thread; @@ -34,7 +34,7 @@ struct sc_v4l2_sink { bool sc_v4l2_sink_init(struct sc_v4l2_sink *vs, const char *device_name, - struct size frame_size, sc_tick buffering_time); + struct sc_size frame_size, sc_tick buffering_time); void sc_v4l2_sink_destroy(struct sc_v4l2_sink *vs); From dae091e3ab58bcc592b166a55998addef23be01a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 30 Oct 2021 20:21:00 +0200 Subject: [PATCH 0720/2244] Handle SDL_PushEvent() errors Pushing an event to the main thread may fail. If this happens, log an error, and try to recover when possible. --- app/src/scrcpy.c | 11 +++++++++-- app/src/screen.c | 21 +++++++++++++++++++-- app/src/screen.h | 2 ++ 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index ea71d4a1..d2e35bb2 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -62,7 +62,10 @@ BOOL WINAPI windows_ctrl_handler(DWORD ctrl_type) { if (ctrl_type == CTRL_C_EVENT) { SDL_Event event; event.type = SDL_QUIT; - SDL_PushEvent(&event); + int ret = SDL_PushEvent(&event); + if (ret < 0) { + LOGW("Could not post SDL_QUIT event: %s", SDL_GetError()); + } return TRUE; } return FALSE; @@ -250,7 +253,11 @@ stream_on_eos(struct stream *stream, void *userdata) { SDL_Event stop_event; stop_event.type = EVENT_STREAM_STOPPED; - SDL_PushEvent(&stop_event); + int ret = SDL_PushEvent(&stop_event); + if (ret < 0) { + LOGE("Could not post stream stopped event: %s", SDL_GetError()); + // XXX What could we do? + } } bool diff --git a/app/src/screen.c b/app/src/screen.c index f82f5003..e2d15180 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -282,17 +282,33 @@ sc_video_buffer_on_new_frame(struct sc_video_buffer *vb, bool previous_skipped, (void) vb; struct screen *screen = userdata; + // event_failed implies previous_skipped (the previous frame may not have + // been consumed if the event was not sent) + assert(!screen->event_failed || previous_skipped); + + bool need_new_event; if (previous_skipped) { fps_counter_add_skipped_frame(&screen->fps_counter); // The EVENT_NEW_FRAME triggered for the previous frame will consume - // this new frame instead + // this new frame instead, unless the previous event failed + need_new_event = screen->event_failed; } else { + need_new_event = true; + } + + if (need_new_event) { static SDL_Event new_frame_event = { .type = EVENT_NEW_FRAME, }; // Post the event on the UI thread - SDL_PushEvent(&new_frame_event); + int ret = SDL_PushEvent(&new_frame_event); + if (ret < 0) { + LOGW("Could not post new frame event: %s", SDL_GetError()); + screen->event_failed = true; + } else { + screen->event_failed = false; + } } } @@ -302,6 +318,7 @@ screen_init(struct screen *screen, const struct screen_params *params) { screen->has_frame = false; screen->fullscreen = false; screen->maximized = false; + screen->event_failed = false; static const struct sc_video_buffer_callbacks cbs = { .on_new_frame = sc_video_buffer_on_new_frame, diff --git a/app/src/screen.h b/app/src/screen.h index 0804e50e..51946dbb 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -44,6 +44,8 @@ struct screen { bool maximized; bool mipmaps; + bool event_failed; // in case SDL_PushEvent() returned an error + AVFrame *frame; }; From d2d18466d4bac6cb2d2d9f40ee07519e3342f204 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 30 Oct 2021 22:36:56 +0200 Subject: [PATCH 0721/2244] Factorize SDL event push Add a macro and inline function to call SDL_PushEvent() and log on error (without actually handling the error, because there is nothing we could do). --- app/src/scrcpy.c | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index d2e35bb2..05f8cba2 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -57,15 +57,22 @@ struct scrcpy { struct input_manager input_manager; }; +static inline void +push_event(uint32_t type, const char *name) { + SDL_Event event; + event.type = type; + int ret = SDL_PushEvent(&event); + if (ret < 0) { + LOGE("Could not post %s event: %s", name, SDL_GetError()); + // What could we do? + } +} +#define PUSH_EVENT(TYPE) push_event(TYPE, # TYPE) + #ifdef _WIN32 BOOL WINAPI windows_ctrl_handler(DWORD ctrl_type) { if (ctrl_type == CTRL_C_EVENT) { - SDL_Event event; - event.type = SDL_QUIT; - int ret = SDL_PushEvent(&event); - if (ret < 0) { - LOGW("Could not post SDL_QUIT event: %s", SDL_GetError()); - } + PUSH_EVENT(SDL_QUIT); return TRUE; } return FALSE; @@ -251,13 +258,7 @@ stream_on_eos(struct stream *stream, void *userdata) { (void) stream; (void) userdata; - SDL_Event stop_event; - stop_event.type = EVENT_STREAM_STOPPED; - int ret = SDL_PushEvent(&stop_event); - if (ret < 0) { - LOGE("Could not post stream stopped event: %s", SDL_GetError()); - // XXX What could we do? - } + PUSH_EVENT(EVENT_STREAM_STOPPED); } bool From a57c7d3a2b6949635994fc0ba344ec4817624d3c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 31 Oct 2021 10:35:03 +0100 Subject: [PATCH 0722/2244] Extract SDL hints Set all SDL hints in a separate function. --- app/src/scrcpy.c | 52 +++++++++++++++++++++++++++--------------------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 05f8cba2..707051d5 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -79,29 +79,8 @@ BOOL WINAPI windows_ctrl_handler(DWORD ctrl_type) { } #endif // _WIN32 -// init SDL and set appropriate hints -static bool -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()); - return false; - } - - 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; - } +static void +sdl_set_hints(const char *render_driver) { if (render_driver && !SDL_SetHint(SDL_HINT_RENDER_DRIVER, render_driver)) { LOGW("Could not set render driver"); @@ -130,6 +109,33 @@ sdl_init_and_configure(bool display, const char *render_driver, if (!SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "0")) { LOGW("Could not disable minimize on focus loss"); } +} + +// init SDL and set appropriate hints +static bool +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()); + return false; + } + + 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; + } + + sdl_set_hints(render_driver); if (disable_screensaver) { LOGD("Screensaver disabled"); From ac539e13123e0638fab29f26b29b626d5d11d13f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 31 Oct 2021 10:35:58 +0100 Subject: [PATCH 0723/2244] Set SDL hints before initialization In theory, some SDL hints should be initialized before calling SDL_Init(). --- app/src/scrcpy.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 707051d5..0c2dc5b3 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -115,6 +115,10 @@ sdl_set_hints(const char *render_driver) { static bool sdl_init_and_configure(bool display, const char *render_driver, bool disable_screensaver) { + if (display) { + sdl_set_hints(render_driver); + } + uint32_t flags = display ? SDL_INIT_VIDEO : SDL_INIT_EVENTS; if (SDL_Init(flags)) { LOGC("Could not initialize SDL: %s", SDL_GetError()); @@ -135,8 +139,6 @@ sdl_init_and_configure(bool display, const char *render_driver, return true; } - sdl_set_hints(render_driver); - if (disable_screensaver) { LOGD("Screensaver disabled"); SDL_DisableScreenSaver(); From 688477ff6521af87f925da80d9519b334bbb6736 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 31 Oct 2021 12:03:35 +0100 Subject: [PATCH 0724/2244] Move SDL initialization Inline SDL initialization in scrcpy(). This will allow to split minimal initialization and video initialization. --- app/src/scrcpy.c | 35 ++++++++++++++--------------------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 0c2dc5b3..042cf43c 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -111,22 +111,8 @@ sdl_set_hints(const char *render_driver) { } } -// init SDL and set appropriate hints -static bool -sdl_init_and_configure(bool display, const char *render_driver, - bool disable_screensaver) { - if (display) { - sdl_set_hints(render_driver); - } - - uint32_t flags = display ? SDL_INIT_VIDEO : SDL_INIT_EVENTS; - if (SDL_Init(flags)) { - LOGC("Could not initialize SDL: %s", SDL_GetError()); - return false; - } - - atexit(SDL_Quit); - +static void +sdl_configure(bool display, bool disable_screensaver) { #ifdef _WIN32 // Clean up properly on Ctrl+C on Windows bool ok = SetConsoleCtrlHandler(windows_ctrl_handler, TRUE); @@ -136,7 +122,7 @@ sdl_init_and_configure(bool display, const char *render_driver, #endif // _WIN32 if (!display) { - return true; + return; } if (disable_screensaver) { @@ -146,8 +132,6 @@ sdl_init_and_configure(bool display, const char *render_driver, LOGD("Screensaver enabled"); SDL_EnableScreenSaver(); } - - return true; } static bool @@ -319,11 +303,20 @@ scrcpy(struct scrcpy_options *options) { server_started = true; - if (!sdl_init_and_configure(options->display, options->render_driver, - options->disable_screensaver)) { + if (options->display) { + sdl_set_hints(options->render_driver); + } + + uint32_t flags = options->display ? SDL_INIT_VIDEO : SDL_INIT_EVENTS; + if (SDL_Init(flags)) { + LOGC("Could not initialize SDL: %s", SDL_GetError()); goto end; } + atexit(SDL_Quit); + + sdl_configure(options->display, options->disable_screensaver); + char device_name[DEVICE_NAME_FIELD_LENGTH]; struct sc_size frame_size; From caf594c90ef1b71ed844b2a9b42c3b3371215d6f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 31 Oct 2021 12:11:34 +0100 Subject: [PATCH 0725/2244] Split SDL initialization Initialize SDL_INIT_EVENTS first (very quick) and SDL_INIT_VIDEO after the server (quite long). --- app/src/scrcpy.c | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 042cf43c..2235f727 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -258,6 +258,14 @@ scrcpy(struct scrcpy_options *options) { static struct scrcpy scrcpy; struct scrcpy *s = &scrcpy; + // Minimal SDL initialization + if (SDL_Init(SDL_INIT_EVENTS)) { + LOGC("Could not initialize SDL: %s", SDL_GetError()); + return false; + } + + atexit(SDL_Quit); + if (!server_init(&s->server)) { return false; } @@ -307,14 +315,12 @@ scrcpy(struct scrcpy_options *options) { sdl_set_hints(options->render_driver); } - uint32_t flags = options->display ? SDL_INIT_VIDEO : SDL_INIT_EVENTS; - if (SDL_Init(flags)) { + // Initialize SDL video in addition if display is enabled + if (options->display && SDL_Init(SDL_INIT_VIDEO)) { LOGC("Could not initialize SDL: %s", SDL_GetError()); goto end; } - atexit(SDL_Quit); - sdl_configure(options->display, options->disable_screensaver); char device_name[DEVICE_NAME_FIELD_LENGTH]; From 13c4aa1a3bbcedb212c4b6ddf3b3855d02243ea0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 31 Oct 2021 12:40:51 +0100 Subject: [PATCH 0726/2244] Disable synthetic mouse events from touch events Touch events with id SDL_TOUCH_MOUSEID are ignored anyway, but it is better not to generate them in the first place. --- app/src/compat.h | 5 +++++ app/src/scrcpy.c | 9 +++++++++ 2 files changed, 14 insertions(+) diff --git a/app/src/compat.h b/app/src/compat.h index 8e2d18f4..9f66ce95 100644 --- a/app/src/compat.h +++ b/app/src/compat.h @@ -43,6 +43,11 @@ # define SCRCPY_SDL_HAS_WINDOW_ALWAYS_ON_TOP #endif +#if SDL_VERSION_ATLEAST(2, 0, 6) +// +# define SCRCPY_SDL_HAS_HINT_TOUCH_MOUSE_EVENTS +#endif + #if SDL_VERSION_ATLEAST(2, 0, 8) // # define SCRCPY_SDL_HAS_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 2235f727..2efc043a 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -98,6 +98,15 @@ sdl_set_hints(const char *render_driver) { } #endif +#ifdef SCRCPY_SDL_HAS_HINT_TOUCH_MOUSE_EVENTS + // Disable synthetic mouse events from touch events + // Touch events with id SDL_TOUCH_MOUSEID are ignored anyway, but it is + // better not to generate them in the first place. + if (!SDL_SetHint(SDL_HINT_TOUCH_MOUSE_EVENTS, "0")) { + LOGW("Could not disable synthetic mouse events"); + } +#endif + #ifdef SCRCPY_SDL_HAS_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR // Disable compositor bypassing on X11 if (!SDL_SetHint(SDL_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR, "0")) { From 58ea238fb2c5ddf69e1b2d598ccf58b6a9fe8270 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 31 Oct 2021 12:20:45 +0100 Subject: [PATCH 0727/2244] Remove unnecessary variable Test directly if a record filename is provided, without an intermediate boolean variable. --- app/src/scrcpy.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 2efc043a..c7033a26 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -295,7 +295,6 @@ scrcpy(struct scrcpy_options *options) { bool controller_started = false; bool screen_initialized = false; - bool record = !!options->record_filename; struct server_params params = { .serial = options->serial, .log_level = options->log_level, @@ -358,7 +357,7 @@ scrcpy(struct scrcpy_options *options) { } struct recorder *rec = NULL; - if (record) { + if (options->record_filename) { if (!recorder_init(&s->recorder, options->record_filename, options->record_format, From 790f04e91faa14c5ebd2b987530d1e3db080e682 Mon Sep 17 00:00:00 2001 From: Kaleidot725 Date: Sat, 30 Oct 2021 18:29:52 +0900 Subject: [PATCH 0728/2244] Update README.jp.md to v1.19 PR #2740 Signed-off-by: Romain Vimont --- README.jp.md | 132 ++++++++++++++++++++++++++++++++++++++++----------- README.md | 2 +- 2 files changed, 104 insertions(+), 30 deletions(-) diff --git a/README.jp.md b/README.jp.md index e42c528e..a97ef765 100644 --- a/README.jp.md +++ b/README.jp.md @@ -1,6 +1,6 @@ _Only the original [README](README.md) is guaranteed to be up-to-date._ -# scrcpy (v1.17) +# scrcpy (v1.19) このアプリケーションはUSB(もしくは[TCP/IP経由][article-tcpip])で接続されたAndroidデバイスの表示と制御を提供します。このアプリケーションは _root_ でのアクセスを必要としません。このアプリケーションは _GNU/Linux_ 、 _Windows_ そして _macOS_ 上で動作します。 @@ -103,18 +103,21 @@ scoop install adb # まだ入手していない場合 brew install scrcpy ``` -`PATH`から`adb`へのアクセスが必要です。もしまだ持っていない場合: +`PATH`からアクセス可能な`adb`が必要です。もし持っていない場合はインストールしてください。 ```bash -# Homebrew >= 2.6.0 -brew install --cask android-platform-tools - -# Homebrew < 2.6.0 -brew cask install android-platform-tools +brew install android-platform-tools ``` -また、[アプリケーションをビルド][BUILD]することも可能です。 +`adb`は[MacPorts]からでもインストールできます。 +```bash +sudo port install scrcpy +``` + +[MacPorts]: https://www.macports.org/ + +また、[アプリケーションをビルド][BUILD]することも可能です。 ## 実行 @@ -184,10 +187,11 @@ scrcpy --crop 1224:1440:0:0 # オフセット位置(0,0)で1224x1440 ミラーリングの向きをロックするには: ```bash -scrcpy --lock-video-orientation 0 # 自然な向き -scrcpy --lock-video-orientation 1 # 90°反時計回り -scrcpy --lock-video-orientation 2 # 180° -scrcpy --lock-video-orientation 3 # 90°時計回り +scrcpy --lock-video-orientation # 現在の向き +scrcpy --lock-video-orientation=0 # 自然な向き +scrcpy --lock-video-orientation=1 # 90°反時計回り +scrcpy --lock-video-orientation=2 # 180° +scrcpy --lock-video-orientation=3 # 90°時計回り ``` この設定は録画の向きに影響します。 @@ -210,7 +214,9 @@ scrcpy --encoder OMX.qcom.video.encoder.avc scrcpy --encoder _ ``` -### 録画 +### キャプチャ + +#### 録画 ミラーリング中に画面の録画をすることが可能です: @@ -233,6 +239,77 @@ scrcpy -Nr file.mkv [パケット遅延のバリエーション]: https://en.wikipedia.org/wiki/Packet_delay_variation +#### v4l2loopback + +Linuxでは、ビデオストリームをv4l2ループバックデバイスに送信することができます。 +v4l2loopbackのデバイスにビデオストリームを送信することで、Androidデバイスをウェブカメラのようにv4l2対応ツールで開くこともできます。 + +`v4l2loopback` モジュールのインストールが必要です。 + +```bash +sudo apt install v4l2loopback-dkms +``` + +v4l2デバイスを作成する。 + +```bash +sudo modprobe v4l2loopback +``` + +これにより、新しいビデオデバイスが `/dev/videoN` に作成されます。(`N` は整数) +(複数のデバイスや特定のIDのデバイスを作成するために、より多くの[オプション](https://github.com/umlaeute/v4l2loopback#options)が利用可能です。 +多くの[オプション]()が利用可能で複数のデバイスや特定のIDのデバイスを作成できます。 + + +有効なデバイスを一覧表示する: + +```bash +# v4l-utilsパッケージが必要 +v4l2-ctl --list-devices + +# シンプルですが十分これで確認できます +ls /dev/video* +``` + +v4l2シンクを使用してscrcpyを起動する。 + +```bash +scrcpy --v4l2-sink=/dev/videoN +scrcpy --v4l2-sink=/dev/videoN --no-display # ミラーリングウィンドウを無効化する +scrcpy --v4l2-sink=/dev/videoN -N # 短縮版 +``` + +(`N` をデバイス ID に置き換えて、`ls /dev/video*` で確認してください) +有効にすると、v4l2対応のツールでビデオストリームを開けます。 + +```bash +ffplay -i /dev/videoN +vlc v4l2:///dev/videoN # VLCではバッファリングの遅延が発生する場合があります +``` + +例えばですが [OBS]の中にこの映像を取り込めことができます。 + +[OBS]: https://obsproject.com/ + + +#### Buffering + +バッファリングを追加することも可能です。これによりレイテンシーは増加しますが、ジッターは減少します。(参照 +[#2464]) + +[#2464]: https://github.com/Genymobile/scrcpy/issues/2464 + +このオプションでディスプレイバッファリングを設定できます。 + +```bash +scrcpy --display-buffer=50 # ディスプレイに50msのバッファリングを追加する +``` + +V4L2の場合はこちらのオプションで設定できます。 + +```bash +scrcpy --v4l2-buffer=500 # add 500 ms buffering for v4l2 sink +``` ### 接続 @@ -457,16 +534,6 @@ scrcpy -Sw ``` -#### 期限切れフレームをレンダリングする - -初期状態では、待ち時間を最小限にするために、_scrcpy_ は最後にデコードされたフレームをレンダリングし、前のフレームを削除します。 - -全フレームのレンダリングを強制するには(待ち時間が長くなる可能性があります): - -```bash -scrcpy --render-expired-frames -``` - #### タッチを表示 プレゼンテーションの場合(物理デバイス上で)物理的なタッチを表示すると便利な場合があります。 @@ -586,14 +653,14 @@ APKをインストールするには、(`.apk`で終わる)APKファイルを _s #### デバイスにファイルを送る -デバイスの`/sdcard/`ディレクトリにファイルを送るには、(APKではない)ファイルを _scrcpy_ の画面にドラッグ&ドロップします。 +デバイスの`/sdcard/Download`ディレクトリにファイルを送るには、(APKではない)ファイルを _scrcpy_ の画面にドラッグ&ドロップします。 見た目のフィードバックはありません。コンソールにログが出力されます。 転送先ディレクトリを起動時に変更することができます: ```bash -scrcpy --push-target /sdcard/foo/bar/ +scrcpy --push-target=/sdcard/Movies/ ``` @@ -634,7 +701,7 @@ _[Super]は通常WindowsもしくはCmdキー | ウィンドウサイズを変更して黒い境界線を削除 | MOD+w \| _ダブルクリック¹_ | `HOME`をクリック | MOD+h \| _真ん中クリック_ | `BACK`をクリック | MOD+b \| _右クリック²_ - | `APP_SWITCH`をクリック | MOD+s + | `APP_SWITCH`をクリック | MOD+s \| _4クリック³_ | `MENU` (画面のアンロック)をクリック | MOD+m | `VOLUME_UP`をクリック | MOD+ _(上)_ | `VOLUME_DOWN`をクリック | MOD+ _(下)_ @@ -643,7 +710,8 @@ _[Super]は通常WindowsもしくはCmdキー | デバイス画面をオフにする(ミラーリングしたまま) | MOD+o | デバイス画面をオンにする | MOD+Shift+o | デバイス画面を回転する | MOD+r - | 通知パネルを展開する | MOD+n + | 通知パネルを展開する | MOD+n \| _5ボタンクリック³_ + | 設定パネルを展開する | MOD+n+n \| _5ダブルクリック³_ | 通知パネルを折りたたむ | MOD+Shift+n | クリップボードへのコピー³ | MOD+c | クリップボードへのカット³ | MOD+x @@ -654,11 +722,17 @@ _[Super]は通常WindowsもしくはCmdキー _¹黒い境界線を削除するため、境界線上でダブルクリック_ _²もしスクリーンがオフの場合、右クリックでスクリーンをオンする。それ以外の場合はBackを押します._ -_³Android 7以上のみ._ +_³4と5はマウスのボタンです、もしあなたのマウスにボタンがあれば使えます._ +_⁴Android 7以上のみ._ + +キーを繰り返すショートカットはキーを離して2回目を押したら実行されます。例えば「設定パネルを展開する」を実行する場合は以下のように操作する。 + + 1. MOD キーを押し、押したままにする. + 2. その後に nキーを2回押す. + 3. 最後に MODキーを離す. 全てのCtrl+_キー_ ショートカットはデバイスに転送されます、そのためアクティブなアプリケーションによって処理されます。 - ## カスタムパス 特定の _adb_ バイナリを使用する場合、そのパスを環境変数`ADB`で構成します: diff --git a/README.md b/README.md index 81410b80..bd91be07 100644 --- a/README.md +++ b/README.md @@ -889,7 +889,7 @@ This README is available in other languages: - [Indonesian (Indonesia, `id`) - v1.16](README.id.md) - [Italiano (Italiano, `it`) - v1.19](README.it.md) -- [日本語 (Japanese, `jp`) - v1.17](README.jp.md) +- [日本語 (Japanese, `jp`) - v1.19](README.jp.md) - [한국어 (Korean, `ko`) - v1.11](README.ko.md) - [Português Brasileiro (Brazilian Portuguese, `pt-BR`) - v1.19](README.pt-br.md) - [Español (Spanish, `sp`) - v1.17](README.sp.md) From 676fa73d2c19d4828921640588e42d1911ad2147 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 30 Oct 2021 19:07:35 +0200 Subject: [PATCH 0729/2244] Wrap device name and size in a struct As a benefit, this avoids to take care of the device name length on the caller side. --- app/src/scrcpy.c | 15 +++++++-------- app/src/server.c | 21 +++++++++------------ app/src/server.h | 11 +++++++---- 3 files changed, 23 insertions(+), 24 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index c7033a26..c8adb5a1 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -331,10 +331,9 @@ scrcpy(struct scrcpy_options *options) { sdl_configure(options->display, options->disable_screensaver); - char device_name[DEVICE_NAME_FIELD_LENGTH]; - struct sc_size frame_size; + struct server_info info; - if (!server_connect_to(&s->server, device_name, &frame_size)) { + if (!server_connect_to(&s->server, &info)) { goto end; } @@ -361,7 +360,7 @@ scrcpy(struct scrcpy_options *options) { if (!recorder_init(&s->recorder, options->record_filename, options->record_format, - frame_size)) { + info.frame_size)) { goto end; } rec = &s->recorder; @@ -407,11 +406,11 @@ scrcpy(struct scrcpy_options *options) { if (options->display) { const char *window_title = - options->window_title ? options->window_title : device_name; + options->window_title ? options->window_title : info.device_name; struct screen_params screen_params = { .window_title = window_title, - .frame_size = frame_size, + .frame_size = info.frame_size, .always_on_top = options->always_on_top, .window_x = options->window_x, .window_y = options->window_y, @@ -434,8 +433,8 @@ scrcpy(struct scrcpy_options *options) { #ifdef HAVE_V4L2 if (options->v4l2_device) { - if (!sc_v4l2_sink_init(&s->v4l2_sink, options->v4l2_device, frame_size, - options->v4l2_buffer)) { + if (!sc_v4l2_sink_init(&s->v4l2_sink, options->v4l2_device, + info.frame_size, options->v4l2_buffer)) { goto end; } diff --git a/app/src/server.c b/app/src/server.c index 50d7f836..4a4d3ec4 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -426,8 +426,7 @@ error: } static bool -device_read_info(sc_socket device_socket, char *device_name, - struct sc_size *size) { +device_read_info(sc_socket device_socket, struct server_info *info) { unsigned char buf[DEVICE_NAME_FIELD_LENGTH + 4]; ssize_t r = net_recv_all(device_socket, buf, sizeof(buf)); if (r < DEVICE_NAME_FIELD_LENGTH + 4) { @@ -436,19 +435,17 @@ device_read_info(sc_socket device_socket, char *device_name, } // in case the client sends garbage buf[DEVICE_NAME_FIELD_LENGTH - 1] = '\0'; - // strcpy is safe here, since name contains at least - // DEVICE_NAME_FIELD_LENGTH bytes and strlen(buf) < DEVICE_NAME_FIELD_LENGTH - strcpy(device_name, (char *) buf); - size->width = (buf[DEVICE_NAME_FIELD_LENGTH] << 8) - | buf[DEVICE_NAME_FIELD_LENGTH + 1]; - size->height = (buf[DEVICE_NAME_FIELD_LENGTH + 2] << 8) - | buf[DEVICE_NAME_FIELD_LENGTH + 3]; + memcpy(info->device_name, (char *) buf, sizeof(info->device_name)); + + info->frame_size.width = (buf[DEVICE_NAME_FIELD_LENGTH] << 8) + | buf[DEVICE_NAME_FIELD_LENGTH + 1]; + info->frame_size.height = (buf[DEVICE_NAME_FIELD_LENGTH + 2] << 8) + | buf[DEVICE_NAME_FIELD_LENGTH + 3]; return true; } bool -server_connect_to(struct server *server, char *device_name, - struct sc_size *size) { +server_connect_to(struct server *server, struct server_info *info) { if (!server->tunnel_forward) { server->video_socket = net_accept(server->server_socket); if (server->video_socket == SC_INVALID_SOCKET) { @@ -489,7 +486,7 @@ server_connect_to(struct server *server, char *device_name, server->tunnel_enabled = false; // The sockets will be closed on stop if device_read_info() fails - return device_read_info(server->video_socket, device_name, size); + return device_read_info(server->video_socket, info); } void diff --git a/app/src/server.h b/app/src/server.h index b291c0a5..242b0525 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -14,6 +14,12 @@ #include "util/net.h" #include "util/thread.h" +#define DEVICE_NAME_FIELD_LENGTH 64 +struct server_info { + char device_name[DEVICE_NAME_FIELD_LENGTH]; + struct sc_size frame_size; +}; + struct server { char *serial; process_t process; @@ -58,12 +64,9 @@ server_init(struct server *server); bool server_start(struct server *server, const struct server_params *params); -#define DEVICE_NAME_FIELD_LENGTH 64 // block until the communication with the server is established -// device_name must point to a buffer of at least DEVICE_NAME_FIELD_LENGTH bytes bool -server_connect_to(struct server *server, char *device_name, - struct sc_size *size); +server_connect_to(struct server *server, struct server_info *info); // disconnect and kill the server process void From 48fcfa96ab659a830ca4bec6fa4d70ddacf855d5 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 6 Nov 2021 19:26:29 +0100 Subject: [PATCH 0730/2244] Add missing include "common.h" --- app/src/util/tick.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/util/tick.h b/app/src/util/tick.h index 472a18a7..47d02529 100644 --- a/app/src/util/tick.h +++ b/app/src/util/tick.h @@ -1,6 +1,8 @@ #ifndef SC_TICK_H #define SC_TICK_H +#include "common.h" + #include typedef int64_t sc_tick; From f65c3fde69e216ecc3912601006cd4e19853269f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 7 Nov 2021 18:07:17 +0100 Subject: [PATCH 0731/2244] Fix typos in help --- app/src/cli.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index cab25772..02b89aa0 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -66,12 +66,12 @@ scrcpy_print_usage(const char *arg0) { "\n" " --force-adb-forward\n" " Do not attempt to use \"adb reverse\" to connect to the\n" - " the device.\n" + " device.\n" "\n" " --forward-all-clicks\n" " By default, right-click triggers BACK (or POWER on) and\n" " middle-click triggers HOME. This option disables these\n" - " shortcuts and forward the clicks to the device instead.\n" + " shortcuts and forwards the clicks to the device instead.\n" "\n" " -f, --fullscreen\n" " Start in fullscreen.\n" @@ -142,7 +142,7 @@ scrcpy_print_usage(const char *arg0) { "\n" " --push-target path\n" " Set the target directory for pushing files to the device by\n" - " drag & drop. It is passed as-is to \"adb push\".\n" + " drag & drop. It is passed as is to \"adb push\".\n" " Default is \"/sdcard/Download/\".\n" "\n" " -r, --record file.mp4\n" @@ -162,14 +162,14 @@ scrcpy_print_usage(const char *arg0) { "\n" " --rotation value\n" " Set the initial display rotation.\n" - " Possibles values are 0, 1, 2 and 3. Each increment adds a 90\n" + " Possible 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" "\n" - " --shortcut-mod key[+...]][,...]\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" From fc64445555ecb29e145f45dd152f33f71d676dd8 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 7 Nov 2021 18:46:52 +0100 Subject: [PATCH 0732/2244] Remove extra space in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fdd74087..28d492f8 100644 --- a/README.md +++ b/README.md @@ -830,7 +830,7 @@ _[Super] is typically the Windows or Cmd key._ | Turn device screen on | MOD+Shift+o | Rotate device screen | MOD+r | Expand notification panel | MOD+n \| _5th-click³_ - | Expand settings panel | MOD+n+n \| _Double-5th-click³_ + | Expand settings panel | MOD+n+n \| _Double-5th-click³_ | Collapse panels | MOD+Shift+n | Copy to clipboard⁴ | MOD+c | Cut to clipboard⁴ | MOD+x From d72c7076f750f593abee4d041e3a3bd8d5a89bc6 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 7 Nov 2021 18:48:27 +0100 Subject: [PATCH 0733/2244] Mention drag & drop APK in README For consistency with --help and manpage. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 28d492f8..70080105 100644 --- a/README.md +++ b/README.md @@ -838,6 +838,7 @@ _[Super] is typically the Windows or Cmd key._ | Inject computer clipboard text | MOD+Shift+v | Enable/disable FPS counter (on stdout) | MOD+i | Pinch-to-zoom | Ctrl+_click-and-move_ + | Drag & drop APK file | Install APK from computer _¹Double-click on black borders to remove them._ _²Right-click turns the screen on if it was off, presses BACK otherwise._ From f489f7fcadac5325cdda7eac52e32c77ac207678 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 7 Nov 2021 18:44:07 +0100 Subject: [PATCH 0734/2244] Mention drag & drop for non-APK files in help --- README.md | 1 + app/scrcpy.1 | 4 ++++ app/src/cli.c | 3 +++ 3 files changed, 8 insertions(+) diff --git a/README.md b/README.md index 70080105..f86fff9f 100644 --- a/README.md +++ b/README.md @@ -839,6 +839,7 @@ _[Super] is typically the Windows or Cmd key._ | Enable/disable FPS counter (on stdout) | MOD+i | Pinch-to-zoom | Ctrl+_click-and-move_ | Drag & drop APK file | Install APK from computer + | Drag & drop non-APK file | [Push file to device](#push-file-to-device) _¹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 46db6e1d..9ad405c5 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -364,6 +364,10 @@ Pinch-to-zoom from the center of the screen .B Drag & drop APK file Install APK from computer +.TP +.B Drag & drop non-APK file +Push file to device (see \fB\-\-push\-target\fR) + .SH Environment variables diff --git a/app/src/cli.c b/app/src/cli.c index 02b89aa0..ea7cb1a0 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -328,6 +328,9 @@ scrcpy_print_usage(const char *arg0) { "\n" " Drag & drop APK file\n" " Install APK from computer\n" + "\n" + " Drag & drop non-APK file\n" + " Push file to device (see --push-target)\n" "\n", arg0); } From 30d40f4e786beaaeabde1f3c324b7680ef433be6 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 7 Nov 2021 19:27:53 +0100 Subject: [PATCH 0735/2244] Document --power-off-on-close The option was not documented. Refs #824 --- README.md | 8 ++++++++ app/scrcpy.1 | 4 ++++ app/src/cli.c | 3 +++ 3 files changed, 15 insertions(+) diff --git a/README.md b/README.md index f86fff9f..a0ab271c 100644 --- a/README.md +++ b/README.md @@ -582,6 +582,14 @@ scrcpy --turn-screen-off --stay-awake scrcpy -Sw ``` +#### Power off on close + +To turn the device screen off when closing scrcpy: + +```bash +scrcpy --power-off-on-close +``` + #### Show touches diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 9ad405c5..399fd172 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -136,6 +136,10 @@ Set the TCP port (range) used by the client to listen. Default is 27183:27199. +.TP +.B \-\-power\-off\-on\-close +Turn the device screen off when closing scrcpy. + .TP .B \-\-prefer\-text Inject alpha characters and space as text events instead of key events. diff --git a/app/src/cli.c b/app/src/cli.c index ea7cb1a0..a0bf451d 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -133,6 +133,9 @@ scrcpy_print_usage(const char *arg0) { " Default is " STR(DEFAULT_LOCAL_PORT_RANGE_FIRST) ":" STR(DEFAULT_LOCAL_PORT_RANGE_LAST) ".\n" "\n" + " --power-off-on-close\n" + " Turn the device screen off when closing scrcpy.\n" + "\n" " --prefer-text\n" " Inject alpha characters and space as text events instead of\n" " key events.\n" From b62df7ee91f30e0ca5c7e7787f649b51f9384156 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 7 Nov 2021 20:54:45 +0100 Subject: [PATCH 0736/2244] Remove deprecated -c option The short option -c is deprecated since v1.11. Only the long version (--crop) remains. --- app/src/cli.c | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index a0bf451d..dbf6940b 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -799,7 +799,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:hKm:nNp:r:s:StTvV:w", + while ((c = getopt_long(argc, argv, "b:fF:hKm:nNp:r:s:StTvV:w", long_options, NULL)) != -1) { switch (c) { case 'b': @@ -807,9 +807,6 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { return false; } break; - case 'c': - LOGW("Deprecated option -c. Use --crop instead."); - // fall through case OPT_CROP: opts->crop = optarg; break; From 570a003c39edc6e289695b0e97c064dcb8ba0b34 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 7 Nov 2021 21:35:14 +0100 Subject: [PATCH 0737/2244] Remove deprecated -T option The short option -T is deprecated since v1.11. Only the long version (--always-on-top) remains. --- app/src/cli.c | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index dbf6940b..45b87fb2 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -799,7 +799,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:fF:hKm:nNp:r:s:StTvV:w", + while ((c = getopt_long(argc, argv, "b:fF:hKm:nNp:r:s:StvV:w", long_options, NULL)) != -1) { switch (c) { case 'b': @@ -871,9 +871,6 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { case 't': opts->show_touches = true; break; - case 'T': - LOGW("Deprecated option -T. Use --always-on-top instead."); - // fall through case OPT_ALWAYS_ON_TOP: opts->always_on_top = true; break; From 6dba1922c1fa00765ca6cdec264c53939364cda7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 6 Nov 2021 19:51:47 +0100 Subject: [PATCH 0738/2244] Add string buffer util This will help to build strings incrementally. --- app/meson.build | 5 +++ app/src/util/strbuf.c | 87 +++++++++++++++++++++++++++++++++++++++++ app/src/util/strbuf.h | 73 ++++++++++++++++++++++++++++++++++ app/tests/test_strbuf.c | 47 ++++++++++++++++++++++ 4 files changed, 212 insertions(+) create mode 100644 app/src/util/strbuf.c create mode 100644 app/src/util/strbuf.h create mode 100644 app/tests/test_strbuf.c diff --git a/app/meson.build b/app/meson.build index 2ced151b..f08d4cab 100644 --- a/app/meson.build +++ b/app/meson.build @@ -27,6 +27,7 @@ src = [ 'src/util/log.c', 'src/util/net.c', 'src/util/process.c', + 'src/util/strbuf.c', 'src/util/str_util.c', 'src/util/thread.c', 'src/util/tick.c', @@ -204,6 +205,10 @@ if get_option('buildtype') == 'debug' ['test_queue', [ 'tests/test_queue.c', ]], + ['test_strbuf', [ + 'tests/test_strbuf.c', + 'src/util/strbuf.c', + ]], ['test_strutil', [ 'tests/test_strutil.c', 'src/util/str_util.c', diff --git a/app/src/util/strbuf.c b/app/src/util/strbuf.c new file mode 100644 index 00000000..b2b6f494 --- /dev/null +++ b/app/src/util/strbuf.c @@ -0,0 +1,87 @@ +#include "strbuf.h" + +#include +#include +#include + +#include + +bool +sc_strbuf_init(struct sc_strbuf *buf, size_t init_cap) { + buf->s = malloc(init_cap + 1); // +1 for '\0' + if (!buf->s) { + return false; + } + + buf->len = 0; + buf->cap = init_cap; + return true; +} + +static bool +sc_strbuf_reserve(struct sc_strbuf *buf, size_t len) { + if (buf->len + len > buf->cap) { + size_t new_cap = buf->cap * 3 / 2 + len; + char *s = realloc(buf->s, new_cap + 1); // +1 for '\0' + if (!s) { + // Leave the old buf->s + return false; + } + buf->s = s; + buf->cap = new_cap; + } + return true; +} + +bool +sc_strbuf_append(struct sc_strbuf *buf, const char *s, size_t len) { + assert(s); + assert(*s); + assert(strlen(s) >= len); + if (!sc_strbuf_reserve(buf, len)) { + return false; + } + + memcpy(&buf->s[buf->len], s, len); + buf->len += len; + buf->s[buf->len] = '\0'; + + return true; +} + +bool +sc_strbuf_append_char(struct sc_strbuf *buf, const char c) { + if (!sc_strbuf_reserve(buf, 1)) { + return false; + } + + buf->s[buf->len] = c; + buf->len ++; + buf->s[buf->len] = '\0'; + + return true; +} + +bool +sc_strbuf_append_n(struct sc_strbuf *buf, const char c, size_t n) { + if (!sc_strbuf_reserve(buf, n)) { + return false; + } + + memset(&buf->s[buf->len], c, n); + buf->len += n; + buf->s[buf->len] = '\0'; + + return true; +} + +void +sc_strbuf_shrink(struct sc_strbuf *buf) { + assert(buf->len <= buf->cap); + if (buf->len != buf->cap) { + char *s = realloc(buf->s, buf->len + 1); // +1 for '\0' + assert(s); // decreasing the size may not fail + buf->s = s; + buf->cap = buf->len; + } +} diff --git a/app/src/util/strbuf.h b/app/src/util/strbuf.h new file mode 100644 index 00000000..1878df2f --- /dev/null +++ b/app/src/util/strbuf.h @@ -0,0 +1,73 @@ +#ifndef SC_STRBUF_H +#define SC_STRBUF_H + +#include "common.h" + +#include +#include +#include + +struct sc_strbuf { + char *s; + size_t len; + size_t cap; +}; + +/** + * Initialize the string buffer + * + * `buf->s` must be manually freed by the caller. + */ +bool +sc_strbuf_init(struct sc_strbuf *buf, size_t init_cap); + +/** + * Append a string + * + * Append `len` characters from `s` to the buffer. + */ +bool +sc_strbuf_append(struct sc_strbuf *buf, const char *s, size_t len); + +/** + * Append a char + * + * Append a single character to the buffer. + */ +bool +sc_strbuf_append_char(struct sc_strbuf *buf, const char c); + +/** + * Append a char `n` times + * + * Append the same characters `n` times to the buffer. + */ +bool +sc_strbuf_append_n(struct sc_strbuf *buf, const char c, size_t n); + +/** + * Append a NUL-terminated string + */ +static inline bool +sc_strbuf_append_str(struct sc_strbuf *buf, const char *s) { + return sc_strbuf_append(buf, s, strlen(s)); +} + +/** + * Append a static string + * + * Append a string whose size is known at compile time (for + * example a string literal). + */ +#define sc_strbuf_append_staticstr(BUF, S) \ + sc_strbuf_append(BUF, S, sizeof(S) - 1) + +/** + * Shrink the buffer capacity to its current length + * + * This resizes `buf->s` to fit the content. + */ +void +sc_strbuf_shrink(struct sc_strbuf *buf); + +#endif diff --git a/app/tests/test_strbuf.c b/app/tests/test_strbuf.c new file mode 100644 index 00000000..97417677 --- /dev/null +++ b/app/tests/test_strbuf.c @@ -0,0 +1,47 @@ +#include "common.h" + +#include +#include +#include + +#include "util/strbuf.h" + +static void test_strbuf_simple(void) { + struct sc_strbuf buf; + bool ok = sc_strbuf_init(&buf, 10); + assert(ok); + + ok = sc_strbuf_append_staticstr(&buf, "Hello"); + assert(ok); + + ok = sc_strbuf_append_char(&buf, ' '); + assert(ok); + + ok = sc_strbuf_append_staticstr(&buf, "world"); + assert(ok); + + ok = sc_strbuf_append_staticstr(&buf, "!\n"); + assert(ok); + + ok = sc_strbuf_append_staticstr(&buf, "This is a test"); + assert(ok); + + ok = sc_strbuf_append_n(&buf, '.', 3); + assert(ok); + + assert(!strcmp(buf.s, "Hello world!\nThis is a test...")); + + sc_strbuf_shrink(&buf); + assert(buf.len == buf.cap); + assert(!strcmp(buf.s, "Hello world!\nThis is a test...")); + + free(buf.s); +} + +int main(int argc, char *argv[]) { + (void) argc; + (void) argv; + + test_strbuf_simple(); + return 0; +} From 9ec3406568a444fab5b2416944479fa6248ef409 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 6 Nov 2021 20:26:35 +0100 Subject: [PATCH 0739/2244] Add line wrapper Add a tool to wrap lines at words boundaries (spaces) to fit in a specified number of columns, left-indented by a specified number of spaces. --- app/meson.build | 3 ++ app/src/util/str_util.c | 80 ++++++++++++++++++++++++++++++++++++++++ app/src/util/str_util.h | 8 ++++ app/tests/test_strutil.c | 39 ++++++++++++++++++++ 4 files changed, 130 insertions(+) diff --git a/app/meson.build b/app/meson.build index f08d4cab..0e33c084 100644 --- a/app/meson.build +++ b/app/meson.build @@ -187,6 +187,7 @@ if get_option('buildtype') == 'debug' 'tests/test_cli.c', 'src/cli.c', 'src/options.c', + 'src/util/strbuf.c', 'src/util/str_util.c', ]], ['test_clock', [ @@ -196,6 +197,7 @@ if get_option('buildtype') == 'debug' ['test_control_msg_serialize', [ 'tests/test_control_msg_serialize.c', 'src/control_msg.c', + 'src/util/strbuf.c', 'src/util/str_util.c', ]], ['test_device_msg_deserialize', [ @@ -211,6 +213,7 @@ if get_option('buildtype') == 'debug' ]], ['test_strutil', [ 'tests/test_strutil.c', + 'src/util/strbuf.c', 'src/util/str_util.c', ]], ] diff --git a/app/src/util/str_util.c b/app/src/util/str_util.c index 287c08de..b4e7cac4 100644 --- a/app/src/util/str_util.c +++ b/app/src/util/str_util.c @@ -1,9 +1,11 @@ #include "str_util.h" +#include #include #include #include #include +#include "util/strbuf.h" #ifdef _WIN32 # include @@ -209,3 +211,81 @@ utf8_from_wide_char(const wchar_t *ws) { } #endif + +char *sc_str_wrap_lines(const char *input, unsigned columns, unsigned indent) { + assert(indent < columns); + + struct sc_strbuf buf; + + // The output string should not be much longer than the input string (just + // a few '\n' added), so this initial capacity should hopefully almost + // always avoid internal realloc() in string buffer + size_t cap = strlen(input) * 3 / 2; + + if (!sc_strbuf_init(&buf, cap)) { + return false; + } + +#define APPEND(S,N) if (!sc_strbuf_append(&buf, S, N)) goto error +#define APPEND_CHAR(C) if (!sc_strbuf_append_char(&buf, C)) goto error +#define APPEND_N(C,N) if (!sc_strbuf_append_n(&buf, C, N)) goto error +#define APPEND_INDENT() if (indent) APPEND_N(' ', indent) + + APPEND_INDENT(); + + // The last separator encountered, it must be inserted only conditionnaly, + // depending on the next token + char pending = 0; + + // col tracks the current column in the current line + size_t col = indent; + while (*input) { + size_t sep_idx = strcspn(input, "\n "); + size_t new_col = col + sep_idx; + if (pending == ' ') { + // The pending space counts + ++new_col; + } + bool wrap = new_col > columns; + + char sep = input[sep_idx]; + if (sep == ' ') + sep = ' '; + + if (wrap) { + APPEND_CHAR('\n'); + APPEND_INDENT(); + col = indent; + } else if (pending) { + APPEND_CHAR(pending); + ++col; + if (pending == '\n') + { + APPEND_INDENT(); + col = indent; + } + } + + if (sep_idx) { + APPEND(input, sep_idx); + col += sep_idx; + } + + pending = sep; + + input += sep_idx; + if (*input != '\0') { + // Skip the separator + ++input; + } + } + + if (pending) + APPEND_CHAR(pending); + + return buf.s; + +error: + free(buf.s); + return NULL; +} diff --git a/app/src/util/str_util.h b/app/src/util/str_util.h index 361d2bdd..344522c9 100644 --- a/app/src/util/str_util.h +++ b/app/src/util/str_util.h @@ -62,4 +62,12 @@ char * utf8_from_wide_char(const wchar_t *s); #endif +/** + * Wrap input lines to fit in `columns` columns + * + * Break input lines at word boundaries (spaces) so that they fit in `columns` + * columns, left-indented by `indent` spaces. + */ +char *sc_str_wrap_lines(const char *input, unsigned columns, unsigned indent); + #endif diff --git a/app/tests/test_strutil.c b/app/tests/test_strutil.c index dfd99658..2d23176e 100644 --- a/app/tests/test_strutil.c +++ b/app/tests/test_strutil.c @@ -299,6 +299,44 @@ static void test_strlist_contains(void) { assert(strlist_contains("xyz", '\0', "xyz")); } +static void test_wrap_lines(void) { + const char *s = "This is a text to test line wrapping. The lines must be " + "wrapped at a space or a line break.\n" + "\n" + "This rectangle must remains a rectangle because it is " + "drawn in lines having lengths lower than the specified " + "number of columns:\n" + " +----+\n" + " | |\n" + " +----+\n"; + + // |---- 1 1 2 2| + // |0 5 0 5 0 3| <-- 24 columns + const char *expected = " This is a text to\n" + " test line wrapping.\n" + " The lines must be\n" + " wrapped at a space\n" + " or a line break.\n" + " \n" + " This rectangle must\n" + " remains a rectangle\n" + " because it is drawn\n" + " in lines having\n" + " lengths lower than\n" + " the specified number\n" + " of columns:\n" + " +----+\n" + " | |\n" + " +----+\n"; + + char *formatted = sc_str_wrap_lines(s, 24, 4); + assert(formatted); + + assert(!strcmp(formatted, expected)); + + free(formatted); +} + int main(int argc, char *argv[]) { (void) argc; (void) argv; @@ -317,5 +355,6 @@ int main(int argc, char *argv[]) { test_parse_integers(); test_parse_integer_with_suffix(); test_strlist_contains(); + test_wrap_lines(); return 0; } From f59e9e3cb03bb9cdb8d69767ed0ffa1b686f6d87 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 7 Nov 2021 13:31:26 +0100 Subject: [PATCH 0740/2244] Structure command line options help It will allow to correctly print the help for the current terminal size (even if for now it is hardcoded to 80 columns). --- app/src/cli.c | 639 ++++++++++++++++++++++++++++++++------------------ 1 file changed, 407 insertions(+), 232 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index 45b87fb2..a4e7335d 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -8,244 +8,419 @@ #include "options.h" #include "util/log.h" +#include "util/strbuf.h" #include "util/str_util.h" #define STR_IMPL_(x) #x #define STR(x) STR_IMPL_(x) +struct sc_option { + char shortopt; + const char *longopt; + // no argument: argdesc == NULL && !optional_arg + // optional argument: argdesc != NULL && optional_arg + // required argument: argdesc != NULL && !optional_arg + const char *argdesc; + bool optional_arg; + const char *text; +}; + +static const struct sc_option options[] = { + { + .longopt = "always-on-top", + .text = "Make scrcpy window always on top (above other windows).", + }, + { + .shortopt = 'b', + .longopt = "bit-rate", + .argdesc = "value", + .text = "Encode the video at the gitven bit-rate, expressed in bits/s. " + "Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n" + "Default is " STR(DEFAULT_BIT_RATE) ".", + }, + { + .longopt = "codec-options", + .argdesc = "key[:type]=value[,...]", + .text = "Set a list of comma-separated key:type=value options for the " + "device encoder.\n" + "The possible values for 'type' are 'int' (default), 'long', " + "'float' and 'string'.\n" + "The list of possible codec options is available in the " + "Android documentation: " + "", + }, + { + .longopt = "crop", + .argdesc = "width:height:x:y", + .text = "Crop the device screen on the server.\n" + "The values are expressed in the device natural orientation " + "(typically, portrait for a phone, landscape for a tablet). " + "Any --max-size value is cmoputed on the cropped size.", + }, + { + .longopt = "disable-screensaver", + .text = "Disable screensaver while scrcpy is running.", + }, + { + .longopt = "display", + .argdesc = "id", + .text = "Specify the display id to mirror.\n" + "The list of possible display ids can be listed by:\n" + " adb shell dumpsys display\n" + "(search \"mDisplayId=\" in the output)\n" + "Default is 0.", + }, + { + .longopt = "display-buffer", + .argdesc = "ms", + .text = "Add a buffering delay (in milliseconds) before displaying. " + "This increases latency to compensate for jitter.\n" + "Default is 0 (no buffering).", + }, + { + .longopt = "encoder", + .argdesc = "name", + .text = "Use a specific MediaCodec encoder (must be a H.264 encoder).", + }, + { + .longopt = "force-adb-forward", + .text = "Do not attempt to use \"adb reverse\" to connect to the " + "device.", + }, + { + .longopt = "forward-all-clicks", + .text = "By default, right-click triggers BACK (or POWER on) and " + "middle-click triggers HOME. This option disables these " + "shortcuts and forwards the clicks to the device instead.", + }, + { + .shortopt = 'f', + .longopt = "fullscreen", + .text = "Start in fullscreen.", + }, + { + .shortopt = 'K', + .longopt = "hid-keyboard", + .text = "Simulate a physical keyboard by using HID over AOAv2.\n" + "It provides a better experience for IME users, and allows to " + "generate non-ASCII characters, contrary to the default " + "injection method.\n" + "It may only work over USB, and is currently only supported " + "on Linux.", + }, + { + .shortopt = 'h', + .longopt = "help", + .text = "Print this help.", + }, + { + .longopt = "legacy-paste", + .text = "Inject computer clipboard text as a sequence of key events " + "on Ctrl+v (like MOD+Shift+v).\n" + "This is a workaround for some devices not behaving as " + "expected when setting the device clipboard programmatically.", + }, + { + .longopt = "lock-video-orientation", + .argdesc = "value", + .optional_arg = true, + .text = "Lock video orientation to value.\n" + "Possible values are \"unlocked\", \"initial\" (locked to the " + "initial orientation), 0, 1, 2 and 3. Natural device " + "orientation is 0, and each increment adds a 90 degrees " + "rotation counterclockwise.\n" + "Default is \"unlocked\".\n" + "Passing the option without argument is equivalent to passing " + "\"initial\".", + }, + { + .longopt = "max-fps", + .argdesc = "value", + .text = "Limit the frame rate of screen capture (officially supported " + "since Android 10, but may work on earlier versions).", + }, + { + .shortopt = 'm', + .longopt = "max-size", + .argdesc = "value", + .text = "Limit both the width and height of the video to value. The " + "other dimension is computed so that the device aspect-ratio " + "is preserved.\n" + "Default is 0 (unlimited).", + }, + { + .shortopt = 'n', + .longopt = "no-control", + .text = "Disable device control (mirror the device in read-only).", + }, + { + .shortopt = 'N', + .longopt = "no-display", + .text = "Do not display device (only when screen recording " +#ifdef HAVE_V4L2 + "or V4L2 sink " +#endif + "is enabled).", + }, + { + .longopt = "no-key-repeat", + .text = "Do not forward repeated key events when a key is held down.", + }, + { + .longopt = "no-mipmaps", + .text = "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.", + }, + { + .shortopt = 'p', + .longopt = "port", + .argdesc = "port[:port]", + .text = "Set the TCP port (range) used by the client to listen.\n" + "Default is " STR(DEFAULT_LOCAL_PORT_RANGE_FIRST) ":" + STR(DEFAULT_LOCAL_PORT_RANGE_LAST) ".", + }, + { + .longopt = "power-off-on-close", + .text = "Turn the device screen off when closing scrcpy.", + }, + { + .longopt = "prefer-text", + .text = "Inject alpha characters and space as text events instead of" + "key events.\n" + "This avoids issues when combining multiple keys to enter a " + "special character, but breaks the expected behavior of alpha " + "keys in games (typically WASD).", + }, + { + .longopt = "push-target", + .argdesc = "path", + .text = "Set the target directory for pushing files to the device by " + "drag & drop. It is passed as is to \"adb push\".\n" + "Default is \"/sdcard/Download/\".", + }, + { + .shortopt = 'r', + .longopt = "record", + .argdesc = "file.mp4", + .text = "Record screen to file.\n" + "The format is determined by the --record-format option if " + "set, or by the file extension (.mp4 or .mkv).", + }, + { + .longopt = "record-format", + .argdesc = "format", + .text = "Force recording format (either mp4 or mkv).", + }, + { + .longopt = "render-driver", + .argdesc = "name", + .text = "Request SDL to use the given render driver (this is just a " + "hint).\n" + "Supported names are currently \"direct3d\", \"opengl\", " + "\"opengles2\", \"opengles\", \"metal\" and \"software\".\n" + "", + }, + { + .longopt = "rotation", + .argdesc = "value", + .text = "Set the initial display rotation.\n" + "Possible values are 0, 1, 2 and 3. Each increment adds a 90 " + "degrees rotation counterclockwise.", + }, + { + .shortopt = 's', + .longopt = "serial", + .argdesc = "serial", + .text = "The device serial number. Mandatory only if several devices " + "are connected to adb.", + }, + { + .longopt = "shortcut-mod", + .argdesc = "key[+...][,...]", + .text = "Specify the modifiers to use for scrcpy shortcuts.\n" + "Possible keys are \"lctrl\", \"rctrl\", \"lalt\", \"ralt\", " + "\"lsuper\" and \"rsuper\".\n" + "A shortcut can consist in several keys, separated by '+'. " + "Several shortcuts can be specified, separated by ','.\n" + "For example, to use either LCtrl+LAlt or LSuper for scrcpy " + "shortcuts, pass \"lctrl+lalt,lsuper\".\n" + "Default is \"lalt,lsuper\" (left-Alt or left-Super).", + }, + { + .shortopt = 'S', + .longopt = "turn-screen-off", + .text = "Turn the device screen off immediately.", + }, + { + .shortopt = 't', + .longopt = "show-touches", + .text = "Enable \"show touches\" on start, restore the initial value " + "on exit.\n" + "It only shows physical touches (not clicks from scrcpy).", + }, +#ifdef HAVE_V4L2 + { + .longopt = "v4l2-sink", + .argdesc = "/dev/videoN", + .text = "Output to v4l2loopback device.\n" + "It requires to lock the video orientation (see " + "--lock-video-orientation).", + }, + { + .longopt = "v4l2-buffer", + .argdesc = "ms", + .text = "Add a buffering delay (in milliseconds) before pushing " + "frames. This increases latency to compensate for jitter.\n" + "This option is similar to --display-buffer, but specific to " + "V4L2 sink.\n" + "Default is 0 (no buffering).", + }, +#endif + { + .shortopt = 'V', + .longopt = "verbosity", + .argdesc = "value", + .text = "Set the log level (verbose, debug, info, warn or error).\n" +#ifndef NDEBUG + "Default is debug.", +#else + "Default is info.", +#endif + }, + { + .shortopt = 'v', + .longopt = "version", + .text = "Print the version of scrcpy.", + }, + { + .shortopt = 'w', + .longopt = "stay-awake", + .text = "Keep the device on while scrcpy is running, when the device " + "is plugged in.", + }, + { + .longopt = "window-borderless", + .text = "Disable window decorations (display borderless window)." + }, + { + .longopt = "window-title", + .argdesc = "text", + .text = "Set a custom window title.", + }, + { + .longopt = "window-x", + .argdesc = "value", + .text = "Set the initial window horizontal position.\n" + "Default is \"auto\".", + }, + { + .longopt = "window-y", + .argdesc = "value", + .text = "Set the initial window vertical position.\n" + "Default is \"auto\".", + }, + { + .longopt = "window-width", + .argdesc = "value", + .text = "Set the initial window width.\n" + "Default is 0 (automatic).", + }, + { + .longopt = "window-height", + .argdesc = "value", + .text = "Set the initial window height.\n" + "Default is 0 (automatic).", + }, +}; + +static void +print_option_usage_header(const struct sc_option *opt) { + struct sc_strbuf buf; + if (!sc_strbuf_init(&buf, 64)) { + goto error; + } + + bool ok = true; + (void) ok; // only used for assertions + + if (opt->shortopt) { + ok = sc_strbuf_append_char(&buf, '-'); + assert(ok); + + ok = sc_strbuf_append_char(&buf, opt->shortopt); + assert(ok); + + if (opt->longopt) { + ok = sc_strbuf_append_staticstr(&buf, ", "); + assert(ok); + } + } + + if (opt->longopt) { + ok = sc_strbuf_append_staticstr(&buf, "--"); + assert(ok); + + if (!sc_strbuf_append_str(&buf, opt->longopt)) { + goto error; + } + } + + if (opt->argdesc) { + if (opt->optional_arg && !sc_strbuf_append_char(&buf, '[')) { + goto error; + } + + if (!sc_strbuf_append_char(&buf, '=')) { + goto error; + } + + if (!sc_strbuf_append_str(&buf, opt->argdesc)) { + goto error; + } + + if (opt->optional_arg && !sc_strbuf_append_char(&buf, ']')) { + goto error; + } + } + + fprintf(stderr, "\n %s\n", buf.s); + free(buf.s); + return; + +error: + fprintf(stderr, "\n"); +} + +static void +print_option_usage(const struct sc_option *opt, unsigned cols) { + assert(cols > 8); // sc_str_wrap_lines() requires indent < columns + assert(opt->text); + + print_option_usage_header(opt); + + char *text = sc_str_wrap_lines(opt->text, cols, 8); + if (!text) { + fprintf(stderr, "\n"); + return; + } + + fprintf(stderr, "%s\n", text); + free(text); +} + void scrcpy_print_usage(const char *arg0) { - fprintf(stderr, - "Usage: %s [options]\n" - "\n" - "Options:\n" - "\n" - " --always-on-top\n" - " Make scrcpy window always on top (above other windows).\n" - "\n" - " -b, --bit-rate value\n" - " Encode the video at the given bit-rate, expressed in bits/s.\n" - " Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n" - " Default is " STR(DEFAULT_BIT_RATE) ".\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" - " (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" - " 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" - " --display-buffer ms\n" - " Add a buffering delay (in milliseconds) before displaying.\n" - " This increases latency to compensate for jitter.\n" - "\n" - " Default is 0 (no buffering).\n" - "\n" - " --encoder name\n" - " Use a specific MediaCodec encoder (must be a H.264 encoder).\n" - "\n" - " --force-adb-forward\n" - " Do not attempt to use \"adb reverse\" to connect to the\n" - " device.\n" - "\n" - " --forward-all-clicks\n" - " By default, right-click triggers BACK (or POWER on) and\n" - " middle-click triggers HOME. This option disables these\n" - " shortcuts and forwards the clicks to the device instead.\n" - "\n" - " -f, --fullscreen\n" - " Start in fullscreen.\n" - "\n" - " -K, --hid-keyboard\n" - " Simulate a physical keyboard by using HID over AOAv2.\n" - " It provides a better experience for IME users, and allows to\n" - " generate non-ASCII characters, contrary to the default\n" - " injection method.\n" - " It may only work over USB, and is currently only supported\n" - " on Linux.\n" - "\n" - " -h, --help\n" - " Print this help.\n" - "\n" - " --legacy-paste\n" - " Inject computer clipboard text as a sequence of key events\n" - " on Ctrl+v (like MOD+Shift+v).\n" - " This is a workaround for some devices not behaving as\n" - " expected when setting the device clipboard programmatically.\n" - "\n" - " --lock-video-orientation[=value]\n" - " Lock video orientation to value.\n" - " Possible values are \"unlocked\", \"initial\" (locked to the\n" - " initial orientation), 0, 1, 2 and 3.\n" - " Natural device orientation is 0, and each increment adds a\n" - " 90 degrees rotation counterclockwise.\n" - " Default is \"unlocked\".\n" - " Passing the option without argument is equivalent to passing\n" - " \"initial\".\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" - "\n" - " -m, --max-size value\n" - " Limit both the width and height of the video to value. The\n" - " other dimension is computed so that the device aspect-ratio\n" - " is preserved.\n" - " Default is 0 (unlimited).\n" - "\n" - " -n, --no-control\n" - " Disable device control (mirror the device in read-only).\n" - "\n" - " -N, --no-display\n" - " Do not display device (only when screen recording is\n" - " enabled).\n" - "\n" - " --no-key-repeat\n" - " Do not forward repeated key events when a key is held down.\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 " STR(DEFAULT_LOCAL_PORT_RANGE_FIRST) ":" - STR(DEFAULT_LOCAL_PORT_RANGE_LAST) ".\n" - "\n" - " --power-off-on-close\n" - " Turn the device screen off when closing scrcpy.\n" - "\n" - " --prefer-text\n" - " Inject alpha characters and space as text events instead of\n" - " key events.\n" - " This avoids issues when combining multiple keys to enter a\n" - " special character, but breaks the expected behavior of alpha\n" - " keys in games (typically WASD).\n" - "\n" - " --push-target path\n" - " Set the target directory for pushing files to the device by\n" - " drag & drop. It is passed as is to \"adb push\".\n" - " Default is \"/sdcard/Download/\".\n" - "\n" - " -r, --record file.mp4\n" - " Record screen to file.\n" - " The format is determined by the --record-format option if\n" - " set, or by the file extension (.mp4 or .mkv).\n" - "\n" - " --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" - " --rotation value\n" - " Set the initial display rotation.\n" - " Possible 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" - "\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,lsuper\" (left-Alt or left-Super).\n" - "\n" - " -S, --turn-screen-off\n" - " Turn the device screen off immediately.\n" - "\n" - " -t, --show-touches\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" -#ifdef HAVE_V4L2 - " --v4l2-sink /dev/videoN\n" - " Output to v4l2loopback device.\n" - " It requires to lock the video orientation (see\n" - " --lock-video-orientation).\n" - "\n" - " --v4l2-buffer ms\n" - " Add a buffering delay (in milliseconds) before pushing\n" - " frames. This increases latency to compensate for jitter.\n" - "\n" - " This option is similar to --display-buffer, but specific to\n" - " V4L2 sink.\n" - "\n" - " Default is 0 (no buffering).\n" - "\n" -#endif - " -V, --verbosity value\n" - " Set the log level (verbose, debug, info, warn or error).\n" -#ifndef NDEBUG - " Default is debug.\n" -#else - " Default is info.\n" -#endif - "\n" - " -v, --version\n" - " Print the version of scrcpy.\n" - "\n" - " -w, --stay-awake\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" - "\n" - " --window-title text\n" - " Set a custom window title.\n" - "\n" - " --window-x value\n" - " Set the initial window horizontal position.\n" - " Default is \"auto\".\n" - "\n" - " --window-y value\n" - " Set the initial window vertical position.\n" - " Default is \"auto\".\n" - "\n" - " --window-width value\n" - " Set the initial window width.\n" - " Default is 0 (automatic).\n" - "\n" - " --window-height value\n" - " Set the initial window height.\n" - " Default is 0 (automatic).\n" - "\n" + const unsigned cols = 80; // For now, use a hardcoded value + + fprintf(stderr, "Usage: %s [options]\n\n" + "Options:\n", arg0); + for (size_t i = 0; i < ARRAY_LEN(options); ++i) { + print_option_usage(&options[i], cols); + } + + // Print shortcuts section + fprintf(stderr, "\n" "Shortcuts:\n" "\n" " In the following list, MOD is the shortcut modifier. By default,\n" @@ -334,7 +509,7 @@ scrcpy_print_usage(const char *arg0) { "\n" " Drag & drop non-APK file\n" " Push file to device (see --push-target)\n" - "\n", arg0); + "\n"); } static bool From 74ab0a4df814064877d0f84b6c03c82c2039aba7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 7 Nov 2021 18:53:06 +0100 Subject: [PATCH 0741/2244] Structure shortcuts help --- app/src/cli.c | 253 ++++++++++++++++++++++++++++++++------------------ 1 file changed, 163 insertions(+), 90 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index a4e7335d..28474a9d 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -4,6 +4,7 @@ #include #include #include +#include #include #include "options.h" @@ -25,6 +26,12 @@ struct sc_option { const char *text; }; +#define MAX_EQUIVALENT_SHORTCUTS 3 +struct sc_shortcut { + const char *shortcuts[MAX_EQUIVALENT_SHORTCUTS + 1]; + const char *text; +}; + static const struct sc_option options[] = { { .longopt = "always-on-top", @@ -334,6 +341,118 @@ static const struct sc_option options[] = { }, }; +static const struct sc_shortcut shortcuts[] = { + { + .shortcuts = { "MOD+f" }, + .text = "Switch fullscreen mode", + }, + { + .shortcuts = { "MOD+Left" }, + .text = "Rotate display left", + }, + { + .shortcuts = { "MOD+Right" }, + .text = "Rotate display right", + }, + { + .shortcuts = { "MOD+g" }, + .text = "Resize window to 1:1 (pixel-perfect)", + }, + { + .shortcuts = { "MOD+w", "Double-click on black borders" }, + .text = "Resize window to remove black borders", + }, + { + .shortcuts = { "MOD+h", "Middle-click" }, + .text = "Click on HOME", + }, + { + .shortcuts = { + "MOD+b", + "MOD+Backspace", + "Right-click (when screen is on)", + }, + .text = "Click on BACK", + }, + { + .shortcuts = { "MOD+s" }, + .text = "Click on APP_SWITCH", + }, + { + .shortcuts = { "MOD+m" }, + .text = "Click on MENU", + }, + { + .shortcuts = { "MOD+Up" }, + .text = "Click on VOLUME_UP", + }, + { + .shortcuts = { "MOD+Down" }, + .text = "Click on VOLUME_DOWN", + }, + { + .shortcuts = { "MOD+p" }, + .text = "Click on POWER (turn screen on/off)", + }, + { + .shortcuts = { "Right-click (when screen is off)" }, + .text = "Power on", + }, + { + .shortcuts = { "MOD+o" }, + .text = "Turn device screen off (keep mirroring)", + }, + { + .shortcuts = { "MOD+Shift+o" }, + .text = "Turn device screen on", + }, + { + .shortcuts = { "MOD+r" }, + .text = "Rotate device screen", + }, + { + .shortcuts = { "MOD+n" }, + .text = "Expand notification panel", + }, + { + .shortcuts = { "MOD+Shift+n" }, + .text = "Collapse notification panel", + }, + { + .shortcuts = { "MOD+c" }, + .text = "Copy to clipboard (inject COPY keycode, Android >= 7 only)", + }, + { + .shortcuts = { "MOD+x" }, + .text = "Cut to clipboard (inject CUT keycode, Android >= 7 only)", + }, + { + .shortcuts = { "MOD+v" }, + .text = "Copy computer clipboard to device, then paste (inject PASTE " + "keycode, Android >= 7 only)", + }, + { + .shortcuts = { "MOD+Shift+v" }, + .text = "Inject computer clipboard text as a sequence of key events", + }, + { + .shortcuts = { "MOD+i" }, + .text = "Enable/disable FPS counter (print frames/second in logs)", + }, + { + .shortcuts = { "Ctrl+click-and-move" }, + .text = "Pinch-to-zoom from the center of the screen", + }, + { + .shortcuts = { "Drag & drop APK file" }, + .text = "Install APK from computer", + }, + { + .shortcuts = { "Drag & drop non-APK file" }, + .text = "Push file to device (see --push-target)", + }, +}; + static void print_option_usage_header(const struct sc_option *opt) { struct sc_strbuf buf; @@ -409,6 +528,45 @@ print_option_usage(const struct sc_option *opt, unsigned cols) { free(text); } +static void +print_shortcuts_intro(unsigned cols) { + char *intro = sc_str_wrap_lines( + "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 (see above).", cols, 4); + if (!intro) { + fprintf(stderr, "\n"); + return; + } + + fprintf(stderr, "%s\n", intro); + free(intro); +} + +static void +print_shortcut(const struct sc_shortcut *shortcut, unsigned cols) { + assert(cols > 8); // sc_str_wrap_lines() requires indent < columns + assert(shortcut->shortcuts[0]); // At least one shortcut + assert(shortcut->text); + + fprintf(stderr, "\n"); + + unsigned i = 0; + while (shortcut->shortcuts[i]) { + fprintf(stderr, " %s\n", shortcut->shortcuts[i]); + ++i; + }; + + char *text = sc_str_wrap_lines(shortcut->text, cols, 8); + if (!text) { + fprintf(stderr, "\n"); + return; + } + + fprintf(stderr, "%s\n", text); + free(text); +} + void scrcpy_print_usage(const char *arg0) { const unsigned cols = 80; // For now, use a hardcoded value @@ -420,96 +578,11 @@ scrcpy_print_usage(const char *arg0) { } // Print shortcuts section - fprintf(stderr, "\n" - "Shortcuts:\n" - "\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 (see above).\n" - "\n" - " MOD+f\n" - " Switch fullscreen mode\n" - "\n" - " MOD+Left\n" - " Rotate display left\n" - "\n" - " MOD+Right\n" - " Rotate display right\n" - "\n" - " MOD+g\n" - " Resize window to 1:1 (pixel-perfect)\n" - "\n" - " MOD+w\n" - " Double-click on black borders\n" - " Resize window to remove black borders\n" - "\n" - " MOD+h\n" - " Middle-click\n" - " Click on HOME\n" - "\n" - " MOD+b\n" - " MOD+Backspace\n" - " Right-click (when screen is on)\n" - " Click on BACK\n" - "\n" - " MOD+s\n" - " Click on APP_SWITCH\n" - "\n" - " MOD+m\n" - " Click on MENU\n" - "\n" - " MOD+Up\n" - " Click on VOLUME_UP\n" - "\n" - " MOD+Down\n" - " Click on VOLUME_DOWN\n" - "\n" - " MOD+p\n" - " Click on POWER (turn screen on/off)\n" - "\n" - " Right-click (when screen is off)\n" - " Power on\n" - "\n" - " MOD+o\n" - " Turn device screen off (keep mirroring)\n" - "\n" - " MOD+Shift+o\n" - " Turn device screen on\n" - "\n" - " MOD+r\n" - " Rotate device screen\n" - "\n" - " MOD+n\n" - " Expand notification panel\n" - "\n" - " 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" - " Copy computer clipboard to device, then paste (inject PASTE\n" - " keycode, Android >= 7 only)\n" - "\n" - " MOD+Shift+v\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" - "\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" - " Drag & drop non-APK file\n" - " Push file to device (see --push-target)\n" - "\n"); + fprintf(stderr, "\nShortcuts:\n\n"); + print_shortcuts_intro(cols); + for (size_t i = 0; i < ARRAY_LEN(shortcuts); ++i) { + print_shortcut(&shortcuts[i], cols); + } } static bool From 3f51a2ab43db7bfd0a09923e2a9d4604e556fce9 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 7 Nov 2021 19:24:13 +0100 Subject: [PATCH 0742/2244] Generate getopt params from option structures Use the option descriptions to generate the optstring and longopts parameters for the getopt_long() command. That way, the options are completely described in a single place. --- app/src/cli.c | 288 +++++++++++++++++++++++++++++++++----------------- 1 file changed, 193 insertions(+), 95 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index 28474a9d..f94504c6 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -15,15 +15,47 @@ #define STR_IMPL_(x) #x #define STR(x) STR_IMPL_(x) +#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 +#define OPT_DISPLAY_ID 1014 +#define OPT_ROTATION 1015 +#define OPT_RENDER_DRIVER 1016 +#define OPT_NO_MIPMAPS 1017 +#define OPT_CODEC_OPTIONS 1018 +#define OPT_FORCE_ADB_FORWARD 1019 +#define OPT_DISABLE_SCREENSAVER 1020 +#define OPT_SHORTCUT_MOD 1021 +#define OPT_NO_KEY_REPEAT 1022 +#define OPT_FORWARD_ALL_CLICKS 1023 +#define OPT_LEGACY_PASTE 1024 +#define OPT_ENCODER_NAME 1025 +#define OPT_POWER_OFF_ON_CLOSE 1026 +#define OPT_V4L2_SINK 1027 +#define OPT_DISPLAY_BUFFER 1028 +#define OPT_V4L2_BUFFER 1029 + struct sc_option { char shortopt; + int longopt_id; // either shortopt or longopt_id is non-zero const char *longopt; // no argument: argdesc == NULL && !optional_arg // optional argument: argdesc != NULL && optional_arg // required argument: argdesc != NULL && !optional_arg const char *argdesc; bool optional_arg; - const char *text; + const char *text; // if NULL, the option does not appear in the help }; #define MAX_EQUIVALENT_SHORTCUTS 3 @@ -32,8 +64,14 @@ struct sc_shortcut { const char *text; }; +struct sc_getopt_adapter { + char *optstring; + struct option *longopts; +}; + static const struct sc_option options[] = { { + .longopt_id = OPT_ALWAYS_ON_TOP, .longopt = "always-on-top", .text = "Make scrcpy window always on top (above other windows).", }, @@ -46,6 +84,7 @@ static const struct sc_option options[] = { "Default is " STR(DEFAULT_BIT_RATE) ".", }, { + .longopt_id = OPT_CODEC_OPTIONS, .longopt = "codec-options", .argdesc = "key[:type]=value[,...]", .text = "Set a list of comma-separated key:type=value options for the " @@ -57,6 +96,7 @@ static const struct sc_option options[] = { "", }, { + .longopt_id = OPT_CROP, .longopt = "crop", .argdesc = "width:height:x:y", .text = "Crop the device screen on the server.\n" @@ -65,10 +105,12 @@ static const struct sc_option options[] = { "Any --max-size value is cmoputed on the cropped size.", }, { + .longopt_id = OPT_DISABLE_SCREENSAVER, .longopt = "disable-screensaver", .text = "Disable screensaver while scrcpy is running.", }, { + .longopt_id = OPT_DISPLAY_ID, .longopt = "display", .argdesc = "id", .text = "Specify the display id to mirror.\n" @@ -78,6 +120,7 @@ static const struct sc_option options[] = { "Default is 0.", }, { + .longopt_id = OPT_DISPLAY_BUFFER, .longopt = "display-buffer", .argdesc = "ms", .text = "Add a buffering delay (in milliseconds) before displaying. " @@ -85,16 +128,19 @@ static const struct sc_option options[] = { "Default is 0 (no buffering).", }, { + .longopt_id = OPT_ENCODER_NAME, .longopt = "encoder", .argdesc = "name", .text = "Use a specific MediaCodec encoder (must be a H.264 encoder).", }, { + .longopt_id = OPT_FORCE_ADB_FORWARD, .longopt = "force-adb-forward", .text = "Do not attempt to use \"adb reverse\" to connect to the " "device.", }, { + .longopt_id = OPT_FORWARD_ALL_CLICKS, .longopt = "forward-all-clicks", .text = "By default, right-click triggers BACK (or POWER on) and " "middle-click triggers HOME. This option disables these " @@ -121,6 +167,7 @@ static const struct sc_option options[] = { .text = "Print this help.", }, { + .longopt_id = OPT_LEGACY_PASTE, .longopt = "legacy-paste", .text = "Inject computer clipboard text as a sequence of key events " "on Ctrl+v (like MOD+Shift+v).\n" @@ -128,6 +175,7 @@ static const struct sc_option options[] = { "expected when setting the device clipboard programmatically.", }, { + .longopt_id = OPT_LOCK_VIDEO_ORIENTATION, .longopt = "lock-video-orientation", .argdesc = "value", .optional_arg = true, @@ -141,6 +189,7 @@ static const struct sc_option options[] = { "\"initial\".", }, { + .longopt_id = OPT_MAX_FPS, .longopt = "max-fps", .argdesc = "value", .text = "Limit the frame rate of screen capture (officially supported " @@ -170,10 +219,12 @@ static const struct sc_option options[] = { "is enabled).", }, { + .longopt_id = OPT_NO_KEY_REPEAT, .longopt = "no-key-repeat", .text = "Do not forward repeated key events when a key is held down.", }, { + .longopt_id = OPT_NO_MIPMAPS, .longopt = "no-mipmaps", .text = "If the renderer is OpenGL 3.0+ or OpenGL ES 2.0+, then " "mipmaps are automatically generated to improve downscaling " @@ -188,10 +239,12 @@ static const struct sc_option options[] = { STR(DEFAULT_LOCAL_PORT_RANGE_LAST) ".", }, { + .longopt_id = OPT_POWER_OFF_ON_CLOSE, .longopt = "power-off-on-close", .text = "Turn the device screen off when closing scrcpy.", }, { + .longopt_id = OPT_PREFER_TEXT, .longopt = "prefer-text", .text = "Inject alpha characters and space as text events instead of" "key events.\n" @@ -200,6 +253,7 @@ static const struct sc_option options[] = { "keys in games (typically WASD).", }, { + .longopt_id = OPT_PUSH_TARGET, .longopt = "push-target", .argdesc = "path", .text = "Set the target directory for pushing files to the device by " @@ -215,11 +269,13 @@ static const struct sc_option options[] = { "set, or by the file extension (.mp4 or .mkv).", }, { + .longopt_id = OPT_RECORD_FORMAT, .longopt = "record-format", .argdesc = "format", .text = "Force recording format (either mp4 or mkv).", }, { + .longopt_id = OPT_RENDER_DRIVER, .longopt = "render-driver", .argdesc = "name", .text = "Request SDL to use the given render driver (this is just a " @@ -229,6 +285,12 @@ static const struct sc_option options[] = { "", }, { + // deprecated + .longopt_id = OPT_RENDER_EXPIRED_FRAMES, + .longopt = "render-expired-frames", + }, + { + .longopt_id = OPT_ROTATION, .longopt = "rotation", .argdesc = "value", .text = "Set the initial display rotation.\n" @@ -243,6 +305,7 @@ static const struct sc_option options[] = { "are connected to adb.", }, { + .longopt_id = OPT_SHORTCUT_MOD, .longopt = "shortcut-mod", .argdesc = "key[+...][,...]", .text = "Specify the modifiers to use for scrcpy shortcuts.\n" @@ -268,6 +331,7 @@ static const struct sc_option options[] = { }, #ifdef HAVE_V4L2 { + .longopt_id = OPT_V4L2_SINK, .longopt = "v4l2-sink", .argdesc = "/dev/videoN", .text = "Output to v4l2loopback device.\n" @@ -275,6 +339,7 @@ static const struct sc_option options[] = { "--lock-video-orientation).", }, { + .longopt_id = OPT_V4L2_BUFFER, .longopt = "v4l2-buffer", .argdesc = "ms", .text = "Add a buffering delay (in milliseconds) before pushing " @@ -307,33 +372,39 @@ static const struct sc_option options[] = { "is plugged in.", }, { + .longopt_id = OPT_WINDOW_BORDERLESS, .longopt = "window-borderless", .text = "Disable window decorations (display borderless window)." }, { + .longopt_id = OPT_WINDOW_TITLE, .longopt = "window-title", .argdesc = "text", .text = "Set a custom window title.", }, { + .longopt_id = OPT_WINDOW_X, .longopt = "window-x", .argdesc = "value", .text = "Set the initial window horizontal position.\n" "Default is \"auto\".", }, { + .longopt_id = OPT_WINDOW_Y, .longopt = "window-y", .argdesc = "value", .text = "Set the initial window vertical position.\n" "Default is \"auto\".", }, { + .longopt_id = OPT_WINDOW_WIDTH, .longopt = "window-width", .argdesc = "value", .text = "Set the initial window width.\n" "Default is 0 (automatic).", }, { + .longopt_id = OPT_WINDOW_HEIGHT, .longopt = "window-height", .argdesc = "value", .text = "Set the initial window height.\n" @@ -453,6 +524,102 @@ static const struct sc_shortcut shortcuts[] = { }, }; +static char * +sc_getopt_adapter_create_optstring(void) { + struct sc_strbuf buf; + if (!sc_strbuf_init(&buf, 64)) { + return false; + } + + for (size_t i = 0; i < ARRAY_LEN(options); ++i) { + const struct sc_option *opt = &options[i]; + if (opt->shortopt) { + if (!sc_strbuf_append_char(&buf, opt->shortopt)) { + goto error; + } + // If there is an argument, add ':' + if (opt->argdesc) { + if (!sc_strbuf_append_char(&buf, ':')) { + goto error; + } + // If the argument is optional, add another ':' + if (opt->optional_arg && !sc_strbuf_append_char(&buf, ':')) { + goto error; + } + } + } + } + + return buf.s; + +error: + free(buf.s); + return NULL; +} + +static struct option * +sc_getopt_adapter_create_longopts(void) { + struct option *longopts = + malloc((ARRAY_LEN(options) + 1) * sizeof(*longopts)); + if (!longopts) { + return NULL; + } + + size_t out_idx = 0; + for (size_t i = 0; i < ARRAY_LEN(options); ++i) { + const struct sc_option *in = &options[i]; + if (!in->longopt) { + // The longopts array must only contain long options + continue; + } + struct option *out = &longopts[out_idx++]; + + out->name = in->longopt; + + if (!in->argdesc) { + assert(!in->optional_arg); + out->has_arg = no_argument; + } else if (in->optional_arg) { + out->has_arg = optional_argument; + } else { + out->has_arg = required_argument; + } + + out->flag = NULL; + + // Either shortopt or longopt_id is set, but not both + assert(!!in->shortopt ^ !!in->longopt_id); + out->val = in->shortopt ? in->shortopt : in->longopt_id; + } + + // The array must be terminated by a NULL item + longopts[out_idx] = (struct option) {0}; + + return longopts; +} + +static bool +sc_getopt_adapter_init(struct sc_getopt_adapter *adapter) { + adapter->optstring = sc_getopt_adapter_create_optstring(); + if (!adapter->optstring) { + return false; + } + + adapter->longopts = sc_getopt_adapter_create_longopts(); + if (!adapter->longopts) { + free(adapter->optstring); + return false; + } + + return true; +}; + +static void +sc_getopt_adapter_destroy(struct sc_getopt_adapter *adapter) { + free(adapter->optstring); + free(adapter->longopts); +} + static void print_option_usage_header(const struct sc_option *opt) { struct sc_strbuf buf; @@ -514,7 +681,11 @@ error: static void print_option_usage(const struct sc_option *opt, unsigned cols) { assert(cols > 8); // sc_str_wrap_lines() requires indent < columns - assert(opt->text); + + if (!opt->text) { + // Option not documented in help (for example because it is deprecated) + return; + } print_option_usage_header(opt); @@ -951,104 +1122,15 @@ 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_LOCK_VIDEO_ORIENTATION 1013 -#define OPT_DISPLAY_ID 1014 -#define OPT_ROTATION 1015 -#define OPT_RENDER_DRIVER 1016 -#define OPT_NO_MIPMAPS 1017 -#define OPT_CODEC_OPTIONS 1018 -#define OPT_FORCE_ADB_FORWARD 1019 -#define OPT_DISABLE_SCREENSAVER 1020 -#define OPT_SHORTCUT_MOD 1021 -#define OPT_NO_KEY_REPEAT 1022 -#define OPT_FORWARD_ALL_CLICKS 1023 -#define OPT_LEGACY_PASTE 1024 -#define OPT_ENCODER_NAME 1025 -#define OPT_POWER_OFF_ON_CLOSE 1026 -#define OPT_V4L2_SINK 1027 -#define OPT_DISPLAY_BUFFER 1028 -#define OPT_V4L2_BUFFER 1029 - -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}, - {"disable-screensaver", no_argument, NULL, - OPT_DISABLE_SCREENSAVER}, - {"display", required_argument, NULL, OPT_DISPLAY_ID}, - {"display-buffer", required_argument, NULL, OPT_DISPLAY_BUFFER}, - {"encoder", required_argument, NULL, OPT_ENCODER_NAME}, - {"force-adb-forward", no_argument, NULL, - OPT_FORCE_ADB_FORWARD}, - {"forward-all-clicks", no_argument, NULL, - OPT_FORWARD_ALL_CLICKS}, - {"fullscreen", no_argument, NULL, 'f'}, - {"help", no_argument, NULL, 'h'}, - {"hid-keyboard", no_argument, NULL, 'K'}, - {"legacy-paste", no_argument, NULL, OPT_LEGACY_PASTE}, - {"lock-video-orientation", optional_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'}, - {"no-key-repeat", no_argument, NULL, OPT_NO_KEY_REPEAT}, - {"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}, - {"render-driver", required_argument, NULL, OPT_RENDER_DRIVER}, - {"render-expired-frames", no_argument, NULL, - 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'}, -#ifdef HAVE_V4L2 - {"v4l2-sink", required_argument, NULL, OPT_V4L2_SINK}, - {"v4l2-buffer", required_argument, NULL, OPT_V4L2_BUFFER}, -#endif - {"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}, - {"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}, - {"power-off-on-close", no_argument, NULL, - OPT_POWER_OFF_ON_CLOSE}, - {NULL, 0, NULL, 0 }, - }; - +static bool +parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], + const char *optstring, const struct option *longopts) { struct scrcpy_options *opts = &args->opts; optind = 0; // reset to start from the first argument in tests int c; - while ((c = getopt_long(argc, argv, "b:fF:hKm:nNp:r:s:StvV:w", - long_options, NULL)) != -1) { + while ((c = getopt_long(argc, argv, optstring, longopts, NULL)) != -1) { switch (c) { case 'b': if (!parse_bit_rate(optarg, &opts->bit_rate)) { @@ -1288,3 +1370,19 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { return true; } + +bool +scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { + struct sc_getopt_adapter adapter; + if (!sc_getopt_adapter_init(&adapter)) { + LOGW("Could not create getopt adapter"); + return false; + } + + bool ret = parse_args_with_getopt(args, argc, argv, adapter.optstring, + adapter.longopts); + + sc_getopt_adapter_destroy(&adapter); + + return ret; +} From 38332f683c6744725d8f197f16fe513d4a287ee9 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 11 Nov 2021 15:22:39 +0100 Subject: [PATCH 0743/2244] Add util function to get terminal size --- app/meson.build | 2 ++ app/src/util/term.c | 51 +++++++++++++++++++++++++++++++++++++++++++++ app/src/util/term.h | 21 +++++++++++++++++++ 3 files changed, 74 insertions(+) create mode 100644 app/src/util/term.c create mode 100644 app/src/util/term.h diff --git a/app/meson.build b/app/meson.build index 0e33c084..d13e421e 100644 --- a/app/meson.build +++ b/app/meson.build @@ -29,6 +29,7 @@ src = [ 'src/util/process.c', 'src/util/strbuf.c', 'src/util/str_util.c', + 'src/util/term.c', 'src/util/thread.c', 'src/util/tick.c', ] @@ -189,6 +190,7 @@ if get_option('buildtype') == 'debug' 'src/options.c', 'src/util/strbuf.c', 'src/util/str_util.c', + 'src/util/term.c', ]], ['test_clock', [ 'tests/test_clock.c', diff --git a/app/src/util/term.c b/app/src/util/term.c new file mode 100644 index 00000000..ff6bc4b1 --- /dev/null +++ b/app/src/util/term.c @@ -0,0 +1,51 @@ +#include "term.h" + +#include + +#ifdef _WIN32 +# include +#else +# include +# include +#endif + +bool +sc_term_get_size(unsigned *rows, unsigned *cols) { +#ifdef _WIN32 + CONSOLE_SCREEN_BUFFER_INFO csbi; + + bool ok = + GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &csbi); + if (!ok) { + return false; + } + + if (rows) { + assert(csbi.srWindow.Bottom >= csbi.srWindow.Top); + *rows = csbi.srWindow.Bottom - csbi.srWindow.Top + 1; + } + + if (cols) { + assert(csbi.srWindow.Right >= csbi.srWindow.Left); + *cols = csbi.srWindow.Right - csbi.srWindow.Left + 1; + } + + return true; +#else + struct winsize ws; + int r = ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws); + if (r == -1) { + return false; + } + + if (rows) { + *rows = ws.ws_row; + } + + if (cols) { + *cols = ws.ws_col; + } + + return true; +#endif +} diff --git a/app/src/util/term.h b/app/src/util/term.h new file mode 100644 index 00000000..0211bcb4 --- /dev/null +++ b/app/src/util/term.h @@ -0,0 +1,21 @@ +#ifndef SC_TERM_H +#define SC_TERM_H + +#include "common.h" + +#include + +/** + * Return the terminal dimensions + * + * Return false if the dimensions could not be retrieved. + * + * Otherwise, return true, and: + * - if `rows` is not NULL, then the number of rows is written to `*rows`. + * - if `columns` is not NULL, then the number of columns is written to + * `*columns`. + */ +bool +sc_term_get_size(unsigned *rows, unsigned *cols); + +#endif From 7a733328bc5d20c1b5e2fed1b6f1715f2c89dc23 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 11 Nov 2021 15:22:39 +0100 Subject: [PATCH 0744/2244] Adapt help to terminal size If the error stream is a terminal, and we can retrieve the terminal size, wrap the help content according to the terminal width. --- app/src/cli.c | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/app/src/cli.c b/app/src/cli.c index f94504c6..e38ff9f6 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -11,6 +11,7 @@ #include "util/log.h" #include "util/strbuf.h" #include "util/str_util.h" +#include "util/term.h" #define STR_IMPL_(x) #x #define STR(x) STR_IMPL_(x) @@ -740,7 +741,23 @@ print_shortcut(const struct sc_shortcut *shortcut, unsigned cols) { void scrcpy_print_usage(const char *arg0) { - const unsigned cols = 80; // For now, use a hardcoded value +#define SC_TERM_COLS_DEFAULT 80 + unsigned cols; + + if (!isatty(STDERR_FILENO)) { + // Not a tty + cols = SC_TERM_COLS_DEFAULT; + } else { + bool ok = sc_term_get_size(NULL, &cols); + if (!ok) { + // Could not get the terminal size + cols = SC_TERM_COLS_DEFAULT; + } + if (cols < 20) { + // Do not accept a too small value + cols = 20; + } + } fprintf(stderr, "Usage: %s [options]\n\n" "Options:\n", arg0); From c548f017bf26823c9781e6156c25f14e92144f45 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 12 Nov 2021 22:42:08 +0100 Subject: [PATCH 0745/2244] Upgrade junit to 4.13.1 --- server/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/build.gradle b/server/build.gradle index ef936d1b..cc03ecc8 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -20,7 +20,7 @@ android { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - testImplementation 'junit:junit:4.13' + testImplementation 'junit:junit:4.13.1' } apply from: "$project.rootDir/config/android-checkstyle.gradle" From be55e250ca64563d1a24f0d0d2d8c9d5fecb6ada Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 12 Nov 2021 22:39:36 +0100 Subject: [PATCH 0746/2244] Make screen_render() static It is only used from screen.c. --- app/src/screen.c | 73 ++++++++++++++++++++++++++---------------------- app/src/screen.h | 7 ----- 2 files changed, 39 insertions(+), 41 deletions(-) diff --git a/app/src/screen.c b/app/src/screen.c index e2d15180..d402b402 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -224,6 +224,45 @@ create_texture(struct screen *screen) { return texture; } +// 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 +static void +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, &screen->rect); + } 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) { + 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, + angle, NULL, 0); + } + SDL_RenderPresent(screen->renderer); +} + + #if defined(__APPLE__) || defined(__WINDOWS__) # define CONTINUOUS_RESIZING_WORKAROUND #endif @@ -642,40 +681,6 @@ screen_update_frame(struct screen *screen) { return true; } -void -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, &screen->rect); - } 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) { - 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, - angle, NULL, 0); - } - SDL_RenderPresent(screen->renderer); -} - void screen_switch_fullscreen(struct screen *screen) { uint32_t new_mode = screen->fullscreen ? 0 : SDL_WINDOW_FULLSCREEN_DESKTOP; diff --git a/app/src/screen.h b/app/src/screen.h index 51946dbb..b82bf631 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -93,13 +93,6 @@ screen_destroy(struct screen *screen); void screen_hide_window(struct screen *screen); -// 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, bool update_content_rect); - // switch the fullscreen mode void screen_switch_fullscreen(struct screen *screen); From d4c262301fb0543ecd4b6c7240f3d87d908b3bf2 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 11 Nov 2021 16:12:17 +0100 Subject: [PATCH 0747/2244] Move functions from process to file Move filesystem-related functions from process.[ch] to file.[ch]. --- app/meson.build | 11 +++++- app/src/adb.c | 1 + app/src/icon.c | 2 +- app/src/server.c | 1 + app/src/sys/unix/file.c | 75 ++++++++++++++++++++++++++++++++++++++ app/src/sys/unix/process.c | 71 ------------------------------------ app/src/sys/win/file.c | 43 ++++++++++++++++++++++ app/src/sys/win/process.c | 35 ------------------ app/src/util/file.c | 48 ++++++++++++++++++++++++ app/src/util/file.h | 34 +++++++++++++++++ app/src/util/process.c | 41 --------------------- app/src/util/process.h | 22 ----------- 12 files changed, 212 insertions(+), 172 deletions(-) create mode 100644 app/src/sys/unix/file.c create mode 100644 app/src/sys/win/file.c create mode 100644 app/src/util/file.c create mode 100644 app/src/util/file.h diff --git a/app/meson.build b/app/meson.build index d13e421e..94b4994f 100644 --- a/app/meson.build +++ b/app/meson.build @@ -24,6 +24,7 @@ src = [ 'src/server.c', 'src/stream.c', 'src/video_buffer.c', + 'src/util/file.c', 'src/util/log.c', 'src/util/net.c', 'src/util/process.c', @@ -35,9 +36,15 @@ src = [ ] if host_machine.system() == 'windows' - src += [ 'src/sys/win/process.c' ] + src += [ + 'src/sys/win/file.c', + 'src/sys/win/process.c', + ] else - src += [ 'src/sys/unix/process.c' ] + src += [ + 'src/sys/unix/file.c', + 'src/sys/unix/process.c', + ] endif v4l2_support = host_machine.system() == 'linux' diff --git a/app/src/adb.c b/app/src/adb.c index 7f7e28d8..d127494a 100644 --- a/app/src/adb.c +++ b/app/src/adb.c @@ -5,6 +5,7 @@ #include #include +#include "util/file.h" #include "util/log.h" #include "util/str_util.h" diff --git a/app/src/icon.c b/app/src/icon.c index 607c7162..787d048f 100644 --- a/app/src/icon.c +++ b/app/src/icon.c @@ -8,8 +8,8 @@ #include "config.h" #include "compat.h" +#include "util/file.h" #include "util/log.h" -#include "util/process.h" #include "util/str_util.h" #define SCRCPY_PORTABLE_ICON_FILENAME "icon.png" diff --git a/app/src/server.c b/app/src/server.c index 4a4d3ec4..aba77288 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -8,6 +8,7 @@ #include #include "adb.h" +#include "util/file.h" #include "util/log.h" #include "util/net.h" #include "util/str_util.h" diff --git a/app/src/sys/unix/file.c b/app/src/sys/unix/file.c new file mode 100644 index 00000000..2e71b113 --- /dev/null +++ b/app/src/sys/unix/file.c @@ -0,0 +1,75 @@ +#include "util/file.h" + +#include +#include +#include +#include +#include + +bool +search_executable(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; +} + +char * +get_executable_path(void) { +// +#ifdef __linux__ + char buf[PATH_MAX + 1]; // +1 for the null byte + ssize_t len = readlink("/proc/self/exe", buf, PATH_MAX); + if (len == -1) { + perror("readlink"); + return NULL; + } + buf[len] = '\0'; + return strdup(buf); +#else + // in practice, we only need this feature for portable builds, only used on + // Windows, so we don't care implementing it for every platform + // (it's useful to have a working version on Linux for debugging though) + 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/unix/process.c b/app/src/sys/unix/process.c index 451b6491..e534ae9d 100644 --- a/app/src/sys/unix/process.c +++ b/app/src/sys/unix/process.c @@ -3,53 +3,13 @@ #include #include #include -#include #include -#include -#include -#include #include #include #include #include "util/log.h" -bool -search_executable(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 process_execute_redirect(const char *const argv[], pid_t *pid, int *pipe_stdin, int *pipe_stdout, int *pipe_stderr) { @@ -232,37 +192,6 @@ process_close(pid_t pid) { process_wait(pid, true); // ignore exit code } -char * -get_executable_path(void) { -// -#ifdef __linux__ - char buf[PATH_MAX + 1]; // +1 for the null byte - ssize_t len = readlink("/proc/self/exe", buf, PATH_MAX); - if (len == -1) { - perror("readlink"); - return NULL; - } - buf[len] = '\0'; - return strdup(buf); -#else - // in practice, we only need this feature for portable builds, only used on - // Windows, so we don't care implementing it for every platform - // (it's useful to have a working version on Linux for debugging though) - 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); -} - ssize_t read_pipe(int pipe, char *data, size_t len) { return read(pipe, data, len); diff --git a/app/src/sys/win/file.c b/app/src/sys/win/file.c new file mode 100644 index 00000000..0f9101e9 --- /dev/null +++ b/app/src/sys/win/file.c @@ -0,0 +1,43 @@ +#include "util/file.h" + +#include + +#include + +#include "util/log.h" +#include "util/str_util.h" + +char * +get_executable_path(void) { + HMODULE hModule = GetModuleHandleW(NULL); + if (!hModule) { + return NULL; + } + WCHAR buf[MAX_PATH + 1]; // +1 for the null byte + int len = GetModuleFileNameW(hModule, buf, MAX_PATH); + if (!len) { + return NULL; + } + 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); + free(wide_path); + + if (r) { + perror("stat"); + return false; + } + return S_ISREG(path_stat.st_mode); +} + diff --git a/app/src/sys/win/process.c b/app/src/sys/win/process.c index 9a846fad..289d1fca 100644 --- a/app/src/sys/win/process.c +++ b/app/src/sys/win/process.c @@ -1,7 +1,6 @@ #include "util/process.h" #include -#include #include "util/log.h" #include "util/str_util.h" @@ -174,40 +173,6 @@ process_close(HANDLE handle) { (void) closed; } -char * -get_executable_path(void) { - HMODULE hModule = GetModuleHandleW(NULL); - if (!hModule) { - return NULL; - } - WCHAR buf[MAX_PATH + 1]; // +1 for the null byte - int len = GetModuleFileNameW(hModule, buf, MAX_PATH); - if (!len) { - return NULL; - } - 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); - free(wide_path); - - if (r) { - perror("stat"); - return false; - } - return S_ISREG(path_stat.st_mode); -} - ssize_t read_pipe(HANDLE pipe, char *data, size_t len) { DWORD r; diff --git a/app/src/util/file.c b/app/src/util/file.c new file mode 100644 index 00000000..bb7fdb2c --- /dev/null +++ b/app/src/util/file.c @@ -0,0 +1,48 @@ +#include "file.h" + +#include +#include + +#include "util/log.h" + +char * +get_local_file_path(const char *name) { + char *executable_path = get_executable_path(); + if (!executable_path) { + return NULL; + } + + // dirname() does not work correctly everywhere, so get the parent + // directory manually. + // See + char *p = strrchr(executable_path, PATH_SEPARATOR); + if (!p) { + LOGE("Unexpected executable path: \"%s\" (it should contain a '%c')", + executable_path, PATH_SEPARATOR); + free(executable_path); + return NULL; + } + + *p = '\0'; // modify executable_path in place + char *dir = executable_path; + size_t dirlen = strlen(dir); + size_t namelen = strlen(name); + + size_t len = dirlen + namelen + 2; // +2: '/' and '\0' + char *file_path = malloc(len); + if (!file_path) { + LOGE("Could not alloc path"); + free(executable_path); + return NULL; + } + + memcpy(file_path, dir, dirlen); + file_path[dirlen] = PATH_SEPARATOR; + // namelen + 1 to copy the final '\0' + memcpy(&file_path[dirlen + 1], name, namelen + 1); + + free(executable_path); + + return file_path; +} + diff --git a/app/src/util/file.h b/app/src/util/file.h new file mode 100644 index 00000000..813af486 --- /dev/null +++ b/app/src/util/file.h @@ -0,0 +1,34 @@ +#ifndef SC_FILE_H +#define SC_FILE_H + +#include "common.h" + +#include + +#ifdef _WIN32 +# define PATH_SEPARATOR '\\' +#else +# define PATH_SEPARATOR '/' +#endif + +#ifndef _WIN32 +// only used to find package manager, not implemented for Windows +bool +search_executable(const char *file); +#endif + +// return the absolute path of the executable (the scrcpy binary) +// may be NULL on error; to be freed by free() +char * +get_executable_path(void); + +// Return the absolute path of a file in the same directory as he executable. +// May be NULL on error. To be freed by free(). +char * +get_local_file_path(const char *name); + +// returns true if the file exists and is not a directory +bool +is_regular_file(const char *path); + +#endif diff --git a/app/src/util/process.c b/app/src/util/process.c index 5d572c26..637132d9 100644 --- a/app/src/util/process.c +++ b/app/src/util/process.c @@ -21,47 +21,6 @@ process_check_success(process_t proc, const char *name, bool close) { return true; } -char * -get_local_file_path(const char *name) { - char *executable_path = get_executable_path(); - if (!executable_path) { - return NULL; - } - - // dirname() does not work correctly everywhere, so get the parent - // directory manually. - // See - char *p = strrchr(executable_path, PATH_SEPARATOR); - if (!p) { - LOGE("Unexpected executable path: \"%s\" (it should contain a '%c')", - executable_path, PATH_SEPARATOR); - free(executable_path); - return NULL; - } - - *p = '\0'; // modify executable_path in place - char *dir = executable_path; - size_t dirlen = strlen(dir); - size_t namelen = strlen(name); - - size_t len = dirlen + namelen + 2; // +2: '/' and '\0' - char *file_path = malloc(len); - if (!file_path) { - LOGE("Could not alloc path"); - free(executable_path); - return NULL; - } - - memcpy(file_path, dir, dirlen); - file_path[dirlen] = PATH_SEPARATOR; - // namelen + 1 to copy the final '\0' - memcpy(&file_path[dirlen + 1], name, namelen + 1); - - free(executable_path); - - return file_path; -} - ssize_t read_pipe_all(pipe_t pipe, char *data, size_t len) { size_t copied = 0; diff --git a/app/src/util/process.h b/app/src/util/process.h index d6471a16..d609ae71 100644 --- a/app/src/util/process.h +++ b/app/src/util/process.h @@ -10,7 +10,6 @@ // not needed here, but winsock2.h must never be included AFTER windows.h # include # include -# define PATH_SEPARATOR '\\' # define PRIexitcode "lu" // # define PRIsizet "Iu" @@ -23,7 +22,6 @@ #else # include -# define PATH_SEPARATOR '/' # define PRIsizet "zu" # define PRIexitcode "d" # define PROCESS_NONE -1 @@ -73,26 +71,6 @@ process_close(process_t pid); bool process_check_success(process_t proc, const char *name, bool close); -#ifndef _WIN32 -// only used to find package manager, not implemented for Windows -bool -search_executable(const char *file); -#endif - -// return the absolute path of the executable (the scrcpy binary) -// may be NULL on error; to be freed by free() -char * -get_executable_path(void); - -// Return the absolute path of a file in the same directory as he executable. -// May be NULL on error. To be freed by free(). -char * -get_local_file_path(const char *name); - -// returns true if the file exists and is not a directory -bool -is_regular_file(const char *path); - ssize_t read_pipe(pipe_t pipe, char *data, size_t len); From fcc04f967b9463a949641d8d22f15959ff79f2f9 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 11 Nov 2021 16:21:07 +0100 Subject: [PATCH 0748/2244] Improve file API Prefix symbols and constants names and improve documentation. --- app/src/adb.c | 2 +- app/src/icon.c | 2 +- app/src/server.c | 4 ++-- app/src/sys/unix/file.c | 6 +++--- app/src/sys/win/file.c | 4 ++-- app/src/util/file.c | 10 +++++----- app/src/util/file.h | 39 +++++++++++++++++++++++++++------------ 7 files changed, 41 insertions(+), 26 deletions(-) diff --git a/app/src/adb.c b/app/src/adb.c index d127494a..1eb8d9ed 100644 --- a/app/src/adb.c +++ b/app/src/adb.c @@ -69,7 +69,7 @@ show_adb_installation_msg() { {"pacman", "pacman -S android-tools"}, }; for (size_t i = 0; i < ARRAY_LEN(pkg_managers); ++i) { - if (search_executable(pkg_managers[i].binary)) { + if (sc_file_executable_exists(pkg_managers[i].binary)) { LOGI("You may install 'adb' by \"%s\"", pkg_managers[i].command); return; } diff --git a/app/src/icon.c b/app/src/icon.c index 787d048f..f9799de7 100644 --- a/app/src/icon.c +++ b/app/src/icon.c @@ -46,7 +46,7 @@ get_icon_path(void) { return NULL; } #else - char *icon_path = get_local_file_path(SCRCPY_PORTABLE_ICON_FILENAME); + char *icon_path = sc_file_get_local_path(SCRCPY_PORTABLE_ICON_FILENAME); if (!icon_path) { LOGE("Could not get icon path"); return NULL; diff --git a/app/src/server.c b/app/src/server.c index aba77288..25d786c4 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -49,7 +49,7 @@ get_server_path(void) { return NULL; } #else - char *server_path = get_local_file_path(SERVER_FILENAME); + char *server_path = sc_file_get_local_path(SERVER_FILENAME); if (!server_path) { LOGE("Could not get local file path, " "using " SERVER_FILENAME " from current directory"); @@ -68,7 +68,7 @@ push_server(const char *serial) { if (!server_path) { return false; } - if (!is_regular_file(server_path)) { + if (!sc_file_is_regular(server_path)) { LOGE("'%s' does not exist or is not a regular file\n", server_path); free(server_path); return false; diff --git a/app/src/sys/unix/file.c b/app/src/sys/unix/file.c index 2e71b113..4e9e45b3 100644 --- a/app/src/sys/unix/file.c +++ b/app/src/sys/unix/file.c @@ -7,7 +7,7 @@ #include bool -search_executable(const char *file) { +sc_file_executable_exists(const char *file) { char *path = getenv("PATH"); if (!path) return false; @@ -43,7 +43,7 @@ search_executable(const char *file) { } char * -get_executable_path(void) { +sc_file_get_executable_path(void) { // #ifdef __linux__ char buf[PATH_MAX + 1]; // +1 for the null byte @@ -63,7 +63,7 @@ get_executable_path(void) { } bool -is_regular_file(const char *path) { +sc_file_is_regular(const char *path) { struct stat path_stat; if (stat(path, &path_stat)) { diff --git a/app/src/sys/win/file.c b/app/src/sys/win/file.c index 0f9101e9..badb8087 100644 --- a/app/src/sys/win/file.c +++ b/app/src/sys/win/file.c @@ -8,7 +8,7 @@ #include "util/str_util.h" char * -get_executable_path(void) { +sc_file_get_executable_path(void) { HMODULE hModule = GetModuleHandleW(NULL); if (!hModule) { return NULL; @@ -23,7 +23,7 @@ get_executable_path(void) { } bool -is_regular_file(const char *path) { +sc_file_is_regular(const char *path) { wchar_t *wide_path = utf8_to_wide_char(path); if (!wide_path) { LOGC("Could not allocate wide char string"); diff --git a/app/src/util/file.c b/app/src/util/file.c index bb7fdb2c..59be2d91 100644 --- a/app/src/util/file.c +++ b/app/src/util/file.c @@ -6,8 +6,8 @@ #include "util/log.h" char * -get_local_file_path(const char *name) { - char *executable_path = get_executable_path(); +sc_file_get_local_path(const char *name) { + char *executable_path = sc_file_get_executable_path(); if (!executable_path) { return NULL; } @@ -15,10 +15,10 @@ get_local_file_path(const char *name) { // dirname() does not work correctly everywhere, so get the parent // directory manually. // See - char *p = strrchr(executable_path, PATH_SEPARATOR); + char *p = strrchr(executable_path, SC_PATH_SEPARATOR); if (!p) { LOGE("Unexpected executable path: \"%s\" (it should contain a '%c')", - executable_path, PATH_SEPARATOR); + executable_path, SC_PATH_SEPARATOR); free(executable_path); return NULL; } @@ -37,7 +37,7 @@ get_local_file_path(const char *name) { } memcpy(file_path, dir, dirlen); - file_path[dirlen] = PATH_SEPARATOR; + file_path[dirlen] = SC_PATH_SEPARATOR; // namelen + 1 to copy the final '\0' memcpy(&file_path[dirlen + 1], name, namelen + 1); diff --git a/app/src/util/file.h b/app/src/util/file.h index 813af486..089f6f75 100644 --- a/app/src/util/file.h +++ b/app/src/util/file.h @@ -6,29 +6,44 @@ #include #ifdef _WIN32 -# define PATH_SEPARATOR '\\' +# define SC_PATH_SEPARATOR '\\' #else -# define PATH_SEPARATOR '/' +# define SC_PATH_SEPARATOR '/' #endif #ifndef _WIN32 -// only used to find package manager, not implemented for Windows +/** + * Indicate if an executable exists using $PATH + * + * In practice, it is only used to know if a package manager is available on + * the system. It is only implemented on Linux. + */ bool -search_executable(const char *file); +sc_file_executable_exists(const char *file); #endif -// return the absolute path of the executable (the scrcpy binary) -// may be NULL on error; to be freed by free() +/** + * Return the absolute path of the executable (the scrcpy binary) + * + * The result must be freed by the caller using free(). It may return NULL on + * error. + */ char * -get_executable_path(void); +sc_file_get_executable_path(void); -// Return the absolute path of a file in the same directory as he executable. -// May be NULL on error. To be freed by free(). +/** + * Return the absolute path of a file in the same directory as the executable + * + * The result must be freed by the caller using free(). It may return NULL on + * error. + */ char * -get_local_file_path(const char *name); +sc_file_get_local_path(const char *name); -// returns true if the file exists and is not a directory +/** + * Indicate if the file exists and is not a directory + */ bool -is_regular_file(const char *path); +sc_file_is_regular(const char *path); #endif From e80e6631e4c259e2e217f8425900b56002850a40 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 11 Nov 2021 17:29:36 +0100 Subject: [PATCH 0749/2244] Remove duplicate function declaration The function process_terminate() was declared twice. --- app/src/util/process.h | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/src/util/process.h b/app/src/util/process.h index d609ae71..08a70657 100644 --- a/app/src/util/process.h +++ b/app/src/util/process.h @@ -47,9 +47,6 @@ process_execute_redirect(const char *const argv[], process_t *process, pipe_t *pipe_stdin, pipe_t *pipe_stdout, pipe_t *pipe_stderr); -bool -process_terminate(process_t pid); - // kill the process bool process_terminate(process_t pid); From 7e93abcf6d8982f194e016b3ea0227c1834893fa Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 11 Nov 2021 17:49:47 +0100 Subject: [PATCH 0750/2244] Factorize common impl of process_execute() Both implementations are the same. Move them to the common process.c. --- app/src/sys/unix/process.c | 5 ----- app/src/sys/win/process.c | 5 ----- app/src/util/process.c | 5 +++++ 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/app/src/sys/unix/process.c b/app/src/sys/unix/process.c index e534ae9d..ef5d9d79 100644 --- a/app/src/sys/unix/process.c +++ b/app/src/sys/unix/process.c @@ -153,11 +153,6 @@ process_execute_redirect(const char *const argv[], pid_t *pid, int *pipe_stdin, return res; } -enum process_result -process_execute(const char *const argv[], pid_t *pid) { - return process_execute_redirect(argv, pid, NULL, NULL, NULL); -} - bool process_terminate(pid_t pid) { if (pid <= 0) { diff --git a/app/src/sys/win/process.c b/app/src/sys/win/process.c index 289d1fca..257427f3 100644 --- a/app/src/sys/win/process.c +++ b/app/src/sys/win/process.c @@ -142,11 +142,6 @@ error_close_stdin: return ret; } -enum process_result -process_execute(const char *const argv[], HANDLE *handle) { - return process_execute_redirect(argv, handle, NULL, NULL, NULL); -} - bool process_terminate(HANDLE handle) { return TerminateProcess(handle, 1); diff --git a/app/src/util/process.c b/app/src/util/process.c index 637132d9..dcb715e6 100644 --- a/app/src/util/process.c +++ b/app/src/util/process.c @@ -3,6 +3,11 @@ #include #include "log.h" +enum process_result +process_execute(const char *const argv[], process_t *pid) { + return process_execute_redirect(argv, pid, NULL, NULL, NULL); +} + bool process_check_success(process_t proc, const char *name, bool close) { if (proc == PROCESS_NONE) { From aa011832c155ad57230e8241932c3331218c95b3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 11 Nov 2021 17:48:41 +0100 Subject: [PATCH 0751/2244] Improve process API Prefix symbols and constants names and improve documentation. --- app/src/adb.c | 71 +++++++++++------------ app/src/adb.h | 21 ++++--- app/src/file_handler.c | 32 +++++------ app/src/file_handler.h | 2 +- app/src/server.c | 38 ++++++------- app/src/server.h | 2 +- app/src/sys/unix/process.c | 80 +++++++++++++------------- app/src/sys/win/process.c | 75 ++++++++++++------------ app/src/util/process.c | 21 +++---- app/src/util/process.h | 113 +++++++++++++++++++++++-------------- 10 files changed, 240 insertions(+), 215 deletions(-) diff --git a/app/src/adb.c b/app/src/adb.c index 1eb8d9ed..c7a64501 100644 --- a/app/src/adb.c +++ b/app/src/adb.c @@ -81,7 +81,7 @@ show_adb_installation_msg() { } static void -show_adb_err_msg(enum process_result err, const char *const argv[]) { +show_adb_err_msg(enum sc_process_result err, const char *const argv[]) { #define MAX_COMMAND_STRING_LEN 1024 char *buf = malloc(MAX_COMMAND_STRING_LEN); if (!buf) { @@ -90,18 +90,18 @@ show_adb_err_msg(enum process_result err, const char *const argv[]) { } switch (err) { - case PROCESS_ERROR_GENERIC: + case SC_PROCESS_ERROR_GENERIC: argv_to_string(argv, buf, MAX_COMMAND_STRING_LEN); LOGE("Failed to execute: %s", buf); break; - case PROCESS_ERROR_MISSING_BINARY: + case SC_PROCESS_ERROR_MISSING_BINARY: argv_to_string(argv, buf, MAX_COMMAND_STRING_LEN); 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: + case SC_PROCESS_SUCCESS: // do nothing break; } @@ -109,16 +109,15 @@ show_adb_err_msg(enum process_result err, const char *const argv[]) { free(buf); } -process_t -adb_execute_redirect(const char *serial, const char *const adb_cmd[], - size_t len, pipe_t *pipe_stdin, pipe_t *pipe_stdout, - pipe_t *pipe_stderr) { +sc_pid +adb_execute_p(const char *serial, const char *const adb_cmd[], + size_t len, sc_pipe *pin, sc_pipe *pout, sc_pipe *perr) { int i; - process_t process; + sc_pid pid; const char **argv = malloc((len + 4) * sizeof(*argv)); if (!argv) { - return PROCESS_NONE; + return SC_PROCESS_NONE; } argv[0] = get_adb_command(); @@ -132,24 +131,23 @@ adb_execute_redirect(const char *serial, const char *const adb_cmd[], memcpy(&argv[i], adb_cmd, len * sizeof(const char *)); argv[len + i] = NULL; - enum process_result r = - process_execute_redirect(argv, &process, pipe_stdin, pipe_stdout, - pipe_stderr); - if (r != PROCESS_SUCCESS) { + enum sc_process_result r = + sc_process_execute_p(argv, &pid, pin, pout, perr); + if (r != SC_PROCESS_SUCCESS) { show_adb_err_msg(r, argv); - process = PROCESS_NONE; + pid = SC_PROCESS_NONE; } free(argv); - return process; + return pid; } -process_t +sc_pid adb_execute(const char *serial, const char *const adb_cmd[], size_t len) { - return adb_execute_redirect(serial, adb_cmd, len, NULL, NULL, NULL); + return adb_execute_p(serial, adb_cmd, len, NULL, NULL, NULL); } -process_t +sc_pid adb_forward(const char *serial, uint16_t local_port, const char *device_socket_name) { char local[4 + 5 + 1]; // tcp:PORT @@ -160,7 +158,7 @@ adb_forward(const char *serial, uint16_t local_port, return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd)); } -process_t +sc_pid adb_forward_remove(const char *serial, uint16_t local_port) { char local[4 + 5 + 1]; // tcp:PORT sprintf(local, "tcp:%" PRIu16, local_port); @@ -168,7 +166,7 @@ adb_forward_remove(const char *serial, uint16_t local_port) { return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd)); } -process_t +sc_pid adb_reverse(const char *serial, const char *device_socket_name, uint16_t local_port) { char local[4 + 5 + 1]; // tcp:PORT @@ -179,7 +177,7 @@ adb_reverse(const char *serial, const char *device_socket_name, return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd)); } -process_t +sc_pid adb_reverse_remove(const char *serial, const char *device_socket_name) { char remote[108 + 14 + 1]; // localabstract:NAME snprintf(remote, sizeof(remote), "localabstract:%s", device_socket_name); @@ -187,66 +185,65 @@ adb_reverse_remove(const char *serial, const char *device_socket_name) { return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd)); } -process_t +sc_pid adb_push(const char *serial, const char *local, const char *remote) { #ifdef __WINDOWS__ // Windows will parse the string, so the paths must be quoted // (see sys/win/command.c) local = strquote(local); if (!local) { - return PROCESS_NONE; + return SC_PROCESS_NONE; } remote = strquote(remote); if (!remote) { free((void *) local); - return PROCESS_NONE; + return SC_PROCESS_NONE; } #endif const char *const adb_cmd[] = {"push", local, remote}; - process_t proc = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd)); + sc_pid pid = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd)); #ifdef __WINDOWS__ free((void *) remote); free((void *) local); #endif - return proc; + return pid; } -process_t +sc_pid adb_install(const char *serial, const char *local) { #ifdef __WINDOWS__ // Windows will parse the string, so the local name must be quoted // (see sys/win/command.c) local = strquote(local); if (!local) { - return PROCESS_NONE; + return SC_PROCESS_NONE; } #endif const char *const adb_cmd[] = {"install", "-r", local}; - process_t proc = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd)); + sc_pid pid = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd)); #ifdef __WINDOWS__ free((void *) local); #endif - return proc; + return pid; } static ssize_t adb_execute_for_output(const char *serial, const char *const adb_cmd[], size_t adb_cmd_len, char *buf, size_t buf_len, const char *name) { - pipe_t pipe_stdout; - process_t proc = adb_execute_redirect(serial, adb_cmd, adb_cmd_len, NULL, - &pipe_stdout, NULL); + sc_pipe pout; + sc_pid pid = adb_execute_p(serial, adb_cmd, adb_cmd_len, NULL, &pout, NULL); - ssize_t r = read_pipe_all(pipe_stdout, buf, buf_len); - close_pipe(pipe_stdout); + ssize_t r = sc_pipe_read_all(pout, buf, buf_len); + sc_pipe_close(pout); - if (!process_check_success(proc, name, true)) { + if (!sc_process_check_success(pid, name, true)) { return -1; } diff --git a/app/src/adb.h b/app/src/adb.h index 34182fd3..085b3e6b 100644 --- a/app/src/adb.h +++ b/app/src/adb.h @@ -8,32 +8,31 @@ #include "util/process.h" -process_t +sc_pid adb_execute(const char *serial, const char *const adb_cmd[], size_t len); -process_t -adb_execute_redirect(const char *serial, const char *const adb_cmd[], - size_t len, pipe_t *pipe_stdin, pipe_t *pipe_stdout, - pipe_t *pipe_stderr); +sc_pid +adb_execute_p(const char *serial, const char *const adb_cmd[], + size_t len, sc_pipe *pin, sc_pipe *pout, sc_pipe *perr); -process_t +sc_pid adb_forward(const char *serial, uint16_t local_port, const char *device_socket_name); -process_t +sc_pid adb_forward_remove(const char *serial, uint16_t local_port); -process_t +sc_pid adb_reverse(const char *serial, const char *device_socket_name, uint16_t local_port); -process_t +sc_pid adb_reverse_remove(const char *serial, const char *device_socket_name); -process_t +sc_pid adb_push(const char *serial, const char *local, const char *remote); -process_t +sc_pid adb_install(const char *serial, const char *local); // Return the result of "adb get-serialno". diff --git a/app/src/file_handler.c b/app/src/file_handler.c index 27fe6fa3..fe0ab857 100644 --- a/app/src/file_handler.c +++ b/app/src/file_handler.c @@ -46,7 +46,7 @@ file_handler_init(struct file_handler *file_handler, const char *serial, file_handler->initialized = false; file_handler->stopped = false; - file_handler->current_process = PROCESS_NONE; + file_handler->current_process = SC_PROCESS_NONE; file_handler->push_target = push_target ? push_target : DEFAULT_PUSH_TARGET; @@ -65,12 +65,12 @@ file_handler_destroy(struct file_handler *file_handler) { } } -static process_t +static sc_pid install_apk(const char *serial, const char *file) { return adb_install(serial, file); } -static process_t +static sc_pid push_file(const char *serial, const char *file, const char *push_target) { return adb_push(serial, file, push_target); } @@ -109,7 +109,7 @@ run_file_handler(void *data) { for (;;) { sc_mutex_lock(&file_handler->mutex); - file_handler->current_process = PROCESS_NONE; + file_handler->current_process = SC_PROCESS_NONE; while (!file_handler->stopped && cbuf_is_empty(&file_handler->queue)) { sc_cond_wait(&file_handler->event_cond, &file_handler->mutex); } @@ -123,26 +123,26 @@ run_file_handler(void *data) { assert(non_empty); (void) non_empty; - process_t process; + sc_pid pid; if (req.action == ACTION_INSTALL_APK) { LOGI("Installing %s...", req.file); - process = install_apk(file_handler->serial, req.file); + pid = install_apk(file_handler->serial, req.file); } else { LOGI("Pushing %s...", req.file); - process = push_file(file_handler->serial, req.file, - file_handler->push_target); + pid = push_file(file_handler->serial, req.file, + file_handler->push_target); } - file_handler->current_process = process; + file_handler->current_process = pid; sc_mutex_unlock(&file_handler->mutex); if (req.action == ACTION_INSTALL_APK) { - if (process_check_success(process, "adb install", false)) { + if (sc_process_check_success(pid, "adb install", false)) { LOGI("%s successfully installed", req.file); } else { LOGE("Failed to install %s", req.file); } } else { - if (process_check_success(process, "adb push", false)) { + if (sc_process_check_success(pid, "adb push", false)) { LOGI("%s successfully pushed to %s", req.file, file_handler->push_target); } else { @@ -152,11 +152,11 @@ run_file_handler(void *data) { } sc_mutex_lock(&file_handler->mutex); - // Close the process (it is necessary already terminated) + // Close the process (it is necessarily already terminated) // Execute this call with mutex locked to avoid race conditions with // file_handler_stop() - process_close(file_handler->current_process); - file_handler->current_process = PROCESS_NONE; + sc_process_close(file_handler->current_process); + file_handler->current_process = SC_PROCESS_NONE; sc_mutex_unlock(&file_handler->mutex); file_handler_request_destroy(&req); @@ -183,8 +183,8 @@ file_handler_stop(struct file_handler *file_handler) { sc_mutex_lock(&file_handler->mutex); file_handler->stopped = true; sc_cond_signal(&file_handler->event_cond); - if (file_handler->current_process != PROCESS_NONE) { - if (!process_terminate(file_handler->current_process)) { + if (file_handler->current_process != SC_PROCESS_NONE) { + if (!sc_process_terminate(file_handler->current_process)) { LOGW("Could not terminate push/install process"); } } diff --git a/app/src/file_handler.h b/app/src/file_handler.h index fe1d1804..e2067533 100644 --- a/app/src/file_handler.h +++ b/app/src/file_handler.h @@ -29,7 +29,7 @@ struct file_handler { sc_cond event_cond; bool stopped; bool initialized; - process_t current_process; + sc_pid current_process; struct file_handler_request_queue queue; }; diff --git a/app/src/server.c b/app/src/server.c index 25d786c4..326d0e79 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -73,33 +73,33 @@ push_server(const char *serial) { free(server_path); return false; } - process_t process = adb_push(serial, server_path, DEVICE_SERVER_PATH); + sc_pid pid = adb_push(serial, server_path, DEVICE_SERVER_PATH); free(server_path); - return process_check_success(process, "adb push", true); + return sc_process_check_success(pid, "adb push", true); } static bool enable_tunnel_reverse(const char *serial, uint16_t local_port) { - process_t process = adb_reverse(serial, SOCKET_NAME, local_port); - return process_check_success(process, "adb reverse", true); + sc_pid pid = adb_reverse(serial, SOCKET_NAME, local_port); + return sc_process_check_success(pid, "adb reverse", true); } static bool disable_tunnel_reverse(const char *serial) { - process_t process = adb_reverse_remove(serial, SOCKET_NAME); - return process_check_success(process, "adb reverse --remove", true); + sc_pid pid = adb_reverse_remove(serial, SOCKET_NAME); + return sc_process_check_success(pid, "adb reverse --remove", true); } static bool enable_tunnel_forward(const char *serial, uint16_t local_port) { - process_t process = adb_forward(serial, local_port, SOCKET_NAME); - return process_check_success(process, "adb forward", true); + sc_pid pid = adb_forward(serial, local_port, SOCKET_NAME); + return sc_process_check_success(pid, "adb forward", true); } static bool disable_tunnel_forward(const char *serial, uint16_t local_port) { - process_t process = adb_forward_remove(serial, local_port); - return process_check_success(process, "adb forward --remove", true); + sc_pid pid = adb_forward_remove(serial, local_port); + return sc_process_check_success(pid, "adb forward --remove", true); } static bool @@ -228,7 +228,7 @@ log_level_to_server_string(enum sc_log_level level) { } } -static process_t +static sc_pid execute_server(struct server *server, const struct server_params *params) { char max_size_string[6]; char bit_rate_string[11]; @@ -327,7 +327,7 @@ connect_to_server(uint16_t port, uint32_t attempts, uint32_t delay) { bool server_init(struct server *server) { server->serial = NULL; - server->process = PROCESS_NONE; + server->process = SC_PROCESS_NONE; bool ok = sc_mutex_init(&server->mutex); if (!ok) { @@ -357,7 +357,7 @@ server_init(struct server *server) { static int run_wait_server(void *data) { struct server *server = data; - process_wait(server->process, false); // ignore exit code + sc_process_wait(server->process, false); // ignore exit code sc_mutex_lock(&server->mutex); server->process_terminated = true; @@ -396,7 +396,7 @@ server_start(struct server *server, const struct server_params *params) { // server will connect to our server socket server->process = execute_server(server, params); - if (server->process == PROCESS_NONE) { + if (server->process == SC_PROCESS_NONE) { goto error; } @@ -409,8 +409,8 @@ server_start(struct server *server, const struct server_params *params) { bool ok = sc_thread_create(&server->wait_server_thread, run_wait_server, "wait-server", server); if (!ok) { - process_terminate(server->process); - process_wait(server->process, true); // ignore exit code + sc_process_terminate(server->process); + sc_process_wait(server->process, true); // ignore exit code goto error; } @@ -508,7 +508,7 @@ server_stop(struct server *server) { } } - assert(server->process != PROCESS_NONE); + assert(server->process != SC_PROCESS_NONE); if (server->tunnel_enabled) { // ignore failure @@ -533,11 +533,11 @@ server_stop(struct server *server) { // The process is terminated, but not reaped (closed) yet, so its PID // is still valid. LOGW("Killing the server..."); - process_terminate(server->process); + sc_process_terminate(server->process); } sc_thread_join(&server->wait_server_thread, NULL); - process_close(server->process); + sc_process_close(server->process); } void diff --git a/app/src/server.h b/app/src/server.h index 242b0525..18721cf7 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -22,7 +22,7 @@ struct server_info { struct server { char *serial; - process_t process; + sc_pid process; sc_thread wait_server_thread; sc_mutex mutex; diff --git a/app/src/sys/unix/process.c b/app/src/sys/unix/process.c index ef5d9d79..5f4a9890 100644 --- a/app/src/sys/unix/process.c +++ b/app/src/sys/unix/process.c @@ -10,9 +10,9 @@ #include "util/log.h" -enum process_result -process_execute_redirect(const char *const argv[], pid_t *pid, int *pipe_stdin, - int *pipe_stdout, int *pipe_stderr) { +enum sc_process_result +sc_process_execute_p(const char *const argv[], sc_pid *pid, + int *pin, int *pout, int *perr) { int in[2]; int out[2]; int err[2]; @@ -20,44 +20,44 @@ process_execute_redirect(const char *const argv[], pid_t *pid, int *pipe_stdin, if (pipe(internal) == -1) { perror("pipe"); - return PROCESS_ERROR_GENERIC; + return SC_PROCESS_ERROR_GENERIC; } - if (pipe_stdin) { + if (pin) { if (pipe(in) == -1) { perror("pipe"); close(internal[0]); close(internal[1]); - return PROCESS_ERROR_GENERIC; + return SC_PROCESS_ERROR_GENERIC; } } - if (pipe_stdout) { + if (pout) { if (pipe(out) == -1) { perror("pipe"); // clean up - if (pipe_stdin) { + if (pin) { close(in[0]); close(in[1]); } close(internal[0]); close(internal[1]); - return PROCESS_ERROR_GENERIC; + return SC_PROCESS_ERROR_GENERIC; } } - if (pipe_stderr) { + if (perr) { if (pipe(err) == -1) { perror("pipe"); // clean up - if (pipe_stdout) { + if (pout) { close(out[0]); close(out[1]); } - if (pipe_stdin) { + if (pin) { close(in[0]); close(in[1]); } close(internal[0]); close(internal[1]); - return PROCESS_ERROR_GENERIC; + return SC_PROCESS_ERROR_GENERIC; } } @@ -65,39 +65,39 @@ process_execute_redirect(const char *const argv[], pid_t *pid, int *pipe_stdin, if (*pid == -1) { perror("fork"); // clean up - if (pipe_stderr) { + if (perr) { close(err[0]); close(err[1]); } - if (pipe_stdout) { + if (pout) { close(out[0]); close(out[1]); } - if (pipe_stdin) { + if (pin) { close(in[0]); close(in[1]); } close(internal[0]); close(internal[1]); - return PROCESS_ERROR_GENERIC; + return SC_PROCESS_ERROR_GENERIC; } if (*pid == 0) { - if (pipe_stdin) { + if (pin) { if (in[0] != STDIN_FILENO) { dup2(in[0], STDIN_FILENO); close(in[0]); } close(in[1]); } - if (pipe_stdout) { + if (pout) { if (out[1] != STDOUT_FILENO) { dup2(out[1], STDOUT_FILENO); close(out[1]); } close(out[0]); } - if (pipe_stderr) { + if (perr) { if (err[1] != STDERR_FILENO) { dup2(err[1], STDERR_FILENO); close(err[1]); @@ -105,15 +105,15 @@ process_execute_redirect(const char *const argv[], pid_t *pid, int *pipe_stdin, close(err[0]); } close(internal[0]); - enum process_result err; + enum sc_process_result err; if (fcntl(internal[1], F_SETFD, FD_CLOEXEC) == 0) { execvp(argv[0], (char *const *) argv); perror("exec"); - err = errno == ENOENT ? PROCESS_ERROR_MISSING_BINARY - : PROCESS_ERROR_GENERIC; + err = errno == ENOENT ? SC_PROCESS_ERROR_MISSING_BINARY + : SC_PROCESS_ERROR_GENERIC; } else { perror("fcntl"); - err = PROCESS_ERROR_GENERIC; + err = SC_PROCESS_ERROR_GENERIC; } // send err to the parent if (write(internal[1], &err, sizeof(err)) == -1) { @@ -128,25 +128,25 @@ process_execute_redirect(const char *const argv[], pid_t *pid, int *pipe_stdin, close(internal[1]); - enum process_result res = PROCESS_SUCCESS; + enum sc_process_result res = SC_PROCESS_SUCCESS; // wait for EOF or receive err from child if (read(internal[0], &res, sizeof(res)) == -1) { perror("read"); - res = PROCESS_ERROR_GENERIC; + res = SC_PROCESS_ERROR_GENERIC; } close(internal[0]); - if (pipe_stdin) { + if (pin) { close(in[0]); - *pipe_stdin = in[1]; + *pin = in[1]; } - if (pipe_stdout) { - *pipe_stdout = out[0]; + if (pout) { + *pout = out[0]; close(out[1]); } - if (pipe_stderr) { - *pipe_stderr = err[0]; + if (perr) { + *perr = err[0]; close(err[1]); } @@ -154,7 +154,7 @@ process_execute_redirect(const char *const argv[], pid_t *pid, int *pipe_stdin, } bool -process_terminate(pid_t pid) { +sc_process_terminate(pid_t pid) { if (pid <= 0) { LOGC("Requested to kill %d, this is an error. Please report the bug.\n", (int) pid); @@ -163,8 +163,8 @@ process_terminate(pid_t pid) { return kill(pid, SIGKILL) != -1; } -exit_code_t -process_wait(pid_t pid, bool close) { +sc_exit_code +sc_process_wait(pid_t pid, bool close) { int code; int options = WEXITED; if (!close) { @@ -175,7 +175,7 @@ process_wait(pid_t pid, bool close) { int r = waitid(P_PID, pid, &info, options); if (r == -1 || info.si_code != CLD_EXITED) { // could not wait, or exited unexpectedly, probably by a signal - code = NO_EXIT_CODE; + code = SC_EXIT_CODE_NONE; } else { code = info.si_status; } @@ -183,17 +183,17 @@ process_wait(pid_t pid, bool close) { } void -process_close(pid_t pid) { - process_wait(pid, true); // ignore exit code +sc_process_close(pid_t pid) { + sc_process_wait(pid, true); // ignore exit code } ssize_t -read_pipe(int pipe, char *data, size_t len) { +sc_pipe_read(int pipe, char *data, size_t len) { return read(pipe, data, len); } void -close_pipe(int pipe) { +sc_pipe_close(int pipe) { if (close(pipe)) { perror("close pipe"); } diff --git a/app/src/sys/win/process.c b/app/src/sys/win/process.c index 257427f3..971806d6 100644 --- a/app/src/sys/win/process.c +++ b/app/src/sys/win/process.c @@ -15,17 +15,16 @@ build_cmd(char *cmd, size_t len, const char *const argv[]) { // (don't handle escaping nor quotes) size_t ret = xstrjoin(cmd, argv, ' ', len); if (ret >= len) { - LOGE("Command too long (%" PRIsizet " chars)", len - 1); + LOGE("Command too long (%" SC_PRIsizet " chars)", len - 1); return false; } return true; } -enum process_result -process_execute_redirect(const char *const argv[], HANDLE *handle, - HANDLE *pipe_stdin, HANDLE *pipe_stdout, - HANDLE *pipe_stderr) { - enum process_result ret = PROCESS_ERROR_GENERIC; +enum sc_process_result +sc_process_execute_p(const char *const argv[], HANDLE *handle, + HANDLE *pin, HANDLE *pout, HANDLE *perr) { + enum sc_process_result ret = SC_PROCESS_ERROR_GENERIC; SECURITY_ATTRIBUTES sa; sa.nLength = sizeof(SECURITY_ATTRIBUTES); @@ -35,32 +34,32 @@ process_execute_redirect(const char *const argv[], HANDLE *handle, HANDLE stdin_read_handle; HANDLE stdout_write_handle; HANDLE stderr_write_handle; - if (pipe_stdin) { - if (!CreatePipe(&stdin_read_handle, pipe_stdin, &sa, 0)) { + if (pin) { + if (!CreatePipe(&stdin_read_handle, pin, &sa, 0)) { perror("pipe"); - return PROCESS_ERROR_GENERIC; + return SC_PROCESS_ERROR_GENERIC; } - if (!SetHandleInformation(*pipe_stdin, HANDLE_FLAG_INHERIT, 0)) { + if (!SetHandleInformation(*pin, HANDLE_FLAG_INHERIT, 0)) { LOGE("SetHandleInformation stdin failed"); goto error_close_stdin; } } - if (pipe_stdout) { - if (!CreatePipe(pipe_stdout, &stdout_write_handle, &sa, 0)) { + if (pout) { + if (!CreatePipe(pout, &stdout_write_handle, &sa, 0)) { perror("pipe"); goto error_close_stdin; } - if (!SetHandleInformation(*pipe_stdout, HANDLE_FLAG_INHERIT, 0)) { + if (!SetHandleInformation(*pout, HANDLE_FLAG_INHERIT, 0)) { LOGE("SetHandleInformation stdout failed"); goto error_close_stdout; } } - if (pipe_stderr) { - if (!CreatePipe(pipe_stderr, &stderr_write_handle, &sa, 0)) { + if (perr) { + if (!CreatePipe(perr, &stderr_write_handle, &sa, 0)) { perror("pipe"); goto error_close_stdout; } - if (!SetHandleInformation(*pipe_stderr, HANDLE_FLAG_INHERIT, 0)) { + if (!SetHandleInformation(*perr, HANDLE_FLAG_INHERIT, 0)) { LOGE("SetHandleInformation stderr failed"); goto error_close_stderr; } @@ -70,15 +69,15 @@ process_execute_redirect(const char *const argv[], HANDLE *handle, PROCESS_INFORMATION pi; memset(&si, 0, sizeof(si)); si.cb = sizeof(si); - if (pipe_stdin || pipe_stdout || pipe_stderr) { + if (pin || pout || perr) { si.dwFlags = STARTF_USESTDHANDLES; - if (pipe_stdin) { + if (pin) { si.hStdInput = stdin_read_handle; } - if (pipe_stdout) { + if (pout) { si.hStdOutput = stdout_write_handle; } - if (pipe_stderr) { + if (perr) { si.hStdError = stderr_write_handle; } } @@ -102,40 +101,40 @@ process_execute_redirect(const char *const argv[], HANDLE *handle, *handle = NULL; if (GetLastError() == ERROR_FILE_NOT_FOUND) { - ret = PROCESS_ERROR_MISSING_BINARY; + ret = SC_PROCESS_ERROR_MISSING_BINARY; } goto error_close_stderr; } // These handles are used by the child process, close them for this process - if (pipe_stdin) { + if (pin) { CloseHandle(stdin_read_handle); } - if (pipe_stdout) { + if (pout) { CloseHandle(stdout_write_handle); } - if (pipe_stderr) { + if (perr) { CloseHandle(stderr_write_handle); } free(wide); *handle = pi.hProcess; - return PROCESS_SUCCESS; + return SC_PROCESS_SUCCESS; error_close_stderr: - if (pipe_stderr) { - CloseHandle(*pipe_stderr); + if (perr) { + CloseHandle(*perr); CloseHandle(stderr_write_handle); } error_close_stdout: - if (pipe_stdout) { - CloseHandle(*pipe_stdout); + if (pout) { + CloseHandle(*pout); CloseHandle(stdout_write_handle); } error_close_stdin: - if (pipe_stdin) { - CloseHandle(*pipe_stdin); + if (pin) { + CloseHandle(*pin); CloseHandle(stdin_read_handle); } @@ -143,17 +142,17 @@ error_close_stdin: } bool -process_terminate(HANDLE handle) { +sc_process_terminate(HANDLE handle) { return TerminateProcess(handle, 1); } -exit_code_t -process_wait(HANDLE handle, bool close) { +sc_exit_code +sc_process_wait(HANDLE handle, bool close) { DWORD code; if (WaitForSingleObject(handle, INFINITE) != WAIT_OBJECT_0 || !GetExitCodeProcess(handle, &code)) { // could not wait or retrieve the exit code - code = NO_EXIT_CODE; // max value, it's unsigned + code = SC_EXIT_CODE_NONE; } if (close) { CloseHandle(handle); @@ -162,14 +161,14 @@ process_wait(HANDLE handle, bool close) { } void -process_close(HANDLE handle) { +sc_process_close(HANDLE handle) { bool closed = CloseHandle(handle); assert(closed); (void) closed; } ssize_t -read_pipe(HANDLE pipe, char *data, size_t len) { +sc_read_pipe(HANDLE pipe, char *data, size_t len) { DWORD r; if (!ReadFile(pipe, data, len, &r, NULL)) { return -1; @@ -178,7 +177,7 @@ read_pipe(HANDLE pipe, char *data, size_t len) { } void -close_pipe(HANDLE pipe) { +sc_close_pipe(HANDLE pipe) { if (!CloseHandle(pipe)) { LOGW("Cannot close pipe"); } diff --git a/app/src/util/process.c b/app/src/util/process.c index dcb715e6..6743438e 100644 --- a/app/src/util/process.c +++ b/app/src/util/process.c @@ -3,21 +3,22 @@ #include #include "log.h" -enum process_result -process_execute(const char *const argv[], process_t *pid) { - return process_execute_redirect(argv, pid, NULL, NULL, NULL); +enum sc_process_result +sc_process_execute(const char *const argv[], sc_pid *pid) { + return sc_process_execute_p(argv, pid, NULL, NULL, NULL); } bool -process_check_success(process_t proc, const char *name, bool close) { - if (proc == PROCESS_NONE) { +sc_process_check_success(sc_pid pid, const char *name, bool close) { + if (pid == SC_PROCESS_NONE) { LOGE("Could not execute \"%s\"", name); return false; } - exit_code_t exit_code = process_wait(proc, close); + sc_exit_code exit_code = sc_process_wait(pid, close); if (exit_code) { - if (exit_code != NO_EXIT_CODE) { - LOGE("\"%s\" returned with value %" PRIexitcode, name, exit_code); + if (exit_code != SC_EXIT_CODE_NONE) { + LOGE("\"%s\" returned with value %" SC_PRIexitcode, name, + exit_code); } else { LOGE("\"%s\" exited unexpectedly", name); } @@ -27,10 +28,10 @@ process_check_success(process_t proc, const char *name, bool close) { } ssize_t -read_pipe_all(pipe_t pipe, char *data, size_t len) { +sc_pipe_read_all(sc_pipe pipe, char *data, size_t len) { size_t copied = 0; while (len > 0) { - ssize_t r = read_pipe(pipe, data, len); + ssize_t r = sc_pipe_read(pipe, data, len); if (r <= 0) { return copied ? (ssize_t) copied : r; } diff --git a/app/src/util/process.h b/app/src/util/process.h index 08a70657..b4980b43 100644 --- a/app/src/util/process.h +++ b/app/src/util/process.h @@ -10,71 +10,100 @@ // not needed here, but winsock2.h must never be included AFTER windows.h # include # include -# define PRIexitcode "lu" +# define SC_PRIexitcode "lu" // -# define PRIsizet "Iu" -# define PROCESS_NONE NULL -# define NO_EXIT_CODE -1u // max value as unsigned - typedef HANDLE process_t; - typedef DWORD exit_code_t; - typedef HANDLE pipe_t; +# define SC_PRIsizet "Iu" +# define SC_PROCESS_NONE NULL +# define SC_EXIT_CODE_NONE -1u // max value as unsigned + typedef HANDLE sc_pid; + typedef DWORD sc_exit_code; + typedef HANDLE sc_pipe; #else # include -# define PRIsizet "zu" -# define PRIexitcode "d" -# define PROCESS_NONE -1 -# define NO_EXIT_CODE -1 - typedef pid_t process_t; - typedef int exit_code_t; - typedef int pipe_t; +# define SC_PRIsizet "zu" +# define SC_PRIexitcode "d" +# define SC_PROCESS_NONE -1 +# define SC_EXIT_CODE_NONE -1 + typedef pid_t sc_pid; + typedef int sc_exit_code; + typedef int sc_pipe; #endif -enum process_result { - PROCESS_SUCCESS, - PROCESS_ERROR_GENERIC, - PROCESS_ERROR_MISSING_BINARY, +enum sc_process_result { + SC_PROCESS_SUCCESS, + SC_PROCESS_ERROR_GENERIC, + SC_PROCESS_ERROR_MISSING_BINARY, }; -// execute the command and write the result to the output parameter "process" -enum process_result -process_execute(const char *const argv[], process_t *process); +/** + * Execute the command and write the process id to `pid` + */ +enum sc_process_result +sc_process_execute(const char *const argv[], sc_pid *pid); -enum process_result -process_execute_redirect(const char *const argv[], process_t *process, - pipe_t *pipe_stdin, pipe_t *pipe_stdout, - pipe_t *pipe_stderr); +/** + * Execute the command and write the process id to `pid` + * + * If not NULL, provide a pipe for stdin (`pin`), stdout (`pout`) and stderr + * (`perr`). + */ +enum sc_process_result +sc_process_execute_p(const char *const argv[], sc_pid *pid, + sc_pipe *pin, sc_pipe *pout, sc_pipe *perr); -// kill the process +/** + * Kill the process + */ bool -process_terminate(process_t pid); +sc_process_terminate(sc_pid pid); -// wait and close the process (like waitpid()) -// the "close" flag indicates if the process must be "closed" (reaped) -// (passing false is equivalent to enable WNOWAIT in waitid()) -exit_code_t -process_wait(process_t pid, bool close); +/** + * Wait and close the process (similar to waitpid()) + * + * The `close` flag indicates if the process must be _closed_ (reaped) (passing + * false is equivalent to enable WNOWAIT in waitid()). + */ +sc_exit_code +sc_process_wait(sc_pid pid, bool close); -// close the process -// -// Semantically, process_wait(close) = process_wait(noclose) + process_close +/** + * Close (reap) the process + * + * Semantically: + * sc_process_wait(close) = sc_process_wait(noclose) + sc_process_close() + */ void -process_close(process_t pid); +sc_process_close(sc_pid pid); -// convenience function to wait for a successful process execution -// automatically log process errors with the provided process name +/** + * Convenience function to wait for a successful process execution + * + * Automatically log process errors with the provided process name. + */ bool -process_check_success(process_t proc, const char *name, bool close); +sc_process_check_success(sc_pid pid, const char *name, bool close); +/** + * Read from the pipe + * + * Same semantic as read(). + */ ssize_t -read_pipe(pipe_t pipe, char *data, size_t len); +sc_pipe_read(sc_pipe pipe, char *data, size_t len); +/** + * Read exactly `len` chars from a pipe (unless EOF) + */ ssize_t -read_pipe_all(pipe_t pipe, char *data, size_t len); +sc_pipe_read_all(sc_pipe pipe, char *data, size_t len); +/** + * Close the pipe + */ void -close_pipe(pipe_t pipe); +sc_pipe_close(sc_pipe pipe); #endif From 03de9224fc45fdccf772b975a63851284a4579b3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 12 Nov 2021 09:49:37 +0100 Subject: [PATCH 0752/2244] Introduce process observer Add a tool to easily observe process termination. This allows to move this complexity out of the server code. --- app/src/server.c | 71 +++++++++++++------------------------- app/src/server.h | 8 ++--- app/src/util/process.c | 78 ++++++++++++++++++++++++++++++++++++++++++ app/src/util/process.h | 67 ++++++++++++++++++++++++++++++++++++ 4 files changed, 171 insertions(+), 53 deletions(-) diff --git a/app/src/server.c b/app/src/server.c index 326d0e79..f108c7cc 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -329,19 +329,6 @@ server_init(struct server *server) { server->serial = NULL; server->process = SC_PROCESS_NONE; - bool ok = sc_mutex_init(&server->mutex); - if (!ok) { - return false; - } - - ok = sc_cond_init(&server->process_terminated_cond); - if (!ok) { - sc_mutex_destroy(&server->mutex); - return false; - } - - server->process_terminated = false; - server->server_socket = SC_INVALID_SOCKET; server->video_socket = SC_INVALID_SOCKET; server->control_socket = SC_INVALID_SOCKET; @@ -354,25 +341,20 @@ server_init(struct server *server) { return true; } -static int -run_wait_server(void *data) { - struct server *server = data; - sc_process_wait(server->process, false); // ignore exit code +static void +server_on_terminated(void *userdata) { + struct server *server = userdata; - sc_mutex_lock(&server->mutex); - server->process_terminated = true; - sc_cond_signal(&server->process_terminated_cond); - sc_mutex_unlock(&server->mutex); - - // no need for synchronization, server_socket is initialized before this - // thread was created + // No need for synchronization, server_socket is initialized before the + // observer thread is created. if (server->server_socket != SC_INVALID_SOCKET) { - // Unblock any accept() + // If the server process dies before connecting to the server socket, + // then the client will be stuck forever on accept(). To avoid the + // problem, wake up the accept() call when the server dies. net_interrupt(server->server_socket); } LOGD("Server terminated"); - return 0; } bool @@ -400,14 +382,11 @@ server_start(struct server *server, const struct server_params *params) { goto error; } - // 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. - bool ok = sc_thread_create(&server->wait_server_thread, run_wait_server, - "wait-server", server); + static const struct sc_process_listener listener = { + .on_terminated = server_on_terminated, + }; + bool ok = sc_process_observer_init(&server->observer, server->process, + &listener, server); if (!ok) { sc_process_terminate(server->process); sc_process_wait(server->process, true); // ignore exit code @@ -516,27 +495,25 @@ server_stop(struct server *server) { } // Give some delay for the server to terminate properly - sc_mutex_lock(&server->mutex); - bool signaled = false; - if (!server->process_terminated) { #define WATCHDOG_DELAY SC_TICK_FROM_SEC(1) - signaled = sc_cond_timedwait(&server->process_terminated_cond, - &server->mutex, - sc_tick_now() + WATCHDOG_DELAY); - } - sc_mutex_unlock(&server->mutex); + sc_tick deadline = sc_tick_now() + WATCHDOG_DELAY; + bool terminated = + sc_process_observer_timedwait(&server->observer, deadline); // After this delay, kill the server if it's not dead already. // On some devices, closing the sockets is not sufficient to wake up the // blocking calls while the device is asleep. - if (!signaled) { - // The process is terminated, but not reaped (closed) yet, so its PID - // is still valid. + if (!terminated) { + // The process may have terminated since the check, but it is not + // reaped (closed) yet, so its PID is still valid, and it is ok to call + // sc_process_terminate() even in that case. LOGW("Killing the server..."); sc_process_terminate(server->process); } - sc_thread_join(&server->wait_server_thread, NULL); + sc_process_observer_join(&server->observer); + sc_process_observer_destroy(&server->observer); + sc_process_close(server->process); } @@ -558,6 +535,4 @@ server_destroy(struct server *server) { } } free(server->serial); - sc_cond_destroy(&server->process_terminated_cond); - sc_mutex_destroy(&server->mutex); } diff --git a/app/src/server.h b/app/src/server.h index 18721cf7..c06b3562 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -22,12 +22,10 @@ struct server_info { struct server { char *serial; - sc_pid process; - sc_thread wait_server_thread; - sc_mutex mutex; - sc_cond process_terminated_cond; - bool process_terminated; + sc_pid process; + // alive only between start() and stop() + struct sc_process_observer observer; sc_socket server_socket; // only used if !tunnel_forward sc_socket video_socket; diff --git a/app/src/util/process.c b/app/src/util/process.c index 6743438e..28f51edd 100644 --- a/app/src/util/process.c +++ b/app/src/util/process.c @@ -1,5 +1,6 @@ #include "process.h" +#include #include #include "log.h" @@ -41,3 +42,80 @@ sc_pipe_read_all(sc_pipe pipe, char *data, size_t len) { } return copied; } + +static int +run_observer(void *data) { + struct sc_process_observer *observer = data; + sc_process_wait(observer->pid, false); // ignore exit code + + sc_mutex_lock(&observer->mutex); + observer->terminated = true; + sc_cond_signal(&observer->cond_terminated); + sc_mutex_unlock(&observer->mutex); + + if (observer->listener) { + observer->listener->on_terminated(observer->listener_userdata); + } + + return 0; +} + +bool +sc_process_observer_init(struct sc_process_observer *observer, sc_pid pid, + const struct sc_process_listener *listener, + void *listener_userdata) { + // Either no listener, or on_terminated() is defined + assert(!listener || listener->on_terminated); + + bool ok = sc_mutex_init(&observer->mutex); + if (!ok) { + return false; + } + + ok = sc_cond_init(&observer->cond_terminated); + if (!ok) { + sc_mutex_destroy(&observer->mutex); + return false; + } + + observer->pid = pid; + observer->listener = listener; + observer->listener_userdata = listener_userdata; + observer->terminated = false; + + ok = sc_thread_create(&observer->thread, run_observer, "process_observer", + observer); + if (!ok) { + sc_cond_destroy(&observer->cond_terminated); + sc_mutex_destroy(&observer->mutex); + return false; + } + + return true; +} + +bool +sc_process_observer_timedwait(struct sc_process_observer *observer, + sc_tick deadline) { + sc_mutex_lock(&observer->mutex); + bool timed_out = false; + while (!observer->terminated && !timed_out) { + timed_out = !sc_cond_timedwait(&observer->cond_terminated, + &observer->mutex, deadline); + } + bool terminated = observer->terminated; + sc_mutex_unlock(&observer->mutex); + + return terminated; +} + +void +sc_process_observer_join(struct sc_process_observer *observer) { + sc_thread_join(&observer->thread, NULL); +} + +void +sc_process_observer_destroy(struct sc_process_observer *observer) { + sc_cond_destroy(&observer->cond_terminated); + sc_mutex_destroy(&observer->mutex); +} diff --git a/app/src/util/process.h b/app/src/util/process.h index b4980b43..7964be5c 100644 --- a/app/src/util/process.h +++ b/app/src/util/process.h @@ -4,6 +4,7 @@ #include "common.h" #include +#include "util/thread.h" #ifdef _WIN32 @@ -32,6 +33,34 @@ #endif +struct sc_process_listener { + void (*on_terminated)(void *userdata); +}; + +/** + * Tool to observe process termination + * + * To keep things simple and multiplatform, it runs a separate thread to wait + * for process termination (without closing the process to avoid race + * conditions). + * + * It allows a caller to block until the process is terminated (with a + * timeout), and to be notified asynchronously from the observer thread. + * + * The process is not owned by the observer (the observer will never close it). + */ +struct sc_process_observer { + sc_pid pid; + + sc_mutex mutex; + sc_cond cond_terminated; + bool terminated; + + sc_thread thread; + const struct sc_process_listener *listener; + void *listener_userdata; +}; + enum sc_process_result { SC_PROCESS_SUCCESS, SC_PROCESS_ERROR_GENERIC, @@ -106,4 +135,42 @@ sc_pipe_read_all(sc_pipe pipe, char *data, size_t len); void sc_pipe_close(sc_pipe pipe); +/** + * Start observing process + * + * The listener is optional. If set, its callback will be called from the + * observer thread once the process is terminated. + */ +bool +sc_process_observer_init(struct sc_process_observer *observer, sc_pid pid, + const struct sc_process_listener *listener, + void *listener_userdata); + +/** + * Wait for process termination until a deadline + * + * Return true if the process is already terminated. Return false if the + * process terminatation has not been detected yet (however, it may have + * terminated in the meantime). + * + * To wait without timeout/deadline, just use sc_process_wait() instead. + */ +bool +sc_process_observer_timedwait(struct sc_process_observer *observer, + sc_tick deadline); + +/** + * Join the observer thread + */ +void +sc_process_observer_join(struct sc_process_observer *observer); + +/** + * Destroy the observer + * + * This does not close the associated process. + */ +void +sc_process_observer_destroy(struct sc_process_observer *observer); + #endif From e69356c5506667135a6479758b594863c8ef15f0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 1 Nov 2021 11:08:01 +0100 Subject: [PATCH 0753/2244] Initialize tunnel_enabled flag internally Set the flag from the inner methods, to avoid bypassing the assignment by mistake (on error for example). --- app/src/server.c | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/app/src/server.c b/app/src/server.c index f108c7cc..e1cf5165 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -104,10 +104,16 @@ disable_tunnel_forward(const char *serial, uint16_t 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); + assert(server->tunnel_enabled); + + bool ok = server->tunnel_forward + ? disable_tunnel_forward(server->serial, server->local_port) + : disable_tunnel_reverse(server->serial); + + // Consider tunnel disabled even if the command failed + server->tunnel_enabled = false; + + return ok; } static sc_socket @@ -136,6 +142,7 @@ enable_tunnel_reverse_any_port(struct server *server, if (server->server_socket != SC_INVALID_SOCKET) { // success server->local_port = port; + server->tunnel_enabled = true; return true; } @@ -171,6 +178,7 @@ enable_tunnel_forward_any_port(struct server *server, if (enable_tunnel_forward(server->serial, port)) { // success server->local_port = port; + server->tunnel_enabled = true; return true; } @@ -393,8 +401,6 @@ server_start(struct server *server, const struct server_params *params) { goto error; } - server->tunnel_enabled = true; - return true; error: @@ -463,7 +469,6 @@ server_connect_to(struct server *server, struct server_info *info) { // we don't need the adb tunnel anymore disable_tunnel(server); // ignore failure - server->tunnel_enabled = false; // The sockets will be closed on stop if device_read_info() fails return device_read_info(server->video_socket, info); From ed19901db1d24d4791f5836a613505083a0d94d3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 1 Nov 2021 11:44:58 +0100 Subject: [PATCH 0754/2244] Set video and control sockets only on success Store the video and control sockets only if server_connect_to() returns successfully. --- app/src/server.c | 64 ++++++++++++++++++++++++++++++++++-------------- 1 file changed, 46 insertions(+), 18 deletions(-) diff --git a/app/src/server.c b/app/src/server.c index e1cf5165..473bbeed 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -138,9 +138,10 @@ enable_tunnel_reverse_any_port(struct server *server, // 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(port); - if (server->server_socket != SC_INVALID_SOCKET) { + sc_socket server_socket = listen_on_port(port); + if (server_socket != SC_INVALID_SOCKET) { // success + server->server_socket = server_socket; server->local_port = port; server->tunnel_enabled = true; return true; @@ -432,16 +433,19 @@ device_read_info(sc_socket device_socket, struct server_info *info) { bool server_connect_to(struct server *server, struct server_info *info) { + assert(server->tunnel_enabled); + + sc_socket video_socket = SC_INVALID_SOCKET; + sc_socket control_socket = SC_INVALID_SOCKET; if (!server->tunnel_forward) { - server->video_socket = net_accept(server->server_socket); - if (server->video_socket == SC_INVALID_SOCKET) { - return false; + video_socket = net_accept(server->server_socket); + if (video_socket == SC_INVALID_SOCKET) { + goto fail; } - server->control_socket = net_accept(server->server_socket); - if (server->control_socket == SC_INVALID_SOCKET) { - // the video_socket will be cleaned up on destroy - return false; + control_socket = net_accept(server->server_socket); + if (control_socket == SC_INVALID_SOCKET) { + goto fail; } // we don't need the server socket anymore @@ -453,17 +457,15 @@ server_connect_to(struct server *server, struct server_info *info) { } else { uint32_t attempts = 100; uint32_t delay = 100; // ms - server->video_socket = - connect_to_server(server->local_port, attempts, delay); - if (server->video_socket == SC_INVALID_SOCKET) { - return false; + video_socket = connect_to_server(server->local_port, attempts, delay); + if (video_socket == SC_INVALID_SOCKET) { + goto fail; } // we know that the device is listening, we don't need several attempts - server->control_socket = - net_connect(IPV4_LOCALHOST, server->local_port); - if (server->control_socket == SC_INVALID_SOCKET) { - return false; + control_socket = net_connect(IPV4_LOCALHOST, server->local_port); + if (control_socket == SC_INVALID_SOCKET) { + goto fail; } } @@ -471,7 +473,33 @@ server_connect_to(struct server *server, struct server_info *info) { disable_tunnel(server); // ignore failure // The sockets will be closed on stop if device_read_info() fails - return device_read_info(server->video_socket, info); + bool ok = device_read_info(video_socket, info); + if (!ok) { + goto fail; + } + + assert(video_socket != SC_INVALID_SOCKET); + assert(control_socket != SC_INVALID_SOCKET); + + server->video_socket = video_socket; + server->control_socket = control_socket; + + return true; + +fail: + if (video_socket != SC_INVALID_SOCKET) { + if (!net_close(video_socket)) { + LOGW("Could not close video socket"); + } + } + + if (control_socket != SC_INVALID_SOCKET) { + if (!net_close(control_socket)) { + LOGW("Could not close control socket"); + } + } + + return false; } void From 0bfa75a48b49d70bf0a50d48d5d1d63dd1d126ac Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 31 Oct 2021 22:01:57 +0100 Subject: [PATCH 0755/2244] Split socket creation and connect/listen This will allow to assign the socket to a variable before connecting or listening, so that it can be interrupted from another thread. --- app/src/server.c | 57 +++++++++++++++++++++++++++++----------------- app/src/util/net.c | 33 ++++++++++++--------------- app/src/util/net.h | 9 +++++--- 3 files changed, 57 insertions(+), 42 deletions(-) diff --git a/app/src/server.c b/app/src/server.c index 473bbeed..abf9170e 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -116,10 +116,10 @@ disable_tunnel(struct server *server) { return ok; } -static sc_socket -listen_on_port(uint16_t port) { +static bool +listen_on_port(sc_socket socket, uint16_t port) { #define IPV4_LOCALHOST 0x7F000001 - return net_listen(IPV4_LOCALHOST, port, 1); + return net_listen(socket, IPV4_LOCALHOST, port, 1); } static bool @@ -138,13 +138,18 @@ enable_tunnel_reverse_any_port(struct server *server, // 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. - sc_socket server_socket = listen_on_port(port); + sc_socket server_socket = net_socket(); if (server_socket != SC_INVALID_SOCKET) { - // success - server->server_socket = server_socket; - server->local_port = port; - server->tunnel_enabled = true; - return true; + bool ok = listen_on_port(server_socket, port); + if (ok) { + // success + server->server_socket = server_socket; + server->local_port = port; + server->tunnel_enabled = true; + return true; + } + + net_close(server_socket); } // failure, disable tunnel and try another port @@ -299,11 +304,11 @@ execute_server(struct server *server, const struct server_params *params) { return adb_execute(server->serial, cmd, ARRAY_LEN(cmd)); } -static sc_socket -connect_and_read_byte(uint16_t port) { - sc_socket socket = net_connect(IPV4_LOCALHOST, port); - if (socket == SC_INVALID_SOCKET) { - return SC_INVALID_SOCKET; +static bool +connect_and_read_byte(sc_socket socket, uint16_t port) { + bool ok = net_connect(socket, IPV4_LOCALHOST, port); + if (!ok) { + return false; } char byte; @@ -311,20 +316,25 @@ connect_and_read_byte(uint16_t port) { // is not listening, so read one byte to detect a working connection if (net_recv(socket, &byte, 1) != 1) { // the server is not listening yet behind the adb tunnel - net_close(socket); - return SC_INVALID_SOCKET; + return false; } - return socket; + + return true; } static sc_socket connect_to_server(uint16_t port, uint32_t attempts, uint32_t delay) { do { LOGD("Remaining connection attempts: %d", (int) attempts); - sc_socket socket = connect_and_read_byte(port); + sc_socket socket = net_socket(); if (socket != SC_INVALID_SOCKET) { - // it worked! - return socket; + bool ok = connect_and_read_byte(socket, port); + if (ok) { + // it worked! + return socket; + } + + net_close(socket); } if (attempts) { SDL_Delay(delay); @@ -463,10 +473,15 @@ server_connect_to(struct server *server, struct server_info *info) { } // we know that the device is listening, we don't need several attempts - control_socket = net_connect(IPV4_LOCALHOST, server->local_port); + control_socket = net_socket(); if (control_socket == SC_INVALID_SOCKET) { goto fail; } + bool ok = net_connect(control_socket, IPV4_LOCALHOST, + server->local_port); + if (!ok) { + goto fail; + } } // we don't need the adb tunnel anymore diff --git a/app/src/util/net.c b/app/src/util/net.c index cfc60433..6bfe7a52 100644 --- a/app/src/util/net.c +++ b/app/src/util/net.c @@ -94,13 +94,18 @@ net_perror(const char *s) { } sc_socket -net_connect(uint32_t addr, uint16_t port) { +net_socket(void) { sc_raw_socket raw_sock = socket(AF_INET, SOCK_STREAM, 0); sc_socket sock = wrap(raw_sock); if (sock == SC_INVALID_SOCKET) { net_perror("socket"); - return SC_INVALID_SOCKET; } + return sock; +} + +bool +net_connect(sc_socket socket, uint32_t addr, uint16_t port) { + sc_raw_socket raw_sock = unwrap(socket); SOCKADDR_IN sin; sin.sin_family = AF_INET; @@ -109,21 +114,15 @@ net_connect(uint32_t addr, uint16_t port) { if (connect(raw_sock, (SOCKADDR *) &sin, sizeof(sin)) == SOCKET_ERROR) { net_perror("connect"); - net_close(sock); - return SC_INVALID_SOCKET; + return false; } - return sock; + return true; } -sc_socket -net_listen(uint32_t addr, uint16_t port, int backlog) { - sc_raw_socket raw_sock = socket(AF_INET, SOCK_STREAM, 0); - sc_socket sock = wrap(raw_sock); - if (sock == SC_INVALID_SOCKET) { - net_perror("socket"); - return SC_INVALID_SOCKET; - } +bool +net_listen(sc_socket socket, uint32_t addr, uint16_t port, int backlog) { + sc_raw_socket raw_sock = unwrap(socket); int reuse = 1; if (setsockopt(raw_sock, SOL_SOCKET, SO_REUSEADDR, (const void *) &reuse, @@ -138,17 +137,15 @@ net_listen(uint32_t addr, uint16_t port, int backlog) { if (bind(raw_sock, (SOCKADDR *) &sin, sizeof(sin)) == SOCKET_ERROR) { net_perror("bind"); - net_close(sock); - return SC_INVALID_SOCKET; + return false; } if (listen(raw_sock, backlog) == SOCKET_ERROR) { net_perror("listen"); - net_close(sock); - return SC_INVALID_SOCKET; + return false; } - return sock; + return true; } sc_socket diff --git a/app/src/util/net.h b/app/src/util/net.h index c742c00e..9d675a2d 100644 --- a/app/src/util/net.h +++ b/app/src/util/net.h @@ -31,10 +31,13 @@ void net_cleanup(void); sc_socket -net_connect(uint32_t addr, uint16_t port); +net_socket(void); -sc_socket -net_listen(uint32_t addr, uint16_t port, int backlog); +bool +net_connect(sc_socket socket, uint32_t addr, uint16_t port); + +bool +net_listen(sc_socket socket, uint32_t addr, uint16_t port, int backlog); sc_socket net_accept(sc_socket server_socket); From 9fa4d1cd4a7a54d19593aa460eacbd34a665216b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 27 Oct 2021 23:20:47 +0200 Subject: [PATCH 0756/2244] Reorder server and server_params This will allow to define a server_params field in server. --- app/src/server.h | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/app/src/server.h b/app/src/server.h index c06b3562..0506bf10 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -20,21 +20,6 @@ struct server_info { struct sc_size frame_size; }; -struct server { - char *serial; - - sc_pid process; - // alive only between start() and stop() - struct sc_process_observer observer; - - sc_socket server_socket; // only used if !tunnel_forward - sc_socket video_socket; - sc_socket control_socket; - uint16_t local_port; // selected from port_range - bool tunnel_enabled; - bool tunnel_forward; // use "adb forward" instead of "adb reverse" -}; - struct server_params { const char *serial; enum sc_log_level log_level; @@ -54,6 +39,21 @@ struct server_params { bool power_off_on_close; }; +struct server { + char *serial; + + sc_pid process; + // alive only between start() and stop() + struct sc_process_observer observer; + + sc_socket server_socket; // only used if !tunnel_forward + sc_socket video_socket; + sc_socket control_socket; + uint16_t local_port; // selected from port_range + bool tunnel_enabled; + bool tunnel_forward; // use "adb forward" instead of "adb reverse" +}; + // init default values bool server_init(struct server *server); From 882e4cff5f2e1d22748b79d3371e47e4d0874ca3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 27 Oct 2021 23:40:52 +0200 Subject: [PATCH 0757/2244] Copy server params This is a preliminary step necessary to move the server to a separate thread. --- app/src/scrcpy.c | 13 +++++---- app/src/server.c | 76 ++++++++++++++++++++++++++++++++++++++---------- app/src/server.h | 9 +++--- 3 files changed, 72 insertions(+), 26 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index c8adb5a1..43ed428e 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -275,10 +275,6 @@ scrcpy(struct scrcpy_options *options) { atexit(SDL_Quit); - if (!server_init(&s->server)) { - return false; - } - bool ret = false; bool server_started = false; @@ -313,7 +309,12 @@ scrcpy(struct scrcpy_options *options) { .force_adb_forward = options->force_adb_forward, .power_off_on_close = options->power_off_on_close, }; - if (!server_start(&s->server, ¶ms)) { + + if (!server_init(&s->server, ¶ms)) { + return false; + } + + if (!server_start(&s->server)) { goto end; } @@ -338,7 +339,7 @@ scrcpy(struct scrcpy_options *options) { } if (options->display && options->control) { - if (!file_handler_init(&s->file_handler, s->server.serial, + if (!file_handler_init(&s->file_handler, options->serial, options->push_target)) { goto end; } diff --git a/app/src/server.c b/app/src/server.c index abf9170e..9e6ef3f6 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -62,6 +62,44 @@ get_server_path(void) { return server_path; } +static void +server_params_destroy(struct server_params *params) { + // The server stores a copy of the params provided by the user + free((char *) params->serial); + free((char *) params->crop); + free((char *) params->codec_options); + free((char *) params->encoder_name); +} + +static bool +server_params_copy(struct server_params *dst, const struct server_params *src) { + *dst = *src; + + // The params reference user-allocated memory, so we must copy them to + // handle them from another thread + +#define COPY(FIELD) \ + dst->FIELD = NULL; \ + if (src->FIELD) { \ + dst->FIELD = strdup(src->FIELD); \ + if (!dst->FIELD) { \ + goto error; \ + } \ + } + + COPY(serial); + COPY(crop); + COPY(codec_options); + COPY(encoder_name); +#undef COPY + + return true; + +error: + server_params_destroy(dst); + return false; +} + static bool push_server(const char *serial) { char *server_path = get_server_path(); @@ -106,9 +144,10 @@ static bool disable_tunnel(struct server *server) { assert(server->tunnel_enabled); + const char *serial = server->params.serial; bool ok = server->tunnel_forward - ? disable_tunnel_forward(server->serial, server->local_port) - : disable_tunnel_reverse(server->serial); + ? disable_tunnel_forward(serial, server->local_port) + : disable_tunnel_reverse(serial); // Consider tunnel disabled even if the command failed server->tunnel_enabled = false; @@ -125,9 +164,10 @@ listen_on_port(sc_socket socket, uint16_t port) { static bool enable_tunnel_reverse_any_port(struct server *server, struct sc_port_range port_range) { + const char *serial = server->params.serial; uint16_t port = port_range.first; for (;;) { - if (!enable_tunnel_reverse(server->serial, port)) { + if (!enable_tunnel_reverse(serial, port)) { // the command itself failed, it will fail on any port return false; } @@ -153,7 +193,7 @@ enable_tunnel_reverse_any_port(struct server *server, } // failure, disable tunnel and try another port - if (!disable_tunnel_reverse(server->serial)) { + if (!disable_tunnel_reverse(serial)) { LOGW("Could not remove reverse tunnel on port %" PRIu16, port); } @@ -179,9 +219,11 @@ static bool enable_tunnel_forward_any_port(struct server *server, struct sc_port_range port_range) { server->tunnel_forward = true; + + const char *serial = server->params.serial; uint16_t port = port_range.first; for (;;) { - if (enable_tunnel_forward(server->serial, port)) { + if (enable_tunnel_forward(serial, port)) { // success server->local_port = port; server->tunnel_enabled = true; @@ -244,6 +286,8 @@ log_level_to_server_string(enum sc_log_level level) { static sc_pid execute_server(struct server *server, const struct server_params *params) { + const char *serial = server->params.serial; + char max_size_string[6]; char bit_rate_string[11]; char max_fps_string[6]; @@ -301,7 +345,7 @@ execute_server(struct server *server, const struct server_params *params) { // Port: 5005 // Then click on "Debug" #endif - return adb_execute(server->serial, cmd, ARRAY_LEN(cmd)); + return adb_execute(serial, cmd, ARRAY_LEN(cmd)); } static bool @@ -344,8 +388,13 @@ connect_to_server(uint16_t port, uint32_t attempts, uint32_t delay) { } bool -server_init(struct server *server) { - server->serial = NULL; +server_init(struct server *server, const struct server_params *params) { + bool ok = server_params_copy(&server->params, params); + if (!ok) { + LOGE("Could not copy server params"); + return false; + } + server->process = SC_PROCESS_NONE; server->server_socket = SC_INVALID_SOCKET; @@ -377,13 +426,8 @@ server_on_terminated(void *userdata) { } bool -server_start(struct server *server, const struct server_params *params) { - if (params->serial) { - server->serial = strdup(params->serial); - if (!server->serial) { - return false; - } - } +server_start(struct server *server) { + const struct server_params *params = &server->params; if (!push_server(params->serial)) { /* server->serial will be freed on server_destroy() */ @@ -582,5 +626,5 @@ server_destroy(struct server *server) { LOGW("Could not close control socket"); } } - free(server->serial); + server_params_destroy(&server->params); } diff --git a/app/src/server.h b/app/src/server.h index 0506bf10..42082fa9 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -40,7 +40,8 @@ struct server_params { }; struct server { - char *serial; + // The internal allocated strings are copies owned by the server + struct server_params params; sc_pid process; // alive only between start() and stop() @@ -54,13 +55,13 @@ struct server { bool tunnel_forward; // use "adb forward" instead of "adb reverse" }; -// init default values +// init the server with the given params bool -server_init(struct server *server); +server_init(struct server *server, const struct server_params *params); // push, enable tunnel et start the server bool -server_start(struct server *server, const struct server_params *params); +server_start(struct server *server); // block until the communication with the server is established bool From a54dc8212ffb1eec975a27a4d70fea25ed8c3f24 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 12 Nov 2021 18:30:20 +0100 Subject: [PATCH 0758/2244] Reorder server functions This will avoid forward declarations in future commits. --- app/src/server.c | 114 +++++++++++++++++++++++------------------------ 1 file changed, 57 insertions(+), 57 deletions(-) diff --git a/app/src/server.c b/app/src/server.c index 9e6ef3f6..1491052a 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -409,63 +409,6 @@ server_init(struct server *server, const struct server_params *params) { return true; } -static void -server_on_terminated(void *userdata) { - struct server *server = userdata; - - // No need for synchronization, server_socket is initialized before the - // observer thread is created. - if (server->server_socket != SC_INVALID_SOCKET) { - // If the server process dies before connecting to the server socket, - // then the client will be stuck forever on accept(). To avoid the - // problem, wake up the accept() call when the server dies. - net_interrupt(server->server_socket); - } - - LOGD("Server terminated"); -} - -bool -server_start(struct server *server) { - const struct server_params *params = &server->params; - - if (!push_server(params->serial)) { - /* server->serial will be freed on server_destroy() */ - return false; - } - - if (!enable_tunnel_any_port(server, params->port_range, - params->force_adb_forward)) { - return false; - } - - // server will connect to our server socket - server->process = execute_server(server, params); - if (server->process == SC_PROCESS_NONE) { - goto error; - } - - static const struct sc_process_listener listener = { - .on_terminated = server_on_terminated, - }; - bool ok = sc_process_observer_init(&server->observer, server->process, - &listener, server); - if (!ok) { - sc_process_terminate(server->process); - sc_process_wait(server->process, true); // ignore exit code - goto error; - } - - return true; - -error: - // The server socket (if any) will be closed on server_destroy() - - disable_tunnel(server); - - return false; -} - static bool device_read_info(sc_socket device_socket, struct server_info *info) { unsigned char buf[DEVICE_NAME_FIELD_LENGTH + 4]; @@ -561,6 +504,63 @@ fail: return false; } +static void +server_on_terminated(void *userdata) { + struct server *server = userdata; + + // No need for synchronization, server_socket is initialized before the + // observer thread is created. + if (server->server_socket != SC_INVALID_SOCKET) { + // If the server process dies before connecting to the server socket, + // then the client will be stuck forever on accept(). To avoid the + // problem, wake up the accept() call when the server dies. + net_interrupt(server->server_socket); + } + + LOGD("Server terminated"); +} + +bool +server_start(struct server *server) { + const struct server_params *params = &server->params; + + if (!push_server(params->serial)) { + /* server->serial will be freed on server_destroy() */ + return false; + } + + if (!enable_tunnel_any_port(server, params->port_range, + params->force_adb_forward)) { + return false; + } + + // server will connect to our server socket + server->process = execute_server(server, params); + if (server->process == SC_PROCESS_NONE) { + goto error; + } + + static const struct sc_process_listener listener = { + .on_terminated = server_on_terminated, + }; + bool ok = sc_process_observer_init(&server->observer, server->process, + &listener, server); + if (!ok) { + sc_process_terminate(server->process); + sc_process_wait(server->process, true); // ignore exit code + goto error; + } + + return true; + +error: + // The server socket (if any) will be closed on server_destroy() + + disable_tunnel(server); + + return false; +} + void server_stop(struct server *server) { if (server->server_socket != SC_INVALID_SOCKET) { From 5b9c88693ea1b2d19f325ab6ff6c06119781a101 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 31 Oct 2021 14:56:37 +0100 Subject: [PATCH 0759/2244] Wait using a condition variable in server Currently, server_stop() is called from the same thread as server_connect_to(), so interruption may never happen. This is a step to prepare executing the server from a dedicated thread. --- app/src/server.c | 45 +++++++++++++++++++++++++++++++++++++++++---- app/src/server.h | 4 ++++ 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/app/src/server.c b/app/src/server.c index 1491052a..5ed1984c 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -367,7 +367,8 @@ connect_and_read_byte(sc_socket socket, uint16_t port) { } static sc_socket -connect_to_server(uint16_t port, uint32_t attempts, uint32_t delay) { +connect_to_server(struct server *server, uint32_t attempts, sc_tick delay) { + uint16_t port = server->local_port; do { LOGD("Remaining connection attempts: %d", (int) attempts); sc_socket socket = net_socket(); @@ -381,7 +382,20 @@ connect_to_server(uint16_t port, uint32_t attempts, uint32_t delay) { net_close(socket); } if (attempts) { - SDL_Delay(delay); + sc_mutex_lock(&server->mutex); + sc_tick deadline = sc_tick_now() + delay; + bool timed_out = false; + while (!server->stopped && !timed_out) { + timed_out = !sc_cond_timedwait(&server->cond_stopped, + &server->mutex, deadline); + } + bool stopped = server->stopped; + sc_mutex_unlock(&server->mutex); + + if (stopped) { + LOGI("Connection attempt stopped"); + break; + } } } while (--attempts > 0); return SC_INVALID_SOCKET; @@ -395,7 +409,23 @@ server_init(struct server *server, const struct server_params *params) { return false; } + ok = sc_mutex_init(&server->mutex); + if (!ok) { + LOGE("Could not create server mutex"); + server_params_destroy(&server->params); + return false; + } + + ok = sc_cond_init(&server->cond_stopped); + if (!ok) { + LOGE("Could not create server cond_stopped"); + sc_mutex_destroy(&server->mutex); + server_params_destroy(&server->params); + return false; + } + server->process = SC_PROCESS_NONE; + server->stopped = false; server->server_socket = SC_INVALID_SOCKET; server->video_socket = SC_INVALID_SOCKET; @@ -453,8 +483,8 @@ server_connect_to(struct server *server, struct server_info *info) { server->server_socket = SC_INVALID_SOCKET; } else { uint32_t attempts = 100; - uint32_t delay = 100; // ms - video_socket = connect_to_server(server->local_port, attempts, delay); + sc_tick delay = SC_TICK_FROM_MS(100); + video_socket = connect_to_server(server, attempts, delay); if (video_socket == SC_INVALID_SOCKET) { goto fail; } @@ -563,6 +593,11 @@ error: void server_stop(struct server *server) { + sc_mutex_lock(&server->mutex); + server->stopped = true; + sc_cond_signal(&server->cond_stopped); + sc_mutex_unlock(&server->mutex); + if (server->server_socket != SC_INVALID_SOCKET) { if (!net_interrupt(server->server_socket)) { LOGW("Could not interrupt server socket"); @@ -627,4 +662,6 @@ server_destroy(struct server *server) { } } server_params_destroy(&server->params); + sc_cond_destroy(&server->cond_stopped); + sc_mutex_destroy(&server->mutex); } diff --git a/app/src/server.h b/app/src/server.h index 42082fa9..5807c322 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -47,6 +47,10 @@ struct server { // alive only between start() and stop() struct sc_process_observer observer; + sc_mutex mutex; + sc_cond cond_stopped; + bool stopped; + sc_socket server_socket; // only used if !tunnel_forward sc_socket video_socket; sc_socket control_socket; From 04267085441d6fcd05eff7df0118708f7622e237 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 30 Oct 2021 15:33:23 +0200 Subject: [PATCH 0760/2244] Run the server from a dedicated thread Define server callbacks, start the server asynchronously and listen to connection events to initialize scrcpy properly. It will help to simplify the server code, and allows to run the UI event loop while the server is connecting. In particular, this will allow to receive SIGINT/Ctrl+C events during connection to interrupt immediately. --- app/src/events.h | 6 +- app/src/scrcpy.c | 72 +++++++++++++++++++++--- app/src/server.c | 144 ++++++++++++++++++++++++++++------------------- app/src/server.h | 37 +++++++++--- 4 files changed, 181 insertions(+), 78 deletions(-) diff --git a/app/src/events.h b/app/src/events.h index a4d6f3df..abe1a72c 100644 --- a/app/src/events.h +++ b/app/src/events.h @@ -1,2 +1,4 @@ -#define EVENT_NEW_FRAME SDL_USEREVENT -#define EVENT_STREAM_STOPPED (SDL_USEREVENT + 1) +#define EVENT_NEW_FRAME SDL_USEREVENT +#define EVENT_STREAM_STOPPED (SDL_USEREVENT + 1) +#define EVENT_SERVER_CONNECTION_FAILED (SDL_USEREVENT + 2) +#define EVENT_SERVER_CONNECTED (SDL_USEREVENT + 3) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 43ed428e..38a50a0b 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -217,6 +217,29 @@ event_loop(struct scrcpy *s, const struct scrcpy_options *options) { return false; } +static bool +await_for_server(void) { + SDL_Event event; + while (SDL_WaitEvent(&event)) { + switch (event.type) { + case SDL_QUIT: + LOGD("User requested to quit"); + return false; + case EVENT_SERVER_CONNECTION_FAILED: + LOGE("Server connection failed"); + return false; + case EVENT_SERVER_CONNECTED: + LOGD("Server connected"); + return true; + default: + break; + } + } + + LOGE("SDL_WaitEvent() error: %s", SDL_GetError()); + return false; +} + static SDL_LogPriority sdl_priority_from_av_level(int level) { switch (level) { @@ -262,6 +285,32 @@ stream_on_eos(struct stream *stream, void *userdata) { PUSH_EVENT(EVENT_STREAM_STOPPED); } +static void +server_on_connection_failed(struct server *server, void *userdata) { + (void) server; + (void) userdata; + + PUSH_EVENT(EVENT_SERVER_CONNECTION_FAILED); +} + +static void +server_on_connected(struct server *server, void *userdata) { + (void) server; + (void) userdata; + + PUSH_EVENT(EVENT_SERVER_CONNECTED); +} + +static void +server_on_disconnected(struct server *server, void *userdata) { + (void) server; + (void) userdata; + + LOGD("Server disconnected"); + // Do nothing, the disconnection will be handled by the "stream stopped" + // event +} + bool scrcpy(struct scrcpy_options *options) { static struct scrcpy scrcpy; @@ -310,7 +359,12 @@ scrcpy(struct scrcpy_options *options) { .power_off_on_close = options->power_off_on_close, }; - if (!server_init(&s->server, ¶ms)) { + static const struct server_callbacks cbs = { + .on_connection_failed = server_on_connection_failed, + .on_connected = server_on_connected, + .on_disconnected = server_on_disconnected, + }; + if (!server_init(&s->server, ¶ms, &cbs, NULL)) { return false; } @@ -332,12 +386,14 @@ scrcpy(struct scrcpy_options *options) { sdl_configure(options->display, options->disable_screensaver); - struct server_info info; - - if (!server_connect_to(&s->server, &info)) { + // Await for server without blocking Ctrl+C handling + if (!await_for_server()) { goto end; } + // It is necessarily initialized here, since the device is connected + struct server_info *info = &s->server.info; + if (options->display && options->control) { if (!file_handler_init(&s->file_handler, options->serial, options->push_target)) { @@ -361,7 +417,7 @@ scrcpy(struct scrcpy_options *options) { if (!recorder_init(&s->recorder, options->record_filename, options->record_format, - info.frame_size)) { + info->frame_size)) { goto end; } rec = &s->recorder; @@ -407,11 +463,11 @@ scrcpy(struct scrcpy_options *options) { if (options->display) { const char *window_title = - options->window_title ? options->window_title : info.device_name; + options->window_title ? options->window_title : info->device_name; struct screen_params screen_params = { .window_title = window_title, - .frame_size = info.frame_size, + .frame_size = info->frame_size, .always_on_top = options->always_on_top, .window_x = options->window_x, .window_y = options->window_y, @@ -435,7 +491,7 @@ scrcpy(struct scrcpy_options *options) { #ifdef HAVE_V4L2 if (options->v4l2_device) { if (!sc_v4l2_sink_init(&s->v4l2_sink, options->v4l2_device, - info.frame_size, options->v4l2_buffer)) { + info->frame_size, options->v4l2_buffer)) { goto end; } diff --git a/app/src/server.c b/app/src/server.c index 5ed1984c..4709059f 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -402,7 +402,8 @@ connect_to_server(struct server *server, uint32_t attempts, sc_tick delay) { } bool -server_init(struct server *server, const struct server_params *params) { +server_init(struct server *server, const struct server_params *params, + const struct server_callbacks *cbs, void *cbs_userdata) { bool ok = server_params_copy(&server->params, params); if (!ok) { LOGE("Could not copy server params"); @@ -424,7 +425,6 @@ server_init(struct server *server, const struct server_params *params) { return false; } - server->process = SC_PROCESS_NONE; server->stopped = false; server->server_socket = SC_INVALID_SOCKET; @@ -436,6 +436,14 @@ server_init(struct server *server, const struct server_params *params) { server->tunnel_enabled = false; server->tunnel_forward = false; + assert(cbs); + assert(cbs->on_connection_failed); + assert(cbs->on_connected); + assert(cbs->on_disconnected); + + server->cbs = cbs; + server->cbs_userdata = cbs_userdata; + return true; } @@ -458,7 +466,7 @@ device_read_info(sc_socket device_socket, struct server_info *info) { return true; } -bool +static bool server_connect_to(struct server *server, struct server_info *info) { assert(server->tunnel_enabled); @@ -531,6 +539,9 @@ fail: } } + // Always leave this function with tunnel disabled + disable_tunnel(server); + return false; } @@ -547,57 +558,68 @@ server_on_terminated(void *userdata) { net_interrupt(server->server_socket); } + server->cbs->on_disconnected(server, server->cbs_userdata); + LOGD("Server terminated"); } -bool -server_start(struct server *server) { +static int +run_server(void *data) { + struct server *server = data; + const struct server_params *params = &server->params; - if (!push_server(params->serial)) { - /* server->serial will be freed on server_destroy() */ - return false; + bool ok = push_server(params->serial); + if (!ok) { + goto error_connection_failed; } - if (!enable_tunnel_any_port(server, params->port_range, - params->force_adb_forward)) { - return false; + ok = enable_tunnel_any_port(server, params->port_range, + params->force_adb_forward); + if (!ok) { + goto error_connection_failed; } // server will connect to our server socket - server->process = execute_server(server, params); - if (server->process == SC_PROCESS_NONE) { - goto error; + sc_pid pid = execute_server(server, params); + if (pid == SC_PROCESS_NONE) { + disable_tunnel(server); + goto error_connection_failed; } static const struct sc_process_listener listener = { .on_terminated = server_on_terminated, }; - bool ok = sc_process_observer_init(&server->observer, server->process, - &listener, server); + struct sc_process_observer observer; + ok = sc_process_observer_init(&observer, pid, &listener, server); if (!ok) { - sc_process_terminate(server->process); - sc_process_wait(server->process, true); // ignore exit code - goto error; + sc_process_terminate(pid); + sc_process_wait(pid, true); // ignore exit code + disable_tunnel(server); + goto error_connection_failed; } - return true; + ok = server_connect_to(server, &server->info); + // The tunnel is always closed by server_connect_to() + if (!ok) { + sc_process_terminate(pid); + sc_process_wait(pid, true); // ignore exit code + sc_process_observer_join(&observer); + sc_process_observer_destroy(&observer); + goto error_connection_failed; + } -error: - // The server socket (if any) will be closed on server_destroy() + // Now connected + server->cbs->on_connected(server, server->cbs_userdata); - disable_tunnel(server); - - return false; -} - -void -server_stop(struct server *server) { + // Wait for server_stop() sc_mutex_lock(&server->mutex); - server->stopped = true; - sc_cond_signal(&server->cond_stopped); + while (!server->stopped) { + sc_cond_wait(&server->cond_stopped, &server->mutex); + } sc_mutex_unlock(&server->mutex); + // Server stop has been requested if (server->server_socket != SC_INVALID_SOCKET) { if (!net_interrupt(server->server_socket)) { LOGW("Could not interrupt server socket"); @@ -614,18 +636,10 @@ server_stop(struct server *server) { } } - assert(server->process != SC_PROCESS_NONE); - - if (server->tunnel_enabled) { - // ignore failure - disable_tunnel(server); - } - // Give some delay for the server to terminate properly #define WATCHDOG_DELAY SC_TICK_FROM_SEC(1) sc_tick deadline = sc_tick_now() + WATCHDOG_DELAY; - bool terminated = - sc_process_observer_timedwait(&server->observer, deadline); + bool terminated = sc_process_observer_timedwait(&observer, deadline); // After this delay, kill the server if it's not dead already. // On some devices, closing the sockets is not sufficient to wake up the @@ -635,32 +649,44 @@ server_stop(struct server *server) { // reaped (closed) yet, so its PID is still valid, and it is ok to call // sc_process_terminate() even in that case. LOGW("Killing the server..."); - sc_process_terminate(server->process); + sc_process_terminate(pid); } - sc_process_observer_join(&server->observer); - sc_process_observer_destroy(&server->observer); + sc_process_observer_join(&observer); + sc_process_observer_destroy(&observer); - sc_process_close(server->process); + sc_process_close(pid); + + return 0; + +error_connection_failed: + server->cbs->on_connection_failed(server, server->cbs_userdata); + return -1; +} + +bool +server_start(struct server *server) { + bool ok = sc_thread_create(&server->thread, run_server, "server", server); + if (!ok) { + LOGE("Could not create server thread"); + return false; + } + + return true; +} + +void +server_stop(struct server *server) { + sc_mutex_lock(&server->mutex); + server->stopped = true; + sc_cond_signal(&server->cond_stopped); + sc_mutex_unlock(&server->mutex); + + sc_thread_join(&server->thread, NULL); } void server_destroy(struct server *server) { - if (server->server_socket != SC_INVALID_SOCKET) { - if (!net_close(server->server_socket)) { - LOGW("Could not close server socket"); - } - } - if (server->video_socket != SC_INVALID_SOCKET) { - if (!net_close(server->video_socket)) { - LOGW("Could not close video socket"); - } - } - if (server->control_socket != SC_INVALID_SOCKET) { - if (!net_close(server->control_socket)) { - LOGW("Could not close control socket"); - } - } server_params_destroy(&server->params); sc_cond_destroy(&server->cond_stopped); sc_mutex_destroy(&server->mutex); diff --git a/app/src/server.h b/app/src/server.h index 5807c322..e4666346 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -43,9 +43,8 @@ struct server { // The internal allocated strings are copies owned by the server struct server_params params; - sc_pid process; - // alive only between start() and stop() - struct sc_process_observer observer; + sc_thread thread; + struct server_info info; // initialized once connected sc_mutex mutex; sc_cond cond_stopped; @@ -57,20 +56,40 @@ struct server { uint16_t local_port; // selected from port_range bool tunnel_enabled; bool tunnel_forward; // use "adb forward" instead of "adb reverse" + + const struct server_callbacks *cbs; + void *cbs_userdata; +}; + +struct server_callbacks { + /** + * Called when the server failed to connect + * + * If it is called, then on_connected() and on_disconnected() will never be + * called. + */ + void (*on_connection_failed)(struct server *server, void *userdata); + + /** + * Called on server connection + */ + void (*on_connected)(struct server *server, void *userdata); + + /** + * Called on server disconnection (after it has been connected) + */ + void (*on_disconnected)(struct server *server, void *userdata); }; // init the server with the given params bool -server_init(struct server *server, const struct server_params *params); +server_init(struct server *server, const struct server_params *params, + const struct server_callbacks *cbs, void *cbs_userdata); -// push, enable tunnel et start the server +// start the server asynchronously bool server_start(struct server *server); -// block until the communication with the server is established -bool -server_connect_to(struct server *server, struct server_info *info); - // disconnect and kill the server process void server_stop(struct server *server); From e0896142dbbbadd87a6339111a406307edca1b97 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 12 Nov 2021 18:50:50 +0100 Subject: [PATCH 0761/2244] Introduce interruptor tool An interruptor instance will help to wake up a blocking call from another thread (typically to terminate immediately on Ctrl+C). --- app/meson.build | 1 + app/src/util/intr.c | 83 +++++++++++++++++++++++++++++++++++++++++++++ app/src/util/intr.h | 78 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 162 insertions(+) create mode 100644 app/src/util/intr.c create mode 100644 app/src/util/intr.h diff --git a/app/meson.build b/app/meson.build index 94b4994f..8240dd16 100644 --- a/app/meson.build +++ b/app/meson.build @@ -25,6 +25,7 @@ src = [ 'src/stream.c', 'src/video_buffer.c', 'src/util/file.c', + 'src/util/intr.c', 'src/util/log.c', 'src/util/net.c', 'src/util/process.c', diff --git a/app/src/util/intr.c b/app/src/util/intr.c new file mode 100644 index 00000000..d4fce55c --- /dev/null +++ b/app/src/util/intr.c @@ -0,0 +1,83 @@ +#include "intr.h" + +#include "util/log.h" + +#include + +bool +sc_intr_init(struct sc_intr *intr) { + bool ok = sc_mutex_init(&intr->mutex); + if (!ok) { + LOGE("Could not init intr mutex"); + return false; + } + + intr->socket = SC_INVALID_SOCKET; + intr->process = SC_PROCESS_NONE; + + atomic_store_explicit(&intr->interrupted, false, memory_order_relaxed); + + return true; +} + +bool +sc_intr_set_socket(struct sc_intr *intr, sc_socket socket) { + assert(intr->process == SC_PROCESS_NONE); + + sc_mutex_lock(&intr->mutex); + bool interrupted = + atomic_load_explicit(&intr->interrupted, memory_order_relaxed); + if (!interrupted) { + intr->socket = socket; + } + sc_mutex_unlock(&intr->mutex); + + return !interrupted; +} + +bool +sc_intr_set_process(struct sc_intr *intr, sc_pid pid) { + assert(intr->socket == SC_INVALID_SOCKET); + + sc_mutex_lock(&intr->mutex); + bool interrupted = + atomic_load_explicit(&intr->interrupted, memory_order_relaxed); + if (!interrupted) { + intr->process = pid; + } + sc_mutex_unlock(&intr->mutex); + + return !interrupted; +} + +void +sc_intr_interrupt(struct sc_intr *intr) { + sc_mutex_lock(&intr->mutex); + + atomic_store_explicit(&intr->interrupted, true, memory_order_relaxed); + + // No more than one component to interrupt + assert(intr->socket == SC_INVALID_SOCKET || + intr->process == SC_PROCESS_NONE); + + if (intr->socket != SC_INVALID_SOCKET) { + LOGD("Interrupting socket"); + net_interrupt(intr->socket); + intr->socket = SC_INVALID_SOCKET; + } + if (intr->process != SC_PROCESS_NONE) { + LOGD("Interrupting process"); + sc_process_terminate(intr->process); + intr->process = SC_PROCESS_NONE; + } + + sc_mutex_unlock(&intr->mutex); +} + +void +sc_intr_destroy(struct sc_intr *intr) { + assert(intr->socket == SC_INVALID_SOCKET); + assert(intr->process == SC_PROCESS_NONE); + + sc_mutex_destroy(&intr->mutex); +} diff --git a/app/src/util/intr.h b/app/src/util/intr.h new file mode 100644 index 00000000..7f0fb9b7 --- /dev/null +++ b/app/src/util/intr.h @@ -0,0 +1,78 @@ +#ifndef SC_INTR_H +#define SC_INTR_H + +#include "common.h" + +#include +#include + +#include "net.h" +#include "process.h" +#include "thread.h" + +/** + * Interruptor to wake up a blocking call from another thread + * + * It allows to register a socket or a process before a blocking call, and + * interrupt/close from another thread to wake up the blocking call. + */ +struct sc_intr { + sc_mutex mutex; + + sc_socket socket; + sc_pid process; + + // Written protected by the mutex to avoid race conditions against + // sc_intr_set_socket() and sc_intr_set_process(), but can be read + // (atomically) without mutex + atomic_bool interrupted; +}; + +/** + * Initialize an interruptor + */ +bool +sc_intr_init(struct sc_intr *intr); + +/** + * Set a socket as the interruptible component + * + * Call with SC_INVALID_SOCKET to unset. + */ +bool +sc_intr_set_socket(struct sc_intr *intr, sc_socket socket); + +/** + * Set a process as the interruptible component + * + * Call with SC_PROCESS_NONE to unset. + */ +bool +sc_intr_set_process(struct sc_intr *intr, sc_pid socket); + +/** + * Interrupt the current interruptible component + * + * Must be called from a different thread. + */ +void +sc_intr_interrupt(struct sc_intr *intr); + +/** + * Read the interrupted state + * + * It is exposed as a static inline function because it just loads from an + * atomic. + */ +static inline bool +sc_intr_is_interrupted(struct sc_intr *intr) { + return atomic_load_explicit(&intr->interrupted, memory_order_relaxed); +} + +/** + * Destroy the interruptor + */ +void +sc_intr_destroy(struct sc_intr *intr); + +#endif From 40340509d9245694bf46e60604c0fb262f668465 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 12 Nov 2021 18:50:50 +0100 Subject: [PATCH 0762/2244] Add interruptor utilities Expose wrapper functions for interrupting blocking calls, for process and sockets. --- app/meson.build | 2 + app/src/util/net_intr.c | 97 +++++++++++++++++++++++++++++++++++++ app/src/util/net_intr.h | 35 +++++++++++++ app/src/util/process_intr.c | 16 ++++++ app/src/util/process_intr.h | 13 +++++ 5 files changed, 163 insertions(+) create mode 100644 app/src/util/net_intr.c create mode 100644 app/src/util/net_intr.h create mode 100644 app/src/util/process_intr.c create mode 100644 app/src/util/process_intr.h diff --git a/app/meson.build b/app/meson.build index 8240dd16..09c79f21 100644 --- a/app/meson.build +++ b/app/meson.build @@ -28,7 +28,9 @@ src = [ 'src/util/intr.c', 'src/util/log.c', 'src/util/net.c', + 'src/util/net_intr.c', 'src/util/process.c', + 'src/util/process_intr.c', 'src/util/strbuf.c', 'src/util/str_util.c', 'src/util/term.c', diff --git a/app/src/util/net_intr.c b/app/src/util/net_intr.c new file mode 100644 index 00000000..b9443e2f --- /dev/null +++ b/app/src/util/net_intr.c @@ -0,0 +1,97 @@ +#include "net_intr.h" + +bool +net_connect_intr(struct sc_intr *intr, sc_socket socket, uint32_t addr, + uint16_t port) { + if (!sc_intr_set_socket(intr, socket)) { + // Already interrupted + return false; + } + + bool ret = net_connect(socket, addr, port); + + sc_intr_set_socket(intr, SC_INVALID_SOCKET); + return ret; +} + +bool +net_listen_intr(struct sc_intr *intr, sc_socket socket, uint32_t addr, + uint16_t port, int backlog) { + if (!sc_intr_set_socket(intr, socket)) { + // Already interrupted + return false; + } + + bool ret = net_listen(socket, addr, port, backlog); + + sc_intr_set_socket(intr, SC_INVALID_SOCKET); + return ret; +} + +sc_socket +net_accept_intr(struct sc_intr *intr, sc_socket server_socket) { + if (!sc_intr_set_socket(intr, server_socket)) { + // Already interrupted + return SC_INVALID_SOCKET; + } + + sc_socket socket = net_accept(server_socket); + + sc_intr_set_socket(intr, SC_INVALID_SOCKET); + return socket; +} + +ssize_t +net_recv_intr(struct sc_intr *intr, sc_socket socket, void *buf, size_t len) { + if (!sc_intr_set_socket(intr, socket)) { + // Already interrupted + return -1; + } + + ssize_t r = net_recv(socket, buf, len); + + sc_intr_set_socket(intr, SC_INVALID_SOCKET); + return r; +} + +ssize_t +net_recv_all_intr(struct sc_intr *intr, sc_socket socket, void *buf, + size_t len) { + if (!sc_intr_set_socket(intr, socket)) { + // Already interrupted + return -1; + } + + ssize_t r = net_recv_all(socket, buf, len); + + sc_intr_set_socket(intr, SC_INVALID_SOCKET); + return r; +} + +ssize_t +net_send_intr(struct sc_intr *intr, sc_socket socket, const void *buf, + size_t len) { + if (!sc_intr_set_socket(intr, socket)) { + // Already interrupted + return -1; + } + + ssize_t w = net_send(socket, buf, len); + + sc_intr_set_socket(intr, SC_INVALID_SOCKET); + return w; +} + +ssize_t +net_send_all_intr(struct sc_intr *intr, sc_socket socket, const void *buf, + size_t len) { + if (!sc_intr_set_socket(intr, socket)) { + // Already interrupted + return -1; + } + + ssize_t w = net_send_all(socket, buf, len); + + sc_intr_set_socket(intr, SC_INVALID_SOCKET); + return w; +} diff --git a/app/src/util/net_intr.h b/app/src/util/net_intr.h new file mode 100644 index 00000000..a83fadda --- /dev/null +++ b/app/src/util/net_intr.h @@ -0,0 +1,35 @@ +#ifndef SC_NET_INTR_H +#define SC_NET_INTR_H + +#include "common.h" + +#include "intr.h" +#include "net.h" + +bool +net_connect_intr(struct sc_intr *intr, sc_socket socket, uint32_t addr, + uint16_t port); + +bool +net_listen_intr(struct sc_intr *intr, sc_socket socket, uint32_t addr, + uint16_t port, int backlog); + +sc_socket +net_accept_intr(struct sc_intr *intr, sc_socket server_socket); + +ssize_t +net_recv_intr(struct sc_intr *intr, sc_socket socket, void *buf, size_t len); + +ssize_t +net_recv_all_intr(struct sc_intr *intr, sc_socket socket, void *buf, + size_t len); + +ssize_t +net_send_intr(struct sc_intr *intr, sc_socket socket, const void *buf, + size_t len); + +ssize_t +net_send_all_intr(struct sc_intr *intr, sc_socket socket, const void *buf, + size_t len); + +#endif diff --git a/app/src/util/process_intr.c b/app/src/util/process_intr.c new file mode 100644 index 00000000..bb483123 --- /dev/null +++ b/app/src/util/process_intr.c @@ -0,0 +1,16 @@ +#include "process_intr.h" + +bool +sc_process_check_success_intr(struct sc_intr *intr, sc_pid pid, + const char *name) { + if (!sc_intr_set_process(intr, pid)) { + // Already interrupted + return false; + } + + // Always pass close=false, interrupting would be racy otherwise + bool ret = sc_process_check_success(pid, name, false); + + sc_intr_set_process(intr, SC_PROCESS_NONE); + return ret; +} diff --git a/app/src/util/process_intr.h b/app/src/util/process_intr.h new file mode 100644 index 00000000..ff0dfc76 --- /dev/null +++ b/app/src/util/process_intr.h @@ -0,0 +1,13 @@ +#ifndef SC_PROCESS_INTR_H +#define SC_PROCESS_INTR_H + +#include "common.h" + +#include "intr.h" +#include "process.h" + +bool +sc_process_check_success_intr(struct sc_intr *intr, sc_pid pid, + const char *name); + +#endif From f488cbd7e7f8a74ace23416faa962b6a510f8374 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 12 Nov 2021 18:50:50 +0100 Subject: [PATCH 0763/2244] Make server interruptible Use an interruptor to immediately wake up blocking calls on server_stop(). --- app/src/server.c | 105 +++++++++++++++++++++++++---------------------- app/src/server.h | 3 ++ 2 files changed, 60 insertions(+), 48 deletions(-) diff --git a/app/src/server.c b/app/src/server.c index 4709059f..b21fa802 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -10,7 +10,8 @@ #include "adb.h" #include "util/file.h" #include "util/log.h" -#include "util/net.h" +#include "util/net_intr.h" +#include "util/process_intr.h" #include "util/str_util.h" #define SOCKET_NAME "scrcpy" @@ -101,7 +102,7 @@ error: } static bool -push_server(const char *serial) { +push_server(struct sc_intr *intr, const char *serial) { char *server_path = get_server_path(); if (!server_path) { return false; @@ -113,31 +114,34 @@ push_server(const char *serial) { } sc_pid pid = adb_push(serial, server_path, DEVICE_SERVER_PATH); free(server_path); - return sc_process_check_success(pid, "adb push", true); + return sc_process_check_success_intr(intr, pid, "adb push"); } static bool -enable_tunnel_reverse(const char *serial, uint16_t local_port) { +enable_tunnel_reverse(struct sc_intr *intr, const char *serial, + uint16_t local_port) { sc_pid pid = adb_reverse(serial, SOCKET_NAME, local_port); - return sc_process_check_success(pid, "adb reverse", true); + return sc_process_check_success_intr(intr, pid, "adb reverse"); } static bool -disable_tunnel_reverse(const char *serial) { +disable_tunnel_reverse(struct sc_intr *intr, const char *serial) { sc_pid pid = adb_reverse_remove(serial, SOCKET_NAME); - return sc_process_check_success(pid, "adb reverse --remove", true); + return sc_process_check_success_intr(intr, pid, "adb reverse --remove"); } static bool -enable_tunnel_forward(const char *serial, uint16_t local_port) { +enable_tunnel_forward(struct sc_intr *intr, const char *serial, + uint16_t local_port) { sc_pid pid = adb_forward(serial, local_port, SOCKET_NAME); - return sc_process_check_success(pid, "adb forward", true); + return sc_process_check_success_intr(intr, pid, "adb forward"); } static bool -disable_tunnel_forward(const char *serial, uint16_t local_port) { +disable_tunnel_forward(struct sc_intr *intr, const char *serial, + uint16_t local_port) { sc_pid pid = adb_forward_remove(serial, local_port); - return sc_process_check_success(pid, "adb forward --remove", true); + return sc_process_check_success_intr(intr, pid, "adb forward --remove"); } static bool @@ -146,8 +150,8 @@ disable_tunnel(struct server *server) { const char *serial = server->params.serial; bool ok = server->tunnel_forward - ? disable_tunnel_forward(serial, server->local_port) - : disable_tunnel_reverse(serial); + ? disable_tunnel_forward(&server->intr, serial, server->local_port) + : disable_tunnel_reverse(&server->intr, serial); // Consider tunnel disabled even if the command failed server->tunnel_enabled = false; @@ -156,9 +160,9 @@ disable_tunnel(struct server *server) { } static bool -listen_on_port(sc_socket socket, uint16_t port) { +listen_on_port(struct sc_intr *intr, sc_socket socket, uint16_t port) { #define IPV4_LOCALHOST 0x7F000001 - return net_listen(socket, IPV4_LOCALHOST, port, 1); + return net_listen_intr(intr, socket, IPV4_LOCALHOST, port, 1); } static bool @@ -167,7 +171,7 @@ enable_tunnel_reverse_any_port(struct server *server, const char *serial = server->params.serial; uint16_t port = port_range.first; for (;;) { - if (!enable_tunnel_reverse(serial, port)) { + if (!enable_tunnel_reverse(&server->intr, serial, port)) { // the command itself failed, it will fail on any port return false; } @@ -180,7 +184,7 @@ enable_tunnel_reverse_any_port(struct server *server, // device. sc_socket server_socket = net_socket(); if (server_socket != SC_INVALID_SOCKET) { - bool ok = listen_on_port(server_socket, port); + bool ok = listen_on_port(&server->intr, server_socket, port); if (ok) { // success server->server_socket = server_socket; @@ -192,8 +196,13 @@ enable_tunnel_reverse_any_port(struct server *server, net_close(server_socket); } + if (sc_intr_is_interrupted(&server->intr)) { + // Stop immediately + return false; + } + // failure, disable tunnel and try another port - if (!disable_tunnel_reverse(serial)) { + if (!disable_tunnel_reverse(&server->intr, serial)) { LOGW("Could not remove reverse tunnel on port %" PRIu16, port); } @@ -223,13 +232,18 @@ enable_tunnel_forward_any_port(struct server *server, const char *serial = server->params.serial; uint16_t port = port_range.first; for (;;) { - if (enable_tunnel_forward(serial, port)) { + if (enable_tunnel_forward(&server->intr, serial, port)) { // success server->local_port = port; server->tunnel_enabled = true; return true; } + if (sc_intr_is_interrupted(&server->intr)) { + // Stop immediately + return false; + } + if (port < port_range.last) { LOGW("Could not forward port %" PRIu16", retrying on %" PRIu16, port, (uint16_t) (port + 1)); @@ -349,8 +363,8 @@ execute_server(struct server *server, const struct server_params *params) { } static bool -connect_and_read_byte(sc_socket socket, uint16_t port) { - bool ok = net_connect(socket, IPV4_LOCALHOST, port); +connect_and_read_byte(struct sc_intr *intr, sc_socket socket, uint16_t port) { + bool ok = net_connect_intr(intr, socket, IPV4_LOCALHOST, port); if (!ok) { return false; } @@ -358,7 +372,7 @@ connect_and_read_byte(sc_socket socket, uint16_t port) { char byte; // the connection may succeed even if the server behind the "adb tunnel" // is not listening, so read one byte to detect a working connection - if (net_recv(socket, &byte, 1) != 1) { + if (net_recv_intr(intr, socket, &byte, 1) != 1) { // the server is not listening yet behind the adb tunnel return false; } @@ -373,7 +387,7 @@ connect_to_server(struct server *server, uint32_t attempts, sc_tick delay) { LOGD("Remaining connection attempts: %d", (int) attempts); sc_socket socket = net_socket(); if (socket != SC_INVALID_SOCKET) { - bool ok = connect_and_read_byte(socket, port); + bool ok = connect_and_read_byte(&server->intr, socket, port); if (ok) { // it worked! return socket; @@ -425,6 +439,15 @@ server_init(struct server *server, const struct server_params *params, return false; } + ok = sc_intr_init(&server->intr); + if (!ok) { + LOGE("Could not create intr"); + sc_cond_destroy(&server->cond_stopped); + sc_mutex_destroy(&server->mutex); + server_params_destroy(&server->params); + return false; + } + server->stopped = false; server->server_socket = SC_INVALID_SOCKET; @@ -448,9 +471,10 @@ server_init(struct server *server, const struct server_params *params, } static bool -device_read_info(sc_socket device_socket, struct server_info *info) { +device_read_info(struct sc_intr *intr, sc_socket device_socket, + struct server_info *info) { unsigned char buf[DEVICE_NAME_FIELD_LENGTH + 4]; - ssize_t r = net_recv_all(device_socket, buf, sizeof(buf)); + ssize_t r = net_recv_all_intr(intr, device_socket, buf, sizeof(buf)); if (r < DEVICE_NAME_FIELD_LENGTH + 4) { LOGE("Could not retrieve device information"); return false; @@ -473,12 +497,12 @@ server_connect_to(struct server *server, struct server_info *info) { sc_socket video_socket = SC_INVALID_SOCKET; sc_socket control_socket = SC_INVALID_SOCKET; if (!server->tunnel_forward) { - video_socket = net_accept(server->server_socket); + video_socket = net_accept_intr(&server->intr, server->server_socket); if (video_socket == SC_INVALID_SOCKET) { goto fail; } - control_socket = net_accept(server->server_socket); + control_socket = net_accept_intr(&server->intr, server->server_socket); if (control_socket == SC_INVALID_SOCKET) { goto fail; } @@ -502,8 +526,8 @@ server_connect_to(struct server *server, struct server_info *info) { if (control_socket == SC_INVALID_SOCKET) { goto fail; } - bool ok = net_connect(control_socket, IPV4_LOCALHOST, - server->local_port); + bool ok = net_connect_intr(&server->intr, control_socket, + IPV4_LOCALHOST, server->local_port); if (!ok) { goto fail; } @@ -513,7 +537,7 @@ server_connect_to(struct server *server, struct server_info *info) { disable_tunnel(server); // ignore failure // The sockets will be closed on stop if device_read_info() fails - bool ok = device_read_info(video_socket, info); + bool ok = device_read_info(&server->intr, video_socket, info); if (!ok) { goto fail; } @@ -569,7 +593,7 @@ run_server(void *data) { const struct server_params *params = &server->params; - bool ok = push_server(params->serial); + bool ok = push_server(&server->intr, params->serial); if (!ok) { goto error_connection_failed; } @@ -619,23 +643,6 @@ run_server(void *data) { } sc_mutex_unlock(&server->mutex); - // Server stop has been requested - if (server->server_socket != SC_INVALID_SOCKET) { - if (!net_interrupt(server->server_socket)) { - LOGW("Could not interrupt server socket"); - } - } - if (server->video_socket != SC_INVALID_SOCKET) { - if (!net_interrupt(server->video_socket)) { - LOGW("Could not interrupt video socket"); - } - } - if (server->control_socket != SC_INVALID_SOCKET) { - if (!net_interrupt(server->control_socket)) { - LOGW("Could not interrupt control socket"); - } - } - // Give some delay for the server to terminate properly #define WATCHDOG_DELAY SC_TICK_FROM_SEC(1) sc_tick deadline = sc_tick_now() + WATCHDOG_DELAY; @@ -680,6 +687,7 @@ server_stop(struct server *server) { sc_mutex_lock(&server->mutex); server->stopped = true; sc_cond_signal(&server->cond_stopped); + sc_intr_interrupt(&server->intr); sc_mutex_unlock(&server->mutex); sc_thread_join(&server->thread, NULL); @@ -688,6 +696,7 @@ server_stop(struct server *server) { void server_destroy(struct server *server) { server_params_destroy(&server->params); + sc_intr_destroy(&server->intr); sc_cond_destroy(&server->cond_stopped); sc_mutex_destroy(&server->mutex); } diff --git a/app/src/server.h b/app/src/server.h index e4666346..b021ba32 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -10,6 +10,7 @@ #include "adb.h" #include "coords.h" #include "options.h" +#include "util/intr.h" #include "util/log.h" #include "util/net.h" #include "util/thread.h" @@ -50,6 +51,8 @@ struct server { sc_cond cond_stopped; bool stopped; + struct sc_intr intr; + sc_socket server_socket; // only used if !tunnel_forward sc_socket video_socket; sc_socket control_socket; From 37c840a4c83a240d7cf327395bd4be7a882eda65 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 12 Nov 2021 21:40:22 +0100 Subject: [PATCH 0764/2244] Interrupt on process terminated Interrupt any blocking call on process terminated, like on server_stop(). This allows to interrupt any blocking accept() with correct synchronization without additional complexity. --- app/src/server.c | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/app/src/server.c b/app/src/server.c index b21fa802..bb49db37 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -511,8 +511,8 @@ server_connect_to(struct server *server, struct server_info *info) { if (!net_close(server->server_socket)) { LOGW("Could not close server socket on connect"); } - // Do not attempt to close it again on server_destroy() - server->server_socket = SC_INVALID_SOCKET; + + // server_socket is never used anymore } else { uint32_t attempts = 100; sc_tick delay = SC_TICK_FROM_MS(100); @@ -573,14 +573,11 @@ static void server_on_terminated(void *userdata) { struct server *server = userdata; - // No need for synchronization, server_socket is initialized before the - // observer thread is created. - if (server->server_socket != SC_INVALID_SOCKET) { - // If the server process dies before connecting to the server socket, - // then the client will be stuck forever on accept(). To avoid the - // problem, wake up the accept() call when the server dies. - net_interrupt(server->server_socket); - } + // If the server process dies before connecting to the server socket, + // then the client will be stuck forever on accept(). To avoid the problem, + // wake up the accept() call (or any other) when the server dies, like on + // stop() (it is safe to call interrupt() twice). + sc_intr_interrupt(&server->intr); server->cbs->on_disconnected(server, server->cbs_userdata); From 0d45c29d13be15764a3e58e47d5e55f07b4e5dee Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 12 Nov 2021 18:59:56 +0100 Subject: [PATCH 0765/2244] Move IPV4_LOCALHOST to net.h This constant will be used from several files. --- app/src/server.c | 1 - app/src/util/net.h | 3 +++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/server.c b/app/src/server.c index bb49db37..7a1f2abc 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -161,7 +161,6 @@ disable_tunnel(struct server *server) { static bool listen_on_port(struct sc_intr *intr, sc_socket socket, uint16_t port) { -#define IPV4_LOCALHOST 0x7F000001 return net_listen_intr(intr, socket, IPV4_LOCALHOST, port, 1); } diff --git a/app/src/util/net.h b/app/src/util/net.h index 9d675a2d..7a9fbe47 100644 --- a/app/src/util/net.h +++ b/app/src/util/net.h @@ -22,8 +22,11 @@ # include # define SC_INVALID_SOCKET -1 typedef int sc_socket; + #endif +#define IPV4_LOCALHOST 0x7F000001 + bool net_init(void); From c4d008b96ae3f23a467642fed38f91e54b1f36b8 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 12 Nov 2021 22:32:29 +0100 Subject: [PATCH 0766/2244] Extract adb tunnel to a separate component This simplifies the server code. --- app/meson.build | 1 + app/src/adb_tunnel.c | 192 ++++++++++++++++++++++++++++++++++++++++ app/src/adb_tunnel.h | 47 ++++++++++ app/src/server.c | 205 ++++--------------------------------------- app/src/server.h | 6 +- 5 files changed, 260 insertions(+), 191 deletions(-) create mode 100644 app/src/adb_tunnel.c create mode 100644 app/src/adb_tunnel.h diff --git a/app/meson.build b/app/meson.build index 09c79f21..7abef1e2 100644 --- a/app/meson.build +++ b/app/meson.build @@ -1,6 +1,7 @@ src = [ 'src/main.c', 'src/adb.c', + 'src/adb_tunnel.c', 'src/cli.c', 'src/clock.c', 'src/compat.c', diff --git a/app/src/adb_tunnel.c b/app/src/adb_tunnel.c new file mode 100644 index 00000000..d1a19afc --- /dev/null +++ b/app/src/adb_tunnel.c @@ -0,0 +1,192 @@ +#include "adb_tunnel.h" + +#include + +#include "adb.h" +#include "util/log.h" +#include "util/net_intr.h" +#include "util/process_intr.h" + +#define SC_SOCKET_NAME "scrcpy" + +static bool +enable_tunnel_reverse(struct sc_intr *intr, const char *serial, + uint16_t local_port) { + sc_pid pid = adb_reverse(serial, SC_SOCKET_NAME, local_port); + return sc_process_check_success_intr(intr, pid, "adb reverse"); +} + +static bool +disable_tunnel_reverse(struct sc_intr *intr, const char *serial) { + sc_pid pid = adb_reverse_remove(serial, SC_SOCKET_NAME); + return sc_process_check_success_intr(intr, pid, "adb reverse --remove"); +} + +static bool +enable_tunnel_forward(struct sc_intr *intr, const char *serial, + uint16_t local_port) { + sc_pid pid = adb_forward(serial, local_port, SC_SOCKET_NAME); + return sc_process_check_success_intr(intr, pid, "adb forward"); +} + +static bool +disable_tunnel_forward(struct sc_intr *intr, const char *serial, + uint16_t local_port) { + sc_pid pid = adb_forward_remove(serial, local_port); + return sc_process_check_success_intr(intr, pid, "adb forward --remove"); +} + +static bool +listen_on_port(struct sc_intr *intr, sc_socket socket, uint16_t port) { + return net_listen_intr(intr, socket, IPV4_LOCALHOST, port, 1); +} + +static bool +enable_tunnel_reverse_any_port(struct sc_adb_tunnel *tunnel, + struct sc_intr *intr, const char *serial, + struct sc_port_range port_range) { + uint16_t port = port_range.first; + for (;;) { + if (!enable_tunnel_reverse(intr, 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. + sc_socket server_socket = net_socket(); + if (server_socket != SC_INVALID_SOCKET) { + bool ok = listen_on_port(intr, server_socket, port); + if (ok) { + // success + tunnel->server_socket = server_socket; + tunnel->local_port = port; + tunnel->enabled = true; + return true; + } + + net_close(server_socket); + } + + if (sc_intr_is_interrupted(intr)) { + // Stop immediately + return false; + } + + // failure, disable tunnel and try another port + if (!disable_tunnel_reverse(intr, 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, (uint16_t) (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 sc_adb_tunnel *tunnel, + struct sc_intr *intr, const char *serial, + struct sc_port_range port_range) { + tunnel->forward = true; + + uint16_t port = port_range.first; + for (;;) { + if (enable_tunnel_forward(intr, serial, port)) { + // success + tunnel->local_port = port; + tunnel->enabled = true; + return true; + } + + if (sc_intr_is_interrupted(intr)) { + // Stop immediately + return false; + } + + if (port < port_range.last) { + LOGW("Could not forward port %" PRIu16", retrying on %" PRIu16, + port, (uint16_t) (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; + } +} + +void +sc_adb_tunnel_init(struct sc_adb_tunnel *tunnel) { + tunnel->enabled = false; + tunnel->forward = false; + tunnel->server_socket = SC_INVALID_SOCKET; + tunnel->local_port = 0; +} + +bool +sc_adb_tunnel_open(struct sc_adb_tunnel *tunnel, struct sc_intr *intr, + const char *serial, struct sc_port_range port_range, + bool force_adb_forward) { + assert(!tunnel->enabled); + + if (!force_adb_forward) { + // Attempt to use "adb reverse" + if (enable_tunnel_reverse_any_port(tunnel, intr, serial, 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'"); + } + + return enable_tunnel_forward_any_port(tunnel, intr, serial, port_range); +} + +bool +sc_adb_tunnel_close(struct sc_adb_tunnel *tunnel, struct sc_intr *intr, + const char *serial) { + assert(tunnel->enabled); + + bool ret; + if (tunnel->forward) { + ret = disable_tunnel_forward(intr, serial, tunnel->local_port); + } else { + ret = disable_tunnel_reverse(intr, serial); + + assert(tunnel->server_socket != SC_INVALID_SOCKET); + if (!net_close(tunnel->server_socket)) { + LOGW("Could not close server socket"); + } + + // server_socket is never used anymore + } + + // Consider tunnel disabled even if the command failed + tunnel->enabled = false; + + return ret; +} diff --git a/app/src/adb_tunnel.h b/app/src/adb_tunnel.h new file mode 100644 index 00000000..12e3cf17 --- /dev/null +++ b/app/src/adb_tunnel.h @@ -0,0 +1,47 @@ +#ifndef SC_ADB_TUNNEL_H +#define SC_ADB_TUNNEL_H + +#include "common.h" + +#include +#include + +#include "options.h" +#include "util/intr.h" +#include "util/net.h" + +struct sc_adb_tunnel { + bool enabled; + bool forward; // use "adb forward" instead of "adb reverse" + sc_socket server_socket; // only used if !forward + uint16_t local_port; +}; + +/** + * Initialize the adb tunnel struct to default values + */ +void +sc_adb_tunnel_init(struct sc_adb_tunnel *tunnel); + +/** + * Open a tunnel + * + * Blocking calls may be interrupted asynchronously via `intr`. + * + * If `force_adb_forward` is not set, then attempts to set up an "adb reverse" + * tunnel first. Only if it fails (typical on old Android version connected via + * TCP/IP), use "adb forward". + */ +bool +sc_adb_tunnel_open(struct sc_adb_tunnel *tunnel, struct sc_intr *intr, + const char *serial, struct sc_port_range port_range, + bool force_adb_forward); + +/** + * Close the tunnel + */ +bool +sc_adb_tunnel_close(struct sc_adb_tunnel *tunnel, struct sc_intr *intr, + const char *serial); + +#endif diff --git a/app/src/server.c b/app/src/server.c index 7a1f2abc..923e3a76 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -14,7 +14,6 @@ #include "util/process_intr.h" #include "util/str_util.h" -#define SOCKET_NAME "scrcpy" #define SERVER_FILENAME "scrcpy-server" #define DEFAULT_SERVER_PATH PREFIX "/share/scrcpy/" SERVER_FILENAME @@ -117,167 +116,6 @@ push_server(struct sc_intr *intr, const char *serial) { return sc_process_check_success_intr(intr, pid, "adb push"); } -static bool -enable_tunnel_reverse(struct sc_intr *intr, const char *serial, - uint16_t local_port) { - sc_pid pid = adb_reverse(serial, SOCKET_NAME, local_port); - return sc_process_check_success_intr(intr, pid, "adb reverse"); -} - -static bool -disable_tunnel_reverse(struct sc_intr *intr, const char *serial) { - sc_pid pid = adb_reverse_remove(serial, SOCKET_NAME); - return sc_process_check_success_intr(intr, pid, "adb reverse --remove"); -} - -static bool -enable_tunnel_forward(struct sc_intr *intr, const char *serial, - uint16_t local_port) { - sc_pid pid = adb_forward(serial, local_port, SOCKET_NAME); - return sc_process_check_success_intr(intr, pid, "adb forward"); -} - -static bool -disable_tunnel_forward(struct sc_intr *intr, const char *serial, - uint16_t local_port) { - sc_pid pid = adb_forward_remove(serial, local_port); - return sc_process_check_success_intr(intr, pid, "adb forward --remove"); -} - -static bool -disable_tunnel(struct server *server) { - assert(server->tunnel_enabled); - - const char *serial = server->params.serial; - bool ok = server->tunnel_forward - ? disable_tunnel_forward(&server->intr, serial, server->local_port) - : disable_tunnel_reverse(&server->intr, serial); - - // Consider tunnel disabled even if the command failed - server->tunnel_enabled = false; - - return ok; -} - -static bool -listen_on_port(struct sc_intr *intr, sc_socket socket, uint16_t port) { - return net_listen_intr(intr, socket, IPV4_LOCALHOST, port, 1); -} - -static bool -enable_tunnel_reverse_any_port(struct server *server, - struct sc_port_range port_range) { - const char *serial = server->params.serial; - uint16_t port = port_range.first; - for (;;) { - if (!enable_tunnel_reverse(&server->intr, 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. - sc_socket server_socket = net_socket(); - if (server_socket != SC_INVALID_SOCKET) { - bool ok = listen_on_port(&server->intr, server_socket, port); - if (ok) { - // success - server->server_socket = server_socket; - server->local_port = port; - server->tunnel_enabled = true; - return true; - } - - net_close(server_socket); - } - - if (sc_intr_is_interrupted(&server->intr)) { - // Stop immediately - return false; - } - - // failure, disable tunnel and try another port - if (!disable_tunnel_reverse(&server->intr, 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, (uint16_t) (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 sc_port_range port_range) { - server->tunnel_forward = true; - - const char *serial = server->params.serial; - uint16_t port = port_range.first; - for (;;) { - if (enable_tunnel_forward(&server->intr, serial, port)) { - // success - server->local_port = port; - server->tunnel_enabled = true; - return true; - } - - if (sc_intr_is_interrupted(&server->intr)) { - // Stop immediately - return false; - } - - if (port < port_range.last) { - LOGW("Could not forward port %" PRIu16", retrying on %" PRIu16, - port, (uint16_t) (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 sc_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'"); - } - - return enable_tunnel_forward_any_port(server, port_range); -} - static const char * log_level_to_server_string(enum sc_log_level level) { switch (level) { @@ -336,7 +174,7 @@ execute_server(struct server *server, const struct server_params *params) { bit_rate_string, max_fps_string, lock_video_orientation_string, - server->tunnel_forward ? "true" : "false", + server->tunnel.forward ? "true" : "false", params->crop ? params->crop : "-", "true", // always send frame meta (packet boundaries + timestamp) params->control ? "true" : "false", @@ -381,7 +219,7 @@ connect_and_read_byte(struct sc_intr *intr, sc_socket socket, uint16_t port) { static sc_socket connect_to_server(struct server *server, uint32_t attempts, sc_tick delay) { - uint16_t port = server->local_port; + uint16_t port = server->tunnel.local_port; do { LOGD("Remaining connection attempts: %d", (int) attempts); sc_socket socket = net_socket(); @@ -449,14 +287,10 @@ server_init(struct server *server, const struct server_params *params, server->stopped = false; - server->server_socket = SC_INVALID_SOCKET; server->video_socket = SC_INVALID_SOCKET; server->control_socket = SC_INVALID_SOCKET; - server->local_port = 0; - - server->tunnel_enabled = false; - server->tunnel_forward = false; + sc_adb_tunnel_init(&server->tunnel); assert(cbs); assert(cbs->on_connection_failed); @@ -491,27 +325,24 @@ device_read_info(struct sc_intr *intr, sc_socket device_socket, static bool server_connect_to(struct server *server, struct server_info *info) { - assert(server->tunnel_enabled); + struct sc_adb_tunnel *tunnel = &server->tunnel; + + assert(tunnel->enabled); + + const char *serial = server->params.serial; sc_socket video_socket = SC_INVALID_SOCKET; sc_socket control_socket = SC_INVALID_SOCKET; - if (!server->tunnel_forward) { - video_socket = net_accept_intr(&server->intr, server->server_socket); + if (!tunnel->forward) { + video_socket = net_accept_intr(&server->intr, tunnel->server_socket); if (video_socket == SC_INVALID_SOCKET) { goto fail; } - control_socket = net_accept_intr(&server->intr, server->server_socket); + control_socket = net_accept_intr(&server->intr, tunnel->server_socket); if (control_socket == SC_INVALID_SOCKET) { goto fail; } - - // we don't need the server socket anymore - if (!net_close(server->server_socket)) { - LOGW("Could not close server socket on connect"); - } - - // server_socket is never used anymore } else { uint32_t attempts = 100; sc_tick delay = SC_TICK_FROM_MS(100); @@ -526,14 +357,14 @@ server_connect_to(struct server *server, struct server_info *info) { goto fail; } bool ok = net_connect_intr(&server->intr, control_socket, - IPV4_LOCALHOST, server->local_port); + IPV4_LOCALHOST, tunnel->local_port); if (!ok) { goto fail; } } // we don't need the adb tunnel anymore - disable_tunnel(server); // ignore failure + sc_adb_tunnel_close(tunnel, &server->intr, serial); // The sockets will be closed on stop if device_read_info() fails bool ok = device_read_info(&server->intr, video_socket, info); @@ -563,7 +394,7 @@ fail: } // Always leave this function with tunnel disabled - disable_tunnel(server); + sc_adb_tunnel_close(tunnel, &server->intr, serial); return false; } @@ -594,8 +425,8 @@ run_server(void *data) { goto error_connection_failed; } - ok = enable_tunnel_any_port(server, params->port_range, - params->force_adb_forward); + ok = sc_adb_tunnel_open(&server->tunnel, &server->intr, params->serial, + params->port_range, params->force_adb_forward); if (!ok) { goto error_connection_failed; } @@ -603,7 +434,7 @@ run_server(void *data) { // server will connect to our server socket sc_pid pid = execute_server(server, params); if (pid == SC_PROCESS_NONE) { - disable_tunnel(server); + sc_adb_tunnel_close(&server->tunnel, &server->intr, params->serial); goto error_connection_failed; } @@ -615,7 +446,7 @@ run_server(void *data) { if (!ok) { sc_process_terminate(pid); sc_process_wait(pid, true); // ignore exit code - disable_tunnel(server); + sc_adb_tunnel_close(&server->tunnel, &server->intr, params->serial); goto error_connection_failed; } diff --git a/app/src/server.h b/app/src/server.h index b021ba32..cb2cf3a8 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -8,6 +8,7 @@ #include #include "adb.h" +#include "adb_tunnel.h" #include "coords.h" #include "options.h" #include "util/intr.h" @@ -52,13 +53,10 @@ struct server { bool stopped; struct sc_intr intr; + struct sc_adb_tunnel tunnel; - sc_socket server_socket; // only used if !tunnel_forward sc_socket video_socket; sc_socket control_socket; - uint16_t local_port; // selected from port_range - bool tunnel_enabled; - bool tunnel_forward; // use "adb forward" instead of "adb reverse" const struct server_callbacks *cbs; void *cbs_userdata; From 9a0bd545d533d13fda8823481330329d6865cb2c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 13 Nov 2021 09:58:52 +0100 Subject: [PATCH 0767/2244] Rename SC_INVALID_SOCKET to SC_SOCKET_NONE For consistency with SC_PROCESS_NONE. --- app/src/adb_tunnel.c | 6 +++--- app/src/server.c | 28 ++++++++++++++-------------- app/src/util/intr.c | 12 ++++++------ app/src/util/intr.h | 2 +- app/src/util/net.c | 10 +++++----- app/src/util/net.h | 4 ++-- app/src/util/net_intr.c | 16 ++++++++-------- 7 files changed, 39 insertions(+), 39 deletions(-) diff --git a/app/src/adb_tunnel.c b/app/src/adb_tunnel.c index d1a19afc..f02eb83e 100644 --- a/app/src/adb_tunnel.c +++ b/app/src/adb_tunnel.c @@ -59,7 +59,7 @@ enable_tunnel_reverse_any_port(struct sc_adb_tunnel *tunnel, // need to try to connect until the server socket is listening on the // device. sc_socket server_socket = net_socket(); - if (server_socket != SC_INVALID_SOCKET) { + if (server_socket != SC_SOCKET_NONE) { bool ok = listen_on_port(intr, server_socket, port); if (ok) { // success @@ -141,7 +141,7 @@ void sc_adb_tunnel_init(struct sc_adb_tunnel *tunnel) { tunnel->enabled = false; tunnel->forward = false; - tunnel->server_socket = SC_INVALID_SOCKET; + tunnel->server_socket = SC_SOCKET_NONE; tunnel->local_port = 0; } @@ -177,7 +177,7 @@ sc_adb_tunnel_close(struct sc_adb_tunnel *tunnel, struct sc_intr *intr, } else { ret = disable_tunnel_reverse(intr, serial); - assert(tunnel->server_socket != SC_INVALID_SOCKET); + assert(tunnel->server_socket != SC_SOCKET_NONE); if (!net_close(tunnel->server_socket)) { LOGW("Could not close server socket"); } diff --git a/app/src/server.c b/app/src/server.c index 923e3a76..d6b143aa 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -223,7 +223,7 @@ connect_to_server(struct server *server, uint32_t attempts, sc_tick delay) { do { LOGD("Remaining connection attempts: %d", (int) attempts); sc_socket socket = net_socket(); - if (socket != SC_INVALID_SOCKET) { + if (socket != SC_SOCKET_NONE) { bool ok = connect_and_read_byte(&server->intr, socket, port); if (ok) { // it worked! @@ -249,7 +249,7 @@ connect_to_server(struct server *server, uint32_t attempts, sc_tick delay) { } } } while (--attempts > 0); - return SC_INVALID_SOCKET; + return SC_SOCKET_NONE; } bool @@ -287,8 +287,8 @@ server_init(struct server *server, const struct server_params *params, server->stopped = false; - server->video_socket = SC_INVALID_SOCKET; - server->control_socket = SC_INVALID_SOCKET; + server->video_socket = SC_SOCKET_NONE; + server->control_socket = SC_SOCKET_NONE; sc_adb_tunnel_init(&server->tunnel); @@ -331,29 +331,29 @@ server_connect_to(struct server *server, struct server_info *info) { const char *serial = server->params.serial; - sc_socket video_socket = SC_INVALID_SOCKET; - sc_socket control_socket = SC_INVALID_SOCKET; + sc_socket video_socket = SC_SOCKET_NONE; + sc_socket control_socket = SC_SOCKET_NONE; if (!tunnel->forward) { video_socket = net_accept_intr(&server->intr, tunnel->server_socket); - if (video_socket == SC_INVALID_SOCKET) { + if (video_socket == SC_SOCKET_NONE) { goto fail; } control_socket = net_accept_intr(&server->intr, tunnel->server_socket); - if (control_socket == SC_INVALID_SOCKET) { + if (control_socket == SC_SOCKET_NONE) { goto fail; } } else { uint32_t attempts = 100; sc_tick delay = SC_TICK_FROM_MS(100); video_socket = connect_to_server(server, attempts, delay); - if (video_socket == SC_INVALID_SOCKET) { + if (video_socket == SC_SOCKET_NONE) { goto fail; } // we know that the device is listening, we don't need several attempts control_socket = net_socket(); - if (control_socket == SC_INVALID_SOCKET) { + if (control_socket == SC_SOCKET_NONE) { goto fail; } bool ok = net_connect_intr(&server->intr, control_socket, @@ -372,8 +372,8 @@ server_connect_to(struct server *server, struct server_info *info) { goto fail; } - assert(video_socket != SC_INVALID_SOCKET); - assert(control_socket != SC_INVALID_SOCKET); + assert(video_socket != SC_SOCKET_NONE); + assert(control_socket != SC_SOCKET_NONE); server->video_socket = video_socket; server->control_socket = control_socket; @@ -381,13 +381,13 @@ server_connect_to(struct server *server, struct server_info *info) { return true; fail: - if (video_socket != SC_INVALID_SOCKET) { + if (video_socket != SC_SOCKET_NONE) { if (!net_close(video_socket)) { LOGW("Could not close video socket"); } } - if (control_socket != SC_INVALID_SOCKET) { + if (control_socket != SC_SOCKET_NONE) { if (!net_close(control_socket)) { LOGW("Could not close control socket"); } diff --git a/app/src/util/intr.c b/app/src/util/intr.c index d4fce55c..50d9abbe 100644 --- a/app/src/util/intr.c +++ b/app/src/util/intr.c @@ -12,7 +12,7 @@ sc_intr_init(struct sc_intr *intr) { return false; } - intr->socket = SC_INVALID_SOCKET; + intr->socket = SC_SOCKET_NONE; intr->process = SC_PROCESS_NONE; atomic_store_explicit(&intr->interrupted, false, memory_order_relaxed); @@ -37,7 +37,7 @@ sc_intr_set_socket(struct sc_intr *intr, sc_socket socket) { bool sc_intr_set_process(struct sc_intr *intr, sc_pid pid) { - assert(intr->socket == SC_INVALID_SOCKET); + assert(intr->socket == SC_SOCKET_NONE); sc_mutex_lock(&intr->mutex); bool interrupted = @@ -57,13 +57,13 @@ sc_intr_interrupt(struct sc_intr *intr) { atomic_store_explicit(&intr->interrupted, true, memory_order_relaxed); // No more than one component to interrupt - assert(intr->socket == SC_INVALID_SOCKET || + assert(intr->socket == SC_SOCKET_NONE || intr->process == SC_PROCESS_NONE); - if (intr->socket != SC_INVALID_SOCKET) { + if (intr->socket != SC_SOCKET_NONE) { LOGD("Interrupting socket"); net_interrupt(intr->socket); - intr->socket = SC_INVALID_SOCKET; + intr->socket = SC_SOCKET_NONE; } if (intr->process != SC_PROCESS_NONE) { LOGD("Interrupting process"); @@ -76,7 +76,7 @@ sc_intr_interrupt(struct sc_intr *intr) { void sc_intr_destroy(struct sc_intr *intr) { - assert(intr->socket == SC_INVALID_SOCKET); + assert(intr->socket == SC_SOCKET_NONE); assert(intr->process == SC_PROCESS_NONE); sc_mutex_destroy(&intr->mutex); diff --git a/app/src/util/intr.h b/app/src/util/intr.h index 7f0fb9b7..1c20f6df 100644 --- a/app/src/util/intr.h +++ b/app/src/util/intr.h @@ -37,7 +37,7 @@ sc_intr_init(struct sc_intr *intr); /** * Set a socket as the interruptible component * - * Call with SC_INVALID_SOCKET to unset. + * Call with SC_SOCKET_NONE to unset. */ bool sc_intr_set_socket(struct sc_intr *intr, sc_socket socket); diff --git a/app/src/util/net.c b/app/src/util/net.c index 6bfe7a52..8595bc79 100644 --- a/app/src/util/net.c +++ b/app/src/util/net.c @@ -46,13 +46,13 @@ static inline sc_socket wrap(sc_raw_socket sock) { #ifdef __WINDOWS__ if (sock == INVALID_SOCKET) { - return SC_INVALID_SOCKET; + return SC_SOCKET_NONE; } struct sc_socket_windows *socket = malloc(sizeof(*socket)); if (!socket) { closesocket(sock); - return SC_INVALID_SOCKET; + return SC_SOCKET_NONE; } socket->socket = sock; @@ -67,7 +67,7 @@ wrap(sc_raw_socket sock) { static inline sc_raw_socket unwrap(sc_socket socket) { #ifdef __WINDOWS__ - if (socket == SC_INVALID_SOCKET) { + if (socket == SC_SOCKET_NONE) { return INVALID_SOCKET; } @@ -97,7 +97,7 @@ sc_socket net_socket(void) { sc_raw_socket raw_sock = socket(AF_INET, SOCK_STREAM, 0); sc_socket sock = wrap(raw_sock); - if (sock == SC_INVALID_SOCKET) { + if (sock == SC_SOCKET_NONE) { net_perror("socket"); } return sock; @@ -195,7 +195,7 @@ net_send_all(sc_socket socket, const void *buf, size_t len) { bool net_interrupt(sc_socket socket) { - assert(socket != SC_INVALID_SOCKET); + assert(socket != SC_SOCKET_NONE); sc_raw_socket raw_sock = unwrap(socket); diff --git a/app/src/util/net.h b/app/src/util/net.h index 7a9fbe47..57fd6c5e 100644 --- a/app/src/util/net.h +++ b/app/src/util/net.h @@ -11,7 +11,7 @@ # include # include -# define SC_INVALID_SOCKET NULL +# define SC_SOCKET_NONE NULL typedef struct sc_socket_windows { SOCKET socket; atomic_flag closed; @@ -20,7 +20,7 @@ #else // not __WINDOWS__ # include -# define SC_INVALID_SOCKET -1 +# define SC_SOCKET_NONE -1 typedef int sc_socket; #endif diff --git a/app/src/util/net_intr.c b/app/src/util/net_intr.c index b9443e2f..bb70010b 100644 --- a/app/src/util/net_intr.c +++ b/app/src/util/net_intr.c @@ -10,7 +10,7 @@ net_connect_intr(struct sc_intr *intr, sc_socket socket, uint32_t addr, bool ret = net_connect(socket, addr, port); - sc_intr_set_socket(intr, SC_INVALID_SOCKET); + sc_intr_set_socket(intr, SC_SOCKET_NONE); return ret; } @@ -24,7 +24,7 @@ net_listen_intr(struct sc_intr *intr, sc_socket socket, uint32_t addr, bool ret = net_listen(socket, addr, port, backlog); - sc_intr_set_socket(intr, SC_INVALID_SOCKET); + sc_intr_set_socket(intr, SC_SOCKET_NONE); return ret; } @@ -32,12 +32,12 @@ sc_socket net_accept_intr(struct sc_intr *intr, sc_socket server_socket) { if (!sc_intr_set_socket(intr, server_socket)) { // Already interrupted - return SC_INVALID_SOCKET; + return SC_SOCKET_NONE; } sc_socket socket = net_accept(server_socket); - sc_intr_set_socket(intr, SC_INVALID_SOCKET); + sc_intr_set_socket(intr, SC_SOCKET_NONE); return socket; } @@ -50,7 +50,7 @@ net_recv_intr(struct sc_intr *intr, sc_socket socket, void *buf, size_t len) { ssize_t r = net_recv(socket, buf, len); - sc_intr_set_socket(intr, SC_INVALID_SOCKET); + sc_intr_set_socket(intr, SC_SOCKET_NONE); return r; } @@ -64,7 +64,7 @@ net_recv_all_intr(struct sc_intr *intr, sc_socket socket, void *buf, ssize_t r = net_recv_all(socket, buf, len); - sc_intr_set_socket(intr, SC_INVALID_SOCKET); + sc_intr_set_socket(intr, SC_SOCKET_NONE); return r; } @@ -78,7 +78,7 @@ net_send_intr(struct sc_intr *intr, sc_socket socket, const void *buf, ssize_t w = net_send(socket, buf, len); - sc_intr_set_socket(intr, SC_INVALID_SOCKET); + sc_intr_set_socket(intr, SC_SOCKET_NONE); return w; } @@ -92,6 +92,6 @@ net_send_all_intr(struct sc_intr *intr, sc_socket socket, const void *buf, ssize_t w = net_send_all(socket, buf, len); - sc_intr_set_socket(intr, SC_INVALID_SOCKET); + sc_intr_set_socket(intr, SC_SOCKET_NONE); return w; } From 979ce64dc091bc54a0ac8612a0368f5163500f39 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 12 Nov 2021 23:08:19 +0100 Subject: [PATCH 0768/2244] Improve string util API Use prefixed names and improve documentation. --- app/src/adb.c | 6 +- app/src/cli.c | 6 +- app/src/control_msg.c | 2 +- app/src/icon.c | 2 +- app/src/recorder.c | 2 +- app/src/server.c | 2 +- app/src/sys/win/file.c | 4 +- app/src/sys/win/process.c | 4 +- app/src/util/str_util.c | 26 +++---- app/src/util/str_util.h | 107 +++++++++++++++++++---------- app/src/v4l2_sink.c | 2 +- app/tests/test_strutil.c | 140 +++++++++++++++++++------------------- 12 files changed, 169 insertions(+), 134 deletions(-) diff --git a/app/src/adb.c b/app/src/adb.c index c7a64501..39777f12 100644 --- a/app/src/adb.c +++ b/app/src/adb.c @@ -190,11 +190,11 @@ adb_push(const char *serial, const char *local, const char *remote) { #ifdef __WINDOWS__ // Windows will parse the string, so the paths must be quoted // (see sys/win/command.c) - local = strquote(local); + local = sc_str_quote(local); if (!local) { return SC_PROCESS_NONE; } - remote = strquote(remote); + remote = sc_str_quote(remote); if (!remote) { free((void *) local); return SC_PROCESS_NONE; @@ -217,7 +217,7 @@ adb_install(const char *serial, const char *local) { #ifdef __WINDOWS__ // Windows will parse the string, so the local name must be quoted // (see sys/win/command.c) - local = strquote(local); + local = sc_str_quote(local); if (!local) { return SC_PROCESS_NONE; } diff --git a/app/src/cli.c b/app/src/cli.c index e38ff9f6..d46485b2 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -779,9 +779,9 @@ parse_integer_arg(const char *s, long *out, bool accept_suffix, long min, long value; bool ok; if (accept_suffix) { - ok = parse_integer_with_suffix(s, &value); + ok = sc_str_parse_integer_with_suffix(s, &value); } else { - ok = parse_integer(s, &value); + ok = sc_str_parse_integer(s, &value); } if (!ok) { LOGE("Could not parse %s: %s", name, s); @@ -801,7 +801,7 @@ parse_integer_arg(const char *s, long *out, bool accept_suffix, long min, 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); + size_t count = sc_str_parse_integers(s, ':', max_items, out); if (!count) { LOGE("Could not parse %s: %s", name, s); return 0; diff --git a/app/src/control_msg.c b/app/src/control_msg.c index a6a020bd..ea552bc2 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -66,7 +66,7 @@ write_position(uint8_t *buf, const struct sc_position *position) { // write length (2 bytes) + string (non nul-terminated) static size_t write_string(const char *utf8, size_t max_len, unsigned char *buf) { - size_t len = utf8_truncation_index(utf8, max_len); + size_t len = sc_str_utf8_truncation_index(utf8, max_len); buffer_write32be(buf, len); memcpy(&buf[4], utf8, len); return 4 + len; diff --git a/app/src/icon.c b/app/src/icon.c index f9799de7..8e18b102 100644 --- a/app/src/icon.c +++ b/app/src/icon.c @@ -26,7 +26,7 @@ get_icon_path(void) { if (icon_path_env) { // if the envvar is set, use it #ifdef __WINDOWS__ - char *icon_path = utf8_from_wide_char(icon_path_env); + char *icon_path = sc_str_from_wchars(icon_path_env); #else char *icon_path = strdup(icon_path_env); #endif diff --git a/app/src/recorder.c b/app/src/recorder.c index a690e2de..20e4340f 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -26,7 +26,7 @@ find_muxer(const char *name) { oformat = av_oformat_next(oformat); #endif // until null or containing the requested name - } while (oformat && !strlist_contains(oformat->name, ',', name)); + } while (oformat && !sc_str_list_contains(oformat->name, ',', name)); return oformat; } diff --git a/app/src/server.c b/app/src/server.c index d6b143aa..0edd8a05 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -29,7 +29,7 @@ get_server_path(void) { if (server_path_env) { // if the envvar is set, use it #ifdef __WINDOWS__ - char *server_path = utf8_from_wide_char(server_path_env); + char *server_path = sc_str_from_wchars(server_path_env); #else char *server_path = strdup(server_path_env); #endif diff --git a/app/src/sys/win/file.c b/app/src/sys/win/file.c index badb8087..650e0b4b 100644 --- a/app/src/sys/win/file.c +++ b/app/src/sys/win/file.c @@ -19,12 +19,12 @@ sc_file_get_executable_path(void) { return NULL; } buf[len] = '\0'; - return utf8_from_wide_char(buf); + return sc_str_from_wchars(buf); } bool sc_file_is_regular(const char *path) { - wchar_t *wide_path = utf8_to_wide_char(path); + wchar_t *wide_path = sc_str_to_wchars(path); if (!wide_path) { LOGC("Could not allocate wide char string"); return false; diff --git a/app/src/sys/win/process.c b/app/src/sys/win/process.c index 971806d6..cfbb3948 100644 --- a/app/src/sys/win/process.c +++ b/app/src/sys/win/process.c @@ -13,7 +13,7 @@ build_cmd(char *cmd, size_t len, const char *const argv[]) { // // only make it work for this very specific program // (don't handle escaping nor quotes) - size_t ret = xstrjoin(cmd, argv, ' ', len); + size_t ret = sc_str_join(cmd, argv, ' ', len); if (ret >= len) { LOGE("Command too long (%" SC_PRIsizet " chars)", len - 1); return false; @@ -88,7 +88,7 @@ sc_process_execute_p(const char *const argv[], HANDLE *handle, goto error_close_stderr; } - wchar_t *wide = utf8_to_wide_char(cmd); + wchar_t *wide = sc_str_to_wchars(cmd); free(cmd); if (!wide) { LOGC("Could not allocate wide char string"); diff --git a/app/src/util/str_util.c b/app/src/util/str_util.c index b4e7cac4..d15dad20 100644 --- a/app/src/util/str_util.c +++ b/app/src/util/str_util.c @@ -13,7 +13,7 @@ #endif size_t -xstrncpy(char *dest, const char *src, size_t n) { +sc_strncpy(char *dest, const char *src, size_t n) { size_t i; for (i = 0; i < n - 1 && src[i] != '\0'; ++i) dest[i] = src[i]; @@ -23,7 +23,7 @@ xstrncpy(char *dest, const char *src, size_t n) { } size_t -xstrjoin(char *dst, const char *const tokens[], char sep, size_t n) { +sc_str_join(char *dst, const char *const tokens[], char sep, size_t n) { const char *const *remaining = tokens; const char *token = *remaining++; size_t i = 0; @@ -33,7 +33,7 @@ xstrjoin(char *dst, const char *const tokens[], char sep, size_t n) { if (i == n) goto truncated; } - size_t w = xstrncpy(dst + i, token, n - i); + size_t w = sc_strncpy(dst + i, token, n - i); if (w >= n - i) goto truncated; i += w; @@ -47,7 +47,7 @@ truncated: } char * -strquote(const char *src) { +sc_str_quote(const char *src) { size_t len = strlen(src); char *quoted = malloc(len + 3); if (!quoted) { @@ -61,7 +61,7 @@ strquote(const char *src) { } bool -parse_integer(const char *s, long *out) { +sc_str_parse_integer(const char *s, long *out) { char *endptr; if (*s == '\0') { return false; @@ -80,7 +80,8 @@ parse_integer(const char *s, long *out) { } size_t -parse_integers(const char *s, const char sep, size_t max_items, long *out) { +sc_str_parse_integers(const char *s, const char sep, size_t max_items, + long *out) { size_t count = 0; char *endptr; do { @@ -109,7 +110,7 @@ parse_integers(const char *s, const char sep, size_t max_items, long *out) { } bool -parse_integer_with_suffix(const char *s, long *out) { +sc_str_parse_integer_with_suffix(const char *s, long *out) { char *endptr; if (*s == '\0') { return false; @@ -143,7 +144,7 @@ parse_integer_with_suffix(const char *s, long *out) { } bool -strlist_contains(const char *list, char sep, const char *s) { +sc_str_list_contains(const char *list, char sep, const char *s) { char *p; do { p = strchr(list, sep); @@ -161,7 +162,7 @@ strlist_contains(const char *list, char sep, const char *s) { } size_t -utf8_truncation_index(const char *utf8, size_t max_len) { +sc_str_utf8_truncation_index(const char *utf8, size_t max_len) { size_t len = strlen(utf8); if (len <= max_len) { return len; @@ -179,7 +180,7 @@ utf8_truncation_index(const char *utf8, size_t max_len) { #ifdef _WIN32 wchar_t * -utf8_to_wide_char(const char *utf8) { +sc_str_to_wchars(const char *utf8) { int len = MultiByteToWideChar(CP_UTF8, 0, utf8, -1, NULL, 0); if (!len) { return NULL; @@ -195,7 +196,7 @@ utf8_to_wide_char(const char *utf8) { } char * -utf8_from_wide_char(const wchar_t *ws) { +sc_str_from_wchars(const wchar_t *ws) { int len = WideCharToMultiByte(CP_UTF8, 0, ws, -1, NULL, 0, NULL, NULL); if (!len) { return NULL; @@ -212,7 +213,8 @@ utf8_from_wide_char(const wchar_t *ws) { #endif -char *sc_str_wrap_lines(const char *input, unsigned columns, unsigned indent) { +char * +sc_str_wrap_lines(const char *input, unsigned columns, unsigned indent) { assert(indent < columns); struct sc_strbuf buf; diff --git a/app/src/util/str_util.h b/app/src/util/str_util.h index 344522c9..47e01344 100644 --- a/app/src/util/str_util.h +++ b/app/src/util/str_util.h @@ -1,65 +1,97 @@ -#ifndef STRUTIL_H -#define STRUTIL_H +#ifndef SC_STRUTIL_H +#define SC_STRUTIL_H #include "common.h" #include #include -// like strncpy, except: -// - it copies at most n-1 chars -// - the dest string is nul-terminated -// - it does not write useless bytes if strlen(src) < n -// - it returns the number of chars actually written (max n-1) if src has -// been copied completely, or n if src has been truncated +/** + * Like strncpy(), except: + * - it copies at most n-1 chars + * - the dest string is nul-terminated + * - it does not write useless bytes if strlen(src) < n + * - it returns the number of chars actually written (max n-1) if src has + * been copied completely, or n if src has been truncated + */ size_t -xstrncpy(char *dest, const char *src, size_t n); +sc_strncpy(char *dest, const char *src, size_t n); -// join tokens by sep into dst -// returns the number of chars actually written (max n-1) if no truncation -// occurred, or n if truncated +/** + * Join tokens by separator `sep` into `dst` + * + * Return the number of chars actually written (max n-1) if no truncation + * occurred, or n if truncated. + */ size_t -xstrjoin(char *dst, const char *const tokens[], char sep, size_t n); +sc_str_join(char *dst, const char *const tokens[], char sep, size_t n); -// quote a string -// returns the new allocated string, to be freed by the caller +/** + * Quote a string + * + * Return a new allocated string, surrounded with quotes (`"`). + */ char * -strquote(const char *src); +sc_str_quote(const char *src); -// parse s as an integer into value -// returns true if the conversion succeeded, false otherwise +/** + * Parse `s` as an integer into `out` + * + * Return true if the conversion succeeded, false otherwise. + */ bool -parse_integer(const char *s, long *out); +sc_str_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 +/** + * Parse `s` as integers separated by `sep` (for example `1234:2000`) into `out` + * + * 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); +sc_str_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 -// returns true if the conversion succeeded, false otherwise +/** + * Parse `s` as an integer into `out` + * + * Like `sc_str_parse_integer()`, but accept 'k'/'K' (x1000) and 'm'/'M' + * (x1000000) as suffixes. + * + * Return true if the conversion succeeded, false otherwise. + */ bool -parse_integer_with_suffix(const char *s, long *out); +sc_str_parse_integer_with_suffix(const char *s, long *out); -// search s in the list separated by sep -// for example, strlist_contains("a,bc,def", ',', "bc") returns true +/** + * Search `s` in the list separated by `sep` + * + * For example, sc_str_list_contains("a,bc,def", ',', "bc") returns true. + */ bool -strlist_contains(const char *list, char sep, const char *s); +sc_str_list_contains(const char *list, char sep, const char *s); -// return the index to truncate a UTF-8 string at a valid position +/** + * Return the index to truncate a UTF-8 string at a valid position + */ size_t -utf8_truncation_index(const char *utf8, size_t max_len); +sc_str_utf8_truncation_index(const char *utf8, size_t max_len); #ifdef _WIN32 -// convert a UTF-8 string to a wchar_t string -// returns the new allocated string, to be freed by the caller +/** + * Convert a UTF-8 string to a wchar_t string + * + * Return the new allocated string, to be freed by the caller. + */ wchar_t * -utf8_to_wide_char(const char *utf8); +sc_str_to_wchars(const char *utf8); +/** + * Convert a wchar_t string to a UTF-8 string + * + * Return the new allocated string, to be freed by the caller. + */ char * -utf8_from_wide_char(const wchar_t *s); +sc_str_from_wchars(const wchar_t *s); #endif /** @@ -68,6 +100,7 @@ utf8_from_wide_char(const wchar_t *s); * Break input lines at word boundaries (spaces) so that they fit in `columns` * columns, left-indented by `indent` spaces. */ -char *sc_str_wrap_lines(const char *input, unsigned columns, unsigned indent); +char * +sc_str_wrap_lines(const char *input, unsigned columns, unsigned indent); #endif diff --git a/app/src/v4l2_sink.c b/app/src/v4l2_sink.c index 48d90364..a35a9096 100644 --- a/app/src/v4l2_sink.c +++ b/app/src/v4l2_sink.c @@ -21,7 +21,7 @@ find_muxer(const char *name) { oformat = av_oformat_next(oformat); #endif // until null or containing the requested name - } while (oformat && !strlist_contains(oformat->name, ',', name)); + } while (oformat && !sc_str_list_contains(oformat->name, ',', name)); return oformat; } diff --git a/app/tests/test_strutil.c b/app/tests/test_strutil.c index 2d23176e..6a53f94b 100644 --- a/app/tests/test_strutil.c +++ b/app/tests/test_strutil.c @@ -7,9 +7,9 @@ #include "util/str_util.h" -static void test_xstrncpy_simple(void) { +static void test_strncpy_simple(void) { char s[] = "xxxxxxxxxx"; - size_t w = xstrncpy(s, "abcdef", sizeof(s)); + size_t w = sc_strncpy(s, "abcdef", sizeof(s)); // returns strlen of copied string assert(w == 6); @@ -24,9 +24,9 @@ static void test_xstrncpy_simple(void) { assert(!strcmp("abcdef", s)); } -static void test_xstrncpy_just_fit(void) { +static void test_strncpy_just_fit(void) { char s[] = "xxxxxx"; - size_t w = xstrncpy(s, "abcdef", sizeof(s)); + size_t w = sc_strncpy(s, "abcdef", sizeof(s)); // returns strlen of copied string assert(w == 6); @@ -38,9 +38,9 @@ static void test_xstrncpy_just_fit(void) { assert(!strcmp("abcdef", s)); } -static void test_xstrncpy_truncated(void) { +static void test_strncpy_truncated(void) { char s[] = "xxx"; - size_t w = xstrncpy(s, "abcdef", sizeof(s)); + size_t w = sc_strncpy(s, "abcdef", sizeof(s)); // returns 'n' (sizeof(s)) assert(w == 4); @@ -52,10 +52,10 @@ static void test_xstrncpy_truncated(void) { assert(!strncmp("abcdef", s, 3)); } -static void test_xstrjoin_simple(void) { +static void test_join_simple(void) { const char *const tokens[] = { "abc", "de", "fghi", NULL }; char s[] = "xxxxxxxxxxxxxx"; - size_t w = xstrjoin(s, tokens, ' ', sizeof(s)); + size_t w = sc_str_join(s, tokens, ' ', sizeof(s)); // returns strlen of concatenation assert(w == 11); @@ -70,10 +70,10 @@ static void test_xstrjoin_simple(void) { assert(!strcmp("abc de fghi", s)); } -static void test_xstrjoin_just_fit(void) { +static void test_join_just_fit(void) { const char *const tokens[] = { "abc", "de", "fghi", NULL }; char s[] = "xxxxxxxxxxx"; - size_t w = xstrjoin(s, tokens, ' ', sizeof(s)); + size_t w = sc_str_join(s, tokens, ' ', sizeof(s)); // returns strlen of concatenation assert(w == 11); @@ -85,10 +85,10 @@ static void test_xstrjoin_just_fit(void) { assert(!strcmp("abc de fghi", s)); } -static void test_xstrjoin_truncated_in_token(void) { +static void test_join_truncated_in_token(void) { const char *const tokens[] = { "abc", "de", "fghi", NULL }; char s[] = "xxxxx"; - size_t w = xstrjoin(s, tokens, ' ', sizeof(s)); + size_t w = sc_str_join(s, tokens, ' ', sizeof(s)); // returns 'n' (sizeof(s)) assert(w == 6); @@ -100,10 +100,10 @@ static void test_xstrjoin_truncated_in_token(void) { assert(!strcmp("abc d", s)); } -static void test_xstrjoin_truncated_before_sep(void) { +static void test_join_truncated_before_sep(void) { const char *const tokens[] = { "abc", "de", "fghi", NULL }; char s[] = "xxxxxx"; - size_t w = xstrjoin(s, tokens, ' ', sizeof(s)); + size_t w = sc_str_join(s, tokens, ' ', sizeof(s)); // returns 'n' (sizeof(s)) assert(w == 7); @@ -115,10 +115,10 @@ static void test_xstrjoin_truncated_before_sep(void) { assert(!strcmp("abc de", s)); } -static void test_xstrjoin_truncated_after_sep(void) { +static void test_join_truncated_after_sep(void) { const char *const tokens[] = { "abc", "de", "fghi", NULL }; char s[] = "xxxxxxx"; - size_t w = xstrjoin(s, tokens, ' ', sizeof(s)); + size_t w = sc_str_join(s, tokens, ' ', sizeof(s)); // returns 'n' (sizeof(s)) assert(w == 8); @@ -130,9 +130,9 @@ static void test_xstrjoin_truncated_after_sep(void) { assert(!strcmp("abc de ", s)); } -static void test_strquote(void) { +static void test_quote(void) { const char *s = "abcde"; - char *out = strquote(s); + char *out = sc_str_quote(s); // add '"' at the beginning and the end assert(!strcmp("\"abcde\"", out)); @@ -146,71 +146,71 @@ static void test_utf8_truncate(void) { size_t count; - count = utf8_truncation_index(s, 1); + count = sc_str_utf8_truncation_index(s, 1); assert(count == 1); - count = utf8_truncation_index(s, 2); + count = sc_str_utf8_truncation_index(s, 2); assert(count == 1); // É is 2 bytes-wide - count = utf8_truncation_index(s, 3); + count = sc_str_utf8_truncation_index(s, 3); assert(count == 3); - count = utf8_truncation_index(s, 4); + count = sc_str_utf8_truncation_index(s, 4); assert(count == 4); - count = utf8_truncation_index(s, 5); + count = sc_str_utf8_truncation_index(s, 5); assert(count == 4); // Ô is 2 bytes-wide - count = utf8_truncation_index(s, 6); + count = sc_str_utf8_truncation_index(s, 6); assert(count == 6); - count = utf8_truncation_index(s, 7); + count = sc_str_utf8_truncation_index(s, 7); assert(count == 7); - count = utf8_truncation_index(s, 8); + count = sc_str_utf8_truncation_index(s, 8); assert(count == 7); // no more chars } static void test_parse_integer(void) { long value; - bool ok = parse_integer("1234", &value); + bool ok = sc_str_parse_integer("1234", &value); assert(ok); assert(value == 1234); - ok = parse_integer("-1234", &value); + ok = sc_str_parse_integer("-1234", &value); assert(ok); assert(value == -1234); - ok = parse_integer("1234k", &value); + ok = sc_str_parse_integer("1234k", &value); assert(!ok); - ok = parse_integer("123456789876543212345678987654321", &value); + ok = sc_str_parse_integer("123456789876543212345678987654321", &value); assert(!ok); // out-of-range } static void test_parse_integers(void) { long values[5]; - size_t count = parse_integers("1234", ':', 5, values); + size_t count = sc_str_parse_integers("1234", ':', 5, values); assert(count == 1); assert(values[0] == 1234); - count = parse_integers("1234:5678", ':', 5, values); + count = sc_str_parse_integers("1234:5678", ':', 5, values); assert(count == 2); assert(values[0] == 1234); assert(values[1] == 5678); - count = parse_integers("1234:5678", ':', 2, values); + count = sc_str_parse_integers("1234:5678", ':', 2, values); assert(count == 2); assert(values[0] == 1234); assert(values[1] == 5678); - count = parse_integers("1234:-5678", ':', 2, values); + count = sc_str_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); + count = sc_str_parse_integers("1:2:3:4:5", ':', 5, values); assert(count == 5); assert(values[0] == 1); assert(values[1] == 2); @@ -218,85 +218,85 @@ static void test_parse_integers(void) { assert(values[3] == 4); assert(values[4] == 5); - count = parse_integers("1234:5678", ':', 1, values); + count = sc_str_parse_integers("1234:5678", ':', 1, values); assert(count == 0); // max_items == 1 - count = parse_integers("1:2:3:4:5", ':', 3, values); + count = sc_str_parse_integers("1:2:3:4:5", ':', 3, values); assert(count == 0); // max_items == 3 - count = parse_integers(":1234", ':', 5, values); + count = sc_str_parse_integers(":1234", ':', 5, values); assert(count == 0); // invalid - count = parse_integers("1234:", ':', 5, values); + count = sc_str_parse_integers("1234:", ':', 5, values); assert(count == 0); // invalid - count = parse_integers("1234:", ':', 1, values); + count = sc_str_parse_integers("1234:", ':', 1, values); assert(count == 0); // invalid, even when max_items == 1 - count = parse_integers("1234::5678", ':', 5, values); + count = sc_str_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); + bool ok = sc_str_parse_integer_with_suffix("1234", &value); assert(ok); assert(value == 1234); - ok = parse_integer_with_suffix("-1234", &value); + ok = sc_str_parse_integer_with_suffix("-1234", &value); assert(ok); assert(value == -1234); - ok = parse_integer_with_suffix("1234k", &value); + ok = sc_str_parse_integer_with_suffix("1234k", &value); assert(ok); assert(value == 1234000); - ok = parse_integer_with_suffix("1234m", &value); + ok = sc_str_parse_integer_with_suffix("1234m", &value); assert(ok); assert(value == 1234000000); - ok = parse_integer_with_suffix("-1234k", &value); + ok = sc_str_parse_integer_with_suffix("-1234k", &value); assert(ok); assert(value == -1234000); - ok = parse_integer_with_suffix("-1234m", &value); + ok = sc_str_parse_integer_with_suffix("-1234m", &value); assert(ok); assert(value == -1234000000); - ok = parse_integer_with_suffix("123456789876543212345678987654321", &value); + ok = sc_str_parse_integer_with_suffix("123456789876543212345678987654321", &value); assert(!ok); // out-of-range char buf[32]; sprintf(buf, "%ldk", LONG_MAX / 2000); - ok = parse_integer_with_suffix(buf, &value); + ok = sc_str_parse_integer_with_suffix(buf, &value); assert(ok); assert(value == LONG_MAX / 2000 * 1000); sprintf(buf, "%ldm", LONG_MAX / 2000); - ok = parse_integer_with_suffix(buf, &value); + ok = sc_str_parse_integer_with_suffix(buf, &value); assert(!ok); sprintf(buf, "%ldk", LONG_MIN / 2000); - ok = parse_integer_with_suffix(buf, &value); + ok = sc_str_parse_integer_with_suffix(buf, &value); assert(ok); assert(value == LONG_MIN / 2000 * 1000); sprintf(buf, "%ldm", LONG_MIN / 2000); - ok = parse_integer_with_suffix(buf, &value); + ok = sc_str_parse_integer_with_suffix(buf, &value); assert(!ok); } static void test_strlist_contains(void) { - assert(strlist_contains("a,bc,def", ',', "bc")); - assert(!strlist_contains("a,bc,def", ',', "b")); - assert(strlist_contains("", ',', "")); - assert(strlist_contains("abc,", ',', "")); - assert(strlist_contains(",abc", ',', "")); - assert(strlist_contains("abc,,def", ',', "")); - assert(!strlist_contains("abc", ',', "")); - assert(strlist_contains(",,|x", '|', ",,")); - assert(strlist_contains("xyz", '\0', "xyz")); + assert(sc_str_list_contains("a,bc,def", ',', "bc")); + assert(!sc_str_list_contains("a,bc,def", ',', "b")); + assert(sc_str_list_contains("", ',', "")); + assert(sc_str_list_contains("abc,", ',', "")); + assert(sc_str_list_contains(",abc", ',', "")); + assert(sc_str_list_contains("abc,,def", ',', "")); + assert(!sc_str_list_contains("abc", ',', "")); + assert(sc_str_list_contains(",,|x", '|', ",,")); + assert(sc_str_list_contains("xyz", '\0', "xyz")); } static void test_wrap_lines(void) { @@ -341,15 +341,15 @@ int main(int argc, char *argv[]) { (void) argc; (void) argv; - test_xstrncpy_simple(); - test_xstrncpy_just_fit(); - test_xstrncpy_truncated(); - test_xstrjoin_simple(); - test_xstrjoin_just_fit(); - test_xstrjoin_truncated_in_token(); - test_xstrjoin_truncated_before_sep(); - test_xstrjoin_truncated_after_sep(); - test_strquote(); + test_strncpy_simple(); + test_strncpy_just_fit(); + test_strncpy_truncated(); + test_join_simple(); + test_join_just_fit(); + test_join_truncated_in_token(); + test_join_truncated_before_sep(); + test_join_truncated_after_sep(); + test_quote(); test_utf8_truncate(); test_parse_integer(); test_parse_integers(); From 057c7a4df4ec1bd70c660cfe03a590e996c744ad Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 12 Nov 2021 23:12:51 +0100 Subject: [PATCH 0769/2244] Move str_util to str Simplify naming. --- app/meson.build | 12 ++++++------ app/src/adb.c | 2 +- app/src/cli.c | 2 +- app/src/control_msg.c | 2 +- app/src/icon.c | 2 +- app/src/recorder.c | 2 +- app/src/server.c | 2 +- app/src/sys/win/file.c | 2 +- app/src/sys/win/process.c | 2 +- app/src/util/{str_util.c => str.c} | 2 +- app/src/util/{str_util.h => str.h} | 4 ++-- app/src/v4l2_sink.c | 2 +- app/tests/{test_strutil.c => test_str.c} | 2 +- 13 files changed, 19 insertions(+), 19 deletions(-) rename app/src/util/{str_util.c => str.c} (99%) rename app/src/util/{str_util.h => str.h} (98%) rename app/tests/{test_strutil.c => test_str.c} (99%) diff --git a/app/meson.build b/app/meson.build index 7abef1e2..4894babc 100644 --- a/app/meson.build +++ b/app/meson.build @@ -33,7 +33,7 @@ src = [ 'src/util/process.c', 'src/util/process_intr.c', 'src/util/strbuf.c', - 'src/util/str_util.c', + 'src/util/str.c', 'src/util/term.c', 'src/util/thread.c', 'src/util/tick.c', @@ -199,8 +199,8 @@ if get_option('buildtype') == 'debug' 'tests/test_cli.c', 'src/cli.c', 'src/options.c', + 'src/util/str.c', 'src/util/strbuf.c', - 'src/util/str_util.c', 'src/util/term.c', ]], ['test_clock', [ @@ -210,8 +210,8 @@ if get_option('buildtype') == 'debug' ['test_control_msg_serialize', [ 'tests/test_control_msg_serialize.c', 'src/control_msg.c', + 'src/util/str.c', 'src/util/strbuf.c', - 'src/util/str_util.c', ]], ['test_device_msg_deserialize', [ 'tests/test_device_msg_deserialize.c', @@ -224,10 +224,10 @@ if get_option('buildtype') == 'debug' 'tests/test_strbuf.c', 'src/util/strbuf.c', ]], - ['test_strutil', [ - 'tests/test_strutil.c', + ['test_str', [ + 'tests/test_str.c', + 'src/util/str.c', 'src/util/strbuf.c', - 'src/util/str_util.c', ]], ] diff --git a/app/src/adb.c b/app/src/adb.c index 39777f12..6251174e 100644 --- a/app/src/adb.c +++ b/app/src/adb.c @@ -7,7 +7,7 @@ #include "util/file.h" #include "util/log.h" -#include "util/str_util.h" +#include "util/str.h" static const char *adb_command; diff --git a/app/src/cli.c b/app/src/cli.c index d46485b2..1550c706 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -9,8 +9,8 @@ #include "options.h" #include "util/log.h" +#include "util/str.h" #include "util/strbuf.h" -#include "util/str_util.h" #include "util/term.h" #define STR_IMPL_(x) #x diff --git a/app/src/control_msg.c b/app/src/control_msg.c index ea552bc2..74e3315c 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -7,7 +7,7 @@ #include "util/buffer_util.h" #include "util/log.h" -#include "util/str_util.h" +#include "util/str.h" /** * Map an enum value to a string based on an array, without crashing on an diff --git a/app/src/icon.c b/app/src/icon.c index 8e18b102..a3efbb01 100644 --- a/app/src/icon.c +++ b/app/src/icon.c @@ -10,7 +10,7 @@ #include "compat.h" #include "util/file.h" #include "util/log.h" -#include "util/str_util.h" +#include "util/str.h" #define SCRCPY_PORTABLE_ICON_FILENAME "icon.png" #define SCRCPY_DEFAULT_ICON_PATH \ diff --git a/app/src/recorder.c b/app/src/recorder.c index 20e4340f..b9c585f4 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -6,7 +6,7 @@ #include #include "util/log.h" -#include "util/str_util.h" +#include "util/str.h" /** Downcast packet_sink to recorder */ #define DOWNCAST(SINK) container_of(SINK, struct recorder, packet_sink) diff --git a/app/src/server.c b/app/src/server.c index 0edd8a05..29c92eb3 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -12,7 +12,7 @@ #include "util/log.h" #include "util/net_intr.h" #include "util/process_intr.h" -#include "util/str_util.h" +#include "util/str.h" #define SERVER_FILENAME "scrcpy-server" diff --git a/app/src/sys/win/file.c b/app/src/sys/win/file.c index 650e0b4b..5233b177 100644 --- a/app/src/sys/win/file.c +++ b/app/src/sys/win/file.c @@ -5,7 +5,7 @@ #include #include "util/log.h" -#include "util/str_util.h" +#include "util/str.h" char * sc_file_get_executable_path(void) { diff --git a/app/src/sys/win/process.c b/app/src/sys/win/process.c index cfbb3948..4dcd542e 100644 --- a/app/src/sys/win/process.c +++ b/app/src/sys/win/process.c @@ -3,7 +3,7 @@ #include #include "util/log.h" -#include "util/str_util.h" +#include "util/str.h" #define CMD_MAX_LEN 8192 diff --git a/app/src/util/str_util.c b/app/src/util/str.c similarity index 99% rename from app/src/util/str_util.c rename to app/src/util/str.c index d15dad20..7935c6bb 100644 --- a/app/src/util/str_util.c +++ b/app/src/util/str.c @@ -1,4 +1,4 @@ -#include "str_util.h" +#include "str.h" #include #include diff --git a/app/src/util/str_util.h b/app/src/util/str.h similarity index 98% rename from app/src/util/str_util.h rename to app/src/util/str.h index 47e01344..54e32808 100644 --- a/app/src/util/str_util.h +++ b/app/src/util/str.h @@ -1,5 +1,5 @@ -#ifndef SC_STRUTIL_H -#define SC_STRUTIL_H +#ifndef SC_STR_H +#define SC_STR_H #include "common.h" diff --git a/app/src/v4l2_sink.c b/app/src/v4l2_sink.c index a35a9096..753d5b6a 100644 --- a/app/src/v4l2_sink.c +++ b/app/src/v4l2_sink.c @@ -1,7 +1,7 @@ #include "v4l2_sink.h" #include "util/log.h" -#include "util/str_util.h" +#include "util/str.h" /** Downcast frame_sink to sc_v4l2_sink */ #define DOWNCAST(SINK) container_of(SINK, struct sc_v4l2_sink, frame_sink) diff --git a/app/tests/test_strutil.c b/app/tests/test_str.c similarity index 99% rename from app/tests/test_strutil.c rename to app/tests/test_str.c index 6a53f94b..2b030885 100644 --- a/app/tests/test_strutil.c +++ b/app/tests/test_str.c @@ -5,7 +5,7 @@ #include #include -#include "util/str_util.h" +#include "util/str.h" static void test_strncpy_simple(void) { char s[] = "xxxxxxxxxx"; From c1a34881d733dd91d2c4d2184ad4b89ffee8314e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 12 Nov 2021 23:24:12 +0100 Subject: [PATCH 0770/2244] Use sc_ prefix for server --- app/src/scrcpy.c | 28 ++++++++--------- app/src/server.c | 82 +++++++++++++++++++++++++----------------------- app/src/server.h | 34 ++++++++++---------- 3 files changed, 73 insertions(+), 71 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 38a50a0b..9643b04e 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -34,7 +34,7 @@ #endif struct scrcpy { - struct server server; + struct sc_server server; struct screen screen; struct stream stream; struct decoder decoder; @@ -286,7 +286,7 @@ stream_on_eos(struct stream *stream, void *userdata) { } static void -server_on_connection_failed(struct server *server, void *userdata) { +sc_server_on_connection_failed(struct sc_server *server, void *userdata) { (void) server; (void) userdata; @@ -294,7 +294,7 @@ server_on_connection_failed(struct server *server, void *userdata) { } static void -server_on_connected(struct server *server, void *userdata) { +sc_server_on_connected(struct sc_server *server, void *userdata) { (void) server; (void) userdata; @@ -302,7 +302,7 @@ server_on_connected(struct server *server, void *userdata) { } static void -server_on_disconnected(struct server *server, void *userdata) { +sc_server_on_disconnected(struct sc_server *server, void *userdata) { (void) server; (void) userdata; @@ -340,7 +340,7 @@ scrcpy(struct scrcpy_options *options) { bool controller_started = false; bool screen_initialized = false; - struct server_params params = { + struct sc_server_params params = { .serial = options->serial, .log_level = options->log_level, .crop = options->crop, @@ -359,16 +359,16 @@ scrcpy(struct scrcpy_options *options) { .power_off_on_close = options->power_off_on_close, }; - static const struct server_callbacks cbs = { - .on_connection_failed = server_on_connection_failed, - .on_connected = server_on_connected, - .on_disconnected = server_on_disconnected, + static const struct sc_server_callbacks cbs = { + .on_connection_failed = sc_server_on_connection_failed, + .on_connected = sc_server_on_connected, + .on_disconnected = sc_server_on_disconnected, }; - if (!server_init(&s->server, ¶ms, &cbs, NULL)) { + if (!sc_server_init(&s->server, ¶ms, &cbs, NULL)) { return false; } - if (!server_start(&s->server)) { + if (!sc_server_start(&s->server)) { goto end; } @@ -392,7 +392,7 @@ scrcpy(struct scrcpy_options *options) { } // It is necessarily initialized here, since the device is connected - struct server_info *info = &s->server.info; + struct sc_server_info *info = &s->server.info; if (options->display && options->control) { if (!file_handler_init(&s->file_handler, options->serial, @@ -608,7 +608,7 @@ end: if (server_started) { // shutdown the sockets and kill the server - server_stop(&s->server); + sc_server_stop(&s->server); } // now that the sockets are shutdown, the stream and controller are @@ -653,7 +653,7 @@ end: file_handler_destroy(&s->file_handler); } - server_destroy(&s->server); + sc_server_destroy(&s->server); return ret; } diff --git a/app/src/server.c b/app/src/server.c index 29c92eb3..58247a72 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -14,10 +14,10 @@ #include "util/process_intr.h" #include "util/str.h" -#define SERVER_FILENAME "scrcpy-server" +#define SC_SERVER_FILENAME "scrcpy-server" -#define DEFAULT_SERVER_PATH PREFIX "/share/scrcpy/" SERVER_FILENAME -#define DEVICE_SERVER_PATH "/data/local/tmp/scrcpy-server.jar" +#define SC_SERVER_PATH_DEFAULT PREFIX "/share/scrcpy/" SC_SERVER_FILENAME +#define SC_DEVICE_SERVER_PATH "/data/local/tmp/scrcpy-server.jar" static char * get_server_path(void) { @@ -42,18 +42,18 @@ get_server_path(void) { } #ifndef PORTABLE - LOGD("Using server: " DEFAULT_SERVER_PATH); - char *server_path = strdup(DEFAULT_SERVER_PATH); + LOGD("Using server: " SC_SERVER_PATH_DEFAULT); + char *server_path = strdup(SC_SERVER_PATH_DEFAULT); if (!server_path) { LOGE("Could not allocate memory"); return NULL; } #else - char *server_path = sc_file_get_local_path(SERVER_FILENAME); + char *server_path = sc_file_get_local_path(SC_SERVER_FILENAME); if (!server_path) { LOGE("Could not get local file path, " - "using " SERVER_FILENAME " from current directory"); - return strdup(SERVER_FILENAME); + "using " SC_SERVER_FILENAME " from current directory"); + return strdup(SC_SERVER_FILENAME); } LOGD("Using server (portable): %s", server_path); @@ -63,7 +63,7 @@ get_server_path(void) { } static void -server_params_destroy(struct server_params *params) { +sc_server_params_destroy(struct sc_server_params *params) { // The server stores a copy of the params provided by the user free((char *) params->serial); free((char *) params->crop); @@ -72,7 +72,8 @@ server_params_destroy(struct server_params *params) { } static bool -server_params_copy(struct server_params *dst, const struct server_params *src) { +sc_server_params_copy(struct sc_server_params *dst, + const struct sc_server_params *src) { *dst = *src; // The params reference user-allocated memory, so we must copy them to @@ -96,7 +97,7 @@ server_params_copy(struct server_params *dst, const struct server_params *src) { return true; error: - server_params_destroy(dst); + sc_server_params_destroy(dst); return false; } @@ -111,7 +112,7 @@ push_server(struct sc_intr *intr, const char *serial) { free(server_path); return false; } - sc_pid pid = adb_push(serial, server_path, DEVICE_SERVER_PATH); + sc_pid pid = adb_push(serial, server_path, SC_DEVICE_SERVER_PATH); free(server_path); return sc_process_check_success_intr(intr, pid, "adb push"); } @@ -136,7 +137,8 @@ log_level_to_server_string(enum sc_log_level level) { } static sc_pid -execute_server(struct server *server, const struct server_params *params) { +execute_server(struct sc_server *server, + const struct sc_server_params *params) { const char *serial = server->params.serial; char max_size_string[6]; @@ -152,7 +154,7 @@ execute_server(struct server *server, const struct server_params *params) { sprintf(display_id_string, "%"PRIu32, params->display_id); const char *const cmd[] = { "shell", - "CLASSPATH=" DEVICE_SERVER_PATH, + "CLASSPATH=" SC_DEVICE_SERVER_PATH, "app_process", #ifdef SERVER_DEBUGGER # define SERVER_DEBUGGER_PORT "5005" @@ -218,7 +220,7 @@ connect_and_read_byte(struct sc_intr *intr, sc_socket socket, uint16_t port) { } static sc_socket -connect_to_server(struct server *server, uint32_t attempts, sc_tick delay) { +connect_to_server(struct sc_server *server, uint32_t attempts, sc_tick delay) { uint16_t port = server->tunnel.local_port; do { LOGD("Remaining connection attempts: %d", (int) attempts); @@ -253,9 +255,9 @@ connect_to_server(struct server *server, uint32_t attempts, sc_tick delay) { } bool -server_init(struct server *server, const struct server_params *params, - const struct server_callbacks *cbs, void *cbs_userdata) { - bool ok = server_params_copy(&server->params, params); +sc_server_init(struct sc_server *server, const struct sc_server_params *params, + const struct sc_server_callbacks *cbs, void *cbs_userdata) { + bool ok = sc_server_params_copy(&server->params, params); if (!ok) { LOGE("Could not copy server params"); return false; @@ -264,7 +266,7 @@ server_init(struct server *server, const struct server_params *params, ok = sc_mutex_init(&server->mutex); if (!ok) { LOGE("Could not create server mutex"); - server_params_destroy(&server->params); + sc_server_params_destroy(&server->params); return false; } @@ -272,7 +274,7 @@ server_init(struct server *server, const struct server_params *params, if (!ok) { LOGE("Could not create server cond_stopped"); sc_mutex_destroy(&server->mutex); - server_params_destroy(&server->params); + sc_server_params_destroy(&server->params); return false; } @@ -281,7 +283,7 @@ server_init(struct server *server, const struct server_params *params, LOGE("Could not create intr"); sc_cond_destroy(&server->cond_stopped); sc_mutex_destroy(&server->mutex); - server_params_destroy(&server->params); + sc_server_params_destroy(&server->params); return false; } @@ -305,26 +307,26 @@ server_init(struct server *server, const struct server_params *params, static bool device_read_info(struct sc_intr *intr, sc_socket device_socket, - struct server_info *info) { - unsigned char buf[DEVICE_NAME_FIELD_LENGTH + 4]; + struct sc_server_info *info) { + unsigned char buf[SC_DEVICE_NAME_FIELD_LENGTH + 4]; ssize_t r = net_recv_all_intr(intr, device_socket, buf, sizeof(buf)); - if (r < DEVICE_NAME_FIELD_LENGTH + 4) { + if (r < SC_DEVICE_NAME_FIELD_LENGTH + 4) { LOGE("Could not retrieve device information"); return false; } // in case the client sends garbage - buf[DEVICE_NAME_FIELD_LENGTH - 1] = '\0'; + buf[SC_DEVICE_NAME_FIELD_LENGTH - 1] = '\0'; memcpy(info->device_name, (char *) buf, sizeof(info->device_name)); - info->frame_size.width = (buf[DEVICE_NAME_FIELD_LENGTH] << 8) - | buf[DEVICE_NAME_FIELD_LENGTH + 1]; - info->frame_size.height = (buf[DEVICE_NAME_FIELD_LENGTH + 2] << 8) - | buf[DEVICE_NAME_FIELD_LENGTH + 3]; + info->frame_size.width = (buf[SC_DEVICE_NAME_FIELD_LENGTH] << 8) + | buf[SC_DEVICE_NAME_FIELD_LENGTH + 1]; + info->frame_size.height = (buf[SC_DEVICE_NAME_FIELD_LENGTH + 2] << 8) + | buf[SC_DEVICE_NAME_FIELD_LENGTH + 3]; return true; } static bool -server_connect_to(struct server *server, struct server_info *info) { +sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) { struct sc_adb_tunnel *tunnel = &server->tunnel; assert(tunnel->enabled); @@ -400,8 +402,8 @@ fail: } static void -server_on_terminated(void *userdata) { - struct server *server = userdata; +sc_server_on_terminated(void *userdata) { + struct sc_server *server = userdata; // If the server process dies before connecting to the server socket, // then the client will be stuck forever on accept(). To avoid the problem, @@ -416,9 +418,9 @@ server_on_terminated(void *userdata) { static int run_server(void *data) { - struct server *server = data; + struct sc_server *server = data; - const struct server_params *params = &server->params; + const struct sc_server_params *params = &server->params; bool ok = push_server(&server->intr, params->serial); if (!ok) { @@ -439,7 +441,7 @@ run_server(void *data) { } static const struct sc_process_listener listener = { - .on_terminated = server_on_terminated, + .on_terminated = sc_server_on_terminated, }; struct sc_process_observer observer; ok = sc_process_observer_init(&observer, pid, &listener, server); @@ -450,7 +452,7 @@ run_server(void *data) { goto error_connection_failed; } - ok = server_connect_to(server, &server->info); + ok = sc_server_connect_to(server, &server->info); // The tunnel is always closed by server_connect_to() if (!ok) { sc_process_terminate(pid); @@ -499,7 +501,7 @@ error_connection_failed: } bool -server_start(struct server *server) { +sc_server_start(struct sc_server *server) { bool ok = sc_thread_create(&server->thread, run_server, "server", server); if (!ok) { LOGE("Could not create server thread"); @@ -510,7 +512,7 @@ server_start(struct server *server) { } void -server_stop(struct server *server) { +sc_server_stop(struct sc_server *server) { sc_mutex_lock(&server->mutex); server->stopped = true; sc_cond_signal(&server->cond_stopped); @@ -521,8 +523,8 @@ server_stop(struct server *server) { } void -server_destroy(struct server *server) { - server_params_destroy(&server->params); +sc_server_destroy(struct sc_server *server) { + sc_server_params_destroy(&server->params); sc_intr_destroy(&server->intr); sc_cond_destroy(&server->cond_stopped); sc_mutex_destroy(&server->mutex); diff --git a/app/src/server.h b/app/src/server.h index cb2cf3a8..3859c328 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -16,13 +16,13 @@ #include "util/net.h" #include "util/thread.h" -#define DEVICE_NAME_FIELD_LENGTH 64 -struct server_info { - char device_name[DEVICE_NAME_FIELD_LENGTH]; +#define SC_DEVICE_NAME_FIELD_LENGTH 64 +struct sc_server_info { + char device_name[SC_DEVICE_NAME_FIELD_LENGTH]; struct sc_size frame_size; }; -struct server_params { +struct sc_server_params { const char *serial; enum sc_log_level log_level; const char *crop; @@ -41,12 +41,12 @@ struct server_params { bool power_off_on_close; }; -struct server { +struct sc_server { // The internal allocated strings are copies owned by the server - struct server_params params; + struct sc_server_params params; sc_thread thread; - struct server_info info; // initialized once connected + struct sc_server_info info; // initialized once connected sc_mutex mutex; sc_cond cond_stopped; @@ -58,45 +58,45 @@ struct server { sc_socket video_socket; sc_socket control_socket; - const struct server_callbacks *cbs; + const struct sc_server_callbacks *cbs; void *cbs_userdata; }; -struct server_callbacks { +struct sc_server_callbacks { /** * Called when the server failed to connect * * If it is called, then on_connected() and on_disconnected() will never be * called. */ - void (*on_connection_failed)(struct server *server, void *userdata); + void (*on_connection_failed)(struct sc_server *server, void *userdata); /** * Called on server connection */ - void (*on_connected)(struct server *server, void *userdata); + void (*on_connected)(struct sc_server *server, void *userdata); /** * Called on server disconnection (after it has been connected) */ - void (*on_disconnected)(struct server *server, void *userdata); + void (*on_disconnected)(struct sc_server *server, void *userdata); }; // init the server with the given params bool -server_init(struct server *server, const struct server_params *params, - const struct server_callbacks *cbs, void *cbs_userdata); +sc_server_init(struct sc_server *server, const struct sc_server_params *params, + const struct sc_server_callbacks *cbs, void *cbs_userdata); // start the server asynchronously bool -server_start(struct server *server); +sc_server_start(struct sc_server *server); // disconnect and kill the server process void -server_stop(struct server *server); +sc_server_stop(struct sc_server *server); // close and release sockets void -server_destroy(struct server *server); +sc_server_destroy(struct sc_server *server); #endif From 45b0f8123a52f5c73a5860d616f4ceba2766ca6a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 13 Nov 2021 20:59:01 +0100 Subject: [PATCH 0771/2244] Increase delay to inject HID on Ctrl+v 2 milliseconds turn out to be insufficient sometimes. It seems that 5 milliseconds is enough (and still not noticeable). --- app/src/hid_keyboard.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/hid_keyboard.c b/app/src/hid_keyboard.c index c6fba21c..3ac1a441 100644 --- a/app/src/hid_keyboard.c +++ b/app/src/hid_keyboard.c @@ -307,7 +307,7 @@ sc_key_processor_process_key(struct sc_key_processor *kp, // requested. Wait a bit so that the clipboard is set before // injecting Ctrl+v via HID, otherwise it would paste the old // clipboard content. - hid_event.delay = SC_TICK_FROM_MS(2); + hid_event.delay = SC_TICK_FROM_MS(5); } if (!sc_aoa_push_hid_event(kb->aoa, &hid_event)) { From 1d97d77032f62cc48e6eb03e3c77d68951b5cb92 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 13 Nov 2021 09:39:26 +0100 Subject: [PATCH 0772/2244] Improve scrcpy presentation in README --- README.md | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index a0ab271c..1bfaff7c 100644 --- a/README.md +++ b/README.md @@ -5,22 +5,33 @@ [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. +USB (or [over TCP/IP](#wireless)). It does not require any _root_ access. It works on _GNU/Linux_, _Windows_ and _macOS_. ![screenshot](assets/screenshot-debian-600.jpg) It focuses on: - - **lightness** (native, displays only the device screen) - - **performance** (30~60fps) - - **quality** (1920×1080 or above) - - **low latency** ([35~70ms][lowlatency]) - - **low startup time** (~1 second to display the first image) - - **non-intrusiveness** (nothing is left installed on the device) + - **lightness**: native, displays only the device screen + - **performance**: 30~120fps, depending on the device + - **quality**: 1920×1080 or above + - **low latency**: [35~70ms][lowlatency] + - **low startup time**: ~1 second to display the first image + - **non-intrusiveness**: nothing is left installed on the device + - **user benefits**: no account, no ads, no internet required + - **freedom**: free and open source software [lowlatency]: https://github.com/Genymobile/scrcpy/pull/646 +Its features include: + - [recording](#recording) + - mirroring with [device screen off](#turn-screen-off) + - [copy-paste](#copy-paste) in both directions + - [configurable quality](#capture-configuration) + - device screen [as a webcam (V4L2)](#v4l2loopback) (Linux-only) + - [physical keyboard simulation (HID)](#physical-keyboard-simulation-hid) + (Linux-only) + - and more… ## Requirements From 99a4a4847711c4ab02dac63550bda8435330d338 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 13 Nov 2021 19:33:18 +0100 Subject: [PATCH 0773/2244] Replace "connected on" to "connected via" I'm not sure "connected on USB" is correct. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1bfaff7c..7d2d3e0b 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [Read in another language](#translations) -This application provides display and control of Android devices connected on +This application provides display and control of Android devices connected via USB (or [over TCP/IP](#wireless)). It does not require any _root_ access. It works on _GNU/Linux_, _Windows_ and _macOS_. From 4e811a4a685aba17c517db1f68e434dcfa907d5b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 13 Nov 2021 19:17:30 +0100 Subject: [PATCH 0774/2244] Adapt icon in README Use SVG format, align to the right, and reduce its size. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7d2d3e0b..7b1ad410 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # scrcpy (v1.19) -![icon](data/icon.png) +scrcpy [Read in another language](#translations) From 57387fa7077850f11b9ab6ce699cb5c7876964d6 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 13 Nov 2021 23:20:06 +0100 Subject: [PATCH 0775/2244] Mention crash on Android 12 on old scrcpy in FAQ --- FAQ.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/FAQ.md b/FAQ.md index a1c2fb76..fdc6e1ae 100644 --- a/FAQ.md +++ b/FAQ.md @@ -222,6 +222,27 @@ scrcpy -m 800 You could also try another [encoder](README.md#encoder). +If you encounter this exception on Android 12, then just upgrade to scrcpy >= +1.18 (see [#2129]): + +``` +> ERROR: Exception on thread Thread[main,5,main] +java.lang.AssertionError: java.lang.reflect.InvocationTargetException + at com.genymobile.scrcpy.wrappers.SurfaceControl.setDisplaySurface(SurfaceControl.java:75) + ... +Caused by: java.lang.reflect.InvocationTargetException + at java.lang.reflect.Method.invoke(Native Method) + at com.genymobile.scrcpy.wrappers.SurfaceControl.setDisplaySurface(SurfaceControl.java:73) + ... 7 more +Caused by: java.lang.IllegalArgumentException: displayToken must not be null + at android.view.SurfaceControl$Transaction.setDisplaySurface(SurfaceControl.java:3067) + at android.view.SurfaceControl.setDisplaySurface(SurfaceControl.java:2147) + ... 9 more +``` + +[#2129]: https://github.com/Genymobile/scrcpy/issues/2129 + + ## Command line on Windows Some Windows users are not familiar with the command line. Here is how to open a From 1be5daf999a59cd4b70fbf8e4745217f90eb4e03 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 13 Nov 2021 23:57:18 +0100 Subject: [PATCH 0776/2244] Fix word order in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7b1ad410..337e2944 100644 --- a/README.md +++ b/README.md @@ -697,7 +697,7 @@ a location inverted through the center of the screen. By default, scrcpy uses Android key or text injection: it works everywhere, but is limited to ASCII. -On Linux, scrcpy can simulate a USB physical keyboard on Android to provide a +On Linux, scrcpy can simulate a physical USB keyboard on Android to provide a better input experience (using [USB HID over AOAv2][hid-aoav2]): the virtual keyboard is disabled and it works for all characters and IME. From 1bb0df5da17a08473a7bdf2e6419a0f0966aa023 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 14 Nov 2021 00:34:12 +0100 Subject: [PATCH 0777/2244] Extract ANDROID_JAR path in build script This will allow to reuse it. --- server/build_without_gradle.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index 05b822ba..926c19f5 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -21,6 +21,7 @@ BUILD_DIR="$(realpath ${BUILD_DIR:-build_manual})" CLASSES_DIR="$BUILD_DIR/classes" SERVER_DIR=$(dirname "$0") SERVER_BINARY=scrcpy-server +ANDROID_JAR="$ANDROID_HOME/platforms/android-$PLATFORM/android.jar" echo "Platform: android-$PLATFORM" echo "Build-tools: $BUILD_TOOLS" @@ -47,8 +48,8 @@ cd "$SERVER_DIR/src/main/aidl" echo "Compiling java sources..." cd ../java -javac -bootclasspath "$ANDROID_HOME/platforms/android-$PLATFORM/android.jar" \ - -cp "$CLASSES_DIR" -d "$CLASSES_DIR" -source 1.8 -target 1.8 \ +javac -bootclasspath "$ANDROID_JAR" -cp "$CLASSES_DIR" -d "$CLASSES_DIR" \ + -source 1.8 -target 1.8 \ com/genymobile/scrcpy/*.java \ com/genymobile/scrcpy/wrappers/*.java From 52138fd9213216a21fbdcda4d430394d7c8f0979 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 14 Nov 2021 00:23:36 +0100 Subject: [PATCH 0778/2244] Update script to build without gradle to SDK 31 Build tools 31.x.x do not ship dx anymore. Use d8 instead. Refs 8bf28e9f5340d2a54dbc9b2c9d924b7ee4fa8d31 --- server/build_without_gradle.sh | 42 ++++++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index 926c19f5..c33ee47a 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -14,8 +14,9 @@ set -e SCRCPY_DEBUG=false SCRCPY_VERSION_NAME=1.19 -PLATFORM=${ANDROID_PLATFORM:-30} -BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-30.0.0} +PLATFORM_VERSION=31 +PLATFORM=${ANDROID_PLATFORM:-$PLATFORM_VERSION} +BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-31.0.0} BUILD_DIR="$(realpath ${BUILD_DIR:-build_manual})" CLASSES_DIR="$BUILD_DIR/classes" @@ -55,16 +56,33 @@ javac -bootclasspath "$ANDROID_JAR" -cp "$CLASSES_DIR" -d "$CLASSES_DIR" \ echo "Dexing..." 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 -echo "Archiving..." -cd "$BUILD_DIR" -jar cvf "$SERVER_BINARY" classes.dex -rm -rf classes.dex classes +if [[ $PLATFORM_VERSION -lt 31 ]] +then + # use dx + "$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 + + echo "Archiving..." + cd "$BUILD_DIR" + jar cvf "$SERVER_BINARY" classes.dex + rm -rf classes.dex classes +else + # use d8 + "$ANDROID_HOME/build-tools/$BUILD_TOOLS/d8" --classpath "$ANDROID_JAR" \ + --output "$BUILD_DIR/classes.zip" \ + android/view/*.class \ + android/content/*.class \ + com/genymobile/scrcpy/*.class \ + com/genymobile/scrcpy/wrappers/*.class + + cd "$BUILD_DIR" + mv classes.zip "$SERVER_BINARY" + rm -rf classes +fi echo "Server generated in $BUILD_DIR/$SERVER_BINARY" From a045e28df83711ea5ea10d8dc9f8dfaf781a022a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 14 Nov 2021 00:22:12 +0100 Subject: [PATCH 0779/2244] Bump version to 1.20 --- 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 fc61bcea..7a814212 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('scrcpy', 'c', - version: '1.19', + version: '1.20', meson_version: '>= 0.48', default_options: [ 'c_std=c11', diff --git a/server/build.gradle b/server/build.gradle index cc03ecc8..52781a29 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -6,8 +6,8 @@ android { applicationId "com.genymobile.scrcpy" minSdkVersion 21 targetSdkVersion 31 - versionCode 11900 - versionName "1.19" + versionCode 12000 + versionName "1.20" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index c33ee47a..61b42103 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.19 +SCRCPY_VERSION_NAME=1.20 PLATFORM_VERSION=31 PLATFORM=${ANDROID_PLATFORM:-$PLATFORM_VERSION} From 65b023ac6d586593193fd5290f65e25603b68e02 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 14 Nov 2021 01:51:32 +0100 Subject: [PATCH 0780/2244] Update links to v1.20 --- BUILD.md | 6 +++--- README.md | 8 ++++---- install_release.sh | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/BUILD.md b/BUILD.md index 5edb5a21..9eda3715 100644 --- a/BUILD.md +++ b/BUILD.md @@ -270,10 +270,10 @@ install` must be run as root)._ #### Option 2: Use prebuilt server - - [`scrcpy-server-v1.19`][direct-scrcpy-server] - _(SHA-256: 876f9322182e6aac6a58db1334f4225855ef3a17eaebc80aab6601d9d1ecb867)_ + - [`scrcpy-server-v1.20`][direct-scrcpy-server] + _(SHA-256: b20aee4951f99b060c4a44000ba94de973f9604758ef62beb253b371aad3df34)_ -[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.19/scrcpy-server-v1.19 +[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.20/scrcpy-server-v1.20 Download the prebuilt server somewhere, and specify its path during the Meson configuration: diff --git a/README.md b/README.md index 2b52cf72..c9080861 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# scrcpy (v1.19) +# scrcpy (v1.20) scrcpy @@ -101,10 +101,10 @@ process][BUILD_simple]). For Windows, for simplicity, a prebuilt archive with all the dependencies (including `adb`) is available: - - [`scrcpy-win64-v1.19.zip`][direct-win64] - _(SHA-256: 383d6483f25ac0092d4bb9fef6c967351ecd50fc248e0c82932db97d6d32f11b)_ + - [`scrcpy-win64-v1.20.zip`][direct-win64] + _(SHA-256: 548532b616288bcaeceff6881ad5e6f0928e5ae2b48c380385f03627401cfdba)_ -[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.19/scrcpy-win64-v1.19.zip +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.20/scrcpy-win64-v1.20.zip It is also available in [Chocolatey]: diff --git a/install_release.sh b/install_release.sh index dcb254f9..e12b4469 100755 --- a/install_release.sh +++ b/install_release.sh @@ -2,8 +2,8 @@ set -e BUILDDIR=build-auto -PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v1.19/scrcpy-server-v1.19 -PREBUILT_SERVER_SHA256=876f9322182e6aac6a58db1334f4225855ef3a17eaebc80aab6601d9d1ecb867 +PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v1.20/scrcpy-server-v1.20 +PREBUILT_SERVER_SHA256=b20aee4951f99b060c4a44000ba94de973f9604758ef62beb253b371aad3df34 echo "[scrcpy] Downloading prebuilt server..." wget "$PREBUILT_SERVER_URL" -O scrcpy-server From 739ff9dce011238844dc00e4e245afbb0561928d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 14 Nov 2021 15:15:34 +0100 Subject: [PATCH 0781/2244] Fix compilation errors with old SDL versions SDL_PixelFormatEnum has been introduced in SDL 2.0.10: SDL_PIXELFORMAT_BGR444 has been introduced in SDL 2.0.12: Fixes #2777 PR #2781 Reviewed-by: Yu-Chen Lin --- app/src/icon.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/src/icon.c b/app/src/icon.c index a3efbb01..2616007e 100644 --- a/app/src/icon.c +++ b/app/src/icon.c @@ -158,6 +158,12 @@ free_ctx: return result; } +#if !SDL_VERSION_ATLEAST(2, 0, 10) +// SDL_PixelFormatEnum has been introduced in SDL 2.0.10. Use int for older SDL +// versions. +typedef int SDL_PixelFormatEnum; +#endif + static SDL_PixelFormatEnum to_sdl_pixel_format(enum AVPixelFormat fmt) { switch (fmt) { @@ -172,7 +178,9 @@ to_sdl_pixel_format(enum AVPixelFormat fmt) { case AV_PIX_FMT_BGR565BE: return SDL_PIXELFORMAT_BGR565; case AV_PIX_FMT_BGR555BE: return SDL_PIXELFORMAT_BGR555; case AV_PIX_FMT_RGB444BE: return SDL_PIXELFORMAT_RGB444; +#if SDL_VERSION_ATLEAST(2, 0, 12) case AV_PIX_FMT_BGR444BE: return SDL_PIXELFORMAT_BGR444; +#endif case AV_PIX_FMT_PAL8: return SDL_PIXELFORMAT_INDEX8; default: return SDL_PIXELFORMAT_UNKNOWN; } From 6a27062f486a1abba07defac4ca0458b0e43ecb6 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 14 Nov 2021 15:39:20 +0100 Subject: [PATCH 0782/2244] Stop connection attempts if interrupted If the interruptor is interrupted, every network call will fail, but the retry-on-error mechanism must also be stopped. --- app/src/server.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/server.c b/app/src/server.c index 58247a72..d792364d 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -234,6 +234,12 @@ connect_to_server(struct sc_server *server, uint32_t attempts, sc_tick delay) { net_close(socket); } + + if (sc_intr_is_interrupted(&server->intr)) { + // Stop immediately + break; + } + if (attempts) { sc_mutex_lock(&server->mutex); sc_tick deadline = sc_tick_now() + delay; From 52cebe1597cef201f28dc3c6122dc022616362c2 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 14 Nov 2021 22:37:51 +0100 Subject: [PATCH 0783/2244] Fix Windows sc_pipe function names The implementation name was incorrect (it was harmless, because they are not used on Windows). --- app/src/sys/win/process.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/sys/win/process.c b/app/src/sys/win/process.c index 4dcd542e..e4460ea7 100644 --- a/app/src/sys/win/process.c +++ b/app/src/sys/win/process.c @@ -168,7 +168,7 @@ sc_process_close(HANDLE handle) { } ssize_t -sc_read_pipe(HANDLE pipe, char *data, size_t len) { +sc_pipe_read(HANDLE pipe, char *data, size_t len) { DWORD r; if (!ReadFile(pipe, data, len, &r, NULL)) { return -1; @@ -177,7 +177,7 @@ sc_read_pipe(HANDLE pipe, char *data, size_t len) { } void -sc_close_pipe(HANDLE pipe) { +sc_pipe_close(HANDLE pipe) { if (!CloseHandle(pipe)) { LOGW("Cannot close pipe"); } From fd4ec784e02d5f3ce715c3b1e1f967716a069667 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 14 Nov 2021 22:53:02 +0100 Subject: [PATCH 0784/2244] Remove useless assignments on error Leave the output parameter untouched on error. --- app/src/sys/win/process.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/sys/win/process.c b/app/src/sys/win/process.c index e4460ea7..f76fa12e 100644 --- a/app/src/sys/win/process.c +++ b/app/src/sys/win/process.c @@ -84,7 +84,6 @@ sc_process_execute_p(const char *const argv[], HANDLE *handle, char *cmd = malloc(CMD_MAX_LEN); if (!cmd || !build_cmd(cmd, CMD_MAX_LEN, argv)) { - *handle = NULL; goto error_close_stderr; } @@ -98,7 +97,6 @@ sc_process_execute_p(const char *const argv[], HANDLE *handle, if (!CreateProcessW(NULL, wide, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi)) { free(wide); - *handle = NULL; if (GetLastError() == ERROR_FILE_NOT_FOUND) { ret = SC_PROCESS_ERROR_MISSING_BINARY; From 9cb8766220b55e9ac5d222d16f2138c481034e0d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 15 Nov 2021 07:49:01 +0100 Subject: [PATCH 0785/2244] Factorize resource release after CreateProcess() Free the wide characters string in all cases before checking for errors. --- app/src/sys/win/process.c | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/app/src/sys/win/process.c b/app/src/sys/win/process.c index f76fa12e..45980790 100644 --- a/app/src/sys/win/process.c +++ b/app/src/sys/win/process.c @@ -94,10 +94,10 @@ sc_process_execute_p(const char *const argv[], HANDLE *handle, goto error_close_stderr; } - if (!CreateProcessW(NULL, wide, NULL, NULL, TRUE, 0, NULL, NULL, &si, - &pi)) { - free(wide); - + BOOL ok = CreateProcessW(NULL, wide, NULL, NULL, TRUE, 0, NULL, NULL, &si, + &pi); + free(wide); + if (!ok) { if (GetLastError() == ERROR_FILE_NOT_FOUND) { ret = SC_PROCESS_ERROR_MISSING_BINARY; } @@ -115,7 +115,6 @@ sc_process_execute_p(const char *const argv[], HANDLE *handle, CloseHandle(stderr_write_handle); } - free(wide); *handle = pi.hProcess; return SC_PROCESS_SUCCESS; From 9cb14b51663d61c6f0b41da3c5699de418d633aa Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 14 Nov 2021 19:29:54 +0100 Subject: [PATCH 0786/2244] Inherit only specific handles on Windows To be able to communicate with a child process via stdin, stdout and stderr, the CreateProcess() parameter bInheritHandles must be set to TRUE. But this causes *all* handles to be inherited, including sockets. As a result, the server socket was inherited by the process running adb to execute the server on the device, so it could not be closed properly, causing other scrcpy instances to fail. To fix the issue, use an extended API to explicitly set the HANDLEs to inherit: - - Fixes #2779 PR #2783 --- app/src/sys/win/process.c | 88 +++++++++++++++++++++++++++++++++------ 1 file changed, 76 insertions(+), 12 deletions(-) diff --git a/app/src/sys/win/process.c b/app/src/sys/win/process.c index 45980790..6566b80e 100644 --- a/app/src/sys/win/process.c +++ b/app/src/sys/win/process.c @@ -1,5 +1,10 @@ +// +#define _WIN32_WINNT 0x0600 // For extended process API + #include "util/process.h" +#include + #include #include "util/log.h" @@ -26,6 +31,9 @@ sc_process_execute_p(const char *const argv[], HANDLE *handle, HANDLE *pin, HANDLE *pout, HANDLE *perr) { enum sc_process_result ret = SC_PROCESS_ERROR_GENERIC; + // Add 1 per non-NULL pointer + unsigned handle_count = !!pin + !!pout + !!perr; + SECURITY_ATTRIBUTES sa; sa.nLength = sizeof(SECURITY_ATTRIBUTES); sa.lpSecurityDescriptor = NULL; @@ -65,43 +73,94 @@ sc_process_execute_p(const char *const argv[], HANDLE *handle, } } - STARTUPINFOW si; + STARTUPINFOEXW si; PROCESS_INFORMATION pi; memset(&si, 0, sizeof(si)); - si.cb = sizeof(si); - if (pin || pout || perr) { - si.dwFlags = STARTF_USESTDHANDLES; + si.StartupInfo.cb = sizeof(si); + HANDLE handles[3]; + + LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList = NULL; + if (handle_count) { + si.StartupInfo.dwFlags = STARTF_USESTDHANDLES; if (pin) { - si.hStdInput = stdin_read_handle; + si.StartupInfo.hStdInput = stdin_read_handle; } if (pout) { - si.hStdOutput = stdout_write_handle; + si.StartupInfo.hStdOutput = stdout_write_handle; } if (perr) { - si.hStdError = stderr_write_handle; + si.StartupInfo.hStdError = stderr_write_handle; } + + SIZE_T size; + // Call it once to know the required buffer size + BOOL ok = + InitializeProcThreadAttributeList(NULL, 1, 0, &size) + || GetLastError() == ERROR_INSUFFICIENT_BUFFER; + if (!ok) { + goto error_close_stderr; + } + + lpAttributeList = malloc(size); + if (!lpAttributeList) { + goto error_close_stderr; + } + + ok = InitializeProcThreadAttributeList(lpAttributeList, 1, 0, &size); + if (!ok) { + free(lpAttributeList); + goto error_close_stderr; + } + + // Explicitly pass the HANDLEs that must be inherited + unsigned i = 0; + if (pin) { + handles[i++] = stdin_read_handle; + } + if (pout) { + handles[i++] = stdout_write_handle; + } + if (perr) { + handles[i++] = stderr_write_handle; + } + ok = UpdateProcThreadAttribute(lpAttributeList, 0, + PROC_THREAD_ATTRIBUTE_HANDLE_LIST, + handles, handle_count * sizeof(HANDLE), + NULL, NULL); + if (!ok) { + goto error_free_attribute_list; + } + + si.lpAttributeList = lpAttributeList; } char *cmd = malloc(CMD_MAX_LEN); if (!cmd || !build_cmd(cmd, CMD_MAX_LEN, argv)) { - goto error_close_stderr; + goto error_free_attribute_list; } wchar_t *wide = sc_str_to_wchars(cmd); free(cmd); if (!wide) { LOGC("Could not allocate wide char string"); - goto error_close_stderr; + goto error_free_attribute_list; } - BOOL ok = CreateProcessW(NULL, wide, NULL, NULL, TRUE, 0, NULL, NULL, &si, - &pi); + BOOL bInheritHandles = handle_count > 0; + DWORD dwCreationFlags = handle_count > 0 ? EXTENDED_STARTUPINFO_PRESENT : 0; + BOOL ok = CreateProcessW(NULL, wide, NULL, NULL, bInheritHandles, + dwCreationFlags, NULL, NULL, &si.StartupInfo, &pi); free(wide); if (!ok) { if (GetLastError() == ERROR_FILE_NOT_FOUND) { ret = SC_PROCESS_ERROR_MISSING_BINARY; } - goto error_close_stderr; + goto error_free_attribute_list; + } + + if (lpAttributeList) { + DeleteProcThreadAttributeList(lpAttributeList); + free(lpAttributeList); } // These handles are used by the child process, close them for this process @@ -119,6 +178,11 @@ sc_process_execute_p(const char *const argv[], HANDLE *handle, return SC_PROCESS_SUCCESS; +error_free_attribute_list: + if (lpAttributeList) { + DeleteProcThreadAttributeList(lpAttributeList); + free(lpAttributeList); + } error_close_stderr: if (perr) { CloseHandle(*perr); From 02ae0db6cd14a06899e6f8e92bfe107210ec51a3 Mon Sep 17 00:00:00 2001 From: Alex Burdusel <1262636+AlexBurdu@users.noreply.github.com> Date: Mon, 15 Nov 2021 22:18:40 +0200 Subject: [PATCH 0787/2244] Fix wrong package to install for Ubuntu/Debian Without this package, meson fails: Run-time dependency libusb-1.0 found: NO (tried pkgconfig and cmake) app/meson.build:88:8: ERROR: Dependency "libusb-1.0" not found, tried pkgconfig and cmake PR #2790 Signed-off-by: Romain Vimont --- BUILD.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/BUILD.md b/BUILD.md index 9eda3715..5f473ce2 100644 --- a/BUILD.md +++ b/BUILD.md @@ -15,7 +15,7 @@ First, you need to install the required packages: sudo apt install ffmpeg libsdl2-2.0-0 adb wget \ gcc git pkg-config meson ninja-build libsdl2-dev \ libavcodec-dev libavdevice-dev libavformat-dev libavutil-dev \ - libusb-1.0-0 libusb-dev + libusb-1.0-0 libusb-1.0-0-dev ``` Then clone the repo and execute the installation script @@ -94,7 +94,7 @@ sudo apt install ffmpeg libsdl2-2.0-0 adb libusb-1.0-0 # client build dependencies sudo apt install gcc git pkg-config meson ninja-build libsdl2-dev \ libavcodec-dev libavdevice-dev libavformat-dev libavutil-dev \ - libusb-dev + libusb-1.0-0-dev # server build dependencies sudo apt install openjdk-11-jdk From 57fb08e443272a9d346b036fb421cdf0ccff3074 Mon Sep 17 00:00:00 2001 From: LuXu Date: Mon, 15 Nov 2021 17:20:57 +0800 Subject: [PATCH 0788/2244] Update Simplified Chinese README to 1.20 PR #2786 Signed-off-by: Romain Vimont --- README.md | 2 +- README.zh-Hans.md | 313 +++++++++++++++++++++++++++++++++------------- 2 files changed, 224 insertions(+), 91 deletions(-) diff --git a/README.md b/README.md index c9080861..8d80cb1f 100644 --- a/README.md +++ b/README.md @@ -955,7 +955,7 @@ This README is available in other languages: - [한국어 (Korean, `ko`) - v1.11](README.ko.md) - [Português Brasileiro (Brazilian Portuguese, `pt-BR`) - v1.19](README.pt-br.md) - [Español (Spanish, `sp`) - v1.17](README.sp.md) -- [简体中文 (Simplified Chinese, `zh-Hans`) - v1.17](README.zh-Hans.md) +- [简体中文 (Simplified Chinese, `zh-Hans`) - v1.20](README.zh-Hans.md) - [繁體中文 (Traditional Chinese, `zh-Hant`) - v1.15](README.zh-Hant.md) - [Turkish (Turkish, `tr`) - v1.18](README.tr.md) diff --git a/README.zh-Hans.md b/README.zh-Hans.md index bdd8023c..b96d6d5a 100644 --- a/README.zh-Hans.md +++ b/README.zh-Hans.md @@ -2,27 +2,41 @@ _Only the original [README](README.md) is guaranteed to be up-to-date._ 只有原版的[README](README.md)会保持最新。 -本文根据[ed130e05]进行翻译。 +Current version is based on [65b023a] -[ed130e05]: https://github.com/Genymobile/scrcpy/blob/ed130e05d55615d6014d93f15cfcb92ad62b01d8/README.md +本文根据[65b023a]进行翻译。 -# scrcpy (v1.17) +[65b023a]: https://github.com/Genymobile/scrcpy/blob/65b023ac6d586593193fd5290f65e25603b68e02/README.md + +# scrcpy (v1.20) + +scrcpy 本应用程序可以显示并控制通过 USB (或 [TCP/IP][article-tcpip]) 连接的安卓设备,且不需要任何 _root_ 权限。本程序支持 _GNU/Linux_, _Windows_ 和 _macOS_。 ![screenshot](assets/screenshot-debian-600.jpg) -它专注于: +本应用专注于: - - **轻量** (原生,仅显示设备屏幕) - - **性能** (30~60fps) - - **质量** (分辨率可达 1920×1080 或更高) - - **低延迟** ([35~70ms][lowlatency]) - - **快速启动** (最快 1 秒内即可显示第一帧) - - **无侵入性** (不会在设备上遗留任何程序) + - **轻量**: 原生,仅显示设备屏幕 + - **性能**: 30~120fps,取决于设备 + - **质量**: 分辨率可达 1920×1080 或更高 + - **低延迟**: [35~70ms][lowlatency] + - **快速启动**: 最快 1 秒内即可显示第一帧 + - **无侵入性**: 不会在设备上遗留任何程序 + - **用户利益**: 无需帐号,无广告,无需联网 + - **自由**: 自由和开源软件 [lowlatency]: https://github.com/Genymobile/scrcpy/pull/646 +功能: + - [屏幕录制](#屏幕录制) + - 镜像时[关闭设备屏幕](#关闭设备屏幕) + - 双向[复制粘贴](#复制粘贴) + - [可配置显示质量](#采集设置) + - 以设备屏幕[作为摄像头(V4L2)](#v4l2loopback) (仅限 Linux) + - [模拟物理键盘 (HID)](#物理键盘模拟-hid) (仅限 Linux) + - 更多 …… ## 系统要求 @@ -41,6 +55,17 @@ _Only the original [README](README.md) is guaranteed to be up-to-date._ Packaging status +### 概要 + + - Linux: `apt install scrcpy` + - Windows: [下载][direct-win64] + - macOS: `brew install scrcpy` + +从源代码编译: [构建][BUILD] ([简化过程][BUILD_simple]) + +[BUILD]: BUILD.md +[BUILD_simple]: BUILD.md#simple + ### Linux 在 Debian (目前仅支持 _testing_ 和 _sid_ 分支) 和Ubuntu (20.04) 上: @@ -70,13 +95,12 @@ apt install scrcpy [Ebuild]: https://wiki.gentoo.org/wiki/Ebuild [ebuild-link]: https://github.com/maggu2810/maggu2810-overlay/tree/master/app-mobilephone/scrcpy -您也可以[自行构建][BUILD] (不必担心,这并不困难)。 - +您也可以[自行构建][BUILD] ([简化过程][BUILD_simple])。 ### Windows -在 Windows 上,简便起见,我们提供包含了所有依赖 (包括 `adb`) 的预编译包。 +在 Windows 上,为简便起见,我们提供包含了所有依赖 (包括 `adb`) 的预编译包。 - [README](README.md#windows) @@ -114,13 +138,17 @@ brew install scrcpy 你还需要在 `PATH` 内有 `adb`。如果还没有: ```bash -# Homebrew >= 2.6.0 -brew install --cask android-platform-tools - -# Homebrew < 2.6.0 -brew cask install android-platform-tools +brew install android-platform-tools ``` +或者通过 [MacPorts],该方法同时设置好 adb: + +```bash +sudo port install scrcpy +``` + +[MacPorts]: https://www.macports.org/ + 您也可以[自行构建][BUILD]。 @@ -140,7 +168,7 @@ scrcpy --help ## 功能介绍 -### 捕获设置 +### 采集设置 #### 降低分辨率 @@ -158,7 +186,7 @@ scrcpy -m 1024 # 简写 #### 修改码率 -默认码率是 8Mbps。要改变视频的码率 (例如改为 2Mbps): +默认码率是 8 Mbps。改变视频码率 (例如改为 2 Mbps): ```bash scrcpy --bit-rate 2M @@ -167,7 +195,7 @@ scrcpy -b 2M # 简写 #### 限制帧率 -要限制捕获的帧率: +要限制采集的帧率: ```bash scrcpy --max-fps 15 @@ -194,10 +222,11 @@ scrcpy --crop 1224:1440:0:0 # 以 (0,0) 为原点的 1224x1440 像素 要锁定镜像画面的方向: ```bash -scrcpy --lock-video-orientation 0 # 自然方向 -scrcpy --lock-video-orientation 1 # 逆时针旋转 90° -scrcpy --lock-video-orientation 2 # 180° -scrcpy --lock-video-orientation 3 # 顺时针旋转 90° +scrcpy --lock-video-orientation # 初始(目前)方向 +scrcpy --lock-video-orientation=0 # 自然方向 +scrcpy --lock-video-orientation=1 # 逆时针旋转 90° +scrcpy --lock-video-orientation=2 # 180° +scrcpy --lock-video-orientation=3 # 顺时针旋转 90° ``` 只影响录制的方向。 @@ -219,7 +248,9 @@ scrcpy --encoder OMX.qcom.video.encoder.avc scrcpy --encoder _ ``` -### 屏幕录制 +### 采集 + +#### 屏幕录制 可以在镜像的同时录制视频: @@ -241,6 +272,75 @@ scrcpy -Nr file.mkv [packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation +#### v4l2loopback + +在 Linux 上,可以将视频流发送至 v4l2 回环 (loopback) 设备,因此可以使用任何 v4l2 工具像摄像头一样打开安卓设备。 + +需安装 `v4l2loopback` 模块: + +```bash +sudo apt install v4l2loopback-dkms +``` + +创建一个 v4l2 设备: + +```bash +sudo modprobe v4l2loopback +``` + +这样会在 `/dev/videoN` 创建一个新的视频设备,其中 `N` 是整数。 ([更多选项](https://github.com/umlaeute/v4l2loopback#options) 可以用来创建多个设备或者特定 ID 的设备)。 + +列出已启用的设备: + +```bash +# 需要 v4l-utils 包 +v4l2-ctl --list-devices + +# 简单但或许足够 +ls /dev/video* +``` + +使用一个 v4l2 漏开启 scrcpy: + +```bash +scrcpy --v4l2-sink=/dev/videoN +scrcpy --v4l2-sink=/dev/videoN --no-display # 禁用窗口镜像 +scrcpy --v4l2-sink=/dev/videoN -N # 简写 +``` + +(将 `N` 替换为设备 ID,使用 `ls /dev/video*` 命令查看) + +启用之后,可以使用 v4l2 工具打开视频流: + +```bash +ffplay -i /dev/videoN +vlc v4l2:///dev/videoN # VLC 可能存在一些缓冲延迟 +``` + +例如,可以在 [OBS] 中采集视频。 + +[OBS]: https://obsproject.com/ + + +#### 缓冲 + +可以加入缓冲,会增加延迟,但可以减少抖动 (见 [#2464])。 + +[#2464]: https://github.com/Genymobile/scrcpy/issues/2464 + +对于显示缓冲: + +```bash +scrcpy --display-buffer=50 # 为显示增加 50 毫秒的缓冲 +``` + +对于 V4L2 漏: + +```bash +scrcpy --v4l2-buffer=500 # 为 v4l2 漏增加 500 毫秒的缓冲 +``` + + ### 连接 #### 无线 @@ -249,16 +349,17 @@ _Scrcpy_ 使用 `adb` 与设备通信,并且 `adb` 支持通过 TCP/IP [连接 1. 将设备和电脑连接至同一 Wi-Fi。 2. 打开 设置 → 关于手机 → 状态信息,获取设备的 IP 地址,也可以执行以下的命令: + ```bash adb shell ip route | awk '{print $9}' ``` -3. 启用设备的网络 adb 功能 `adb tcpip 5555`。 +3. 启用设备的网络 adb 功能: `adb tcpip 5555`。 4. 断开设备的 USB 连接。 -5. 连接到您的设备:`adb connect DEVICE_IP:5555` _(将 `DEVICE_IP` 替换为设备 IP)_. +5. 连接到您的设备:`adb connect DEVICE_IP:5555` _(将 `DEVICE_IP` 替换为设备 IP)_。 6. 正常运行 `scrcpy`。 -可能需要降低码率和分辨率: +可能降低码率和分辨率会更好一些: ```bash scrcpy --bit-rate 2M --max-size 800 @@ -327,7 +428,7 @@ scrcpy --force-adb-forward ``` -类似无线网络连接,可能需要降低画面质量: +类似地,对于无线连接,可能需要降低画面质量: ``` scrcpy -b2M -m800 --max-fps 15 @@ -353,7 +454,7 @@ scrcpy --window-x 100 --window-y 100 --window-width 800 --window-height 600 #### 无边框 -关闭边框: +禁用窗口边框: ```bash scrcpy --window-borderless @@ -369,7 +470,7 @@ scrcpy --always-on-top #### 全屏 -您可以通过如下命令直接全屏启动scrcpy: +您可以通过如下命令直接全屏启动 scrcpy: ```bash scrcpy --fullscreen @@ -394,7 +495,7 @@ scrcpy --rotation 1 也可以使用 MOD+ _(左箭头)_ 和 MOD+ _(右箭头)_ 随时更改。 -需要注意的是, _scrcpy_ 有三个不同的方向: +需要注意的是, _scrcpy_ 中有三类旋转方向: - MOD+r 请求设备在竖屏和横屏之间切换 (如果前台应用程序不支持请求的朝向,可能会拒绝该请求)。 - [`--lock-video-orientation`](#锁定屏幕方向) 改变镜像的朝向 (设备传输到电脑的画面的朝向)。这会影响录制。 - `--rotation` (或 MOD+/MOD+) 只旋转窗口的内容。这只影响显示,不影响录制。 @@ -404,7 +505,7 @@ scrcpy --rotation 1 #### 只读 -禁用电脑对设备的控制 (如键盘输入、鼠标事件和文件拖放): +禁用电脑对设备的控制 (任何可与设备交互的方式:如键盘输入、鼠标事件和文件拖放): ```bash scrcpy --no-control @@ -430,14 +531,14 @@ adb shell dumpsys display # 在输出中搜索 “mDisplayId=” #### 保持常亮 -阻止设备在连接时休眠: +阻止设备在连接时一段时间后休眠: ```bash scrcpy --stay-awake scrcpy -w ``` -程序关闭时会恢复设备原来的设置。 +scrcpy 关闭时会恢复设备原来的设置。 #### 关闭设备屏幕 @@ -451,7 +552,7 @@ scrcpy -S 或者在任何时候按 MOD+o。 -要重新打开屏幕,按下 MOD+Shift+o. +要重新打开屏幕,按下 MOD+Shift+o。 在Android上,`电源` 按钮始终能把屏幕打开。为了方便,对于在 _scrcpy_ 中发出的 `电源` 事件 (通过鼠标右键或 MOD+p),会 (尽最大的努力) 在短暂的延迟后将屏幕关闭。设备上的 `电源` 按钮仍然能打开设备屏幕。 @@ -462,20 +563,17 @@ scrcpy --turn-screen-off --stay-awake scrcpy -Sw ``` +#### 退出时息屏 -#### 渲染过期帧 - -默认状态下,为了降低延迟, _scrcpy_ 永远渲染解码成功的最近一帧,并跳过前面任意帧。 - -强制渲染所有帧 (可能导致延迟变高): +scrcpy 退出时关闭设备屏幕: ```bash -scrcpy --render-expired-frames +scrcpy --power-off-on-close ``` #### 显示触摸 -在演示时,可能会需要显示物理触摸点 (在物理设备上的触摸点)。 +在演示时,可能会需要显示 (在物理设备上的) 物理触摸点。 Android 在 _开发者选项_ 中提供了这项功能。 @@ -538,10 +636,32 @@ scrcpy --disable-screensaver 更准确的说,在按住鼠标左键时按住 Ctrl。直到松开鼠标左键,所有鼠标移动将以屏幕中心为原点,缩放或旋转内容 (如果应用支持)。 -实际上,_scrcpy_ 会在以屏幕中心对称的位置上生成由“虚拟手指”发出的额外触摸事件。 +实际上,_scrcpy_ 会在关于屏幕中心对称的位置上用“虚拟手指”发出触摸事件。 +#### 物理键盘模拟 (HID) -#### 文字注入偏好 +默认情况下,scrcpy 使用安卓按键或文本注入,这在任何情况都可以使用,但仅限于ASCII字符。 + +在 Linux 上,scrcpy 可以模拟为 Android 上的物理 USB 键盘,以提供更好地输入体验 (使用 [USB HID over AOAv2][hid-aoav2]):禁用虚拟键盘,并适用于任何字符和输入法。 + +[hid-aoav2]: https://source.android.com/devices/accessories/aoa2#hid-support + +不过,这种方法仅支持 USB 连接以及 Linux平台。 + +启用 HID 模式: + +```bash +scrcpy --hid-keyboard +scrcpy -K # 简写 +``` + +如果失败了 (如设备未通过 USB 连接),则自动回退至默认模式 (终端中会输出日志)。这即允许通过 USB 和 TCP/IP 连接时使用相同的命令行参数。 + +在这种模式下,原始按键事件 (扫描码) 被发送给设备,而与宿主机按键映射无关。因此,若键盘布局不匹配,需要在 Android 设备上进行配置,具体为 设置 → 系统 → 语言和输入法 → [实体键盘]。 + +[Physical keyboard]: https://github.com/Genymobile/scrcpy/pull/2632#issuecomment-923756915 + +#### 文本注入偏好 打字的时候,系统会产生两种[事件][textevents]: - _按键事件_ ,代表一个按键被按下或松开。 @@ -557,13 +677,15 @@ scrcpy --prefer-text (这会导致键盘在游戏中工作不正常) +该选项不影响 HID 键盘 (该模式下,所有按键都发送为扫描码)。 + [textevents]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-text-input [prefertext]: https://github.com/Genymobile/scrcpy/issues/650#issuecomment-512945343 #### 按键重复 -默认状态下,按住一个按键不放会生成多个重复按键事件。在某些游戏中这可能会导致性能问题。 +默认状态下,按住一个按键不放会生成多个重复按键事件。在某些游戏中这通常没有实际用途,且可能会导致性能问题。 避免转发重复按键事件: @@ -571,10 +693,11 @@ scrcpy --prefer-text scrcpy --no-key-repeat ``` +该选项不影响 HID 键盘 (该模式下,按键重复由 Android 直接管理)。 #### 右键和中键 -默认状态下,右键会触发返回键 (或电源键),中键会触发 HOME 键。要禁用这些快捷键并把所有点击转发到设备: +默认状态下,右键会触发返回键 (或电源键开启),中键会触发 HOME 键。要禁用这些快捷键并把所有点击转发到设备: ```bash scrcpy --forward-all-clicks @@ -587,27 +710,27 @@ scrcpy --forward-all-clicks 将 APK 文件 (文件名以 `.apk` 结尾) 拖放到 _scrcpy_ 窗口来安装。 -该操作在屏幕上不会出现任何变化,而会在控制台输出一条日志。 +不会有视觉反馈,终端会输出一条日志。 #### 将文件推送至设备 -要推送文件到设备的 `/sdcard/`,将 (非 APK) 文件拖放至 _scrcpy_ 窗口。 +要推送文件到设备的 `/sdcard/Download/`,将 (非 APK) 文件拖放至 _scrcpy_ 窗口。 -该操作没有可见的响应,只会在控制台输出日志。 +不会有视觉反馈,终端会输出一条日志。 在启动时可以修改目标目录: ```bash -scrcpy --push-target /sdcard/foo/bar/ +scrcpy --push-target=/sdcard/Movies/ ``` ### 音频转发 -_Scrcpy_ 不支持音频。请使用 [sndcpy]. +_Scrcpy_ 不支持音频。请使用 [sndcpy]。 -另外请阅读 [issue #14]。 +另见 [issue #14]。 [sndcpy]: https://github.com/rom1v/sndcpy [issue #14]: https://github.com/Genymobile/scrcpy/issues/14 @@ -632,36 +755,46 @@ _[Super] 键通常是指 WindowsCmd 键。 [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+ _(上箭头)_ | - | 点按 `音量-` | 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 | - | 捏拉缩放 | Ctrl+_按住并移动鼠标_ | + | 操作 | 快捷键 + | --------------------------------- | :------------------------------------------- + | 全屏 | MOD+f + | 向左旋转屏幕 | MOD+ _(左箭头)_ + | 向右旋转屏幕 | MOD+ _(右箭头)_ + | 将窗口大小重置为1:1 (匹配像素) | MOD+g + | 将窗口大小重置为消除黑边 | MOD+w \| _双击左键¹_ + | 点按 `主屏幕` | MOD+h \| _中键_ + | 点按 `返回` | MOD+b \| _右键²_ + | 点按 `切换应用` | MOD+s \| _第4键³_ + | 点按 `菜单` (解锁屏幕) | MOD+m + | 点按 `音量+` | MOD+ _(上箭头)_ + | 点按 `音量-` | MOD+ _(下箭头)_ + | 点按 `电源` | MOD+p + | 打开屏幕 | _鼠标右键²_ + | 关闭设备屏幕 (但继续在电脑上显示) | MOD+o + | 打开设备屏幕 | MOD+Shift+o + | 旋转设备屏幕 | MOD+r + | 展开通知面板 | MOD+n \| _第5键³_ + | 展开设置面板 | MOD+n+n \| _双击第5键³_ + | 收起通知面板 | MOD+Shift+n + | 复制到剪贴板⁴ | MOD+c + | 剪切到剪贴板⁴ | MOD+x + | 同步剪贴板并粘贴⁴ | MOD+v + | 注入电脑剪贴板文本 | MOD+Shift+v + | 打开/关闭FPS显示 (至标准输出) | MOD+i + | 捏拉缩放 | Ctrl+_按住并移动鼠标_ + | 拖放 APK 文件 | 从电脑安装 APK 文件 + | 拖放非 APK 文件 | [将文件推送至设备](#push-file-to-device) -_¹双击黑边可以去除黑边_ -_²点击鼠标右键将在屏幕熄灭时点亮屏幕,其余情况则视为按下返回键 。_ -_³需要安卓版本 Android >= 7。_ +_¹双击黑边可以去除黑边。_ +_²点击鼠标右键将在屏幕熄灭时点亮屏幕,其余情况则视为按下返回键 。_ +_³鼠标的第4键和第5键。_ +_⁴需要安卓版本 Android >= 7。_ + +有重复按键的快捷键通过松开再按下一个按键来进行,如“展开设置面板”: + + 1. 按下 MOD 不放。 + 2. 双击 n。 + 3. 松开 MOD。 所有的 Ctrl+_按键_ 的快捷键都会被转发到设备,所以会由当前应用程序进行处理。 @@ -670,18 +803,20 @@ _³需要安卓版本 Android >= 7。_ 要使用指定的 _adb_ 二进制文件,可以设置环境变量 `ADB`: - ADB=/path/to/adb scrcpy +```bash +ADB=/path/to/adb scrcpy +``` 要覆盖 `scrcpy-server` 的路径,可以设置 `SCRCPY_SERVER_PATH`。 -[useful]: https://github.com/Genymobile/scrcpy/issues/278#issuecomment-429330345 +要覆盖图标,可以设置其路径至 `SCRCPY_ICON_PATH`。 ## 为什么叫 _scrcpy_ ? 一个同事让我找出一个和 [gnirehtet] 一样难以发音的名字。 -[`strcpy`] 复制一个 **str**ing; `scrcpy` 复制一个 **scr**een。 +[`strcpy`] 复制一个 **str**ing (字符串); `scrcpy` 复制一个 **scr**een (屏幕)。 [gnirehtet]: https://github.com/Genymobile/gnirehtet [`strcpy`]: http://man7.org/linux/man-pages/man3/strcpy.3.html @@ -689,14 +824,12 @@ _³需要安卓版本 Android >= 7。_ ## 如何构建? -请查看[BUILD]。 - -[BUILD]: BUILD.md +请查看 [BUILD]。 ## 常见问题 -请查看[FAQ](FAQ.md)。 +请查看 [FAQ](FAQ.md)。 ## 开发者 From 67170437f1eb2c9f2780d720b3079d7d8722b9f4 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 16 Nov 2021 22:10:34 +0100 Subject: [PATCH 0789/2244] Wrap settings management into a Settings class Until now, the code that needed to read/write the Android settings had to explicitly open and close a ContentProvider. Wrap these details into a Settings class. This paves the way to provide an alternative implementation of settings read/write for Android >= 12. PR #2802 --- .../java/com/genymobile/scrcpy/CleanUp.java | 18 ++++----- .../java/com/genymobile/scrcpy/Device.java | 6 +-- .../java/com/genymobile/scrcpy/Server.java | 35 ++++++++--------- .../java/com/genymobile/scrcpy/Settings.java | 39 +++++++++++++++++++ .../scrcpy/wrappers/ContentProvider.java | 8 ---- 5 files changed, 66 insertions(+), 40 deletions(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/Settings.java diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java index ec61a1c0..9001eef7 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java +++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java @@ -1,6 +1,5 @@ package com.genymobile.scrcpy; -import com.genymobile.scrcpy.wrappers.ContentProvider; import com.genymobile.scrcpy.wrappers.ServiceManager; import android.os.Parcel; @@ -166,15 +165,14 @@ public final class CleanUp { if (config.disableShowTouches || config.restoreStayOn != -1) { ServiceManager serviceManager = new ServiceManager(); - try (ContentProvider settings = serviceManager.getActivityManager().createSettingsProvider()) { - if (config.disableShowTouches) { - Ln.i("Disabling \"show touches\""); - settings.putValue(ContentProvider.TABLE_SYSTEM, "show_touches", "0"); - } - if (config.restoreStayOn != -1) { - Ln.i("Restoring \"stay awake\""); - settings.putValue(ContentProvider.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(config.restoreStayOn)); - } + Settings settings = new Settings(serviceManager); + if (config.disableShowTouches) { + Ln.i("Disabling \"show touches\""); + settings.putValue(Settings.TABLE_SYSTEM, "show_touches", "0"); + } + if (config.restoreStayOn != -1) { + Ln.i("Restoring \"stay awake\""); + settings.putValue(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(config.restoreStayOn)); } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index 3e71fe9c..093646e2 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -1,7 +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; import com.genymobile.scrcpy.wrappers.SurfaceControl; @@ -29,6 +28,7 @@ public final class Device { public static final int LOCK_VIDEO_ORIENTATION_INITIAL = -2; private static final ServiceManager SERVICE_MANAGER = new ServiceManager(); + private static final Settings SETTINGS = new Settings(SERVICE_MANAGER); public interface RotationListener { void onRotationChanged(int rotation); @@ -296,7 +296,7 @@ public final class Device { } } - public static ContentProvider createSettingsProvider() { - return SERVICE_MANAGER.getActivityManager().createSettingsProvider(); + public static Settings getSettings() { + return SETTINGS; } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index fdd9db88..c900f872 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -1,7 +1,5 @@ package com.genymobile.scrcpy; -import com.genymobile.scrcpy.wrappers.ContentProvider; - import android.graphics.Rect; import android.media.MediaCodec; import android.media.MediaCodecInfo; @@ -27,25 +25,24 @@ public final class Server { boolean mustDisableShowTouchesOnCleanUp = false; int restoreStayOn = -1; if (options.getShowTouches() || options.getStayAwake()) { - try (ContentProvider settings = Device.createSettingsProvider()) { - 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); - } + Settings settings = Device.getSettings(); + if (options.getShowTouches()) { + String oldValue = settings.getAndPutValue(Settings.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; + if (options.getStayAwake()) { + int stayOn = BatteryManager.BATTERY_PLUGGED_AC | BatteryManager.BATTERY_PLUGGED_USB | BatteryManager.BATTERY_PLUGGED_WIRELESS; + String oldValue = settings.getAndPutValue(Settings.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; } } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Settings.java b/server/src/main/java/com/genymobile/scrcpy/Settings.java new file mode 100644 index 00000000..b59188d5 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/Settings.java @@ -0,0 +1,39 @@ +package com.genymobile.scrcpy; + +import com.genymobile.scrcpy.wrappers.ContentProvider; +import com.genymobile.scrcpy.wrappers.ServiceManager; + +public class Settings { + + public static final String TABLE_SYSTEM = ContentProvider.TABLE_SYSTEM; + public static final String TABLE_SECURE = ContentProvider.TABLE_SECURE; + public static final String TABLE_GLOBAL = ContentProvider.TABLE_GLOBAL; + + private final ServiceManager serviceManager; + + public Settings(ServiceManager serviceManager) { + this.serviceManager = serviceManager; + } + + public String getValue(String table, String key) { + try (ContentProvider provider = serviceManager.getActivityManager().createSettingsProvider()) { + return provider.getValue(table, key); + } + } + + public void putValue(String table, String key, String value) { + try (ContentProvider provider = serviceManager.getActivityManager().createSettingsProvider()) { + provider.putValue(table, key, value); + } + } + + public String getAndPutValue(String table, String key, String value) { + try (ContentProvider provider = serviceManager.getActivityManager().createSettingsProvider()) { + String oldValue = provider.getValue(table, key); + if (!value.equals(oldValue)) { + provider.putValue(table, key, value); + } + return oldValue; + } + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java index 387c7a60..ab95f0df 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java @@ -160,12 +160,4 @@ public class ContentProvider implements Closeable { 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; - } } From 94feae71f2bf603b1cbd78611a2fdecdfcf40b67 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 16 Nov 2021 22:40:53 +0100 Subject: [PATCH 0790/2244] Report settings errors via Exceptions Settings read/write errors were silently ignored. Report them via a SettingsException so that the caller can handle them. This allows to log a proper error message, and will also allow to fallback to a different settings method in case of failure. PR #2802 --- .../java/com/genymobile/scrcpy/CleanUp.java | 12 ++++++-- .../java/com/genymobile/scrcpy/Server.java | 28 +++++++++++------- .../java/com/genymobile/scrcpy/Settings.java | 6 ++-- .../genymobile/scrcpy/SettingsException.java | 11 +++++++ .../scrcpy/wrappers/ContentProvider.java | 29 +++++++++++++------ 5 files changed, 62 insertions(+), 24 deletions(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/SettingsException.java diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java index 9001eef7..319a957d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java +++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java @@ -168,11 +168,19 @@ public final class CleanUp { Settings settings = new Settings(serviceManager); if (config.disableShowTouches) { Ln.i("Disabling \"show touches\""); - settings.putValue(Settings.TABLE_SYSTEM, "show_touches", "0"); + try { + settings.putValue(Settings.TABLE_SYSTEM, "show_touches", "0"); + } catch (SettingsException e) { + Ln.e("Could not restore \"show_touches\"", e); + } } if (config.restoreStayOn != -1) { Ln.i("Restoring \"stay awake\""); - settings.putValue(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(config.restoreStayOn)); + try { + settings.putValue(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(config.restoreStayOn)); + } catch (SettingsException e) { + Ln.e("Could not restore \"stay_on_while_plugged_in\"", e); + } } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index c900f872..efb295b3 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -27,22 +27,30 @@ public final class Server { if (options.getShowTouches() || options.getStayAwake()) { Settings settings = Device.getSettings(); if (options.getShowTouches()) { - String oldValue = settings.getAndPutValue(Settings.TABLE_SYSTEM, "show_touches", "1"); - // If "show touches" was disabled, it must be disabled back on clean up - mustDisableShowTouchesOnCleanUp = !"1".equals(oldValue); + try { + String oldValue = settings.getAndPutValue(Settings.TABLE_SYSTEM, "show_touches", "1"); + // If "show touches" was disabled, it must be disabled back on clean up + mustDisableShowTouchesOnCleanUp = !"1".equals(oldValue); + } catch (SettingsException e) { + Ln.e("Could not change \"show_touches\"", e); + } } if (options.getStayAwake()) { int stayOn = BatteryManager.BATTERY_PLUGGED_AC | BatteryManager.BATTERY_PLUGGED_USB | BatteryManager.BATTERY_PLUGGED_WIRELESS; - String oldValue = settings.getAndPutValue(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(stayOn)); try { - restoreStayOn = Integer.parseInt(oldValue); - if (restoreStayOn == stayOn) { - // No need to restore - restoreStayOn = -1; + String oldValue = settings.getAndPutValue(Settings.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; } - } catch (NumberFormatException e) { - restoreStayOn = 0; + } catch (SettingsException e) { + Ln.e("Could not change \"stay_on_while_plugged_in\"", e); } } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Settings.java b/server/src/main/java/com/genymobile/scrcpy/Settings.java index b59188d5..83b63477 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Settings.java +++ b/server/src/main/java/com/genymobile/scrcpy/Settings.java @@ -15,19 +15,19 @@ public class Settings { this.serviceManager = serviceManager; } - public String getValue(String table, String key) { + public String getValue(String table, String key) throws SettingsException { try (ContentProvider provider = serviceManager.getActivityManager().createSettingsProvider()) { return provider.getValue(table, key); } } - public void putValue(String table, String key, String value) { + public void putValue(String table, String key, String value) throws SettingsException { try (ContentProvider provider = serviceManager.getActivityManager().createSettingsProvider()) { provider.putValue(table, key, value); } } - public String getAndPutValue(String table, String key, String value) { + public String getAndPutValue(String table, String key, String value) throws SettingsException { try (ContentProvider provider = serviceManager.getActivityManager().createSettingsProvider()) { String oldValue = provider.getValue(table, key); if (!value.equals(oldValue)) { diff --git a/server/src/main/java/com/genymobile/scrcpy/SettingsException.java b/server/src/main/java/com/genymobile/scrcpy/SettingsException.java new file mode 100644 index 00000000..36ef63ee --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/SettingsException.java @@ -0,0 +1,11 @@ +package com.genymobile.scrcpy; + +public class SettingsException extends Exception { + private static String createMessage(String method, String table, String key, String value) { + return "Could not access settings: " + method + " " + table + " " + key + (value != null ? " " + value : ""); + } + + public SettingsException(String method, String table, String key, String value, Throwable cause) { + super(createMessage(method, table, key, value), cause); + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java index ab95f0df..47eae64d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java @@ -1,6 +1,7 @@ package com.genymobile.scrcpy.wrappers; import com.genymobile.scrcpy.Ln; +import com.genymobile.scrcpy.SettingsException; import android.annotation.SuppressLint; import android.os.Bundle; @@ -87,7 +88,8 @@ public class ContentProvider implements Closeable { return attributionSource; } - private Bundle call(String callMethod, String arg, Bundle extras) { + private Bundle call(String callMethod, String arg, Bundle extras) + throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { try { Method method = getCallMethod(); Object[] args; @@ -108,7 +110,7 @@ public class ContentProvider implements Closeable { return (Bundle) method.invoke(provider, args); } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException | ClassNotFoundException | InstantiationException e) { Ln.e("Could not invoke method", e); - return null; + throw e; } } @@ -142,22 +144,31 @@ public class ContentProvider implements Closeable { } } - public String getValue(String table, String key) { + public String getValue(String table, String key) throws SettingsException { 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; + try { + Bundle bundle = call(method, key, arg); + if (bundle == null) { + return null; + } + return bundle.getString("value"); + } catch (Exception e) { + throw new SettingsException(table, "get", key, null, e); } - return bundle.getString("value"); + } - public void putValue(String table, String key, String value) { + public void putValue(String table, String key, String value) throws SettingsException { 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); + try { + call(method, key, arg); + } catch (Exception e) { + throw new SettingsException(table, "put", key, value, e); + } } } From 48b572c272901af1b32afa204ae011ebb9344b40 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 17 Nov 2021 10:01:13 +0100 Subject: [PATCH 0791/2244] Add throwable parameter to Log.w() When an exception occurs, we might want to log a warning instead of an error. PR #2802 --- server/src/main/java/com/genymobile/scrcpy/Ln.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Ln.java b/server/src/main/java/com/genymobile/scrcpy/Ln.java index 061cda95..c39fc621 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Ln.java +++ b/server/src/main/java/com/genymobile/scrcpy/Ln.java @@ -57,13 +57,20 @@ public final class Ln { } } - public static void w(String message) { + public static void w(String message, Throwable throwable) { if (isEnabled(Level.WARN)) { - Log.w(TAG, message); + Log.w(TAG, message, throwable); System.out.println(PREFIX + "WARN: " + message); + if (throwable != null) { + throwable.printStackTrace(); + } } } + public static void w(String message) { + w(message, null); + } + public static void e(String message, Throwable throwable) { if (isEnabled(Level.ERROR)) { Log.e(TAG, message, throwable); From cc0902b13c87fc98b1ed90b0700cc53ac4d7ee3c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 17 Nov 2021 10:05:10 +0100 Subject: [PATCH 0792/2244] Read/write settings via command on Android >= 12 Before Android 8, executing the "settings" command from a shell was very slow (~1 second), because it spawned a new app_process to execute Java code. Therefore, to access settings without performance issues, scrcpy used private APIs to read from and write to settings. However, since Android 12, this is not possible anymore, due to permissions changes. To make it work again, execute the "settings" command on Android 12 (or on previous version if the other method failed). This method is faster than before Android 8 (~100ms). Fixes #2671 Fixes #2788 PR #2802 --- .../java/com/genymobile/scrcpy/Command.java | 33 ++++++++++ .../java/com/genymobile/scrcpy/Settings.java | 65 ++++++++++++++++--- 2 files changed, 88 insertions(+), 10 deletions(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/Command.java diff --git a/server/src/main/java/com/genymobile/scrcpy/Command.java b/server/src/main/java/com/genymobile/scrcpy/Command.java new file mode 100644 index 00000000..0ef976a6 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/Command.java @@ -0,0 +1,33 @@ +package com.genymobile.scrcpy; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Scanner; + +public final class Command { + private Command() { + // not instantiable + } + + public static void exec(String... cmd) throws IOException, InterruptedException { + Process process = Runtime.getRuntime().exec(cmd); + int exitCode = process.waitFor(); + if (exitCode != 0) { + throw new IOException("Command " + Arrays.toString(cmd) + " returned with value " + exitCode); + } + } + + public static String execReadLine(String... cmd) throws IOException, InterruptedException { + String result = null; + Process process = Runtime.getRuntime().exec(cmd); + Scanner scanner = new Scanner(process.getInputStream()); + if (scanner.hasNextLine()) { + result = scanner.nextLine(); + } + int exitCode = process.waitFor(); + if (exitCode != 0) { + throw new IOException("Command " + Arrays.toString(cmd) + " returned with value " + exitCode); + } + return result; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/Settings.java b/server/src/main/java/com/genymobile/scrcpy/Settings.java index 83b63477..cb15ebb4 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Settings.java +++ b/server/src/main/java/com/genymobile/scrcpy/Settings.java @@ -3,6 +3,10 @@ package com.genymobile.scrcpy; import com.genymobile.scrcpy.wrappers.ContentProvider; import com.genymobile.scrcpy.wrappers.ServiceManager; +import android.os.Build; + +import java.io.IOException; + public class Settings { public static final String TABLE_SYSTEM = ContentProvider.TABLE_SYSTEM; @@ -15,25 +19,66 @@ public class Settings { this.serviceManager = serviceManager; } - public String getValue(String table, String key) throws SettingsException { - try (ContentProvider provider = serviceManager.getActivityManager().createSettingsProvider()) { - return provider.getValue(table, key); + private static void execSettingsPut(String table, String key, String value) throws SettingsException { + try { + Command.exec("settings", "put", table, key, value); + } catch (IOException | InterruptedException e) { + throw new SettingsException("put", table, key, value, e); } } + private static String execSettingsGet(String table, String key) throws SettingsException { + try { + return Command.execReadLine("settings", "get", table, key); + } catch (IOException | InterruptedException e) { + throw new SettingsException("get", table, key, null, e); + } + } + + public String getValue(String table, String key) throws SettingsException { + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { + // on Android >= 12, it always fails: + try (ContentProvider provider = serviceManager.getActivityManager().createSettingsProvider()) { + return provider.getValue(table, key); + } catch (SettingsException e) { + Ln.w("Could not get settings value via ContentProvider, fallback to settings process", e); + } + } + + return execSettingsGet(table, key); + } + public void putValue(String table, String key, String value) throws SettingsException { - try (ContentProvider provider = serviceManager.getActivityManager().createSettingsProvider()) { - provider.putValue(table, key, value); + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { + // on Android >= 12, it always fails: + try (ContentProvider provider = serviceManager.getActivityManager().createSettingsProvider()) { + provider.putValue(table, key, value); + } catch (SettingsException e) { + Ln.w("Could not put settings value via ContentProvider, fallback to settings process", e); + } } + + execSettingsPut(table, key, value); } public String getAndPutValue(String table, String key, String value) throws SettingsException { - try (ContentProvider provider = serviceManager.getActivityManager().createSettingsProvider()) { - String oldValue = provider.getValue(table, key); - if (!value.equals(oldValue)) { - provider.putValue(table, key, value); + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { + // on Android >= 12, it always fails: + try (ContentProvider provider = serviceManager.getActivityManager().createSettingsProvider()) { + String oldValue = provider.getValue(table, key); + if (!value.equals(oldValue)) { + provider.putValue(table, key, value); + } + return oldValue; + } catch (SettingsException e) { + Ln.w("Could not get and put settings value via ContentProvider, fallback to settings process", e); } - return oldValue; } + + String oldValue = getValue(table, key); + if (!value.equals(oldValue)) { + putValue(table, key, value); + } + return oldValue; } } From 411bb0d18e4d09f10480d2a60dcb7187b4fc62aa Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 17 Nov 2021 10:16:11 +0100 Subject: [PATCH 0793/2244] Move init and cleanup to a separate method PR #2802 --- .../main/java/com/genymobile/scrcpy/Server.java | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index efb295b3..488a877f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -17,11 +17,7 @@ public final class Server { // not instantiable } - 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()); - + private static void initAndCleanUp(Options options) throws IOException { boolean mustDisableShowTouchesOnCleanUp = false; int restoreStayOn = -1; if (options.getShowTouches() || options.getStayAwake()) { @@ -56,6 +52,14 @@ public final class Server { } CleanUp.configure(options.getDisplayId(), restoreStayOn, mustDisableShowTouchesOnCleanUp, true, options.getPowerOffScreenOnClose()); + } + + 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()); + + initAndCleanUp(options); boolean tunnelForward = options.isTunnelForward(); From c29a0bf675dc94fd350ea696d3ffa772651ff4bb Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 17 Nov 2021 10:21:42 +0100 Subject: [PATCH 0794/2244] Do not quit on cleanup configuration failure Cleanup is used for some options like --show-touches to restore the state on exit. If the configuration fails, do not crash the whole process. Just log an error. PR #2802 --- server/src/main/java/com/genymobile/scrcpy/Server.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 488a877f..db5bc7ec 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -17,7 +17,7 @@ public final class Server { // not instantiable } - private static void initAndCleanUp(Options options) throws IOException { + private static void initAndCleanUp(Options options) { boolean mustDisableShowTouchesOnCleanUp = false; int restoreStayOn = -1; if (options.getShowTouches() || options.getStayAwake()) { @@ -51,7 +51,11 @@ public final class Server { } } - CleanUp.configure(options.getDisplayId(), restoreStayOn, mustDisableShowTouchesOnCleanUp, true, options.getPowerOffScreenOnClose()); + try { + CleanUp.configure(options.getDisplayId(), restoreStayOn, mustDisableShowTouchesOnCleanUp, true, options.getPowerOffScreenOnClose()); + } catch (IOException e) { + Ln.e("Could not configure cleanup", e); + } } private static void scrcpy(Options options) throws IOException { From ee93d2aac17ddb996ba42e1f898e34a970ade88a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 17 Nov 2021 10:29:41 +0100 Subject: [PATCH 0795/2244] Configure init and cleanup asynchronously Accessing the settings (like --show-touches) on start should not delay screen mirroring. PR #2802 --- .../main/java/com/genymobile/scrcpy/Server.java | 14 +++++++++++++- 1 file changed, 13 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 db5bc7ec..5a1f4619 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -63,7 +63,7 @@ public final class Server { final Device device = new Device(options); List codecOptions = CodecOption.parse(options.getCodecOptions()); - initAndCleanUp(options); + Thread initThread = startInitThread(options); boolean tunnelForward = options.isTunnelForward(); @@ -95,6 +95,7 @@ public final class Server { // this is expected on close Ln.d("Screen streaming stopped"); } finally { + initThread.interrupt(); if (controllerThread != null) { controllerThread.interrupt(); } @@ -105,6 +106,17 @@ public final class Server { } } + private static Thread startInitThread(final Options options) { + Thread thread = new Thread(new Runnable() { + @Override + public void run() { + initAndCleanUp(options); + } + }); + thread.start(); + return thread; + } + private static Thread startController(final Controller controller) { Thread thread = new Thread(new Runnable() { @Override From de5084690524a9f01ef3012e8e87b116f65625d4 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 18 Nov 2021 18:31:36 +0100 Subject: [PATCH 0796/2244] Close process on check success The interruptible version of the function to check process success (sc_process_check_success_intr()) did not accept a close parameter to avoid a race condition. But as the result, the processes were not closed at all. Add a close parameter, and close the process separately to avoid the race condition. --- app/src/adb_tunnel.c | 10 ++++++---- app/src/server.c | 2 +- app/src/util/process_intr.c | 8 +++++++- app/src/util/process_intr.h | 2 +- 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/app/src/adb_tunnel.c b/app/src/adb_tunnel.c index f02eb83e..df5a3d1f 100644 --- a/app/src/adb_tunnel.c +++ b/app/src/adb_tunnel.c @@ -13,27 +13,29 @@ static bool enable_tunnel_reverse(struct sc_intr *intr, const char *serial, uint16_t local_port) { sc_pid pid = adb_reverse(serial, SC_SOCKET_NAME, local_port); - return sc_process_check_success_intr(intr, pid, "adb reverse"); + return sc_process_check_success_intr(intr, pid, "adb reverse", true); } static bool disable_tunnel_reverse(struct sc_intr *intr, const char *serial) { sc_pid pid = adb_reverse_remove(serial, SC_SOCKET_NAME); - return sc_process_check_success_intr(intr, pid, "adb reverse --remove"); + return sc_process_check_success_intr(intr, pid, "adb reverse --remove", + true); } static bool enable_tunnel_forward(struct sc_intr *intr, const char *serial, uint16_t local_port) { sc_pid pid = adb_forward(serial, local_port, SC_SOCKET_NAME); - return sc_process_check_success_intr(intr, pid, "adb forward"); + return sc_process_check_success_intr(intr, pid, "adb forward", true); } static bool disable_tunnel_forward(struct sc_intr *intr, const char *serial, uint16_t local_port) { sc_pid pid = adb_forward_remove(serial, local_port); - return sc_process_check_success_intr(intr, pid, "adb forward --remove"); + return sc_process_check_success_intr(intr, pid, "adb forward --remove", + true); } static bool diff --git a/app/src/server.c b/app/src/server.c index d792364d..82a6af40 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -114,7 +114,7 @@ push_server(struct sc_intr *intr, const char *serial) { } sc_pid pid = adb_push(serial, server_path, SC_DEVICE_SERVER_PATH); free(server_path); - return sc_process_check_success_intr(intr, pid, "adb push"); + return sc_process_check_success_intr(intr, pid, "adb push", true); } static const char * diff --git a/app/src/util/process_intr.c b/app/src/util/process_intr.c index bb483123..ddf94e97 100644 --- a/app/src/util/process_intr.c +++ b/app/src/util/process_intr.c @@ -2,7 +2,7 @@ bool sc_process_check_success_intr(struct sc_intr *intr, sc_pid pid, - const char *name) { + const char *name, bool close) { if (!sc_intr_set_process(intr, pid)) { // Already interrupted return false; @@ -12,5 +12,11 @@ sc_process_check_success_intr(struct sc_intr *intr, sc_pid pid, bool ret = sc_process_check_success(pid, name, false); sc_intr_set_process(intr, SC_PROCESS_NONE); + + if (close) { + // Close separately + sc_process_close(pid); + } + return ret; } diff --git a/app/src/util/process_intr.h b/app/src/util/process_intr.h index ff0dfc76..a1574ce3 100644 --- a/app/src/util/process_intr.h +++ b/app/src/util/process_intr.h @@ -8,6 +8,6 @@ bool sc_process_check_success_intr(struct sc_intr *intr, sc_pid pid, - const char *name); + const char *name, bool close); #endif From 632bd5697b6e699e8945a79642a11fd3ffdcc289 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 17 Nov 2021 18:36:03 +0100 Subject: [PATCH 0797/2244] Add missing error handling If "adb get-serialno" fails, attempting to read from the uninitialized pipe is incorrect. --- app/src/adb.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/adb.c b/app/src/adb.c index 6251174e..4f50ee5f 100644 --- a/app/src/adb.c +++ b/app/src/adb.c @@ -239,6 +239,9 @@ adb_execute_for_output(const char *serial, const char *const adb_cmd[], const char *name) { sc_pipe pout; sc_pid pid = adb_execute_p(serial, adb_cmd, adb_cmd_len, NULL, &pout, NULL); + if (pid == SC_PROCESS_NONE) { + return -1; + } ssize_t r = sc_pipe_read_all(pout, buf, buf_len); sc_pipe_close(pout); From b30c3a429f63bfa83079d2af48378cd0dbc0bdb8 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 17 Nov 2021 21:58:36 +0100 Subject: [PATCH 0798/2244] Always retrieve device serial This allows to execute all adb commands with the specific -s parameter, even if it is not provided by the user. In practice, calling adb without -s works if there is exactly one device connected. But some adb commands (for example "adb push" on drag & drop) could be executed after another device is connected, so the actual device serial must be known. --- app/src/scrcpy.c | 19 ++++--------------- app/src/server.c | 24 ++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 9643b04e..40bf72a8 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -394,8 +394,11 @@ scrcpy(struct scrcpy_options *options) { // It is necessarily initialized here, since the device is connected struct sc_server_info *info = &s->server.info; + const char *serial = s->server.params.serial; + assert(serial); + if (options->display && options->control) { - if (!file_handler_init(&s->file_handler, options->serial, + if (!file_handler_init(&s->file_handler, serial, options->push_target)) { goto end; } @@ -516,21 +519,7 @@ scrcpy(struct scrcpy_options *options) { #ifdef HAVE_AOA_HID bool aoa_hid_ok = false; - char *serialno = NULL; - - const char *serial = options->serial; - if (!serial) { - serialno = adb_get_serialno(); - if (!serialno) { - LOGE("Could not get device serial"); - goto aoa_hid_end; - } - serial = serialno; - LOGI("Device serial: %s", serial); - } - bool ok = sc_aoa_init(&s->aoa, serial); - free(serialno); if (!ok) { goto aoa_hid_end; } diff --git a/app/src/server.c b/app/src/server.c index 82a6af40..56ba6b68 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -422,12 +422,36 @@ sc_server_on_terminated(void *userdata) { LOGD("Server terminated"); } +static bool +sc_server_fill_serial(struct sc_server *server) { + // Retrieve the actual device immediately if not provided, so that all + // future adb commands are executed for this specific device, even if other + // devices are connected afterwards (without "more than one + // device/emulator" error) + if (!server->params.serial) { + // The serial is owned by sc_server_params, and will be freed on destroy + server->params.serial = adb_get_serialno(); + if (!server->params.serial) { + LOGE("Could not get device serial"); + return false; + } + } + + return true; +} + static int run_server(void *data) { struct sc_server *server = data; + if (!sc_server_fill_serial(server)) { + goto error_connection_failed; + } + const struct sc_server_params *params = &server->params; + LOGD("Device serial: %s", params->serial); + bool ok = push_server(&server->intr, params->serial); if (!ok) { goto error_connection_failed; From 443cb14d6e8c37cb6f633bfd486ff669af9d4121 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 18 Nov 2021 18:48:11 +0100 Subject: [PATCH 0799/2244] Assume non-NULL serial in file_handler The previous commit guarantees to always initialize the serial, so the file_handle may assume it is never NULL. --- app/src/file_handler.c | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/app/src/file_handler.c b/app/src/file_handler.c index fe0ab857..b067d2f1 100644 --- a/app/src/file_handler.c +++ b/app/src/file_handler.c @@ -16,6 +16,7 @@ file_handler_request_destroy(struct file_handler_request *req) { bool file_handler_init(struct file_handler *file_handler, const char *serial, const char *push_target) { + assert(serial); cbuf_init(&file_handler->queue); @@ -30,16 +31,12 @@ file_handler_init(struct file_handler *file_handler, const char *serial, return false; } - if (serial) { - file_handler->serial = strdup(serial); - if (!file_handler->serial) { - LOGW("Could not strdup serial"); - sc_cond_destroy(&file_handler->event_cond); - sc_mutex_destroy(&file_handler->mutex); - return false; - } - } else { - file_handler->serial = NULL; + file_handler->serial = strdup(serial); + if (!file_handler->serial) { + LOGE("Could not strdup serial"); + sc_cond_destroy(&file_handler->event_cond); + sc_mutex_destroy(&file_handler->mutex); + return false; } // lazy initialization From f2781a8b6d5c37d1fa3c0d0291a01a8bd918dddd Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 17 Nov 2021 18:25:56 +0100 Subject: [PATCH 0800/2244] Expose util function to truncate first line Move the local implementation from adb functions to the string util functions. --- app/src/adb.c | 13 +------------ app/src/util/str.c | 11 +++++++++++ app/src/util/str.h | 11 +++++++++++ app/tests/test_str.c | 9 +++++++++ 4 files changed, 32 insertions(+), 12 deletions(-) diff --git a/app/src/adb.c b/app/src/adb.c index 4f50ee5f..efeef4f8 100644 --- a/app/src/adb.c +++ b/app/src/adb.c @@ -253,17 +253,6 @@ adb_execute_for_output(const char *serial, const char *const adb_cmd[], return r; } -static size_t -truncate_first_line(char *data, size_t len) { - data[len - 1] = '\0'; - char *eol = strpbrk(data, "\r\n"); - if (eol) { - *eol = '\0'; - len = eol - data; - } - return len; -} - char * adb_get_serialno(void) { char buf[128]; @@ -275,6 +264,6 @@ adb_get_serialno(void) { return NULL; } - truncate_first_line(buf, r); + sc_str_truncate_first_line(buf, r); return strdup(buf); } diff --git a/app/src/util/str.c b/app/src/util/str.c index 7935c6bb..e63b0270 100644 --- a/app/src/util/str.c +++ b/app/src/util/str.c @@ -291,3 +291,14 @@ error: free(buf.s); return NULL; } + +size_t +sc_str_truncate_first_line(char *data, size_t len) { + data[len - 1] = '\0'; + char *eol = strpbrk(data, "\r\n"); + if (eol) { + *eol = '\0'; + len = eol - data; + } + return len; +} diff --git a/app/src/util/str.h b/app/src/util/str.h index 54e32808..77017e90 100644 --- a/app/src/util/str.h +++ b/app/src/util/str.h @@ -103,4 +103,15 @@ sc_str_from_wchars(const wchar_t *s); char * sc_str_wrap_lines(const char *input, unsigned columns, unsigned indent); +/** + * Truncate the data after the first line + * + * An '\0' is always written at the end of the data, even if no newline + * character is encountered. + * + * Return the size of the resulting line. + */ +size_t +sc_str_truncate_first_line(char *data, size_t len); + #endif diff --git a/app/tests/test_str.c b/app/tests/test_str.c index 2b030885..1cd9a37d 100644 --- a/app/tests/test_str.c +++ b/app/tests/test_str.c @@ -337,6 +337,14 @@ static void test_wrap_lines(void) { free(formatted); } +static void test_truncate_first_line(void) { + char s[] = "hello\nworld\n!"; + size_t len = sc_str_truncate_first_line(s, sizeof(s)); + + assert(len == 5); + assert(!strcmp("hello", s)); +} + int main(int argc, char *argv[]) { (void) argc; (void) argv; @@ -356,5 +364,6 @@ int main(int argc, char *argv[]) { test_parse_integer_with_suffix(); test_strlist_contains(); test_wrap_lines(); + test_truncate_first_line(); return 0; } From 9619ade706824a92ea387bdc9d0d27816bf79da5 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 17 Nov 2021 21:38:59 +0100 Subject: [PATCH 0801/2244] Generalize string trunctation util function Add an additional argument to let the client pass the possible end chars. --- app/src/adb.c | 2 +- app/src/util/str.c | 4 ++-- app/src/util/str.h | 4 ++-- app/tests/test_str.c | 24 +++++++++++++++++++++--- 4 files changed, 26 insertions(+), 8 deletions(-) diff --git a/app/src/adb.c b/app/src/adb.c index efeef4f8..a8712bf4 100644 --- a/app/src/adb.c +++ b/app/src/adb.c @@ -264,6 +264,6 @@ adb_get_serialno(void) { return NULL; } - sc_str_truncate_first_line(buf, r); + sc_str_truncate(buf, r, "\r\n"); return strdup(buf); } diff --git a/app/src/util/str.c b/app/src/util/str.c index e63b0270..fdc1a8b3 100644 --- a/app/src/util/str.c +++ b/app/src/util/str.c @@ -293,9 +293,9 @@ error: } size_t -sc_str_truncate_first_line(char *data, size_t len) { +sc_str_truncate(char *data, size_t len, const char *endchars) { data[len - 1] = '\0'; - char *eol = strpbrk(data, "\r\n"); + char *eol = strpbrk(data, endchars); if (eol) { *eol = '\0'; len = eol - data; diff --git a/app/src/util/str.h b/app/src/util/str.h index 77017e90..521dfff5 100644 --- a/app/src/util/str.h +++ b/app/src/util/str.h @@ -104,7 +104,7 @@ char * sc_str_wrap_lines(const char *input, unsigned columns, unsigned indent); /** - * Truncate the data after the first line + * Truncate the data after any of the characters from `endchars` * * An '\0' is always written at the end of the data, even if no newline * character is encountered. @@ -112,6 +112,6 @@ sc_str_wrap_lines(const char *input, unsigned columns, unsigned indent); * Return the size of the resulting line. */ size_t -sc_str_truncate_first_line(char *data, size_t len); +sc_str_truncate(char *data, size_t len, const char *endchars); #endif diff --git a/app/tests/test_str.c b/app/tests/test_str.c index 1cd9a37d..770ddd95 100644 --- a/app/tests/test_str.c +++ b/app/tests/test_str.c @@ -337,12 +337,30 @@ static void test_wrap_lines(void) { free(formatted); } -static void test_truncate_first_line(void) { +static void test_truncate(void) { char s[] = "hello\nworld\n!"; - size_t len = sc_str_truncate_first_line(s, sizeof(s)); + size_t len = sc_str_truncate(s, sizeof(s), "\n"); assert(len == 5); assert(!strcmp("hello", s)); + + char s2[] = "hello\r\nworkd\r\n!"; + len = sc_str_truncate(s2, sizeof(s2), "\n\r"); + + assert(len == 5); + assert(!strcmp("hello", s)); + + char s3[] = "hello world\n!"; + len = sc_str_truncate(s3, sizeof(s3), " \n\r"); + + assert(len == 5); + assert(!strcmp("hello", s3)); + + char s4[] = "hello "; + len = sc_str_truncate(s4, sizeof(s4), " \n\r"); + + assert(len == 5); + assert(!strcmp("hello", s4)); } int main(int argc, char *argv[]) { @@ -364,6 +382,6 @@ int main(int argc, char *argv[]) { test_parse_integer_with_suffix(); test_strlist_contains(); test_wrap_lines(); - test_truncate_first_line(); + test_truncate(); return 0; } From cb655315330f5435b7849d256e33f8c148fd38d6 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 18 Nov 2021 09:34:54 +0100 Subject: [PATCH 0802/2244] Simplify sc_str_truncate() Use strcspn() to get the prefix length directly. --- app/src/util/str.c | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/app/src/util/str.c b/app/src/util/str.c index fdc1a8b3..3bd2752f 100644 --- a/app/src/util/str.c +++ b/app/src/util/str.c @@ -295,10 +295,7 @@ error: size_t sc_str_truncate(char *data, size_t len, const char *endchars) { data[len - 1] = '\0'; - char *eol = strpbrk(data, endchars); - if (eol) { - *eol = '\0'; - len = eol - data; - } - return len; + size_t idx = strcspn(data, endchars); + data[idx] = '\0'; + return idx; } From ea454e9cee1cedb1fb7b8aed7fde46bd76bb031b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 17 Nov 2021 18:30:54 +0100 Subject: [PATCH 0803/2244] Add interruptible function to read from pipe This will avoid to block Ctrl+c if the process the pipe is read from takes too much time. --- app/src/util/process_intr.c | 28 ++++++++++++++++++++++++++++ app/src/util/process_intr.h | 8 ++++++++ 2 files changed, 36 insertions(+) diff --git a/app/src/util/process_intr.c b/app/src/util/process_intr.c index ddf94e97..dcb81100 100644 --- a/app/src/util/process_intr.c +++ b/app/src/util/process_intr.c @@ -20,3 +20,31 @@ sc_process_check_success_intr(struct sc_intr *intr, sc_pid pid, return ret; } + +ssize_t +sc_pipe_read_intr(struct sc_intr *intr, sc_pid pid, sc_pipe pipe, char *data, + size_t len) { + if (!sc_intr_set_process(intr, pid)) { + // Already interrupted + return false; + } + + ssize_t ret = sc_pipe_read(pipe, data, len); + + sc_intr_set_process(intr, SC_PROCESS_NONE); + return ret; +} + +ssize_t +sc_pipe_read_all_intr(struct sc_intr *intr, sc_pid pid, sc_pipe pipe, + char *data, size_t len) { + if (!sc_intr_set_process(intr, pid)) { + // Already interrupted + return false; + } + + ssize_t ret = sc_pipe_read_all(pipe, data, len); + + sc_intr_set_process(intr, SC_PROCESS_NONE); + return ret; +} diff --git a/app/src/util/process_intr.h b/app/src/util/process_intr.h index a1574ce3..9406d889 100644 --- a/app/src/util/process_intr.h +++ b/app/src/util/process_intr.h @@ -10,4 +10,12 @@ bool sc_process_check_success_intr(struct sc_intr *intr, sc_pid pid, const char *name, bool close); +ssize_t +sc_pipe_read_intr(struct sc_intr *intr, sc_pid pid, sc_pipe pipe, char *data, + size_t len); + +ssize_t +sc_pipe_read_all_intr(struct sc_intr *intr, sc_pid pid, sc_pipe pipe, + char *data, size_t len); + #endif From 0426ae885c13e5901a4994f01086839a3bc13728 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 17 Nov 2021 18:47:20 +0100 Subject: [PATCH 0804/2244] Make "adb get-serialno" interruptible All process executions must be interruptible, so that Ctrl+c reacts immediately. --- app/src/adb.c | 35 +++-------------------------------- app/src/adb.h | 10 +++++++--- app/src/server.c | 25 ++++++++++++++++++++++++- 3 files changed, 34 insertions(+), 36 deletions(-) diff --git a/app/src/adb.c b/app/src/adb.c index a8712bf4..d98efe6f 100644 --- a/app/src/adb.c +++ b/app/src/adb.c @@ -233,37 +233,8 @@ adb_install(const char *serial, const char *local) { return pid; } -static ssize_t -adb_execute_for_output(const char *serial, const char *const adb_cmd[], - size_t adb_cmd_len, char *buf, size_t buf_len, - const char *name) { - sc_pipe pout; - sc_pid pid = adb_execute_p(serial, adb_cmd, adb_cmd_len, NULL, &pout, NULL); - if (pid == SC_PROCESS_NONE) { - return -1; - } - - ssize_t r = sc_pipe_read_all(pout, buf, buf_len); - sc_pipe_close(pout); - - if (!sc_process_check_success(pid, name, true)) { - return -1; - } - - return r; -} - -char * -adb_get_serialno(void) { - char buf[128]; - +sc_pid +adb_get_serialno(sc_pipe *pout) { const char *const adb_cmd[] = {"get-serialno"}; - ssize_t r = adb_execute_for_output(NULL, adb_cmd, ARRAY_LEN(adb_cmd), - buf, sizeof(buf), "get-serialno"); - if (r <= 0) { - return NULL; - } - - sc_str_truncate(buf, r, "\r\n"); - return strdup(buf); + return adb_execute_p(NULL, adb_cmd, ARRAY_LEN(adb_cmd), NULL, pout, NULL); } diff --git a/app/src/adb.h b/app/src/adb.h index 085b3e6b..d3b3faae 100644 --- a/app/src/adb.h +++ b/app/src/adb.h @@ -35,8 +35,12 @@ adb_push(const char *serial, const char *local, const char *remote); sc_pid adb_install(const char *serial, const char *local); -// Return the result of "adb get-serialno". -char * -adb_get_serialno(void); +/** + * Execute `adb get-serialno` + * + * The result can be read from the output parameter `pout`. + */ +sc_pid +adb_get_serialno(sc_pipe *pout); #endif diff --git a/app/src/server.c b/app/src/server.c index 56ba6b68..5ad8d70f 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -422,6 +422,29 @@ sc_server_on_terminated(void *userdata) { LOGD("Server terminated"); } +static char * +sc_server_get_serialno(struct sc_intr *intr) { + sc_pipe pout; + sc_pid pid = adb_get_serialno(&pout); + if (pid == SC_PROCESS_NONE) { + return false; + } + + char buf[128]; + ssize_t r = sc_pipe_read_all_intr(intr, pid, pout, buf, sizeof(buf)); + sc_pipe_close(pout); + + bool ok = + sc_process_check_success_intr(intr, pid, "adb get-serialno", true); + if (!ok) { + return NULL; + } + + sc_str_truncate(buf, r, " \r\n"); + + return strdup(buf); +} + static bool sc_server_fill_serial(struct sc_server *server) { // Retrieve the actual device immediately if not provided, so that all @@ -430,7 +453,7 @@ sc_server_fill_serial(struct sc_server *server) { // device/emulator" error) if (!server->params.serial) { // The serial is owned by sc_server_params, and will be freed on destroy - server->params.serial = adb_get_serialno(); + server->params.serial = sc_server_get_serialno(&server->intr); if (!server->params.serial) { LOGE("Could not get device serial"); return false; From 13fd693b501b59a95e4e72b7ccba7c267cdc8590 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 17 Nov 2021 21:53:11 +0100 Subject: [PATCH 0805/2244] Simplify adb_execute_p() Only pass the stdout pipe as parameter, scrcpy never writes to stdin or reads from stderr of an adb process. --- app/src/adb.c | 8 ++++---- app/src/adb.h | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/src/adb.c b/app/src/adb.c index d98efe6f..3a5faed3 100644 --- a/app/src/adb.c +++ b/app/src/adb.c @@ -111,7 +111,7 @@ show_adb_err_msg(enum sc_process_result err, const char *const argv[]) { sc_pid adb_execute_p(const char *serial, const char *const adb_cmd[], - size_t len, sc_pipe *pin, sc_pipe *pout, sc_pipe *perr) { + size_t len, sc_pipe *pout) { int i; sc_pid pid; @@ -132,7 +132,7 @@ adb_execute_p(const char *serial, const char *const adb_cmd[], memcpy(&argv[i], adb_cmd, len * sizeof(const char *)); argv[len + i] = NULL; enum sc_process_result r = - sc_process_execute_p(argv, &pid, pin, pout, perr); + sc_process_execute_p(argv, &pid, NULL, pout, NULL); if (r != SC_PROCESS_SUCCESS) { show_adb_err_msg(r, argv); pid = SC_PROCESS_NONE; @@ -144,7 +144,7 @@ adb_execute_p(const char *serial, const char *const adb_cmd[], sc_pid adb_execute(const char *serial, const char *const adb_cmd[], size_t len) { - return adb_execute_p(serial, adb_cmd, len, NULL, NULL, NULL); + return adb_execute_p(serial, adb_cmd, len, NULL); } sc_pid @@ -236,5 +236,5 @@ adb_install(const char *serial, const char *local) { sc_pid adb_get_serialno(sc_pipe *pout) { const char *const adb_cmd[] = {"get-serialno"}; - return adb_execute_p(NULL, adb_cmd, ARRAY_LEN(adb_cmd), NULL, pout, NULL); + return adb_execute_p(NULL, adb_cmd, ARRAY_LEN(adb_cmd), pout); } diff --git a/app/src/adb.h b/app/src/adb.h index d3b3faae..0107e6d4 100644 --- a/app/src/adb.h +++ b/app/src/adb.h @@ -12,8 +12,8 @@ sc_pid adb_execute(const char *serial, const char *const adb_cmd[], size_t len); sc_pid -adb_execute_p(const char *serial, const char *const adb_cmd[], - size_t len, sc_pipe *pin, sc_pipe *pout, sc_pipe *perr); +adb_execute_p(const char *serial, const char *const adb_cmd[], size_t len, + sc_pipe *pout); sc_pid adb_forward(const char *serial, uint16_t local_port, From 55648d4d6479efd1052091fda34e60a871a2c1c1 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 18 Nov 2021 19:46:40 +0100 Subject: [PATCH 0806/2244] Improve file_handler readability Use local variables as short name aliases. --- app/src/file_handler.c | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/app/src/file_handler.c b/app/src/file_handler.c index b067d2f1..1e8d2641 100644 --- a/app/src/file_handler.c +++ b/app/src/file_handler.c @@ -104,6 +104,12 @@ static int run_file_handler(void *data) { struct file_handler *file_handler = data; + const char *serial = file_handler->serial; + assert(serial); + + const char *push_target = file_handler->push_target; + assert(push_target); + for (;;) { sc_mutex_lock(&file_handler->mutex); file_handler->current_process = SC_PROCESS_NONE; @@ -123,11 +129,10 @@ run_file_handler(void *data) { sc_pid pid; if (req.action == ACTION_INSTALL_APK) { LOGI("Installing %s...", req.file); - pid = install_apk(file_handler->serial, req.file); + pid = install_apk(serial, req.file); } else { LOGI("Pushing %s...", req.file); - pid = push_file(file_handler->serial, req.file, - file_handler->push_target); + pid = push_file(serial, req.file, push_target); } file_handler->current_process = pid; sc_mutex_unlock(&file_handler->mutex); @@ -140,11 +145,9 @@ run_file_handler(void *data) { } } else { if (sc_process_check_success(pid, "adb push", false)) { - LOGI("%s successfully pushed to %s", req.file, - file_handler->push_target); + LOGI("%s successfully pushed to %s", req.file, push_target); } else { - LOGE("Failed to push %s to %s", req.file, - file_handler->push_target); + LOGE("Failed to push %s to %s", req.file, push_target); } } From 0ba2686e1db2fff2e65fc53a478d1266aca6b776 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 12 Nov 2021 22:43:22 +0100 Subject: [PATCH 0807/2244] Simplify file_handler Call the target functions directly. --- app/src/file_handler.c | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/app/src/file_handler.c b/app/src/file_handler.c index 1e8d2641..73b6a004 100644 --- a/app/src/file_handler.c +++ b/app/src/file_handler.c @@ -62,16 +62,6 @@ file_handler_destroy(struct file_handler *file_handler) { } } -static sc_pid -install_apk(const char *serial, const char *file) { - return adb_install(serial, file); -} - -static sc_pid -push_file(const char *serial, const char *file, const char *push_target) { - return adb_push(serial, file, push_target); -} - bool file_handler_request(struct file_handler *file_handler, file_handler_action_t action, char *file) { @@ -129,10 +119,10 @@ run_file_handler(void *data) { sc_pid pid; if (req.action == ACTION_INSTALL_APK) { LOGI("Installing %s...", req.file); - pid = install_apk(serial, req.file); + pid = adb_install(serial, req.file); } else { LOGI("Pushing %s...", req.file); - pid = push_file(serial, req.file, push_target); + pid = adb_push(serial, req.file, push_target); } file_handler->current_process = pid; sc_mutex_unlock(&file_handler->mutex); From 84334cf7db68732eb92caf6cf9682c7ef2c426f6 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 18 Nov 2021 21:33:25 +0100 Subject: [PATCH 0808/2244] Use sc_intr in file_handler Replace manual interruption handling by the recent sc_intr mechanism. --- app/src/file_handler.c | 34 +++++++++++++++++----------------- app/src/file_handler.h | 4 +++- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/app/src/file_handler.c b/app/src/file_handler.c index 73b6a004..d84ae4cb 100644 --- a/app/src/file_handler.c +++ b/app/src/file_handler.c @@ -5,6 +5,7 @@ #include "adb.h" #include "util/log.h" +#include "util/process_intr.h" #define DEFAULT_PUSH_TARGET "/sdcard/Download/" @@ -31,9 +32,17 @@ file_handler_init(struct file_handler *file_handler, const char *serial, return false; } + ok = sc_intr_init(&file_handler->intr); + if (!ok) { + LOGE("Could not create intr"); + sc_cond_destroy(&file_handler->event_cond); + sc_mutex_destroy(&file_handler->mutex); + } + file_handler->serial = strdup(serial); if (!file_handler->serial) { LOGE("Could not strdup serial"); + sc_intr_destroy(&file_handler->intr); sc_cond_destroy(&file_handler->event_cond); sc_mutex_destroy(&file_handler->mutex); return false; @@ -43,7 +52,6 @@ file_handler_init(struct file_handler *file_handler, const char *serial, file_handler->initialized = false; file_handler->stopped = false; - file_handler->current_process = SC_PROCESS_NONE; file_handler->push_target = push_target ? push_target : DEFAULT_PUSH_TARGET; @@ -54,6 +62,7 @@ void file_handler_destroy(struct file_handler *file_handler) { sc_cond_destroy(&file_handler->event_cond); sc_mutex_destroy(&file_handler->mutex); + sc_intr_destroy(&file_handler->intr); free(file_handler->serial); struct file_handler_request req; @@ -93,6 +102,7 @@ file_handler_request(struct file_handler *file_handler, static int run_file_handler(void *data) { struct file_handler *file_handler = data; + struct sc_intr *intr = &file_handler->intr; const char *serial = file_handler->serial; assert(serial); @@ -102,7 +112,6 @@ run_file_handler(void *data) { for (;;) { sc_mutex_lock(&file_handler->mutex); - file_handler->current_process = SC_PROCESS_NONE; while (!file_handler->stopped && cbuf_is_empty(&file_handler->queue)) { sc_cond_wait(&file_handler->event_cond, &file_handler->mutex); } @@ -115,6 +124,7 @@ run_file_handler(void *data) { bool non_empty = cbuf_take(&file_handler->queue, &req); assert(non_empty); (void) non_empty; + sc_mutex_unlock(&file_handler->mutex); sc_pid pid; if (req.action == ACTION_INSTALL_APK) { @@ -124,30 +134,24 @@ run_file_handler(void *data) { LOGI("Pushing %s...", req.file); pid = adb_push(serial, req.file, push_target); } - file_handler->current_process = pid; - sc_mutex_unlock(&file_handler->mutex); if (req.action == ACTION_INSTALL_APK) { - if (sc_process_check_success(pid, "adb install", false)) { + if (sc_process_check_success_intr(intr, pid, "adb install", + false)) { LOGI("%s successfully installed", req.file); } else { LOGE("Failed to install %s", req.file); } } else { - if (sc_process_check_success(pid, "adb push", false)) { + if (sc_process_check_success_intr(intr, pid, "adb push", false)) { LOGI("%s successfully pushed to %s", req.file, push_target); } else { LOGE("Failed to push %s to %s", req.file, push_target); } } - sc_mutex_lock(&file_handler->mutex); // Close the process (it is necessarily already terminated) - // Execute this call with mutex locked to avoid race conditions with - // file_handler_stop() - sc_process_close(file_handler->current_process); - file_handler->current_process = SC_PROCESS_NONE; - sc_mutex_unlock(&file_handler->mutex); + sc_process_close(pid); file_handler_request_destroy(&req); } @@ -173,11 +177,7 @@ file_handler_stop(struct file_handler *file_handler) { sc_mutex_lock(&file_handler->mutex); file_handler->stopped = true; sc_cond_signal(&file_handler->event_cond); - if (file_handler->current_process != SC_PROCESS_NONE) { - if (!sc_process_terminate(file_handler->current_process)) { - LOGW("Could not terminate push/install process"); - } - } + sc_intr_interrupt(&file_handler->intr); sc_mutex_unlock(&file_handler->mutex); } diff --git a/app/src/file_handler.h b/app/src/file_handler.h index e2067533..4c0313cc 100644 --- a/app/src/file_handler.h +++ b/app/src/file_handler.h @@ -8,6 +8,7 @@ #include "adb.h" #include "util/cbuf.h" #include "util/thread.h" +#include "util/intr.h" typedef enum { ACTION_INSTALL_APK, @@ -29,8 +30,9 @@ struct file_handler { sc_cond event_cond; bool stopped; bool initialized; - sc_pid current_process; struct file_handler_request_queue queue; + + struct sc_intr intr; }; bool From afb5a5e80f8946dc4fb7d68d753ed3d0cd6bc877 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 18 Nov 2021 21:47:17 +0100 Subject: [PATCH 0809/2244] Rename adb functions to adb_exec_* This paves the way to replace them by more user-friendly functions that will call them internally. --- app/src/adb.c | 18 +++++++++--------- app/src/adb.h | 18 +++++++++--------- app/src/adb_tunnel.c | 8 ++++---- app/src/file_handler.c | 4 ++-- app/src/server.c | 4 ++-- 5 files changed, 26 insertions(+), 26 deletions(-) diff --git a/app/src/adb.c b/app/src/adb.c index 3a5faed3..608abca1 100644 --- a/app/src/adb.c +++ b/app/src/adb.c @@ -148,8 +148,8 @@ adb_execute(const char *serial, const char *const adb_cmd[], size_t len) { } sc_pid -adb_forward(const char *serial, uint16_t local_port, - const char *device_socket_name) { +adb_exec_forward(const char *serial, uint16_t local_port, + const char *device_socket_name) { char local[4 + 5 + 1]; // tcp:PORT char remote[108 + 14 + 1]; // localabstract:NAME sprintf(local, "tcp:%" PRIu16, local_port); @@ -159,7 +159,7 @@ adb_forward(const char *serial, uint16_t local_port, } sc_pid -adb_forward_remove(const char *serial, uint16_t local_port) { +adb_exec_forward_remove(const char *serial, uint16_t local_port) { char local[4 + 5 + 1]; // tcp:PORT sprintf(local, "tcp:%" PRIu16, local_port); const char *const adb_cmd[] = {"forward", "--remove", local}; @@ -167,8 +167,8 @@ adb_forward_remove(const char *serial, uint16_t local_port) { } sc_pid -adb_reverse(const char *serial, const char *device_socket_name, - uint16_t local_port) { +adb_exec_reverse(const char *serial, const char *device_socket_name, + uint16_t local_port) { char local[4 + 5 + 1]; // tcp:PORT char remote[108 + 14 + 1]; // localabstract:NAME sprintf(local, "tcp:%" PRIu16, local_port); @@ -178,7 +178,7 @@ adb_reverse(const char *serial, const char *device_socket_name, } sc_pid -adb_reverse_remove(const char *serial, const char *device_socket_name) { +adb_exec_reverse_remove(const char *serial, const char *device_socket_name) { char remote[108 + 14 + 1]; // localabstract:NAME snprintf(remote, sizeof(remote), "localabstract:%s", device_socket_name); const char *const adb_cmd[] = {"reverse", "--remove", remote}; @@ -186,7 +186,7 @@ adb_reverse_remove(const char *serial, const char *device_socket_name) { } sc_pid -adb_push(const char *serial, const char *local, const char *remote) { +adb_exec_push(const char *serial, const char *local, const char *remote) { #ifdef __WINDOWS__ // Windows will parse the string, so the paths must be quoted // (see sys/win/command.c) @@ -213,7 +213,7 @@ adb_push(const char *serial, const char *local, const char *remote) { } sc_pid -adb_install(const char *serial, const char *local) { +adb_exec_install(const char *serial, const char *local) { #ifdef __WINDOWS__ // Windows will parse the string, so the local name must be quoted // (see sys/win/command.c) @@ -234,7 +234,7 @@ adb_install(const char *serial, const char *local) { } sc_pid -adb_get_serialno(sc_pipe *pout) { +adb_exec_get_serialno(sc_pipe *pout) { const char *const adb_cmd[] = {"get-serialno"}; return adb_execute_p(NULL, adb_cmd, ARRAY_LEN(adb_cmd), pout); } diff --git a/app/src/adb.h b/app/src/adb.h index 0107e6d4..c2c21343 100644 --- a/app/src/adb.h +++ b/app/src/adb.h @@ -16,24 +16,24 @@ adb_execute_p(const char *serial, const char *const adb_cmd[], size_t len, sc_pipe *pout); sc_pid -adb_forward(const char *serial, uint16_t local_port, - const char *device_socket_name); +adb_exec_forward(const char *serial, uint16_t local_port, + const char *device_socket_name); sc_pid -adb_forward_remove(const char *serial, uint16_t local_port); +adb_exec_forward_remove(const char *serial, uint16_t local_port); sc_pid -adb_reverse(const char *serial, const char *device_socket_name, - uint16_t local_port); +adb_exec_reverse(const char *serial, const char *device_socket_name, + uint16_t local_port); sc_pid -adb_reverse_remove(const char *serial, const char *device_socket_name); +adb_exec_reverse_remove(const char *serial, const char *device_socket_name); sc_pid -adb_push(const char *serial, const char *local, const char *remote); +adb_exec_push(const char *serial, const char *local, const char *remote); sc_pid -adb_install(const char *serial, const char *local); +adb_exec_install(const char *serial, const char *local); /** * Execute `adb get-serialno` @@ -41,6 +41,6 @@ adb_install(const char *serial, const char *local); * The result can be read from the output parameter `pout`. */ sc_pid -adb_get_serialno(sc_pipe *pout); +adb_exec_get_serialno(sc_pipe *pout); #endif diff --git a/app/src/adb_tunnel.c b/app/src/adb_tunnel.c index df5a3d1f..b5850bf8 100644 --- a/app/src/adb_tunnel.c +++ b/app/src/adb_tunnel.c @@ -12,13 +12,13 @@ static bool enable_tunnel_reverse(struct sc_intr *intr, const char *serial, uint16_t local_port) { - sc_pid pid = adb_reverse(serial, SC_SOCKET_NAME, local_port); + sc_pid pid = adb_exec_reverse(serial, SC_SOCKET_NAME, local_port); return sc_process_check_success_intr(intr, pid, "adb reverse", true); } static bool disable_tunnel_reverse(struct sc_intr *intr, const char *serial) { - sc_pid pid = adb_reverse_remove(serial, SC_SOCKET_NAME); + sc_pid pid = adb_exec_reverse_remove(serial, SC_SOCKET_NAME); return sc_process_check_success_intr(intr, pid, "adb reverse --remove", true); } @@ -26,14 +26,14 @@ disable_tunnel_reverse(struct sc_intr *intr, const char *serial) { static bool enable_tunnel_forward(struct sc_intr *intr, const char *serial, uint16_t local_port) { - sc_pid pid = adb_forward(serial, local_port, SC_SOCKET_NAME); + sc_pid pid = adb_exec_forward(serial, local_port, SC_SOCKET_NAME); return sc_process_check_success_intr(intr, pid, "adb forward", true); } static bool disable_tunnel_forward(struct sc_intr *intr, const char *serial, uint16_t local_port) { - sc_pid pid = adb_forward_remove(serial, local_port); + sc_pid pid = adb_exec_forward_remove(serial, local_port); return sc_process_check_success_intr(intr, pid, "adb forward --remove", true); } diff --git a/app/src/file_handler.c b/app/src/file_handler.c index d84ae4cb..f7c79cff 100644 --- a/app/src/file_handler.c +++ b/app/src/file_handler.c @@ -129,10 +129,10 @@ run_file_handler(void *data) { sc_pid pid; if (req.action == ACTION_INSTALL_APK) { LOGI("Installing %s...", req.file); - pid = adb_install(serial, req.file); + pid = adb_exec_install(serial, req.file); } else { LOGI("Pushing %s...", req.file); - pid = adb_push(serial, req.file, push_target); + pid = adb_exec_push(serial, req.file, push_target); } if (req.action == ACTION_INSTALL_APK) { diff --git a/app/src/server.c b/app/src/server.c index 5ad8d70f..6d535202 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -112,7 +112,7 @@ push_server(struct sc_intr *intr, const char *serial) { free(server_path); return false; } - sc_pid pid = adb_push(serial, server_path, SC_DEVICE_SERVER_PATH); + sc_pid pid = adb_exec_push(serial, server_path, SC_DEVICE_SERVER_PATH); free(server_path); return sc_process_check_success_intr(intr, pid, "adb push", true); } @@ -425,7 +425,7 @@ sc_server_on_terminated(void *userdata) { static char * sc_server_get_serialno(struct sc_intr *intr) { sc_pipe pout; - sc_pid pid = adb_get_serialno(&pout); + sc_pid pid = adb_exec_get_serialno(&pout); if (pid == SC_PROCESS_NONE) { return false; } From b7559744a7164124e7d358d2cb3bd744993487a2 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 18 Nov 2021 22:08:15 +0100 Subject: [PATCH 0810/2244] Expose new user-friendly adb functions Expose interruptible adb functions which return the expected result directly, without exposing the process (sc_pid) to the caller. --- app/src/adb.c | 68 +++++++++++++++++++++++++++++++++++++++++++++++++++ app/src/adb.h | 32 ++++++++++++++++++++++++ 2 files changed, 100 insertions(+) diff --git a/app/src/adb.c b/app/src/adb.c index 608abca1..92a52b5b 100644 --- a/app/src/adb.c +++ b/app/src/adb.c @@ -7,6 +7,7 @@ #include "util/file.h" #include "util/log.h" +#include "util/process_intr.h" #include "util/str.h" static const char *adb_command; @@ -238,3 +239,70 @@ adb_exec_get_serialno(sc_pipe *pout) { const char *const adb_cmd[] = {"get-serialno"}; return adb_execute_p(NULL, adb_cmd, ARRAY_LEN(adb_cmd), pout); } + +bool +adb_forward(struct sc_intr *intr, const char *serial, uint16_t local_port, + const char *device_socket_name) { + sc_pid pid = adb_exec_forward(serial, local_port, device_socket_name); + return sc_process_check_success_intr(intr, pid, "adb forward", true); +} + +bool +adb_forward_remove(struct sc_intr *intr, const char *serial, + uint16_t local_port) { + sc_pid pid = adb_exec_forward_remove(serial, local_port); + return sc_process_check_success_intr(intr, pid, "adb forward --remove", + true); +} + +bool +adb_reverse(struct sc_intr *intr, const char *serial, + const char *device_socket_name, uint16_t local_port) { + sc_pid pid = adb_exec_reverse(serial, device_socket_name, local_port); + return sc_process_check_success_intr(intr, pid, "adb reverse", true); +} + +bool +adb_reverse_remove(struct sc_intr *intr, const char *serial, + const char *device_socket_name) { + sc_pid pid = adb_exec_reverse_remove(serial, device_socket_name); + return sc_process_check_success_intr(intr, pid, "adb reverse --remove", + true); +} + +bool +adb_push(struct sc_intr *intr, const char *serial, const char *local, + const char *remote) { + sc_pid pid = adb_exec_push(serial, local, remote); + return sc_process_check_success_intr(intr, pid, "adb push", true); +} + +bool +adb_install(struct sc_intr *intr, const char *serial, const char *local) { + sc_pid pid = adb_exec_install(serial, local); + return sc_process_check_success_intr(intr, pid, "adb install", true); +} + +char * +adb_get_serialno(struct sc_intr *intr) { + sc_pipe pout; + sc_pid pid = adb_exec_get_serialno(&pout); + if (pid == SC_PROCESS_NONE) { + LOGE("Could not execute \"adb get-serialno\""); + return NULL; + } + + char buf[128]; + ssize_t r = sc_pipe_read_all_intr(intr, pid, pout, buf, sizeof(buf)); + sc_pipe_close(pout); + + bool ok = + sc_process_check_success_intr(intr, pid, "adb get-serialno", true); + if (!ok) { + return NULL; + } + + sc_str_truncate(buf, r, " \r\n"); + + return strdup(buf); +} diff --git a/app/src/adb.h b/app/src/adb.h index c2c21343..3714c17f 100644 --- a/app/src/adb.h +++ b/app/src/adb.h @@ -7,6 +7,7 @@ #include #include "util/process.h" +#include "util/intr.h" sc_pid adb_execute(const char *serial, const char *const adb_cmd[], size_t len); @@ -43,4 +44,35 @@ adb_exec_install(const char *serial, const char *local); sc_pid adb_exec_get_serialno(sc_pipe *pout); +bool +adb_forward(struct sc_intr *intr, const char *serial, uint16_t local_port, + const char *device_socket_name); + +bool +adb_forward_remove(struct sc_intr *intr, const char *serial, + uint16_t local_port); + +bool +adb_reverse(struct sc_intr *intr, const char *serial, + const char *device_socket_name, uint16_t local_port); + +bool +adb_reverse_remove(struct sc_intr *intr, const char *serial, + const char *device_socket_name); + +bool +adb_push(struct sc_intr *intr, const char *serial, const char *local, + const char *remote); + +bool +adb_install(struct sc_intr *intr, const char *serial, const char *local); + +/** + * Execute `adb get-serialno` + * + * Return the result, to be freed by the caller, or NULL on error. + */ +char * +adb_get_serialno(struct sc_intr *intr); + #endif From ce225f019accf3c9e043a04a7a405d76fb5b0888 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 18 Nov 2021 22:11:19 +0100 Subject: [PATCH 0811/2244] Use new user-friendly adb API Replace the adb_exec_*() calls by the new adb functions to simplify. --- app/src/adb_tunnel.c | 14 ++++---------- app/src/file_handler.c | 19 +++++-------------- app/src/server.c | 29 +++-------------------------- 3 files changed, 12 insertions(+), 50 deletions(-) diff --git a/app/src/adb_tunnel.c b/app/src/adb_tunnel.c index b5850bf8..b94609e2 100644 --- a/app/src/adb_tunnel.c +++ b/app/src/adb_tunnel.c @@ -12,30 +12,24 @@ static bool enable_tunnel_reverse(struct sc_intr *intr, const char *serial, uint16_t local_port) { - sc_pid pid = adb_exec_reverse(serial, SC_SOCKET_NAME, local_port); - return sc_process_check_success_intr(intr, pid, "adb reverse", true); + return adb_reverse(intr, serial, SC_SOCKET_NAME, local_port); } static bool disable_tunnel_reverse(struct sc_intr *intr, const char *serial) { - sc_pid pid = adb_exec_reverse_remove(serial, SC_SOCKET_NAME); - return sc_process_check_success_intr(intr, pid, "adb reverse --remove", - true); + return adb_reverse_remove(intr, serial, SC_SOCKET_NAME); } static bool enable_tunnel_forward(struct sc_intr *intr, const char *serial, uint16_t local_port) { - sc_pid pid = adb_exec_forward(serial, local_port, SC_SOCKET_NAME); - return sc_process_check_success_intr(intr, pid, "adb forward", true); + return adb_forward(intr, serial, local_port, SC_SOCKET_NAME); } static bool disable_tunnel_forward(struct sc_intr *intr, const char *serial, uint16_t local_port) { - sc_pid pid = adb_exec_forward_remove(serial, local_port); - return sc_process_check_success_intr(intr, pid, "adb forward --remove", - true); + return adb_forward_remove(intr, serial, local_port); } static bool diff --git a/app/src/file_handler.c b/app/src/file_handler.c index f7c79cff..eead2117 100644 --- a/app/src/file_handler.c +++ b/app/src/file_handler.c @@ -126,33 +126,24 @@ run_file_handler(void *data) { (void) non_empty; sc_mutex_unlock(&file_handler->mutex); - sc_pid pid; if (req.action == ACTION_INSTALL_APK) { LOGI("Installing %s...", req.file); - pid = adb_exec_install(serial, req.file); - } else { - LOGI("Pushing %s...", req.file); - pid = adb_exec_push(serial, req.file, push_target); - } - - if (req.action == ACTION_INSTALL_APK) { - if (sc_process_check_success_intr(intr, pid, "adb install", - false)) { + bool ok = adb_install(intr, serial, req.file); + if (ok) { LOGI("%s successfully installed", req.file); } else { LOGE("Failed to install %s", req.file); } } else { - if (sc_process_check_success_intr(intr, pid, "adb push", false)) { + LOGI("Pushing %s...", req.file); + bool ok = adb_push(intr, serial, req.file, push_target); + if (ok) { LOGI("%s successfully pushed to %s", req.file, push_target); } else { LOGE("Failed to push %s to %s", req.file, push_target); } } - // Close the process (it is necessarily already terminated) - sc_process_close(pid); - file_handler_request_destroy(&req); } return 0; diff --git a/app/src/server.c b/app/src/server.c index 6d535202..3f12db62 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -112,9 +112,9 @@ push_server(struct sc_intr *intr, const char *serial) { free(server_path); return false; } - sc_pid pid = adb_exec_push(serial, server_path, SC_DEVICE_SERVER_PATH); + bool ok = adb_push(intr, serial, server_path, SC_DEVICE_SERVER_PATH); free(server_path); - return sc_process_check_success_intr(intr, pid, "adb push", true); + return ok; } static const char * @@ -422,29 +422,6 @@ sc_server_on_terminated(void *userdata) { LOGD("Server terminated"); } -static char * -sc_server_get_serialno(struct sc_intr *intr) { - sc_pipe pout; - sc_pid pid = adb_exec_get_serialno(&pout); - if (pid == SC_PROCESS_NONE) { - return false; - } - - char buf[128]; - ssize_t r = sc_pipe_read_all_intr(intr, pid, pout, buf, sizeof(buf)); - sc_pipe_close(pout); - - bool ok = - sc_process_check_success_intr(intr, pid, "adb get-serialno", true); - if (!ok) { - return NULL; - } - - sc_str_truncate(buf, r, " \r\n"); - - return strdup(buf); -} - static bool sc_server_fill_serial(struct sc_server *server) { // Retrieve the actual device immediately if not provided, so that all @@ -453,7 +430,7 @@ sc_server_fill_serial(struct sc_server *server) { // device/emulator" error) if (!server->params.serial) { // The serial is owned by sc_server_params, and will be freed on destroy - server->params.serial = sc_server_get_serialno(&server->intr); + server->params.serial = adb_get_serialno(&server->intr); if (!server->params.serial) { LOGE("Could not get device serial"); return false; From 3fdbd994e0f0a2608eed28ca8047b8bc1ec3f92c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 18 Nov 2021 22:12:56 +0100 Subject: [PATCH 0812/2244] Privatize low-level adb functions Only expose the interruptible user-friendly API. --- app/src/adb.c | 16 ++++++++-------- app/src/adb.h | 33 --------------------------------- 2 files changed, 8 insertions(+), 41 deletions(-) diff --git a/app/src/adb.c b/app/src/adb.c index 92a52b5b..630a1952 100644 --- a/app/src/adb.c +++ b/app/src/adb.c @@ -110,7 +110,7 @@ show_adb_err_msg(enum sc_process_result err, const char *const argv[]) { free(buf); } -sc_pid +static sc_pid adb_execute_p(const char *serial, const char *const adb_cmd[], size_t len, sc_pipe *pout) { int i; @@ -148,7 +148,7 @@ adb_execute(const char *serial, const char *const adb_cmd[], size_t len) { return adb_execute_p(serial, adb_cmd, len, NULL); } -sc_pid +static sc_pid adb_exec_forward(const char *serial, uint16_t local_port, const char *device_socket_name) { char local[4 + 5 + 1]; // tcp:PORT @@ -159,7 +159,7 @@ adb_exec_forward(const char *serial, uint16_t local_port, return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd)); } -sc_pid +static sc_pid adb_exec_forward_remove(const char *serial, uint16_t local_port) { char local[4 + 5 + 1]; // tcp:PORT sprintf(local, "tcp:%" PRIu16, local_port); @@ -167,7 +167,7 @@ adb_exec_forward_remove(const char *serial, uint16_t local_port) { return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd)); } -sc_pid +static sc_pid adb_exec_reverse(const char *serial, const char *device_socket_name, uint16_t local_port) { char local[4 + 5 + 1]; // tcp:PORT @@ -178,7 +178,7 @@ adb_exec_reverse(const char *serial, const char *device_socket_name, return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd)); } -sc_pid +static sc_pid adb_exec_reverse_remove(const char *serial, const char *device_socket_name) { char remote[108 + 14 + 1]; // localabstract:NAME snprintf(remote, sizeof(remote), "localabstract:%s", device_socket_name); @@ -186,7 +186,7 @@ adb_exec_reverse_remove(const char *serial, const char *device_socket_name) { return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd)); } -sc_pid +static sc_pid adb_exec_push(const char *serial, const char *local, const char *remote) { #ifdef __WINDOWS__ // Windows will parse the string, so the paths must be quoted @@ -213,7 +213,7 @@ adb_exec_push(const char *serial, const char *local, const char *remote) { return pid; } -sc_pid +static sc_pid adb_exec_install(const char *serial, const char *local) { #ifdef __WINDOWS__ // Windows will parse the string, so the local name must be quoted @@ -234,7 +234,7 @@ adb_exec_install(const char *serial, const char *local) { return pid; } -sc_pid +static sc_pid adb_exec_get_serialno(sc_pipe *pout) { const char *const adb_cmd[] = {"get-serialno"}; return adb_execute_p(NULL, adb_cmd, ARRAY_LEN(adb_cmd), pout); diff --git a/app/src/adb.h b/app/src/adb.h index 3714c17f..f58bc165 100644 --- a/app/src/adb.h +++ b/app/src/adb.h @@ -6,44 +6,11 @@ #include #include -#include "util/process.h" #include "util/intr.h" sc_pid adb_execute(const char *serial, const char *const adb_cmd[], size_t len); -sc_pid -adb_execute_p(const char *serial, const char *const adb_cmd[], size_t len, - sc_pipe *pout); - -sc_pid -adb_exec_forward(const char *serial, uint16_t local_port, - const char *device_socket_name); - -sc_pid -adb_exec_forward_remove(const char *serial, uint16_t local_port); - -sc_pid -adb_exec_reverse(const char *serial, const char *device_socket_name, - uint16_t local_port); - -sc_pid -adb_exec_reverse_remove(const char *serial, const char *device_socket_name); - -sc_pid -adb_exec_push(const char *serial, const char *local, const char *remote); - -sc_pid -adb_exec_install(const char *serial, const char *local); - -/** - * Execute `adb get-serialno` - * - * The result can be read from the output parameter `pout`. - */ -sc_pid -adb_exec_get_serialno(sc_pipe *pout); - bool adb_forward(struct sc_intr *intr, const char *serial, uint16_t local_port, const char *device_socket_name); From 2fc80eae2db098f613b45b99b6d49ea9a455724d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 18 Nov 2021 22:25:15 +0100 Subject: [PATCH 0813/2244] Simplify adb_tunnel With the new adb functions, the static adb_tunnel functions become unnecessary. --- app/src/adb_tunnel.c | 33 +++++---------------------------- 1 file changed, 5 insertions(+), 28 deletions(-) diff --git a/app/src/adb_tunnel.c b/app/src/adb_tunnel.c index b94609e2..fa86a8a5 100644 --- a/app/src/adb_tunnel.c +++ b/app/src/adb_tunnel.c @@ -9,29 +9,6 @@ #define SC_SOCKET_NAME "scrcpy" -static bool -enable_tunnel_reverse(struct sc_intr *intr, const char *serial, - uint16_t local_port) { - return adb_reverse(intr, serial, SC_SOCKET_NAME, local_port); -} - -static bool -disable_tunnel_reverse(struct sc_intr *intr, const char *serial) { - return adb_reverse_remove(intr, serial, SC_SOCKET_NAME); -} - -static bool -enable_tunnel_forward(struct sc_intr *intr, const char *serial, - uint16_t local_port) { - return adb_forward(intr, serial, local_port, SC_SOCKET_NAME); -} - -static bool -disable_tunnel_forward(struct sc_intr *intr, const char *serial, - uint16_t local_port) { - return adb_forward_remove(intr, serial, local_port); -} - static bool listen_on_port(struct sc_intr *intr, sc_socket socket, uint16_t port) { return net_listen_intr(intr, socket, IPV4_LOCALHOST, port, 1); @@ -43,7 +20,7 @@ enable_tunnel_reverse_any_port(struct sc_adb_tunnel *tunnel, struct sc_port_range port_range) { uint16_t port = port_range.first; for (;;) { - if (!enable_tunnel_reverse(intr, serial, port)) { + if (!adb_reverse(intr, serial, SC_SOCKET_NAME, port)) { // the command itself failed, it will fail on any port return false; } @@ -74,7 +51,7 @@ enable_tunnel_reverse_any_port(struct sc_adb_tunnel *tunnel, } // failure, disable tunnel and try another port - if (!disable_tunnel_reverse(intr, serial)) { + if (!adb_reverse_remove(intr, serial, SC_SOCKET_NAME)) { LOGW("Could not remove reverse tunnel on port %" PRIu16, port); } @@ -104,7 +81,7 @@ enable_tunnel_forward_any_port(struct sc_adb_tunnel *tunnel, uint16_t port = port_range.first; for (;;) { - if (enable_tunnel_forward(intr, serial, port)) { + if (adb_forward(intr, serial, port, SC_SOCKET_NAME)) { // success tunnel->local_port = port; tunnel->enabled = true; @@ -169,9 +146,9 @@ sc_adb_tunnel_close(struct sc_adb_tunnel *tunnel, struct sc_intr *intr, bool ret; if (tunnel->forward) { - ret = disable_tunnel_forward(intr, serial, tunnel->local_port); + ret = adb_forward_remove(intr, serial, tunnel->local_port); } else { - ret = disable_tunnel_reverse(intr, serial); + ret = adb_reverse_remove(intr, serial, SC_SOCKET_NAME); assert(tunnel->server_socket != SC_SOCKET_NONE); if (!net_close(tunnel->server_socket)) { From 4cfc1cd70a269c8a27f8d2ef0ac5e7eaebaae8b5 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 19 Nov 2021 08:06:23 +0100 Subject: [PATCH 0814/2244] Assert that long options are correctly set --- app/src/cli.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/cli.c b/app/src/cli.c index 1550c706..dabcaaf5 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -569,6 +569,10 @@ sc_getopt_adapter_create_longopts(void) { size_t out_idx = 0; for (size_t i = 0; i < ARRAY_LEN(options); ++i) { const struct sc_option *in = &options[i]; + + // If longopt_id is set, then longopt must be set + assert(!in->longopt_id || in->longopt); + if (!in->longopt) { // The longopts array must only contain long options continue; From b25404ee4b6f2cbdd41992fa3e087dd8a73412c9 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 19 Nov 2021 08:15:20 +0100 Subject: [PATCH 0815/2244] Print help to stdout The output of -h/--help was printed on stderr, although it is not an error. Printing on stdout allows to pipe the result directly: scrcpy --help | less Instead of (in bash): scrcpy --help |& less --- app/src/cli.c | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index dabcaaf5..aa71c659 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -675,12 +675,12 @@ print_option_usage_header(const struct sc_option *opt) { } } - fprintf(stderr, "\n %s\n", buf.s); + printf("\n %s\n", buf.s); free(buf.s); return; error: - fprintf(stderr, "\n"); + printf("\n"); } static void @@ -696,11 +696,11 @@ print_option_usage(const struct sc_option *opt, unsigned cols) { char *text = sc_str_wrap_lines(opt->text, cols, 8); if (!text) { - fprintf(stderr, "\n"); + printf("\n"); return; } - fprintf(stderr, "%s\n", text); + printf("%s\n", text); free(text); } @@ -711,11 +711,11 @@ print_shortcuts_intro(unsigned cols) { "(left) Alt or (left) Super, but it can be configured by " "--shortcut-mod (see above).", cols, 4); if (!intro) { - fprintf(stderr, "\n"); + printf("\n"); return; } - fprintf(stderr, "%s\n", intro); + printf("%s\n", intro); free(intro); } @@ -725,21 +725,21 @@ print_shortcut(const struct sc_shortcut *shortcut, unsigned cols) { assert(shortcut->shortcuts[0]); // At least one shortcut assert(shortcut->text); - fprintf(stderr, "\n"); + printf("\n"); unsigned i = 0; while (shortcut->shortcuts[i]) { - fprintf(stderr, " %s\n", shortcut->shortcuts[i]); + printf(" %s\n", shortcut->shortcuts[i]); ++i; }; char *text = sc_str_wrap_lines(shortcut->text, cols, 8); if (!text) { - fprintf(stderr, "\n"); + printf("\n"); return; } - fprintf(stderr, "%s\n", text); + printf("%s\n", text); free(text); } @@ -763,14 +763,14 @@ scrcpy_print_usage(const char *arg0) { } } - fprintf(stderr, "Usage: %s [options]\n\n" - "Options:\n", arg0); + printf("Usage: %s [options]\n\n" + "Options:\n", arg0); for (size_t i = 0; i < ARRAY_LEN(options); ++i) { print_option_usage(&options[i], cols); } // Print shortcuts section - fprintf(stderr, "\nShortcuts:\n\n"); + printf("\nShortcuts:\n\n"); print_shortcuts_intro(cols); for (size_t i = 0; i < ARRAY_LEN(shortcuts); ++i) { print_shortcut(&shortcuts[i], cols); From 6da6d905c2aeac17255347037d44c32a62c4c504 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 19 Nov 2021 09:14:38 +0100 Subject: [PATCH 0816/2244] Print scrcpy header first Inconditionnally print the scrcpy version first, without using LOGI(). The log level is configured only after parsing the command line parameters (it may be changed via -V), and command line parsing logs should not appear before the scrcpy version. --- app/src/main.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main.c b/app/src/main.c index 831b98fa..690e4070 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -47,6 +47,9 @@ main(int argc, char *argv[]) { setbuf(stderr, NULL); #endif + printf("scrcpy " SCRCPY_VERSION + " \n"); + struct scrcpy_cli_args args = { .opts = scrcpy_options_default, .help = false, @@ -73,8 +76,6 @@ main(int argc, char *argv[]) { return 0; } - LOGI("scrcpy " SCRCPY_VERSION " "); - #ifdef SCRCPY_LAVF_REQUIRES_REGISTER_ALL av_register_all(); #endif From 2d6a96776c9e4afeba74e98f70dd98d29e226b0d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 19 Nov 2021 19:02:43 +0100 Subject: [PATCH 0817/2244] Improve SSH tunnel documentation in README --- README.md | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index c9080861..e0dfa5b3 100644 --- a/README.md +++ b/README.md @@ -416,17 +416,27 @@ autoadb scrcpy -s '{}' To connect to a remote device, it is possible to connect a local `adb` client to a remote `adb` server (provided they use the same version of the _adb_ -protocol): +protocol). + +First, make sure the ADB server is running on the remote computer: ```bash -adb kill-server # kill the local adb server on 5037 -ssh -CN -L5037:localhost:5037 -R27183:localhost:27183 your_remote_computer +adb start-server +``` + +Then, establish a SSH tunnel: + +```bash +# local 5038 --> remote 5037 +# local 27183 <-- remote 27183 +ssh -CN -L5038:localhost:5037 -R27183:localhost:27183 your_remote_computer # keep this open ``` -From another terminal: +From another terminal, run scrcpy: ```bash +export ADB_SERVER_SOCKET=tcp:localhost:5038 scrcpy ``` @@ -434,14 +444,16 @@ 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 +# local 5038 --> remote 5037 +# local 27183 --> remote 27183 +ssh -CN -L5038:localhost:5037 -L27183:localhost:27183 your_remote_computer # keep this open ``` -From another terminal: +From another terminal, run scrcpy: ```bash +export ADB_SERVER_SOCKET=tcp:localhost:5038 scrcpy --force-adb-forward ``` From 226f3b2c915075266088d5483e385a7ce0d8b654 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 21 Nov 2021 00:12:10 +0100 Subject: [PATCH 0818/2244] Add missing include config.h --- app/src/compat.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/compat.h b/app/src/compat.h index 9f66ce95..92b0c43f 100644 --- a/app/src/compat.h +++ b/app/src/compat.h @@ -8,6 +8,8 @@ # define _DARWIN_C_SOURCE #endif +#include "config.h" + #include #include From ba547e3895397e3710e7eb14faafbabbd7e3a077 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 20 Nov 2021 21:24:56 +0100 Subject: [PATCH 0819/2244] Configure feature test macros in meson Refs #2807 Co-authored-by: RipleyTom --- app/meson.build | 12 ++++++++++-- app/src/compat.h | 7 ------- app/src/sys/win/process.c | 3 --- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/app/meson.build b/app/meson.build index 4894babc..befe1658 100644 --- a/app/meson.build +++ b/app/meson.build @@ -39,16 +39,26 @@ src = [ 'src/util/tick.c', ] +conf = configuration_data() + if host_machine.system() == 'windows' src += [ 'src/sys/win/file.c', 'src/sys/win/process.c', ] + conf.set('_WIN32_WINNT', '0x0600') + conf.set('WINVER', '0x0600') else src += [ 'src/sys/unix/file.c', 'src/sys/unix/process.c', ] + conf.set('_POSIX_C_SOURCE', '200809L') + conf.set('_XOPEN_SOURCE', '700') + conf.set('_GNU_SOURCE', true) + if host_machine.system() == 'darwin' + conf.set('_DARWIN_C_SOURCE', true) + endif endif v4l2_support = host_machine.system() == 'linux' @@ -128,8 +138,6 @@ if host_machine.system() == 'windows' dependencies += cc.find_library('ws2_32') endif -conf = configuration_data() - foreach f : check_functions if cc.has_function(f) define = 'HAVE_' + f.underscorify().to_upper() diff --git a/app/src/compat.h b/app/src/compat.h index 92b0c43f..32759c01 100644 --- a/app/src/compat.h +++ b/app/src/compat.h @@ -1,13 +1,6 @@ #ifndef COMPAT_H #define COMPAT_H -#define _POSIX_C_SOURCE 200809L -#define _XOPEN_SOURCE 700 -#define _GNU_SOURCE -#ifdef __APPLE__ -# define _DARWIN_C_SOURCE -#endif - #include "config.h" #include diff --git a/app/src/sys/win/process.c b/app/src/sys/win/process.c index 6566b80e..326a3d99 100644 --- a/app/src/sys/win/process.c +++ b/app/src/sys/win/process.c @@ -1,6 +1,3 @@ -// -#define _WIN32_WINNT 0x0600 // For extended process API - #include "util/process.h" #include From 52e5181c841580b582e44dbdbdf08df533637d07 Mon Sep 17 00:00:00 2001 From: RipleyTom Date: Thu, 18 Nov 2021 01:02:53 +0100 Subject: [PATCH 0820/2244] Add function to parse IPv4 addresses PR #2807 Signed-off-by: Romain Vimont --- app/src/util/net.c | 13 +++++++++++++ app/src/util/net.h | 6 ++++++ 2 files changed, 19 insertions(+) diff --git a/app/src/util/net.c b/app/src/util/net.c index 8595bc79..824d4886 100644 --- a/app/src/util/net.c +++ b/app/src/util/net.c @@ -7,6 +7,7 @@ #include "log.h" #ifdef __WINDOWS__ +# include typedef int socklen_t; typedef SOCKET sc_raw_socket; #else @@ -225,3 +226,15 @@ net_close(sc_socket socket) { return !close(raw_sock); #endif } + +bool +net_parse_ipv4(const char *s, uint32_t *ipv4) { + struct in_addr addr; + if (!inet_pton(AF_INET, s, &addr)) { + LOGE("Invalid IPv4 address: %s", s); + return false; + } + + *ipv4 = ntohl(addr.s_addr); + return true; +} diff --git a/app/src/util/net.h b/app/src/util/net.h index 57fd6c5e..15979cf9 100644 --- a/app/src/util/net.h +++ b/app/src/util/net.h @@ -68,4 +68,10 @@ net_interrupt(sc_socket socket); bool net_close(sc_socket socket); +/** + * Parse `ip` "xxx.xxx.xxx.xxx" to an IPv4 host representation + */ +bool +net_parse_ipv4(const char *ip, uint32_t *ipv4); + #endif From 7bdbde7363836b79864f876f6a4cc107785a7147 Mon Sep 17 00:00:00 2001 From: RipleyTom Date: Thu, 18 Nov 2021 01:02:53 +0100 Subject: [PATCH 0821/2244] Add options to configure tunnel host and port In "adb forward" mode, by default, scrcpy connects to localhost:PORT, where PORT is the local port passed to "adb forward". This assumes that the tunnel is established on the local host with a local adb server (which is the common case). For advanced usage, add --tunnel-host and --tunnel-port to force the connection to a different destination. Fixes #2801 PR #2807 Signed-off-by: Romain Vimont --- app/meson.build | 1 + app/scrcpy.1 | 12 ++++++++++++ app/src/cli.c | 47 +++++++++++++++++++++++++++++++++++++++++++++++ app/src/options.c | 2 ++ app/src/options.h | 2 ++ app/src/scrcpy.c | 2 ++ app/src/server.c | 28 ++++++++++++++++++++-------- app/src/server.h | 2 ++ 8 files changed, 88 insertions(+), 8 deletions(-) diff --git a/app/meson.build b/app/meson.build index befe1658..3a5cb12a 100644 --- a/app/meson.build +++ b/app/meson.build @@ -207,6 +207,7 @@ if get_option('buildtype') == 'debug' 'tests/test_cli.c', 'src/cli.c', 'src/options.c', + 'src/util/net.c', 'src/util/str.c', 'src/util/strbuf.c', 'src/util/term.c', diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 399fd172..7438118b 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -203,6 +203,18 @@ Enable "show touches" on start, restore the initial value on exit. It only shows physical touches (not clicks from scrcpy). +.TP +.BI "\-\-tunnel\-host " ip +Set the IP address of the adb tunnel to reach the scrcpy server. This option automatically enables --force-adb-forward. + +Default is localhost. + +.TP +.BI "\-\-tunnel\-port " port +Set the TCP port of the adb tunnel to reach the scrcpy server. This option automatically enables --force-adb-forward. + +Default is 0 (not forced): the local port used for establishing the tunnel will be used. + .TP .BI "\-\-v4l2-sink " /dev/videoN Output to v4l2loopback device. diff --git a/app/src/cli.c b/app/src/cli.c index aa71c659..f00611a7 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -9,6 +9,7 @@ #include "options.h" #include "util/log.h" +#include "util/net.h" #include "util/str.h" #include "util/strbuf.h" #include "util/term.h" @@ -46,6 +47,8 @@ #define OPT_V4L2_SINK 1027 #define OPT_DISPLAY_BUFFER 1028 #define OPT_V4L2_BUFFER 1029 +#define OPT_TUNNEL_HOST 1030 +#define OPT_TUNNEL_PORT 1031 struct sc_option { char shortopt; @@ -330,6 +333,25 @@ static const struct sc_option options[] = { "on exit.\n" "It only shows physical touches (not clicks from scrcpy).", }, + { + .longopt_id = OPT_TUNNEL_HOST, + .longopt = "tunnel-host", + .argdesc = "ip", + .text = "Set the IP address of the adb tunnel to reach the scrcpy " + "server. This option automatically enables " + "--force-adb-forward.\n" + "Default is localhost.", + }, + { + .longopt_id = OPT_TUNNEL_PORT, + .longopt = "tunnel-port", + .argdesc = "port", + .text = "Set the TCP port of the adb tunnel to reach the scrcpy " + "server. This option automatically enables " + "--force-adb-forward.\n" + "Default is 0 (not forced): the local port used for " + "establishing the tunnel will be used.", + }, #ifdef HAVE_V4L2 { .longopt_id = OPT_V4L2_SINK, @@ -1127,6 +1149,21 @@ parse_record_format(const char *optarg, enum sc_record_format *format) { return false; } +static bool +parse_ip(const char *optarg, uint32_t *ipv4) { + return net_parse_ipv4(optarg, ipv4); +} + +static bool +parse_port(const char *optarg, uint16_t *port) { + long value; + if (!parse_integer_arg(optarg, &value, false, 0, 0xFFFF, "port")) { + return false; + } + *port = (uint16_t) value; + return true; +} + static enum sc_record_format guess_record_format(const char *filename) { size_t len = strlen(filename); @@ -1199,6 +1236,16 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; } break; + case OPT_TUNNEL_HOST: + if (!parse_ip(optarg, &opts->tunnel_host)) { + return false; + } + break; + case OPT_TUNNEL_PORT: + if (!parse_port(optarg, &opts->tunnel_port)) { + return false; + } + break; case 'n': opts->control = false; break; diff --git a/app/src/options.c b/app/src/options.c index 82f25342..074bdf08 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -19,6 +19,8 @@ const struct scrcpy_options scrcpy_options_default = { .first = DEFAULT_LOCAL_PORT_RANGE_FIRST, .last = DEFAULT_LOCAL_PORT_RANGE_LAST, }, + .tunnel_host = 0, + .tunnel_port = 0, .shortcut_mods = { .data = {SC_MOD_LALT, SC_MOD_LSUPER}, .count = 2, diff --git a/app/src/options.h b/app/src/options.h index 434225b9..82d094cd 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -77,6 +77,8 @@ struct scrcpy_options { enum sc_record_format record_format; enum sc_keyboard_input_mode keyboard_input_mode; struct sc_port_range port_range; + uint32_t tunnel_host; + uint16_t tunnel_port; struct sc_shortcut_mods shortcut_mods; uint16_t max_size; uint32_t bit_rate; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 40bf72a8..61087681 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -345,6 +345,8 @@ scrcpy(struct scrcpy_options *options) { .log_level = options->log_level, .crop = options->crop, .port_range = options->port_range, + .tunnel_host = options->tunnel_host, + .tunnel_port = options->tunnel_port, .max_size = options->max_size, .bit_rate = options->bit_rate, .max_fps = options->max_fps, diff --git a/app/src/server.c b/app/src/server.c index 3f12db62..78127d2d 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -202,8 +202,9 @@ execute_server(struct sc_server *server, } static bool -connect_and_read_byte(struct sc_intr *intr, sc_socket socket, uint16_t port) { - bool ok = net_connect_intr(intr, socket, IPV4_LOCALHOST, port); +connect_and_read_byte(struct sc_intr *intr, sc_socket socket, + uint32_t tunnel_host, uint16_t tunnel_port) { + bool ok = net_connect_intr(intr, socket, tunnel_host, tunnel_port); if (!ok) { return false; } @@ -220,13 +221,13 @@ connect_and_read_byte(struct sc_intr *intr, sc_socket socket, uint16_t port) { } static sc_socket -connect_to_server(struct sc_server *server, uint32_t attempts, sc_tick delay) { - uint16_t port = server->tunnel.local_port; +connect_to_server(struct sc_server *server, uint32_t attempts, sc_tick delay, + uint32_t host, uint16_t port) { do { LOGD("Remaining connection attempts: %d", (int) attempts); sc_socket socket = net_socket(); if (socket != SC_SOCKET_NONE) { - bool ok = connect_and_read_byte(&server->intr, socket, port); + bool ok = connect_and_read_byte(&server->intr, socket, host, port); if (ok) { // it worked! return socket; @@ -352,9 +353,20 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) { goto fail; } } else { + uint32_t tunnel_host = server->params.tunnel_host; + if (!tunnel_host) { + tunnel_host = IPV4_LOCALHOST; + } + + uint16_t tunnel_port = server->params.tunnel_port; + if (!tunnel_port) { + tunnel_port = tunnel->local_port; + } + uint32_t attempts = 100; sc_tick delay = SC_TICK_FROM_MS(100); - video_socket = connect_to_server(server, attempts, delay); + video_socket = connect_to_server(server, attempts, delay, tunnel_host, + tunnel_port); if (video_socket == SC_SOCKET_NONE) { goto fail; } @@ -364,8 +376,8 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) { if (control_socket == SC_SOCKET_NONE) { goto fail; } - bool ok = net_connect_intr(&server->intr, control_socket, - IPV4_LOCALHOST, tunnel->local_port); + bool ok = net_connect_intr(&server->intr, control_socket, tunnel_host, + tunnel_port); if (!ok) { goto fail; } diff --git a/app/src/server.h b/app/src/server.h index 3859c328..cb1fadbb 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -29,6 +29,8 @@ struct sc_server_params { const char *codec_options; const char *encoder_name; struct sc_port_range port_range; + uint32_t tunnel_host; + uint16_t tunnel_port; uint16_t max_size; uint32_t bit_rate; uint16_t max_fps; From 68eaee5bb086383e601ac64e9ff0aa65830599e3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 19 Nov 2021 21:07:22 +0100 Subject: [PATCH 0822/2244] Force adb forward if tunnel host/port is provided Tunnel host and port are only meaningful in "adb forward" mode. They indicate where the client must connect to communicate with the server. In "adb reverse" mode, the client _listens_, it does not _connect_. Refs #2807 --- app/src/cli.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/cli.c b/app/src/cli.c index f00611a7..5256d50e 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -1405,6 +1405,12 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } #endif + if ((opts->tunnel_host || opts->tunnel_port) && !opts->force_adb_forward) { + LOGI("Tunnel host/port is set, " + "--force-adb-forward automatically enabled."); + opts->force_adb_forward = true; + } + int index = optind; if (index < argc) { LOGE("Unexpected additional argument: %s", argv[index]); From 44721ed98235b262fc77d686ec5020c72a8dcb42 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 19 Nov 2021 21:11:56 +0100 Subject: [PATCH 0823/2244] Document remote ADB server in README Refs #2807 --- README.md | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e0dfa5b3..f355c7b6 100644 --- a/README.md +++ b/README.md @@ -412,12 +412,47 @@ autoadb scrcpy -s '{}' [AutoAdb]: https://github.com/rom1v/autoadb -#### SSH tunnel +#### Tunnels To connect to a remote device, it is possible to connect a local `adb` client to a remote `adb` server (provided they use the same version of the _adb_ protocol). +##### Remote ADB server + +To connect to a remote ADB server, make the server listen on all interfaces: + +```bash +adb kill-server +adb -a nodaemon server start +# keep this open +``` + +**Warning: all communications between clients and ADB server are unencrypted.** + +Suppose that this server is accessible at 192.168.1.2. Then, from another +terminal, run scrcpy: + +```bash +export ADB_SERVER_SOCKET=tcp:192.168.1.2:5037 +scrcpy --tunnel-host=192.168.1.2 +``` + +By default, scrcpy uses the local port used for `adb forward` tunnel +establishment (typically `27183`, see `--port`). It is also possible to force a +different tunnel port (it may be useful in more complex situations, when more +redirections are involved): + +``` +scrcpy --tunnel-port=1234 +``` + + +##### SSH tunnel + +To communicate with a remote ADB server securely, it is preferable to use a SSH +tunnel. + First, make sure the ADB server is running on the remote computer: ```bash From 0427a981e59ff5c5f188de5d34722c7f098ecf43 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 20 Nov 2021 12:15:15 +0100 Subject: [PATCH 0824/2244] Use UINT64_C macro for uint64_t constant in tests A long constant might not be sufficient. --- app/tests/test_control_msg_serialize.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c index ef9247ca..b237e28e 100644 --- a/app/tests/test_control_msg_serialize.c +++ b/app/tests/test_control_msg_serialize.c @@ -78,7 +78,7 @@ static void test_serialize_inject_touch_event(void) { .type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, .inject_touch_event = { .action = AMOTION_EVENT_ACTION_DOWN, - .pointer_id = 0x1234567887654321L, + .pointer_id = UINT64_C(0x1234567887654321), .position = { .point = { .x = 100, From ea8028332ced7ef61e755742ad9e91586e3193e3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 20 Nov 2021 17:39:19 +0100 Subject: [PATCH 0825/2244] Synchronize computer-to-device empty clipboard Set the device clipboard to empty string if necessary. Otherwise, the current device clipboard will be pasted on Ctrl+v. PR #2814 --- app/src/input_manager.c | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index b84f3bea..6158f6d2 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -215,11 +215,6 @@ set_device_clipboard(struct controller *controller, bool paste) { LOGW("Could not get clipboard text: %s", SDL_GetError()); return; } - if (!*text) { - // empty text - SDL_free(text); - return; - } char *text_dup = strdup(text); SDL_free(text); From 854de9659a503291fc310ed6702bd4278bf2a725 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 20 Nov 2021 17:44:00 +0100 Subject: [PATCH 0826/2244] Do not inject Ctrl+v if clipboard sync failed This prevents to paste the current Android clipboard, which would be unexpected. PR #2814 --- app/src/input_manager.c | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 6158f6d2..c15e0427 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -208,19 +208,19 @@ collapse_panels(struct controller *controller) { } } -static void +static bool set_device_clipboard(struct controller *controller, bool paste) { char *text = SDL_GetClipboardText(); if (!text) { LOGW("Could not get clipboard text: %s", SDL_GetError()); - return; + return false; } char *text_dup = strdup(text); SDL_free(text); if (!text_dup) { LOGW("Could not strdup input text"); - return; + return false; } struct control_msg msg; @@ -231,7 +231,10 @@ set_device_clipboard(struct controller *controller, bool paste) { if (!controller_push_msg(controller, &msg)) { free(text_dup); LOGW("Could not request 'set device clipboard'"); + return false; } + + return true; } static void @@ -515,7 +518,11 @@ input_manager_process_key(struct input_manager *im, } // Synchronize the computer clipboard to the device clipboard before // sending Ctrl+v, to allow seamless copy-paste. - set_device_clipboard(controller, false); + bool ok = set_device_clipboard(controller, false); + if (!ok) { + LOGW("Clipboard could not be synchronized, Ctrl+v not injected"); + return; + } } im->kp->ops->process_key(im->kp, event); From 5b3856c3b6bed67abfd20585daa8eb43fa863582 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 21 Nov 2021 17:24:34 +0100 Subject: [PATCH 0827/2244] Explicitly indicate when device clipboard is set Pass the information that device clipboard has been set to the key processor. This avoids the keyprocessor to "guess", and paves the way to implement a proper acknowledgement mechanism. PR #2814 --- app/src/hid_keyboard.c | 9 +++------ app/src/input_manager.c | 5 +++-- app/src/keyboard_inject.c | 8 +++++++- app/src/trait/key_processor.h | 11 ++++++++++- 4 files changed, 23 insertions(+), 10 deletions(-) diff --git a/app/src/hid_keyboard.c b/app/src/hid_keyboard.c index 3ac1a441..2809ccd6 100644 --- a/app/src/hid_keyboard.c +++ b/app/src/hid_keyboard.c @@ -278,7 +278,8 @@ push_mod_lock_state(struct sc_hid_keyboard *kb, uint16_t sdl_mod) { static void sc_key_processor_process_key(struct sc_key_processor *kp, - const SDL_KeyboardEvent *event) { + const SDL_KeyboardEvent *event, + bool device_clipboard_set) { if (event->repeat) { // In USB HID protocol, key repeat is handled by the host (Android), so // just ignore key repeat here. @@ -298,11 +299,7 @@ sc_key_processor_process_key(struct sc_key_processor *kp, } } - 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; - if (ctrl && !shift && keycode == SDLK_v && down) { + if (device_clipboard_set) { // Ctrl+v is pressed, so clipboard synchronization has been // requested. Wait a bit so that the clipboard is set before // injecting Ctrl+v via HID, otherwise it would paste the old diff --git a/app/src/input_manager.c b/app/src/input_manager.c index c15e0427..16c82234 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -510,7 +510,8 @@ input_manager_process_key(struct input_manager *im, return; } - if (ctrl && !shift && keycode == SDLK_v && down && !repeat) { + bool is_ctrl_v = ctrl && !shift && keycode == SDLK_v && down && !repeat; + if (is_ctrl_v) { if (im->legacy_paste) { // inject the text as input events clipboard_paste(controller); @@ -525,7 +526,7 @@ input_manager_process_key(struct input_manager *im, } } - im->kp->ops->process_key(im->kp, event); + im->kp->ops->process_key(im->kp, event, is_ctrl_v); } static void diff --git a/app/src/keyboard_inject.c b/app/src/keyboard_inject.c index bcc85da8..e59b5520 100644 --- a/app/src/keyboard_inject.c +++ b/app/src/keyboard_inject.c @@ -188,7 +188,13 @@ convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to, static void sc_key_processor_process_key(struct sc_key_processor *kp, - const SDL_KeyboardEvent *event) { + const SDL_KeyboardEvent *event, + bool device_clipboard_set) { + // The device clipboard synchronization and the key event messages are + // serialized, there is nothing special to do to ensure that the clipboard + // is set before injecting Ctrl+v. + (void) device_clipboard_set; + struct sc_keyboard_inject *ki = DOWNCAST(kp); if (event->repeat) { diff --git a/app/src/trait/key_processor.h b/app/src/trait/key_processor.h index 5790310b..1f8132f2 100644 --- a/app/src/trait/key_processor.h +++ b/app/src/trait/key_processor.h @@ -18,8 +18,17 @@ struct sc_key_processor { }; struct sc_key_processor_ops { + + /** + * Process the keyboard event + * + * The flag `device_clipboard_set` indicates that the input manager sent a + * control message to synchronize the device clipboard as a result of this + * key event. + */ void - (*process_key)(struct sc_key_processor *kp, const SDL_KeyboardEvent *event); + (*process_key)(struct sc_key_processor *kp, const SDL_KeyboardEvent *event, + bool device_clipboard_set); void (*process_text)(struct sc_key_processor *kp, From aba1fc03c3702404b30b7a8aebd2489f8b7fed1c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 20 Nov 2021 14:33:13 +0100 Subject: [PATCH 0828/2244] Add acksync helper to wait for acks This will allow to send requests with sequence numbers to the server and wait for acknowledgements. PR #2814 --- app/meson.build | 1 + app/src/util/acksync.c | 76 ++++++++++++++++++++++++++++++++++++++++++ app/src/util/acksync.h | 61 +++++++++++++++++++++++++++++++++ 3 files changed, 138 insertions(+) create mode 100644 app/src/util/acksync.c create mode 100644 app/src/util/acksync.h diff --git a/app/meson.build b/app/meson.build index 3a5cb12a..d6fac580 100644 --- a/app/meson.build +++ b/app/meson.build @@ -25,6 +25,7 @@ src = [ 'src/server.c', 'src/stream.c', 'src/video_buffer.c', + 'src/util/acksync.c', 'src/util/file.c', 'src/util/intr.c', 'src/util/log.c', diff --git a/app/src/util/acksync.c b/app/src/util/acksync.c new file mode 100644 index 00000000..2899cdcb --- /dev/null +++ b/app/src/util/acksync.c @@ -0,0 +1,76 @@ +#include "acksync.h" + +#include +#include "util/log.h" + +bool +sc_acksync_init(struct sc_acksync *as) { + bool ok = sc_mutex_init(&as->mutex); + if (!ok) { + return false; + } + + ok = sc_cond_init(&as->cond); + if (!ok) { + sc_mutex_destroy(&as->mutex); + return false; + } + + as->stopped = false; + as->ack = SC_SEQUENCE_INVALID; + + return true; +} + +void +sc_acksync_destroy(struct sc_acksync *as) { + sc_cond_destroy(&as->cond); + sc_mutex_destroy(&as->mutex); +} + +void +sc_acksync_ack(struct sc_acksync *as, uint64_t sequence) { + sc_mutex_lock(&as->mutex); + + // Acknowledgements must be monotonic + assert(sequence >= as->ack); + + as->ack = sequence; + sc_cond_signal(&as->cond); + + sc_mutex_unlock(&as->mutex); +} + +enum sc_acksync_wait_result +sc_acksync_wait(struct sc_acksync *as, uint64_t ack, sc_tick deadline) { + sc_mutex_lock(&as->mutex); + + bool timed_out = false; + while (!as->stopped && as->ack < ack && !timed_out) { + timed_out = !sc_cond_timedwait(&as->cond, &as->mutex, deadline); + } + + enum sc_acksync_wait_result ret; + if (as->stopped) { + ret = SC_ACKSYNC_WAIT_INTR; + } else if (as->ack >= ack) { + ret = SC_ACKSYNC_WAIT_OK; + } else { + assert(timed_out); + ret = SC_ACKSYNC_WAIT_TIMEOUT; + } + sc_mutex_unlock(&as->mutex); + + return ret; +} + +/** + * Interrupt any `sc_acksync_wait()` + */ +void +sc_acksync_interrupt(struct sc_acksync *as) { + sc_mutex_lock(&as->mutex); + as->stopped = true; + sc_cond_signal(&as->cond); + sc_mutex_unlock(&as->mutex); +} diff --git a/app/src/util/acksync.h b/app/src/util/acksync.h new file mode 100644 index 00000000..1fd34444 --- /dev/null +++ b/app/src/util/acksync.h @@ -0,0 +1,61 @@ +#ifndef SC_ACK_SYNC_H +#define SC_ACK_SYNC_H + +#include "common.h" + +#include "thread.h" + +#define SC_SEQUENCE_INVALID 0 + +/** + * Helper to wait for acknowledgments + */ +struct sc_acksync { + sc_mutex mutex; + sc_cond cond; + + bool stopped; + + // Last acked value, initially SC_SEQUENCE_INVALID + uint64_t ack; +}; + +enum sc_acksync_wait_result { + // Acknowledgment received + SC_ACKSYNC_WAIT_OK, + + // Timeout expired + SC_ACKSYNC_WAIT_TIMEOUT, + + // Interrupted from another thread by sc_acksync_interrupt() + SC_ACKSYNC_WAIT_INTR, +}; + +bool +sc_acksync_init(struct sc_acksync *as); + +void +sc_acksync_destroy(struct sc_acksync *as); + +/** + * Acknowledge `sequence` + * + * The `sequence` must be greater than (or equal to) any previous acknowledged + * sequence. + */ +void +sc_acksync_ack(struct sc_acksync *as, uint64_t sequence); + +/** + * Wait for acknowledgment of sequence `ack` (or higher) + */ +enum sc_acksync_wait_result +sc_acksync_wait(struct sc_acksync *as, uint64_t ack, sc_tick deadline); + +/** + * Interrupt any `sc_acksync_wait()` + */ +void +sc_acksync_interrupt(struct sc_acksync *as); + +#endif From 901d8371655582b432d5d92430177a59df8058b9 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 20 Nov 2021 11:50:33 +0100 Subject: [PATCH 0829/2244] Add sequence number to set_clipboard request This will allow the client to request an acknowledgement. PR #2814 --- app/src/control_msg.c | 10 ++++++---- app/src/control_msg.h | 1 + app/src/input_manager.c | 1 + app/tests/test_control_msg_serialize.c | 4 +++- .../java/com/genymobile/scrcpy/ControlMessage.java | 8 +++++++- .../com/genymobile/scrcpy/ControlMessageReader.java | 7 ++++--- .../genymobile/scrcpy/ControlMessageReaderTest.java | 4 ++++ 7 files changed, 26 insertions(+), 9 deletions(-) diff --git a/app/src/control_msg.c b/app/src/control_msg.c index 74e3315c..83ab0b7b 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -118,11 +118,12 @@ control_msg_serialize(const struct control_msg *msg, unsigned char *buf) { buf[1] = msg->inject_keycode.action; return 2; case CONTROL_MSG_TYPE_SET_CLIPBOARD: { - buf[1] = !!msg->set_clipboard.paste; + buffer_write64be(&buf[1], msg->set_clipboard.sequence); + buf[9] = !!msg->set_clipboard.paste; size_t len = write_string(msg->set_clipboard.text, CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH, - &buf[2]); - return 2 + len; + &buf[10]); + return 10 + len; } case CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE: buf[1] = msg->set_screen_power_mode.mode; @@ -199,7 +200,8 @@ control_msg_log(const struct control_msg *msg) { KEYEVENT_ACTION_LABEL(msg->inject_keycode.action)); break; case CONTROL_MSG_TYPE_SET_CLIPBOARD: - LOG_CMSG("clipboard %s \"%s\"", + LOG_CMSG("clipboard %" PRIu64_ " %s \"%s\"", + msg->set_clipboard.sequence, msg->set_clipboard.paste ? "paste" : "copy", msg->set_clipboard.text); break; diff --git a/app/src/control_msg.h b/app/src/control_msg.h index 16492849..7352defe 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -70,6 +70,7 @@ struct control_msg { // screen may only be turned on on ACTION_DOWN } back_or_screen_on; struct { + uint64_t sequence; char *text; // owned, to be freed by free() bool paste; } set_clipboard; diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 16c82234..e7afee77 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -225,6 +225,7 @@ set_device_clipboard(struct controller *controller, bool paste) { struct control_msg msg; msg.type = CONTROL_MSG_TYPE_SET_CLIPBOARD; + msg.set_clipboard.sequence = 0; // unused for now msg.set_clipboard.text = text_dup; msg.set_clipboard.paste = paste; diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c index b237e28e..5cd7056b 100644 --- a/app/tests/test_control_msg_serialize.c +++ b/app/tests/test_control_msg_serialize.c @@ -226,6 +226,7 @@ static void test_serialize_set_clipboard(void) { struct control_msg msg = { .type = CONTROL_MSG_TYPE_SET_CLIPBOARD, .set_clipboard = { + .sequence = UINT64_C(0x0102030405060708), .paste = true, .text = "hello, world!", }, @@ -233,10 +234,11 @@ static void test_serialize_set_clipboard(void) { unsigned char buf[CONTROL_MSG_MAX_SIZE]; size_t size = control_msg_serialize(&msg, buf); - assert(size == 19); + assert(size == 27); const unsigned char expected[] = { CONTROL_MSG_TYPE_SET_CLIPBOARD, + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, // sequence 1, // paste 0x00, 0x00, 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 f8edd53c..a536ed91 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java @@ -31,6 +31,7 @@ public final class ControlMessage { private int vScroll; private boolean paste; private int repeat; + private long sequence; private ControlMessage() { } @@ -79,9 +80,10 @@ public final class ControlMessage { return msg; } - public static ControlMessage createSetClipboard(String text, boolean paste) { + public static ControlMessage createSetClipboard(long sequence, String text, boolean paste) { ControlMessage msg = new ControlMessage(); msg.type = TYPE_SET_CLIPBOARD; + msg.sequence = sequence; msg.text = text; msg.paste = paste; return msg; @@ -154,4 +156,8 @@ public final class ControlMessage { public int getRepeat() { return repeat; } + + public long getSequence() { + return sequence; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java index e4ab8402..80931e94 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java @@ -13,11 +13,11 @@ public class ControlMessageReader { static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20; static final int BACK_OR_SCREEN_ON_LENGTH = 1; static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1; - static final int SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH = 1; + static final int SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH = 9; 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 CLIPBOARD_TEXT_MAX_LENGTH = MESSAGE_MAX_SIZE - 14; // type: 1 byte; sequence: 8 bytes; paste flag: 1 byte; length: 4 bytes public static final int INJECT_TEXT_MAX_LENGTH = 300; private final byte[] rawBuffer = new byte[MESSAGE_MAX_SIZE]; @@ -166,12 +166,13 @@ public class ControlMessageReader { if (buffer.remaining() < SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH) { return null; } + long sequence = buffer.getLong(); boolean paste = buffer.get() != 0; String text = parseString(); if (text == null) { return null; } - return ControlMessage.createSetClipboard(text, paste); + return ControlMessage.createSetClipboard(sequence, text, paste); } private ControlMessage parseSetScreenPowerMode() { diff --git a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java index 7f3d3f61..3b0b6c0d 100644 --- a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java @@ -235,6 +235,7 @@ public class ControlMessageReaderTest { ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_SET_CLIPBOARD); + dos.writeLong(0x0102030405060708L); // sequence dos.writeByte(1); // paste byte[] text = "testé".getBytes(StandardCharsets.UTF_8); dos.writeInt(text.length); @@ -246,6 +247,7 @@ public class ControlMessageReaderTest { ControlMessage event = reader.next(); Assert.assertEquals(ControlMessage.TYPE_SET_CLIPBOARD, event.getType()); + Assert.assertEquals(0x0102030405060708L, event.getSequence()); Assert.assertEquals("testé", event.getText()); Assert.assertTrue(event.getPaste()); } @@ -259,6 +261,7 @@ public class ControlMessageReaderTest { dos.writeByte(ControlMessage.TYPE_SET_CLIPBOARD); byte[] rawText = new byte[ControlMessageReader.CLIPBOARD_TEXT_MAX_LENGTH]; + dos.writeLong(0x0807060504030201L); // sequence dos.writeByte(1); // paste Arrays.fill(rawText, (byte) 'a'); String text = new String(rawText, 0, rawText.length); @@ -272,6 +275,7 @@ public class ControlMessageReaderTest { ControlMessage event = reader.next(); Assert.assertEquals(ControlMessage.TYPE_SET_CLIPBOARD, event.getType()); + Assert.assertEquals(0x0807060504030201L, event.getSequence()); Assert.assertEquals(text, event.getText()); Assert.assertTrue(event.getPaste()); } From 2a0730ee9bacc8df92c7c38ce83a735182d685e0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 20 Nov 2021 12:10:09 +0100 Subject: [PATCH 0830/2244] Add device clipboard set acknowledgement Add a device message type so that the device could send acknowledgements for SET_CLIPBOARD requests. PR #2814 --- app/src/device_msg.c | 6 ++++++ app/src/device_msg.h | 4 ++++ app/src/receiver.c | 3 +++ app/tests/test_device_msg_deserialize.c | 15 ++++++++++++++ .../com/genymobile/scrcpy/DeviceMessage.java | 13 ++++++++++++ .../scrcpy/DeviceMessageWriter.java | 6 +++++- .../scrcpy/DeviceMessageWriterTest.java | 20 +++++++++++++++++++ 7 files changed, 66 insertions(+), 1 deletion(-) diff --git a/app/src/device_msg.c b/app/src/device_msg.c index 827f4213..4163b9fc 100644 --- a/app/src/device_msg.c +++ b/app/src/device_msg.c @@ -1,5 +1,6 @@ #include "device_msg.h" +#include #include #include @@ -34,6 +35,11 @@ device_msg_deserialize(const unsigned char *buf, size_t len, msg->clipboard.text = text; return 5 + clipboard_len; } + case DEVICE_MSG_TYPE_ACK_CLIPBOARD: { + uint64_t sequence = buffer_read64be(&buf[1]); + msg->ack_clipboard.sequence = sequence; + return 9; + } default: LOGW("Unknown device message type: %d", (int) msg->type); return -1; // error, we cannot recover diff --git a/app/src/device_msg.h b/app/src/device_msg.h index 888d9216..a0c989e3 100644 --- a/app/src/device_msg.h +++ b/app/src/device_msg.h @@ -13,6 +13,7 @@ enum device_msg_type { DEVICE_MSG_TYPE_CLIPBOARD, + DEVICE_MSG_TYPE_ACK_CLIPBOARD, }; struct device_msg { @@ -21,6 +22,9 @@ struct device_msg { struct { char *text; // owned, to be freed by free() } clipboard; + struct { + uint64_t sequence; + } ack_clipboard; }; }; diff --git a/app/src/receiver.c b/app/src/receiver.c index b5cf9b39..52e6034d 100644 --- a/app/src/receiver.c +++ b/app/src/receiver.c @@ -37,6 +37,9 @@ process_msg(struct device_msg *msg) { SDL_SetClipboardText(msg->clipboard.text); break; } + case DEVICE_MSG_TYPE_ACK_CLIPBOARD: + // TODO + break; } } diff --git a/app/tests/test_device_msg_deserialize.c b/app/tests/test_device_msg_deserialize.c index 3427d640..835096c0 100644 --- a/app/tests/test_device_msg_deserialize.c +++ b/app/tests/test_device_msg_deserialize.c @@ -47,11 +47,26 @@ static void test_deserialize_clipboard_big(void) { device_msg_destroy(&msg); } +static void test_deserialize_ack_set_clipboard(void) { + const unsigned char input[] = { + DEVICE_MSG_TYPE_ACK_CLIPBOARD, + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, // sequence + }; + + struct device_msg msg; + ssize_t r = device_msg_deserialize(input, sizeof(input), &msg); + assert(r == 9); + + assert(msg.type == DEVICE_MSG_TYPE_ACK_CLIPBOARD); + assert(msg.ack_clipboard.sequence == UINT64_C(0x0102030405060708)); +} + int main(int argc, char *argv[]) { (void) argc; (void) argv; test_deserialize_clipboard(); test_deserialize_clipboard_big(); + test_deserialize_ack_set_clipboard(); return 0; } diff --git a/server/src/main/java/com/genymobile/scrcpy/DeviceMessage.java b/server/src/main/java/com/genymobile/scrcpy/DeviceMessage.java index c6eebd38..2e333e3f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DeviceMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/DeviceMessage.java @@ -3,9 +3,11 @@ package com.genymobile.scrcpy; public final class DeviceMessage { public static final int TYPE_CLIPBOARD = 0; + public static final int TYPE_ACK_CLIPBOARD = 1; private int type; private String text; + private long sequence; private DeviceMessage() { } @@ -17,6 +19,13 @@ public final class DeviceMessage { return event; } + public static DeviceMessage createAckClipboard(long sequence) { + DeviceMessage event = new DeviceMessage(); + event.type = TYPE_ACK_CLIPBOARD; + event.sequence = sequence; + return event; + } + public int getType() { return type; } @@ -24,4 +33,8 @@ public final class DeviceMessage { public String getText() { return text; } + + public long getSequence() { + return sequence; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java b/server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java index 15d91a35..bcd8d206 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java +++ b/server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java @@ -15,7 +15,7 @@ public class DeviceMessageWriter { public void writeTo(DeviceMessage msg, OutputStream output) throws IOException { buffer.clear(); - buffer.put((byte) DeviceMessage.TYPE_CLIPBOARD); + buffer.put((byte) msg.getType()); switch (msg.getType()) { case DeviceMessage.TYPE_CLIPBOARD: String text = msg.getText(); @@ -25,6 +25,10 @@ public class DeviceMessageWriter { buffer.put(raw, 0, len); output.write(rawBuffer, 0, buffer.position()); break; + case DeviceMessage.TYPE_ACK_CLIPBOARD: + buffer.putLong(msg.getSequence()); + output.write(rawBuffer, 0, buffer.position()); + break; default: Ln.w("Unknown device message: " + msg.getType()); break; diff --git a/server/src/test/java/com/genymobile/scrcpy/DeviceMessageWriterTest.java b/server/src/test/java/com/genymobile/scrcpy/DeviceMessageWriterTest.java index 88bf2af9..7b917d33 100644 --- a/server/src/test/java/com/genymobile/scrcpy/DeviceMessageWriterTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/DeviceMessageWriterTest.java @@ -32,4 +32,24 @@ public class DeviceMessageWriterTest { Assert.assertArrayEquals(expected, actual); } + + @Test + public void testSerializeAckSetClipboard() throws IOException { + DeviceMessageWriter writer = new DeviceMessageWriter(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(DeviceMessage.TYPE_ACK_CLIPBOARD); + dos.writeLong(0x0102030405060708L); + + byte[] expected = bos.toByteArray(); + + DeviceMessage msg = DeviceMessage.createAckClipboard(0x0102030405060708L); + bos = new ByteArrayOutputStream(); + writer.writeTo(msg, bos); + + byte[] actual = bos.toByteArray(); + + Assert.assertArrayEquals(expected, actual); + } } From 41abe021e2a73efd4899b0efcd0b9eef9ec68c9b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 20 Nov 2021 12:23:45 +0100 Subject: [PATCH 0831/2244] Make the device acknowledge device clipboard If the client provided a sequence number on SET_CLIPBOARD request, make the device send back an acknowledgement once the clipboard is set. PR #2814 --- .../com/genymobile/scrcpy/ControlMessage.java | 2 ++ .../com/genymobile/scrcpy/Controller.java | 5 ++++ .../com/genymobile/scrcpy/DeviceMessage.java | 2 ++ .../scrcpy/DeviceMessageSender.java | 24 ++++++++++++++++--- 4 files changed, 30 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java index a536ed91..2cd80191 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java @@ -18,6 +18,8 @@ public final class ControlMessage { public static final int TYPE_SET_SCREEN_POWER_MODE = 10; public static final int TYPE_ROTATE_DEVICE = 11; + public static final long SEQUENCE_INVALID = 0; + private int type; private String text; private int metaState; // KeyEvent.META_* diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index 45882bb9..8b24b300 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -120,7 +120,12 @@ public class Controller { } break; case ControlMessage.TYPE_SET_CLIPBOARD: + long sequence = msg.getSequence(); setClipboard(msg.getText(), msg.getPaste()); + if (sequence != ControlMessage.SEQUENCE_INVALID) { + // Acknowledgement requested + sender.pushAckClipboard(sequence); + } break; case ControlMessage.TYPE_SET_SCREEN_POWER_MODE: if (device.supportsInputEvents()) { diff --git a/server/src/main/java/com/genymobile/scrcpy/DeviceMessage.java b/server/src/main/java/com/genymobile/scrcpy/DeviceMessage.java index 2e333e3f..5b7c4de5 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DeviceMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/DeviceMessage.java @@ -5,6 +5,8 @@ public final class DeviceMessage { public static final int TYPE_CLIPBOARD = 0; public static final int TYPE_ACK_CLIPBOARD = 1; + public static final long SEQUENCE_INVALID = ControlMessage.SEQUENCE_INVALID; + private int type; private String text; private long sequence; diff --git a/server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java b/server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java index bbf4dd2e..4ebccacc 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java +++ b/server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java @@ -8,6 +8,8 @@ public final class DeviceMessageSender { private String clipboardText; + private long ack; + public DeviceMessageSender(DesktopConnection connection) { this.connection = connection; } @@ -17,18 +19,34 @@ public final class DeviceMessageSender { notify(); } + public synchronized void pushAckClipboard(long sequence) { + ack = sequence; + notify(); + } + public void loop() throws IOException, InterruptedException { while (true) { String text; + long sequence; synchronized (this) { - while (clipboardText == null) { + while (ack == DeviceMessage.SEQUENCE_INVALID && clipboardText == null) { wait(); } text = clipboardText; clipboardText = null; + + sequence = ack; + ack = DeviceMessage.SEQUENCE_INVALID; + } + + if (sequence != DeviceMessage.SEQUENCE_INVALID) { + DeviceMessage event = DeviceMessage.createAckClipboard(sequence); + connection.sendDeviceMessage(event); + } + if (text != null) { + DeviceMessage event = DeviceMessage.createClipboard(text); + connection.sendDeviceMessage(event); } - DeviceMessage event = DeviceMessage.createClipboard(text); - connection.sendDeviceMessage(event); } } } From 2d5525eac128c6bb02344d733ab487b3aedd2431 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 21 Nov 2021 22:11:08 +0100 Subject: [PATCH 0832/2244] Move PRIu64 Windows workaround to compat.h So that we can use it from several files. PR #2814 --- app/src/compat.h | 6 ++++++ app/src/control_msg.c | 5 ----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/src/compat.h b/app/src/compat.h index 32759c01..c06c23fa 100644 --- a/app/src/compat.h +++ b/app/src/compat.h @@ -6,6 +6,12 @@ #include #include +#ifndef __WIN32 +# define PRIu64_ PRIu64 +#else +# define PRIu64_ "I64u" // Windows... +#endif + // In ffmpeg/doc/APIchanges: // 2018-02-06 - 0694d87024 - lavf 58.9.100 - avformat.h // Deprecate use of av_register_input_format(), av_register_output_format(), diff --git a/app/src/control_msg.c b/app/src/control_msg.c index 83ab0b7b..90cde0cf 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -171,11 +171,6 @@ control_msg_log(const struct control_msg *msg) { (long) msg->inject_touch_event.buttons); } else { // numeric pointer id -#ifndef __WIN32 -# define PRIu64_ PRIu64 -#else -# define PRIu64_ "I64u" // Windows... -#endif LOG_CMSG("touch [id=%" PRIu64_ "] %-4s position=%" PRIi32 ",%" PRIi32 " pressure=%g buttons=%06lx", id, From 5d17bcf1bc38690739c88ad2b0a069a67c8bedce Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 21 Nov 2021 17:40:11 +0100 Subject: [PATCH 0833/2244] Wait SET_CLIPBOARD ack before Ctrl+v via HID To allow seamless copy-paste, on Ctrl+v, a SET_CLIPBOARD request is performed before injecting Ctrl+v. But when HID keyboard is enabled, the Ctrl+v injection is not sent on the same channel as the clipboard request, so they are not serialized, and may occur in any order. If Ctrl+v happens to be injected before the new clipboard content is set, then the old content is pasted instead, which is incorrect. To minimize the probability of occurrence of the wrong order, a delay of 2 milliseconds was added before injecting Ctrl+v. Then 5ms. But even with 5ms, the wrong behavior sometimes happens. To handle it properly, add an acknowledgement mechanism, so that Ctrl+v is injected over AOA only after the SET_CLIPBOARD request has been performed and acknowledged by the server. Refs e4163321f00bb3830c6049bdb6c1515e7cc668a0 Refs 45b0f8123a52f5c73a5860d616f4ceba2766ca6a PR #2814 --- app/src/aoa_hid.c | 41 ++++++++++++++++++++++------------- app/src/aoa_hid.h | 7 ++++-- app/src/controller.c | 5 +++-- app/src/controller.h | 4 +++- app/src/hid_keyboard.c | 16 +++++++++----- app/src/input_manager.c | 30 ++++++++++++++++++++----- app/src/input_manager.h | 2 ++ app/src/keyboard_inject.c | 6 +++-- app/src/receiver.c | 19 +++++++++++----- app/src/receiver.h | 6 ++++- app/src/scrcpy.c | 23 ++++++++++++++++++-- app/src/trait/key_processor.h | 18 +++++++++++---- app/src/util/acksync.h | 5 +++++ 13 files changed, 135 insertions(+), 47 deletions(-) diff --git a/app/src/aoa_hid.c b/app/src/aoa_hid.c index 4c0b2bda..6fd32610 100644 --- a/app/src/aoa_hid.c +++ b/app/src/aoa_hid.c @@ -35,7 +35,7 @@ sc_hid_event_init(struct sc_hid_event *hid_event, uint16_t accessory_id, hid_event->accessory_id = accessory_id; hid_event->buffer = buffer; hid_event->size = buffer_size; - hid_event->delay = 0; + hid_event->ack_to_wait = SC_SEQUENCE_INVALID; } void @@ -118,7 +118,10 @@ sc_aoa_open_usb_handle(libusb_device *device, libusb_device_handle **handle) { } bool -sc_aoa_init(struct sc_aoa *aoa, const char *serial) { +sc_aoa_init(struct sc_aoa *aoa, const char *serial, + struct sc_acksync *acksync) { + assert(acksync); + cbuf_init(&aoa->queue); if (!sc_mutex_init(&aoa->mutex)) { @@ -155,6 +158,7 @@ sc_aoa_init(struct sc_aoa *aoa, const char *serial) { } aoa->stopped = false; + aoa->acksync = acksync; return true; } @@ -332,23 +336,28 @@ run_aoa_thread(void *data) { assert(non_empty); (void) non_empty; - assert(event.delay >= 0); - if (event.delay) { - // Wait during the specified delay before injecting the HID event - sc_tick deadline = sc_tick_now() + event.delay; - bool timed_out = false; - while (!aoa->stopped && !timed_out) { - timed_out = !sc_cond_timedwait(&aoa->event_cond, &aoa->mutex, - deadline); - } - if (aoa->stopped) { - sc_mutex_unlock(&aoa->mutex); + uint64_t ack_to_wait = event.ack_to_wait; + sc_mutex_unlock(&aoa->mutex); + + if (ack_to_wait != SC_SEQUENCE_INVALID) { + LOGD("Waiting ack from server sequence=%" PRIu64_, ack_to_wait); + // Do not block the loop indefinitely if the ack never comes (it should + // never happen) + sc_tick deadline = sc_tick_now() + SC_TICK_FROM_MS(500); + enum sc_acksync_wait_result result = + sc_acksync_wait(aoa->acksync, ack_to_wait, deadline); + + if (result == SC_ACKSYNC_WAIT_TIMEOUT) { + LOGW("Ack not received after 500ms, discarding HID event"); + sc_hid_event_destroy(&event); + continue; + } else if (result == SC_ACKSYNC_WAIT_INTR) { + // stopped + sc_hid_event_destroy(&event); break; } } - sc_mutex_unlock(&aoa->mutex); - bool ok = sc_aoa_send_hid_event(aoa, &event); sc_hid_event_destroy(&event); if (!ok) { @@ -377,6 +386,8 @@ sc_aoa_stop(struct sc_aoa *aoa) { aoa->stopped = true; sc_cond_signal(&aoa->event_cond); sc_mutex_unlock(&aoa->mutex); + + sc_acksync_interrupt(aoa->acksync); } void diff --git a/app/src/aoa_hid.h b/app/src/aoa_hid.h index 24cef502..e8fb9708 100644 --- a/app/src/aoa_hid.h +++ b/app/src/aoa_hid.h @@ -6,6 +6,7 @@ #include +#include "util/acksync.h" #include "util/cbuf.h" #include "util/thread.h" #include "util/tick.h" @@ -14,7 +15,7 @@ struct sc_hid_event { uint16_t accessory_id; unsigned char *buffer; uint16_t size; - sc_tick delay; + uint64_t ack_to_wait; }; // Takes ownership of buffer @@ -36,10 +37,12 @@ struct sc_aoa { sc_cond event_cond; bool stopped; struct sc_hid_event_queue queue; + + struct sc_acksync *acksync; }; bool -sc_aoa_init(struct sc_aoa *aoa, const char *serial); +sc_aoa_init(struct sc_aoa *aoa, const char *serial, struct sc_acksync *acksync); void sc_aoa_destroy(struct sc_aoa *aoa); diff --git a/app/src/controller.c b/app/src/controller.c index e486ea72..6cf3d20e 100644 --- a/app/src/controller.c +++ b/app/src/controller.c @@ -5,10 +5,11 @@ #include "util/log.h" bool -controller_init(struct controller *controller, sc_socket control_socket) { +controller_init(struct controller *controller, sc_socket control_socket, + struct sc_acksync *acksync) { cbuf_init(&controller->queue); - bool ok = receiver_init(&controller->receiver, control_socket); + bool ok = receiver_init(&controller->receiver, control_socket, acksync); if (!ok) { return false; } diff --git a/app/src/controller.h b/app/src/controller.h index e7004131..00267878 100644 --- a/app/src/controller.h +++ b/app/src/controller.h @@ -7,6 +7,7 @@ #include "control_msg.h" #include "receiver.h" +#include "util/acksync.h" #include "util/cbuf.h" #include "util/net.h" #include "util/thread.h" @@ -24,7 +25,8 @@ struct controller { }; bool -controller_init(struct controller *controller, sc_socket control_socket); +controller_init(struct controller *controller, sc_socket control_socket, + struct sc_acksync *acksync); void controller_destroy(struct controller *controller); diff --git a/app/src/hid_keyboard.c b/app/src/hid_keyboard.c index 2809ccd6..2afc09b2 100644 --- a/app/src/hid_keyboard.c +++ b/app/src/hid_keyboard.c @@ -279,7 +279,7 @@ push_mod_lock_state(struct sc_hid_keyboard *kb, uint16_t sdl_mod) { static void sc_key_processor_process_key(struct sc_key_processor *kp, const SDL_KeyboardEvent *event, - bool device_clipboard_set) { + uint64_t ack_to_wait) { if (event->repeat) { // In USB HID protocol, key repeat is handled by the host (Android), so // just ignore key repeat here. @@ -299,12 +299,12 @@ sc_key_processor_process_key(struct sc_key_processor *kp, } } - if (device_clipboard_set) { + if (ack_to_wait) { // Ctrl+v is pressed, so clipboard synchronization has been - // requested. Wait a bit so that the clipboard is set before - // injecting Ctrl+v via HID, otherwise it would paste the old - // clipboard content. - hid_event.delay = SC_TICK_FROM_MS(5); + // requested. Wait until clipboard synchronization is acknowledged + // by the server, otherwise it could paste the old clipboard + // content. + hid_event.ack_to_wait = ack_to_wait; } if (!sc_aoa_push_hid_event(kb->aoa, &hid_event)) { @@ -345,6 +345,10 @@ sc_hid_keyboard_init(struct sc_hid_keyboard *kb, struct sc_aoa *aoa) { .process_text = sc_key_processor_process_text, }; + // Clipboard synchronization is requested over the control socket, while HID + // events are sent over AOA, so it must wait for clipboard synchronization + // to be acknowledged by the device before injecting Ctrl+v. + kb->key_processor.async_paste = true; kb->key_processor.ops = &ops; return true; diff --git a/app/src/input_manager.c b/app/src/input_manager.c index e7afee77..f1715b79 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -82,6 +82,8 @@ input_manager_init(struct input_manager *im, struct controller *controller, im->last_keycode = SDLK_UNKNOWN; im->last_mod = 0; im->key_repeat = 0; + + im->next_sequence = 1; // 0 is reserved for SC_SEQUENCE_INVALID } static void @@ -209,7 +211,8 @@ collapse_panels(struct controller *controller) { } static bool -set_device_clipboard(struct controller *controller, bool paste) { +set_device_clipboard(struct controller *controller, bool paste, + uint64_t sequence) { char *text = SDL_GetClipboardText(); if (!text) { LOGW("Could not get clipboard text: %s", SDL_GetError()); @@ -225,7 +228,7 @@ set_device_clipboard(struct controller *controller, bool paste) { struct control_msg msg; msg.type = CONTROL_MSG_TYPE_SET_CLIPBOARD; - msg.set_clipboard.sequence = 0; // unused for now + msg.set_clipboard.sequence = sequence; msg.set_clipboard.text = text_dup; msg.set_clipboard.paste = paste; @@ -461,8 +464,10 @@ input_manager_process_key(struct input_manager *im, // inject the text as input events clipboard_paste(controller); } else { - // store the text in the device clipboard and paste - set_device_clipboard(controller, true); + // store the text in the device clipboard and paste, + // without requesting an acknowledgment + set_device_clipboard(controller, true, + SC_SEQUENCE_INVALID); } } return; @@ -511,6 +516,7 @@ input_manager_process_key(struct input_manager *im, return; } + uint64_t ack_to_wait = SC_SEQUENCE_INVALID; bool is_ctrl_v = ctrl && !shift && keycode == SDLK_v && down && !repeat; if (is_ctrl_v) { if (im->legacy_paste) { @@ -518,16 +524,28 @@ input_manager_process_key(struct input_manager *im, clipboard_paste(controller); return; } + + // Request an acknowledgement only if necessary + uint64_t sequence = im->kp->async_paste ? im->next_sequence + : SC_SEQUENCE_INVALID; + // Synchronize the computer clipboard to the device clipboard before // sending Ctrl+v, to allow seamless copy-paste. - bool ok = set_device_clipboard(controller, false); + bool ok = set_device_clipboard(controller, false, sequence); if (!ok) { LOGW("Clipboard could not be synchronized, Ctrl+v not injected"); return; } + + if (im->kp->async_paste) { + // The key processor must wait for this ack before injecting Ctrl+v + ack_to_wait = sequence; + // Increment only when the request succeeded + ++im->next_sequence; + } } - im->kp->ops->process_key(im->kp, event, is_ctrl_v); + im->kp->ops->process_key(im->kp, event, ack_to_wait); } static void diff --git a/app/src/input_manager.h b/app/src/input_manager.h index f018f98a..22d77381 100644 --- a/app/src/input_manager.h +++ b/app/src/input_manager.h @@ -38,6 +38,8 @@ struct input_manager { unsigned key_repeat; SDL_Keycode last_keycode; uint16_t last_mod; + + uint64_t next_sequence; // used for request acknowledgements }; void diff --git a/app/src/keyboard_inject.c b/app/src/keyboard_inject.c index e59b5520..4112fd4c 100644 --- a/app/src/keyboard_inject.c +++ b/app/src/keyboard_inject.c @@ -189,11 +189,11 @@ convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to, static void sc_key_processor_process_key(struct sc_key_processor *kp, const SDL_KeyboardEvent *event, - bool device_clipboard_set) { + uint64_t ack_to_wait) { // The device clipboard synchronization and the key event messages are // serialized, there is nothing special to do to ensure that the clipboard // is set before injecting Ctrl+v. - (void) device_clipboard_set; + (void) ack_to_wait; struct sc_keyboard_inject *ki = DOWNCAST(kp); @@ -256,5 +256,7 @@ sc_keyboard_inject_init(struct sc_keyboard_inject *ki, .process_text = sc_key_processor_process_text, }; + // Key injection and clipboard synchronization are serialized + ki->key_processor.async_paste = false; ki->key_processor.ops = &ops; } diff --git a/app/src/receiver.c b/app/src/receiver.c index 52e6034d..eeb206f1 100644 --- a/app/src/receiver.c +++ b/app/src/receiver.c @@ -7,12 +7,16 @@ #include "util/log.h" bool -receiver_init(struct receiver *receiver, sc_socket control_socket) { +receiver_init(struct receiver *receiver, sc_socket control_socket, + struct sc_acksync *acksync) { bool ok = sc_mutex_init(&receiver->mutex); if (!ok) { return false; } + receiver->control_socket = control_socket; + receiver->acksync = acksync; + return true; } @@ -22,7 +26,7 @@ receiver_destroy(struct receiver *receiver) { } static void -process_msg(struct device_msg *msg) { +process_msg(struct receiver *receiver, struct device_msg *msg) { switch (msg->type) { case DEVICE_MSG_TYPE_CLIPBOARD: { char *current = SDL_GetClipboardText(); @@ -38,13 +42,16 @@ process_msg(struct device_msg *msg) { break; } case DEVICE_MSG_TYPE_ACK_CLIPBOARD: - // TODO + assert(receiver->acksync); + LOGD("Ack device clipboard sequence=%" PRIu64_, + msg->ack_clipboard.sequence); + sc_acksync_ack(receiver->acksync, msg->ack_clipboard.sequence); break; } } static ssize_t -process_msgs(const unsigned char *buf, size_t len) { +process_msgs(struct receiver *receiver, const unsigned char *buf, size_t len) { size_t head = 0; for (;;) { struct device_msg msg; @@ -56,7 +63,7 @@ process_msgs(const unsigned char *buf, size_t len) { return head; } - process_msg(&msg); + process_msg(receiver, &msg); device_msg_destroy(&msg); head += r; @@ -84,7 +91,7 @@ run_receiver(void *data) { } head += r; - ssize_t consumed = process_msgs(buf, head); + ssize_t consumed = process_msgs(receiver, buf, head); if (consumed == -1) { // an error occurred break; diff --git a/app/src/receiver.h b/app/src/receiver.h index 99f128a4..3c4e8c64 100644 --- a/app/src/receiver.h +++ b/app/src/receiver.h @@ -5,6 +5,7 @@ #include +#include "util/acksync.h" #include "util/net.h" #include "util/thread.h" @@ -14,10 +15,13 @@ struct receiver { sc_socket control_socket; sc_thread thread; sc_mutex mutex; + + struct sc_acksync *acksync; }; bool -receiver_init(struct receiver *receiver, sc_socket control_socket); +receiver_init(struct receiver *receiver, sc_socket control_socket, + struct sc_acksync *acksync); void receiver_destroy(struct receiver *receiver); diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 61087681..04239bf5 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -27,6 +27,7 @@ #include "screen.h" #include "server.h" #include "stream.h" +#include "util/acksync.h" #include "util/log.h" #include "util/net.h" #ifdef HAVE_V4L2 @@ -46,6 +47,8 @@ struct scrcpy { struct file_handler file_handler; #ifdef HAVE_AOA_HID struct sc_aoa aoa; + // sequence/ack helper to synchronize clipboard and Ctrl+v via HID + struct sc_acksync acksync; #endif union { struct sc_keyboard_inject keyboard_inject; @@ -340,6 +343,8 @@ scrcpy(struct scrcpy_options *options) { bool controller_started = false; bool screen_initialized = false; + struct sc_acksync *acksync = NULL; + struct sc_server_params params = { .serial = options->serial, .log_level = options->log_level, @@ -445,7 +450,18 @@ scrcpy(struct scrcpy_options *options) { } if (options->control) { - if (!controller_init(&s->controller, s->server.control_socket)) { +#ifdef HAVE_AOA_HID + if (options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_HID) { + bool ok = sc_acksync_init(&s->acksync); + if (!ok) { + goto end; + } + + acksync = &s->acksync; + } +#endif + if (!controller_init(&s->controller, s->server.control_socket, + acksync)) { goto end; } controller_initialized = true; @@ -521,7 +537,7 @@ scrcpy(struct scrcpy_options *options) { #ifdef HAVE_AOA_HID bool aoa_hid_ok = false; - bool ok = sc_aoa_init(&s->aoa, serial); + bool ok = sc_aoa_init(&s->aoa, serial, acksync); if (!ok) { goto aoa_hid_end; } @@ -586,6 +602,9 @@ end: sc_hid_keyboard_destroy(&s->keyboard_hid); sc_aoa_stop(&s->aoa); } + if (acksync) { + sc_acksync_destroy(acksync); + } #endif if (controller_started) { controller_stop(&s->controller); diff --git a/app/src/trait/key_processor.h b/app/src/trait/key_processor.h index 1f8132f2..f4afe27b 100644 --- a/app/src/trait/key_processor.h +++ b/app/src/trait/key_processor.h @@ -14,6 +14,15 @@ * Component able to process and inject keys should implement this trait. */ struct sc_key_processor { + /** + * Set by the implementation to indicate that it must explicitly wait for + * the clipboard to be set on the device before injecting Ctrl+v to avoid + * race conditions. If it is set, the input_manager will pass a valid + * ack_to_wait to process_key() in case of clipboard synchronization + * resulting of the key event. + */ + bool async_paste; + const struct sc_key_processor_ops *ops; }; @@ -22,13 +31,14 @@ struct sc_key_processor_ops { /** * Process the keyboard event * - * The flag `device_clipboard_set` indicates that the input manager sent a - * control message to synchronize the device clipboard as a result of this - * key event. + * The `sequence` number (if different from `SC_SEQUENCE_INVALID`) indicates + * the acknowledgement number to wait for before injecting this event. + * This allows to ensure that the device clipboard is set before injecting + * Ctrl+v on the device. */ void (*process_key)(struct sc_key_processor *kp, const SDL_KeyboardEvent *event, - bool device_clipboard_set); + uint64_t ack_to_wait); void (*process_text)(struct sc_key_processor *kp, diff --git a/app/src/util/acksync.h b/app/src/util/acksync.h index 1fd34444..58ab1b35 100644 --- a/app/src/util/acksync.h +++ b/app/src/util/acksync.h @@ -9,6 +9,11 @@ /** * Helper to wait for acknowledgments + * + * In practice, it is used to wait for device clipboard acknowledgement from the + * server before injecting Ctrl+v via AOA HID, in order to avoid pasting the + * content of the old device clipboard (if Ctrl+v was injected before the + * clipboard content was actually set). */ struct sc_acksync { sc_mutex mutex; From 6abff46c9f8414bfc62cc8bb85c10555ab62eff9 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 22 Nov 2021 08:49:10 +0100 Subject: [PATCH 0834/2244] Add option to disable clipboard autosync By default, scrcpy automatically synchronizes the computer clipboard to the device clipboard before injecting Ctrl+v, and the device clipboard to the computer clipboard whenever it changes. This new option --no-clipboard-autosync disables this automatic synchronization. Fixes #2228 PR #2817 --- README.md | 3 +++ app/scrcpy.1 | 6 ++++++ app/src/cli.c | 13 +++++++++++++ app/src/input_manager.c | 3 ++- app/src/input_manager.h | 1 + app/src/options.c | 1 + app/src/options.h | 1 + app/src/scrcpy.c | 1 + app/src/server.c | 1 + app/src/server.h | 1 + .../src/main/java/com/genymobile/scrcpy/Device.java | 4 ++-- .../main/java/com/genymobile/scrcpy/Options.java | 9 +++++++++ .../src/main/java/com/genymobile/scrcpy/Server.java | 5 ++++- 13 files changed, 45 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index f355c7b6..fa4aff68 100644 --- a/README.md +++ b/README.md @@ -730,6 +730,9 @@ of Ctrl+v and MOD+v so that they also inject the computer clipboard text as a sequence of key events (the same way as MOD+Shift+v). +To disable automatic clipboard synchronization, use +`--no-clipboard-autosync`. + #### Pinch-to-zoom To simulate "pinch-to-zoom": Ctrl+_click-and-move_. diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 7438118b..122af757 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -114,6 +114,12 @@ Limit both the width and height of the video to \fIvalue\fR. The other dimension Default is 0 (unlimited). +.TP +.B \-\-no\-clipboard\-autosync +By default, scrcpy automatically synchronizes the computer clipboard to the device clipboard before injecting Ctrl+v, and the device clipboard to the computer clipboard whenever it changes. + +This option disables this automatic synchronization. + .TP .B \-n, \-\-no\-control Disable device control (mirror the device in read\-only). diff --git a/app/src/cli.c b/app/src/cli.c index 5256d50e..29cb8932 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -49,6 +49,7 @@ #define OPT_V4L2_BUFFER 1029 #define OPT_TUNNEL_HOST 1030 #define OPT_TUNNEL_PORT 1031 +#define OPT_NO_CLIPBOARD_AUTOSYNC 1032 struct sc_option { char shortopt; @@ -208,6 +209,15 @@ static const struct sc_option options[] = { "is preserved.\n" "Default is 0 (unlimited).", }, + { + .longopt_id = OPT_NO_CLIPBOARD_AUTOSYNC, + .longopt = "no-clipboard-autosync", + .text = "By default, scrcpy automatically synchronizes the computer " + "clipboard to the device clipboard before injecting Ctrl+v, " + "and the device clipboard to the computer clipboard whenever " + "it changes.\n" + "This option disables this automatic synchronization." + }, { .shortopt = 'n', .longopt = "no-control", @@ -1364,6 +1374,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; } break; + case OPT_NO_CLIPBOARD_AUTOSYNC: + opts->clipboard_autosync = false; + break; #ifdef HAVE_V4L2 case OPT_V4L2_SINK: opts->v4l2_device = optarg; diff --git a/app/src/input_manager.c b/app/src/input_manager.c index f1715b79..31d6540f 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -66,6 +66,7 @@ input_manager_init(struct input_manager *im, struct controller *controller, im->control = options->control; im->forward_all_clicks = options->forward_all_clicks; im->legacy_paste = options->legacy_paste; + im->clipboard_autosync = options->clipboard_autosync; const struct sc_shortcut_mods *shortcut_mods = &options->shortcut_mods; assert(shortcut_mods->count); @@ -518,7 +519,7 @@ input_manager_process_key(struct input_manager *im, uint64_t ack_to_wait = SC_SEQUENCE_INVALID; bool is_ctrl_v = ctrl && !shift && keycode == SDLK_v && down && !repeat; - if (is_ctrl_v) { + if (im->clipboard_autosync && is_ctrl_v) { if (im->legacy_paste) { // inject the text as input events clipboard_paste(controller); diff --git a/app/src/input_manager.h b/app/src/input_manager.h index 22d77381..5e02b457 100644 --- a/app/src/input_manager.h +++ b/app/src/input_manager.h @@ -24,6 +24,7 @@ struct input_manager { bool control; bool forward_all_clicks; bool legacy_paste; + bool clipboard_autosync; struct { unsigned data[SC_MAX_SHORTCUT_MODS]; diff --git a/app/src/options.c b/app/src/options.c index 074bdf08..a99b09da 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -53,4 +53,5 @@ const struct scrcpy_options scrcpy_options_default = { .forward_all_clicks = false, .legacy_paste = false, .power_off_on_close = false, + .clipboard_autosync = true, }; diff --git a/app/src/options.h b/app/src/options.h index 82d094cd..d10b2e8a 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -108,6 +108,7 @@ struct scrcpy_options { bool forward_all_clicks; bool legacy_paste; bool power_off_on_close; + bool clipboard_autosync; }; extern const struct scrcpy_options scrcpy_options_default; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 04239bf5..061a4e76 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -364,6 +364,7 @@ scrcpy(struct scrcpy_options *options) { .encoder_name = options->encoder_name, .force_adb_forward = options->force_adb_forward, .power_off_on_close = options->power_off_on_close, + .clipboard_autosync = options->clipboard_autosync, }; static const struct sc_server_callbacks cbs = { diff --git a/app/src/server.c b/app/src/server.c index 78127d2d..546216e9 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -186,6 +186,7 @@ execute_server(struct sc_server *server, params->codec_options ? params->codec_options : "-", params->encoder_name ? params->encoder_name : "-", params->power_off_on_close ? "true" : "false", + params->clipboard_autosync ? "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 cb1fadbb..5b25ff46 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -41,6 +41,7 @@ struct sc_server_params { bool stay_awake; bool force_adb_forward; bool power_off_on_close; + bool clipboard_autosync; }; struct sc_server { diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index 093646e2..03ae9b22 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -82,8 +82,8 @@ public final class Device { } }, displayId); - if (options.getControl()) { - // If control is enabled, synchronize Android clipboard to the computer automatically + if (options.getControl() && options.getClipboardAutosync()) { + // If control and autosync are enabled, synchronize Android clipboard to the computer automatically ClipboardManager clipboardManager = SERVICE_MANAGER.getClipboardManager(); if (clipboardManager != null) { clipboardManager.addPrimaryClipChangedListener(new IOnPrimaryClipChangedListener.Stub() { diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index cf11df0f..74467c7c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -18,6 +18,7 @@ public class Options { private String codecOptions; private String encoderName; private boolean powerOffScreenOnClose; + private boolean clipboardAutosync; public Ln.Level getLogLevel() { return logLevel; @@ -138,4 +139,12 @@ public class Options { public boolean getPowerOffScreenOnClose() { return this.powerOffScreenOnClose; } + + public boolean getClipboardAutosync() { + return clipboardAutosync; + } + + public void setClipboardAutosync(boolean clipboardAutosync) { + this.clipboardAutosync = clipboardAutosync; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 5a1f4619..188974ff 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -160,7 +160,7 @@ public final class Server { "The server version (" + BuildConfig.VERSION_NAME + ") does not match the client " + "(" + clientVersion + ")"); } - final int expectedParameters = 16; + final int expectedParameters = 17; if (args.length != expectedParameters) { throw new IllegalArgumentException("Expecting " + expectedParameters + " parameters"); } @@ -213,6 +213,9 @@ public final class Server { boolean powerOffScreenOnClose = Boolean.parseBoolean(args[15]); options.setPowerOffScreenOnClose(powerOffScreenOnClose); + boolean clipboardAutosync = Boolean.parseBoolean(args[16]); + options.setClipboardAutosync(clipboardAutosync); + return options; } From dc0ac01e0089c42703e422e00af439c57e41b9a4 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 24 Nov 2021 19:36:33 +0100 Subject: [PATCH 0835/2244] Define common feature test macros for all systems _POSIX_C_SOURCE, _XOPEN_SOURCE and _GNU_SOURCE are also used on Windows. Fix regression introduced by ba547e3895397e3710e7eb14faafbabbd7e3a077. --- app/meson.build | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/meson.build b/app/meson.build index d6fac580..71a0b2e3 100644 --- a/app/meson.build +++ b/app/meson.build @@ -42,6 +42,10 @@ src = [ conf = configuration_data() +conf.set('_POSIX_C_SOURCE', '200809L') +conf.set('_XOPEN_SOURCE', '700') +conf.set('_GNU_SOURCE', true) + if host_machine.system() == 'windows' src += [ 'src/sys/win/file.c', @@ -54,9 +58,6 @@ else 'src/sys/unix/file.c', 'src/sys/unix/process.c', ] - conf.set('_POSIX_C_SOURCE', '200809L') - conf.set('_XOPEN_SOURCE', '700') - conf.set('_GNU_SOURCE', true) if host_machine.system() == 'darwin' conf.set('_DARWIN_C_SOURCE', true) endif From 6f487a28928c396fb6d1d5bb8d58f5b5f5df4d29 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 24 Nov 2021 19:37:33 +0100 Subject: [PATCH 0836/2244] Add missing includes in compat implementation These includes are necessary for the strdup() compat implementation. --- app/src/compat.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/compat.c b/app/src/compat.c index b3b98bf1..4d084517 100644 --- a/app/src/compat.c +++ b/app/src/compat.c @@ -2,6 +2,9 @@ #include "config.h" +#include +#include + #ifndef HAVE_STRDUP char *strdup(const char *s) { size_t size = strlen(s) + 1; From d6c0054545dda7b105ecda51c4f99a221dc36f79 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 24 Nov 2021 19:38:33 +0100 Subject: [PATCH 0837/2244] Move check_functions in meson script Move the variable to the place it is actually used. --- app/meson.build | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/meson.build b/app/meson.build index 71a0b2e3..32adf18b 100644 --- a/app/meson.build +++ b/app/meson.build @@ -76,10 +76,6 @@ if aoa_hid_support ] endif -check_functions = [ - 'strdup' -] - cc = meson.get_compiler('c') if not get_option('crossbuild_windows') @@ -140,6 +136,10 @@ if host_machine.system() == 'windows' dependencies += cc.find_library('ws2_32') endif +check_functions = [ + 'strdup', +] + foreach f : check_functions if cc.has_function(f) define = 'HAVE_' + f.underscorify().to_upper() From d5f6697f3a592245e8bd12a0ea8dbd24d3129296 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 24 Nov 2021 19:55:00 +0100 Subject: [PATCH 0838/2244] Add (v)asprintf compatibility functions In case they are not available on the platform. --- app/meson.build | 2 ++ app/src/compat.c | 36 ++++++++++++++++++++++++++++++++++++ app/src/compat.h | 8 ++++++++ 3 files changed, 46 insertions(+) diff --git a/app/meson.build b/app/meson.build index 32adf18b..1561fdcd 100644 --- a/app/meson.build +++ b/app/meson.build @@ -138,6 +138,8 @@ endif check_functions = [ 'strdup', + 'asprintf', + 'vasprintf', ] foreach f : check_functions diff --git a/app/src/compat.c b/app/src/compat.c index 4d084517..11ddd3cb 100644 --- a/app/src/compat.c +++ b/app/src/compat.c @@ -2,7 +2,10 @@ #include "config.h" +#include #include +#include +#include #include #ifndef HAVE_STRDUP @@ -15,3 +18,36 @@ char *strdup(const char *s) { return dup; } #endif + +#ifndef HAVE_ASPRINTF +int asprintf(char **strp, const char *fmt, ...) { + va_list va; + va_start(va, fmt); + int ret = vasprintf(strp, fmt, va); + va_end(va); + return ret; +} +#endif + +#ifndef HAVE_VASPRINTF +int vasprintf(char **strp, const char *fmt, va_list ap) { + va_list va; + va_copy(va, ap); + int len = vsnprintf(NULL, 0, fmt, va); + va_end(va); + + char *str = malloc(len + 1); + if (!str) { + return -1; + } + + va_copy(va, ap); + int len2 = vsnprintf(str, len + 1, fmt, va); + (void) len2; + assert(len == len2); + va_end(va); + + *strp = str; + return len; +} +#endif diff --git a/app/src/compat.h b/app/src/compat.h index c06c23fa..3ab56049 100644 --- a/app/src/compat.h +++ b/app/src/compat.h @@ -58,4 +58,12 @@ char *strdup(const char *s); #endif +#ifndef HAVE_ASPRINTF +int asprintf(char **strp, const char *fmt, ...); +#endif + +#ifndef HAVE_VASPRINTF +int vasprintf(char **strp, const char *fmt, va_list ap); +#endif + #endif From 5434ea543cf94fcec389483b1fc021c2f13a34b3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 24 Nov 2021 21:01:25 +0100 Subject: [PATCH 0839/2244] Remove local "serial" variable In execute_server(), the serial is used only once. Moreover, it can be retrieved from the `params` argument directly. --- app/src/server.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/src/server.c b/app/src/server.c index 546216e9..f70c0640 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -139,8 +139,6 @@ log_level_to_server_string(enum sc_log_level level) { static sc_pid execute_server(struct sc_server *server, const struct sc_server_params *params) { - const char *serial = server->params.serial; - char max_size_string[6]; char bit_rate_string[11]; char max_fps_string[6]; @@ -199,7 +197,7 @@ execute_server(struct sc_server *server, // Port: 5005 // Then click on "Debug" #endif - return adb_execute(serial, cmd, ARRAY_LEN(cmd)); + return adb_execute(params->serial, cmd, ARRAY_LEN(cmd)); } static bool From 2c3099e2de13913332f3e0085faf646e19bb976b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 24 Nov 2021 21:06:02 +0100 Subject: [PATCH 0840/2244] Parse codec options early For consistency with other options, parse the codec options on the server before storing them in the Options instance. --- server/src/main/java/com/genymobile/scrcpy/Options.java | 8 +++++--- server/src/main/java/com/genymobile/scrcpy/Server.java | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 74467c7c..20a579ed 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -2,6 +2,8 @@ package com.genymobile.scrcpy; import android.graphics.Rect; +import java.util.List; + public class Options { private Ln.Level logLevel; private int maxSize; @@ -15,7 +17,7 @@ public class Options { private int displayId; private boolean showTouches; private boolean stayAwake; - private String codecOptions; + private List codecOptions; private String encoderName; private boolean powerOffScreenOnClose; private boolean clipboardAutosync; @@ -116,11 +118,11 @@ public class Options { this.stayAwake = stayAwake; } - public String getCodecOptions() { + public List getCodecOptions() { return codecOptions; } - public void setCodecOptions(String codecOptions) { + public void setCodecOptions(List codecOptions) { this.codecOptions = codecOptions; } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 188974ff..bcdaf6bb 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -61,7 +61,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()); + List codecOptions = options.getCodecOptions(); Thread initThread = startInitThread(options); @@ -204,7 +204,7 @@ public final class Server { boolean stayAwake = Boolean.parseBoolean(args[12]); options.setStayAwake(stayAwake); - String codecOptions = args[13]; + List codecOptions = CodecOption.parse(args[13]); options.setCodecOptions(codecOptions); String encoderName = "-".equals(args[14]) ? null : args[14]; From 2eb881c5f13ae2c3a8c9ca68188c1abce77b48bd Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 24 Nov 2021 21:10:18 +0100 Subject: [PATCH 0841/2244] Allocate and format server command args This simplifies formatting. --- app/src/server.c | 90 ++++++++++++++++++++++++++++-------------------- 1 file changed, 53 insertions(+), 37 deletions(-) diff --git a/app/src/server.c b/app/src/server.c index f70c0640..2ea37af6 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -139,23 +139,17 @@ log_level_to_server_string(enum sc_log_level level) { static sc_pid execute_server(struct sc_server *server, const struct sc_server_params *params) { - char max_size_string[6]; - char bit_rate_string[11]; - char max_fps_string[6]; - char lock_video_orientation_string[5]; - char display_id_string[11]; - 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, "%"PRIu32, params->display_id); - const char *const cmd[] = { - "shell", - "CLASSPATH=" SC_DEVICE_SERVER_PATH, - "app_process", + sc_pid pid = SC_PROCESS_NONE; + + const char *cmd[128]; + unsigned count = 0; + cmd[count++] = "shell"; + cmd[count++] = "CLASSPATH=" SC_DEVICE_SERVER_PATH; + cmd[count++] = "app_process"; + #ifdef SERVER_DEBUGGER # define SERVER_DEBUGGER_PORT "5005" + cmd[count++] = # ifdef SERVER_DEBUGGER_METHOD_NEW /* Android 9 and above */ "-XjdwpProvider:internal -XjdwpOptions:transport=dt_socket,suspend=y," @@ -164,28 +158,43 @@ execute_server(struct sc_server *server, /* Android 8 and below */ "-agentlib:jdwp=transport=dt_socket,suspend=y,server=y,address=" # endif - SERVER_DEBUGGER_PORT, + SERVER_DEBUGGER_PORT; #endif - "/", // unused - "com.genymobile.scrcpy.Server", - SCRCPY_VERSION, - log_level_to_server_string(params->log_level), - 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) - params->control ? "true" : "false", - display_id_string, - params->show_touches ? "true" : "false", - params->stay_awake ? "true" : "false", - params->codec_options ? params->codec_options : "-", - params->encoder_name ? params->encoder_name : "-", - params->power_off_on_close ? "true" : "false", - params->clipboard_autosync ? "true" : "false", - }; + cmd[count++] = "/"; // unused + cmd[count++] = "com.genymobile.scrcpy.Server"; + cmd[count++] = SCRCPY_VERSION; + cmd[count++] = log_level_to_server_string(params->log_level); + + unsigned dyn_idx = count; // from there, the strings are allocated +#define ADD_PARAM(fmt, ...) { \ + char *p = (char *) &cmd[count]; \ + if (asprintf(&p, fmt, ## __VA_ARGS__) == -1) { \ + goto end; \ + } \ + cmd[count++] = p; \ + } +#define STRBOOL(v) (v ? "true" : "false") + + ADD_PARAM("%" PRIu16, params->max_size); + ADD_PARAM("%" PRIu32, params->bit_rate); + ADD_PARAM("%" PRIu16, params->max_fps); + ADD_PARAM("%" PRIi8, params->lock_video_orientation); + ADD_PARAM("%s", STRBOOL(server->tunnel.forward)); + ADD_PARAM("%s", params->crop ? params->crop : "-"); + // always send frame meta (packet boundaries + timestamp) + ADD_PARAM("true"); + ADD_PARAM("%s", STRBOOL(params->control)); + ADD_PARAM("%" PRIu32, params->display_id); + ADD_PARAM("%s", STRBOOL(params->show_touches)); + ADD_PARAM("%s", STRBOOL(params->stay_awake)); + ADD_PARAM("%s", params->codec_options ? params->codec_options : "-"); + ADD_PARAM("%s", params->encoder_name ? params->encoder_name : "-"); + ADD_PARAM("%s", STRBOOL(params->power_off_on_close)); + ADD_PARAM("%s", STRBOOL(params->clipboard_autosync)); + +#undef ADD_PARAM +#undef STRBOOL + #ifdef SERVER_DEBUGGER LOGI("Server debugger waiting for a client on device port " SERVER_DEBUGGER_PORT "..."); @@ -197,7 +206,14 @@ execute_server(struct sc_server *server, // Port: 5005 // Then click on "Debug" #endif - return adb_execute(params->serial, cmd, ARRAY_LEN(cmd)); + pid = adb_execute(params->serial, cmd, count); + +end: + for (unsigned i = dyn_idx; i < count; ++i) { + free((char *) cmd[i]); + } + + return pid; } static bool From 04e5537f8ce21dbae365dcd980efda911188c945 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 24 Nov 2021 21:23:20 +0100 Subject: [PATCH 0842/2244] Pass server parameters as key=value pairs The options values to configure the server were identified by their command-line argument index. Now that there are a lot of arguments, many of them being booleans, it became unreadable and error-prone. Identify the arguments by a key string instead, and make them optional. This will also simplify running the server manually for debugging. --- app/src/server.c | 34 ++--- .../com/genymobile/scrcpy/CodecOption.java | 2 +- .../java/com/genymobile/scrcpy/Options.java | 12 +- .../java/com/genymobile/scrcpy/Server.java | 134 +++++++++++------- 4 files changed, 105 insertions(+), 77 deletions(-) diff --git a/app/src/server.c b/app/src/server.c index 2ea37af6..98f0a188 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -163,7 +163,6 @@ execute_server(struct sc_server *server, cmd[count++] = "/"; // unused cmd[count++] = "com.genymobile.scrcpy.Server"; cmd[count++] = SCRCPY_VERSION; - cmd[count++] = log_level_to_server_string(params->log_level); unsigned dyn_idx = count; // from there, the strings are allocated #define ADD_PARAM(fmt, ...) { \ @@ -175,22 +174,25 @@ execute_server(struct sc_server *server, } #define STRBOOL(v) (v ? "true" : "false") - ADD_PARAM("%" PRIu16, params->max_size); - ADD_PARAM("%" PRIu32, params->bit_rate); - ADD_PARAM("%" PRIu16, params->max_fps); - ADD_PARAM("%" PRIi8, params->lock_video_orientation); - ADD_PARAM("%s", STRBOOL(server->tunnel.forward)); - ADD_PARAM("%s", params->crop ? params->crop : "-"); + ADD_PARAM("log_level=%s", log_level_to_server_string(params->log_level)); + ADD_PARAM("max_size=%" PRIu16, params->max_size); + ADD_PARAM("bit_rate=%" PRIu32, params->bit_rate); + ADD_PARAM("max_fps=%" PRIu16, params->max_fps); + ADD_PARAM("lock_video_orientation=%" PRIi8, params->lock_video_orientation); + ADD_PARAM("tunnel_forward=%s", STRBOOL(server->tunnel.forward)); + ADD_PARAM("crop=%s", params->crop ? params->crop : ""); // always send frame meta (packet boundaries + timestamp) - ADD_PARAM("true"); - ADD_PARAM("%s", STRBOOL(params->control)); - ADD_PARAM("%" PRIu32, params->display_id); - ADD_PARAM("%s", STRBOOL(params->show_touches)); - ADD_PARAM("%s", STRBOOL(params->stay_awake)); - ADD_PARAM("%s", params->codec_options ? params->codec_options : "-"); - ADD_PARAM("%s", params->encoder_name ? params->encoder_name : "-"); - ADD_PARAM("%s", STRBOOL(params->power_off_on_close)); - ADD_PARAM("%s", STRBOOL(params->clipboard_autosync)); + ADD_PARAM("send_frame_meta=true"); + ADD_PARAM("control=%s", STRBOOL(params->control)); + ADD_PARAM("display_id=%" PRIu32, params->display_id); + ADD_PARAM("show_touches=%s", STRBOOL(params->show_touches)); + ADD_PARAM("stay_awake=%s", STRBOOL(params->stay_awake)); + ADD_PARAM("codec_options=%s", + params->codec_options ? params->codec_options : ""); + ADD_PARAM("encoder_name=%s", + params->encoder_name ? params->encoder_name : ""); + ADD_PARAM("power_off_on_close=%s", STRBOOL(params->power_off_on_close)); + ADD_PARAM("clipboard_autosync=%s", STRBOOL(params->clipboard_autosync)); #undef ADD_PARAM #undef STRBOOL diff --git a/server/src/main/java/com/genymobile/scrcpy/CodecOption.java b/server/src/main/java/com/genymobile/scrcpy/CodecOption.java index 1897bda3..12f2a889 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CodecOption.java +++ b/server/src/main/java/com/genymobile/scrcpy/CodecOption.java @@ -21,7 +21,7 @@ public class CodecOption { } public static List parse(String codecOptions) { - if ("-".equals(codecOptions)) { + if (codecOptions.isEmpty()) { return null; } diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 20a579ed..f649d776 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -5,22 +5,22 @@ import android.graphics.Rect; import java.util.List; public class Options { - private Ln.Level logLevel; + private Ln.Level logLevel = Ln.Level.DEBUG; private int maxSize; - private int bitRate; + private int bitRate = 8000000; private int maxFps; - private int lockedVideoOrientation; + private int lockedVideoOrientation = -1; private boolean tunnelForward; private Rect crop; - private boolean sendFrameMeta; // send PTS so that the client may record properly - private boolean control; + private boolean sendFrameMeta = true; // send PTS so that the client may record properly + private boolean control = true; private int displayId; private boolean showTouches; private boolean stayAwake; private List codecOptions; private String encoderName; private boolean powerOffScreenOnClose; - private boolean clipboardAutosync; + private boolean clipboardAutosync = true; public Ln.Level getLogLevel() { return logLevel; diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index bcdaf6bb..71920627 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -160,67 +160,93 @@ public final class Server { "The server version (" + BuildConfig.VERSION_NAME + ") does not match the client " + "(" + clientVersion + ")"); } - final int expectedParameters = 17; - if (args.length != expectedParameters) { - throw new IllegalArgumentException("Expecting " + expectedParameters + " parameters"); - } - Options options = new Options(); - Ln.Level level = Ln.Level.valueOf(args[1].toUpperCase(Locale.ENGLISH)); - options.setLogLevel(level); - - int maxSize = Integer.parseInt(args[2]) & ~7; // multiple of 8 - options.setMaxSize(maxSize); - - int bitRate = Integer.parseInt(args[3]); - options.setBitRate(bitRate); - - int maxFps = Integer.parseInt(args[4]); - options.setMaxFps(maxFps); - - 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[6]); - options.setTunnelForward(tunnelForward); - - Rect crop = parseCrop(args[7]); - options.setCrop(crop); - - boolean sendFrameMeta = Boolean.parseBoolean(args[8]); - options.setSendFrameMeta(sendFrameMeta); - - boolean control = Boolean.parseBoolean(args[9]); - options.setControl(control); - - int displayId = Integer.parseInt(args[10]); - options.setDisplayId(displayId); - - boolean showTouches = Boolean.parseBoolean(args[11]); - options.setShowTouches(showTouches); - - boolean stayAwake = Boolean.parseBoolean(args[12]); - options.setStayAwake(stayAwake); - - List codecOptions = CodecOption.parse(args[13]); - options.setCodecOptions(codecOptions); - - String encoderName = "-".equals(args[14]) ? null : args[14]; - options.setEncoderName(encoderName); - - boolean powerOffScreenOnClose = Boolean.parseBoolean(args[15]); - options.setPowerOffScreenOnClose(powerOffScreenOnClose); - - boolean clipboardAutosync = Boolean.parseBoolean(args[16]); - options.setClipboardAutosync(clipboardAutosync); + for (int i = 1; i < args.length; ++i) { + String arg = args[i]; + int equalIndex = arg.indexOf('='); + if (equalIndex == -1) { + throw new IllegalArgumentException("Invalid key=value pair: \"" + arg + "\""); + } + String key = arg.substring(0, equalIndex); + String value = arg.substring(equalIndex + 1); + switch (key) { + case "log_level": + Ln.Level level = Ln.Level.valueOf(value.toUpperCase(Locale.ENGLISH)); + options.setLogLevel(level); + break; + case "max_size": + int maxSize = Integer.parseInt(value) & ~7; // multiple of 8 + options.setMaxSize(maxSize); + break; + case "bit_rate": + int bitRate = Integer.parseInt(value); + options.setBitRate(bitRate); + break; + case "max_fps": + int maxFps = Integer.parseInt(value); + options.setMaxFps(maxFps); + break; + case "lock_video_orientation": + int lockedVideoOrientation = Integer.parseInt(value); + options.setLockedVideoOrientation(lockedVideoOrientation); + break; + case "tunnel_forward": + boolean tunnelForward = Boolean.parseBoolean(value); + options.setTunnelForward(tunnelForward); + break; + case "crop": + Rect crop = parseCrop(value); + options.setCrop(crop); + break; + case "send_frame_meta": + boolean sendFrameMeta = Boolean.parseBoolean(value); + options.setSendFrameMeta(sendFrameMeta); + break; + case "control": + boolean control = Boolean.parseBoolean(value); + options.setControl(control); + break; + case "display_id": + int displayId = Integer.parseInt(value); + options.setDisplayId(displayId); + break; + case "show_touches": + boolean showTouches = Boolean.parseBoolean(value); + options.setShowTouches(showTouches); + break; + case "stay_awake": + boolean stayAwake = Boolean.parseBoolean(value); + options.setStayAwake(stayAwake); + break; + case "codec_options": + List codecOptions = CodecOption.parse(value); + options.setCodecOptions(codecOptions); + break; + case "encoder_name": + if (!value.isEmpty()) { + options.setEncoderName(value); + } + break; + case "power_off_on_close": + boolean powerOffScreenOnClose = Boolean.parseBoolean(value); + options.setPowerOffScreenOnClose(powerOffScreenOnClose); + break; + case "clipboard_autosync": + boolean clipboardAutosync = Boolean.parseBoolean(value); + options.setClipboardAutosync(clipboardAutosync); + break; + default: + Ln.w("Unknown server option: " + key); + break; + } + } return options; } private static Rect parseCrop(String crop) { - if ("-".equals(crop)) { + if (crop.isEmpty()) { return null; } // input format: "width:height:x:y" From 7b29f5fd2de8f302e03365013093f5665efd3cfd Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 24 Nov 2021 21:31:12 +0100 Subject: [PATCH 0843/2244] Do not pass default values to the server By default, only pass the version and the bit rate (the default bitrate is a client compilation option). --- app/src/server.c | 60 ++++++++++++++++++++++++++++++++++-------------- 1 file changed, 43 insertions(+), 17 deletions(-) diff --git a/app/src/server.c b/app/src/server.c index 98f0a188..2bb7d717 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -175,24 +175,50 @@ execute_server(struct sc_server *server, #define STRBOOL(v) (v ? "true" : "false") ADD_PARAM("log_level=%s", log_level_to_server_string(params->log_level)); - ADD_PARAM("max_size=%" PRIu16, params->max_size); ADD_PARAM("bit_rate=%" PRIu32, params->bit_rate); - ADD_PARAM("max_fps=%" PRIu16, params->max_fps); - ADD_PARAM("lock_video_orientation=%" PRIi8, params->lock_video_orientation); - ADD_PARAM("tunnel_forward=%s", STRBOOL(server->tunnel.forward)); - ADD_PARAM("crop=%s", params->crop ? params->crop : ""); - // always send frame meta (packet boundaries + timestamp) - ADD_PARAM("send_frame_meta=true"); - ADD_PARAM("control=%s", STRBOOL(params->control)); - ADD_PARAM("display_id=%" PRIu32, params->display_id); - ADD_PARAM("show_touches=%s", STRBOOL(params->show_touches)); - ADD_PARAM("stay_awake=%s", STRBOOL(params->stay_awake)); - ADD_PARAM("codec_options=%s", - params->codec_options ? params->codec_options : ""); - ADD_PARAM("encoder_name=%s", - params->encoder_name ? params->encoder_name : ""); - ADD_PARAM("power_off_on_close=%s", STRBOOL(params->power_off_on_close)); - ADD_PARAM("clipboard_autosync=%s", STRBOOL(params->clipboard_autosync)); + + if (params->max_size) { + ADD_PARAM("max_size=%" PRIu16, params->max_size); + } + if (params->max_fps) { + ADD_PARAM("max_fps=%" PRIu16, params->max_fps); + } + if (params->lock_video_orientation != SC_LOCK_VIDEO_ORIENTATION_UNLOCKED) { + ADD_PARAM("lock_video_orientation=%" PRIi8, + params->lock_video_orientation); + } + if (server->tunnel.forward) { + ADD_PARAM("tunnel_forward=%s", STRBOOL(server->tunnel.forward)); + } + if (params->crop) { + ADD_PARAM("crop=%s", params->crop); + } + if (!params->control) { + // By default, control is true + ADD_PARAM("control=%s", STRBOOL(params->control)); + } + if (params->display_id) { + ADD_PARAM("display_id=%" PRIu32, params->display_id); + } + if (params->show_touches) { + ADD_PARAM("show_touches=%s", STRBOOL(params->show_touches)); + } + if (params->stay_awake) { + ADD_PARAM("stay_awake=%s", STRBOOL(params->stay_awake)); + } + if (params->codec_options) { + ADD_PARAM("codec_options=%s", params->codec_options); + } + if (params->encoder_name) { + ADD_PARAM("encoder_name=%s", params->encoder_name); + } + if (params->power_off_on_close) { + ADD_PARAM("power_off_on_close=%s", STRBOOL(params->power_off_on_close)); + } + if (!params->clipboard_autosync) { + // By defaut, clipboard_autosync is true + ADD_PARAM("clipboard_autosync=%s", STRBOOL(params->clipboard_autosync)); + } #undef ADD_PARAM #undef STRBOOL From 4c4759886518f8c6fed10eefe31043d871b293af Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 24 Nov 2021 21:33:58 +0100 Subject: [PATCH 0844/2244] Make lockVideoOrientation option name uniform On the server, the option was named lockedVideoOrientation. --- server/src/main/java/com/genymobile/scrcpy/Device.java | 2 +- .../src/main/java/com/genymobile/scrcpy/Options.java | 10 +++++----- server/src/main/java/com/genymobile/scrcpy/Server.java | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index 03ae9b22..35f4efd4 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -65,7 +65,7 @@ public final class Device { int displayInfoFlags = displayInfo.getFlags(); - screenInfo = ScreenInfo.computeScreenInfo(displayInfo, options.getCrop(), options.getMaxSize(), options.getLockedVideoOrientation()); + screenInfo = ScreenInfo.computeScreenInfo(displayInfo, options.getCrop(), options.getMaxSize(), options.getLockVideoOrientation()); layerStack = displayInfo.getLayerStack(); SERVICE_MANAGER.getWindowManager().registerRotationWatcher(new IRotationWatcher.Stub() { diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index f649d776..1ac17176 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -9,7 +9,7 @@ public class Options { private int maxSize; private int bitRate = 8000000; private int maxFps; - private int lockedVideoOrientation = -1; + private int lockVideoOrientation = -1; private boolean tunnelForward; private Rect crop; private boolean sendFrameMeta = true; // send PTS so that the client may record properly @@ -54,12 +54,12 @@ public class Options { this.maxFps = maxFps; } - public int getLockedVideoOrientation() { - return lockedVideoOrientation; + public int getLockVideoOrientation() { + return lockVideoOrientation; } - public void setLockedVideoOrientation(int lockedVideoOrientation) { - this.lockedVideoOrientation = lockedVideoOrientation; + public void setLockVideoOrientation(int lockVideoOrientation) { + this.lockVideoOrientation = lockVideoOrientation; } public boolean isTunnelForward() { diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 71920627..0f0abab0 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -188,8 +188,8 @@ public final class Server { options.setMaxFps(maxFps); break; case "lock_video_orientation": - int lockedVideoOrientation = Integer.parseInt(value); - options.setLockedVideoOrientation(lockedVideoOrientation); + int lockVideoOrientation = Integer.parseInt(value); + options.setLockVideoOrientation(lockVideoOrientation); break; case "tunnel_forward": boolean tunnelForward = Boolean.parseBoolean(value); From 007f616302d08993835588e98f2836e0bf6d732d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 24 Nov 2021 21:44:29 +0100 Subject: [PATCH 0845/2244] Add missing includes Include these headers explicitly instead of relying on transitivity. --- app/src/sys/unix/file.c | 1 + app/src/v4l2_sink.c | 2 ++ app/tests/test_str.c | 1 + app/tests/test_strbuf.c | 1 + 4 files changed, 5 insertions(+) diff --git a/app/src/sys/unix/file.c b/app/src/sys/unix/file.c index 4e9e45b3..47b5d8f9 100644 --- a/app/src/sys/unix/file.c +++ b/app/src/sys/unix/file.c @@ -2,6 +2,7 @@ #include #include +#include #include #include #include diff --git a/app/src/v4l2_sink.c b/app/src/v4l2_sink.c index 753d5b6a..429d3f1a 100644 --- a/app/src/v4l2_sink.c +++ b/app/src/v4l2_sink.c @@ -1,5 +1,7 @@ #include "v4l2_sink.h" +#include + #include "util/log.h" #include "util/str.h" diff --git a/app/tests/test_str.c b/app/tests/test_str.c index 770ddd95..c66bb2f4 100644 --- a/app/tests/test_str.c +++ b/app/tests/test_str.c @@ -3,6 +3,7 @@ #include #include #include +#include #include #include "util/str.h" diff --git a/app/tests/test_strbuf.c b/app/tests/test_strbuf.c index 97417677..58562522 100644 --- a/app/tests/test_strbuf.c +++ b/app/tests/test_strbuf.c @@ -2,6 +2,7 @@ #include #include +#include #include #include "util/strbuf.h" From 73e0311d149e8fc64b6ca6d6ac79e5dee219c18c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 24 Nov 2021 21:46:02 +0100 Subject: [PATCH 0846/2244] Add missing return on file_handler failure Mistake introduced by 84334cf7db68732eb92caf6cf9682c7ef2c426f6. --- app/src/file_handler.c | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/file_handler.c b/app/src/file_handler.c index eead2117..a758f575 100644 --- a/app/src/file_handler.c +++ b/app/src/file_handler.c @@ -37,6 +37,7 @@ file_handler_init(struct file_handler *file_handler, const char *serial, LOGE("Could not create intr"); sc_cond_destroy(&file_handler->event_cond); sc_mutex_destroy(&file_handler->mutex); + return false; } file_handler->serial = strdup(serial); From 92a458e846410ff7990229be86a5931046229c80 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 24 Nov 2021 21:48:57 +0100 Subject: [PATCH 0847/2244] Remove unreachable return statements --- app/src/v4l2_sink.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/v4l2_sink.c b/app/src/v4l2_sink.c index 429d3f1a..439d111a 100644 --- a/app/src/v4l2_sink.c +++ b/app/src/v4l2_sink.c @@ -217,7 +217,6 @@ sc_v4l2_sink_open(struct sc_v4l2_sink *vs) { if (!vs->format_ctx->url) { LOGE("Could not strdup v4l2 device name"); goto error_avformat_free_context; - return false; } #else strncpy(vs->format_ctx->filename, vs->device_name, @@ -228,7 +227,6 @@ sc_v4l2_sink_open(struct sc_v4l2_sink *vs) { if (!ostream) { LOGE("Could not allocate new v4l2 stream"); goto error_avformat_free_context; - return false; } ostream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO; From 3653fb6b152af502885734e6d6cadbdda3421773 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 24 Nov 2021 22:06:11 +0100 Subject: [PATCH 0848/2244] Add OutOfMemory log helper Add a special LOG_OOM() function to log all OutOfMemory errors (i.e. allocations returning NULL). --- app/src/adb.c | 4 +++- app/src/aoa_hid.c | 2 ++ app/src/cli.c | 1 + app/src/decoder.c | 4 ++-- app/src/device_msg.c | 2 +- app/src/file_handler.c | 3 +-- app/src/frame_buffer.c | 2 ++ app/src/hid_keyboard.c | 1 + app/src/icon.c | 12 ++++++------ app/src/recorder.c | 12 ++++++------ app/src/scrcpy.c | 2 +- app/src/screen.c | 5 +---- app/src/server.c | 9 +++------ app/src/stream.c | 12 ++++++------ app/src/sys/unix/file.c | 5 +++++ app/src/sys/win/file.c | 2 +- app/src/sys/win/process.c | 4 +++- app/src/util/file.c | 2 +- app/src/util/intr.c | 2 +- app/src/util/log.h | 6 ++++++ app/src/util/net.c | 1 + app/src/util/str.c | 7 ++++++- app/src/util/strbuf.c | 5 ++++- app/src/util/thread.c | 3 +++ app/src/v4l2_sink.c | 18 +++++++----------- app/src/video_buffer.c | 7 +++---- 26 files changed, 77 insertions(+), 56 deletions(-) diff --git a/app/src/adb.c b/app/src/adb.c index 630a1952..176f50b2 100644 --- a/app/src/adb.c +++ b/app/src/adb.c @@ -86,7 +86,8 @@ show_adb_err_msg(enum sc_process_result err, const char *const argv[]) { #define MAX_COMMAND_STRING_LEN 1024 char *buf = malloc(MAX_COMMAND_STRING_LEN); if (!buf) { - LOGE("Failed to execute (could not allocate error message)"); + LOG_OOM(); + LOGE("Failed to execute"); return; } @@ -118,6 +119,7 @@ adb_execute_p(const char *serial, const char *const adb_cmd[], const char **argv = malloc((len + 4) * sizeof(*argv)); if (!argv) { + LOG_OOM(); return SC_PROCESS_NONE; } diff --git a/app/src/aoa_hid.c b/app/src/aoa_hid.c index 6fd32610..a3fc3bd4 100644 --- a/app/src/aoa_hid.c +++ b/app/src/aoa_hid.c @@ -4,6 +4,7 @@ #include #include "aoa_hid.h" +#include "util/log.h" // See . #define ACCESSORY_REGISTER_HID 54 @@ -20,6 +21,7 @@ sc_hid_event_log(const struct sc_hid_event *event) { unsigned buffer_size = event->size * 3 + 1; char *buffer = malloc(buffer_size); if (!buffer) { + LOG_OOM(); return; } for (unsigned i = 0; i < event->size; ++i) { diff --git a/app/src/cli.c b/app/src/cli.c index 29cb8932..7da3b904 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -595,6 +595,7 @@ sc_getopt_adapter_create_longopts(void) { struct option *longopts = malloc((ARRAY_LEN(options) + 1) * sizeof(*longopts)); if (!longopts) { + LOG_OOM(); return NULL; } diff --git a/app/src/decoder.c b/app/src/decoder.c index 7c67e836..7107e01d 100644 --- a/app/src/decoder.c +++ b/app/src/decoder.c @@ -42,7 +42,7 @@ static bool decoder_open(struct decoder *decoder, const AVCodec *codec) { decoder->codec_ctx = avcodec_alloc_context3(codec); if (!decoder->codec_ctx) { - LOGC("Could not allocate decoder context"); + LOG_OOM(); return false; } @@ -54,7 +54,7 @@ decoder_open(struct decoder *decoder, const AVCodec *codec) { decoder->frame = av_frame_alloc(); if (!decoder->frame) { - LOGE("Could not create decoder frame"); + LOG_OOM(); avcodec_close(decoder->codec_ctx); avcodec_free_context(&decoder->codec_ctx); return false; diff --git a/app/src/device_msg.c b/app/src/device_msg.c index 4163b9fc..9a93036b 100644 --- a/app/src/device_msg.c +++ b/app/src/device_msg.c @@ -24,7 +24,7 @@ device_msg_deserialize(const unsigned char *buf, size_t len, } char *text = malloc(clipboard_len + 1); if (!text) { - LOGW("Could not allocate text for clipboard"); + LOG_OOM(); return -1; } if (clipboard_len) { diff --git a/app/src/file_handler.c b/app/src/file_handler.c index a758f575..12498ccf 100644 --- a/app/src/file_handler.c +++ b/app/src/file_handler.c @@ -34,7 +34,6 @@ file_handler_init(struct file_handler *file_handler, const char *serial, ok = sc_intr_init(&file_handler->intr); if (!ok) { - LOGE("Could not create intr"); sc_cond_destroy(&file_handler->event_cond); sc_mutex_destroy(&file_handler->mutex); return false; @@ -42,7 +41,7 @@ file_handler_init(struct file_handler *file_handler, const char *serial, file_handler->serial = strdup(serial); if (!file_handler->serial) { - LOGE("Could not strdup serial"); + LOG_OOM(); sc_intr_destroy(&file_handler->intr); sc_cond_destroy(&file_handler->event_cond); sc_mutex_destroy(&file_handler->mutex); diff --git a/app/src/frame_buffer.c b/app/src/frame_buffer.c index 33ca6227..d177d4fa 100644 --- a/app/src/frame_buffer.c +++ b/app/src/frame_buffer.c @@ -10,11 +10,13 @@ bool sc_frame_buffer_init(struct sc_frame_buffer *fb) { fb->pending_frame = av_frame_alloc(); if (!fb->pending_frame) { + LOG_OOM(); return false; } fb->tmp_frame = av_frame_alloc(); if (!fb->tmp_frame) { + LOG_OOM(); av_frame_free(&fb->pending_frame); return false; } diff --git a/app/src/hid_keyboard.c b/app/src/hid_keyboard.c index 2afc09b2..20fe8d51 100644 --- a/app/src/hid_keyboard.c +++ b/app/src/hid_keyboard.c @@ -160,6 +160,7 @@ static bool sc_hid_keyboard_event_init(struct sc_hid_event *hid_event) { unsigned char *buffer = malloc(HID_KEYBOARD_EVENT_SIZE); if (!buffer) { + LOG_OOM(); return false; } diff --git a/app/src/icon.c b/app/src/icon.c index 2616007e..1d670242 100644 --- a/app/src/icon.c +++ b/app/src/icon.c @@ -31,7 +31,7 @@ get_icon_path(void) { char *icon_path = strdup(icon_path_env); #endif if (!icon_path) { - LOGE("Could not allocate memory"); + LOG_OOM(); return NULL; } LOGD("Using SCRCPY_ICON_PATH: %s", icon_path); @@ -42,7 +42,7 @@ get_icon_path(void) { LOGD("Using icon: " SCRCPY_DEFAULT_ICON_PATH); char *icon_path = strdup(SCRCPY_DEFAULT_ICON_PATH); if (!icon_path) { - LOGE("Could not allocate memory"); + LOG_OOM(); return NULL; } #else @@ -63,7 +63,7 @@ decode_image(const char *path) { AVFormatContext *ctx = avformat_alloc_context(); if (!ctx) { - LOGE("Could not allocate image decoder context"); + LOG_OOM(); return NULL; } @@ -93,7 +93,7 @@ decode_image(const char *path) { AVCodecContext *codec_ctx = avcodec_alloc_context3(codec); if (!codec_ctx) { - LOGE("Could not allocate codec context"); + LOG_OOM(); goto close_input; } @@ -109,13 +109,13 @@ decode_image(const char *path) { AVFrame *frame = av_frame_alloc(); if (!frame) { - LOGE("Could not allocate frame"); + LOG_OOM(); goto close_codec; } AVPacket *packet = av_packet_alloc(); if (!packet) { - LOGE("Could not allocate packet"); + LOG_OOM(); av_frame_free(&frame); goto close_codec; } diff --git a/app/src/recorder.c b/app/src/recorder.c index b9c585f4..4364b9a5 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -34,11 +34,13 @@ static struct record_packet * record_packet_new(const AVPacket *packet) { struct record_packet *rec = malloc(sizeof(*rec)); if (!rec) { + LOG_OOM(); return NULL; } rec->packet = av_packet_alloc(); if (!rec->packet) { + LOG_OOM(); free(rec); return NULL; } @@ -81,7 +83,7 @@ recorder_write_header(struct recorder *recorder, const AVPacket *packet) { uint8_t *extradata = av_malloc(packet->size * sizeof(uint8_t)); if (!extradata) { - LOGC("Could not allocate extradata"); + LOG_OOM(); return false; } @@ -228,13 +230,11 @@ static bool recorder_open(struct recorder *recorder, const AVCodec *input_codec) { bool ok = sc_mutex_init(&recorder->mutex); if (!ok) { - LOGC("Could not create mutex"); return false; } ok = sc_cond_init(&recorder->queue_cond); if (!ok) { - LOGC("Could not create cond"); goto error_mutex_destroy; } @@ -254,7 +254,7 @@ recorder_open(struct recorder *recorder, const AVCodec *input_codec) { recorder->ctx = avformat_alloc_context(); if (!recorder->ctx) { - LOGE("Could not allocate output context"); + LOG_OOM(); goto error_cond_destroy; } @@ -338,7 +338,7 @@ recorder_push(struct recorder *recorder, const AVPacket *packet) { struct record_packet *rec = record_packet_new(packet); if (!rec) { - LOGC("Could not allocate record packet"); + LOG_OOM(); sc_mutex_unlock(&recorder->mutex); return false; } @@ -375,7 +375,7 @@ recorder_init(struct recorder *recorder, struct sc_size declared_frame_size) { recorder->filename = strdup(filename); if (!recorder->filename) { - LOGE("Could not strdup filename"); + LOG_OOM(); return false; } diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 061a4e76..f0142b46 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -271,7 +271,7 @@ av_log_callback(void *avcl, int level, const char *fmt, va_list vl) { size_t fmt_len = strlen(fmt); char *local_fmt = malloc(fmt_len + 10); if (!local_fmt) { - LOGC("Could not allocate string"); + LOG_OOM(); return; } memcpy(local_fmt, "[FFmpeg] ", 9); // do not write the final '\0' diff --git a/app/src/screen.c b/app/src/screen.c index d402b402..34a2d5d9 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -366,18 +366,15 @@ screen_init(struct screen *screen, const struct screen_params *params) { bool ok = sc_video_buffer_init(&screen->vb, params->buffering_time, &cbs, screen); if (!ok) { - LOGE("Could not initialize video buffer"); return false; } ok = sc_video_buffer_start(&screen->vb); if (!ok) { - LOGE("Could not start video_buffer"); goto error_destroy_video_buffer; } if (!fps_counter_init(&screen->fps_counter)) { - LOGE("Could not initialize FPS counter"); goto error_stop_and_join_video_buffer; } @@ -478,7 +475,7 @@ screen_init(struct screen *screen, const struct screen_params *params) { screen->frame = av_frame_alloc(); if (!screen->frame) { - LOGC("Could not create screen frame"); + LOG_OOM(); goto error_destroy_texture; } diff --git a/app/src/server.c b/app/src/server.c index 2bb7d717..bce6c45b 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -34,7 +34,7 @@ get_server_path(void) { char *server_path = strdup(server_path_env); #endif if (!server_path) { - LOGE("Could not allocate memory"); + LOG_OOM(); return NULL; } LOGD("Using SCRCPY_SERVER_PATH: %s", server_path); @@ -45,7 +45,7 @@ get_server_path(void) { LOGD("Using server: " SC_SERVER_PATH_DEFAULT); char *server_path = strdup(SC_SERVER_PATH_DEFAULT); if (!server_path) { - LOGE("Could not allocate memory"); + LOG_OOM(); return NULL; } #else @@ -309,20 +309,18 @@ sc_server_init(struct sc_server *server, const struct sc_server_params *params, const struct sc_server_callbacks *cbs, void *cbs_userdata) { bool ok = sc_server_params_copy(&server->params, params); if (!ok) { - LOGE("Could not copy server params"); + LOG_OOM(); return false; } ok = sc_mutex_init(&server->mutex); if (!ok) { - LOGE("Could not create server mutex"); sc_server_params_destroy(&server->params); return false; } ok = sc_cond_init(&server->cond_stopped); if (!ok) { - LOGE("Could not create server cond_stopped"); sc_mutex_destroy(&server->mutex); sc_server_params_destroy(&server->params); return false; @@ -330,7 +328,6 @@ sc_server_init(struct sc_server *server, const struct sc_server_params *params, ok = sc_intr_init(&server->intr); if (!ok) { - LOGE("Could not create intr"); sc_cond_destroy(&server->cond_stopped); sc_mutex_destroy(&server->mutex); sc_server_params_destroy(&server->params); diff --git a/app/src/stream.c b/app/src/stream.c index 4c770250..3ac0f5e1 100644 --- a/app/src/stream.c +++ b/app/src/stream.c @@ -42,7 +42,7 @@ stream_recv_packet(struct stream *stream, AVPacket *packet) { assert(len); if (av_new_packet(packet, len)) { - LOGE("Could not allocate packet"); + LOG_OOM(); return false; } @@ -111,18 +111,18 @@ stream_push_packet(struct stream *stream, AVPacket *packet) { if (stream->pending) { offset = stream->pending->size; if (av_grow_packet(stream->pending, packet->size)) { - LOGE("Could not grow packet"); + LOG_OOM(); return false; } } else { offset = 0; stream->pending = av_packet_alloc(); if (!stream->pending) { - LOGE("Could not allocate packet"); + LOG_OOM(); return false; } if (av_new_packet(stream->pending, packet->size)) { - LOGE("Could not create packet"); + LOG_OOM(); av_packet_free(&stream->pending); return false; } @@ -200,7 +200,7 @@ run_stream(void *data) { stream->codec_ctx = avcodec_alloc_context3(codec); if (!stream->codec_ctx) { - LOGC("Could not allocate codec context"); + LOG_OOM(); goto end; } @@ -221,7 +221,7 @@ run_stream(void *data) { AVPacket *packet = av_packet_alloc(); if (!packet) { - LOGE("Could not allocate packet"); + LOG_OOM(); goto finally_close_parser; } diff --git a/app/src/sys/unix/file.c b/app/src/sys/unix/file.c index 47b5d8f9..9c3f7333 100644 --- a/app/src/sys/unix/file.c +++ b/app/src/sys/unix/file.c @@ -7,6 +7,8 @@ #include #include +#include "util/log.h" + bool sc_file_executable_exists(const char *file) { char *path = getenv("PATH"); @@ -24,7 +26,10 @@ sc_file_executable_exists(const char *file) { size_t dir_len = strlen(dir); char *fullpath = malloc(dir_len + file_len + 2); if (!fullpath) + { + LOG_OOM(); continue; + } memcpy(fullpath, dir, dir_len); fullpath[dir_len] = '/'; memcpy(fullpath + dir_len + 1, file, file_len + 1); diff --git a/app/src/sys/win/file.c b/app/src/sys/win/file.c index 5233b177..d3cf1760 100644 --- a/app/src/sys/win/file.c +++ b/app/src/sys/win/file.c @@ -26,7 +26,7 @@ bool sc_file_is_regular(const char *path) { wchar_t *wide_path = sc_str_to_wchars(path); if (!wide_path) { - LOGC("Could not allocate wide char string"); + LOG_OOM(); return false; } diff --git a/app/src/sys/win/process.c b/app/src/sys/win/process.c index 326a3d99..cf82f014 100644 --- a/app/src/sys/win/process.c +++ b/app/src/sys/win/process.c @@ -100,6 +100,7 @@ sc_process_execute_p(const char *const argv[], HANDLE *handle, lpAttributeList = malloc(size); if (!lpAttributeList) { + LOG_OOM(); goto error_close_stderr; } @@ -133,13 +134,14 @@ sc_process_execute_p(const char *const argv[], HANDLE *handle, char *cmd = malloc(CMD_MAX_LEN); if (!cmd || !build_cmd(cmd, CMD_MAX_LEN, argv)) { + LOG_OOM(); goto error_free_attribute_list; } wchar_t *wide = sc_str_to_wchars(cmd); free(cmd); if (!wide) { - LOGC("Could not allocate wide char string"); + LOG_OOM(); goto error_free_attribute_list; } diff --git a/app/src/util/file.c b/app/src/util/file.c index 59be2d91..174e5efd 100644 --- a/app/src/util/file.c +++ b/app/src/util/file.c @@ -31,7 +31,7 @@ sc_file_get_local_path(const char *name) { size_t len = dirlen + namelen + 2; // +2: '/' and '\0' char *file_path = malloc(len); if (!file_path) { - LOGE("Could not alloc path"); + LOG_OOM(); free(executable_path); return NULL; } diff --git a/app/src/util/intr.c b/app/src/util/intr.c index 50d9abbe..22bd121a 100644 --- a/app/src/util/intr.c +++ b/app/src/util/intr.c @@ -8,7 +8,7 @@ bool sc_intr_init(struct sc_intr *intr) { bool ok = sc_mutex_init(&intr->mutex); if (!ok) { - LOGE("Could not init intr mutex"); + LOG_OOM(); return false; } diff --git a/app/src/util/log.h b/app/src/util/log.h index 4157d6e5..231e0846 100644 --- a/app/src/util/log.h +++ b/app/src/util/log.h @@ -7,6 +7,9 @@ #include "options.h" +#define LOG_STR_IMPL_(x) # x +#define LOG_STR(x) LOG_STR_IMPL_(x) + #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__) @@ -14,6 +17,9 @@ #define LOGE(...) SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__) #define LOGC(...) SDL_LogCritical(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__) +#define LOG_OOM() \ + LOGC("OOM: %s:%d %s()", __FILE__, __LINE__, __func__) + void sc_set_log_level(enum sc_log_level level); diff --git a/app/src/util/net.c b/app/src/util/net.c index 824d4886..8c51d12b 100644 --- a/app/src/util/net.c +++ b/app/src/util/net.c @@ -52,6 +52,7 @@ wrap(sc_raw_socket sock) { struct sc_socket_windows *socket = malloc(sizeof(*socket)); if (!socket) { + LOG_OOM(); closesocket(sock); return SC_SOCKET_NONE; } diff --git a/app/src/util/str.c b/app/src/util/str.c index 3bd2752f..ab1c8783 100644 --- a/app/src/util/str.c +++ b/app/src/util/str.c @@ -5,13 +5,15 @@ #include #include #include -#include "util/strbuf.h" #ifdef _WIN32 # include # include #endif +#include "log.h" +#include "strbuf.h" + size_t sc_strncpy(char *dest, const char *src, size_t n) { size_t i; @@ -51,6 +53,7 @@ sc_str_quote(const char *src) { size_t len = strlen(src); char *quoted = malloc(len + 3); if (!quoted) { + LOG_OOM(); return NULL; } memcpy("ed[1], src, len); @@ -188,6 +191,7 @@ sc_str_to_wchars(const char *utf8) { wchar_t *wide = malloc(len * sizeof(wchar_t)); if (!wide) { + LOG_OOM(); return NULL; } @@ -204,6 +208,7 @@ sc_str_from_wchars(const wchar_t *ws) { char *utf8 = malloc(len); if (!utf8) { + LOG_OOM(); return NULL; } diff --git a/app/src/util/strbuf.c b/app/src/util/strbuf.c index b2b6f494..1892b46b 100644 --- a/app/src/util/strbuf.c +++ b/app/src/util/strbuf.c @@ -1,15 +1,17 @@ #include "strbuf.h" #include +#include #include #include -#include +#include "log.h" bool sc_strbuf_init(struct sc_strbuf *buf, size_t init_cap) { buf->s = malloc(init_cap + 1); // +1 for '\0' if (!buf->s) { + LOG_OOM(); return false; } @@ -25,6 +27,7 @@ sc_strbuf_reserve(struct sc_strbuf *buf, size_t len) { char *s = realloc(buf->s, new_cap + 1); // +1 for '\0' if (!s) { // Leave the old buf->s + LOG_OOM(); return false; } buf->s = s; diff --git a/app/src/util/thread.c b/app/src/util/thread.c index 2c376e97..23eddf1d 100644 --- a/app/src/util/thread.c +++ b/app/src/util/thread.c @@ -10,6 +10,7 @@ sc_thread_create(sc_thread *thread, sc_thread_fn fn, const char *name, void *userdata) { SDL_Thread *sdl_thread = SDL_CreateThread(fn, name, userdata); if (!sdl_thread) { + LOG_OOM(); return false; } @@ -26,6 +27,7 @@ bool sc_mutex_init(sc_mutex *mutex) { SDL_mutex *sdl_mutex = SDL_CreateMutex(); if (!sdl_mutex) { + LOG_OOM(); return false; } @@ -94,6 +96,7 @@ bool sc_cond_init(sc_cond *cond) { SDL_cond *sdl_cond = SDL_CreateCond(); if (!sdl_cond) { + LOG_OOM(); return false; } diff --git a/app/src/v4l2_sink.c b/app/src/v4l2_sink.c index 439d111a..dce11ce1 100644 --- a/app/src/v4l2_sink.c +++ b/app/src/v4l2_sink.c @@ -33,7 +33,7 @@ write_header(struct sc_v4l2_sink *vs, const AVPacket *packet) { uint8_t *extradata = av_malloc(packet->size * sizeof(uint8_t)); if (!extradata) { - LOGC("Could not allocate extradata"); + LOG_OOM(); return false; } @@ -163,25 +163,21 @@ sc_v4l2_sink_open(struct sc_v4l2_sink *vs) { bool ok = sc_video_buffer_init(&vs->vb, vs->buffering_time, &cbs, vs); if (!ok) { - LOGE("Could not initialize video buffer"); return false; } ok = sc_video_buffer_start(&vs->vb); if (!ok) { - LOGE("Could not start video buffer"); goto error_video_buffer_destroy; } ok = sc_mutex_init(&vs->mutex); if (!ok) { - LOGC("Could not create mutex"); goto error_video_buffer_stop_and_join; } ok = sc_cond_init(&vs->cond); if (!ok) { - LOGC("Could not create cond"); goto error_mutex_destroy; } @@ -203,7 +199,7 @@ sc_v4l2_sink_open(struct sc_v4l2_sink *vs) { vs->format_ctx = avformat_alloc_context(); if (!vs->format_ctx) { - LOGE("Could not allocate v4l2 output context"); + LOG_OOM(); return false; } @@ -215,7 +211,7 @@ sc_v4l2_sink_open(struct sc_v4l2_sink *vs) { #ifdef SCRCPY_LAVF_HAS_AVFORMATCONTEXT_URL vs->format_ctx->url = strdup(vs->device_name); if (!vs->format_ctx->url) { - LOGE("Could not strdup v4l2 device name"); + LOG_OOM(); goto error_avformat_free_context; } #else @@ -225,7 +221,7 @@ sc_v4l2_sink_open(struct sc_v4l2_sink *vs) { AVStream *ostream = avformat_new_stream(vs->format_ctx, encoder); if (!ostream) { - LOGE("Could not allocate new v4l2 stream"); + LOG_OOM(); goto error_avformat_free_context; } @@ -244,7 +240,7 @@ sc_v4l2_sink_open(struct sc_v4l2_sink *vs) { vs->encoder_ctx = avcodec_alloc_context3(encoder); if (!vs->encoder_ctx) { - LOGC("Could not allocate codec context for v4l2"); + LOG_OOM(); goto error_avio_close; } @@ -261,13 +257,13 @@ sc_v4l2_sink_open(struct sc_v4l2_sink *vs) { vs->frame = av_frame_alloc(); if (!vs->frame) { - LOGE("Could not create v4l2 frame"); + LOG_OOM(); goto error_avcodec_close; } vs->packet = av_packet_alloc(); if (!vs->packet) { - LOGE("Could not allocate packet"); + LOG_OOM(); goto error_av_frame_free; } diff --git a/app/src/video_buffer.c b/app/src/video_buffer.c index f71a4e78..12e66cf1 100644 --- a/app/src/video_buffer.c +++ b/app/src/video_buffer.c @@ -14,11 +14,13 @@ static struct sc_video_buffer_frame * sc_video_buffer_frame_new(const AVFrame *frame) { struct sc_video_buffer_frame *vb_frame = malloc(sizeof(*vb_frame)); if (!vb_frame) { + LOG_OOM(); return NULL; } vb_frame->frame = av_frame_alloc(); if (!vb_frame->frame) { + LOG_OOM(); free(vb_frame); return NULL; } @@ -132,14 +134,12 @@ sc_video_buffer_init(struct sc_video_buffer *vb, sc_tick buffering_time, if (buffering_time) { ok = sc_mutex_init(&vb->b.mutex); if (!ok) { - LOGC("Could not create mutex"); sc_frame_buffer_destroy(&vb->fb); return false; } ok = sc_cond_init(&vb->b.queue_cond); if (!ok) { - LOGC("Could not create cond"); sc_mutex_destroy(&vb->b.mutex); sc_frame_buffer_destroy(&vb->fb); return false; @@ -147,7 +147,6 @@ sc_video_buffer_init(struct sc_video_buffer *vb, sc_tick buffering_time, ok = sc_cond_init(&vb->b.wait_cond); if (!ok) { - LOGC("Could not create wait cond"); sc_cond_destroy(&vb->b.queue_cond); sc_mutex_destroy(&vb->b.mutex); sc_frame_buffer_destroy(&vb->fb); @@ -234,7 +233,7 @@ sc_video_buffer_push(struct sc_video_buffer *vb, const AVFrame *frame) { struct sc_video_buffer_frame *vb_frame = sc_video_buffer_frame_new(frame); if (!vb_frame) { sc_mutex_unlock(&vb->b.mutex); - LOGE("Could not allocate frame"); + LOG_OOM(); return false; } From b0eb1a55d6f1180df2d19fda91d08290dff4f687 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 25 Nov 2021 21:35:07 +0100 Subject: [PATCH 0849/2244] Fix adb get-serialno error handling If pipe read fails, return. --- app/src/adb.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/adb.c b/app/src/adb.c index 176f50b2..05516280 100644 --- a/app/src/adb.c +++ b/app/src/adb.c @@ -304,6 +304,10 @@ adb_get_serialno(struct sc_intr *intr) { return NULL; } + if (r == -1) { + return false; + } + sc_str_truncate(buf, r, " \r\n"); return strdup(buf); From d31725f07708908ced7a71ea53a4d8a3e6bbb4b6 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 25 Nov 2021 21:48:05 +0100 Subject: [PATCH 0850/2244] Reorder cli sanity checks Check unexpected additional arguments before other sanity checks. --- app/src/cli.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index 7da3b904..791d3c43 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -1394,6 +1394,12 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } } + int index = optind; + if (index < argc) { + LOGE("Unexpected additional argument: %s", argv[index]); + return false; + } + #ifdef HAVE_V4L2 if (!opts->display && !opts->record_filename && !opts->v4l2_device) { LOGE("-N/--no-display requires either screen recording (-r/--record)" @@ -1425,12 +1431,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], opts->force_adb_forward = true; } - int index = optind; - if (index < argc) { - LOGE("Unexpected additional argument: %s", argv[index]); - return false; - } - if (opts->record_format && !opts->record_filename) { LOGE("Record format specified without recording"); return false; From 8ed3328055c52fd415954626c4a1f763b24a1927 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 25 Nov 2021 21:54:08 +0100 Subject: [PATCH 0851/2244] Use unsigned for connection attempts count There is no reason to use an explicit uint32_t. --- app/src/server.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/server.c b/app/src/server.c index bce6c45b..f0624a13 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -264,10 +264,10 @@ connect_and_read_byte(struct sc_intr *intr, sc_socket socket, } static sc_socket -connect_to_server(struct sc_server *server, uint32_t attempts, sc_tick delay, +connect_to_server(struct sc_server *server, unsigned attempts, sc_tick delay, uint32_t host, uint16_t port) { do { - LOGD("Remaining connection attempts: %d", (int) attempts); + LOGD("Remaining connection attempts: %u", attempts); sc_socket socket = net_socket(); if (socket != SC_SOCKET_NONE) { bool ok = connect_and_read_byte(&server->intr, socket, host, port); @@ -300,7 +300,7 @@ connect_to_server(struct sc_server *server, uint32_t attempts, sc_tick delay, break; } } - } while (--attempts > 0); + } while (--attempts); return SC_SOCKET_NONE; } @@ -403,7 +403,7 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) { tunnel_port = tunnel->local_port; } - uint32_t attempts = 100; + unsigned attempts = 100; sc_tick delay = SC_TICK_FROM_MS(100); video_socket = connect_to_server(server, attempts, delay, tunnel_host, tunnel_port); From 680d2cc9400d371a8c26402aa49ab4cf6a3d6b7e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 25 Nov 2021 08:57:31 +0100 Subject: [PATCH 0852/2244] Extract command argv building This simplifies adb_execute_p(). --- app/src/adb.c | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/app/src/adb.c b/app/src/adb.c index 05516280..aa0cef78 100644 --- a/app/src/adb.c +++ b/app/src/adb.c @@ -111,19 +111,16 @@ show_adb_err_msg(enum sc_process_result err, const char *const argv[]) { free(buf); } -static sc_pid -adb_execute_p(const char *serial, const char *const adb_cmd[], - size_t len, sc_pipe *pout) { - int i; - sc_pid pid; - +static const char ** +adb_create_argv(const char *serial, const char *const adb_cmd[], size_t len) { const char **argv = malloc((len + 4) * sizeof(*argv)); if (!argv) { LOG_OOM(); - return SC_PROCESS_NONE; + return NULL; } argv[0] = get_adb_command(); + int i; if (serial) { argv[1] = "-s"; argv[2] = serial; @@ -134,6 +131,18 @@ adb_execute_p(const char *serial, const char *const adb_cmd[], memcpy(&argv[i], adb_cmd, len * sizeof(const char *)); argv[len + i] = NULL; + return argv; +} + +static sc_pid +adb_execute_p(const char *serial, const char *const adb_cmd[], size_t len, + sc_pipe *pout) { + const char **argv = adb_create_argv(serial, adb_cmd, len); + if (!argv) { + return SC_PROCESS_NONE; + } + + sc_pid pid; enum sc_process_result r = sc_process_execute_p(argv, &pid, NULL, pout, NULL); if (r != SC_PROCESS_SUCCESS) { From 904f0ae61e7902973e862a6151ef972877fc3380 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 25 Nov 2021 09:37:53 +0100 Subject: [PATCH 0853/2244] Check process success locally for adb commands Remove sc_process_check_success() from the process API, it is too specific. --- app/src/adb.c | 54 ++++++++++++++++++++++++++++++------- app/src/util/process.c | 19 ------------- app/src/util/process.h | 8 ------ app/src/util/process_intr.c | 21 --------------- app/src/util/process_intr.h | 4 --- 5 files changed, 44 insertions(+), 62 deletions(-) diff --git a/app/src/adb.c b/app/src/adb.c index aa0cef78..7383d347 100644 --- a/app/src/adb.c +++ b/app/src/adb.c @@ -111,6 +111,43 @@ show_adb_err_msg(enum sc_process_result err, const char *const argv[]) { free(buf); } +static bool +process_check_success_internal(sc_pid pid, const char *name, bool close) { + if (pid == SC_PROCESS_NONE) { + LOGE("Could not execute \"%s\"", name); + return false; + } + sc_exit_code exit_code = sc_process_wait(pid, close); + if (exit_code) { + if (exit_code != SC_EXIT_CODE_NONE) { + LOGE("\"%s\" returned with value %" SC_PRIexitcode, name, + exit_code); + } else { + LOGE("\"%s\" exited unexpectedly", name); + } + return false; + } + return true; +} + +static bool +process_check_success_intr(struct sc_intr *intr, sc_pid pid, const char *name) { + if (!sc_intr_set_process(intr, pid)) { + // Already interrupted + return false; + } + + // Always pass close=false, interrupting would be racy otherwise + bool ret = process_check_success_internal(pid, name, false); + + sc_intr_set_process(intr, SC_PROCESS_NONE); + + // Close separately + sc_process_close(pid); + + return ret; +} + static const char ** adb_create_argv(const char *serial, const char *const adb_cmd[], size_t len) { const char **argv = malloc((len + 4) * sizeof(*argv)); @@ -255,43 +292,41 @@ bool adb_forward(struct sc_intr *intr, const char *serial, uint16_t local_port, const char *device_socket_name) { sc_pid pid = adb_exec_forward(serial, local_port, device_socket_name); - return sc_process_check_success_intr(intr, pid, "adb forward", true); + return process_check_success_intr(intr, pid, "adb forward"); } bool adb_forward_remove(struct sc_intr *intr, const char *serial, uint16_t local_port) { sc_pid pid = adb_exec_forward_remove(serial, local_port); - return sc_process_check_success_intr(intr, pid, "adb forward --remove", - true); + return process_check_success_intr(intr, pid, "adb forward --remove"); } bool adb_reverse(struct sc_intr *intr, const char *serial, const char *device_socket_name, uint16_t local_port) { sc_pid pid = adb_exec_reverse(serial, device_socket_name, local_port); - return sc_process_check_success_intr(intr, pid, "adb reverse", true); + return process_check_success_intr(intr, pid, "adb reverse"); } bool adb_reverse_remove(struct sc_intr *intr, const char *serial, const char *device_socket_name) { sc_pid pid = adb_exec_reverse_remove(serial, device_socket_name); - return sc_process_check_success_intr(intr, pid, "adb reverse --remove", - true); + return process_check_success_intr(intr, pid, "adb reverse --remove"); } bool adb_push(struct sc_intr *intr, const char *serial, const char *local, const char *remote) { sc_pid pid = adb_exec_push(serial, local, remote); - return sc_process_check_success_intr(intr, pid, "adb push", true); + return process_check_success_intr(intr, pid, "adb push"); } bool adb_install(struct sc_intr *intr, const char *serial, const char *local) { sc_pid pid = adb_exec_install(serial, local); - return sc_process_check_success_intr(intr, pid, "adb install", true); + return process_check_success_intr(intr, pid, "adb install"); } char * @@ -307,8 +342,7 @@ adb_get_serialno(struct sc_intr *intr) { ssize_t r = sc_pipe_read_all_intr(intr, pid, pout, buf, sizeof(buf)); sc_pipe_close(pout); - bool ok = - sc_process_check_success_intr(intr, pid, "adb get-serialno", true); + bool ok = process_check_success_intr(intr, pid, "adb get-serialno"); if (!ok) { return NULL; } diff --git a/app/src/util/process.c b/app/src/util/process.c index 28f51edd..38931d9c 100644 --- a/app/src/util/process.c +++ b/app/src/util/process.c @@ -9,25 +9,6 @@ sc_process_execute(const char *const argv[], sc_pid *pid) { return sc_process_execute_p(argv, pid, NULL, NULL, NULL); } -bool -sc_process_check_success(sc_pid pid, const char *name, bool close) { - if (pid == SC_PROCESS_NONE) { - LOGE("Could not execute \"%s\"", name); - return false; - } - sc_exit_code exit_code = sc_process_wait(pid, close); - if (exit_code) { - if (exit_code != SC_EXIT_CODE_NONE) { - LOGE("\"%s\" returned with value %" SC_PRIexitcode, name, - exit_code); - } else { - LOGE("\"%s\" exited unexpectedly", name); - } - return false; - } - return true; -} - ssize_t sc_pipe_read_all(sc_pipe pipe, char *data, size_t len) { size_t copied = 0; diff --git a/app/src/util/process.h b/app/src/util/process.h index 7964be5c..14bc060e 100644 --- a/app/src/util/process.h +++ b/app/src/util/process.h @@ -107,14 +107,6 @@ sc_process_wait(sc_pid pid, bool close); void sc_process_close(sc_pid pid); -/** - * Convenience function to wait for a successful process execution - * - * Automatically log process errors with the provided process name. - */ -bool -sc_process_check_success(sc_pid pid, const char *name, bool close); - /** * Read from the pipe * diff --git a/app/src/util/process_intr.c b/app/src/util/process_intr.c index dcb81100..940fe89f 100644 --- a/app/src/util/process_intr.c +++ b/app/src/util/process_intr.c @@ -1,26 +1,5 @@ #include "process_intr.h" -bool -sc_process_check_success_intr(struct sc_intr *intr, sc_pid pid, - const char *name, bool close) { - if (!sc_intr_set_process(intr, pid)) { - // Already interrupted - return false; - } - - // Always pass close=false, interrupting would be racy otherwise - bool ret = sc_process_check_success(pid, name, false); - - sc_intr_set_process(intr, SC_PROCESS_NONE); - - if (close) { - // Close separately - sc_process_close(pid); - } - - return ret; -} - ssize_t sc_pipe_read_intr(struct sc_intr *intr, sc_pid pid, sc_pipe pipe, char *data, size_t len) { diff --git a/app/src/util/process_intr.h b/app/src/util/process_intr.h index 9406d889..530a9046 100644 --- a/app/src/util/process_intr.h +++ b/app/src/util/process_intr.h @@ -6,10 +6,6 @@ #include "intr.h" #include "process.h" -bool -sc_process_check_success_intr(struct sc_intr *intr, sc_pid pid, - const char *name, bool close); - ssize_t sc_pipe_read_intr(struct sc_intr *intr, sc_pid pid, sc_pipe pipe, char *data, size_t len); From b90c89766b1e13afc82c2aba2ebd5d54c83c067f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 26 Nov 2021 08:10:37 +0100 Subject: [PATCH 0854/2244] Set CLOEXEC flag on sockets If SOCK_CLOEXEC exists, then set the flag on socket creation. Otherwise, use fcntl() (or SetHandleInformation() on Windows) to set the flag afterwards. This avoids the sockets to be inherited in child processes. Refs #2783 --- app/meson.build | 3 +++ app/src/util/net.c | 53 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/app/meson.build b/app/meson.build index 1561fdcd..1baf34fc 100644 --- a/app/meson.build +++ b/app/meson.build @@ -149,6 +149,9 @@ foreach f : check_functions endif endforeach +conf.set('HAVE_SOCK_CLOEXEC', host_machine.system() != 'windows' and + cc.has_header_symbol('sys/socket.h', 'SOCK_CLOEXEC')) + # the version, updated on release conf.set_quoted('SCRCPY_VERSION', meson.project_version()) diff --git a/app/src/util/net.c b/app/src/util/net.c index 8c51d12b..ec678d2e 100644 --- a/app/src/util/net.c +++ b/app/src/util/net.c @@ -1,6 +1,7 @@ #include "net.h" #include +#include #include #include @@ -10,17 +11,20 @@ # include typedef int socklen_t; typedef SOCKET sc_raw_socket; +# define SC_RAW_SOCKET_NONE INVALID_SOCKET #else # include # include # include # include # include +# include # define SOCKET_ERROR -1 typedef struct sockaddr_in SOCKADDR_IN; typedef struct sockaddr SOCKADDR; typedef struct in_addr IN_ADDR; typedef int sc_raw_socket; +# define SC_RAW_SOCKET_NONE -1 #endif bool @@ -79,6 +83,35 @@ unwrap(sc_socket socket) { #endif } +static inline bool +sc_raw_socket_close(sc_raw_socket raw_sock) { +#ifndef _WIN32 + return !close(raw_sock); +#else + return !closesocket(raw_sock); +#endif +} + +#ifndef HAVE_SOCK_CLOEXEC +// If SOCK_CLOEXEC does not exist, the flag must be set manually once the +// socket is created +static bool +set_cloexec_flag(sc_raw_socket raw_sock) { +#ifndef _WIN32 + if (fcntl(raw_sock, F_SETFD, FD_CLOEXEC) == -1) { + perror("fcntl F_SETFD"); + return false; + } +#else + if (!SetHandleInformation((HANDLE) raw_sock, HANDLE_FLAG_INHERIT, 0)) { + LOGE("SetHandleInformation socket failed"); + return false; + } +#endif + return true; +} +#endif + static void net_perror(const char *s) { #ifdef _WIN32 @@ -97,7 +130,16 @@ net_perror(const char *s) { sc_socket net_socket(void) { +#ifdef HAVE_SOCK_CLOEXEC + sc_raw_socket raw_sock = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0); +#else sc_raw_socket raw_sock = socket(AF_INET, SOCK_STREAM, 0); + if (raw_sock != SC_RAW_SOCKET_NONE && !set_cloexec_flag(raw_sock)) { + sc_raw_socket_close(raw_sock); + return SC_SOCKET_NONE; + } +#endif + sc_socket sock = wrap(raw_sock); if (sock == SC_SOCKET_NONE) { net_perror("socket"); @@ -156,8 +198,18 @@ net_accept(sc_socket server_socket) { SOCKADDR_IN csin; socklen_t sinsize = sizeof(csin); + +#ifdef HAVE_SOCK_CLOEXEC + sc_raw_socket raw_sock = + accept4(raw_server_socket, (SOCKADDR *) &csin, &sinsize, SOCK_CLOEXEC); +#else sc_raw_socket raw_sock = accept(raw_server_socket, (SOCKADDR *) &csin, &sinsize); + if (raw_sock != SC_RAW_SOCKET_NONE && !set_cloexec_flag(raw_sock)) { + sc_raw_socket_close(raw_sock); + return SC_SOCKET_NONE; + } +#endif return wrap(raw_sock); } @@ -211,7 +263,6 @@ net_interrupt(sc_socket socket) { #endif } -#include bool net_close(sc_socket socket) { sc_raw_socket raw_sock = unwrap(socket); From 3e54773c4843b94f16df04af2c0edafef060f82e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 25 Nov 2021 21:58:51 +0100 Subject: [PATCH 0855/2244] Remove intermediate static functions from adb.c They can easily be inlined into the public functions. --- app/src/adb.c | 100 +++++++++++++++++--------------------------------- 1 file changed, 33 insertions(+), 67 deletions(-) diff --git a/app/src/adb.c b/app/src/adb.c index 7383d347..92744164 100644 --- a/app/src/adb.c +++ b/app/src/adb.c @@ -196,46 +196,57 @@ adb_execute(const char *serial, const char *const adb_cmd[], size_t len) { return adb_execute_p(serial, adb_cmd, len, NULL); } -static sc_pid -adb_exec_forward(const char *serial, uint16_t local_port, - const char *device_socket_name) { +bool +adb_forward(struct sc_intr *intr, const char *serial, uint16_t local_port, + const char *device_socket_name) { char local[4 + 5 + 1]; // tcp:PORT char remote[108 + 14 + 1]; // localabstract:NAME sprintf(local, "tcp:%" PRIu16, local_port); snprintf(remote, sizeof(remote), "localabstract:%s", device_socket_name); const char *const adb_cmd[] = {"forward", local, remote}; - return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd)); + + sc_pid pid = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd)); + return process_check_success_intr(intr, pid, "adb forward"); } -static sc_pid -adb_exec_forward_remove(const char *serial, uint16_t local_port) { +bool +adb_forward_remove(struct sc_intr *intr, const char *serial, + uint16_t local_port) { char local[4 + 5 + 1]; // tcp:PORT sprintf(local, "tcp:%" PRIu16, local_port); const char *const adb_cmd[] = {"forward", "--remove", local}; - return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd)); + + sc_pid pid = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd)); + return process_check_success_intr(intr, pid, "adb forward --remove"); } -static sc_pid -adb_exec_reverse(const char *serial, const char *device_socket_name, - uint16_t local_port) { +bool +adb_reverse(struct sc_intr *intr, const char *serial, + const char *device_socket_name, uint16_t local_port) { char local[4 + 5 + 1]; // tcp:PORT char remote[108 + 14 + 1]; // localabstract:NAME sprintf(local, "tcp:%" PRIu16, local_port); snprintf(remote, sizeof(remote), "localabstract:%s", device_socket_name); const char *const adb_cmd[] = {"reverse", remote, local}; - return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd)); + + sc_pid pid = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd)); + return process_check_success_intr(intr, pid, "adb reverse"); } -static sc_pid -adb_exec_reverse_remove(const char *serial, const char *device_socket_name) { +bool +adb_reverse_remove(struct sc_intr *intr, const char *serial, + const char *device_socket_name) { char remote[108 + 14 + 1]; // localabstract:NAME snprintf(remote, sizeof(remote), "localabstract:%s", device_socket_name); const char *const adb_cmd[] = {"reverse", "--remove", remote}; - return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd)); + + sc_pid pid = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd)); + return process_check_success_intr(intr, pid, "adb reverse --remove"); } -static sc_pid -adb_exec_push(const char *serial, const char *local, const char *remote) { +bool +adb_push(struct sc_intr *intr, const char *serial, const char *local, + const char *remote) { #ifdef __WINDOWS__ // Windows will parse the string, so the paths must be quoted // (see sys/win/command.c) @@ -258,11 +269,11 @@ adb_exec_push(const char *serial, const char *local, const char *remote) { free((void *) local); #endif - return pid; + return process_check_success_intr(intr, pid, "adb push"); } -static sc_pid -adb_exec_install(const char *serial, const char *local) { +bool +adb_install(struct sc_intr *intr, const char *serial, const char *local) { #ifdef __WINDOWS__ // Windows will parse the string, so the local name must be quoted // (see sys/win/command.c) @@ -279,60 +290,15 @@ adb_exec_install(const char *serial, const char *local) { free((void *) local); #endif - return pid; -} - -static sc_pid -adb_exec_get_serialno(sc_pipe *pout) { - const char *const adb_cmd[] = {"get-serialno"}; - return adb_execute_p(NULL, adb_cmd, ARRAY_LEN(adb_cmd), pout); -} - -bool -adb_forward(struct sc_intr *intr, const char *serial, uint16_t local_port, - const char *device_socket_name) { - sc_pid pid = adb_exec_forward(serial, local_port, device_socket_name); - return process_check_success_intr(intr, pid, "adb forward"); -} - -bool -adb_forward_remove(struct sc_intr *intr, const char *serial, - uint16_t local_port) { - sc_pid pid = adb_exec_forward_remove(serial, local_port); - return process_check_success_intr(intr, pid, "adb forward --remove"); -} - -bool -adb_reverse(struct sc_intr *intr, const char *serial, - const char *device_socket_name, uint16_t local_port) { - sc_pid pid = adb_exec_reverse(serial, device_socket_name, local_port); - return process_check_success_intr(intr, pid, "adb reverse"); -} - -bool -adb_reverse_remove(struct sc_intr *intr, const char *serial, - const char *device_socket_name) { - sc_pid pid = adb_exec_reverse_remove(serial, device_socket_name); - return process_check_success_intr(intr, pid, "adb reverse --remove"); -} - -bool -adb_push(struct sc_intr *intr, const char *serial, const char *local, - const char *remote) { - sc_pid pid = adb_exec_push(serial, local, remote); - return process_check_success_intr(intr, pid, "adb push"); -} - -bool -adb_install(struct sc_intr *intr, const char *serial, const char *local) { - sc_pid pid = adb_exec_install(serial, local); return process_check_success_intr(intr, pid, "adb install"); } char * adb_get_serialno(struct sc_intr *intr) { + const char *const adb_cmd[] = {"get-serialno"}; + sc_pipe pout; - sc_pid pid = adb_exec_get_serialno(&pout); + sc_pid pid = adb_execute_p(NULL, adb_cmd, ARRAY_LEN(adb_cmd), &pout); if (pid == SC_PROCESS_NONE) { LOGE("Could not execute \"adb get-serialno\""); return NULL; From b9b8b6aab828e0f1ca899171ecf5c7bfcaf9df46 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 19 Nov 2021 21:37:40 +0100 Subject: [PATCH 0856/2244] Simplify Windows process inheritance configuration Merge if-blocks together. --- app/src/sys/win/process.c | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/app/src/sys/win/process.c b/app/src/sys/win/process.c index cf82f014..1d356293 100644 --- a/app/src/sys/win/process.c +++ b/app/src/sys/win/process.c @@ -79,14 +79,19 @@ sc_process_execute_p(const char *const argv[], HANDLE *handle, LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList = NULL; if (handle_count) { si.StartupInfo.dwFlags = STARTF_USESTDHANDLES; + + unsigned i = 0; if (pin) { si.StartupInfo.hStdInput = stdin_read_handle; + handles[i++] = si.StartupInfo.hStdInput; } if (pout) { si.StartupInfo.hStdOutput = stdout_write_handle; + handles[i++] = si.StartupInfo.hStdOutput; } if (perr) { si.StartupInfo.hStdError = stderr_write_handle; + handles[i++] = si.StartupInfo.hStdError; } SIZE_T size; @@ -110,17 +115,6 @@ sc_process_execute_p(const char *const argv[], HANDLE *handle, goto error_close_stderr; } - // Explicitly pass the HANDLEs that must be inherited - unsigned i = 0; - if (pin) { - handles[i++] = stdin_read_handle; - } - if (pout) { - handles[i++] = stdout_write_handle; - } - if (perr) { - handles[i++] = stderr_write_handle; - } ok = UpdateProcThreadAttribute(lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST, handles, handle_count * sizeof(HANDLE), From 01ab503c229514196ae633966277b76639e438b2 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 29 Nov 2021 00:35:40 +0100 Subject: [PATCH 0857/2244] Remove obsolete precision in README Scrcpy is available in Debian stable packages, and several Ubuntu versions. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8d80cb1f..f80f6261 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ Build from sources: [BUILD] ([simplified process][BUILD_simple]) ### Linux -On Debian (_testing_ and _sid_ for now) and Ubuntu (20.04): +On Debian and Ubuntu: ``` apt install scrcpy From f801d8b3128cf5aae3725c981f32abfd4b6c307e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 19 Nov 2021 22:42:49 +0100 Subject: [PATCH 0858/2244] Expose flags for process execution Let the caller decide if stdout and stderr must be inherited on process creation, i.e. if stdout and stderr of the child process should be printed in the scrcpy console. This allows to get output and errors for specific adb commands depending on the context. PR #2827 --- app/src/adb.c | 2 +- app/src/sys/unix/process.c | 15 ++++++++++++++- app/src/sys/win/process.c | 25 +++++++++++++++++-------- app/src/util/process.c | 4 ++-- app/src/util/process.h | 16 ++++++++++++++-- 5 files changed, 48 insertions(+), 14 deletions(-) diff --git a/app/src/adb.c b/app/src/adb.c index 92744164..729b5724 100644 --- a/app/src/adb.c +++ b/app/src/adb.c @@ -181,7 +181,7 @@ adb_execute_p(const char *serial, const char *const adb_cmd[], size_t len, sc_pid pid; enum sc_process_result r = - sc_process_execute_p(argv, &pid, NULL, pout, NULL); + sc_process_execute_p(argv, &pid, 0, NULL, pout, NULL); if (r != SC_PROCESS_SUCCESS) { show_adb_err_msg(r, argv); pid = SC_PROCESS_NONE; diff --git a/app/src/sys/unix/process.c b/app/src/sys/unix/process.c index 5f4a9890..54a1bb80 100644 --- a/app/src/sys/unix/process.c +++ b/app/src/sys/unix/process.c @@ -11,8 +11,11 @@ #include "util/log.h" enum sc_process_result -sc_process_execute_p(const char *const argv[], sc_pid *pid, +sc_process_execute_p(const char *const argv[], sc_pid *pid, unsigned flags, int *pin, int *pout, int *perr) { + bool inherit_stdout = !pout && !(flags & SC_PROCESS_NO_STDOUT); + bool inherit_stderr = !perr && !(flags & SC_PROCESS_NO_STDERR); + int in[2]; int out[2]; int err[2]; @@ -90,20 +93,30 @@ sc_process_execute_p(const char *const argv[], sc_pid *pid, } close(in[1]); } + // Do not close stdin in the child process, this makes adb fail on Linux + if (pout) { if (out[1] != STDOUT_FILENO) { dup2(out[1], STDOUT_FILENO); close(out[1]); } close(out[0]); + } else if (!inherit_stdout) { + // Close stdout in the child process + close(STDOUT_FILENO); } + if (perr) { if (err[1] != STDERR_FILENO) { dup2(err[1], STDERR_FILENO); close(err[1]); } close(err[0]); + } else if (!inherit_stderr) { + // Close stderr in the child process + close(STDERR_FILENO); } + close(internal[0]); enum sc_process_result err; if (fcntl(internal[1], F_SETFD, FD_CLOEXEC) == 0) { diff --git a/app/src/sys/win/process.c b/app/src/sys/win/process.c index 1d356293..bed98479 100644 --- a/app/src/sys/win/process.c +++ b/app/src/sys/win/process.c @@ -24,12 +24,17 @@ build_cmd(char *cmd, size_t len, const char *const argv[]) { } enum sc_process_result -sc_process_execute_p(const char *const argv[], HANDLE *handle, +sc_process_execute_p(const char *const argv[], HANDLE *handle, unsigned flags, HANDLE *pin, HANDLE *pout, HANDLE *perr) { - enum sc_process_result ret = SC_PROCESS_ERROR_GENERIC; + bool inherit_stdout = !pout && !(flags & SC_PROCESS_NO_STDOUT); + bool inherit_stderr = !perr && !(flags & SC_PROCESS_NO_STDERR); // Add 1 per non-NULL pointer - unsigned handle_count = !!pin + !!pout + !!perr; + unsigned handle_count = !!pin + + (pout || inherit_stdout) + + (perr || inherit_stderr); + + enum sc_process_result ret = SC_PROCESS_ERROR_GENERIC; SECURITY_ATTRIBUTES sa; sa.nLength = sizeof(SECURITY_ATTRIBUTES); @@ -85,12 +90,14 @@ sc_process_execute_p(const char *const argv[], HANDLE *handle, si.StartupInfo.hStdInput = stdin_read_handle; handles[i++] = si.StartupInfo.hStdInput; } - if (pout) { - si.StartupInfo.hStdOutput = stdout_write_handle; + if (pout || inherit_stdout) { + si.StartupInfo.hStdOutput = pout ? stdout_write_handle + : GetStdHandle(STD_OUTPUT_HANDLE); handles[i++] = si.StartupInfo.hStdOutput; } - if (perr) { - si.StartupInfo.hStdError = stderr_write_handle; + if (perr || inherit_stderr) { + si.StartupInfo.hStdError = perr ? stderr_write_handle + : GetStdHandle(STD_ERROR_HANDLE); handles[i++] = si.StartupInfo.hStdError; } @@ -140,7 +147,9 @@ sc_process_execute_p(const char *const argv[], HANDLE *handle, } BOOL bInheritHandles = handle_count > 0; - DWORD dwCreationFlags = handle_count > 0 ? EXTENDED_STARTUPINFO_PRESENT : 0; + // DETACHED_PROCESS to disable stdin, stdout and stderr + DWORD dwCreationFlags = handle_count > 0 ? EXTENDED_STARTUPINFO_PRESENT + : DETACHED_PROCESS; BOOL ok = CreateProcessW(NULL, wide, NULL, NULL, bInheritHandles, dwCreationFlags, NULL, NULL, &si.StartupInfo, &pi); free(wide); diff --git a/app/src/util/process.c b/app/src/util/process.c index 38931d9c..ad1af0a9 100644 --- a/app/src/util/process.c +++ b/app/src/util/process.c @@ -5,8 +5,8 @@ #include "log.h" enum sc_process_result -sc_process_execute(const char *const argv[], sc_pid *pid) { - return sc_process_execute_p(argv, pid, NULL, NULL, NULL); +sc_process_execute(const char *const argv[], sc_pid *pid, unsigned flags) { + return sc_process_execute_p(argv, pid, flags, NULL, NULL, NULL); } ssize_t diff --git a/app/src/util/process.h b/app/src/util/process.h index 14bc060e..17c09bc5 100644 --- a/app/src/util/process.h +++ b/app/src/util/process.h @@ -67,20 +67,32 @@ enum sc_process_result { SC_PROCESS_ERROR_MISSING_BINARY, }; +#define SC_PROCESS_NO_STDOUT (1 << 0) +#define SC_PROCESS_NO_STDERR (1 << 1) + /** * Execute the command and write the process id to `pid` + * + * The `flags` argument is a bitwise OR of the following values: + * - SC_PROCESS_NO_STDOUT + * - SC_PROCESS_NO_STDERR + * + * It indicates if stdout and stderr must be inherited from the scrcpy process + * (i.e. if the process must output to the scrcpy console). */ enum sc_process_result -sc_process_execute(const char *const argv[], sc_pid *pid); +sc_process_execute(const char *const argv[], sc_pid *pid, unsigned flags); /** * Execute the command and write the process id to `pid` * * If not NULL, provide a pipe for stdin (`pin`), stdout (`pout`) and stderr * (`perr`). + * + * The `flags` argument has the same semantics as in `sc_process_execute()`. */ enum sc_process_result -sc_process_execute_p(const char *const argv[], sc_pid *pid, +sc_process_execute_p(const char *const argv[], sc_pid *pid, unsigned flags, sc_pipe *pin, sc_pipe *pout, sc_pipe *perr); /** From e3d4aa8c5d42279e0095404d76f2b5bc2b7c52d7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 24 Nov 2021 22:27:28 +0100 Subject: [PATCH 0859/2244] Use flags for adb commands Explicitly indicate, for each adb call, if stdout and stderr must be inherited. PR #2827 --- app/src/adb.c | 49 ++++++++++++++++++++++++++---------------- app/src/adb.h | 22 ++++++++++++------- app/src/adb_tunnel.c | 14 +++++++----- app/src/file_handler.c | 4 ++-- app/src/server.c | 7 +++--- 5 files changed, 60 insertions(+), 36 deletions(-) diff --git a/app/src/adb.c b/app/src/adb.c index 729b5724..13018f4d 100644 --- a/app/src/adb.c +++ b/app/src/adb.c @@ -173,16 +173,26 @@ adb_create_argv(const char *serial, const char *const adb_cmd[], size_t len) { static sc_pid adb_execute_p(const char *serial, const char *const adb_cmd[], size_t len, - sc_pipe *pout) { + unsigned flags, sc_pipe *pout) { const char **argv = adb_create_argv(serial, adb_cmd, len); if (!argv) { return SC_PROCESS_NONE; } + unsigned process_flags = 0; + if (flags & SC_ADB_NO_STDOUT) { + process_flags |= SC_PROCESS_NO_STDOUT; + } + if (flags & SC_ADB_NO_STDERR) { + process_flags |= SC_PROCESS_NO_STDERR; + } + sc_pid pid; enum sc_process_result r = - sc_process_execute_p(argv, &pid, 0, NULL, pout, NULL); + sc_process_execute_p(argv, &pid, process_flags, NULL, pout, NULL); if (r != SC_PROCESS_SUCCESS) { + // If the execution itself failed (not the command exit code), log the + // error in all cases show_adb_err_msg(r, argv); pid = SC_PROCESS_NONE; } @@ -192,61 +202,63 @@ adb_execute_p(const char *serial, const char *const adb_cmd[], size_t len, } sc_pid -adb_execute(const char *serial, const char *const adb_cmd[], size_t len) { - return adb_execute_p(serial, adb_cmd, len, NULL); +adb_execute(const char *serial, const char *const adb_cmd[], size_t len, + unsigned flags) { + return adb_execute_p(serial, adb_cmd, len, flags, NULL); } bool adb_forward(struct sc_intr *intr, const char *serial, uint16_t local_port, - const char *device_socket_name) { + const char *device_socket_name, unsigned flags) { char local[4 + 5 + 1]; // tcp:PORT char remote[108 + 14 + 1]; // localabstract:NAME sprintf(local, "tcp:%" PRIu16, local_port); snprintf(remote, sizeof(remote), "localabstract:%s", device_socket_name); const char *const adb_cmd[] = {"forward", local, remote}; - sc_pid pid = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd)); + sc_pid pid = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd), flags); return process_check_success_intr(intr, pid, "adb forward"); } bool adb_forward_remove(struct sc_intr *intr, const char *serial, - uint16_t local_port) { + uint16_t local_port, unsigned flags) { char local[4 + 5 + 1]; // tcp:PORT sprintf(local, "tcp:%" PRIu16, local_port); const char *const adb_cmd[] = {"forward", "--remove", local}; - sc_pid pid = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd)); + sc_pid pid = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd), flags); return process_check_success_intr(intr, pid, "adb forward --remove"); } bool adb_reverse(struct sc_intr *intr, const char *serial, - const char *device_socket_name, uint16_t local_port) { + const char *device_socket_name, uint16_t local_port, + unsigned flags) { char local[4 + 5 + 1]; // tcp:PORT char remote[108 + 14 + 1]; // localabstract:NAME sprintf(local, "tcp:%" PRIu16, local_port); snprintf(remote, sizeof(remote), "localabstract:%s", device_socket_name); const char *const adb_cmd[] = {"reverse", remote, local}; - sc_pid pid = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd)); + sc_pid pid = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd), flags); return process_check_success_intr(intr, pid, "adb reverse"); } bool adb_reverse_remove(struct sc_intr *intr, const char *serial, - const char *device_socket_name) { + const char *device_socket_name, unsigned flags) { char remote[108 + 14 + 1]; // localabstract:NAME snprintf(remote, sizeof(remote), "localabstract:%s", device_socket_name); const char *const adb_cmd[] = {"reverse", "--remove", remote}; - sc_pid pid = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd)); + sc_pid pid = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd), flags); return process_check_success_intr(intr, pid, "adb reverse --remove"); } bool adb_push(struct sc_intr *intr, const char *serial, const char *local, - const char *remote) { + const char *remote, unsigned flags) { #ifdef __WINDOWS__ // Windows will parse the string, so the paths must be quoted // (see sys/win/command.c) @@ -262,7 +274,7 @@ adb_push(struct sc_intr *intr, const char *serial, const char *local, #endif const char *const adb_cmd[] = {"push", local, remote}; - sc_pid pid = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd)); + sc_pid pid = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd), flags); #ifdef __WINDOWS__ free((void *) remote); @@ -273,7 +285,8 @@ adb_push(struct sc_intr *intr, const char *serial, const char *local, } bool -adb_install(struct sc_intr *intr, const char *serial, const char *local) { +adb_install(struct sc_intr *intr, const char *serial, const char *local, + unsigned flags) { #ifdef __WINDOWS__ // Windows will parse the string, so the local name must be quoted // (see sys/win/command.c) @@ -284,7 +297,7 @@ adb_install(struct sc_intr *intr, const char *serial, const char *local) { #endif const char *const adb_cmd[] = {"install", "-r", local}; - sc_pid pid = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd)); + sc_pid pid = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd), flags); #ifdef __WINDOWS__ free((void *) local); @@ -294,11 +307,11 @@ adb_install(struct sc_intr *intr, const char *serial, const char *local) { } char * -adb_get_serialno(struct sc_intr *intr) { +adb_get_serialno(struct sc_intr *intr, unsigned flags) { const char *const adb_cmd[] = {"get-serialno"}; sc_pipe pout; - sc_pid pid = adb_execute_p(NULL, adb_cmd, ARRAY_LEN(adb_cmd), &pout); + sc_pid pid = adb_execute_p(NULL, adb_cmd, ARRAY_LEN(adb_cmd), flags, &pout); if (pid == SC_PROCESS_NONE) { LOGE("Could not execute \"adb get-serialno\""); return NULL; diff --git a/app/src/adb.h b/app/src/adb.h index f58bc165..5033965b 100644 --- a/app/src/adb.h +++ b/app/src/adb.h @@ -8,31 +8,37 @@ #include "util/intr.h" +#define SC_ADB_NO_STDOUT (1 << 0) +#define SC_ADB_NO_STDERR (1 << 1) + sc_pid -adb_execute(const char *serial, const char *const adb_cmd[], size_t len); +adb_execute(const char *serial, const char *const adb_cmd[], size_t len, + unsigned flags); bool adb_forward(struct sc_intr *intr, const char *serial, uint16_t local_port, - const char *device_socket_name); + const char *device_socket_name, unsigned flags); bool adb_forward_remove(struct sc_intr *intr, const char *serial, - uint16_t local_port); + uint16_t local_port, unsigned flags); bool adb_reverse(struct sc_intr *intr, const char *serial, - const char *device_socket_name, uint16_t local_port); + const char *device_socket_name, uint16_t local_port, + unsigned flags); bool adb_reverse_remove(struct sc_intr *intr, const char *serial, - const char *device_socket_name); + const char *device_socket_name, unsigned flags); bool adb_push(struct sc_intr *intr, const char *serial, const char *local, - const char *remote); + const char *remote, unsigned flags); bool -adb_install(struct sc_intr *intr, const char *serial, const char *local); +adb_install(struct sc_intr *intr, const char *serial, const char *local, + unsigned flags); /** * Execute `adb get-serialno` @@ -40,6 +46,6 @@ adb_install(struct sc_intr *intr, const char *serial, const char *local); * Return the result, to be freed by the caller, or NULL on error. */ char * -adb_get_serialno(struct sc_intr *intr); +adb_get_serialno(struct sc_intr *intr, unsigned flags); #endif diff --git a/app/src/adb_tunnel.c b/app/src/adb_tunnel.c index fa86a8a5..00552597 100644 --- a/app/src/adb_tunnel.c +++ b/app/src/adb_tunnel.c @@ -20,7 +20,8 @@ enable_tunnel_reverse_any_port(struct sc_adb_tunnel *tunnel, struct sc_port_range port_range) { uint16_t port = port_range.first; for (;;) { - if (!adb_reverse(intr, serial, SC_SOCKET_NAME, port)) { + if (!adb_reverse(intr, serial, SC_SOCKET_NAME, port, + SC_ADB_NO_STDOUT)) { // the command itself failed, it will fail on any port return false; } @@ -51,7 +52,8 @@ enable_tunnel_reverse_any_port(struct sc_adb_tunnel *tunnel, } // failure, disable tunnel and try another port - if (!adb_reverse_remove(intr, serial, SC_SOCKET_NAME)) { + if (!adb_reverse_remove(intr, serial, SC_SOCKET_NAME, + SC_ADB_NO_STDOUT)) { LOGW("Could not remove reverse tunnel on port %" PRIu16, port); } @@ -81,7 +83,7 @@ enable_tunnel_forward_any_port(struct sc_adb_tunnel *tunnel, uint16_t port = port_range.first; for (;;) { - if (adb_forward(intr, serial, port, SC_SOCKET_NAME)) { + if (adb_forward(intr, serial, port, SC_SOCKET_NAME, SC_ADB_NO_STDOUT)) { // success tunnel->local_port = port; tunnel->enabled = true; @@ -146,9 +148,11 @@ sc_adb_tunnel_close(struct sc_adb_tunnel *tunnel, struct sc_intr *intr, bool ret; if (tunnel->forward) { - ret = adb_forward_remove(intr, serial, tunnel->local_port); + ret = adb_forward_remove(intr, serial, tunnel->local_port, + SC_ADB_NO_STDOUT); } else { - ret = adb_reverse_remove(intr, serial, SC_SOCKET_NAME); + ret = adb_reverse_remove(intr, serial, SC_SOCKET_NAME, + SC_ADB_NO_STDOUT); assert(tunnel->server_socket != SC_SOCKET_NONE); if (!net_close(tunnel->server_socket)) { diff --git a/app/src/file_handler.c b/app/src/file_handler.c index 12498ccf..addbb9a5 100644 --- a/app/src/file_handler.c +++ b/app/src/file_handler.c @@ -128,7 +128,7 @@ run_file_handler(void *data) { if (req.action == ACTION_INSTALL_APK) { LOGI("Installing %s...", req.file); - bool ok = adb_install(intr, serial, req.file); + bool ok = adb_install(intr, serial, req.file, 0); if (ok) { LOGI("%s successfully installed", req.file); } else { @@ -136,7 +136,7 @@ run_file_handler(void *data) { } } else { LOGI("Pushing %s...", req.file); - bool ok = adb_push(intr, serial, req.file, push_target); + bool ok = adb_push(intr, serial, req.file, push_target, 0); if (ok) { LOGI("%s successfully pushed to %s", req.file, push_target); } else { diff --git a/app/src/server.c b/app/src/server.c index f0624a13..a62a093a 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -112,7 +112,7 @@ push_server(struct sc_intr *intr, const char *serial) { free(server_path); return false; } - bool ok = adb_push(intr, serial, server_path, SC_DEVICE_SERVER_PATH); + bool ok = adb_push(intr, serial, server_path, SC_DEVICE_SERVER_PATH, 0); free(server_path); return ok; } @@ -234,7 +234,8 @@ execute_server(struct sc_server *server, // Port: 5005 // Then click on "Debug" #endif - pid = adb_execute(params->serial, cmd, count); + // Inherit both stdout and stderr (all server logs are printed to stdout) + pid = adb_execute(params->serial, cmd, count, 0); end: for (unsigned i = dyn_idx; i < count; ++i) { @@ -482,7 +483,7 @@ sc_server_fill_serial(struct sc_server *server) { // device/emulator" error) if (!server->params.serial) { // The serial is owned by sc_server_params, and will be freed on destroy - server->params.serial = adb_get_serialno(&server->intr); + server->params.serial = adb_get_serialno(&server->intr, 0); if (!server->params.serial) { LOGE("Could not get device serial"); return false; From e6e6f865a01879365f284fd3c8bfe7758ebf1bb5 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 25 Nov 2021 22:05:38 +0100 Subject: [PATCH 0860/2244] Add adb flag to disable execution error logs In addition to disable stdout and stderr of the child process, add a flag to disable the error log printed by scrcpy if the command failed. This will we useful for commands which are expected to fail in some cases (like "adb disconnect" if the device is not connected). PR #2827 --- app/src/adb.c | 40 ++++++++++++++++++++++++---------------- app/src/adb.h | 1 + 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/app/src/adb.c b/app/src/adb.c index 13018f4d..ce6aedbf 100644 --- a/app/src/adb.c +++ b/app/src/adb.c @@ -112,18 +112,25 @@ show_adb_err_msg(enum sc_process_result err, const char *const argv[]) { } static bool -process_check_success_internal(sc_pid pid, const char *name, bool close) { +process_check_success_internal(sc_pid pid, const char *name, bool close, + unsigned flags) { + bool log_errors = !(flags & SC_ADB_NO_LOGERR); + if (pid == SC_PROCESS_NONE) { - LOGE("Could not execute \"%s\"", name); + if (log_errors) { + LOGE("Could not execute \"%s\"", name); + } return false; } sc_exit_code exit_code = sc_process_wait(pid, close); if (exit_code) { - if (exit_code != SC_EXIT_CODE_NONE) { - LOGE("\"%s\" returned with value %" SC_PRIexitcode, name, - exit_code); - } else { - LOGE("\"%s\" exited unexpectedly", name); + if (log_errors) { + if (exit_code != SC_EXIT_CODE_NONE) { + LOGE("\"%s\" returned with value %" SC_PRIexitcode, name, + exit_code); + } else { + LOGE("\"%s\" exited unexpectedly", name); + } } return false; } @@ -131,14 +138,15 @@ process_check_success_internal(sc_pid pid, const char *name, bool close) { } static bool -process_check_success_intr(struct sc_intr *intr, sc_pid pid, const char *name) { +process_check_success_intr(struct sc_intr *intr, sc_pid pid, const char *name, + unsigned flags) { if (!sc_intr_set_process(intr, pid)) { // Already interrupted return false; } // Always pass close=false, interrupting would be racy otherwise - bool ret = process_check_success_internal(pid, name, false); + bool ret = process_check_success_internal(pid, name, false, flags); sc_intr_set_process(intr, SC_PROCESS_NONE); @@ -217,7 +225,7 @@ adb_forward(struct sc_intr *intr, const char *serial, uint16_t local_port, const char *const adb_cmd[] = {"forward", local, remote}; sc_pid pid = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd), flags); - return process_check_success_intr(intr, pid, "adb forward"); + return process_check_success_intr(intr, pid, "adb forward", flags); } bool @@ -228,7 +236,7 @@ adb_forward_remove(struct sc_intr *intr, const char *serial, const char *const adb_cmd[] = {"forward", "--remove", local}; sc_pid pid = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd), flags); - return process_check_success_intr(intr, pid, "adb forward --remove"); + return process_check_success_intr(intr, pid, "adb forward --remove", flags); } bool @@ -242,7 +250,7 @@ adb_reverse(struct sc_intr *intr, const char *serial, const char *const adb_cmd[] = {"reverse", remote, local}; sc_pid pid = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd), flags); - return process_check_success_intr(intr, pid, "adb reverse"); + return process_check_success_intr(intr, pid, "adb reverse", flags); } bool @@ -253,7 +261,7 @@ adb_reverse_remove(struct sc_intr *intr, const char *serial, const char *const adb_cmd[] = {"reverse", "--remove", remote}; sc_pid pid = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd), flags); - return process_check_success_intr(intr, pid, "adb reverse --remove"); + return process_check_success_intr(intr, pid, "adb reverse --remove", flags); } bool @@ -281,7 +289,7 @@ adb_push(struct sc_intr *intr, const char *serial, const char *local, free((void *) local); #endif - return process_check_success_intr(intr, pid, "adb push"); + return process_check_success_intr(intr, pid, "adb push", flags); } bool @@ -303,7 +311,7 @@ adb_install(struct sc_intr *intr, const char *serial, const char *local, free((void *) local); #endif - return process_check_success_intr(intr, pid, "adb install"); + return process_check_success_intr(intr, pid, "adb install", flags); } char * @@ -321,7 +329,7 @@ adb_get_serialno(struct sc_intr *intr, unsigned flags) { ssize_t r = sc_pipe_read_all_intr(intr, pid, pout, buf, sizeof(buf)); sc_pipe_close(pout); - bool ok = process_check_success_intr(intr, pid, "adb get-serialno"); + bool ok = process_check_success_intr(intr, pid, "adb get-serialno", flags); if (!ok) { return NULL; } diff --git a/app/src/adb.h b/app/src/adb.h index 5033965b..7391500d 100644 --- a/app/src/adb.h +++ b/app/src/adb.h @@ -10,6 +10,7 @@ #define SC_ADB_NO_STDOUT (1 << 0) #define SC_ADB_NO_STDERR (1 << 1) +#define SC_ADB_NO_LOGERR (1 << 2) sc_pid adb_execute(const char *serial, const char *const adb_cmd[], size_t len, From bfce22414fd3173b9671140d77d2e8a927d5ff94 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 24 Nov 2021 22:39:46 +0100 Subject: [PATCH 0861/2244] Add adb connect and disconnect PR #2827 --- app/src/adb.c | 18 ++++++++++++++++++ app/src/adb.h | 17 +++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/app/src/adb.c b/app/src/adb.c index ce6aedbf..cb4f14da 100644 --- a/app/src/adb.c +++ b/app/src/adb.c @@ -314,6 +314,24 @@ adb_install(struct sc_intr *intr, const char *serial, const char *local, return process_check_success_intr(intr, pid, "adb install", flags); } +bool +adb_connect(struct sc_intr *intr, const char *ip_port, unsigned flags) { + const char *const adb_cmd[] = {"connect", ip_port}; + + sc_pid pid = adb_execute(NULL, adb_cmd, ARRAY_LEN(adb_cmd), flags); + return process_check_success_intr(intr, pid, "adb connect", flags); +} + +bool +adb_disconnect(struct sc_intr *intr, const char *ip_port, unsigned flags) { + const char *const adb_cmd[] = {"disconnect", ip_port}; + size_t len = ip_port ? ARRAY_LEN(adb_cmd) + : ARRAY_LEN(adb_cmd) - 1; + + sc_pid pid = adb_execute(NULL, adb_cmd, len, flags); + return process_check_success_intr(intr, pid, "adb disconnect", flags); +} + char * adb_get_serialno(struct sc_intr *intr, unsigned flags) { const char *const adb_cmd[] = {"get-serialno"}; diff --git a/app/src/adb.h b/app/src/adb.h index 7391500d..d5d9bb37 100644 --- a/app/src/adb.h +++ b/app/src/adb.h @@ -41,6 +41,23 @@ bool adb_install(struct sc_intr *intr, const char *serial, const char *local, unsigned flags); +/** + * Execute `adb connect ` + * + * `ip_port` may not be NULL. + */ +bool +adb_connect(struct sc_intr *intr, const char *ip_port, unsigned flags); + +/** + * Execute `adb disconnect []` + * + * If `ip_port` is NULL, execute `adb disconnect`. + * Otherwise, execute `adb disconnect `. + */ +bool +adb_disconnect(struct sc_intr *intr, const char *ip_port, unsigned flags); + /** * Execute `adb get-serialno` * From 3bf6fd2894c283a976320b8f0862a63be0d78635 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 25 Nov 2021 21:33:50 +0100 Subject: [PATCH 0862/2244] Workaround "adb connect" error detection "adb connect" always returns successfully (with exit code 0), even in case of failure. As a workaround, capture its output and check if it starts with "connected". PR #2827 --- app/src/adb.c | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/app/src/adb.c b/app/src/adb.c index cb4f14da..819066c0 100644 --- a/app/src/adb.c +++ b/app/src/adb.c @@ -318,8 +318,37 @@ bool adb_connect(struct sc_intr *intr, const char *ip_port, unsigned flags) { const char *const adb_cmd[] = {"connect", ip_port}; - sc_pid pid = adb_execute(NULL, adb_cmd, ARRAY_LEN(adb_cmd), flags); - return process_check_success_intr(intr, pid, "adb connect", flags); + sc_pipe pout; + sc_pid pid = adb_execute_p(NULL, adb_cmd, ARRAY_LEN(adb_cmd), flags, &pout); + if (pid == SC_PROCESS_NONE) { + LOGE("Could not execute \"adb connect\""); + return false; + } + + // "adb connect" always returns successfully (with exit code 0), even in + // case of failure. As a workaround, check if its output starts with + // "connected". + char buf[128]; + ssize_t r = sc_pipe_read_all_intr(intr, pid, pout, buf, sizeof(buf)); + sc_pipe_close(pout); + + bool ok = process_check_success_intr(intr, pid, "adb connect", flags); + if (!ok) { + return false; + } + + if (r == -1) { + return false; + } + + ok = !strncmp("connected", buf, sizeof("connected") - 1); + if (!ok && !(flags & SC_ADB_NO_STDERR)) { + // "adb connect" also prints errors to stdout. Since we capture it, + // re-print the error to stderr. + sc_str_truncate(buf, r, "\r\n"); + fprintf(stderr, "%s\n", buf); + } + return ok; } bool From b52f87a892ec97beb2afb1e0a711632038ccfe4a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 18 Nov 2021 09:32:21 +0100 Subject: [PATCH 0863/2244] Add util function to locate a column in a string This will help to parse the result of "adb shell ip route" to find the device IP address. PR #2827 --- app/src/util/str.c | 26 ++++++++++++++++++++++++++ app/src/util/str.h | 20 ++++++++++++++++++++ app/tests/test_str.c | 21 +++++++++++++++++++++ 3 files changed, 67 insertions(+) diff --git a/app/src/util/str.c b/app/src/util/str.c index ab1c8783..1564ffe2 100644 --- a/app/src/util/str.c +++ b/app/src/util/str.c @@ -304,3 +304,29 @@ sc_str_truncate(char *data, size_t len, const char *endchars) { data[idx] = '\0'; return idx; } + +ssize_t +sc_str_index_of_column(const char *s, unsigned col, const char *seps) { + size_t colidx = 0; + + size_t idx = 0; + while (s[idx] != '\0' && colidx != col) { + size_t r = strcspn(&s[idx], seps); + idx += r; + + if (s[idx] == '\0') { + // Not found + return -1; + } + + size_t consecutive_seps = strspn(&s[idx], seps); + assert(consecutive_seps); // At least one + idx += consecutive_seps; + + if (s[idx] != '\0') { + ++colidx; + } + } + + return col == colidx ? (ssize_t) idx : -1; +} diff --git a/app/src/util/str.h b/app/src/util/str.h index 521dfff5..2c7d7829 100644 --- a/app/src/util/str.h +++ b/app/src/util/str.h @@ -114,4 +114,24 @@ sc_str_wrap_lines(const char *input, unsigned columns, unsigned indent); size_t sc_str_truncate(char *data, size_t len, const char *endchars); +/** + * Find the start of a column in a string + * + * A string may represent several columns, separated by some "spaces" + * (separators). This function aims to find the start of the column number + * `col`. + * + * For example, to find the 4th column (column number 3): + * + * // here + * // v + * const char *s = "abc def ghi jk"; + * ssize_t index = sc_str_index_of_column(s, 3, " "); + * assert(index == 16); // points to "jk" + * + * Return -1 if no such column exists. + */ +ssize_t +sc_str_index_of_column(const char *s, unsigned col, const char *seps); + #endif diff --git a/app/tests/test_str.c b/app/tests/test_str.c index c66bb2f4..bd976b3c 100644 --- a/app/tests/test_str.c +++ b/app/tests/test_str.c @@ -364,6 +364,26 @@ static void test_truncate(void) { assert(!strcmp("hello", s4)); } +static void test_index_of_column(void) { + assert(sc_str_index_of_column("a bc d", 0, " ") == 0); + assert(sc_str_index_of_column("a bc d", 1, " ") == 2); + assert(sc_str_index_of_column("a bc d", 2, " ") == 6); + assert(sc_str_index_of_column("a bc d", 3, " ") == -1); + + assert(sc_str_index_of_column("a ", 0, " ") == 0); + assert(sc_str_index_of_column("a ", 1, " ") == -1); + + assert(sc_str_index_of_column("", 0, " ") == 0); + assert(sc_str_index_of_column("", 1, " ") == -1); + + assert(sc_str_index_of_column("a \t \t bc \t d\t", 0, " \t") == 0); + assert(sc_str_index_of_column("a \t \t bc \t d\t", 1, " \t") == 8); + assert(sc_str_index_of_column("a \t \t bc \t d\t", 2, " \t") == 15); + assert(sc_str_index_of_column("a \t \t bc \t d\t", 3, " \t") == -1); + + assert(sc_str_index_of_column(" a bc d", 1, " ") == 2); +} + int main(int argc, char *argv[]) { (void) argc; (void) argv; @@ -384,5 +404,6 @@ int main(int argc, char *argv[]) { test_strlist_contains(); test_wrap_lines(); test_truncate(); + test_index_of_column(); return 0; } From b7e631791cc0c9af407c9f625047754f6adf369c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 28 Nov 2021 22:02:40 +0100 Subject: [PATCH 0864/2244] Add util function to remove trailing '\r' Depending on the platform and adb versions, the lines output by adb could end with "\r\r\n". This util function helps to remove all trailing '\r'. PR #2827 --- app/src/util/str.c | 11 +++++++++++ app/src/util/str.h | 11 +++++++++++ app/tests/test_str.c | 15 +++++++++++++++ 3 files changed, 37 insertions(+) diff --git a/app/src/util/str.c b/app/src/util/str.c index 1564ffe2..70e3f1de 100644 --- a/app/src/util/str.c +++ b/app/src/util/str.c @@ -330,3 +330,14 @@ sc_str_index_of_column(const char *s, unsigned col, const char *seps) { return col == colidx ? (ssize_t) idx : -1; } + +size_t +sc_str_remove_trailing_cr(char *s, size_t len) { + while (len) { + if (s[len - 1] != '\r') { + break; + } + s[--len] = '\0'; + } + return len; +} diff --git a/app/src/util/str.h b/app/src/util/str.h index 2c7d7829..b81764ef 100644 --- a/app/src/util/str.h +++ b/app/src/util/str.h @@ -134,4 +134,15 @@ sc_str_truncate(char *data, size_t len, const char *endchars); ssize_t sc_str_index_of_column(const char *s, unsigned col, const char *seps); +/** + * Remove all `\r` at the end of the line + * + * The line length is provided by `len` (this avoids a call to `strlen()` when + * the caller already knows the length). + * + * Return the new length. + */ +size_t +sc_str_remove_trailing_cr(char *s, size_t len); + #endif diff --git a/app/tests/test_str.c b/app/tests/test_str.c index bd976b3c..cc3039e7 100644 --- a/app/tests/test_str.c +++ b/app/tests/test_str.c @@ -384,6 +384,20 @@ static void test_index_of_column(void) { assert(sc_str_index_of_column(" a bc d", 1, " ") == 2); } +static void test_remove_trailing_cr() { + char s[] = "abc\r"; + sc_str_remove_trailing_cr(s, sizeof(s) - 1); + assert(!strcmp(s, "abc")); + + char s2[] = "def\r\r\r\r"; + sc_str_remove_trailing_cr(s2, sizeof(s2) - 1); + assert(!strcmp(s2, "def")); + + char s3[] = "adb\rdef\r"; + sc_str_remove_trailing_cr(s3, sizeof(s3) - 1); + assert(!strcmp(s3, "adb\rdef")); +} + int main(int argc, char *argv[]) { (void) argc; (void) argv; @@ -405,5 +419,6 @@ int main(int argc, char *argv[]) { test_wrap_lines(); test_truncate(); test_index_of_column(); + test_remove_trailing_cr(); return 0; } From f609b406c9226144d9a7fdd7cdd12919ceaa2c73 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 25 Nov 2021 22:11:59 +0100 Subject: [PATCH 0865/2244] Add function to find the device IP address Parse the result of "adb shell ip route" to find the device IP address. PR #2827 --- app/meson.build | 9 +++- app/src/adb.c | 38 +++++++++++++++++ app/src/adb.h | 9 ++++ app/src/adb_parser.c | 65 +++++++++++++++++++++++++++++ app/src/adb_parser.h | 14 +++++++ app/tests/test_adb_parser.c | 83 +++++++++++++++++++++++++++++++++++++ 6 files changed, 217 insertions(+), 1 deletion(-) create mode 100644 app/src/adb_parser.c create mode 100644 app/src/adb_parser.h create mode 100644 app/tests/test_adb_parser.c diff --git a/app/meson.build b/app/meson.build index 1baf34fc..720d9c8c 100644 --- a/app/meson.build +++ b/app/meson.build @@ -1,6 +1,7 @@ src = [ 'src/main.c', 'src/adb.c', + 'src/adb_parser.c', 'src/adb_tunnel.c', 'src/cli.c', 'src/clock.c', @@ -204,8 +205,14 @@ install_data('../data/icon.png', # do not build tests in release (assertions would not be executed at all) if get_option('buildtype') == 'debug' tests = [ + ['test_adb_parser', [ + 'tests/test_adb_parser.c', + 'src/adb_parser.c', + 'src/util/str.c', + 'src/util/strbuf.c', + ]], ['test_buffer_util', [ - 'tests/test_buffer_util.c' + 'tests/test_buffer_util.c', ]], ['test_cbuf', [ 'tests/test_cbuf.c', diff --git a/app/src/adb.c b/app/src/adb.c index 819066c0..69bf551a 100644 --- a/app/src/adb.c +++ b/app/src/adb.c @@ -5,6 +5,7 @@ #include #include +#include "adb_parser.h" #include "util/file.h" #include "util/log.h" #include "util/process_intr.h" @@ -389,3 +390,40 @@ adb_get_serialno(struct sc_intr *intr, unsigned flags) { return strdup(buf); } + +char * +adb_get_device_ip(struct sc_intr *intr, const char *serial, unsigned flags) { + const char *const cmd[] = {"shell", "ip", "route"}; + + sc_pipe pout; + sc_pid pid = adb_execute_p(serial, cmd, ARRAY_LEN(cmd), flags, &pout); + if (pid == SC_PROCESS_NONE) { + LOGD("Could not execute \"ip route\""); + return NULL; + } + + // "adb shell ip route" output should contain only a few lines + char buf[1024]; + ssize_t r = sc_pipe_read_all_intr(intr, pid, pout, buf, sizeof(buf)); + sc_pipe_close(pout); + + bool ok = process_check_success_intr(intr, pid, "ip route", flags); + if (!ok) { + return NULL; + } + + if (r == -1) { + return false; + } + + assert((size_t) r <= sizeof(buf)); + if (r == sizeof(buf) && buf[sizeof(buf) - 1] != '\0') { + // The implementation assumes that the output of "ip route" fits in the + // buffer in a single pass + LOGW("Result of \"ip route\" does not fit in 1Kb. " + "Please report an issue.\n"); + return NULL; + } + + return sc_adb_parse_device_ip_from_output(buf, r); +} diff --git a/app/src/adb.h b/app/src/adb.h index d5d9bb37..ba0c2bde 100644 --- a/app/src/adb.h +++ b/app/src/adb.h @@ -66,4 +66,13 @@ adb_disconnect(struct sc_intr *intr, const char *ip_port, unsigned flags); char * adb_get_serialno(struct sc_intr *intr, unsigned flags); +/** + * Attempt to retrieve the device IP + * + * Return the IP as a string of the form "xxx.xxx.xxx.xxx", to be freed by the + * caller, or NULL on error. + */ +char * +adb_get_device_ip(struct sc_intr *intr, const char *serial, unsigned flags); + #endif diff --git a/app/src/adb_parser.c b/app/src/adb_parser.c new file mode 100644 index 00000000..5ac21ede --- /dev/null +++ b/app/src/adb_parser.c @@ -0,0 +1,65 @@ +#include "adb_parser.h" + +#include +#include + +#include "util/log.h" +#include "util/str.h" + +static char * +sc_adb_parse_device_ip_from_line(char *line, size_t len) { + // One line from "ip route" looks lile: + // "192.168.1.0/24 dev wlan0 proto kernel scope link src 192.168.1.x" + + // Get the location of the device name (index of "wlan0" in the example) + ssize_t idx_dev_name = sc_str_index_of_column(line, 2, " "); + if (idx_dev_name == -1) { + return NULL; + } + + // Get the location of the ip address (column 8, but column 6 if we start + // from column 2). Must be computed before truncating individual columns. + ssize_t idx_ip = sc_str_index_of_column(&line[idx_dev_name], 6, " "); + if (idx_ip == -1) { + return NULL; + } + // idx_ip is searched from &line[idx_dev_name] + idx_ip += idx_dev_name; + + char *dev_name = &line[idx_dev_name]; + sc_str_truncate(dev_name, len - idx_dev_name + 1, " \t"); + + char *ip = &line[idx_ip]; + sc_str_truncate(ip, len - idx_ip + 1, " \t"); + + // Only consider lines where the device name starts with "wlan" + if (strncmp(dev_name, "wlan", sizeof("wlan") - 1)) { + LOGD("Device ip lookup: ignoring %s (%s)", ip, dev_name); + return NULL; + } + + return strdup(ip); +} + +char * +sc_adb_parse_device_ip_from_output(char *buf, size_t buf_len) { + size_t idx_line = 0; + while (idx_line < buf_len && buf[idx_line] != '\0') { + char *line = &buf[idx_line]; + size_t len = sc_str_truncate(line, buf_len - idx_line, "\n"); + + // The same, but without any trailing '\r' + size_t line_len = sc_str_remove_trailing_cr(line, len); + + char *ip = sc_adb_parse_device_ip_from_line(line, line_len); + if (ip) { + // Found + return ip; + } + + // The next line starts after the '\n' (replaced by `\0`) + idx_line += len + 1; + } + + return NULL; +} diff --git a/app/src/adb_parser.h b/app/src/adb_parser.h new file mode 100644 index 00000000..79f26631 --- /dev/null +++ b/app/src/adb_parser.h @@ -0,0 +1,14 @@ +#ifndef SC_ADB_PARSER_H +#define SC_ADB_PARSER_H + +#include "common.h" + +#include "stddef.h" + +/** + * Parse the ip from the output of `adb shell ip route` + */ +char * +sc_adb_parse_device_ip_from_output(char *buf, size_t buf_len); + +#endif diff --git a/app/tests/test_adb_parser.c b/app/tests/test_adb_parser.c new file mode 100644 index 00000000..fbc65649 --- /dev/null +++ b/app/tests/test_adb_parser.c @@ -0,0 +1,83 @@ +#include "common.h" + +#include + +#include "adb_parser.h" + +static void test_get_ip_single_line() { + char ip_route[] = "192.168.1.0/24 dev wlan0 proto kernel scope link src " + "192.168.12.34\r\r\n"; + + char *ip = sc_adb_parse_device_ip_from_output(ip_route, sizeof(ip_route)); + assert(ip); + assert(!strcmp(ip, "192.168.12.34")); +} + +static void test_get_ip_single_line_without_eol() { + char ip_route[] = "192.168.1.0/24 dev wlan0 proto kernel scope link src " + "192.168.12.34"; + + char *ip = sc_adb_parse_device_ip_from_output(ip_route, sizeof(ip_route)); + assert(ip); + assert(!strcmp(ip, "192.168.12.34")); +} + +static void test_get_ip_single_line_with_trailing_space() { + char ip_route[] = "192.168.1.0/24 dev wlan0 proto kernel scope link src " + "192.168.12.34 \n"; + + char *ip = sc_adb_parse_device_ip_from_output(ip_route, sizeof(ip_route)); + assert(ip); + assert(!strcmp(ip, "192.168.12.34")); +} + +static void test_get_ip_multiline_first_ok() { + char ip_route[] = "192.168.1.0/24 dev wlan0 proto kernel scope link src " + "192.168.1.2\r\n" + "10.0.0.0/24 dev rmnet proto kernel scope link src " + "10.0.0.2\r\n"; + + char *ip = sc_adb_parse_device_ip_from_output(ip_route, sizeof(ip_route)); + assert(ip); + assert(!strcmp(ip, "192.168.1.2")); +} + +static void test_get_ip_multiline_second_ok() { + char ip_route[] = "10.0.0.0/24 dev rmnet proto kernel scope link src " + "10.0.0.3\r\n" + "192.168.1.0/24 dev wlan0 proto kernel scope link src " + "192.168.1.3\r\n"; + + char *ip = sc_adb_parse_device_ip_from_output(ip_route, sizeof(ip_route)); + assert(ip); + assert(!strcmp(ip, "192.168.1.3")); +} + +static void test_get_ip_no_wlan() { + char ip_route[] = "192.168.1.0/24 dev rmnet proto kernel scope link src " + "192.168.12.34\r\r\n"; + + char *ip = sc_adb_parse_device_ip_from_output(ip_route, sizeof(ip_route)); + assert(!ip); +} + +static void test_get_ip_truncated() { + char ip_route[] = "192.168.1.0/24 dev rmnet proto kernel scope link src " + "\n"; + + char *ip = sc_adb_parse_device_ip_from_output(ip_route, sizeof(ip_route)); + assert(!ip); +} + +int main(int argc, char *argv[]) { + (void) argc; + (void) argv; + + test_get_ip_single_line(); + test_get_ip_single_line_without_eol(); + test_get_ip_single_line_with_trailing_space(); + test_get_ip_multiline_first_ok(); + test_get_ip_multiline_second_ok(); + test_get_ip_no_wlan(); + test_get_ip_truncated(); +} From 8543d842ea4d73d6912c38f9702f7a18651b3d49 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 25 Nov 2021 22:13:39 +0100 Subject: [PATCH 0866/2244] Add function to switch device to TCP/IP mode Expose a function to execute "adb tcpip ". PR #2827 --- app/src/adb.c | 11 +++++++++++ app/src/adb.h | 7 +++++++ 2 files changed, 18 insertions(+) diff --git a/app/src/adb.c b/app/src/adb.c index 69bf551a..747e5ac9 100644 --- a/app/src/adb.c +++ b/app/src/adb.c @@ -315,6 +315,17 @@ adb_install(struct sc_intr *intr, const char *serial, const char *local, return process_check_success_intr(intr, pid, "adb install", flags); } +bool +adb_tcpip(struct sc_intr *intr, const char *serial, uint16_t port, + unsigned flags) { + char port_string[5 + 1]; + sprintf(port_string, "%" PRIu16, port); + const char *const adb_cmd[] = {"tcpip", port_string}; + + sc_pid pid = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd), flags); + return process_check_success_intr(intr, pid, "adb tcpip", flags); +} + bool adb_connect(struct sc_intr *intr, const char *ip_port, unsigned flags) { const char *const adb_cmd[] = {"connect", ip_port}; diff --git a/app/src/adb.h b/app/src/adb.h index ba0c2bde..f56c98c4 100644 --- a/app/src/adb.h +++ b/app/src/adb.h @@ -41,6 +41,13 @@ bool adb_install(struct sc_intr *intr, const char *serial, const char *local, unsigned flags); +/** + * Execute `adb tcpip ` + */ +bool +adb_tcpip(struct sc_intr *intr, const char *serial, uint16_t port, + unsigned flags); + /** * Execute `adb connect ` * From 800ba33ff42be775a279e02deef4b655ec3e991f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 25 Nov 2021 22:18:36 +0100 Subject: [PATCH 0867/2244] Add function to read an adb property This will allow to read the property "service.adb.tcp.port" to know if the TCP/IP mode is enabled on the device, and which listening port is used. PR #2827 --- app/src/adb.c | 31 +++++++++++++++++++++++++++++++ app/src/adb.h | 7 +++++++ 2 files changed, 38 insertions(+) diff --git a/app/src/adb.c b/app/src/adb.c index 747e5ac9..598b331f 100644 --- a/app/src/adb.c +++ b/app/src/adb.c @@ -373,6 +373,37 @@ adb_disconnect(struct sc_intr *intr, const char *ip_port, unsigned flags) { return process_check_success_intr(intr, pid, "adb disconnect", flags); } +char * +adb_getprop(struct sc_intr *intr, const char *serial, const char *prop, + unsigned flags) { + const char *const adb_cmd[] = {"shell", "getprop", prop}; + + sc_pipe pout; + sc_pid pid = + adb_execute_p(serial, adb_cmd, ARRAY_LEN(adb_cmd), flags, &pout); + if (pid == SC_PROCESS_NONE) { + LOGE("Could not execute \"adb getprop\""); + return NULL; + } + + char buf[128]; + ssize_t r = sc_pipe_read_all_intr(intr, pid, pout, buf, sizeof(buf)); + sc_pipe_close(pout); + + bool ok = process_check_success_intr(intr, pid, "adb getprop", flags); + if (!ok) { + return NULL; + } + + if (r == -1) { + return NULL; + } + + sc_str_truncate(buf, r, " \r\n"); + + return strdup(buf); +} + char * adb_get_serialno(struct sc_intr *intr, unsigned flags) { const char *const adb_cmd[] = {"get-serialno"}; diff --git a/app/src/adb.h b/app/src/adb.h index f56c98c4..8d7f3ea1 100644 --- a/app/src/adb.h +++ b/app/src/adb.h @@ -65,6 +65,13 @@ adb_connect(struct sc_intr *intr, const char *ip_port, unsigned flags); bool adb_disconnect(struct sc_intr *intr, const char *ip_port, unsigned flags); +/** + * Execute `adb getprop ` + */ +char * +adb_getprop(struct sc_intr *intr, const char *serial, const char *prop, + unsigned flags); + /** * Execute `adb get-serialno` * From 3b310f8317e4f62ee5aa76786aa75e3d610d34e0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 24 Nov 2021 22:57:36 +0100 Subject: [PATCH 0868/2244] Extract interruptible sleep for server This improves readability, and makes the function reusable. PR #2827 --- app/src/server.c | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/app/src/server.c b/app/src/server.c index a62a093a..6211b08b 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -136,6 +136,20 @@ log_level_to_server_string(enum sc_log_level level) { } } +static bool +sc_server_sleep(struct sc_server *server, sc_tick deadline) { + sc_mutex_lock(&server->mutex); + bool timed_out = false; + while (!server->stopped && !timed_out) { + timed_out = !sc_cond_timedwait(&server->cond_stopped, + &server->mutex, deadline); + } + bool stopped = server->stopped; + sc_mutex_unlock(&server->mutex); + + return !stopped; +} + static sc_pid execute_server(struct sc_server *server, const struct sc_server_params *params) { @@ -286,17 +300,9 @@ connect_to_server(struct sc_server *server, unsigned attempts, sc_tick delay, } if (attempts) { - sc_mutex_lock(&server->mutex); sc_tick deadline = sc_tick_now() + delay; - bool timed_out = false; - while (!server->stopped && !timed_out) { - timed_out = !sc_cond_timedwait(&server->cond_stopped, - &server->mutex, deadline); - } - bool stopped = server->stopped; - sc_mutex_unlock(&server->mutex); - - if (stopped) { + bool ok = sc_server_sleep(server, deadline); + if (!ok) { LOGI("Connection attempt stopped"); break; } From 19858e6aeb99b57de28f042053b8c3d4372e56ca Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 25 Nov 2021 22:22:49 +0100 Subject: [PATCH 0869/2244] Add --tcpip feature Expose an option to automatically configure and reconnect the device over TCP/IP, to simplify wireless connection without using adb explicitly. There are two variants: - If a destination address is provided, then scrcpy connects to this address before starting. The device must listen on the given TCP port (default is 5555). - If no destination address is provided, then scrcpy attempts to find the IP address of the current device (typically connected over USB), enables TCP/IP mode, then connects to this address before starting. PR #2827 --- app/scrcpy.1 | 8 ++ app/src/adb.h | 2 + app/src/cli.c | 27 ++++++ app/src/options.c | 2 + app/src/options.h | 2 + app/src/scrcpy.c | 2 + app/src/server.c | 203 +++++++++++++++++++++++++++++++++++++++++++++- app/src/server.h | 2 + 8 files changed, 244 insertions(+), 4 deletions(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 122af757..715000b6 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -199,6 +199,14 @@ For example, to use either LCtrl+LAlt or LSuper for scrcpy shortcuts, pass "lctr Default is "lalt,lsuper" (left-Alt or left-Super). +.TP +.BI "\-\-tcpip[=ip[:port]] +Configure and reconnect the device over TCP/IP. + +If a destination address is provided, then scrcpy connects to this address before starting. The device must listen on the given TCP port (default is 5555). + +If no destination address is provided, then scrcpy attempts to find the IP address of the current device (typically connected over USB), enables TCP/IP mode, then connects to this address before starting. + .TP .B \-S, \-\-turn\-screen\-off Turn the device screen off immediately. diff --git a/app/src/adb.h b/app/src/adb.h index 8d7f3ea1..4d1278cf 100644 --- a/app/src/adb.h +++ b/app/src/adb.h @@ -12,6 +12,8 @@ #define SC_ADB_NO_STDERR (1 << 1) #define SC_ADB_NO_LOGERR (1 << 2) +#define SC_ADB_SILENT (SC_ADB_NO_STDOUT | SC_ADB_NO_STDERR | SC_ADB_NO_LOGERR) + sc_pid adb_execute(const char *serial, const char *const adb_cmd[], size_t len, unsigned flags); diff --git a/app/src/cli.c b/app/src/cli.c index 791d3c43..8cfaf57b 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -50,6 +50,7 @@ #define OPT_TUNNEL_HOST 1030 #define OPT_TUNNEL_PORT 1031 #define OPT_NO_CLIPBOARD_AUTOSYNC 1032 +#define OPT_TCPIP 1033 struct sc_option { char shortopt; @@ -404,6 +405,20 @@ static const struct sc_option options[] = { .text = "Keep the device on while scrcpy is running, when the device " "is plugged in.", }, + { + .longopt_id = OPT_TCPIP, + .longopt = "tcpip", + .argdesc = "ip[:port]", + .optional_arg = true, + .text = "Configure and reconnect the device over TCP/IP.\n" + "If a destination address is provided, then scrcpy connects to " + "this address before starting. The device must listen on the " + "given TCP port (default is 5555).\n" + "If no destination address is provided, then scrcpy attempts " + "to find the IP address of the current device (typically " + "connected over USB), enables TCP/IP mode, then connects to " + "this address before starting.", + }, { .longopt_id = OPT_WINDOW_BORDERLESS, .longopt = "window-borderless", @@ -1378,6 +1393,10 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], case OPT_NO_CLIPBOARD_AUTOSYNC: opts->clipboard_autosync = false; break; + case OPT_TCPIP: + opts->tcpip = true; + opts->tcpip_dst = optarg; + break; #ifdef HAVE_V4L2 case OPT_V4L2_SINK: opts->v4l2_device = optarg; @@ -1400,6 +1419,14 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; } + // If a TCP/IP address is provided, then tcpip must be enabled + assert(opts->tcpip || !opts->tcpip_dst); + + if (opts->serial && opts->tcpip_dst) { + LOGE("Incompatible options: -s/--serial and --tcpip with an argument"); + return false; + } + #ifdef HAVE_V4L2 if (!opts->display && !opts->record_filename && !opts->v4l2_device) { LOGE("-N/--no-display requires either screen recording (-r/--record)" diff --git a/app/src/options.c b/app/src/options.c index a99b09da..a14bda9a 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -54,4 +54,6 @@ const struct scrcpy_options scrcpy_options_default = { .legacy_paste = false, .power_off_on_close = false, .clipboard_autosync = true, + .tcpip = false, + .tcpip_dst = NULL, }; diff --git a/app/src/options.h b/app/src/options.h index d10b2e8a..f183bd73 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -109,6 +109,8 @@ struct scrcpy_options { bool legacy_paste; bool power_off_on_close; bool clipboard_autosync; + bool tcpip; + const char *tcpip_dst; }; extern const struct scrcpy_options scrcpy_options_default; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index f0142b46..99317ffc 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -365,6 +365,8 @@ scrcpy(struct scrcpy_options *options) { .force_adb_forward = options->force_adb_forward, .power_off_on_close = options->power_off_on_close, .clipboard_autosync = options->clipboard_autosync, + .tcpip = options->tcpip, + .tcpip_dst = options->tcpip_dst, }; static const struct sc_server_callbacks cbs = { diff --git a/app/src/server.c b/app/src/server.c index 6211b08b..06cb7b72 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -69,6 +69,7 @@ sc_server_params_destroy(struct sc_server_params *params) { free((char *) params->crop); free((char *) params->codec_options); free((char *) params->encoder_name); + free((char *) params->tcpip_dst); } static bool @@ -92,6 +93,7 @@ sc_server_params_copy(struct sc_server_params *dst, COPY(crop); COPY(codec_options); COPY(encoder_name); + COPY(tcpip_dst); #undef COPY return true; @@ -494,23 +496,216 @@ sc_server_fill_serial(struct sc_server *server) { LOGE("Could not get device serial"); return false; } + + LOGD("Device serial: %s", server->params.serial); } return true; } +static bool +is_tcpip_mode_enabled(struct sc_server *server) { + struct sc_intr *intr = &server->intr; + const char *serial = server->params.serial; + + char *current_port = + adb_getprop(intr, serial, "service.adb.tcp.port", SC_ADB_SILENT); + if (!current_port) { + return false; + } + + // Is the device is listening on TCP on port 5555? + bool enabled = !strcmp("5555", current_port); + free(current_port); + return enabled; +} + +static bool +wait_tcpip_mode_enabled(struct sc_server *server, unsigned attempts, + sc_tick delay) { + if (is_tcpip_mode_enabled(server)) { + LOGI("TCP/IP mode enabled"); + return true; + } + + // Only print this log if TCP/IP is not enabled + LOGI("Waiting for TCP/IP mode enabled..."); + + do { + sc_tick deadline = sc_tick_now() + delay; + if (!sc_server_sleep(server, deadline)) { + LOGI("TCP/IP mode waiting interrupted"); + return false; + } + + if (is_tcpip_mode_enabled(server)) { + LOGI("TCP/IP mode enabled"); + return true; + } + } while (--attempts); + return false; +} + +char * +append_port_5555(const char *ip) { + size_t len = strlen(ip); + + // sizeof counts the final '\0' + char *ip_port = malloc(len + sizeof(":5555")); + if (!ip_port) { + LOG_OOM(); + return NULL; + } + + memcpy(ip_port, ip, len); + memcpy(ip_port + len, ":5555", sizeof(":5555")); + + return ip_port; +} + +static bool +sc_server_switch_to_tcpip(struct sc_server *server, char **out_ip_port) { + const char *serial = server->params.serial; + assert(serial); + + struct sc_intr *intr = &server->intr; + + char *ip = adb_get_device_ip(intr, serial, 0); + if (!ip) { + LOGE("Device IP not found"); + return false; + } + + char *ip_port = append_port_5555(ip); + free(ip); + if (!ip_port) { + return false; + } + + bool tcp_mode = is_tcpip_mode_enabled(server); + + if (!tcp_mode) { + bool ok = adb_tcpip(intr, serial, 5555, SC_ADB_NO_STDOUT); + if (!ok) { + LOGE("Could not restart adbd in TCP/IP mode"); + goto error; + } + + unsigned attempts = 40; + sc_tick delay = SC_TICK_FROM_MS(250); + ok = wait_tcpip_mode_enabled(server, attempts, delay); + if (!ok) { + goto error; + } + } + + *out_ip_port = ip_port; + + return true; + +error: + free(ip_port); + return false; +} + +static bool +sc_server_connect_to_tcpip(struct sc_server *server, const char *ip_port) { + struct sc_intr *intr = &server->intr; + + // Error expected if not connected, do not report any error + adb_disconnect(intr, ip_port, SC_ADB_SILENT); + + bool ok = adb_connect(intr, ip_port, 0); + if (!ok) { + LOGE("Could not connect to %s", ip_port); + return false; + } + + // Override the serial, owned by the sc_server_params + free((void *) server->params.serial); + server->params.serial = strdup(ip_port); + if (!server->params.serial) { + LOG_OOM(); + return false; + } + + LOGI("Connected to %s", ip_port); + return true; +} + + +static bool +sc_server_configure_tcpip(struct sc_server *server) { + char *ip_port; + + const struct sc_server_params *params = &server->params; + + // If tcpip parameter is given, then it must connect to this address. + // Therefore, the device is unknown, so serial is meaningless at this point. + assert(!params->serial || !params->tcpip_dst); + + if (params->tcpip_dst) { + // Append ":5555" if no port is present + bool contains_port = strchr(params->tcpip_dst, ':'); + ip_port = contains_port ? strdup(params->tcpip_dst) + : append_port_5555(params->tcpip_dst); + if (!ip_port) { + LOG_OOM(); + return false; + } + } else { + // The device IP address must be retrieved from the current + // connected device + if (!sc_server_fill_serial(server)) { + return false; + } + + // The serial is either the real serial when connected via USB, or + // the IP:PORT when connected over TCP/IP. Only the latter contains + // a colon. + bool is_already_tcpip = strchr(params->serial, ':'); + if (is_already_tcpip) { + // Nothing to do + LOGI("Device already connected via TCP/IP: %s", params->serial); + return true; + } + + bool ok = sc_server_switch_to_tcpip(server, &ip_port); + if (!ok) { + return false; + } + } + + // On success, this call changes params->serial + bool ok = sc_server_connect_to_tcpip(server, ip_port); + free(ip_port); + return ok; +} + static int run_server(void *data) { struct sc_server *server = data; + const struct sc_server_params *params = &server->params; + + if (params->serial) { + LOGD("Device serial: %s", params->serial); + } + + if (params->tcpip) { + // params->serial may be changed after this call + bool ok = sc_server_configure_tcpip(server); + if (!ok) { + goto error_connection_failed; + } + } + + // It is ok to call this function even if the device serial has been + // changed by switching over TCP/IP if (!sc_server_fill_serial(server)) { goto error_connection_failed; } - const struct sc_server_params *params = &server->params; - - LOGD("Device serial: %s", params->serial); - bool ok = push_server(&server->intr, params->serial); if (!ok) { goto error_connection_failed; diff --git a/app/src/server.h b/app/src/server.h index 5b25ff46..8ea20dc7 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -42,6 +42,8 @@ struct sc_server_params { bool force_adb_forward; bool power_off_on_close; bool clipboard_autosync; + bool tcpip; + const char *tcpip_dst; }; struct sc_server { From 1a6caeb18c06ddd3a1116a0e82d65be15b2f48b4 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 25 Nov 2021 22:42:34 +0100 Subject: [PATCH 0870/2244] Document --tcpip in README PR #2827 --- README.md | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index fa4aff68..ef9fcc98 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [Read in another language](#translations) This application provides display and control of Android devices connected via -USB (or [over TCP/IP](#wireless)). It does not require any _root_ access. +USB (or [over TCP/IP](#tcpip-wireless)). It does not require any _root_ access. It works on _GNU/Linux_, _Windows_ and _macOS_. ![screenshot](assets/screenshot-debian-600.jpg) @@ -356,10 +356,38 @@ scrcpy --v4l2-buffer=500 # add 500 ms buffering for v4l2 sink ### Connection -#### Wireless +#### TCP/IP (wireless) _Scrcpy_ uses `adb` to communicate with the device, and `adb` can [connect] to a -device over TCP/IP: +device over TCP/IP. + +##### Automatic + +An option `--tcpip` allows to configure the connection automatically. There are +two variants. + +If the device (accessible at 192.168.1.1 in this example) already listens on a +port (typically 5555) for incoming adb connections, then run: + +```bash +scrcpy --tcpip=192.168.1.1 # default port is 5555 +scrcpy --tcpip=192.168.1.1:5555 +``` + +If the device TCP/IP mode is disabled (or if you don't know the IP address), +connect the device over USB, then run: + +```bash +scrcpy --tcpip # without arguments +``` + +It will automatically find the device IP address, enable TCP/IP mode, then +connect to the device before starting. + +##### Manual + +Alternatively, it is possible to enable the TCP/IP connection manually using +`adb`: 1. Connect the device to the same Wi-Fi as your computer. 2. Get your device IP address, in Settings → About phone → Status, or by From 82a053015d01d3a1e1b6f60fbe6c0cd6910dc344 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 26 Nov 2021 21:19:39 +0100 Subject: [PATCH 0871/2244] Improve HID keyboard documentation Explain how to configure the keyboard layout. --- README.md | 9 +++++++++ app/scrcpy.1 | 6 ++++++ app/src/cli.c | 9 ++++++++- 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ef9fcc98..3e167b06 100644 --- a/README.md +++ b/README.md @@ -803,6 +803,15 @@ of the host key mapping. Therefore, if your keyboard layout does not match, it must be configured on the Android device, in Settings → System → Languages and input → [Physical keyboard]. +This settings page can be started directly: + +```bash +adb shell am start -a android.settings.HARD_KEYBOARD_SETTINGS +``` + +However, the option is only available when the HID keyboard is enabled (or when +a physical keyboard is connected). + [Physical keyboard]: https://github.com/Genymobile/scrcpy/pull/2632#issuecomment-923756915 diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 715000b6..f7508d5e 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -90,6 +90,12 @@ This provides a better experience for IME users, and allows to generate non-ASCI It may only work over USB, and is currently only supported on Linux. +The keyboard layout must be configured (once and for all) on the device, via Settings -> System -> Languages and input -> Physical keyboard. This settings page can be started directly: + + adb shell am start -a android.settings.HARD_KEYBOARD_SETTINGS + +However, the option is only available when the HID keyboard is enabled (or a physical keyboard is connected). + .TP .B \-\-legacy\-paste Inject computer clipboard text as a sequence of key events on Ctrl+v (like MOD+Shift+v). diff --git a/app/src/cli.c b/app/src/cli.c index 8cfaf57b..410ce25a 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -165,7 +165,14 @@ static const struct sc_option options[] = { "generate non-ASCII characters, contrary to the default " "injection method.\n" "It may only work over USB, and is currently only supported " - "on Linux.", + "on Linux.\n" + "The keyboard layout must be configured (once and for all) on " + "the device, via Settings -> System -> Languages and input -> " + "Physical keyboard. This settings page can be started " + "directly: `adb shell am start -a " + "android.settings.HARD_KEYBOARD_SETTINGS`.\n" + "However, the option is only available when the HID keyboard " + "is enabled (or a physical keyboard is connected).", }, { .shortopt = 'h', From c96505200a21d6816740fa1d0e272180280306e6 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 26 Nov 2021 21:57:06 +0100 Subject: [PATCH 0872/2244] Fix code style in keyboard_inject --- app/src/keyboard_inject.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/keyboard_inject.c b/app/src/keyboard_inject.c index 4112fd4c..83924ab7 100644 --- a/app/src/keyboard_inject.c +++ b/app/src/keyboard_inject.c @@ -9,8 +9,7 @@ #include "util/log.h" /** Downcast key processor to sc_keyboard_inject */ -#define DOWNCAST(KP) \ - container_of(KP, struct sc_keyboard_inject, key_processor) +#define DOWNCAST(KP) container_of(KP, struct sc_keyboard_inject, key_processor) #define MAP(FROM, TO) case FROM: *to = TO; return true #define FAIL default: return false From 0c0f62e4abe5bd58d9a2a90585b252c574009eec Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 26 Nov 2021 21:58:56 +0100 Subject: [PATCH 0873/2244] Use static maps to convert input events This improves readability (hopefully). PR #2831 --- app/meson.build | 1 + app/src/keyboard_inject.c | 163 +++++++++++++++++++++++--------------- app/src/mouse_inject.c | 35 +++++--- app/src/util/intmap.c | 13 +++ app/src/util/intmap.h | 24 ++++++ 5 files changed, 160 insertions(+), 76 deletions(-) create mode 100644 app/src/util/intmap.c create mode 100644 app/src/util/intmap.h diff --git a/app/meson.build b/app/meson.build index 720d9c8c..7d9c2b5c 100644 --- a/app/meson.build +++ b/app/meson.build @@ -28,6 +28,7 @@ src = [ 'src/video_buffer.c', 'src/util/acksync.c', 'src/util/file.c', + 'src/util/intmap.c', 'src/util/intr.c', 'src/util/log.c', 'src/util/net.c', diff --git a/app/src/keyboard_inject.c b/app/src/keyboard_inject.c index 83924ab7..bb3bb953 100644 --- a/app/src/keyboard_inject.c +++ b/app/src/keyboard_inject.c @@ -6,60 +6,115 @@ #include "android/input.h" #include "control_msg.h" #include "controller.h" +#include "util/intmap.h" #include "util/log.h" /** Downcast key processor to sc_keyboard_inject */ #define DOWNCAST(KP) container_of(KP, struct sc_keyboard_inject, key_processor) -#define MAP(FROM, TO) case FROM: *to = TO; return true -#define FAIL default: return false static bool convert_keycode_action(SDL_EventType from, enum android_keyevent_action *to) { - switch (from) { - MAP(SDL_KEYDOWN, AKEY_EVENT_ACTION_DOWN); - MAP(SDL_KEYUP, AKEY_EVENT_ACTION_UP); - FAIL; + static const struct sc_intmap_entry actions[] = { + {SDL_KEYDOWN, AKEY_EVENT_ACTION_DOWN}, + {SDL_KEYUP, AKEY_EVENT_ACTION_UP}, + }; + + const struct sc_intmap_entry *entry = SC_INTMAP_FIND_ENTRY(actions, from); + if (entry) { + *to = entry->value; + return true; } + + return false; } static bool convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod, bool prefer_text) { - switch (from) { - MAP(SDLK_RETURN, AKEYCODE_ENTER); - MAP(SDLK_KP_ENTER, AKEYCODE_NUMPAD_ENTER); - MAP(SDLK_ESCAPE, AKEYCODE_ESCAPE); - MAP(SDLK_BACKSPACE, AKEYCODE_DEL); - MAP(SDLK_TAB, AKEYCODE_TAB); - MAP(SDLK_PAGEUP, AKEYCODE_PAGE_UP); - MAP(SDLK_DELETE, AKEYCODE_FORWARD_DEL); - MAP(SDLK_HOME, AKEYCODE_MOVE_HOME); - MAP(SDLK_END, AKEYCODE_MOVE_END); - MAP(SDLK_PAGEDOWN, AKEYCODE_PAGE_DOWN); - MAP(SDLK_RIGHT, AKEYCODE_DPAD_RIGHT); - 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); - MAP(SDLK_LSHIFT, AKEYCODE_SHIFT_LEFT); - MAP(SDLK_RSHIFT, AKEYCODE_SHIFT_RIGHT); + // Navigation keys and ENTER. + // Used in all modes. + static const struct sc_intmap_entry special_keys[] = { + {SDLK_RETURN, AKEYCODE_ENTER}, + {SDLK_KP_ENTER, AKEYCODE_NUMPAD_ENTER}, + {SDLK_ESCAPE, AKEYCODE_ESCAPE}, + {SDLK_BACKSPACE, AKEYCODE_DEL}, + {SDLK_TAB, AKEYCODE_TAB}, + {SDLK_PAGEUP, AKEYCODE_PAGE_UP}, + {SDLK_DELETE, AKEYCODE_FORWARD_DEL}, + {SDLK_HOME, AKEYCODE_MOVE_HOME}, + {SDLK_END, AKEYCODE_MOVE_END}, + {SDLK_PAGEDOWN, AKEYCODE_PAGE_DOWN}, + {SDLK_RIGHT, AKEYCODE_DPAD_RIGHT}, + {SDLK_LEFT, AKEYCODE_DPAD_LEFT}, + {SDLK_DOWN, AKEYCODE_DPAD_DOWN}, + {SDLK_UP, AKEYCODE_DPAD_UP}, + {SDLK_LCTRL, AKEYCODE_CTRL_LEFT}, + {SDLK_RCTRL, AKEYCODE_CTRL_RIGHT}, + {SDLK_LSHIFT, AKEYCODE_SHIFT_LEFT}, + {SDLK_RSHIFT, AKEYCODE_SHIFT_RIGHT}, + }; + + // Numpad navigation keys. + // Used in all modes, when NumLock and Shift are disabled. + static const struct sc_intmap_entry kp_nav_keys[] = { + {SDLK_KP_0, AKEYCODE_INSERT}, + {SDLK_KP_1, AKEYCODE_MOVE_END}, + {SDLK_KP_2, AKEYCODE_DPAD_DOWN}, + {SDLK_KP_3, AKEYCODE_PAGE_DOWN}, + {SDLK_KP_4, AKEYCODE_DPAD_LEFT}, + {SDLK_KP_6, AKEYCODE_DPAD_RIGHT}, + {SDLK_KP_7, AKEYCODE_MOVE_HOME}, + {SDLK_KP_8, AKEYCODE_DPAD_UP}, + {SDLK_KP_9, AKEYCODE_PAGE_UP}, + {SDLK_KP_PERIOD, AKEYCODE_FORWARD_DEL}, + }; + + // Letters and space. + // Used in non-text mode. + static const struct sc_intmap_entry alphaspace_keys[] = { + {SDLK_a, AKEYCODE_A}, + {SDLK_b, AKEYCODE_B}, + {SDLK_c, AKEYCODE_C}, + {SDLK_d, AKEYCODE_D}, + {SDLK_e, AKEYCODE_E}, + {SDLK_f, AKEYCODE_F}, + {SDLK_g, AKEYCODE_G}, + {SDLK_h, AKEYCODE_H}, + {SDLK_i, AKEYCODE_I}, + {SDLK_j, AKEYCODE_J}, + {SDLK_k, AKEYCODE_K}, + {SDLK_l, AKEYCODE_L}, + {SDLK_m, AKEYCODE_M}, + {SDLK_n, AKEYCODE_N}, + {SDLK_o, AKEYCODE_O}, + {SDLK_p, AKEYCODE_P}, + {SDLK_q, AKEYCODE_Q}, + {SDLK_r, AKEYCODE_R}, + {SDLK_s, AKEYCODE_S}, + {SDLK_t, AKEYCODE_T}, + {SDLK_u, AKEYCODE_U}, + {SDLK_v, AKEYCODE_V}, + {SDLK_w, AKEYCODE_W}, + {SDLK_x, AKEYCODE_X}, + {SDLK_y, AKEYCODE_Y}, + {SDLK_z, AKEYCODE_Z}, + {SDLK_SPACE, AKEYCODE_SPACE}, + }; + + const struct sc_intmap_entry *entry = + SC_INTMAP_FIND_ENTRY(special_keys, from); + if (entry) { + *to = entry->value; + return true; } 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); + entry = SC_INTMAP_FIND_ENTRY(kp_nav_keys, from); + if (entry) { + *to = entry->value; + return true; } } @@ -71,37 +126,15 @@ convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod, if (mod & (KMOD_LALT | KMOD_RALT | KMOD_LGUI | KMOD_RGUI)) { return false; } + // if ALT and META are not pressed, also handle letters and space - switch (from) { - MAP(SDLK_a, AKEYCODE_A); - MAP(SDLK_b, AKEYCODE_B); - MAP(SDLK_c, AKEYCODE_C); - MAP(SDLK_d, AKEYCODE_D); - MAP(SDLK_e, AKEYCODE_E); - MAP(SDLK_f, AKEYCODE_F); - MAP(SDLK_g, AKEYCODE_G); - MAP(SDLK_h, AKEYCODE_H); - MAP(SDLK_i, AKEYCODE_I); - MAP(SDLK_j, AKEYCODE_J); - MAP(SDLK_k, AKEYCODE_K); - MAP(SDLK_l, AKEYCODE_L); - MAP(SDLK_m, AKEYCODE_M); - MAP(SDLK_n, AKEYCODE_N); - MAP(SDLK_o, AKEYCODE_O); - MAP(SDLK_p, AKEYCODE_P); - MAP(SDLK_q, AKEYCODE_Q); - MAP(SDLK_r, AKEYCODE_R); - MAP(SDLK_s, AKEYCODE_S); - MAP(SDLK_t, AKEYCODE_T); - MAP(SDLK_u, AKEYCODE_U); - MAP(SDLK_v, AKEYCODE_V); - MAP(SDLK_w, AKEYCODE_W); - MAP(SDLK_x, AKEYCODE_X); - MAP(SDLK_y, AKEYCODE_Y); - MAP(SDLK_z, AKEYCODE_Z); - MAP(SDLK_SPACE, AKEYCODE_SPACE); - FAIL; + entry = SC_INTMAP_FIND_ENTRY(alphaspace_keys, from); + if (entry) { + *to = entry->value; + return true; } + + return false; } static enum android_metastate diff --git a/app/src/mouse_inject.c b/app/src/mouse_inject.c index 1d5fe230..8f7e363d 100644 --- a/app/src/mouse_inject.c +++ b/app/src/mouse_inject.c @@ -6,6 +6,7 @@ #include "android/input.h" #include "control_msg.h" #include "controller.h" +#include "util/intmap.h" #include "util/log.h" /** Downcast mouse processor to sc_mouse_inject */ @@ -32,25 +33,37 @@ convert_mouse_buttons(uint32_t state) { return buttons; } -#define MAP(FROM, TO) case FROM: *to = TO; return true -#define FAIL default: return false static bool convert_mouse_action(SDL_EventType from, enum android_motionevent_action *to) { - switch (from) { - MAP(SDL_MOUSEBUTTONDOWN, AMOTION_EVENT_ACTION_DOWN); - MAP(SDL_MOUSEBUTTONUP, AMOTION_EVENT_ACTION_UP); - FAIL; + static const struct sc_intmap_entry actions[] = { + {SDL_MOUSEBUTTONDOWN, AMOTION_EVENT_ACTION_DOWN}, + {SDL_MOUSEBUTTONUP, AMOTION_EVENT_ACTION_UP}, + }; + + const struct sc_intmap_entry *entry = SC_INTMAP_FIND_ENTRY(actions, from); + if (entry) { + *to = entry->value; + return true; } + + return false; } static bool convert_touch_action(SDL_EventType from, enum android_motionevent_action *to) { - switch (from) { - MAP(SDL_FINGERMOTION, AMOTION_EVENT_ACTION_MOVE); - MAP(SDL_FINGERDOWN, AMOTION_EVENT_ACTION_DOWN); - MAP(SDL_FINGERUP, AMOTION_EVENT_ACTION_UP); - FAIL; + static const struct sc_intmap_entry actions[] = { + {SDL_FINGERMOTION, AMOTION_EVENT_ACTION_MOVE}, + {SDL_FINGERDOWN, AMOTION_EVENT_ACTION_DOWN}, + {SDL_FINGERUP, AMOTION_EVENT_ACTION_UP}, + }; + + const struct sc_intmap_entry *entry = SC_INTMAP_FIND_ENTRY(actions, from); + if (entry) { + *to = entry->value; + return true; } + + return false; } static bool diff --git a/app/src/util/intmap.c b/app/src/util/intmap.c new file mode 100644 index 00000000..fa11acef --- /dev/null +++ b/app/src/util/intmap.c @@ -0,0 +1,13 @@ +#include "intmap.h" + +const struct sc_intmap_entry * +sc_intmap_find_entry(const struct sc_intmap_entry entries[], size_t len, + int32_t key) { + for (size_t i = 0; i < len; ++i) { + const struct sc_intmap_entry *entry = &entries[i]; + if (entry->key == key) { + return entry; + } + } + return NULL; +} diff --git a/app/src/util/intmap.h b/app/src/util/intmap.h new file mode 100644 index 00000000..2898c461 --- /dev/null +++ b/app/src/util/intmap.h @@ -0,0 +1,24 @@ +#ifndef SC_ARRAYMAP_H +#define SC_ARRAYMAP_H + +#include "common.h" + +#include + +struct sc_intmap_entry { + int32_t key; + int32_t value; +}; + +const struct sc_intmap_entry * +sc_intmap_find_entry(const struct sc_intmap_entry entries[], size_t len, + int32_t key); + +/** + * MAP is expected to be a static array of sc_intmap_entry, so that + * ARRAY_LEN(MAP) can be computed statically. + */ +#define SC_INTMAP_FIND_ENTRY(MAP, KEY) \ + sc_intmap_find_entry(MAP, ARRAY_LEN(MAP), KEY) + +#endif From 5e918ac0c39cf7004c7f6843f2d28d4a06fd216e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 26 Nov 2021 22:05:28 +0100 Subject: [PATCH 0874/2244] Use enum for key injection mode There was only two key injection modes: - the default one - the mode with --prefer-text enabled To prepare the addition of another mode (--raw-key-events), use an enum instead of a bool. PR #2831 --- app/src/cli.c | 2 +- app/src/keyboard_inject.c | 16 ++++++++-------- app/src/keyboard_inject.h | 2 +- app/src/options.c | 2 +- app/src/options.h | 13 ++++++++++++- app/tests/test_cli.c | 2 +- 6 files changed, 24 insertions(+), 13 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index 410ce25a..67a7a89f 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -1350,7 +1350,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], opts->push_target = optarg; break; case OPT_PREFER_TEXT: - opts->prefer_text = true; + opts->key_inject_mode = SC_KEY_INJECT_MODE_TEXT; break; case OPT_ROTATION: if (!parse_rotation(optarg, &opts->rotation)) { diff --git a/app/src/keyboard_inject.c b/app/src/keyboard_inject.c index bb3bb953..e6212193 100644 --- a/app/src/keyboard_inject.c +++ b/app/src/keyboard_inject.c @@ -30,7 +30,7 @@ convert_keycode_action(SDL_EventType from, enum android_keyevent_action *to) { static bool convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod, - bool prefer_text) { + enum sc_key_inject_mode key_inject_mode) { // Navigation keys and ENTER. // Used in all modes. static const struct sc_intmap_entry special_keys[] = { @@ -118,7 +118,7 @@ convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod, } } - if (prefer_text && !(mod & KMOD_CTRL)) { + if (key_inject_mode == SC_KEY_INJECT_MODE_TEXT && !(mod & KMOD_CTRL)) { // do not forward alpha and space key events (unless Ctrl is pressed) return false; } @@ -199,7 +199,7 @@ convert_meta_state(SDL_Keymod mod) { static bool convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to, - bool prefer_text, uint32_t repeat) { + enum sc_key_inject_mode key_inject_mode, uint32_t repeat) { to->type = CONTROL_MSG_TYPE_INJECT_KEYCODE; if (!convert_keycode_action(from->type, &to->inject_keycode.action)) { @@ -208,7 +208,7 @@ convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to, uint16_t mod = from->keysym.mod; if (!convert_keycode(from->keysym.sym, &to->inject_keycode.keycode, mod, - prefer_text)) { + key_inject_mode)) { return false; } @@ -239,7 +239,7 @@ sc_key_processor_process_key(struct sc_key_processor *kp, } struct control_msg msg; - if (convert_input_key(event, &msg, ki->prefer_text, ki->repeat)) { + if (convert_input_key(event, &msg, ki->key_inject_mode, ki->repeat)) { if (!controller_push_msg(ki->controller, &msg)) { LOGW("Could not request 'inject keycode'"); } @@ -251,11 +251,11 @@ sc_key_processor_process_text(struct sc_key_processor *kp, const SDL_TextInputEvent *event) { struct sc_keyboard_inject *ki = DOWNCAST(kp); - if (!ki->prefer_text) { + if (ki->key_inject_mode == SC_KEY_INJECT_MODE_MIXED) { char c = event->text[0]; if (isalpha(c) || c == ' ') { assert(event->text[1] == '\0'); - // letters and space are handled as raw key event + // Letters and space are handled as raw key events return; } } @@ -278,7 +278,7 @@ sc_keyboard_inject_init(struct sc_keyboard_inject *ki, struct controller *controller, const struct scrcpy_options *options) { ki->controller = controller; - ki->prefer_text = options->prefer_text; + ki->key_inject_mode = options->key_inject_mode; ki->forward_key_repeat = options->forward_key_repeat; ki->repeat = 0; diff --git a/app/src/keyboard_inject.h b/app/src/keyboard_inject.h index f4ebe40e..edd5b1ba 100644 --- a/app/src/keyboard_inject.h +++ b/app/src/keyboard_inject.h @@ -18,7 +18,7 @@ struct sc_keyboard_inject { // number of repetitions. This variable keeps track of the count. unsigned repeat; - bool prefer_text; + enum sc_key_inject_mode key_inject_mode; bool forward_key_repeat; }; diff --git a/app/src/options.c b/app/src/options.c index a14bda9a..4860fa07 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -43,7 +43,7 @@ const struct scrcpy_options scrcpy_options_default = { .control = true, .display = true, .turn_screen_off = false, - .prefer_text = false, + .key_inject_mode = SC_KEY_INJECT_MODE_MIXED, .window_borderless = false, .mipmaps = true, .stay_awake = false, diff --git a/app/src/options.h b/app/src/options.h index f183bd73..84a80fbe 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -38,6 +38,17 @@ enum sc_keyboard_input_mode { SC_KEYBOARD_INPUT_MODE_HID, }; +enum sc_key_inject_mode { + // Inject special keys, letters and space as key events. + // Inject numbers and punctuation as text events. + // This is the default mode. + SC_KEY_INJECT_MODE_MIXED, + + // Inject special keys as key events. + // Inject letters and space, numbers and punctuation as text events. + SC_KEY_INJECT_MODE_TEXT, +}; + #define SC_MAX_SHORTCUT_MODS 8 enum sc_shortcut_mod { @@ -98,7 +109,7 @@ struct scrcpy_options { bool control; bool display; bool turn_screen_off; - bool prefer_text; + enum sc_key_inject_mode key_inject_mode; bool window_borderless; bool mipmaps; bool stay_awake; diff --git a/app/tests/test_cli.c b/app/tests/test_cli.c index 05bacbf8..5bc1cc07 100644 --- a/app/tests/test_cli.c +++ b/app/tests/test_cli.c @@ -89,7 +89,7 @@ static void test_options(void) { assert(!strcmp(opts->serial, "0123456789abcdef")); assert(opts->show_touches); assert(opts->turn_screen_off); - assert(opts->prefer_text); + assert(opts->key_inject_mode == SC_KEY_INJECT_MODE_TEXT); assert(!strcmp(opts->window_title, "my device")); assert(opts->window_x == 100); assert(opts->window_y == -1); From bd56d81f728d29a0c710276f64e6c754edde217a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 26 Nov 2021 22:15:44 +0100 Subject: [PATCH 0875/2244] Add --raw-key-events This option allows to inject all input keys as key events, and ignore text events. Fixes #2816 PR #2831 --- README.md | 8 ++++- app/scrcpy.1 | 4 +++ app/src/cli.c | 17 +++++++++++ app/src/keyboard_inject.c | 62 +++++++++++++++++++++++++++++++++++++++ app/src/options.h | 3 ++ 5 files changed, 93 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3e167b06..550f1f7f 100644 --- a/README.md +++ b/README.md @@ -833,7 +833,13 @@ scrcpy --prefer-text (but this will break keyboard behavior in games) -This option has no effect on HID keyboard (all key events are sent as +On the contrary, you could force to always inject raw key events: + +```bash +scrcpy --raw-key-events +``` + +These options have no effect on HID keyboard (all key events are sent as scancodes in this mode). [textevents]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-text-input diff --git a/app/scrcpy.1 b/app/scrcpy.1 index f7508d5e..b99c781f 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -165,6 +165,10 @@ Set the target directory for pushing files to the device by drag & drop. It is p Default is "/sdcard/Download/". +.TP +.B \-\-raw\-key\-events +Inject key events for all input keys, and ignore text events. + .TP .BI "\-r, \-\-record " file Record screen to diff --git a/app/src/cli.c b/app/src/cli.c index 67a7a89f..f0b2e89c 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -51,6 +51,7 @@ #define OPT_TUNNEL_PORT 1031 #define OPT_NO_CLIPBOARD_AUTOSYNC 1032 #define OPT_TCPIP 1033 +#define OPT_RAW_KEY_EVENTS 1034 struct sc_option { char shortopt; @@ -282,6 +283,11 @@ static const struct sc_option options[] = { "drag & drop. It is passed as is to \"adb push\".\n" "Default is \"/sdcard/Download/\".", }, + { + .longopt_id = OPT_RAW_KEY_EVENTS, + .longopt = "raw-key-events", + .text = "Inject key events for all input keys, and ignore text events." + }, { .shortopt = 'r', .longopt = "record", @@ -1350,8 +1356,19 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], opts->push_target = optarg; break; case OPT_PREFER_TEXT: + if (opts->key_inject_mode != SC_KEY_INJECT_MODE_MIXED) { + LOGE("--prefer-text is incompatible with --raw-key-events"); + return false; + } opts->key_inject_mode = SC_KEY_INJECT_MODE_TEXT; break; + case OPT_RAW_KEY_EVENTS: + if (opts->key_inject_mode != SC_KEY_INJECT_MODE_MIXED) { + LOGE("--prefer-text is incompatible with --raw-key-events"); + return false; + } + opts->key_inject_mode = SC_KEY_INJECT_MODE_RAW; + break; case OPT_ROTATION: if (!parse_rotation(optarg, &opts->rotation)) { return false; diff --git a/app/src/keyboard_inject.c b/app/src/keyboard_inject.c index e6212193..5143eafc 100644 --- a/app/src/keyboard_inject.c +++ b/app/src/keyboard_inject.c @@ -101,6 +101,55 @@ convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod, {SDLK_SPACE, AKEYCODE_SPACE}, }; + // Numbers and punctuation keys. + // Used in raw mode only. + static const struct sc_intmap_entry numbers_punct_keys[] = { + {SDLK_HASH, AKEYCODE_POUND}, + {SDLK_PERCENT, AKEYCODE_PERIOD}, + {SDLK_QUOTE, AKEYCODE_APOSTROPHE}, + {SDLK_ASTERISK, AKEYCODE_STAR}, + {SDLK_PLUS, AKEYCODE_PLUS}, + {SDLK_COMMA, AKEYCODE_COMMA}, + {SDLK_MINUS, AKEYCODE_MINUS}, + {SDLK_PERIOD, AKEYCODE_PERIOD}, + {SDLK_SLASH, AKEYCODE_SLASH}, + {SDLK_0, AKEYCODE_0}, + {SDLK_1, AKEYCODE_1}, + {SDLK_2, AKEYCODE_2}, + {SDLK_3, AKEYCODE_3}, + {SDLK_4, AKEYCODE_4}, + {SDLK_5, AKEYCODE_5}, + {SDLK_6, AKEYCODE_6}, + {SDLK_7, AKEYCODE_7}, + {SDLK_8, AKEYCODE_8}, + {SDLK_9, AKEYCODE_9}, + {SDLK_SEMICOLON, AKEYCODE_SEMICOLON}, + {SDLK_EQUALS, AKEYCODE_EQUALS}, + {SDLK_AT, AKEYCODE_AT}, + {SDLK_LEFTBRACKET, AKEYCODE_LEFT_BRACKET}, + {SDLK_BACKSLASH, AKEYCODE_BACKSLASH}, + {SDLK_RIGHTBRACKET, AKEYCODE_RIGHT_BRACKET}, + {SDLK_BACKQUOTE, AKEYCODE_GRAVE}, + {SDLK_KP_1, AKEYCODE_NUMPAD_1}, + {SDLK_KP_2, AKEYCODE_NUMPAD_2}, + {SDLK_KP_3, AKEYCODE_NUMPAD_3}, + {SDLK_KP_4, AKEYCODE_NUMPAD_4}, + {SDLK_KP_5, AKEYCODE_NUMPAD_5}, + {SDLK_KP_6, AKEYCODE_NUMPAD_6}, + {SDLK_KP_7, AKEYCODE_NUMPAD_7}, + {SDLK_KP_8, AKEYCODE_NUMPAD_8}, + {SDLK_KP_9, AKEYCODE_NUMPAD_9}, + {SDLK_KP_0, AKEYCODE_NUMPAD_0}, + {SDLK_KP_DIVIDE, AKEYCODE_NUMPAD_DIVIDE}, + {SDLK_KP_MULTIPLY, AKEYCODE_NUMPAD_MULTIPLY}, + {SDLK_KP_MINUS, AKEYCODE_NUMPAD_SUBTRACT}, + {SDLK_KP_PLUS, AKEYCODE_NUMPAD_ADD}, + {SDLK_KP_PERIOD, AKEYCODE_NUMPAD_DOT}, + {SDLK_KP_EQUALS, AKEYCODE_NUMPAD_EQUALS}, + {SDLK_KP_LEFTPAREN, AKEYCODE_NUMPAD_LEFT_PAREN}, + {SDLK_KP_RIGHTPAREN, AKEYCODE_NUMPAD_RIGHT_PAREN}, + }; + const struct sc_intmap_entry *entry = SC_INTMAP_FIND_ENTRY(special_keys, from); if (entry) { @@ -134,6 +183,14 @@ convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod, return true; } + if (key_inject_mode == SC_KEY_INJECT_MODE_RAW) { + entry = SC_INTMAP_FIND_ENTRY(numbers_punct_keys, from); + if (entry) { + *to = entry->value; + return true; + } + } + return false; } @@ -251,6 +308,11 @@ sc_key_processor_process_text(struct sc_key_processor *kp, const SDL_TextInputEvent *event) { struct sc_keyboard_inject *ki = DOWNCAST(kp); + if (ki->key_inject_mode == SC_KEY_INJECT_MODE_RAW) { + // Never inject text events + return; + } + if (ki->key_inject_mode == SC_KEY_INJECT_MODE_MIXED) { char c = event->text[0]; if (isalpha(c) || c == ' ') { diff --git a/app/src/options.h b/app/src/options.h index 84a80fbe..39703210 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -47,6 +47,9 @@ enum sc_key_inject_mode { // Inject special keys as key events. // Inject letters and space, numbers and punctuation as text events. SC_KEY_INJECT_MODE_TEXT, + + // Inject everything as key events. + SC_KEY_INJECT_MODE_RAW, }; #define SC_MAX_SHORTCUT_MODS 8 From bf97a46b0c93fa1ef71dbeff86f42f5ceccdb2ab Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 29 Nov 2021 21:49:36 +0100 Subject: [PATCH 0876/2244] Upgrade gradle build tools to 7.0.3 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 6ed7e4c6..c7d31ef7 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:7.0.2' + classpath 'com.android.tools.build:gradle:7.0.3' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files From cbe73b0bc3595ec191b6907edcace3fbb49b2506 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 29 Nov 2021 09:05:53 +0100 Subject: [PATCH 0877/2244] Fix set_clipboard message log If paste is disabled on set_clipboard, then the PASTE key is not injected, but COPY is unrelated. PR #2834 --- app/src/control_msg.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/control_msg.c b/app/src/control_msg.c index 90cde0cf..4cc2f9d7 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -197,7 +197,7 @@ control_msg_log(const struct control_msg *msg) { case CONTROL_MSG_TYPE_SET_CLIPBOARD: LOG_CMSG("clipboard %" PRIu64_ " %s \"%s\"", msg->set_clipboard.sequence, - msg->set_clipboard.paste ? "paste" : "copy", + msg->set_clipboard.paste ? "paste" : "nopaste", msg->set_clipboard.text); break; case CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE: From dc19ae334dcc55f64a9e29ec9d5e754c6da69657 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 29 Nov 2021 09:18:23 +0100 Subject: [PATCH 0878/2244] Move acknowledgment handling Handle all actions related to SET_CLIPBOARD from the dedicated method. PR #2834 --- .../java/com/genymobile/scrcpy/Controller.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index 8b24b300..b52d413e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -120,12 +120,7 @@ public class Controller { } break; case ControlMessage.TYPE_SET_CLIPBOARD: - long sequence = msg.getSequence(); - setClipboard(msg.getText(), msg.getPaste()); - if (sequence != ControlMessage.SEQUENCE_INVALID) { - // Acknowledgement requested - sender.pushAckClipboard(sequence); - } + setClipboard(msg.getText(), msg.getPaste(), msg.getSequence()); break; case ControlMessage.TYPE_SET_SCREEN_POWER_MODE: if (device.supportsInputEvents()) { @@ -281,7 +276,7 @@ public class Controller { return device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER); } - private boolean setClipboard(String text, boolean paste) { + private boolean setClipboard(String text, boolean paste, long sequence) { boolean ok = device.setClipboardText(text); if (ok) { Ln.i("Device clipboard set"); @@ -292,6 +287,11 @@ public class Controller { device.pressReleaseKeycode(KeyEvent.KEYCODE_PASTE); } + if (sequence != ControlMessage.SEQUENCE_INVALID) { + // Acknowledgement requested + sender.pushAckClipboard(sequence); + } + return ok; } } From bfcb9d06c351e4976cdc3b69a24bf35b8f1bb1c2 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 29 Nov 2021 21:46:29 +0100 Subject: [PATCH 0879/2244] Expose sync mode for injecting events Expose the inject input event mode so that it is possible to wait for the events to be "finished". This will be necessary to read the clipboard content only after the COPY or CUT key event is handled. PR #2834 --- .../com/genymobile/scrcpy/Controller.java | 16 +++++----- .../java/com/genymobile/scrcpy/Device.java | 31 +++++++++++-------- 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index b52d413e..f853c85a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -55,7 +55,7 @@ public class Controller { public void control() throws IOException { // on start, power on the device if (!Device.isScreenOn()) { - device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER); + device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER, Device.INJECT_MODE_ASYNC); // dirty hack // After POWER is injected, the device is powered on asynchronously. @@ -144,7 +144,7 @@ public class Controller { if (keepPowerModeOff && action == KeyEvent.ACTION_UP && (keycode == KeyEvent.KEYCODE_POWER || keycode == KeyEvent.KEYCODE_WAKEUP)) { schedulePowerModeOff(); } - return device.injectKeyEvent(action, keycode, repeat, metaState); + return device.injectKeyEvent(action, keycode, repeat, metaState, Device.INJECT_MODE_ASYNC); } private boolean injectChar(char c) { @@ -155,7 +155,7 @@ public class Controller { return false; } for (KeyEvent event : events) { - if (!device.injectEvent(event)) { + if (!device.injectEvent(event, Device.INJECT_MODE_ASYNC)) { return false; } } @@ -219,7 +219,7 @@ public class Controller { MotionEvent event = MotionEvent .obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0); - return device.injectEvent(event); + return device.injectEvent(event, Device.INJECT_MODE_ASYNC); } private boolean injectScroll(Position position, int hScroll, int vScroll) { @@ -242,7 +242,7 @@ public class Controller { MotionEvent event = MotionEvent .obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, 0, 1f, 1f, DEFAULT_DEVICE_ID, 0, InputDevice.SOURCE_MOUSE, 0); - return device.injectEvent(event); + return device.injectEvent(event, Device.INJECT_MODE_ASYNC); } /** @@ -260,7 +260,7 @@ public class Controller { private boolean pressBackOrTurnScreenOn(int action) { if (Device.isScreenOn()) { - return device.injectKeyEvent(action, KeyEvent.KEYCODE_BACK, 0, 0); + return device.injectKeyEvent(action, KeyEvent.KEYCODE_BACK, 0, 0, Device.INJECT_MODE_ASYNC); } // Screen is off @@ -273,7 +273,7 @@ public class Controller { if (keepPowerModeOff) { schedulePowerModeOff(); } - return device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER); + return device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER, Device.INJECT_MODE_ASYNC); } private boolean setClipboard(String text, boolean paste, long sequence) { @@ -284,7 +284,7 @@ public class Controller { // On Android >= 7, also press the PASTE key if requested if (paste && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && device.supportsInputEvents()) { - device.pressReleaseKeycode(KeyEvent.KEYCODE_PASTE); + device.pressReleaseKeycode(KeyEvent.KEYCODE_PASTE, Device.INJECT_MODE_ASYNC); } if (sequence != ControlMessage.SEQUENCE_INVALID) { diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index 35f4efd4..ba833a06 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -24,6 +24,10 @@ public final class Device { public static final int POWER_MODE_OFF = SurfaceControl.POWER_MODE_OFF; public static final int POWER_MODE_NORMAL = SurfaceControl.POWER_MODE_NORMAL; + public static final int INJECT_MODE_ASYNC = InputManager.INJECT_INPUT_EVENT_MODE_ASYNC; + public static final int INJECT_MODE_WAIT_FOR_RESULT = InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_RESULT; + public static final int INJECT_MODE_WAIT_FOR_FINISH = InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH; + public static final int LOCK_VIDEO_ORIENTATION_UNLOCKED = -1; public static final int LOCK_VIDEO_ORIENTATION_INITIAL = -2; @@ -164,7 +168,7 @@ public final class Device { return supportsInputEvents; } - public static boolean injectEvent(InputEvent inputEvent, int displayId) { + public static boolean injectEvent(InputEvent inputEvent, int displayId, int injectMode) { if (!supportsInputEvents(displayId)) { throw new AssertionError("Could not inject input event if !supportsInputEvents()"); } @@ -173,30 +177,31 @@ public final class Device { return false; } - return SERVICE_MANAGER.getInputManager().injectInputEvent(inputEvent, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC); + return SERVICE_MANAGER.getInputManager().injectInputEvent(inputEvent, injectMode); } - public boolean injectEvent(InputEvent event) { - return injectEvent(event, displayId); + public boolean injectEvent(InputEvent event, int injectMode) { + return injectEvent(event, displayId, injectMode); } - public static boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState, int displayId) { + public static boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState, int displayId, int injectMode) { 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, displayId); + return injectEvent(event, displayId, injectMode); } - public boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState) { - return injectKeyEvent(action, keyCode, repeat, metaState, displayId); + public boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState, int injectMode) { + return injectKeyEvent(action, keyCode, repeat, metaState, displayId, injectMode); } - public static boolean pressReleaseKeycode(int keyCode, int displayId) { - return injectKeyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0, displayId) && injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0, displayId); + public static boolean pressReleaseKeycode(int keyCode, int displayId, int injectMode) { + return injectKeyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0, displayId, injectMode) + && injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0, displayId, injectMode); } - public boolean pressReleaseKeycode(int keyCode) { - return pressReleaseKeycode(keyCode, displayId); + public boolean pressReleaseKeycode(int keyCode, int injectMode) { + return pressReleaseKeycode(keyCode, displayId, injectMode); } public static boolean isScreenOn() { @@ -272,7 +277,7 @@ public final class Device { if (!isScreenOn()) { return true; } - return pressReleaseKeycode(KeyEvent.KEYCODE_POWER, displayId); + return pressReleaseKeycode(KeyEvent.KEYCODE_POWER, displayId, Device.INJECT_MODE_ASYNC); } /** From e2b3968c66ff1310d7644aef92dcb543604e1b6b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 29 Nov 2021 09:30:57 +0100 Subject: [PATCH 0880/2244] Always synchronize clipboard on explicit COPY/CUT If --no-clipboard-autosync is enabled, the automatic clipboard synchronization performed whenever the device clipboard changes is disabled. But on explicit COPY and CUT scrcpy shortcuts (MOD+c and MOD+x), the clipboard should still be synchronized, so that it remains possible to copy-paste from the device to the computer. This is consistent with the behavior of MOD+v, which pastes the computer clipboard to the device. Refs #2228 Refs #2817 PR #2834 --- app/src/control_msg.c | 17 ++++++--- app/src/control_msg.h | 9 +++++ app/src/input_manager.c | 35 +++++++++++-------- app/tests/test_control_msg_serialize.c | 6 +++- .../com/genymobile/scrcpy/ControlMessage.java | 16 +++++++++ .../scrcpy/ControlMessageReader.java | 13 ++++++- .../com/genymobile/scrcpy/Controller.java | 28 ++++++++++++--- .../java/com/genymobile/scrcpy/Server.java | 2 +- .../scrcpy/ControlMessageReaderTest.java | 2 ++ 9 files changed, 102 insertions(+), 26 deletions(-) diff --git a/app/src/control_msg.c b/app/src/control_msg.c index 4cc2f9d7..7fd77631 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -55,6 +55,12 @@ static const char *const screen_power_mode_labels[] = { "suspend", }; +static const char *const copy_key_labels[] = { + "none", + "copy", + "cut", +}; + static void write_position(uint8_t *buf, const struct sc_position *position) { buffer_write32be(&buf[0], position->point.x); @@ -117,6 +123,9 @@ control_msg_serialize(const struct control_msg *msg, unsigned char *buf) { case CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON: buf[1] = msg->inject_keycode.action; return 2; + case CONTROL_MSG_TYPE_GET_CLIPBOARD: + buf[1] = msg->get_clipboard.copy_key; + return 2; case CONTROL_MSG_TYPE_SET_CLIPBOARD: { buffer_write64be(&buf[1], msg->set_clipboard.sequence); buf[9] = !!msg->set_clipboard.paste; @@ -131,7 +140,6 @@ control_msg_serialize(const struct control_msg *msg, unsigned char *buf) { case CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL: case CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL: case CONTROL_MSG_TYPE_COLLAPSE_PANELS: - case CONTROL_MSG_TYPE_GET_CLIPBOARD: case CONTROL_MSG_TYPE_ROTATE_DEVICE: // no additional data return 1; @@ -194,6 +202,10 @@ control_msg_log(const struct control_msg *msg) { LOG_CMSG("back-or-screen-on %s", KEYEVENT_ACTION_LABEL(msg->inject_keycode.action)); break; + case CONTROL_MSG_TYPE_GET_CLIPBOARD: + LOG_CMSG("get clipboard copy_key=%s", + copy_key_labels[msg->get_clipboard.copy_key]); + break; case CONTROL_MSG_TYPE_SET_CLIPBOARD: LOG_CMSG("clipboard %" PRIu64_ " %s \"%s\"", msg->set_clipboard.sequence, @@ -213,9 +225,6 @@ control_msg_log(const struct control_msg *msg) { case CONTROL_MSG_TYPE_COLLAPSE_PANELS: LOG_CMSG("collapse panels"); break; - case CONTROL_MSG_TYPE_GET_CLIPBOARD: - LOG_CMSG("get clipboard"); - break; case CONTROL_MSG_TYPE_ROTATE_DEVICE: LOG_CMSG("rotate device"); break; diff --git a/app/src/control_msg.h b/app/src/control_msg.h index 7352defe..6f1824bb 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -41,6 +41,12 @@ enum screen_power_mode { SCREEN_POWER_MODE_NORMAL = 2, }; +enum get_clipboard_copy_key { + GET_CLIPBOARD_COPY_KEY_NONE, + GET_CLIPBOARD_COPY_KEY_COPY, + GET_CLIPBOARD_COPY_KEY_CUT, +}; + struct control_msg { enum control_msg_type type; union { @@ -69,6 +75,9 @@ struct control_msg { enum android_keyevent_action action; // action for the BACK key // screen may only be turned on on ACTION_DOWN } back_or_screen_on; + struct { + enum get_clipboard_copy_key copy_key; + } get_clipboard; struct { uint64_t sequence; char *text; // owned, to be freed by free() diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 31d6540f..9a553842 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -148,16 +148,6 @@ 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 // If the screen is off, it is turned on only on ACTION_DOWN static void @@ -211,6 +201,21 @@ collapse_panels(struct controller *controller) { } } +static bool +get_device_clipboard(struct controller *controller, + enum get_clipboard_copy_key copy_key) { + struct control_msg msg; + msg.type = CONTROL_MSG_TYPE_GET_CLIPBOARD; + msg.get_clipboard.copy_key = copy_key; + + if (!controller_push_msg(controller, &msg)) { + LOGW("Could not request 'get device clipboard'"); + return false; + } + + return true; +} + static bool set_device_clipboard(struct controller *controller, bool paste, uint64_t sequence) { @@ -450,13 +455,15 @@ input_manager_process_key(struct input_manager *im, } return; case SDLK_c: - if (control && !shift && !repeat) { - action_copy(controller, action); + if (control && !shift && !repeat && down) { + get_device_clipboard(controller, + GET_CLIPBOARD_COPY_KEY_COPY); } return; case SDLK_x: - if (control && !shift && !repeat) { - action_cut(controller, action); + if (control && !shift && !repeat && down) { + get_device_clipboard(controller, + GET_CLIPBOARD_COPY_KEY_CUT); } return; case SDLK_v: diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c index 5cd7056b..6fed2438 100644 --- a/app/tests/test_control_msg_serialize.c +++ b/app/tests/test_control_msg_serialize.c @@ -210,14 +210,18 @@ static void test_serialize_collapse_panels(void) { static void test_serialize_get_clipboard(void) { struct control_msg msg = { .type = CONTROL_MSG_TYPE_GET_CLIPBOARD, + .get_clipboard = { + .copy_key = GET_CLIPBOARD_COPY_KEY_COPY, + }, }; unsigned char buf[CONTROL_MSG_MAX_SIZE]; size_t size = control_msg_serialize(&msg, buf); - assert(size == 1); + assert(size == 2); const unsigned char expected[] = { CONTROL_MSG_TYPE_GET_CLIPBOARD, + GET_CLIPBOARD_COPY_KEY_COPY, }; 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 2cd80191..63ba0fa3 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java @@ -20,6 +20,10 @@ public final class ControlMessage { public static final long SEQUENCE_INVALID = 0; + public static final int COPY_KEY_NONE = 0; + public static final int COPY_KEY_COPY = 1; + public static final int COPY_KEY_CUT = 2; + private int type; private String text; private int metaState; // KeyEvent.META_* @@ -31,6 +35,7 @@ public final class ControlMessage { private Position position; private int hScroll; private int vScroll; + private int copyKey; private boolean paste; private int repeat; private long sequence; @@ -82,6 +87,13 @@ public final class ControlMessage { return msg; } + public static ControlMessage createGetClipboard(int copyKey) { + ControlMessage msg = new ControlMessage(); + msg.type = TYPE_GET_CLIPBOARD; + msg.copyKey = copyKey; + return msg; + } + public static ControlMessage createSetClipboard(long sequence, String text, boolean paste) { ControlMessage msg = new ControlMessage(); msg.type = TYPE_SET_CLIPBOARD; @@ -151,6 +163,10 @@ public final class ControlMessage { return vScroll; } + public int getCopyKey() { + return copyKey; + } + public boolean getPaste() { return paste; } diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java index 80931e94..f09ed26f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java @@ -13,6 +13,7 @@ public class ControlMessageReader { static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20; static final int BACK_OR_SCREEN_ON_LENGTH = 1; static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1; + static final int GET_CLIPBOARD_LENGTH = 1; static final int SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH = 9; private static final int MESSAGE_MAX_SIZE = 1 << 18; // 256k @@ -70,6 +71,9 @@ public class ControlMessageReader { case ControlMessage.TYPE_BACK_OR_SCREEN_ON: msg = parseBackOrScreenOnEvent(); break; + case ControlMessage.TYPE_GET_CLIPBOARD: + msg = parseGetClipboard(); + break; case ControlMessage.TYPE_SET_CLIPBOARD: msg = parseSetClipboard(); break; @@ -79,7 +83,6 @@ public class ControlMessageReader { case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL: case ControlMessage.TYPE_EXPAND_SETTINGS_PANEL: case ControlMessage.TYPE_COLLAPSE_PANELS: - case ControlMessage.TYPE_GET_CLIPBOARD: case ControlMessage.TYPE_ROTATE_DEVICE: msg = ControlMessage.createEmpty(type); break; @@ -162,6 +165,14 @@ public class ControlMessageReader { return ControlMessage.createBackOrScreenOn(action); } + private ControlMessage parseGetClipboard() { + if (buffer.remaining() < GET_CLIPBOARD_LENGTH) { + return null; + } + int copyKey = toUnsigned(buffer.get()); + return ControlMessage.createGetClipboard(copyKey); + } + private ControlMessage parseSetClipboard() { if (buffer.remaining() < SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH) { return null; diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index f853c85a..9246004a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -21,6 +21,7 @@ public class Controller { private final Device device; private final DesktopConnection connection; private final DeviceMessageSender sender; + private final boolean clipboardAutosync; private final KeyCharacterMap charMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD); @@ -31,9 +32,10 @@ public class Controller { private boolean keepPowerModeOff; - public Controller(Device device, DesktopConnection connection) { + public Controller(Device device, DesktopConnection connection, boolean clipboardAutosync) { this.device = device; this.connection = connection; + this.clipboardAutosync = clipboardAutosync; initPointers(); sender = new DeviceMessageSender(connection); } @@ -114,10 +116,7 @@ public class Controller { Device.collapsePanels(); break; case ControlMessage.TYPE_GET_CLIPBOARD: - String clipboardText = Device.getClipboardText(); - if (clipboardText != null) { - sender.pushClipboardText(clipboardText); - } + getClipboard(msg.getCopyKey()); break; case ControlMessage.TYPE_SET_CLIPBOARD: setClipboard(msg.getText(), msg.getPaste(), msg.getSequence()); @@ -276,6 +275,25 @@ public class Controller { return device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER, Device.INJECT_MODE_ASYNC); } + private void getClipboard(int copyKey) { + // On Android >= 7, press the COPY or CUT key if requested + if (copyKey != ControlMessage.COPY_KEY_NONE && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && device.supportsInputEvents()) { + int key = copyKey == ControlMessage.COPY_KEY_COPY ? KeyEvent.KEYCODE_COPY : KeyEvent.KEYCODE_CUT; + // Wait until the event is finished, to ensure that the clipboard text we read just after is the correct one + device.pressReleaseKeycode(key, Device.INJECT_MODE_WAIT_FOR_FINISH); + } + + // If clipboard autosync is enabled, then the device clipboard is synchronized to the computer clipboard whenever it changes, in + // particular when COPY or CUT are injected, so it should not be synchronized twice. On Android < 7, do not synchronize at all rather than + // copying an old clipboard content. + if (!clipboardAutosync) { + String clipboardText = Device.getClipboardText(); + if (clipboardText != null) { + sender.pushClipboardText(clipboardText); + } + } + } + private boolean setClipboard(String text, boolean paste, long sequence) { boolean ok = device.setClipboardText(text); if (ok) { diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 0f0abab0..fc31dada 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -74,7 +74,7 @@ public final class Server { Thread controllerThread = null; Thread deviceMessageSenderThread = null; if (options.getControl()) { - final Controller controller = new Controller(device, connection); + final Controller controller = new Controller(device, connection, options.getClipboardAutosync()); // asynchronous controllerThread = startController(controller); diff --git a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java index 3b0b6c0d..5e79d4f0 100644 --- a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java @@ -219,6 +219,7 @@ public class ControlMessageReaderTest { ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_GET_CLIPBOARD); + dos.writeByte(ControlMessage.COPY_KEY_COPY); byte[] packet = bos.toByteArray(); @@ -226,6 +227,7 @@ public class ControlMessageReaderTest { ControlMessage event = reader.next(); Assert.assertEquals(ControlMessage.TYPE_GET_CLIPBOARD, event.getType()); + Assert.assertEquals(ControlMessage.COPY_KEY_COPY, event.getCopyKey()); } @Test From b25b674c450db3817b529a89079908d75245a300 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 29 Nov 2021 22:03:20 +0100 Subject: [PATCH 0881/2244] Clarify TCP/IP mode in README --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 550f1f7f..4ece2290 100644 --- a/README.md +++ b/README.md @@ -359,7 +359,8 @@ scrcpy --v4l2-buffer=500 # add 500 ms buffering for v4l2 sink #### TCP/IP (wireless) _Scrcpy_ uses `adb` to communicate with the device, and `adb` can [connect] to a -device over TCP/IP. +device over TCP/IP. The device must be connected on the same network as the +computer. ##### Automatic @@ -374,8 +375,8 @@ scrcpy --tcpip=192.168.1.1 # default port is 5555 scrcpy --tcpip=192.168.1.1:5555 ``` -If the device TCP/IP mode is disabled (or if you don't know the IP address), -connect the device over USB, then run: +If adb TCP/IP mode is disabled on the device (or if you don't know the IP +address), connect the device over USB, then run: ```bash scrcpy --tcpip # without arguments From 003e7381064f2c0e8ce1095d1a65522fab4e118b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 29 Nov 2021 22:15:28 +0100 Subject: [PATCH 0882/2244] Bump version to 1.21 --- 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 7a814212..0b4ed174 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('scrcpy', 'c', - version: '1.20', + version: '1.21', meson_version: '>= 0.48', default_options: [ 'c_std=c11', diff --git a/server/build.gradle b/server/build.gradle index 52781a29..1f939a1a 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -6,8 +6,8 @@ android { applicationId "com.genymobile.scrcpy" minSdkVersion 21 targetSdkVersion 31 - versionCode 12000 - versionName "1.20" + versionCode 12100 + versionName "1.21" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index 61b42103..0f86c29f 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.20 +SCRCPY_VERSION_NAME=1.21 PLATFORM_VERSION=31 PLATFORM=${ANDROID_PLATFORM:-$PLATFORM_VERSION} From cb8713eb1fcb8baff914f04952c55411891f86a5 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 29 Nov 2021 22:24:06 +0100 Subject: [PATCH 0883/2244] Update links to v1.21 --- BUILD.md | 6 +++--- README.md | 8 ++++---- install_release.sh | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/BUILD.md b/BUILD.md index 5f473ce2..c9473f27 100644 --- a/BUILD.md +++ b/BUILD.md @@ -270,10 +270,10 @@ install` must be run as root)._ #### Option 2: Use prebuilt server - - [`scrcpy-server-v1.20`][direct-scrcpy-server] - _(SHA-256: b20aee4951f99b060c4a44000ba94de973f9604758ef62beb253b371aad3df34)_ + - [`scrcpy-server-v1.21`][direct-scrcpy-server] + _(SHA-256: dbcccab523ee26796e55ea33652649e4b7af498edae9aa75e4d4d7869c0ab848)_ -[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.20/scrcpy-server-v1.20 +[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.21/scrcpy-server-v1.21 Download the prebuilt server somewhere, and specify its path during the Meson configuration: diff --git a/README.md b/README.md index af381057..d6908bd6 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# scrcpy (v1.20) +# scrcpy (v1.21) scrcpy @@ -101,10 +101,10 @@ process][BUILD_simple]). For Windows, for simplicity, a prebuilt archive with all the dependencies (including `adb`) is available: - - [`scrcpy-win64-v1.20.zip`][direct-win64] - _(SHA-256: 548532b616288bcaeceff6881ad5e6f0928e5ae2b48c380385f03627401cfdba)_ + - [`scrcpy-win64-v1.21.zip`][direct-win64] + _(SHA-256: fdab0c1421353b592a9bbcebd6e252675eadccca65cca8105686feaa9c1ded53)_ -[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.20/scrcpy-win64-v1.20.zip +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.21/scrcpy-win64-v1.21.zip It is also available in [Chocolatey]: diff --git a/install_release.sh b/install_release.sh index e12b4469..2a59a6f1 100755 --- a/install_release.sh +++ b/install_release.sh @@ -2,8 +2,8 @@ set -e BUILDDIR=build-auto -PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v1.20/scrcpy-server-v1.20 -PREBUILT_SERVER_SHA256=b20aee4951f99b060c4a44000ba94de973f9604758ef62beb253b371aad3df34 +PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v1.21/scrcpy-server-v1.21 +PREBUILT_SERVER_SHA256=dbcccab523ee26796e55ea33652649e4b7af498edae9aa75e4d4d7869c0ab848 echo "[scrcpy] Downloading prebuilt server..." wget "$PREBUILT_SERVER_URL" -O scrcpy-server From 86c91e183d1fae21f43172c7d67140cefdc3cc1c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 30 Nov 2021 09:41:47 +0100 Subject: [PATCH 0884/2244] Log CreateProcessW() error code on Windows Refs #2838 --- app/src/sys/win/process.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/sys/win/process.c b/app/src/sys/win/process.c index bed98479..70da9a9a 100644 --- a/app/src/sys/win/process.c +++ b/app/src/sys/win/process.c @@ -154,7 +154,9 @@ sc_process_execute_p(const char *const argv[], HANDLE *handle, unsigned flags, dwCreationFlags, NULL, NULL, &si.StartupInfo, &pi); free(wide); if (!ok) { - if (GetLastError() == ERROR_FILE_NOT_FOUND) { + int err = GetLastError(); + LOGE("CreateProcessW() error %d", err); + if (err == ERROR_FILE_NOT_FOUND) { ret = SC_PROCESS_ERROR_MISSING_BINARY; } goto error_free_attribute_list; From 64a04b8d4a263df1a6eaf5f9794b9e7974d3b591 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 30 Nov 2021 12:22:12 +0100 Subject: [PATCH 0885/2244] Fix process execution on Windows 7 According to this bug report on Firefox: > CreateProcess fails with ERROR_NO_SYSTEM_RESOURCES on Windows 7. It > looks like the reason why is because PROC_THREAD_ATTRIBUTE_HANDLE_LIST > doesn't like console handles. To avoid the problem, do not pass console handles to PROC_THREAD_ATTRIBUTE_HANDLE_LIST. Refs #2783 Refs f801d8b3128cf5aae3725c981f32abfd4b6c307e Fixes #2838 PR #2840 --- app/src/sys/win/process.c | 39 ++++++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/app/src/sys/win/process.c b/app/src/sys/win/process.c index 70da9a9a..6e9da09c 100644 --- a/app/src/sys/win/process.c +++ b/app/src/sys/win/process.c @@ -30,9 +30,7 @@ sc_process_execute_p(const char *const argv[], HANDLE *handle, unsigned flags, bool inherit_stderr = !perr && !(flags & SC_PROCESS_NO_STDERR); // Add 1 per non-NULL pointer - unsigned handle_count = !!pin - + (pout || inherit_stdout) - + (perr || inherit_stderr); + unsigned handle_count = !!pin || !!pout || !!perr; enum sc_process_result ret = SC_PROCESS_ERROR_GENERIC; @@ -81,23 +79,29 @@ sc_process_execute_p(const char *const argv[], HANDLE *handle, unsigned flags, si.StartupInfo.cb = sizeof(si); HANDLE handles[3]; + si.StartupInfo.dwFlags = STARTF_USESTDHANDLES; + if (inherit_stdout) { + si.StartupInfo.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE); + } + if (inherit_stderr) { + si.StartupInfo.hStdError = GetStdHandle(STD_ERROR_HANDLE); + } + LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList = NULL; if (handle_count) { - si.StartupInfo.dwFlags = STARTF_USESTDHANDLES; - unsigned i = 0; if (pin) { si.StartupInfo.hStdInput = stdin_read_handle; handles[i++] = si.StartupInfo.hStdInput; } - if (pout || inherit_stdout) { - si.StartupInfo.hStdOutput = pout ? stdout_write_handle - : GetStdHandle(STD_OUTPUT_HANDLE); + if (pout) { + assert(!inherit_stdout); + si.StartupInfo.hStdOutput = stdout_write_handle; handles[i++] = si.StartupInfo.hStdOutput; } - if (perr || inherit_stderr) { - si.StartupInfo.hStdError = perr ? stderr_write_handle - : GetStdHandle(STD_ERROR_HANDLE); + if (perr) { + assert(!inherit_stderr); + si.StartupInfo.hStdError = stderr_write_handle; handles[i++] = si.StartupInfo.hStdError; } @@ -146,10 +150,15 @@ sc_process_execute_p(const char *const argv[], HANDLE *handle, unsigned flags, goto error_free_attribute_list; } - BOOL bInheritHandles = handle_count > 0; - // DETACHED_PROCESS to disable stdin, stdout and stderr - DWORD dwCreationFlags = handle_count > 0 ? EXTENDED_STARTUPINFO_PRESENT - : DETACHED_PROCESS; + BOOL bInheritHandles = handle_count > 0 || inherit_stdout || inherit_stderr; + DWORD dwCreationFlags = 0; + if (handle_count > 0) { + dwCreationFlags |= EXTENDED_STARTUPINFO_PRESENT; + } + if (!inherit_stdout && !inherit_stderr) { + // DETACHED_PROCESS to disable stdin, stdout and stderr + dwCreationFlags |= DETACHED_PROCESS; + } BOOL ok = CreateProcessW(NULL, wide, NULL, NULL, bInheritHandles, dwCreationFlags, NULL, NULL, &si.StartupInfo, &pi); free(wide); From 5704ec6967c03510dd7d206e5f59c1e4fd6e10a0 Mon Sep 17 00:00:00 2001 From: Archisman Panigrahi Date: Thu, 2 Dec 2021 02:55:45 +0530 Subject: [PATCH 0886/2244] AUR to official Arch Repository PR #2844 Signed-off-by: Romain Vimont --- README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index d6908bd6..201141a8 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,12 @@ On Debian and Ubuntu: apt install scrcpy ``` +On Arch Linux: + +``` +pacman -S scrcpy +``` + A [Snap] package is available: [`scrcpy`][snap-link]. [snap-link]: https://snapstats.org/snaps/scrcpy @@ -82,10 +88,6 @@ 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 -[aur-link]: https://aur.archlinux.org/packages/scrcpy/ For Gentoo, an [Ebuild] is available: [`scrcpy/`][ebuild-link]. From ab00210b37b56f32da1d4e48188e4584c50e638b Mon Sep 17 00:00:00 2001 From: yangfl Date: Thu, 2 Dec 2021 20:49:35 +0100 Subject: [PATCH 0887/2244] Fix script to build without gradle The PLATFORM variable is assigned either from $ANDROID_PLATFORM or gets a default value (currently $PLATFORM_VERSION). The check to use either dx (SDK < 31) or d8 (SDK >= 31) must be based on the actual $PLATFORM, not the default $PLATFORM_VERSION. Refs 52138fd9213216a21fbdcda4d430394d7c8f0979 Refs PR #2850 Signed-off-by: Romain Vimont --- server/build_without_gradle.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index 0f86c29f..c3c6630c 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -57,7 +57,7 @@ javac -bootclasspath "$ANDROID_JAR" -cp "$CLASSES_DIR" -d "$CLASSES_DIR" \ echo "Dexing..." cd "$CLASSES_DIR" -if [[ $PLATFORM_VERSION -lt 31 ]] +if [[ $PLATFORM -lt 31 ]] then # use dx "$ANDROID_HOME/build-tools/$BUILD_TOOLS/dx" --dex \ From a208400133a2a7e7392649b593f46eed87be4d91 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 2 Dec 2021 20:54:23 +0100 Subject: [PATCH 0888/2244] Remove useless intermediate variable Now that PLATFORM is correctly used in the if-condition, PLATFORM_VERSION is useless. PR #2850 --- server/build_without_gradle.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index c3c6630c..ab5e19e4 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -14,8 +14,7 @@ set -e SCRCPY_DEBUG=false SCRCPY_VERSION_NAME=1.21 -PLATFORM_VERSION=31 -PLATFORM=${ANDROID_PLATFORM:-$PLATFORM_VERSION} +PLATFORM=${ANDROID_PLATFORM:-31} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-31.0.0} BUILD_DIR="$(realpath ${BUILD_DIR:-build_manual})" From 65fbec96436151e8ebce27c0d46e98b86e78c1ce Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 3 Dec 2021 21:43:09 +0100 Subject: [PATCH 0889/2244] Mention SCRCPY_ICON_PATH env var in manpage --- app/scrcpy.1 | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index b99c781f..38c92d39 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -419,6 +419,10 @@ Specify the path to adb. .B SCRCPY_SERVER_PATH Specify the path to server binary. +.TP +.B SCRCPY_ICON_PATH +Specify the path to the program icon. + .SH AUTHORS .B scrcpy From 94702a4309c6b25cf87a18c729fda029f8eb6da1 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 4 Dec 2021 09:10:40 +0100 Subject: [PATCH 0890/2244] Fix memset() size in tests The memset() size was 1 byte too long. It was harmless because the last 'a' was overwritten by '\0`. --- app/tests/test_control_msg_serialize.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c index 6fed2438..c7f464c8 100644 --- a/app/tests/test_control_msg_serialize.c +++ b/app/tests/test_control_msg_serialize.c @@ -54,7 +54,7 @@ static void test_serialize_inject_text_long(void) { struct control_msg msg; msg.type = CONTROL_MSG_TYPE_INJECT_TEXT; char text[CONTROL_MSG_INJECT_TEXT_MAX_LENGTH + 1]; - memset(text, 'a', sizeof(text)); + memset(text, 'a', CONTROL_MSG_INJECT_TEXT_MAX_LENGTH); text[CONTROL_MSG_INJECT_TEXT_MAX_LENGTH] = '\0'; msg.inject_text.text = text; From daa06abd34981a09aaecd85307b5719ca34f6034 Mon Sep 17 00:00:00 2001 From: Yu-Chen Lin Date: Sat, 4 Dec 2021 10:54:21 +0800 Subject: [PATCH 0891/2244] Fix comment in control message serialization Refs 245999aec4a4a1454212b38a988aa96fa701bb04 Signed-off-by: Yu-Chen Lin Signed-off-by: Romain Vimont --- app/src/control_msg.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/control_msg.c b/app/src/control_msg.c index 7fd77631..d2957467 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -69,7 +69,7 @@ write_position(uint8_t *buf, const struct sc_position *position) { buffer_write16be(&buf[10], position->screen_size.height); } -// write length (2 bytes) + string (non nul-terminated) +// write length (4 bytes) + string (non null-terminated) static size_t write_string(const char *utf8, size_t max_len, unsigned char *buf) { size_t len = sc_str_utf8_truncation_index(utf8, max_len); From d80bc25ebacc0fe1ba40ab836e03e6590156aecc Mon Sep 17 00:00:00 2001 From: Yu-Chen Lin Date: Sat, 4 Dec 2021 11:11:30 +0800 Subject: [PATCH 0892/2244] Fix overflow in memcpy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In function ‘memcpy’, inlined from ‘control_msg_serialize.constprop’ at ../app/src/control_msg.c:77:5, inlined from ‘run_controller’ at ../app/src/controller.c:69:12: /usr/include/x86_64-linux-gnu/bits/string_fortified.h:34:10: warning: ‘__builtin___memcpy_chk’ writing 262138 bytes into a region of size 262130 overflows the destination [-Wstringop-overflow=] return __builtin___memcpy_chk (__dest, __src, __len, __bos0 (__dest)); Refs 901d8371655582b432d5d92430177a59df8058b9 PR #2859 Signed-off-by: Yu-Chen Lin Signed-off-by: Romain Vimont --- app/src/control_msg.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/control_msg.h b/app/src/control_msg.h index 6f1824bb..7f3235d7 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -14,8 +14,8 @@ #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 -#define CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH (CONTROL_MSG_MAX_SIZE - 6) +// type: 1 byte; sequence: 8 bytes; paste flag: 1 byte; length: 4 bytes +#define CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH (CONTROL_MSG_MAX_SIZE - 14) #define POINTER_ID_MOUSE UINT64_C(-1) #define POINTER_ID_VIRTUAL_FINGER UINT64_C(-2) From ae90ef22db27ca0709ca57a3c08236ccb4cb4673 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 4 Dec 2021 09:08:21 +0100 Subject: [PATCH 0893/2244] Add a unit test for clipboard text length This would have catched the possible memcpy() overflow fixed by the previous commit. Refs #2859 --- app/tests/test_control_msg_serialize.c | 35 ++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c index c7f464c8..42b72b59 100644 --- a/app/tests/test_control_msg_serialize.c +++ b/app/tests/test_control_msg_serialize.c @@ -250,6 +250,40 @@ static void test_serialize_set_clipboard(void) { assert(!memcmp(buf, expected, sizeof(expected))); } +static void test_serialize_set_clipboard_long(void) { + struct control_msg msg = { + .type = CONTROL_MSG_TYPE_SET_CLIPBOARD, + .set_clipboard = { + .sequence = UINT64_C(0x0102030405060708), + .paste = true, + .text = NULL, + }, + }; + + char text[CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH + 1]; + memset(text, 'a', CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH); + text[CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH] = '\0'; + msg.set_clipboard.text = text; + + unsigned char buf[CONTROL_MSG_MAX_SIZE]; + size_t size = control_msg_serialize(&msg, buf); + assert(size == CONTROL_MSG_MAX_SIZE); + + unsigned char expected[CONTROL_MSG_MAX_SIZE] = { + CONTROL_MSG_TYPE_SET_CLIPBOARD, + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, // sequence + 1, // paste + // text length + CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH >> 24, + (CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH >> 16) & 0xff, + (CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH >> 8) & 0xff, + CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH & 0xff, + }; + memset(expected + 14, 'a', CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH); + + assert(!memcmp(buf, expected, sizeof(expected))); +} + static void test_serialize_set_screen_power_mode(void) { struct control_msg msg = { .type = CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE, @@ -299,6 +333,7 @@ int main(int argc, char *argv[]) { test_serialize_collapse_panels(); test_serialize_get_clipboard(); test_serialize_set_clipboard(); + test_serialize_set_clipboard_long(); test_serialize_set_screen_power_mode(); test_serialize_rotate_device(); return 0; From 36c8778d2d83fe74a2de9873db0eabfed77f528c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 4 Dec 2021 09:25:36 +0100 Subject: [PATCH 0894/2244] Add missing comma Thank you clang: ../app/src/control_msg.c:45:5: warning: suspicious concatenation of string literals in an array initialization; did you mean to separate the elements with a comma? [-Wstring-concatenation] "hover-exit", ^ --- app/src/control_msg.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/control_msg.c b/app/src/control_msg.c index d2957467..6ccdc054 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -41,7 +41,7 @@ static const char *const android_motionevent_action_labels[] = { "pointer-up", "hover-move", "scroll", - "hover-enter" + "hover-enter", "hover-exit", "btn-press", "btn-release", From 90cf956f57338e66c983960d767ad6cdf7e7c7a5 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 4 Dec 2021 09:27:59 +0100 Subject: [PATCH 0895/2244] Remove spurious ';' --- app/src/cli.c | 2 +- app/tests/test_cli.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index f0b2e89c..2ecf96f1 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -678,7 +678,7 @@ sc_getopt_adapter_init(struct sc_getopt_adapter *adapter) { } return true; -}; +} static void sc_getopt_adapter_destroy(struct sc_getopt_adapter *adapter) { diff --git a/app/tests/test_cli.c b/app/tests/test_cli.c index 5bc1cc07..a29d5fdd 100644 --- a/app/tests/test_cli.c +++ b/app/tests/test_cli.c @@ -169,4 +169,4 @@ int main(int argc, char *argv[]) { test_options2(); test_parse_shortcut_mods(); return 0; -}; +} From dca2c5f94fd0f35ef1c6583cc59bb9ba1b6bdbd4 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 6 Dec 2021 23:46:01 +0100 Subject: [PATCH 0896/2244] Require SDL >= 2.0.5 Icon loading uses SDL_CreateRGBSurfaceWithFormatFrom(), available since SDL 2.0.5 (in 2016). Refs #2862 --- app/meson.build | 2 +- app/src/compat.h | 9 --------- app/src/scrcpy.c | 2 -- app/src/screen.c | 12 +----------- 4 files changed, 2 insertions(+), 23 deletions(-) diff --git a/app/meson.build b/app/meson.build index 7d9c2b5c..1907812d 100644 --- a/app/meson.build +++ b/app/meson.build @@ -87,7 +87,7 @@ if not get_option('crossbuild_windows') dependency('libavformat'), dependency('libavcodec'), dependency('libavutil'), - dependency('sdl2'), + dependency('sdl2', version: '>= 2.0.5'), ] if v4l2_support diff --git a/app/src/compat.h b/app/src/compat.h index 3ab56049..311d617d 100644 --- a/app/src/compat.h +++ b/app/src/compat.h @@ -35,15 +35,6 @@ # define SCRCPY_LAVF_HAS_AVFORMATCONTEXT_URL #endif -#if SDL_VERSION_ATLEAST(2, 0, 5) -// -# define SCRCPY_SDL_HAS_HINT_MOUSE_FOCUS_CLICKTHROUGH -// -# define SCRCPY_SDL_HAS_GET_DISPLAY_USABLE_BOUNDS -// -# define SCRCPY_SDL_HAS_WINDOW_ALWAYS_ON_TOP -#endif - #if SDL_VERSION_ATLEAST(2, 0, 6) // # define SCRCPY_SDL_HAS_HINT_TOUCH_MOUSE_EVENTS diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 99317ffc..9c2bd03b 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -94,12 +94,10 @@ sdl_set_hints(const char *render_driver) { LOGW("Could not enable linear filtering"); } -#ifdef SCRCPY_SDL_HAS_HINT_MOUSE_FOCUS_CLICKTHROUGH // Handle a click to gain focus as any other click if (!SDL_SetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1")) { LOGW("Could not enable mouse focus clickthrough"); } -#endif #ifdef SCRCPY_SDL_HAS_HINT_TOUCH_MOUSE_EVENTS // Disable synthetic mouse events from touch events diff --git a/app/src/screen.c b/app/src/screen.c index 34a2d5d9..c72fdf44 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -64,12 +64,7 @@ set_window_size(struct screen *screen, struct sc_size new_size) { static bool get_preferred_display_bounds(struct sc_size *bounds) { SDL_Rect rect; -#ifdef SCRCPY_SDL_HAS_GET_DISPLAY_USABLE_BOUNDS -# define GET_DISPLAY_BOUNDS(i, r) SDL_GetDisplayUsableBounds((i), (r)) -#else -# define GET_DISPLAY_BOUNDS(i, r) SDL_GetDisplayBounds((i), (r)) -#endif - if (GET_DISPLAY_BOUNDS(0, &rect)) { + if (SDL_GetDisplayUsableBounds(0, &rect)) { LOGW("Could not get display usable bounds: %s", SDL_GetError()); return false; } @@ -394,12 +389,7 @@ screen_init(struct screen *screen, const struct screen_params *params) { | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI; if (params->always_on_top) { -#ifdef SCRCPY_SDL_HAS_WINDOW_ALWAYS_ON_TOP window_flags |= SDL_WINDOW_ALWAYS_ON_TOP; -#else - LOGW("The 'always on top' flag is not available " - "(compile with SDL >= 2.0.5 to enable it)"); -#endif } if (params->window_borderless) { window_flags |= SDL_WINDOW_BORDERLESS; From 099c54658057f283d0c4c91b160a77efdbeee4f2 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 6 Dec 2021 23:55:29 +0100 Subject: [PATCH 0897/2244] Require libavformat >= 57.33 In ffmpeg/doc/APIchanges: > 2016-04-11 - 6f69f7a / 9200514 - lavf 57.33.100 / 57.5.0 - avformat.h > Add AVStream.codecpar, deprecate AVStream.codec. Refs 5d9e96dc4eaa41b185c41e8a6df6191762764b1c Refs #2862 --- app/meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/meson.build b/app/meson.build index 1907812d..bac8d25e 100644 --- a/app/meson.build +++ b/app/meson.build @@ -84,7 +84,7 @@ if not get_option('crossbuild_windows') # native build dependencies = [ - dependency('libavformat'), + dependency('libavformat', version: '>= 57.33'), dependency('libavcodec'), dependency('libavutil'), dependency('sdl2', version: '>= 2.0.5'), From 80fe12a95fd240e3698966842eef4c06e84fb152 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 7 Dec 2021 00:02:22 +0100 Subject: [PATCH 0898/2244] Require libavcodec >= 57.37 In ffmpeg/doc/APIchanges: > 2016-04-21 - 7fc329e - lavc 57.37.100 - avcodec.h > Add a new audio/video encoding and decoding API with decoupled input > and output -- avcodec_send_packet(), avcodec_receive_frame(), > avcodec_send_frame() and avcodec_receive_packet(). Refs de9b79ec2dd73e5442c6bf0161669bcd8ca7d5be Refs #2862 --- app/meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/meson.build b/app/meson.build index bac8d25e..3916b098 100644 --- a/app/meson.build +++ b/app/meson.build @@ -85,7 +85,7 @@ if not get_option('crossbuild_windows') # native build dependencies = [ dependency('libavformat', version: '>= 57.33'), - dependency('libavcodec'), + dependency('libavcodec', version: '>= 57.37'), dependency('libavutil'), dependency('sdl2', version: '>= 2.0.5'), ] From cabcbc2b151255b49a442a1c43e7db44432eb58e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 6 Dec 2021 21:50:24 +0100 Subject: [PATCH 0899/2244] Do not create control socket if no control If --no-control is enabled, then it is not necessary to create a second communication socket between the client and the server. This also facilitates the use of the server alone (without the client) to receive only the raw video stream. --- app/src/server.c | 33 ++++++++------ .../genymobile/scrcpy/DesktopConnection.java | 45 ++++++++++++------- .../java/com/genymobile/scrcpy/Server.java | 5 ++- 3 files changed, 51 insertions(+), 32 deletions(-) diff --git a/app/src/server.c b/app/src/server.c index 06cb7b72..8453f959 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -388,6 +388,7 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) { assert(tunnel->enabled); const char *serial = server->params.serial; + bool control = server->params.control; sc_socket video_socket = SC_SOCKET_NONE; sc_socket control_socket = SC_SOCKET_NONE; @@ -397,9 +398,12 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) { goto fail; } - control_socket = net_accept_intr(&server->intr, tunnel->server_socket); - if (control_socket == SC_SOCKET_NONE) { - goto fail; + if (control) { + control_socket = + net_accept_intr(&server->intr, tunnel->server_socket); + if (control_socket == SC_SOCKET_NONE) { + goto fail; + } } } else { uint32_t tunnel_host = server->params.tunnel_host; @@ -420,15 +424,18 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) { goto fail; } - // we know that the device is listening, we don't need several attempts - control_socket = net_socket(); - if (control_socket == SC_SOCKET_NONE) { - goto fail; - } - bool ok = net_connect_intr(&server->intr, control_socket, tunnel_host, - tunnel_port); - if (!ok) { - goto fail; + if (control) { + // we know that the device is listening, we don't need several + // attempts + control_socket = net_socket(); + if (control_socket == SC_SOCKET_NONE) { + goto fail; + } + bool ok = net_connect_intr(&server->intr, control_socket, + tunnel_host, tunnel_port); + if (!ok) { + goto fail; + } } } @@ -442,7 +449,7 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) { } assert(video_socket != SC_SOCKET_NONE); - assert(control_socket != SC_SOCKET_NONE); + assert(!control || control_socket != SC_SOCKET_NONE); server->video_socket = video_socket; server->control_socket = control_socket; diff --git a/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java b/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java index 0ec43040..40cb088c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java +++ b/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java @@ -30,8 +30,13 @@ public final class DesktopConnection implements Closeable { private DesktopConnection(LocalSocket videoSocket, LocalSocket controlSocket) throws IOException { this.videoSocket = videoSocket; this.controlSocket = controlSocket; - controlInputStream = controlSocket.getInputStream(); - controlOutputStream = controlSocket.getOutputStream(); + if (controlSocket != null) { + controlInputStream = controlSocket.getInputStream(); + controlOutputStream = controlSocket.getOutputStream(); + } else { + controlInputStream = null; + controlOutputStream = null; + } videoFd = videoSocket.getFileDescriptor(); } @@ -41,31 +46,35 @@ public final class DesktopConnection implements Closeable { return localSocket; } - public static DesktopConnection open(Device device, boolean tunnelForward) throws IOException { + public static DesktopConnection open(Device device, boolean tunnelForward, boolean control) throws IOException { LocalSocket videoSocket; - LocalSocket controlSocket; + LocalSocket controlSocket = null; if (tunnelForward) { LocalServerSocket localServerSocket = new LocalServerSocket(SOCKET_NAME); try { videoSocket = localServerSocket.accept(); // send one byte so the client may read() to detect a connection error videoSocket.getOutputStream().write(0); - try { - controlSocket = localServerSocket.accept(); - } catch (IOException | RuntimeException e) { - videoSocket.close(); - throw e; + if (control) { + try { + controlSocket = localServerSocket.accept(); + } catch (IOException | RuntimeException e) { + videoSocket.close(); + throw e; + } } } finally { localServerSocket.close(); } } else { videoSocket = connect(SOCKET_NAME); - try { - controlSocket = connect(SOCKET_NAME); - } catch (IOException | RuntimeException e) { - videoSocket.close(); - throw e; + if (control) { + try { + controlSocket = connect(SOCKET_NAME); + } catch (IOException | RuntimeException e) { + videoSocket.close(); + throw e; + } } } @@ -79,9 +88,11 @@ public final class DesktopConnection implements Closeable { videoSocket.shutdownInput(); videoSocket.shutdownOutput(); videoSocket.close(); - controlSocket.shutdownInput(); - controlSocket.shutdownOutput(); - controlSocket.close(); + if (controlSocket != null) { + controlSocket.shutdownInput(); + controlSocket.shutdownOutput(); + controlSocket.close(); + } } private void send(String deviceName, int width, int height) throws IOException { diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index fc31dada..4f9575ae 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -66,14 +66,15 @@ public final class Server { Thread initThread = startInitThread(options); boolean tunnelForward = options.isTunnelForward(); + boolean control = options.getControl(); - try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) { + try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward, control)) { ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps(), codecOptions, options.getEncoderName()); Thread controllerThread = null; Thread deviceMessageSenderThread = null; - if (options.getControl()) { + if (control) { final Controller controller = new Controller(device, connection, options.getClipboardAutosync()); // asynchronous From ddb9396743072f97628fab168ef7fcd45a597b03 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 8 Dec 2021 23:20:17 +0100 Subject: [PATCH 0900/2244] Interrupt and close sockets on server stop The sockets were never interrupted or closed by the client since recent changes to run the server from a dedicated thread (see commit 04267085441d6fcd05eff7df0118708f7622e237). As a side effect, the server could never terminate properly (it was waiting on socket blocking calls), so it was always killed by the client after the WATCHDOG_DELAY. Interrupt the sockets on stop to give the servera chance to terminate property, then close them. --- app/src/server.c | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/src/server.c b/app/src/server.c index 8453f959..e89a6f10 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -763,6 +763,17 @@ run_server(void *data) { } sc_mutex_unlock(&server->mutex); + // Interrupt sockets to wake up socket blocking calls on the server + assert(server->video_socket != SC_SOCKET_NONE); + net_interrupt(server->video_socket); + net_close(server->video_socket); + + if (server->control_socket != SC_SOCKET_NONE) { + // There is no control_socket if --no-control is set + net_interrupt(server->control_socket); + net_close(server->control_socket); + } + // Give some delay for the server to terminate properly #define WATCHDOG_DELAY SC_TICK_FROM_SEC(1) sc_tick deadline = sc_tick_now() + WATCHDOG_DELAY; From 682a6911735cb8f6dccd9653ce30b72f267235c6 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 6 Dec 2021 23:33:59 +0100 Subject: [PATCH 0901/2244] Use timers with microsecond precision SDL only provides millisecond precision. Use system timers to get a better precision. --- app/src/util/tick.c | 59 +++++++++++++++++++++++++++++++++++++-------- app/src/util/tick.h | 2 ++ 2 files changed, 51 insertions(+), 10 deletions(-) diff --git a/app/src/util/tick.c b/app/src/util/tick.c index b85ce971..cc0bab5e 100644 --- a/app/src/util/tick.c +++ b/app/src/util/tick.c @@ -1,16 +1,55 @@ #include "tick.h" -#include +#include +#include +#ifdef _WIN32 +# include +#endif sc_tick sc_tick_now(void) { - // SDL_GetTicks() resolution is in milliseconds, but sc_tick are expressed - // in microseconds to store PTS without precision loss. - // - // As an alternative, SDL_GetPerformanceCounter() and - // SDL_GetPerformanceFrequency() could be used, but: - // - the conversions (avoiding overflow) are expansive, since the - // frequency is not known at compile time; - // - in practice, we don't need more precision for now. - return (sc_tick) SDL_GetTicks() * 1000; +#ifndef _WIN32 + // Maximum sc_tick precision (microsecond) + struct timespec ts; + int ret = clock_gettime(CLOCK_MONOTONIC, &ts); + if (ret) { + abort(); + } + + return SC_TICK_FROM_SEC(ts.tv_sec) + SC_TICK_FROM_NS(ts.tv_nsec); +#else + LARGE_INTEGER c; + + // On systems that run Windows XP or later, the function will always + // succeed and will thus never return zero. + // + // + + BOOL ok = QueryPerformanceCounter(&c); + assert(ok); + (void) ok; + + LONGLONG counter = c.QuadPart; + + static LONGLONG frequency; + if (!frequency) { + // Initialize on first call + LARGE_INTEGER f; + ok = QueryPerformanceFrequency(&f); + assert(ok); + frequency = f.QuadPart; + assert(frequency); + } + + if (frequency % SC_TICK_FREQ == 0) { + // Expected case (typically frequency = 10000000, i.e. 100ns precision) + sc_tick div = frequency / SC_TICK_FREQ; + return SC_TICK_FROM_US(counter / div); + } + + // Split the division to avoid overflow + sc_tick secs = SC_TICK_FROM_SEC(counter / frequency); + sc_tick subsec = SC_TICK_FREQ * (counter % frequency) / frequency; + return secs + subsec; +#endif } diff --git a/app/src/util/tick.h b/app/src/util/tick.h index 47d02529..2d941f23 100644 --- a/app/src/util/tick.h +++ b/app/src/util/tick.h @@ -10,9 +10,11 @@ typedef int64_t sc_tick; #define SC_TICK_FREQ 1000000 // microsecond // To be adapted if SC_TICK_FREQ changes +#define SC_TICK_TO_NS(tick) ((tick) * 1000) #define SC_TICK_TO_US(tick) (tick) #define SC_TICK_TO_MS(tick) ((tick) / 1000) #define SC_TICK_TO_SEC(tick) ((tick) / 1000000) +#define SC_TICK_FROM_NS(ns) ((ns) / 1000) #define SC_TICK_FROM_US(us) (us) #define SC_TICK_FROM_MS(ms) ((ms) * 1000) #define SC_TICK_FROM_SEC(sec) ((sec) * 1000000) From 09c55b0f93f1108d3829e9e4b22670b4ae685280 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 7 Dec 2021 13:59:20 +0100 Subject: [PATCH 0902/2244] Set "low delay" decoder flag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I don't really know the concrete benefits, but scrcpy definitely wants low delay decoding. Suggested-by: François Cartegnie --- app/src/decoder.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/decoder.c b/app/src/decoder.c index 7107e01d..a20986dd 100644 --- a/app/src/decoder.c +++ b/app/src/decoder.c @@ -46,6 +46,8 @@ decoder_open(struct decoder *decoder, const AVCodec *codec) { return false; } + decoder->codec_ctx->flags |= AV_CODEC_FLAG_LOW_DELAY; + if (avcodec_open2(decoder->codec_ctx, codec, NULL) < 0) { LOGE("Could not open codec"); avcodec_free_context(&decoder->codec_ctx); From 3ada5c51bc25cf4175d73ad69e4750ae7a81fe7e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 9 Dec 2021 21:32:11 +0100 Subject: [PATCH 0903/2244] Rename scrcpy threads Prefix the name of threads by "scrcpy-". This improves readability in the output of `top -H` for example. Limit the thread names to 16 bytes, because it is limited on some platforms. --- app/src/aoa_hid.c | 2 +- app/src/controller.c | 2 +- app/src/file_handler.c | 2 +- app/src/fps_counter.c | 2 +- app/src/receiver.c | 4 ++-- app/src/recorder.c | 4 ++-- app/src/server.c | 3 ++- app/src/stream.c | 3 ++- app/src/util/process.c | 2 +- app/src/util/thread.c | 4 ++++ app/src/v4l2_sink.c | 2 +- app/src/video_buffer.c | 2 +- 12 files changed, 19 insertions(+), 13 deletions(-) diff --git a/app/src/aoa_hid.c b/app/src/aoa_hid.c index a3fc3bd4..abf74cf9 100644 --- a/app/src/aoa_hid.c +++ b/app/src/aoa_hid.c @@ -373,7 +373,7 @@ bool sc_aoa_start(struct sc_aoa *aoa) { LOGD("Starting AOA thread"); - bool ok = sc_thread_create(&aoa->thread, run_aoa_thread, "aoa_thread", aoa); + bool ok = sc_thread_create(&aoa->thread, run_aoa_thread, "scrcpy-aoa", aoa); if (!ok) { LOGC("Could not start AOA thread"); return false; diff --git a/app/src/controller.c b/app/src/controller.c index 6cf3d20e..10eceaf2 100644 --- a/app/src/controller.c +++ b/app/src/controller.c @@ -110,7 +110,7 @@ controller_start(struct controller *controller) { LOGD("Starting controller thread"); bool ok = sc_thread_create(&controller->thread, run_controller, - "controller", controller); + "scrcpy-ctl", controller); if (!ok) { LOGC("Could not start controller thread"); return false; diff --git a/app/src/file_handler.c b/app/src/file_handler.c index addbb9a5..95d230ae 100644 --- a/app/src/file_handler.c +++ b/app/src/file_handler.c @@ -154,7 +154,7 @@ file_handler_start(struct file_handler *file_handler) { LOGD("Starting file_handler thread"); bool ok = sc_thread_create(&file_handler->thread, run_file_handler, - "file_handler", file_handler); + "scrcpy-file", file_handler); if (!ok) { LOGC("Could not start file_handler thread"); return false; diff --git a/app/src/fps_counter.c b/app/src/fps_counter.c index c92d4140..25ee00eb 100644 --- a/app/src/fps_counter.c +++ b/app/src/fps_counter.c @@ -108,7 +108,7 @@ fps_counter_start(struct fps_counter *counter) { // same thread, no need to lock if (!counter->thread_started) { bool ok = sc_thread_create(&counter->thread, run_fps_counter, - "fps counter", counter); + "scrcpy-fps", counter); if (!ok) { LOGE("Could not start FPS counter thread"); return false; diff --git a/app/src/receiver.c b/app/src/receiver.c index eeb206f1..1e25536e 100644 --- a/app/src/receiver.c +++ b/app/src/receiver.c @@ -111,8 +111,8 @@ bool receiver_start(struct receiver *receiver) { LOGD("Starting receiver thread"); - bool ok = sc_thread_create(&receiver->thread, run_receiver, "receiver", - receiver); + bool ok = sc_thread_create(&receiver->thread, run_receiver, + "scrcpy-receiver", receiver); if (!ok) { LOGC("Could not start receiver thread"); return false; diff --git a/app/src/recorder.c b/app/src/recorder.c index 4364b9a5..74cfce07 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -287,8 +287,8 @@ recorder_open(struct recorder *recorder, const AVCodec *input_codec) { } LOGD("Starting recorder thread"); - ok = sc_thread_create(&recorder->thread, run_recorder, "recorder", - recorder); + ok = sc_thread_create(&recorder->thread, run_recorder, "scrcpy-recorder", + recorder); if (!ok) { LOGC("Could not start recorder thread"); goto error_avio_close; diff --git a/app/src/server.c b/app/src/server.c index e89a6f10..3b7a0fcc 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -804,7 +804,8 @@ error_connection_failed: bool sc_server_start(struct sc_server *server) { - bool ok = sc_thread_create(&server->thread, run_server, "server", server); + bool ok = + sc_thread_create(&server->thread, run_server, "scrcpy-server", server); if (!ok) { LOGE("Could not create server thread"); return false; diff --git a/app/src/stream.c b/app/src/stream.c index 3ac0f5e1..f8d73a27 100644 --- a/app/src/stream.c +++ b/app/src/stream.c @@ -284,7 +284,8 @@ bool stream_start(struct stream *stream) { LOGD("Starting stream thread"); - bool ok = sc_thread_create(&stream->thread, run_stream, "stream", stream); + bool ok = + sc_thread_create(&stream->thread, run_stream, "scrcpy-stream", stream); if (!ok) { LOGC("Could not start stream thread"); return false; diff --git a/app/src/util/process.c b/app/src/util/process.c index ad1af0a9..9c4dcd9f 100644 --- a/app/src/util/process.c +++ b/app/src/util/process.c @@ -64,7 +64,7 @@ sc_process_observer_init(struct sc_process_observer *observer, sc_pid pid, observer->listener_userdata = listener_userdata; observer->terminated = false; - ok = sc_thread_create(&observer->thread, run_observer, "process_observer", + ok = sc_thread_create(&observer->thread, run_observer, "scrcpy-proc", observer); if (!ok) { sc_cond_destroy(&observer->cond_terminated); diff --git a/app/src/util/thread.c b/app/src/util/thread.c index 23eddf1d..c6e6b81e 100644 --- a/app/src/util/thread.c +++ b/app/src/util/thread.c @@ -8,6 +8,10 @@ bool sc_thread_create(sc_thread *thread, sc_thread_fn fn, const char *name, void *userdata) { + // The thread name length is limited on some systems. Never use a name + // longer than 16 bytes (including the final '\0') + assert(strlen(name) <= 15); + SDL_Thread *sdl_thread = SDL_CreateThread(fn, name, userdata); if (!sdl_thread) { LOG_OOM(); diff --git a/app/src/v4l2_sink.c b/app/src/v4l2_sink.c index dce11ce1..7675fd92 100644 --- a/app/src/v4l2_sink.c +++ b/app/src/v4l2_sink.c @@ -272,7 +272,7 @@ sc_v4l2_sink_open(struct sc_v4l2_sink *vs) { vs->stopped = false; LOGD("Starting v4l2 thread"); - ok = sc_thread_create(&vs->thread, run_v4l2_sink, "v4l2", vs); + ok = sc_thread_create(&vs->thread, run_v4l2_sink, "scrcpy-v4l2", vs); if (!ok) { LOGC("Could not start v4l2 thread"); goto error_av_packet_free; diff --git a/app/src/video_buffer.c b/app/src/video_buffer.c index 12e66cf1..11f76479 100644 --- a/app/src/video_buffer.c +++ b/app/src/video_buffer.c @@ -170,7 +170,7 @@ bool sc_video_buffer_start(struct sc_video_buffer *vb) { if (vb->buffering_time) { bool ok = - sc_thread_create(&vb->b.thread, run_buffering, "buffering", vb); + sc_thread_create(&vb->b.thread, run_buffering, "scrcpy-vbuf", vb); if (!ok) { LOGE("Could not start buffering thread"); return false; From b5d4ec61fcb82c88b7e25ad78651e070194c33f0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 9 Dec 2021 21:43:54 +0100 Subject: [PATCH 0904/2244] Move newline generation in help If we removed the shortcuts intro, we would not need the additional '\n' of the section title, so it should be printed along with the shortcuts intro. --- app/src/cli.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index 2ecf96f1..2281c255 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -776,7 +776,7 @@ print_shortcuts_intro(unsigned cols) { return; } - printf("%s\n", intro); + printf("\n%s\n", intro); free(intro); } @@ -831,7 +831,7 @@ scrcpy_print_usage(const char *arg0) { } // Print shortcuts section - printf("\nShortcuts:\n\n"); + printf("\nShortcuts:\n"); print_shortcuts_intro(cols); for (size_t i = 0; i < ARRAY_LEN(shortcuts); ++i) { print_shortcut(&shortcuts[i], cols); From f0361fc8b374c1827cedcabf3ec011ac78d1f72c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 9 Dec 2021 21:45:39 +0100 Subject: [PATCH 0905/2244] Add environment variables in help Print the list of environment variables used by scrcpy in --help. --- app/src/cli.c | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/app/src/cli.c b/app/src/cli.c index 2281c255..f1f50049 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -71,6 +71,11 @@ struct sc_shortcut { const char *text; }; +struct sc_envvar { + const char *name; + const char *text; +}; + struct sc_getopt_adapter { char *optstring; struct option *longopts; @@ -585,6 +590,21 @@ static const struct sc_shortcut shortcuts[] = { }, }; +static const struct sc_envvar envvars[] = { + { + .name = "ADB", + .text = "Path to adb executable", + }, + { + .name = "SCRCPY_ICON_PATH", + .text = "Path to the program icon", + }, + { + .name = "SCRCPY_SERVER_PATH", + .text = "Path to the server binary", + } +}; + static char * sc_getopt_adapter_create_optstring(void) { struct sc_strbuf buf; @@ -804,6 +824,23 @@ print_shortcut(const struct sc_shortcut *shortcut, unsigned cols) { free(text); } +static void +print_envvar(const struct sc_envvar *envvar, unsigned cols) { + assert(cols > 8); // sc_str_wrap_lines() requires indent < columns + assert(envvar->name); + assert(envvar->text); + + printf("\n %s\n", envvar->name); + char *text = sc_str_wrap_lines(envvar->text, cols, 8); + if (!text) { + printf("\n"); + return; + } + + printf("%s\n", text); + free(text); +} + void scrcpy_print_usage(const char *arg0) { #define SC_TERM_COLS_DEFAULT 80 @@ -836,6 +873,12 @@ scrcpy_print_usage(const char *arg0) { for (size_t i = 0; i < ARRAY_LEN(shortcuts); ++i) { print_shortcut(&shortcuts[i], cols); } + + // Print environment variables section + printf("\nEnvironment variables:\n"); + for (size_t i = 0; i < ARRAY_LEN(envvars); ++i) { + print_envvar(&envvars[i], cols); + } } static bool From 878ffffc36393bc544f4a8cb5a3345d6aab52a02 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 9 Dec 2021 21:47:05 +0100 Subject: [PATCH 0906/2244] Update environment variables section in manpage Use the same content as the section printed with --help. --- app/scrcpy.1 | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 38c92d39..74d3957f 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -413,15 +413,15 @@ Push file to device (see \fB\-\-push\-target\fR) .TP .B ADB -Specify the path to adb. - -.TP -.B SCRCPY_SERVER_PATH -Specify the path to server binary. +Path to adb. .TP .B SCRCPY_ICON_PATH -Specify the path to the program icon. +Path to the program icon. + +.TP +.B SCRCPY_SERVER_PATH +Path to the server binary. .SH AUTHORS From 2cb4e04209e316ff849d4b48cbac1f38dbfc0035 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 9 Dec 2021 23:36:10 +0100 Subject: [PATCH 0907/2244] Update copyright date to 2021 in manpage December, it's time to update to 2021! --- app/scrcpy.1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 74d3957f..7a6c5134 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -446,7 +446,7 @@ Copyright \(co 2018 Genymobile Genymobile .UE -Copyright \(co 2018\-2020 +Copyright \(co 2018\-2021 .MT rom@rom1v.com Romain Vimont .ME From cfcbc2ac218612b2e9d2322ceb98ec99d7cd9381 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 9 Dec 2021 22:58:31 +0100 Subject: [PATCH 0908/2244] Add icon to scrcpy.exe The icon will be associated to scrcpy.exe in the Windows explorer. The .ico was created using imagemagick: convert icon.png icon.ico It is included as a binary for simplicity. Refs #2815 --- app/meson.build | 2 ++ app/scrcpy-windows.rc | 1 + cross_win32.txt | 1 + cross_win64.txt | 1 + data/icon.ico | Bin 0 -> 8840 bytes 5 files changed, 5 insertions(+) create mode 100644 app/scrcpy-windows.rc create mode 100644 data/icon.ico diff --git a/app/meson.build b/app/meson.build index 3916b098..4c0e909d 100644 --- a/app/meson.build +++ b/app/meson.build @@ -49,9 +49,11 @@ conf.set('_XOPEN_SOURCE', '700') conf.set('_GNU_SOURCE', true) if host_machine.system() == 'windows' + windows = import('windows') src += [ 'src/sys/win/file.c', 'src/sys/win/process.c', + windows.compile_resources('scrcpy-windows.rc'), ] conf.set('_WIN32_WINNT', '0x0600') conf.set('WINVER', '0x0600') diff --git a/app/scrcpy-windows.rc b/app/scrcpy-windows.rc new file mode 100644 index 00000000..0dadc84c --- /dev/null +++ b/app/scrcpy-windows.rc @@ -0,0 +1 @@ +0 ICON "../data/icon.ico" diff --git a/cross_win32.txt b/cross_win32.txt index 4db17be7..41ec3078 100644 --- a/cross_win32.txt +++ b/cross_win32.txt @@ -7,6 +7,7 @@ cpp = 'i686-w64-mingw32-g++' ar = 'i686-w64-mingw32-ar' strip = 'i686-w64-mingw32-strip' pkgconfig = 'i686-w64-mingw32-pkg-config' +windres = 'i686-w64-mingw32-windres' [host_machine] system = 'windows' diff --git a/cross_win64.txt b/cross_win64.txt index d03f0272..2565330b 100644 --- a/cross_win64.txt +++ b/cross_win64.txt @@ -7,6 +7,7 @@ cpp = 'x86_64-w64-mingw32-g++' ar = 'x86_64-w64-mingw32-ar' strip = 'x86_64-w64-mingw32-strip' pkgconfig = 'x86_64-w64-mingw32-pkg-config' +windres = 'x86_64-w64-mingw32-windres' [host_machine] system = 'windows' diff --git a/data/icon.ico b/data/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..b3238778fdb225a8df62c1334afb89d86f66d4a1 GIT binary patch literal 8840 zcmZQzU}Ruq00Bk@1%@Ie1_m((28PZ6KX+a(DJ}*E23}7OmmrWT5awWGU|@(TT9L-U zpk(Rk;uunK>+Rjj2`b@pfOKgRsWhbb6&NH@e79x6DvW5!%4~qTEe#- zOxtL7AmN~!Ma0^&y-`Wxk`H?$%$Txyw~43lH>T<+TRL3gSgVkbrm`^m#ZHs*`|fw$ zy0K1yX61gsyle0j!_}! zX&pzQde{Za-G4vTSNt)b_V(Z^-tBgO3RDEHxl2S$5B;rhh{ka0)qnsLNnU<<`x&6TdT#SXxeYcHqp?dsbj(U_J$|S(N&SpUVJqC zsm<{3&ZNK5pH|v^AZzO?Z@)z zw>~VEf4S`a`7Zv8o0q=5aOh-yLEp|8v6W8q{OX+69eyCQ(n)Uf&77Y#i4MW<1Aga* zy*WRLf#GvKgGT7j^v#kYre{|@cJ0vC+|{--MvVKqkDli_PKIL@>I^EMEi>8@uNo>H zxf&AdZWN#OJK15m=MiTCcLb^TR(gdcvQ#g+qx(-VUB6^#@g;YMhkCo?rCxo zPJ5JMbr^c0FD2xAGHA?V3pL++aFT$3P^oN}>)VWJEDn0}g>`L}S}J-%pHER`T#z+O z_iztqq4w%?n%+zf<~&>sPChADEmJ)i0=~yNok_iFnR+&u!Kp(zfI;9Xa#`Q?V5qE}7ZH+;RO zwSKD^ZuOvxAwq>-{0bR-sErIed9q1 zfoIWH7cPZvU-$jZuC?qR`#SErs*81&yKH#9ZC>f#ifLO^-FES z3;pK0P0VKS;_WcMW6t1M^z`9J5l5kd!?nya_Rm;+!gNmX^83Y~)-Ne$4&TD^?28V= z0r{e*TQALJTp(I1va`)lcjL3~FYX*jEfr&X&cLwKu0xi6_uGFODa~avZ`xwwcO5mh zIG$SiZI^h<(qmgiKD9l%#h1K}r{RhZ-{dH+nz`?Tck~t7M%LNxSkBwK_mkCjr!3dx zauMd;(WloR|EeOg?_c__$5C7_Gt0|Fic=YS=6Wft7T2!I4?3@$d}jZ2#^3KG8=eTi z{pXP~S7m-jt-OrR_Fw5eN|UNm)e8>>6n1+;$E0rqM!ax2#zQtwdEQz(; z3BTW#Xg@Hyo&Vl#iyp&*kgxLBCyC~3m#VcV1+D$@ck7v{j0_CI>(4l@|FF;ZF`L?! zke^=(1KWVg%mqB-N@*{a6 z(f#(BPbQjiEfwdn_;9f9dKp`x@JaoI|9k%)s99ZqOG6euT#Hv+EOS8r^1;_-C*B)t z$7kIM5tVH_w@{j`q}Ta$X22(@u1ekquWP3z&3|^!>Pv3E=6<>BlP+BQcp_gj*3jVB zp`OKd^{O%Q4BXq9WXsJ%zWkdK=x|13$}Vo>g!{I&KYo6{w`!&0qQ({crMihnW*wgJ znu+5SBTK5hlb}44BWF)&*u}>WG@gY%*{sDS$=bT5%V$x;lMud>D`mP~-pZ7i{732A z-P_yOyqa&Mwf?W~pF?*;|Ff>+XJGK?OHugVuE5`-RrP}{rrU(V<29ci-@o4ITk_g3O??~1@7fDrU^>szFz>qI z$4@#|?{ZhY_{*()JSmAIf#>(TcZbjG?`Kub|0f=k$G5Py*!C8i)P*ZDjysiiGrKHu zbZKjTA)Ci$ar{mR-{bdc23193Cp|w)U-{$6X@18zz|Kw6cK^mz|GhagEWd~U6|YcZ zU{I*kYJcG%xAEYqJl11hMGiSJ2nh>t{NDIFK)=tv`r4FDu2M}mwrrMs>&)A6_4>Kc zI%f&zJ(f)s#Vm)-%arEoBz(D|b8f$7>zkOT>Mw(KzkB1V@rpU%)qcZu%eC%>{fd!0 zdHLRoU-}KXWS#TVrS_&TE?BlV25pUCXMZkp>`w+~;=ZRov+{LB6dY1` zgj_s)8u~jY%;4Z;`s33Sk+^tKjQEjh%(6E&?UywcmYid6*n2wPKm1PO>RN-od24&0 zUYB0|OM2d_U-}<3mY>GTQaAtF1wl&+tu5Bqf@TaiPI{nU#c*F3Z`>mKzme4hHBhjZW7_Y(|F zww=t#nY{7!HMtl&<7VUI$0o)dKXc~af)?p__d@v2F4?!_aTx2d_*t#*7Owiwbe@%A zL&Vk9(yMa*?Jv~r?0C?A#Dzu4?#<54;_mZz{#)x1o6a&jKhM29Klx|j)hJOO(a1QL za~i%8iEFCYEUq-kb9l_PDPoR&JcDz<2gM_QIymZDSnY2}*)2J*zbHSN`&V1EL8W1* z@cw;GH@#oJ-j{#gcD6vrkJC?YdbH1fvQj2!`Q5t@pJ&-fGB~Wr>VtQ&OI=7;>^(hwZ-u~(fO}jil5&)E}U`4;g*I%uE+%AF0*;>vhCF@ zogMkO(!K=kYi(snQRIl^a7emtd5$OJ9TD(W*Y_V1E+H-y};KRdeW z|BPuXECj@*+V`H?%(&shj^(=#MK|^Yx;UL*7^kCaw&{6aoKL*Zq8(4d6z)kL?7Llc zt>%$-jP~3wRg9H!)^l^T4l<_nDs6HTe9Yir{`BfclXgu*? zw0``7H?2ok-egkbtbYGOd%xd{okt(u7L~9z;?>yxaHom2mCZ6f!HGa?(F-?L*;>R$WL+dX$(c#%r? zm&kIzFB`+=@i8z+{68vlDAv*A;;B4Vx2-Bw&+=KV+NBCEA2(mTcKbWq)4pq`n1`tJ z$OKj0yY{=%T4YC;p>8LqZtDyQr@aFE(v*{aFRR*OCvo`5QtBYJ>8^=LKVH_E)^Zj-jEK|M#3LLD%OND6M?{ah*|X_p3K; z?;`K6IuP2E#W^Qb&2V}8!S#YYQr8W4PF(fBlgCK>%*&1^B_a%;)#Bzg&sz7!vf$-P zhor0fy8k%i^YU3@ zw5nG&9I=kP^1dVN%Rf(D-pFb52p|hoHFfj4SKzd%nmLdY>|Jr$^}j zr5X%jGb&UYJ^Tgs-T1}d%h-2XYpYJQVz$QA_@9g`G7^=rKgO~AYx(eJGf+kF3DYi1yP`LDV5@(o>1LKCSU1t+ZYL`g*6rBr+Nh$S7 zda-9c=hxfX)terdi7;sA>{`gW+x|*;T*xE~d0~mNyLX!x&c5|l&+F{M;|<rxLMe78o<_6srYd&usMlL&l~2rKyUOs4_s)$r+wI?`wZGr?`_*;p zO#YRQJPz&a_x-8*efI8+8t?YIM^{OTFfdeo-;(fs?dj`(KRdSc2eO`gS@U`FOuem- zpXq&hpTBu`hUvl2j2Cw9W&Hi_O|+T&{A0Xx!j~_+@Idx>@Q%LEufBZxDAAL6{-E5K zH|uiDG|WGiFqq^{Dts`nF;#uP)&JIo6RY1XT=UI3mFM@nH_opU_TD|Zd1^YdLmuD5 zWxvh!XYX--c&mE7*bz~aya!EIn{`-b94L6)&K`JQZrkpPZ68w@ekj}B`P6T&9~~0> zP)cuwvFQ1={#o<)d2x8y37XgE7&BcgU6cJlX&yI&W5SM?$;}-u;??adW0p_8uPo-2 zkontBL+HTS{o9}J`fRal;&Z!?=XO_Lyt2>2cl$Bkm?e|%2a7p5L>H~TenINsyXq?Q z4Wa3uLKsTkXB;_j?%Jp9=Hi&3^yxaCu6vxc+Ycmt_@0~Z`2L;P?(VSAd7KRy&H~l% z-$Z}g?0rjz>&or(n?=?=-ek2qhw0MY?cWuyJ(+vZc^(IYn!%g@ClplfKYFpxP`C3A zTiOi8cN3m#xm|Vnx%G+g$9?^Lj$bO1RaU4oCH!8PXR<|S-9O{S=@TyO-6^3`xqeFE zfi2rC7=3i)j(uyMTPD6*E9U-n(|fwf8Q+t(pYI90bK%G&Z9@ijr&a}Po&NKzp{EO@5MH361uyO?T)DAvEPTAUrc(GA)vfeIMn~~*FUZ@ zC+8m7_{Z|{w9uKCE^}}Qr7R5jD}U#1-$mo%8Fu;i%9o1mv|?cKJg~1?to>Wo=V0I2 zsqq`c<1_b2-)4F*8*@qP=mo8#8|=4m*54>#|L6Z}`;XZ-m1kVbT(vPfQ!LfyN&(0B z)4LVdNc3IRaJ!)4w&CdgHvg-8`SZosKj@tK{A1L^Ce7b+p~5ddn@<(fPd)Ijn*GC{ z@Aq1k?fA|)adkdpoBf5>2%C_)T32re^OuG9?bq%Wy#Myc-%$U5Q&|jZeC~BG-}TS5 zjBQWg)-^}YIf&1D^(*|xiapP5)&FOeu~>afIo|xE`G;fhxBUjIKRYc>h~P{3u`O>m zfA`WDX@*kW#GHG(BfieR612?p*Wbj$vOlsO@6G--N$;wP+sFCVa!Ujvj;>1(pZDfR ztx4DG**n`Af7JX@kjt`2@v`iSiQe|;!-IEbSGI?4tJ)&G!;GO!_LXk<-_LDLGgViA z%Q>?5;@cn7zitov^N(Tjhqc@M*ZuB(mv(khPySZBfK^rDFE)RbKVUmCciEJ5zw=YC z=cn-8sJ_1Jwch7jRr`Z1bo9TnP%I%uBvjmiXsBiqcEXVsf+p(=FKfiv9-ZlNJav}G1 z)&mzB!&(+GGB}(IShw@ppQ^jkHxqAdV==fRD!K00$Cak#?%USx)_odvW*0Am*I#Ba zF*RPd8i`#OYd3T3)MBV%6m16#< zj6rtN&f^V?FDAV5y*=+#sn6@B=Nu;aADI??=PS!`Q^p7UUJ`dArA{s_lHIIUz9UoM z?0nOh#l}*}vrG7l!H{Fl96-Sin1J{}m78g-d;2EA2{nxN*YF zOKp{Jh5e?Qo{8ULocrm07xUA7jEnpa9Qb`_^MMX+CWlYm5;uaSp3gU*Qgpa?=CYE{ zzgzr8gQf3x9QM^X*_)rz>UsCi<5`AXC(f~-KCmwA2h+O>r3YOMH@0kFYP0|TrMQ^1 zT_JN0RwSOgk}EgYHuvYom8I*?%`+{n7H(x(C$d(uV(W*ypMJW2d0Tk=h~heqhJCV2 zWMRkf>-qb?m&5d*os`gfy6oeXpVQ6v zXwH@Xam?D@Xz#*HofZwp{r{~0wE0`_kKe`m51qq!34}P3mIseiAIe#lNXFKNWxoy01$gtwmne!jEFP|4}=J-5nuT?c$yv48W0X)1u z2?Bqv*)7*E5Vdq>Om=wBm%d*A#S4ZPl6t)tjIvj}H|6AUT4Hi6!SCdQgYmYF|G&NK zzRV}~aw-+D0Wc#1H z;zRiTV{?LbX>ng

8s1Wo~@`;+pE&o839Bws{A~dP{6PCSbBoIr(7m|EnIni7#GW z+9%?CI5}Vb*!}s{od1&^_}#g4seaJNb6nbyQS~6epjX^XlwI#tWu*I2M>xI&YP_>|)7b_@;ecw9Q2Q6!niU zX8S+%zyC-6MW@L^)d>EZ;)i?;d%bUFp3z!uKl8)ZQ=vP9u3D*aeb{;;;F#F@UzK}1 z)X)Fn@t^loQtFSR8N7r>*-_a=*Tmsd>M3 z-+^Ur91FAUU~ zwn%(*=D6<0F4GsJf8|)2WRuE)4I894iOPIG+f(kU@Y7|uqJ@tsv}8Z`ekpcF z-!W^&-}M=%G#1u7-+e2`Tea##@Afv_sZ8Kel=mHRr10i9p7H>oRWhqOIRd zpWv0s%epZweXYuy6$}%kov!@y&kCC+E|?t?@9(zTQjg(NTkGXI!A*>n&aHPG7!zhr zc;kNYRONq;n+&bl9IkUsPft!}Iv_5P_(N-^LDz&=fv$H0z1uC-7?esGMP9t}&x&bW z-EnNL@#V?MObRjL3y#+>c_zBEZNZy4hKhZh42~%pK@1b5mKX4<=@bT-_Hi~OIW`A9 zy;;u@xnAk|1b^%2fm@TA6f9@9FAA1sSebitQQMOx#nWobeHb(>MQ@%y=Nb0M_vK|v zcVDhYGZ+{eR>rH#l-9fMan+ZYVU>0E)0eQq5QZn}B0cgmSxeazwjK+x)U&td)qT9C zE$jK0h#Au>)SDm8>H6l(%VA=%`nqW0@;gtJj>XMsoK$k{B)^3L1A~Hn)RCRbj|sly zQ!o>8+;EkDzK-Ju%WNaIM?O^t8zxS5}znWxG4~_BQ_6dt=?VGPWE(H!G*H!e7PdGh>9B2$!Nj z!L=#gN30J$H272Zyo8xeMvQ?$p!=u*mu2=D(?auO5{oWsEDC&nmg~(9H-}>qDUog( zB2ISm-4_NhEI-G1gsn+IL4rp~gv(LnXm9=nwm$BLqJyfOn{=F~rDn`xJ8YoA;Sp+Z zOrdL0vj4B8GSUpE?OL96^*ef8wOq&BAdn=-#o%Pq!_*+azgdf6g=wEaab%HSq!2^b zqBD`zIvgLTC2h1wK3b;B*-&F=pZWHo!jb4TR?Brb8zxSin0!r+&uP!oCnqPnuf4HO zbur`Y*|XdGWUaqRdrmqdFXq~DJY$ww^@9VAvzIMf<~Mt9u6GtgaB%RimBGvF68%w)!^y~k9p8tP7gAOOdYAa*+ zXV0EFEev>Z_3!WR^&N(8#pQyV{Mr(q?E5_Tz0H?5o6q|(dN6s+-pjw5$$?3r{^`{4 zv|qn|-MhJ`@-sV&~D4^7-#{XLJDg(+9;ZNI>EkEKED>p#!F|L0S3zpa_ke`UT0N;)riQl9F}s%4Ox z_U}RU{onTv*8cuhyJg?Le{QP;1vZ^9=y`PK&YgTvSnRj|zVrOvJ&)F>GtAkZv_Fik zLHK54_lgQ4=`LjrY<89gW)|mP0UJGyDy!rk! zz8MNibN!y*tNnhrGksp=vr{K0tB0?CB+YP-G2!Hu!(rza-{nY{eDwE*JBRu0YmUFW zyZisCcSRGnRzL1F-}mju0`+w$)IvfO>z z@mhLC#Si{N4>w$P-`#ik&}8`)=L#NZZ&<;&SZd#wrTTXzIathY$MS8mtJ}Z-|H5`T ztCVs}t&{~NPxmK%?OQc@dh9FDTw5D$|9(RhgZoRF zN_MgMpW-(*PMgM|z^2d=vbN1goNzkmO~_3l%Jsn)f>zKF}${dhS4R%~Ck zK|t1n)651ta2b?fbq zkB`eU%>9$M;qBJzalc#cwyp>jVOVWZ^JBvaiRvToiUf)a&i#M$;o;$P^N-wT5@A^V z;{E&oDh-LdPdoZ--noALd&b?|f!&}Xjr~D?Hr{=z@GPn4@5_nG?is8zMKu`0BqSt$ zm^7>{o8CMlPeZJ`_R>42D^gqys(kWxIz{L9=apM_m|8rqciP(URD)sKi`TDz^EBv| zO>d4!(~#<|yYYb55wZSo)`-UezGcr6d+s*Be^OF(1>>gTUBFmTxa``G{gk^#@<>2YdX zPa5?6d3d<}zf)-F`FMuRMJ@||zPPx!^WedQlV_XdS{;#Cyl|0TnuOcGqvG*@7U$PI zmd;)Q?vY^2il+2 z?HQIZp1B#TXI;6Et3^~>qWy59b6koe`?LKz7iUsR8Z{Dma!`J>+ zPa|@7jNbI!MNdzC;jjO3xI8^AElm3R`bAsfmM#zHS;U|b8OE(JuRu<2|1ah%Y#S8% zgLoB^${jsIeV8P+?-tnFz?2XuwDjrB)PS=m)><@N{P~~bMfbj00u5#ig%>^+a`y#g z|CeBR^?oBu?ZNeaECS2|ZZE$zSovCZHwHP!^Eq%e Date: Thu, 9 Dec 2021 23:04:40 +0100 Subject: [PATCH 0909/2244] Add metadata to scrcpy.exe for Windows Refs --- app/scrcpy-windows.rc | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/app/scrcpy-windows.rc b/app/scrcpy-windows.rc index 0dadc84c..8e3a585e 100644 --- a/app/scrcpy-windows.rc +++ b/app/scrcpy-windows.rc @@ -1 +1,20 @@ 0 ICON "../data/icon.ico" +1 VERSIONINFO +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904E4" + BEGIN + VALUE "FileDescription", "Display and control your Android device" + VALUE "InternalName", "scrcpy" + VALUE "LegalCopyright", "Romain Vimont, Genymobile" + VALUE "OriginalFilename", "scrcpy.exe" + VALUE "ProductName", "scrcpy" + VALUE "ProductVersion", "1.21" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END From 892cfe943ee5cf2001c74a1d39ed8496ba5272db Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 10 Dec 2021 19:50:17 +0100 Subject: [PATCH 0910/2244] Add script to bump version The version must now be bumped at 4 different places. Add a script to bump automatically: ./bump_version 1.23.4 --- bump_version | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100755 bump_version diff --git a/bump_version b/bump_version new file mode 100755 index 00000000..a0963666 --- /dev/null +++ b/bump_version @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# +# This script bump scrcpy version by editing all the necessary files. +# +# Usage: +# +# ./bump_version 1.23.4 +# +# Then check the diff manually to confirm that everything is ok. + +set -e + +if [[ $# != 1 ]] +then + echo "Syntax: $0 " >&2 + exit 1 +fi + +VERSION="$1" + +a=( ${VERSION//./ } ) +MAJOR="${a[0]:-0}" +MINOR="${a[1]:-0}" +PATCH="${a[2]:-0}" + +# If VERSION is 1.23.4, then VERSION_CODE is 12304 +VERSION_CODE="$(( $MAJOR * 10000 + $MINOR * 100 + "$PATCH" ))" + +echo "$VERSION: major=$MAJOR minor=$MINOR patch=$PATCH [versionCode=$VERSION_CODE]" +sed -i "s/^\(\s*version: \)'[^']*'/\1'$VERSION'/" meson.build +sed -i "s/^\(\s*versionCode \).*/\1$VERSION_CODE/;s/^\(\s*versionName \).*/\1\"$VERSION\"/" server/build.gradle +sed -i "s/^\(SCRCPY_VERSION_NAME=\).*/\1$VERSION/" server/build_without_gradle.sh +sed -i "s/^\(\s*VALUE \"ProductVersion\", \)\"[^\"]*\"/\1\"$VERSION\"/" app/scrcpy-windows.rc +echo done From 0685c491cd8f680b40e733adbbc367b48ec1801e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 10 Dec 2021 19:50:58 +0100 Subject: [PATCH 0911/2244] Improve crossbuild configuration Use meson native features to detect crossbuild, and remove the user-provided option crossbuild_windows. --- app/meson.build | 4 +++- meson_options.txt | 1 - release.mk | 2 -- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/app/meson.build b/app/meson.build index 4c0e909d..38393f0c 100644 --- a/app/meson.build +++ b/app/meson.build @@ -82,7 +82,9 @@ endif cc = meson.get_compiler('c') -if not get_option('crossbuild_windows') +crossbuild_windows = meson.is_cross_build() and host_machine.system() == 'windows' + +if not crossbuild_windows # native build dependencies = [ diff --git a/meson_options.txt b/meson_options.txt index 66ad5b25..d64e357f 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -1,6 +1,5 @@ option('compile_app', type: 'boolean', value: true, description: 'Build the client') option('compile_server', type: 'boolean', value: true, description: 'Build the server') -option('crossbuild_windows', type: 'boolean', value: false, description: 'Build for Windows from Linux') option('prebuilt_server', type: 'string', description: 'Path of the prebuilt server') option('portable', type: 'boolean', value: false, description: 'Use scrcpy-server from the same directory as the scrcpy executable') option('server_debugger', type: 'boolean', value: false, description: 'Run a server debugger and wait for a client to be attached') diff --git a/release.mk b/release.mk index 05ddbe46..cb6c9b2e 100644 --- a/release.mk +++ b/release.mk @@ -70,7 +70,6 @@ build-win32: prepare-deps-win32 meson "$(WIN32_BUILD_DIR)" \ --cross-file cross_win32.txt \ --buildtype release --strip -Db_lto=true \ - -Dcrossbuild_windows=true \ -Dcompile_server=false \ -Dportable=true ) ninja -C "$(WIN32_BUILD_DIR)" @@ -83,7 +82,6 @@ build-win64: prepare-deps-win64 meson "$(WIN64_BUILD_DIR)" \ --cross-file cross_win64.txt \ --buildtype release --strip -Db_lto=true \ - -Dcrossbuild_windows=true \ -Dcompile_server=false \ -Dportable=true ) ninja -C "$(WIN64_BUILD_DIR)" From d0496719080ccf9ee00fccf6cb0765e31765529f Mon Sep 17 00:00:00 2001 From: Chih-Hsuan Yen Date: Sun, 5 Dec 2021 12:58:02 +0800 Subject: [PATCH 0912/2244] Fix adb server hang Since commit 04267085441d6fcd05eff7df0118708f7622e237, the server is run in a dedicated thread. For SDL, many signals, including SIGINT and SIGTERM, are masked for new threads. As a result, if the adb server is not already running, adb commands invoked by scrcpy will start an adb server that ignores those signals and cannot be terminated at system shutdown. Fixes #2873 PR #2870 Signed-off-by: Romain Vimont --- app/src/sys/unix/process.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/src/sys/unix/process.c b/app/src/sys/unix/process.c index 54a1bb80..cb82f3a9 100644 --- a/app/src/sys/unix/process.c +++ b/app/src/sys/unix/process.c @@ -119,6 +119,13 @@ sc_process_execute_p(const char *const argv[], sc_pid *pid, unsigned flags, close(internal[0]); enum sc_process_result err; + + // Somehow SDL masks many signals - undo them for other processes + // https://github.com/libsdl-org/SDL/blob/release-2.0.18/src/thread/pthread/SDL_systhread.c#L167 + sigset_t mask; + sigemptyset(&mask); + sigprocmask(SIG_SETMASK, &mask, NULL); + if (fcntl(internal[1], F_SETFD, FD_CLOEXEC) == 0) { execvp(argv[0], (char *const *) argv); perror("exec"); From feb250a9738ce80e27d8e82e87d08c4038670993 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 15 Dec 2021 18:27:45 +0100 Subject: [PATCH 0913/2244] Fix typos reported by codespell --- app/src/server.c | 2 +- app/src/util/str.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/server.c b/app/src/server.c index 3b7a0fcc..ab5439ad 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -232,7 +232,7 @@ execute_server(struct sc_server *server, ADD_PARAM("power_off_on_close=%s", STRBOOL(params->power_off_on_close)); } if (!params->clipboard_autosync) { - // By defaut, clipboard_autosync is true + // By default, clipboard_autosync is true ADD_PARAM("clipboard_autosync=%s", STRBOOL(params->clipboard_autosync)); } diff --git a/app/src/util/str.c b/app/src/util/str.c index 70e3f1de..2d67f816 100644 --- a/app/src/util/str.c +++ b/app/src/util/str.c @@ -240,7 +240,7 @@ sc_str_wrap_lines(const char *input, unsigned columns, unsigned indent) { APPEND_INDENT(); - // The last separator encountered, it must be inserted only conditionnaly, + // The last separator encountered, it must be inserted only conditionally, // depending on the next token char pending = 0; From 25a41359358a2032fdcc09482974d5fd15a06511 Mon Sep 17 00:00:00 2001 From: Thomas Van Machelen Date: Tue, 14 Dec 2021 11:26:46 +0100 Subject: [PATCH 0914/2244] Mention react-native menu shortcut in README PR #2879 Signed-off-by: Romain Vimont --- README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 201141a8..c4b25f25 100644 --- a/README.md +++ b/README.md @@ -938,7 +938,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 \| _4th-click³_ - | Click on `MENU` (unlock screen) | 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 @@ -949,9 +949,9 @@ _[Super] is typically the Windows or Cmd key._ | Expand notification panel | MOD+n \| _5th-click³_ | Expand settings panel | MOD+n+n \| _Double-5th-click³_ | Collapse panels | MOD+Shift+n - | Copy to clipboard⁴ | MOD+c - | Cut to clipboard⁴ | MOD+x - | Synchronize clipboards and paste⁴ | MOD+v + | 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 | Pinch-to-zoom | Ctrl+_click-and-move_ @@ -961,7 +961,8 @@ _[Super] is typically the Windows or Cmd key._ _¹Double-click on black borders to remove them._ _²Right-click turns the screen on if it was off, presses BACK otherwise._ _³4th and 5th mouse buttons, if your mouse has them._ -_⁴Only on Android >= 7._ +_⁴For react-native apps in development, `MENU` triggers development menu._ +_⁵Only on Android >= 7._ Shortcuts with repeated keys are executted by releasing and pressing the key a second time. For example, to execute "Expand settings panel": From ad11c5babb0ff89647dd606392ed7907a20277c7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 18 Dec 2021 17:12:46 +0100 Subject: [PATCH 0915/2244] Set DPI awareness for Windows Add a windows manifest to set the DPI awareness by default: Refs #40 Fixes #2865 --- app/scrcpy-windows.manifest | 9 +++++++++ app/scrcpy-windows.rc | 5 ++++- 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 app/scrcpy-windows.manifest diff --git a/app/scrcpy-windows.manifest b/app/scrcpy-windows.manifest new file mode 100644 index 00000000..f2708ecb --- /dev/null +++ b/app/scrcpy-windows.manifest @@ -0,0 +1,9 @@ + + + + + true + PerMonitorV2 + + + diff --git a/app/scrcpy-windows.rc b/app/scrcpy-windows.rc index 8e3a585e..7525d092 100644 --- a/app/scrcpy-windows.rc +++ b/app/scrcpy-windows.rc @@ -1,5 +1,8 @@ +#include + 0 ICON "../data/icon.ico" -1 VERSIONINFO +1 RT_MANIFEST "scrcpy-windows.manifest" +2 VERSIONINFO BEGIN BLOCK "StringFileInfo" BEGIN From 720c3064dfd54c67a58df7aa8e94f12512607f37 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 18 Dec 2021 17:35:08 +0100 Subject: [PATCH 0916/2244] Upgrade SDL (2.0.18) for Windows Include the latest version of SDL in Windows releases. --- cross_win32.txt | 2 +- cross_win64.txt | 2 +- prebuilt-deps/Makefile | 6 +++--- release.mk | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cross_win32.txt b/cross_win32.txt index 41ec3078..b0e43622 100644 --- a/cross_win32.txt +++ b/cross_win32.txt @@ -18,4 +18,4 @@ endian = 'little' [properties] prebuilt_ffmpeg_shared = 'ffmpeg-4.3.1-win32-shared' prebuilt_ffmpeg_dev = 'ffmpeg-4.3.1-win32-dev' -prebuilt_sdl2 = 'SDL2-2.0.16/i686-w64-mingw32' +prebuilt_sdl2 = 'SDL2-2.0.18/i686-w64-mingw32' diff --git a/cross_win64.txt b/cross_win64.txt index 2565330b..6625c0cf 100644 --- a/cross_win64.txt +++ b/cross_win64.txt @@ -18,4 +18,4 @@ endian = 'little' [properties] prebuilt_ffmpeg_shared = 'ffmpeg-4.3.1-win64-shared' prebuilt_ffmpeg_dev = 'ffmpeg-4.3.1-win64-dev' -prebuilt_sdl2 = 'SDL2-2.0.16/x86_64-w64-mingw32' +prebuilt_sdl2 = 'SDL2-2.0.18/x86_64-w64-mingw32' diff --git a/prebuilt-deps/Makefile b/prebuilt-deps/Makefile index dced047c..fa986978 100644 --- a/prebuilt-deps/Makefile +++ b/prebuilt-deps/Makefile @@ -30,9 +30,9 @@ prepare-ffmpeg-dev-win64: ffmpeg-4.3.1-win64-dev prepare-sdl2: - @./prepare-dep https://libsdl.org/release/SDL2-devel-2.0.16-mingw.tar.gz \ - 2bfe48628aa9635c12eac7d421907e291525de1d0b04b3bca4a5bd6e6c881a6f \ - SDL2-2.0.16 + @./prepare-dep https://libsdl.org/release/SDL2-devel-2.0.18-mingw.tar.gz \ + bbad7c6947f6ca3e05292f065852ed8b62f319fc5533047e7708769c4dbae394 \ + SDL2-2.0.18 prepare-adb: @./prepare-dep https://dl.google.com/android/repository/platform-tools_r31.0.3-windows.zip \ diff --git a/release.mk b/release.mk index cb6c9b2e..6414f079 100644 --- a/release.mk +++ b/release.mk @@ -101,7 +101,7 @@ dist-win32: build-server build-win32 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.16/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/SDL2-2.0.18/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/" dist-win64: build-server build-win64 mkdir -p "$(DIST)/$(WIN64_TARGET_DIR)" @@ -118,7 +118,7 @@ dist-win64: build-server build-win64 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.16/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/SDL2-2.0.18/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/" zip-win32: dist-win32 cd "$(DIST)/$(WIN32_TARGET_DIR)"; \ From 2f091beeaadf991245bcdc1ce9eb5722def7f622 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 20 Dec 2021 19:21:19 +0100 Subject: [PATCH 0917/2244] Simplify sc_size assignment Assign the whole struct instead of each field separately. --- app/src/screen.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/screen.c b/app/src/screen.c index c72fdf44..e35641f7 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -101,8 +101,7 @@ get_optimal_size(struct sc_size current_size, struct sc_size content_size) { struct sc_size display_size; if (!get_preferred_display_bounds(&display_size)) { // could not get display bounds, do not constraint the size - window_size.width = current_size.width; - window_size.height = current_size.height; + window_size = current_size; } else { window_size.width = MIN(current_size.width, display_size.width); window_size.height = MIN(current_size.height, display_size.height); From 6261bb0b5a02a9ec0cea81ff3feff047bb94ebbb Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 20 Dec 2021 19:17:15 +0100 Subject: [PATCH 0918/2244] Ignore display bounds on resize-to-fit The "resize to fit" feature (MOD+w or double-click on black borders) computed the "optimal size" using the same function computing the initial window size on start. However, on "resize to fit", only the black borders must be removed (the content size must be preserved), so the display bounds must not be considered. --- app/src/screen.c | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/app/src/screen.c b/app/src/screen.c index e35641f7..0cb09a4b 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -90,7 +90,8 @@ is_optimal_size(struct sc_size current_size, struct sc_size content_size) { // - it keeps the aspect ratio // - it scales down to make it fit in the display_size static struct sc_size -get_optimal_size(struct sc_size current_size, struct sc_size content_size) { +get_optimal_size(struct sc_size current_size, struct sc_size content_size, + bool within_display_bounds) { if (content_size.width == 0 || content_size.height == 0) { // avoid division by 0 return current_size; @@ -99,8 +100,9 @@ get_optimal_size(struct sc_size current_size, struct sc_size content_size) { struct sc_size window_size; struct sc_size display_size; - if (!get_preferred_display_bounds(&display_size)) { - // could not get display bounds, do not constraint the size + if (!within_display_bounds || + !get_preferred_display_bounds(&display_size)) { + // do not constraint the size window_size = current_size; } else { window_size.width = MIN(current_size.width, display_size.width); @@ -134,7 +136,7 @@ get_initial_optimal_size(struct sc_size content_size, uint16_t req_width, uint16_t req_height) { struct sc_size window_size; if (!req_width && !req_height) { - window_size = get_optimal_size(content_size, content_size); + window_size = get_optimal_size(content_size, content_size, true); } else { if (req_width) { window_size.width = req_width; @@ -559,7 +561,7 @@ resize_for_content(struct screen *screen, struct sc_size old_content_size, .height = (uint32_t) window_size.height * new_content_size.height / old_content_size.height, }; - target_size = get_optimal_size(target_size, new_content_size); + target_size = get_optimal_size(target_size, new_content_size, true); set_window_size(screen, target_size); } @@ -694,7 +696,7 @@ screen_resize_to_fit(struct screen *screen) { struct sc_size window_size = get_window_size(screen); struct sc_size optimal_size = - get_optimal_size(window_size, screen->content_size); + get_optimal_size(window_size, screen->content_size, false); // Center the window related to the device screen assert(optimal_size.width <= window_size.width); From 826ddf1a6ee7784e1f72ca6202ebf2cd51b335ef Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 28 Dec 2021 12:16:20 +0100 Subject: [PATCH 0919/2244] Document HID keyboard events --- app/src/hid_keyboard.c | 76 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 75 insertions(+), 1 deletion(-) diff --git a/app/src/hid_keyboard.c b/app/src/hid_keyboard.c index 20fe8d51..cf8eab61 100644 --- a/app/src/hid_keyboard.c +++ b/app/src/hid_keyboard.c @@ -126,6 +126,80 @@ static const unsigned char keyboard_report_desc[] = { 0xC0 }; +/** + * A keyboard HID event is 8 bytes long: + * + * - byte 0: modifiers (1 flag per modifier key, 8 possible modifier keys) + * - byte 1: reserved (always 0) + * - bytes 2 to 7: pressed keys (6 at most) + * + * 7 6 5 4 3 2 1 0 + * +---------------+ + * byte 0: |. . . . . . . .| modifiers + * +---------------+ + * ^ ^ ^ ^ ^ ^ ^ ^ + * | | | | | | | `- left Ctrl + * | | | | | | `--- left Shift + * | | | | | `----- left Alt + * | | | | `------- left Gui + * | | | `--------- right Ctrl + * | | `----------- right Shift + * | `------------- right Alt + * `--------------- right Gui + * + * +---------------+ + * byte 1: |0 0 0 0 0 0 0 0| reserved + * +---------------+ + * + * +---------------+ + * bytes 2 to 7: |. . . . . . . .| scancode of 1st key pressed + * +---------------+ + * |. . . . . . . .| scancode of 2nd key pressed + * +---------------+ + * |. . . . . . . .| scancode of 3rd key pressed + * +---------------+ + * |. . . . . . . .| scancode of 4th key pressed + * +---------------+ + * |. . . . . . . .| scancode of 5th key pressed + * +---------------+ + * |. . . . . . . .| scancode of 6th key pressed + * +---------------+ + * + * If there are less than 6 keys pressed, the last items are set to 0. + * For example, if A and W are pressed: + * + * +---------------+ + * bytes 2 to 7: |0 0 0 0 0 1 0 0| A is pressed (scancode = 4) + * +---------------+ + * |0 0 0 1 1 0 1 0| W is pressed (scancode = 26) + * +---------------+ + * |0 0 0 0 0 0 0 0| ^ + * +---------------+ | only 2 keys are pressed, the + * |0 0 0 0 0 0 0 0| | remaining items are set to 0 + * +---------------+ | + * |0 0 0 0 0 0 0 0| | + * +---------------+ | + * |0 0 0 0 0 0 0 0| v + * +---------------+ + * + * Pressing more than 6 keys is not supported. If this happens (typically, + * never in practice), report a "phantom state": + * + * +---------------+ + * bytes 2 to 7: |0 0 0 0 0 0 0 1| ^ + * +---------------+ | + * |0 0 0 0 0 0 0 1| | more than 6 keys pressed: + * +---------------+ | the list is filled with a special + * |0 0 0 0 0 0 0 1| | rollover error code (0x01) + * +---------------+ | + * |0 0 0 0 0 0 0 1| | + * +---------------+ | + * |0 0 0 0 0 0 0 1| | + * +---------------+ | + * |0 0 0 0 0 0 0 1| v + * +---------------+ + */ + static unsigned char sdl_keymod_to_hid_modifiers(SDL_Keymod mod) { unsigned char modifiers = HID_MODIFIER_NONE; @@ -217,7 +291,7 @@ convert_hid_keyboard_event(struct sc_hid_keyboard *kb, // USB HID protocol says that if keys exceeds report count, a // phantom state should be reported if (keys_pressed_count >= HID_KEYBOARD_MAX_KEYS) { - // Pantom state: + // Phantom state: // - Modifiers // - Reserved // - ErrorRollOver * HID_MAX_KEYS From 1fbc590b26aa0f9b0144c4bbf99a0bf52fe31855 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 31 Dec 2021 10:49:22 +0100 Subject: [PATCH 0920/2244] Fix memory leaks in tests Tests were failing when run with ASAN enabled. --- app/tests/test_adb_parser.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/tests/test_adb_parser.c b/app/tests/test_adb_parser.c index fbc65649..cb3abb0e 100644 --- a/app/tests/test_adb_parser.c +++ b/app/tests/test_adb_parser.c @@ -11,6 +11,7 @@ static void test_get_ip_single_line() { char *ip = sc_adb_parse_device_ip_from_output(ip_route, sizeof(ip_route)); assert(ip); assert(!strcmp(ip, "192.168.12.34")); + free(ip); } static void test_get_ip_single_line_without_eol() { @@ -20,6 +21,7 @@ static void test_get_ip_single_line_without_eol() { char *ip = sc_adb_parse_device_ip_from_output(ip_route, sizeof(ip_route)); assert(ip); assert(!strcmp(ip, "192.168.12.34")); + free(ip); } static void test_get_ip_single_line_with_trailing_space() { @@ -29,6 +31,7 @@ static void test_get_ip_single_line_with_trailing_space() { char *ip = sc_adb_parse_device_ip_from_output(ip_route, sizeof(ip_route)); assert(ip); assert(!strcmp(ip, "192.168.12.34")); + free(ip); } static void test_get_ip_multiline_first_ok() { @@ -40,6 +43,7 @@ static void test_get_ip_multiline_first_ok() { char *ip = sc_adb_parse_device_ip_from_output(ip_route, sizeof(ip_route)); assert(ip); assert(!strcmp(ip, "192.168.1.2")); + free(ip); } static void test_get_ip_multiline_second_ok() { @@ -51,6 +55,7 @@ static void test_get_ip_multiline_second_ok() { char *ip = sc_adb_parse_device_ip_from_output(ip_route, sizeof(ip_route)); assert(ip); assert(!strcmp(ip, "192.168.1.3")); + free(ip); } static void test_get_ip_no_wlan() { From 6b9f39773396c17124fd7411c0b38a4b7d96fa28 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 1 Jan 2022 17:20:36 +0100 Subject: [PATCH 0921/2244] Happy new year 2022! --- LICENSE | 2 +- README.id.md | 2 +- README.it.md | 2 +- README.jp.md | 2 +- README.ko.md | 2 +- README.md | 2 +- README.pt-br.md | 2 +- README.sp.md | 2 +- README.tr.md | 2 +- README.zh-Hans.md | 2 +- README.zh-Hant.md | 2 +- app/scrcpy.1 | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/LICENSE b/LICENSE index b320f699..bea74a6b 100644 --- a/LICENSE +++ b/LICENSE @@ -188,7 +188,7 @@ identification within third-party archives. Copyright (C) 2018 Genymobile - Copyright (C) 2018-2021 Romain Vimont + Copyright (C) 2018-2022 Romain Vimont Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.id.md b/README.id.md index b4b16735..9657b95a 100644 --- a/README.id.md +++ b/README.id.md @@ -672,7 +672,7 @@ Baca [halaman pengembang]. ## Lisensi Copyright (C) 2018 Genymobile - Copyright (C) 2018-2021 Romain Vimont + Copyright (C) 2018-2022 Romain Vimont Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.it.md b/README.it.md index 6b5d6884..52e68ba3 100644 --- a/README.it.md +++ b/README.it.md @@ -790,7 +790,7 @@ Leggi la [pagina per sviluppatori]. ## Licenza (in inglese) Copyright (C) 2018 Genymobile - Copyright (C) 2018-2021 Romain Vimont + Copyright (C) 2018-2022 Romain Vimont Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.jp.md b/README.jp.md index a97ef765..583582fd 100644 --- a/README.jp.md +++ b/README.jp.md @@ -776,7 +776,7 @@ _⁴Android 7以上のみ._ ## ライセンス Copyright (C) 2018 Genymobile - Copyright (C) 2018-2021 Romain Vimont + Copyright (C) 2018-2022 Romain Vimont Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.ko.md b/README.ko.md index 31e38c6f..a77cde48 100644 --- a/README.ko.md +++ b/README.ko.md @@ -475,7 +475,7 @@ _²화면이 꺼진 상태에서 우클릭 시 다시 켜지며, 그 외의 상 ## 라이선스 Copyright (C) 2018 Genymobile - Copyright (C) 2018-2021 Romain Vimont + Copyright (C) 2018-2022 Romain Vimont Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index d6908bd6..8a177406 100644 --- a/README.md +++ b/README.md @@ -1017,7 +1017,7 @@ Read the [developers page]. ## Licence Copyright (C) 2018 Genymobile - Copyright (C) 2018-2021 Romain Vimont + Copyright (C) 2018-2022 Romain Vimont Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.pt-br.md b/README.pt-br.md index cdfeafeb..cc7e5f0b 100644 --- a/README.pt-br.md +++ b/README.pt-br.md @@ -857,7 +857,7 @@ Leia a [página dos desenvolvedores][developers page]. ## Licença Copyright (C) 2018 Genymobile - Copyright (C) 2018-2021 Romain Vimont + Copyright (C) 2018-2022 Romain Vimont Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.sp.md b/README.sp.md index 6f76a7be..05c14533 100644 --- a/README.sp.md +++ b/README.sp.md @@ -720,7 +720,7 @@ Lea la [hoja de desarrolladores (en inglés)](DEVELOP.md). ## Licencia Copyright (C) 2018 Genymobile - Copyright (C) 2018-2021 Romain Vimont + Copyright (C) 2018-2022 Romain Vimont Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.tr.md b/README.tr.md index 15c56b27..9501a889 100644 --- a/README.tr.md +++ b/README.tr.md @@ -801,7 +801,7 @@ Bakınız [FAQ](FAQ.md). ## Lisans Copyright (C) 2018 Genymobile - Copyright (C) 2018-2021 Romain Vimont + Copyright (C) 2018-2022 Romain Vimont Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.zh-Hans.md b/README.zh-Hans.md index b96d6d5a..61bb2842 100644 --- a/README.zh-Hans.md +++ b/README.zh-Hans.md @@ -842,7 +842,7 @@ ADB=/path/to/adb scrcpy ## 许可协议 Copyright (C) 2018 Genymobile - Copyright (C) 2018-2021 Romain Vimont + Copyright (C) 2018-2022 Romain Vimont Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.zh-Hant.md b/README.zh-Hant.md index c0e30254..87c0a8dd 100644 --- a/README.zh-Hant.md +++ b/README.zh-Hant.md @@ -679,7 +679,7 @@ _³只支援 Android 7+。_ ## Licence Copyright (C) 2018 Genymobile - Copyright (C) 2018-2021 Romain Vimont + Copyright (C) 2018-2022 Romain Vimont Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 7a6c5134..971c8a19 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -446,7 +446,7 @@ Copyright \(co 2018 Genymobile Genymobile .UE -Copyright \(co 2018\-2021 +Copyright \(co 2018\-2022 .MT rom@rom1v.com Romain Vimont .ME From 37124e14527192799e15af664186d1504bbc68ec Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 1 Jan 2022 23:27:34 +0100 Subject: [PATCH 0922/2244] Avoid unused function warning If HAVE_SOCK_CLOEXEC is not defined, then sc_raw_socket_close() is never used. Add an #ifndef block to remove the warning. --- app/src/util/net.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/util/net.c b/app/src/util/net.c index ec678d2e..565db2e9 100644 --- a/app/src/util/net.c +++ b/app/src/util/net.c @@ -83,6 +83,7 @@ unwrap(sc_socket socket) { #endif } +#ifndef HAVE_SOCK_CLOEXEC // avoid unused-function warning static inline bool sc_raw_socket_close(sc_raw_socket raw_sock) { #ifndef _WIN32 @@ -91,6 +92,7 @@ sc_raw_socket_close(sc_raw_socket raw_sock) { return !closesocket(raw_sock); #endif } +#endif #ifndef HAVE_SOCK_CLOEXEC // If SOCK_CLOEXEC does not exist, the flag must be set manually once the From ba28d817fbb38725cd5bcc5fe3c3c4d45ddfeace Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 1 Jan 2022 17:28:27 +0100 Subject: [PATCH 0923/2244] Fail on unsupported HID option If the feature is not supported on the platform, fail during command line parsing instead of using a fallback. --- app/src/cli.c | 6 ++++++ app/src/scrcpy.c | 11 ++++------- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index f1f50049..b420996c 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -1300,7 +1300,13 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], args->help = true; break; case 'K': +#ifdef HAVE_AOA_HID opts->keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_HID; +#else + LOGE("HID over AOA (-K/--hid-keyboard) is not supported on " + "this platform. It is only available on Linux."); + return false; +#endif break; case OPT_MAX_FPS: if (!parse_max_fps(optarg, &opts->max_fps)) { diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 9c2bd03b..3a271ee5 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -534,8 +534,8 @@ scrcpy(struct scrcpy_options *options) { struct sc_mouse_processor *mp = NULL; if (options->control) { - if (options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_HID) { #ifdef HAVE_AOA_HID + if (options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_HID) { bool aoa_hid_ok = false; bool ok = sc_aoa_init(&s->aoa, serial, acksync); @@ -566,13 +566,10 @@ aoa_hid_end: "(-K/--hid-keyboard ignored)"); options->keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_INJECT; } -#else - LOGE("HID over AOA is not supported on this platform, " - "fallback to default keyboard injection method " - "(-K/--hid-keyboard ignored)"); - options->keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_INJECT; -#endif } +#else + assert(options->keyboard_input_mode != SC_KEYBOARD_INPUT_MODE_HID); +#endif // keyboard_input_mode may have been reset if HID mode failed if (options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_INJECT) { From 26ee7ce56638788a82f69a0dc4cf93f41575be71 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 1 Jan 2022 17:34:49 +0100 Subject: [PATCH 0924/2244] Expose V4L2 option on all platforms This allows to report a meaningful error message if an unsupported feature is used on an incompatible platform. This is consistent with the behavior of -K/--hid-keyboard. --- app/src/cli.c | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index b420996c..5b727fff 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -240,11 +240,8 @@ static const struct sc_option options[] = { { .shortopt = 'N', .longopt = "no-display", - .text = "Do not display device (only when screen recording " -#ifdef HAVE_V4L2 - "or V4L2 sink " -#endif - "is enabled).", + .text = "Do not display device (only when screen recording or V4L2 " + "sink is enabled).", }, { .longopt_id = OPT_NO_KEY_REPEAT, @@ -381,14 +378,14 @@ static const struct sc_option options[] = { "Default is 0 (not forced): the local port used for " "establishing the tunnel will be used.", }, -#ifdef HAVE_V4L2 { .longopt_id = OPT_V4L2_SINK, .longopt = "v4l2-sink", .argdesc = "/dev/videoN", .text = "Output to v4l2loopback device.\n" "It requires to lock the video orientation (see " - "--lock-video-orientation).", + "--lock-video-orientation).\n" + "This feature is only available on Linux.", }, { .longopt_id = OPT_V4L2_BUFFER, @@ -398,9 +395,9 @@ static const struct sc_option options[] = { "frames. This increases latency to compensate for jitter.\n" "This option is similar to --display-buffer, but specific to " "V4L2 sink.\n" - "Default is 0 (no buffering).", + "Default is 0 (no buffering).\n" + "This option is only available on Linux.", }, -#endif { .shortopt = 'V', .longopt = "verbosity", @@ -1470,16 +1467,24 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], opts->tcpip = true; opts->tcpip_dst = optarg; break; -#ifdef HAVE_V4L2 case OPT_V4L2_SINK: +#ifdef HAVE_V4L2 opts->v4l2_device = optarg; +#else + LOGE("V4L2 (--v4l2-sink) is only available on Linux."); + return false; +#endif break; case OPT_V4L2_BUFFER: +#ifdef HAVE_V4L2 if (!parse_buffering_time(optarg, &opts->v4l2_buffer)) { return false; } - break; +#else + LOGE("V4L2 (--v4l2-buffer) is only available on Linux."); + return false; #endif + break; default: // getopt prints the error message on stderr return false; From cd5891fee6de32a79fe76b6269bd5fe8da0b7972 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 29 Dec 2021 01:34:54 +0100 Subject: [PATCH 0925/2244] Remove actions bitset The input manager exposed functions taking an "actions" parameter, containing a bitmask-OR of ACTION_UP and ACTION_DOWN. But they are never called with both actions simultaneously anymore, so simplify. Refs 964b6d2243fa1921543e48810f3064b9bd2d50d1 Refs d0739911a3e413b70275ded3eef839f9dc57ba7a --- app/src/input_manager.c | 79 +++++++++++++++++------------------------ 1 file changed, 33 insertions(+), 46 deletions(-) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 9a553842..7230414a 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -5,8 +5,10 @@ #include "util/log.h" -static const int ACTION_DOWN = 1; -static const int ACTION_UP = 1 << 1; +enum sc_action { + SC_ACTION_DOWN, + SC_ACTION_UP, +}; #define SC_SDL_SHORTCUT_MODS_MASK (KMOD_CTRL | KMOD_ALT | KMOD_GUI) @@ -89,85 +91,70 @@ input_manager_init(struct input_manager *im, struct controller *controller, static void send_keycode(struct controller *controller, enum android_keycode keycode, - int actions, const char *name) { + enum sc_action action, const char *name) { // send DOWN event struct control_msg msg; msg.type = CONTROL_MSG_TYPE_INJECT_KEYCODE; + msg.inject_keycode.action = action == SC_ACTION_DOWN + ? AKEY_EVENT_ACTION_DOWN + : AKEY_EVENT_ACTION_UP; 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; - if (!controller_push_msg(controller, &msg)) { - LOGW("Could not request 'inject %s (DOWN)'", name); - return; - } - } - - if (actions & ACTION_UP) { - msg.inject_keycode.action = AKEY_EVENT_ACTION_UP; - if (!controller_push_msg(controller, &msg)) { - LOGW("Could not request 'inject %s (UP)'", name); - } + if (!controller_push_msg(controller, &msg)) { + LOGW("Could not request 'inject %s'", name); } } static inline void -action_home(struct controller *controller, int actions) { - send_keycode(controller, AKEYCODE_HOME, actions, "HOME"); +action_home(struct controller *controller, enum sc_action action) { + send_keycode(controller, AKEYCODE_HOME, action, "HOME"); } static inline void -action_back(struct controller *controller, int actions) { - send_keycode(controller, AKEYCODE_BACK, actions, "BACK"); +action_back(struct controller *controller, enum sc_action action) { + send_keycode(controller, AKEYCODE_BACK, action, "BACK"); } static inline void -action_app_switch(struct controller *controller, int actions) { - send_keycode(controller, AKEYCODE_APP_SWITCH, actions, "APP_SWITCH"); +action_app_switch(struct controller *controller, enum sc_action action) { + send_keycode(controller, AKEYCODE_APP_SWITCH, action, "APP_SWITCH"); } static inline void -action_power(struct controller *controller, int actions) { - send_keycode(controller, AKEYCODE_POWER, actions, "POWER"); +action_power(struct controller *controller, enum sc_action action) { + send_keycode(controller, AKEYCODE_POWER, action, "POWER"); } static inline void -action_volume_up(struct controller *controller, int actions) { - send_keycode(controller, AKEYCODE_VOLUME_UP, actions, "VOLUME_UP"); +action_volume_up(struct controller *controller, enum sc_action action) { + send_keycode(controller, AKEYCODE_VOLUME_UP, action, "VOLUME_UP"); } static inline void -action_volume_down(struct controller *controller, int actions) { - send_keycode(controller, AKEYCODE_VOLUME_DOWN, actions, "VOLUME_DOWN"); +action_volume_down(struct controller *controller, enum sc_action action) { + send_keycode(controller, AKEYCODE_VOLUME_DOWN, action, "VOLUME_DOWN"); } static inline void -action_menu(struct controller *controller, int actions) { - send_keycode(controller, AKEYCODE_MENU, actions, "MENU"); +action_menu(struct controller *controller, enum sc_action action) { + send_keycode(controller, AKEYCODE_MENU, action, "MENU"); } // turn the screen on if it was off, press BACK otherwise // If the screen is off, it is turned on only on ACTION_DOWN static void -press_back_or_turn_screen_on(struct controller *controller, int actions) { +press_back_or_turn_screen_on(struct controller *controller, + enum sc_action action) { struct control_msg msg; msg.type = CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON; + msg.back_or_screen_on.action = action == SC_ACTION_DOWN + ? AKEY_EVENT_ACTION_DOWN + : AKEY_EVENT_ACTION_UP; - if (actions & ACTION_DOWN) { - msg.back_or_screen_on.action = AKEY_EVENT_ACTION_DOWN; - if (!controller_push_msg(controller, &msg)) { - LOGW("Could not request 'press back or turn screen on'"); - return; - } - } - - if (actions & ACTION_UP) { - msg.back_or_screen_on.action = AKEY_EVENT_ACTION_UP; - if (!controller_push_msg(controller, &msg)) { - LOGW("Could not request 'press back or turn screen on'"); - } + if (!controller_push_msg(controller, &msg)) { + LOGW("Could not request 'press back or turn screen on'"); } } @@ -396,7 +383,7 @@ input_manager_process_key(struct input_manager *im, // The shortcut modifier is pressed if (smod) { - int action = down ? ACTION_DOWN : ACTION_UP; + enum sc_action action = down ? SC_ACTION_DOWN : SC_ACTION_UP; switch (keycode) { case SDLK_h: if (control && !shift && !repeat) { @@ -601,7 +588,7 @@ input_manager_process_mouse_button(struct input_manager *im, bool down = event->type == SDL_MOUSEBUTTONDOWN; if (!im->forward_all_clicks) { - int action = down ? ACTION_DOWN : ACTION_UP; + enum sc_action action = down ? SC_ACTION_DOWN : SC_ACTION_UP; if (control && event->button == SDL_BUTTON_X1) { action_app_switch(im->controller, action); From d540c72e7cbfe40fc746542c9d2a3e3a1f3193d4 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 28 Dec 2021 15:24:15 +0100 Subject: [PATCH 0926/2244] Rename SC_MOD_* to SC_SHORTCUT_MOD_* This will avoid conflicts with new SC_MOD_* constants. --- app/src/cli.c | 14 +++++++------- app/src/input_manager.c | 14 +++++++------- app/src/options.c | 2 +- app/src/options.h | 12 ++++++------ app/tests/test_cli.c | 15 ++++++++------- 5 files changed, 29 insertions(+), 28 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index 5b727fff..ec53e5ec 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -1118,7 +1118,7 @@ parse_log_level(const char *s, enum sc_log_level *log_level) { } // 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) +// returns a bitwise-or of SC_SHORTCUT_MOD_* constants (or 0 on error) static unsigned parse_shortcut_mods_item(const char *item, size_t len) { unsigned mod = 0; @@ -1136,17 +1136,17 @@ parse_shortcut_mods_item(const char *item, size_t len) { ((sizeof(literal)-1 == len) && !memcmp(literal, s, len)) if (STREQ("lctrl", item, key_len)) { - mod |= SC_MOD_LCTRL; + mod |= SC_SHORTCUT_MOD_LCTRL; } else if (STREQ("rctrl", item, key_len)) { - mod |= SC_MOD_RCTRL; + mod |= SC_SHORTCUT_MOD_RCTRL; } else if (STREQ("lalt", item, key_len)) { - mod |= SC_MOD_LALT; + mod |= SC_SHORTCUT_MOD_LALT; } else if (STREQ("ralt", item, key_len)) { - mod |= SC_MOD_RALT; + mod |= SC_SHORTCUT_MOD_RALT; } else if (STREQ("lsuper", item, key_len)) { - mod |= SC_MOD_LSUPER; + mod |= SC_SHORTCUT_MOD_LSUPER; } else if (STREQ("rsuper", item, key_len)) { - mod |= SC_MOD_RSUPER; + mod |= SC_SHORTCUT_MOD_RSUPER; } else { LOGE("Unknown modifier key: %.*s " "(must be one of: lctrl, rctrl, lalt, ralt, lsuper, rsuper)", diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 7230414a..0688dcab 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -13,24 +13,24 @@ enum sc_action { #define SC_SDL_SHORTCUT_MODS_MASK (KMOD_CTRL | KMOD_ALT | KMOD_GUI) static inline uint16_t -to_sdl_mod(unsigned mod) { +to_sdl_mod(unsigned shortcut_mod) { uint16_t sdl_mod = 0; - if (mod & SC_MOD_LCTRL) { + if (shortcut_mod & SC_SHORTCUT_MOD_LCTRL) { sdl_mod |= KMOD_LCTRL; } - if (mod & SC_MOD_RCTRL) { + if (shortcut_mod & SC_SHORTCUT_MOD_RCTRL) { sdl_mod |= KMOD_RCTRL; } - if (mod & SC_MOD_LALT) { + if (shortcut_mod & SC_SHORTCUT_MOD_LALT) { sdl_mod |= KMOD_LALT; } - if (mod & SC_MOD_RALT) { + if (shortcut_mod & SC_SHORTCUT_MOD_RALT) { sdl_mod |= KMOD_RALT; } - if (mod & SC_MOD_LSUPER) { + if (shortcut_mod & SC_SHORTCUT_MOD_LSUPER) { sdl_mod |= KMOD_LGUI; } - if (mod & SC_MOD_RSUPER) { + if (shortcut_mod & SC_SHORTCUT_MOD_RSUPER) { sdl_mod |= KMOD_RGUI; } return sdl_mod; diff --git a/app/src/options.c b/app/src/options.c index 4860fa07..7a5b71af 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -22,7 +22,7 @@ const struct scrcpy_options scrcpy_options_default = { .tunnel_host = 0, .tunnel_port = 0, .shortcut_mods = { - .data = {SC_MOD_LALT, SC_MOD_LSUPER}, + .data = {SC_SHORTCUT_MOD_LALT, SC_SHORTCUT_MOD_LSUPER}, .count = 2, }, .max_size = 0, diff --git a/app/src/options.h b/app/src/options.h index 39703210..533f4a3b 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -55,12 +55,12 @@ enum sc_key_inject_mode { #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, + SC_SHORTCUT_MOD_LCTRL = 1 << 0, + SC_SHORTCUT_MOD_RCTRL = 1 << 1, + SC_SHORTCUT_MOD_LALT = 1 << 2, + SC_SHORTCUT_MOD_RALT = 1 << 3, + SC_SHORTCUT_MOD_LSUPER = 1 << 4, + SC_SHORTCUT_MOD_RSUPER = 1 << 5, }; struct sc_shortcut_mods { diff --git a/app/tests/test_cli.c b/app/tests/test_cli.c index a29d5fdd..5ea54b7f 100644 --- a/app/tests/test_cli.c +++ b/app/tests/test_cli.c @@ -129,25 +129,26 @@ static void test_parse_shortcut_mods(void) { ok = sc_parse_shortcut_mods("lctrl", &mods); assert(ok); assert(mods.count == 1); - assert(mods.data[0] == SC_MOD_LCTRL); + assert(mods.data[0] == SC_SHORTCUT_MOD_LCTRL); ok = sc_parse_shortcut_mods("lctrl+lalt", &mods); assert(ok); assert(mods.count == 1); - assert(mods.data[0] == (SC_MOD_LCTRL | SC_MOD_LALT)); + assert(mods.data[0] == (SC_SHORTCUT_MOD_LCTRL | SC_SHORTCUT_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); + assert(mods.data[0] == SC_SHORTCUT_MOD_RCTRL); + assert(mods.data[1] == SC_SHORTCUT_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)); + assert(mods.data[0] == SC_SHORTCUT_MOD_LSUPER); + assert(mods.data[1] == (SC_SHORTCUT_MOD_RSUPER | SC_SHORTCUT_MOD_LALT)); + assert(mods.data[2] == (SC_SHORTCUT_MOD_LCTRL | SC_SHORTCUT_MOD_RCTRL | + SC_SHORTCUT_MOD_RALT)); ok = sc_parse_shortcut_mods("", &mods); assert(!ok); From b8fed50639fe3bf8e02a8cc2409494cd6bf30fb7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 29 Dec 2021 15:51:32 +0100 Subject: [PATCH 0927/2244] Add intermediate input events layer This aims to make the key/mouse processors independent of the "screen", by processing scrcpy-specific input events instead of SDL events. In particular, these scrcpy events are not impacted by any UI window scaling or rotation (contrary to SDL events). --- app/src/input_events.h | 378 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 378 insertions(+) create mode 100644 app/src/input_events.h diff --git a/app/src/input_events.h b/app/src/input_events.h new file mode 100644 index 00000000..b40995e6 --- /dev/null +++ b/app/src/input_events.h @@ -0,0 +1,378 @@ +#ifndef SC_INPUT_EVENTS_H +#define SC_INPUT_EVENTS_H + +#include "common.h" + +#include +#include +#include +#include + +#include "coords.h" + +/* The representation of input events in scrcpy is very close to the SDL API, + * for simplicity. + * + * This scrcpy input events API is designed to be consumed by input event + * processors (sc_key_processor and sc_mouse_processor, see app/src/trait/). + * + * One major semantic difference between SDL input events and scrcpy input + * events is their frame of reference (for mouse and touch events): SDL events + * coordinates are expressed in SDL window coordinates (the visible UI), while + * scrcpy events are expressed in device frame coordinates. + * + * In particular, the window may be visually scaled or rotated (with --rotation + * or MOD+Left/Right), but this does not impact scrcpy input events (contrary + * to SDL input events). This allows to abstract these display details from the + * input event processors (and to make them independent from the "screen"). + * + * For many enums below, the values are purposely the same as the SDL + * constants (though not all SDL values are represented), so that the + * implementation to convert from the SDL version to the scrcpy version is + * straightforward. + * + * In practice, there are 3 levels of input events: + * 1. SDL input events (as received from SDL) + * 2. scrcpy input events (this API) + * 3. the key/mouse processors input events (Android API or HID events) + * + * An input event is first received (1), then (if accepted) converted to an + * scrcpy input event (2), then submitted to the relevant key/mouse processor, + * which (if accepted) is converted to an Android event (to be sent to the + * server) or to an HID event (to be sent over USB/AOA directly). + */ + +enum sc_mod { + SC_MOD_LSHIFT = KMOD_LSHIFT, + SC_MOD_RSHIFT = KMOD_RSHIFT, + SC_MOD_LCTRL = KMOD_LCTRL, + SC_MOD_RCTRL = KMOD_RCTRL, + SC_MOD_LALT = KMOD_LALT, + SC_MOD_RALT = KMOD_RALT, + SC_MOD_LGUI = KMOD_LGUI, + SC_MOD_RGUI = KMOD_RGUI, + + SC_MOD_NUM = KMOD_NUM, + SC_MOD_CAPS = KMOD_CAPS, + SC_MOD_SCROLL = KMOD_SCROLL, +}; + +enum sc_action { + SC_ACTION_DOWN, // key or button pressed + SC_ACTION_UP, // key or button released +}; + +enum sc_keycode { + SC_KEYCODE_UNKNOWN = SDLK_UNKNOWN, + + SC_KEYCODE_RETURN = SDLK_RETURN, + SC_KEYCODE_ESCAPE = SDLK_ESCAPE, + SC_KEYCODE_BACKSPACE = SDLK_BACKSPACE, + SC_KEYCODE_TAB = SDLK_TAB, + SC_KEYCODE_SPACE = SDLK_SPACE, + SC_KEYCODE_EXCLAIM = SDLK_EXCLAIM, + SC_KEYCODE_QUOTEDBL = SDLK_QUOTEDBL, + SC_KEYCODE_HASH = SDLK_HASH, + SC_KEYCODE_PERCENT = SDLK_PERCENT, + SC_KEYCODE_DOLLAR = SDLK_DOLLAR, + SC_KEYCODE_AMPERSAND = SDLK_AMPERSAND, + SC_KEYCODE_QUOTE = SDLK_QUOTE, + SC_KEYCODE_LEFTPAREN = SDLK_LEFTPAREN, + SC_KEYCODE_RIGHTPAREN = SDLK_RIGHTPAREN, + SC_KEYCODE_ASTERISK = SDLK_ASTERISK, + SC_KEYCODE_PLUS = SDLK_PLUS, + SC_KEYCODE_COMMA = SDLK_COMMA, + SC_KEYCODE_MINUS = SDLK_MINUS, + SC_KEYCODE_PERIOD = SDLK_PERIOD, + SC_KEYCODE_SLASH = SDLK_SLASH, + SC_KEYCODE_0 = SDLK_0, + SC_KEYCODE_1 = SDLK_1, + SC_KEYCODE_2 = SDLK_2, + SC_KEYCODE_3 = SDLK_3, + SC_KEYCODE_4 = SDLK_4, + SC_KEYCODE_5 = SDLK_5, + SC_KEYCODE_6 = SDLK_6, + SC_KEYCODE_7 = SDLK_7, + SC_KEYCODE_8 = SDLK_8, + SC_KEYCODE_9 = SDLK_9, + SC_KEYCODE_COLON = SDLK_COLON, + SC_KEYCODE_SEMICOLON = SDLK_SEMICOLON, + SC_KEYCODE_LESS = SDLK_LESS, + SC_KEYCODE_EQUALS = SDLK_EQUALS, + SC_KEYCODE_GREATER = SDLK_GREATER, + SC_KEYCODE_QUESTION = SDLK_QUESTION, + SC_KEYCODE_AT = SDLK_AT, + + SC_KEYCODE_LEFTBRACKET = SDLK_LEFTBRACKET, + SC_KEYCODE_BACKSLASH = SDLK_BACKSLASH, + SC_KEYCODE_RIGHTBRACKET = SDLK_RIGHTBRACKET, + SC_KEYCODE_CARET = SDLK_CARET, + SC_KEYCODE_UNDERSCORE = SDLK_UNDERSCORE, + SC_KEYCODE_BACKQUOTE = SDLK_BACKQUOTE, + SC_KEYCODE_a = SDLK_a, + SC_KEYCODE_b = SDLK_b, + SC_KEYCODE_c = SDLK_c, + SC_KEYCODE_d = SDLK_d, + SC_KEYCODE_e = SDLK_e, + SC_KEYCODE_f = SDLK_f, + SC_KEYCODE_g = SDLK_g, + SC_KEYCODE_h = SDLK_h, + SC_KEYCODE_i = SDLK_i, + SC_KEYCODE_j = SDLK_j, + SC_KEYCODE_k = SDLK_k, + SC_KEYCODE_l = SDLK_l, + SC_KEYCODE_m = SDLK_m, + SC_KEYCODE_n = SDLK_n, + SC_KEYCODE_o = SDLK_o, + SC_KEYCODE_p = SDLK_p, + SC_KEYCODE_q = SDLK_q, + SC_KEYCODE_r = SDLK_r, + SC_KEYCODE_s = SDLK_s, + SC_KEYCODE_t = SDLK_t, + SC_KEYCODE_u = SDLK_u, + SC_KEYCODE_v = SDLK_v, + SC_KEYCODE_w = SDLK_w, + SC_KEYCODE_x = SDLK_x, + SC_KEYCODE_y = SDLK_y, + SC_KEYCODE_z = SDLK_z, + + SC_KEYCODE_CAPSLOCK = SDLK_CAPSLOCK, + + SC_KEYCODE_F1 = SDLK_F1, + SC_KEYCODE_F2 = SDLK_F2, + SC_KEYCODE_F3 = SDLK_F3, + SC_KEYCODE_F4 = SDLK_F4, + SC_KEYCODE_F5 = SDLK_F5, + SC_KEYCODE_F6 = SDLK_F6, + SC_KEYCODE_F7 = SDLK_F7, + SC_KEYCODE_F8 = SDLK_F8, + SC_KEYCODE_F9 = SDLK_F9, + SC_KEYCODE_F10 = SDLK_F10, + SC_KEYCODE_F11 = SDLK_F11, + SC_KEYCODE_F12 = SDLK_F12, + + SC_KEYCODE_PRINTSCREEN = SDLK_PRINTSCREEN, + SC_KEYCODE_SCROLLLOCK = SDLK_SCROLLLOCK, + SC_KEYCODE_PAUSE = SDLK_PAUSE, + SC_KEYCODE_INSERT = SDLK_INSERT, + SC_KEYCODE_HOME = SDLK_HOME, + SC_KEYCODE_PAGEUP = SDLK_PAGEUP, + SC_KEYCODE_DELETE = SDLK_DELETE, + SC_KEYCODE_END = SDLK_END, + SC_KEYCODE_PAGEDOWN = SDLK_PAGEDOWN, + SC_KEYCODE_RIGHT = SDLK_RIGHT, + SC_KEYCODE_LEFT = SDLK_LEFT, + SC_KEYCODE_DOWN = SDLK_DOWN, + SC_KEYCODE_UP = SDLK_UP, + + SC_KEYCODE_KP_DIVIDE = SDLK_KP_DIVIDE, + SC_KEYCODE_KP_MULTIPLY = SDLK_KP_MULTIPLY, + SC_KEYCODE_KP_MINUS = SDLK_KP_MINUS, + SC_KEYCODE_KP_PLUS = SDLK_KP_PLUS, + SC_KEYCODE_KP_ENTER = SDLK_KP_ENTER, + SC_KEYCODE_KP_1 = SDLK_KP_1, + SC_KEYCODE_KP_2 = SDLK_KP_2, + SC_KEYCODE_KP_3 = SDLK_KP_3, + SC_KEYCODE_KP_4 = SDLK_KP_4, + SC_KEYCODE_KP_5 = SDLK_KP_5, + SC_KEYCODE_KP_6 = SDLK_KP_6, + SC_KEYCODE_KP_7 = SDLK_KP_7, + SC_KEYCODE_KP_8 = SDLK_KP_8, + SC_KEYCODE_KP_9 = SDLK_KP_9, + SC_KEYCODE_KP_0 = SDLK_KP_0, + SC_KEYCODE_KP_PERIOD = SDLK_KP_PERIOD, + SC_KEYCODE_KP_EQUALS = SDLK_KP_EQUALS, + SC_KEYCODE_KP_LEFTPAREN = SDLK_KP_LEFTPAREN, + SC_KEYCODE_KP_RIGHTPAREN = SDLK_KP_RIGHTPAREN, + + SC_KEYCODE_LCTRL = SDLK_LCTRL, + SC_KEYCODE_LSHIFT = SDLK_LSHIFT, + SC_KEYCODE_LALT = SDLK_LALT, + SC_KEYCODE_LGUI = SDLK_LGUI, + SC_KEYCODE_RCTRL = SDLK_RCTRL, + SC_KEYCODE_RSHIFT = SDLK_RSHIFT, + SC_KEYCODE_RALT = SDLK_RALT, + SC_KEYCODE_RGUI = SDLK_RGUI, +}; + +enum sc_scancode { + SC_SCANCODE_UNKNOWN = SDL_SCANCODE_UNKNOWN, + + SC_SCANCODE_A = SDL_SCANCODE_A, + SC_SCANCODE_B = SDL_SCANCODE_B, + SC_SCANCODE_C = SDL_SCANCODE_C, + SC_SCANCODE_D = SDL_SCANCODE_D, + SC_SCANCODE_E = SDL_SCANCODE_E, + SC_SCANCODE_F = SDL_SCANCODE_F, + SC_SCANCODE_G = SDL_SCANCODE_G, + SC_SCANCODE_H = SDL_SCANCODE_H, + SC_SCANCODE_I = SDL_SCANCODE_I, + SC_SCANCODE_J = SDL_SCANCODE_J, + SC_SCANCODE_K = SDL_SCANCODE_K, + SC_SCANCODE_L = SDL_SCANCODE_L, + SC_SCANCODE_M = SDL_SCANCODE_M, + SC_SCANCODE_N = SDL_SCANCODE_N, + SC_SCANCODE_O = SDL_SCANCODE_O, + SC_SCANCODE_P = SDL_SCANCODE_P, + SC_SCANCODE_Q = SDL_SCANCODE_Q, + SC_SCANCODE_R = SDL_SCANCODE_R, + SC_SCANCODE_S = SDL_SCANCODE_S, + SC_SCANCODE_T = SDL_SCANCODE_T, + SC_SCANCODE_U = SDL_SCANCODE_U, + SC_SCANCODE_V = SDL_SCANCODE_V, + SC_SCANCODE_W = SDL_SCANCODE_W, + SC_SCANCODE_X = SDL_SCANCODE_X, + SC_SCANCODE_Y = SDL_SCANCODE_Y, + SC_SCANCODE_Z = SDL_SCANCODE_Z, + + SC_SCANCODE_1 = SDL_SCANCODE_1, + SC_SCANCODE_2 = SDL_SCANCODE_2, + SC_SCANCODE_3 = SDL_SCANCODE_3, + SC_SCANCODE_4 = SDL_SCANCODE_4, + SC_SCANCODE_5 = SDL_SCANCODE_5, + SC_SCANCODE_6 = SDL_SCANCODE_6, + SC_SCANCODE_7 = SDL_SCANCODE_7, + SC_SCANCODE_8 = SDL_SCANCODE_8, + SC_SCANCODE_9 = SDL_SCANCODE_9, + SC_SCANCODE_0 = SDL_SCANCODE_0, + + SC_SCANCODE_RETURN = SDL_SCANCODE_RETURN, + SC_SCANCODE_ESCAPE = SDL_SCANCODE_ESCAPE, + SC_SCANCODE_BACKSPACE = SDL_SCANCODE_BACKSPACE, + SC_SCANCODE_TAB = SDL_SCANCODE_TAB, + SC_SCANCODE_SPACE = SDL_SCANCODE_SPACE, + + SC_SCANCODE_MINUS = SDL_SCANCODE_MINUS, + SC_SCANCODE_EQUALS = SDL_SCANCODE_EQUALS, + SC_SCANCODE_LEFTBRACKET = SDL_SCANCODE_LEFTBRACKET, + SC_SCANCODE_RIGHTBRACKET = SDL_SCANCODE_RIGHTBRACKET, + SC_SCANCODE_BACKSLASH = SDL_SCANCODE_BACKSLASH, + SC_SCANCODE_NONUSHASH = SDL_SCANCODE_NONUSHASH, + SC_SCANCODE_SEMICOLON = SDL_SCANCODE_SEMICOLON, + SC_SCANCODE_APOSTROPHE = SDL_SCANCODE_APOSTROPHE, + SC_SCANCODE_GRAVE = SDL_SCANCODE_GRAVE, + SC_SCANCODE_COMMA = SDL_SCANCODE_COMMA, + SC_SCANCODE_PERIOD = SDL_SCANCODE_PERIOD, + SC_SCANCODE_SLASH = SDL_SCANCODE_SLASH, + + SC_SCANCODE_CAPSLOCK = SDL_SCANCODE_CAPSLOCK, + + SC_SCANCODE_F1 = SDL_SCANCODE_F1, + SC_SCANCODE_F2 = SDL_SCANCODE_F2, + SC_SCANCODE_F3 = SDL_SCANCODE_F3, + SC_SCANCODE_F4 = SDL_SCANCODE_F4, + SC_SCANCODE_F5 = SDL_SCANCODE_F5, + SC_SCANCODE_F6 = SDL_SCANCODE_F6, + SC_SCANCODE_F7 = SDL_SCANCODE_F7, + SC_SCANCODE_F8 = SDL_SCANCODE_F8, + SC_SCANCODE_F9 = SDL_SCANCODE_F9, + SC_SCANCODE_F10 = SDL_SCANCODE_F10, + SC_SCANCODE_F11 = SDL_SCANCODE_F11, + SC_SCANCODE_F12 = SDL_SCANCODE_F12, + + SC_SCANCODE_PRINTSCREEN = SDL_SCANCODE_PRINTSCREEN, + SC_SCANCODE_SCROLLLOCK = SDL_SCANCODE_SCROLLLOCK, + SC_SCANCODE_PAUSE = SDL_SCANCODE_PAUSE, + SC_SCANCODE_INSERT = SDL_SCANCODE_INSERT, + SC_SCANCODE_HOME = SDL_SCANCODE_HOME, + SC_SCANCODE_PAGEUP = SDL_SCANCODE_PAGEUP, + SC_SCANCODE_DELETE = SDL_SCANCODE_DELETE, + SC_SCANCODE_END = SDL_SCANCODE_END, + SC_SCANCODE_PAGEDOWN = SDL_SCANCODE_PAGEDOWN, + SC_SCANCODE_RIGHT = SDL_SCANCODE_RIGHT, + SC_SCANCODE_LEFT = SDL_SCANCODE_LEFT, + SC_SCANCODE_DOWN = SDL_SCANCODE_DOWN, + SC_SCANCODE_UP = SDL_SCANCODE_UP, + + SC_SCANCODE_NUMLOCK = SDL_SCANCODE_NUMLOCKCLEAR, + SC_SCANCODE_KP_DIVIDE = SDL_SCANCODE_KP_DIVIDE, + SC_SCANCODE_KP_MULTIPLY = SDL_SCANCODE_KP_MULTIPLY, + SC_SCANCODE_KP_MINUS = SDL_SCANCODE_KP_MINUS, + SC_SCANCODE_KP_PLUS = SDL_SCANCODE_KP_PLUS, + SC_SCANCODE_KP_ENTER = SDL_SCANCODE_KP_ENTER, + SC_SCANCODE_KP_1 = SDL_SCANCODE_KP_1, + SC_SCANCODE_KP_2 = SDL_SCANCODE_KP_2, + SC_SCANCODE_KP_3 = SDL_SCANCODE_KP_3, + SC_SCANCODE_KP_4 = SDL_SCANCODE_KP_4, + SC_SCANCODE_KP_5 = SDL_SCANCODE_KP_5, + SC_SCANCODE_KP_6 = SDL_SCANCODE_KP_6, + SC_SCANCODE_KP_7 = SDL_SCANCODE_KP_7, + SC_SCANCODE_KP_8 = SDL_SCANCODE_KP_8, + SC_SCANCODE_KP_9 = SDL_SCANCODE_KP_9, + SC_SCANCODE_KP_0 = SDL_SCANCODE_KP_0, + SC_SCANCODE_KP_PERIOD = SDL_SCANCODE_KP_PERIOD, + + SC_SCANCODE_LCTRL = SDL_SCANCODE_LCTRL, + SC_SCANCODE_LSHIFT = SDL_SCANCODE_LSHIFT, + SC_SCANCODE_LALT = SDL_SCANCODE_LALT, + SC_SCANCODE_LGUI = SDL_SCANCODE_LGUI, + SC_SCANCODE_RCTRL = SDL_SCANCODE_RCTRL, + SC_SCANCODE_RSHIFT = SDL_SCANCODE_RSHIFT, + SC_SCANCODE_RALT = SDL_SCANCODE_RALT, + SC_SCANCODE_RGUI = SDL_SCANCODE_RGUI, +}; + +// On purpose, only use the "mask" values (1, 2, 4, 8, 16) for a single button, +// to avoid unnecessary conversions (and confusion). +enum sc_mouse_button { + SC_MOUSE_BUTTON_UNKNOWN = 0, + SC_MOUSE_BUTTON_LEFT = SDL_BUTTON(SDL_BUTTON_LEFT), + SC_MOUSE_BUTTON_RIGHT = SDL_BUTTON(SDL_BUTTON_RIGHT), + SC_MOUSE_BUTTON_MIDDLE = SDL_BUTTON(SDL_BUTTON_MIDDLE), + SC_MOUSE_BUTTON_X1 = SDL_BUTTON(SDL_BUTTON_X1), + SC_MOUSE_BUTTON_X2 = SDL_BUTTON(SDL_BUTTON_X2), +}; + +static_assert(sizeof(enum sc_mod) >= sizeof(SDL_Keymod), + "SDL_Keymod must be convertible to sc_mod"); + +static_assert(sizeof(enum sc_keycode) >= sizeof(SDL_Keycode), + "SDL_Keycode must be convertible to sc_keycode"); + +static_assert(sizeof(enum sc_scancode) >= sizeof(SDL_Scancode), + "SDL_Scancode must be convertible to sc_scancode"); + +enum sc_touch_action { + SC_TOUCH_ACTION_MOVE, + SC_TOUCH_ACTION_DOWN, + SC_TOUCH_ACTION_UP, +}; + +struct sc_key_event { + enum sc_action action; + enum sc_keycode keycode; + enum sc_scancode scancode; + uint16_t mods_state; // bitwise-OR of sc_mod values + bool repeat; +}; + +struct sc_text_event { + const char *text; // not owned +}; + +struct sc_mouse_click_event { + struct sc_position position; + enum sc_action action; + enum sc_mouse_button button; + uint8_t buttons_state; // bitwise-OR of sc_mouse_button values +}; + +struct sc_mouse_scroll_event { + struct sc_position position; + int32_t hscroll; + int32_t vscroll; +}; + +struct sc_mouse_motion_event { + struct sc_position position; + uint8_t buttons_state; // bitwise-OR of sc_mouse_button values +}; + +struct sc_touch_event { + struct sc_position position; + enum sc_touch_action action; + uint64_t pointer_id; + float pressure; +}; + +#endif From e4396e34c226bd5adfd6c011718a7f101967cce0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 29 Dec 2021 15:56:59 +0100 Subject: [PATCH 0928/2244] Use common sc_action in input manager Now that the scrcpy input events API exposes a sc_action enum, use the same from the input manager. --- app/src/input_manager.c | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 0688dcab..0ed364b1 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -3,13 +3,9 @@ #include #include +#include "input_events.h" #include "util/log.h" -enum sc_action { - SC_ACTION_DOWN, - SC_ACTION_UP, -}; - #define SC_SDL_SHORTCUT_MODS_MASK (KMOD_CTRL | KMOD_ALT | KMOD_GUI) static inline uint16_t From b4b638e8fe2a632b045478e6038e6eef19562c5e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 29 Dec 2021 16:14:34 +0100 Subject: [PATCH 0929/2244] Use scrcpy input events for key processors Pass scrcpy input events instead of SDL input events to key processors. This makes the source code of key processors independent of the SDL API. --- app/src/hid_keyboard.c | 50 ++++--- app/src/input_manager.c | 40 +++++- app/src/keyboard_inject.c | 259 +++++++++++++++++----------------- app/src/trait/key_processor.h | 8 +- 4 files changed, 194 insertions(+), 163 deletions(-) diff --git a/app/src/hid_keyboard.c b/app/src/hid_keyboard.c index cf8eab61..be5f8a19 100644 --- a/app/src/hid_keyboard.c +++ b/app/src/hid_keyboard.c @@ -1,8 +1,8 @@ #include "hid_keyboard.h" #include -#include +#include "input_events.h" #include "util/log.h" /** Downcast key processor to hid_keyboard */ @@ -201,30 +201,30 @@ static const unsigned char keyboard_report_desc[] = { */ static unsigned char -sdl_keymod_to_hid_modifiers(SDL_Keymod mod) { +sdl_keymod_to_hid_modifiers(uint16_t mod) { unsigned char modifiers = HID_MODIFIER_NONE; - if (mod & KMOD_LCTRL) { + if (mod & SC_MOD_LCTRL) { modifiers |= HID_MODIFIER_LEFT_CONTROL; } - if (mod & KMOD_LSHIFT) { + if (mod & SC_MOD_LSHIFT) { modifiers |= HID_MODIFIER_LEFT_SHIFT; } - if (mod & KMOD_LALT) { + if (mod & SC_MOD_LALT) { modifiers |= HID_MODIFIER_LEFT_ALT; } - if (mod & KMOD_LGUI) { + if (mod & SC_MOD_LGUI) { modifiers |= HID_MODIFIER_LEFT_GUI; } - if (mod & KMOD_RCTRL) { + if (mod & SC_MOD_RCTRL) { modifiers |= HID_MODIFIER_RIGHT_CONTROL; } - if (mod & KMOD_RSHIFT) { + if (mod & SC_MOD_RSHIFT) { modifiers |= HID_MODIFIER_RIGHT_SHIFT; } - if (mod & KMOD_RALT) { + if (mod & SC_MOD_RALT) { modifiers |= HID_MODIFIER_RIGHT_ALT; } - if (mod & KMOD_RGUI) { + if (mod & SC_MOD_RGUI) { modifiers |= HID_MODIFIER_RIGHT_GUI; } return modifiers; @@ -248,15 +248,15 @@ sc_hid_keyboard_event_init(struct sc_hid_event *hid_event) { } static inline bool -scancode_is_modifier(SDL_Scancode scancode) { - return scancode >= SDL_SCANCODE_LCTRL && scancode <= SDL_SCANCODE_RGUI; +scancode_is_modifier(enum sc_scancode scancode) { + return scancode >= SC_SCANCODE_LCTRL && scancode <= SC_SCANCODE_RGUI; } static bool convert_hid_keyboard_event(struct sc_hid_keyboard *kb, struct sc_hid_event *hid_event, - const SDL_KeyboardEvent *event) { - SDL_Scancode scancode = event->keysym.scancode; + const struct sc_key_event *event) { + enum sc_scancode scancode = event->scancode; assert(scancode >= 0); // SDL also generates events when only modifiers are pressed, we cannot @@ -272,11 +272,11 @@ convert_hid_keyboard_event(struct sc_hid_keyboard *kb, return false; } - unsigned char modifiers = sdl_keymod_to_hid_modifiers(event->keysym.mod); + unsigned char modifiers = sdl_keymod_to_hid_modifiers(event->mods_state); if (scancode < SC_HID_KEYBOARD_KEYS) { // Pressed is true and released is false - kb->keys[scancode] = (event->type == SDL_KEYDOWN); + kb->keys[scancode] = (event->action == SC_ACTION_DOWN); LOGV("keys[%02x] = %s", scancode, kb->keys[scancode] ? "true" : "false"); } @@ -306,17 +306,17 @@ convert_hid_keyboard_event(struct sc_hid_keyboard *kb, end: LOGV("hid keyboard: key %-4s scancode=%02x (%u) mod=%02x", - event->type == SDL_KEYDOWN ? "down" : "up", event->keysym.scancode, - event->keysym.scancode, modifiers); + event->action == SC_ACTION_DOWN ? "down" : "up", event->scancode, + event->scancode, modifiers); return true; } static bool -push_mod_lock_state(struct sc_hid_keyboard *kb, uint16_t sdl_mod) { - bool capslock = sdl_mod & KMOD_CAPS; - bool numlock = sdl_mod & KMOD_NUM; +push_mod_lock_state(struct sc_hid_keyboard *kb, uint16_t mods_state) { + bool capslock = mods_state & SC_MOD_CAPS; + bool numlock = mods_state & SC_MOD_NUM; if (!capslock && !numlock) { // Nothing to do return true; @@ -328,8 +328,6 @@ push_mod_lock_state(struct sc_hid_keyboard *kb, uint16_t sdl_mod) { return false; } -#define SC_SCANCODE_CAPSLOCK SDL_SCANCODE_CAPSLOCK -#define SC_SCANCODE_NUMLOCK SDL_SCANCODE_NUMLOCKCLEAR unsigned i = 0; if (capslock) { hid_event.buffer[HID_KEYBOARD_INDEX_KEYS + i] = SC_SCANCODE_CAPSLOCK; @@ -353,7 +351,7 @@ push_mod_lock_state(struct sc_hid_keyboard *kb, uint16_t sdl_mod) { static void sc_key_processor_process_key(struct sc_key_processor *kp, - const SDL_KeyboardEvent *event, + const struct sc_key_event *event, uint64_t ack_to_wait) { if (event->repeat) { // In USB HID protocol, key repeat is handled by the host (Android), so @@ -369,7 +367,7 @@ sc_key_processor_process_key(struct sc_key_processor *kp, if (!kb->mod_lock_synchronized) { // Inject CAPSLOCK and/or NUMLOCK if necessary to synchronize // keyboard state - if (push_mod_lock_state(kb, event->keysym.mod)) { + if (push_mod_lock_state(kb, event->mods_state)) { kb->mod_lock_synchronized = true; } } @@ -391,7 +389,7 @@ sc_key_processor_process_key(struct sc_key_processor *kp, static void sc_key_processor_process_text(struct sc_key_processor *kp, - const SDL_TextInputEvent *event) { + const struct sc_text_event *event) { (void) kp; (void) event; diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 0ed364b1..03d19b0e 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -6,6 +6,30 @@ #include "input_events.h" #include "util/log.h" +static inline uint16_t +sc_mods_state_from_sdl(uint16_t mods_state) { + return mods_state; +} + +static inline enum sc_keycode +sc_keycode_from_sdl(SDL_Keycode keycode) { + return (enum sc_keycode) keycode; +} + +static inline enum sc_scancode +sc_scancode_from_sdl(SDL_Scancode scancode) { + return (enum sc_scancode) scancode; +} + +static inline enum sc_action +sc_action_from_sdl_keyboard_type(uint32_t type) { + assert(type == SDL_KEYDOWN || type == SDL_KEYUP); + if (type == SDL_KEYDOWN) { + return SC_ACTION_DOWN; + } + return SC_ACTION_UP; +} + #define SC_SDL_SHORTCUT_MODS_MASK (KMOD_CTRL | KMOD_ALT | KMOD_GUI) static inline uint16_t @@ -317,7 +341,11 @@ input_manager_process_text_input(struct input_manager *im, return; } - im->kp->ops->process_text(im->kp, event); + struct sc_text_event evt = { + .text = event->text, + }; + + im->kp->ops->process_text(im->kp, &evt); } static bool @@ -536,7 +564,15 @@ input_manager_process_key(struct input_manager *im, } } - im->kp->ops->process_key(im->kp, event, ack_to_wait); + struct sc_key_event evt = { + .action = sc_action_from_sdl_keyboard_type(event->type), + .keycode = sc_keycode_from_sdl(event->keysym.sym), + .scancode = sc_scancode_from_sdl(event->keysym.scancode), + .repeat = event->repeat, + .mods_state = sc_mods_state_from_sdl(event->keysym.mod), + }; + + im->kp->ops->process_key(im->kp, &evt, ack_to_wait); } static void diff --git a/app/src/keyboard_inject.c b/app/src/keyboard_inject.c index 5143eafc..968a3c4f 100644 --- a/app/src/keyboard_inject.c +++ b/app/src/keyboard_inject.c @@ -1,11 +1,11 @@ #include "keyboard_inject.h" #include -#include #include "android/input.h" #include "control_msg.h" #include "controller.h" +#include "input_events.h" #include "util/intmap.h" #include "util/log.h" @@ -13,10 +13,10 @@ #define DOWNCAST(KP) container_of(KP, struct sc_keyboard_inject, key_processor) static bool -convert_keycode_action(SDL_EventType from, enum android_keyevent_action *to) { +convert_keycode_action(enum sc_action from, enum android_keyevent_action *to) { static const struct sc_intmap_entry actions[] = { - {SDL_KEYDOWN, AKEY_EVENT_ACTION_DOWN}, - {SDL_KEYUP, AKEY_EVENT_ACTION_UP}, + {SC_ACTION_DOWN, AKEY_EVENT_ACTION_DOWN}, + {SC_ACTION_UP, AKEY_EVENT_ACTION_UP}, }; const struct sc_intmap_entry *entry = SC_INTMAP_FIND_ENTRY(actions, from); @@ -29,125 +29,125 @@ convert_keycode_action(SDL_EventType from, enum android_keyevent_action *to) { } static bool -convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod, +convert_keycode(enum sc_keycode from, enum android_keycode *to, uint16_t mod, enum sc_key_inject_mode key_inject_mode) { // Navigation keys and ENTER. // Used in all modes. static const struct sc_intmap_entry special_keys[] = { - {SDLK_RETURN, AKEYCODE_ENTER}, - {SDLK_KP_ENTER, AKEYCODE_NUMPAD_ENTER}, - {SDLK_ESCAPE, AKEYCODE_ESCAPE}, - {SDLK_BACKSPACE, AKEYCODE_DEL}, - {SDLK_TAB, AKEYCODE_TAB}, - {SDLK_PAGEUP, AKEYCODE_PAGE_UP}, - {SDLK_DELETE, AKEYCODE_FORWARD_DEL}, - {SDLK_HOME, AKEYCODE_MOVE_HOME}, - {SDLK_END, AKEYCODE_MOVE_END}, - {SDLK_PAGEDOWN, AKEYCODE_PAGE_DOWN}, - {SDLK_RIGHT, AKEYCODE_DPAD_RIGHT}, - {SDLK_LEFT, AKEYCODE_DPAD_LEFT}, - {SDLK_DOWN, AKEYCODE_DPAD_DOWN}, - {SDLK_UP, AKEYCODE_DPAD_UP}, - {SDLK_LCTRL, AKEYCODE_CTRL_LEFT}, - {SDLK_RCTRL, AKEYCODE_CTRL_RIGHT}, - {SDLK_LSHIFT, AKEYCODE_SHIFT_LEFT}, - {SDLK_RSHIFT, AKEYCODE_SHIFT_RIGHT}, + {SC_KEYCODE_RETURN, AKEYCODE_ENTER}, + {SC_KEYCODE_KP_ENTER, AKEYCODE_NUMPAD_ENTER}, + {SC_KEYCODE_ESCAPE, AKEYCODE_ESCAPE}, + {SC_KEYCODE_BACKSPACE, AKEYCODE_DEL}, + {SC_KEYCODE_TAB, AKEYCODE_TAB}, + {SC_KEYCODE_PAGEUP, AKEYCODE_PAGE_UP}, + {SC_KEYCODE_DELETE, AKEYCODE_FORWARD_DEL}, + {SC_KEYCODE_HOME, AKEYCODE_MOVE_HOME}, + {SC_KEYCODE_END, AKEYCODE_MOVE_END}, + {SC_KEYCODE_PAGEDOWN, AKEYCODE_PAGE_DOWN}, + {SC_KEYCODE_RIGHT, AKEYCODE_DPAD_RIGHT}, + {SC_KEYCODE_LEFT, AKEYCODE_DPAD_LEFT}, + {SC_KEYCODE_DOWN, AKEYCODE_DPAD_DOWN}, + {SC_KEYCODE_UP, AKEYCODE_DPAD_UP}, + {SC_KEYCODE_LCTRL, AKEYCODE_CTRL_LEFT}, + {SC_KEYCODE_RCTRL, AKEYCODE_CTRL_RIGHT}, + {SC_KEYCODE_LSHIFT, AKEYCODE_SHIFT_LEFT}, + {SC_KEYCODE_RSHIFT, AKEYCODE_SHIFT_RIGHT}, }; // Numpad navigation keys. // Used in all modes, when NumLock and Shift are disabled. static const struct sc_intmap_entry kp_nav_keys[] = { - {SDLK_KP_0, AKEYCODE_INSERT}, - {SDLK_KP_1, AKEYCODE_MOVE_END}, - {SDLK_KP_2, AKEYCODE_DPAD_DOWN}, - {SDLK_KP_3, AKEYCODE_PAGE_DOWN}, - {SDLK_KP_4, AKEYCODE_DPAD_LEFT}, - {SDLK_KP_6, AKEYCODE_DPAD_RIGHT}, - {SDLK_KP_7, AKEYCODE_MOVE_HOME}, - {SDLK_KP_8, AKEYCODE_DPAD_UP}, - {SDLK_KP_9, AKEYCODE_PAGE_UP}, - {SDLK_KP_PERIOD, AKEYCODE_FORWARD_DEL}, + {SC_KEYCODE_KP_0, AKEYCODE_INSERT}, + {SC_KEYCODE_KP_1, AKEYCODE_MOVE_END}, + {SC_KEYCODE_KP_2, AKEYCODE_DPAD_DOWN}, + {SC_KEYCODE_KP_3, AKEYCODE_PAGE_DOWN}, + {SC_KEYCODE_KP_4, AKEYCODE_DPAD_LEFT}, + {SC_KEYCODE_KP_6, AKEYCODE_DPAD_RIGHT}, + {SC_KEYCODE_KP_7, AKEYCODE_MOVE_HOME}, + {SC_KEYCODE_KP_8, AKEYCODE_DPAD_UP}, + {SC_KEYCODE_KP_9, AKEYCODE_PAGE_UP}, + {SC_KEYCODE_KP_PERIOD, AKEYCODE_FORWARD_DEL}, }; // Letters and space. // Used in non-text mode. static const struct sc_intmap_entry alphaspace_keys[] = { - {SDLK_a, AKEYCODE_A}, - {SDLK_b, AKEYCODE_B}, - {SDLK_c, AKEYCODE_C}, - {SDLK_d, AKEYCODE_D}, - {SDLK_e, AKEYCODE_E}, - {SDLK_f, AKEYCODE_F}, - {SDLK_g, AKEYCODE_G}, - {SDLK_h, AKEYCODE_H}, - {SDLK_i, AKEYCODE_I}, - {SDLK_j, AKEYCODE_J}, - {SDLK_k, AKEYCODE_K}, - {SDLK_l, AKEYCODE_L}, - {SDLK_m, AKEYCODE_M}, - {SDLK_n, AKEYCODE_N}, - {SDLK_o, AKEYCODE_O}, - {SDLK_p, AKEYCODE_P}, - {SDLK_q, AKEYCODE_Q}, - {SDLK_r, AKEYCODE_R}, - {SDLK_s, AKEYCODE_S}, - {SDLK_t, AKEYCODE_T}, - {SDLK_u, AKEYCODE_U}, - {SDLK_v, AKEYCODE_V}, - {SDLK_w, AKEYCODE_W}, - {SDLK_x, AKEYCODE_X}, - {SDLK_y, AKEYCODE_Y}, - {SDLK_z, AKEYCODE_Z}, - {SDLK_SPACE, AKEYCODE_SPACE}, + {SC_KEYCODE_a, AKEYCODE_A}, + {SC_KEYCODE_b, AKEYCODE_B}, + {SC_KEYCODE_c, AKEYCODE_C}, + {SC_KEYCODE_d, AKEYCODE_D}, + {SC_KEYCODE_e, AKEYCODE_E}, + {SC_KEYCODE_f, AKEYCODE_F}, + {SC_KEYCODE_g, AKEYCODE_G}, + {SC_KEYCODE_h, AKEYCODE_H}, + {SC_KEYCODE_i, AKEYCODE_I}, + {SC_KEYCODE_j, AKEYCODE_J}, + {SC_KEYCODE_k, AKEYCODE_K}, + {SC_KEYCODE_l, AKEYCODE_L}, + {SC_KEYCODE_m, AKEYCODE_M}, + {SC_KEYCODE_n, AKEYCODE_N}, + {SC_KEYCODE_o, AKEYCODE_O}, + {SC_KEYCODE_p, AKEYCODE_P}, + {SC_KEYCODE_q, AKEYCODE_Q}, + {SC_KEYCODE_r, AKEYCODE_R}, + {SC_KEYCODE_s, AKEYCODE_S}, + {SC_KEYCODE_t, AKEYCODE_T}, + {SC_KEYCODE_u, AKEYCODE_U}, + {SC_KEYCODE_v, AKEYCODE_V}, + {SC_KEYCODE_w, AKEYCODE_W}, + {SC_KEYCODE_x, AKEYCODE_X}, + {SC_KEYCODE_y, AKEYCODE_Y}, + {SC_KEYCODE_z, AKEYCODE_Z}, + {SC_KEYCODE_SPACE, AKEYCODE_SPACE}, }; // Numbers and punctuation keys. // Used in raw mode only. static const struct sc_intmap_entry numbers_punct_keys[] = { - {SDLK_HASH, AKEYCODE_POUND}, - {SDLK_PERCENT, AKEYCODE_PERIOD}, - {SDLK_QUOTE, AKEYCODE_APOSTROPHE}, - {SDLK_ASTERISK, AKEYCODE_STAR}, - {SDLK_PLUS, AKEYCODE_PLUS}, - {SDLK_COMMA, AKEYCODE_COMMA}, - {SDLK_MINUS, AKEYCODE_MINUS}, - {SDLK_PERIOD, AKEYCODE_PERIOD}, - {SDLK_SLASH, AKEYCODE_SLASH}, - {SDLK_0, AKEYCODE_0}, - {SDLK_1, AKEYCODE_1}, - {SDLK_2, AKEYCODE_2}, - {SDLK_3, AKEYCODE_3}, - {SDLK_4, AKEYCODE_4}, - {SDLK_5, AKEYCODE_5}, - {SDLK_6, AKEYCODE_6}, - {SDLK_7, AKEYCODE_7}, - {SDLK_8, AKEYCODE_8}, - {SDLK_9, AKEYCODE_9}, - {SDLK_SEMICOLON, AKEYCODE_SEMICOLON}, - {SDLK_EQUALS, AKEYCODE_EQUALS}, - {SDLK_AT, AKEYCODE_AT}, - {SDLK_LEFTBRACKET, AKEYCODE_LEFT_BRACKET}, - {SDLK_BACKSLASH, AKEYCODE_BACKSLASH}, - {SDLK_RIGHTBRACKET, AKEYCODE_RIGHT_BRACKET}, - {SDLK_BACKQUOTE, AKEYCODE_GRAVE}, - {SDLK_KP_1, AKEYCODE_NUMPAD_1}, - {SDLK_KP_2, AKEYCODE_NUMPAD_2}, - {SDLK_KP_3, AKEYCODE_NUMPAD_3}, - {SDLK_KP_4, AKEYCODE_NUMPAD_4}, - {SDLK_KP_5, AKEYCODE_NUMPAD_5}, - {SDLK_KP_6, AKEYCODE_NUMPAD_6}, - {SDLK_KP_7, AKEYCODE_NUMPAD_7}, - {SDLK_KP_8, AKEYCODE_NUMPAD_8}, - {SDLK_KP_9, AKEYCODE_NUMPAD_9}, - {SDLK_KP_0, AKEYCODE_NUMPAD_0}, - {SDLK_KP_DIVIDE, AKEYCODE_NUMPAD_DIVIDE}, - {SDLK_KP_MULTIPLY, AKEYCODE_NUMPAD_MULTIPLY}, - {SDLK_KP_MINUS, AKEYCODE_NUMPAD_SUBTRACT}, - {SDLK_KP_PLUS, AKEYCODE_NUMPAD_ADD}, - {SDLK_KP_PERIOD, AKEYCODE_NUMPAD_DOT}, - {SDLK_KP_EQUALS, AKEYCODE_NUMPAD_EQUALS}, - {SDLK_KP_LEFTPAREN, AKEYCODE_NUMPAD_LEFT_PAREN}, - {SDLK_KP_RIGHTPAREN, AKEYCODE_NUMPAD_RIGHT_PAREN}, + {SC_KEYCODE_HASH, AKEYCODE_POUND}, + {SC_KEYCODE_PERCENT, AKEYCODE_PERIOD}, + {SC_KEYCODE_QUOTE, AKEYCODE_APOSTROPHE}, + {SC_KEYCODE_ASTERISK, AKEYCODE_STAR}, + {SC_KEYCODE_PLUS, AKEYCODE_PLUS}, + {SC_KEYCODE_COMMA, AKEYCODE_COMMA}, + {SC_KEYCODE_MINUS, AKEYCODE_MINUS}, + {SC_KEYCODE_PERIOD, AKEYCODE_PERIOD}, + {SC_KEYCODE_SLASH, AKEYCODE_SLASH}, + {SC_KEYCODE_0, AKEYCODE_0}, + {SC_KEYCODE_1, AKEYCODE_1}, + {SC_KEYCODE_2, AKEYCODE_2}, + {SC_KEYCODE_3, AKEYCODE_3}, + {SC_KEYCODE_4, AKEYCODE_4}, + {SC_KEYCODE_5, AKEYCODE_5}, + {SC_KEYCODE_6, AKEYCODE_6}, + {SC_KEYCODE_7, AKEYCODE_7}, + {SC_KEYCODE_8, AKEYCODE_8}, + {SC_KEYCODE_9, AKEYCODE_9}, + {SC_KEYCODE_SEMICOLON, AKEYCODE_SEMICOLON}, + {SC_KEYCODE_EQUALS, AKEYCODE_EQUALS}, + {SC_KEYCODE_AT, AKEYCODE_AT}, + {SC_KEYCODE_LEFTBRACKET, AKEYCODE_LEFT_BRACKET}, + {SC_KEYCODE_BACKSLASH, AKEYCODE_BACKSLASH}, + {SC_KEYCODE_RIGHTBRACKET, AKEYCODE_RIGHT_BRACKET}, + {SC_KEYCODE_BACKQUOTE, AKEYCODE_GRAVE}, + {SC_KEYCODE_KP_1, AKEYCODE_NUMPAD_1}, + {SC_KEYCODE_KP_2, AKEYCODE_NUMPAD_2}, + {SC_KEYCODE_KP_3, AKEYCODE_NUMPAD_3}, + {SC_KEYCODE_KP_4, AKEYCODE_NUMPAD_4}, + {SC_KEYCODE_KP_5, AKEYCODE_NUMPAD_5}, + {SC_KEYCODE_KP_6, AKEYCODE_NUMPAD_6}, + {SC_KEYCODE_KP_7, AKEYCODE_NUMPAD_7}, + {SC_KEYCODE_KP_8, AKEYCODE_NUMPAD_8}, + {SC_KEYCODE_KP_9, AKEYCODE_NUMPAD_9}, + {SC_KEYCODE_KP_0, AKEYCODE_NUMPAD_0}, + {SC_KEYCODE_KP_DIVIDE, AKEYCODE_NUMPAD_DIVIDE}, + {SC_KEYCODE_KP_MULTIPLY, AKEYCODE_NUMPAD_MULTIPLY}, + {SC_KEYCODE_KP_MINUS, AKEYCODE_NUMPAD_SUBTRACT}, + {SC_KEYCODE_KP_PLUS, AKEYCODE_NUMPAD_ADD}, + {SC_KEYCODE_KP_PERIOD, AKEYCODE_NUMPAD_DOT}, + {SC_KEYCODE_KP_EQUALS, AKEYCODE_NUMPAD_EQUALS}, + {SC_KEYCODE_KP_LEFTPAREN, AKEYCODE_NUMPAD_LEFT_PAREN}, + {SC_KEYCODE_KP_RIGHTPAREN, AKEYCODE_NUMPAD_RIGHT_PAREN}, }; const struct sc_intmap_entry *entry = @@ -157,7 +157,7 @@ convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod, return true; } - if (!(mod & (KMOD_NUM | KMOD_SHIFT))) { + if (!(mod & (SC_MOD_NUM | SC_MOD_LSHIFT | SC_MOD_RSHIFT))) { // Handle Numpad events when Num Lock is disabled // If SHIFT is pressed, a text event will be sent instead entry = SC_INTMAP_FIND_ENTRY(kp_nav_keys, from); @@ -167,12 +167,13 @@ convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod, } } - if (key_inject_mode == SC_KEY_INJECT_MODE_TEXT && !(mod & KMOD_CTRL)) { + if (key_inject_mode == SC_KEY_INJECT_MODE_TEXT && + !(mod & (SC_MOD_LCTRL | SC_MOD_RCTRL))) { // do not forward alpha and space key events (unless Ctrl is pressed) return false; } - if (mod & (KMOD_LALT | KMOD_RALT | KMOD_LGUI | KMOD_RGUI)) { + if (mod & (SC_MOD_LALT | SC_MOD_RALT | SC_MOD_LGUI | SC_MOD_RGUI)) { return false; } @@ -214,70 +215,66 @@ autocomplete_metastate(enum android_metastate metastate) { } static enum android_metastate -convert_meta_state(SDL_Keymod mod) { +convert_meta_state(uint16_t mod) { enum android_metastate metastate = 0; - if (mod & KMOD_LSHIFT) { + if (mod & SC_MOD_LSHIFT) { metastate |= AMETA_SHIFT_LEFT_ON; } - if (mod & KMOD_RSHIFT) { + if (mod & SC_MOD_RSHIFT) { metastate |= AMETA_SHIFT_RIGHT_ON; } - if (mod & KMOD_LCTRL) { + if (mod & SC_MOD_LCTRL) { metastate |= AMETA_CTRL_LEFT_ON; } - if (mod & KMOD_RCTRL) { + if (mod & SC_MOD_RCTRL) { metastate |= AMETA_CTRL_RIGHT_ON; } - if (mod & KMOD_LALT) { + if (mod & SC_MOD_LALT) { metastate |= AMETA_ALT_LEFT_ON; } - if (mod & KMOD_RALT) { + if (mod & SC_MOD_RALT) { metastate |= AMETA_ALT_RIGHT_ON; } - if (mod & KMOD_LGUI) { // Windows key + if (mod & SC_MOD_LGUI) { // Windows key metastate |= AMETA_META_LEFT_ON; } - if (mod & KMOD_RGUI) { // Windows key + if (mod & SC_MOD_RGUI) { // Windows key metastate |= AMETA_META_RIGHT_ON; } - if (mod & KMOD_NUM) { + if (mod & SC_MOD_NUM) { metastate |= AMETA_NUM_LOCK_ON; } - if (mod & KMOD_CAPS) { + if (mod & SC_MOD_CAPS) { metastate |= AMETA_CAPS_LOCK_ON; } - if (mod & KMOD_MODE) { // Alt Gr - // no mapping? - } // fill the dependent fields return autocomplete_metastate(metastate); } static bool -convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to, +convert_input_key(const struct sc_key_event *event, struct control_msg *msg, enum sc_key_inject_mode key_inject_mode, uint32_t repeat) { - to->type = CONTROL_MSG_TYPE_INJECT_KEYCODE; + msg->type = CONTROL_MSG_TYPE_INJECT_KEYCODE; - if (!convert_keycode_action(from->type, &to->inject_keycode.action)) { + if (!convert_keycode_action(event->action, &msg->inject_keycode.action)) { return false; } - uint16_t mod = from->keysym.mod; - if (!convert_keycode(from->keysym.sym, &to->inject_keycode.keycode, mod, - key_inject_mode)) { + if (!convert_keycode(event->keycode, &msg->inject_keycode.keycode, + event->mods_state, key_inject_mode)) { return false; } - to->inject_keycode.repeat = repeat; - to->inject_keycode.metastate = convert_meta_state(mod); + msg->inject_keycode.repeat = repeat; + msg->inject_keycode.metastate = convert_meta_state(event->mods_state); return true; } static void sc_key_processor_process_key(struct sc_key_processor *kp, - const SDL_KeyboardEvent *event, + const struct sc_key_event *event, uint64_t ack_to_wait) { // The device clipboard synchronization and the key event messages are // serialized, there is nothing special to do to ensure that the clipboard @@ -305,7 +302,7 @@ sc_key_processor_process_key(struct sc_key_processor *kp, static void sc_key_processor_process_text(struct sc_key_processor *kp, - const SDL_TextInputEvent *event) { + const struct sc_text_event *event) { struct sc_keyboard_inject *ki = DOWNCAST(kp); if (ki->key_inject_mode == SC_KEY_INJECT_MODE_RAW) { diff --git a/app/src/trait/key_processor.h b/app/src/trait/key_processor.h index f4afe27b..045e5e7b 100644 --- a/app/src/trait/key_processor.h +++ b/app/src/trait/key_processor.h @@ -6,7 +6,7 @@ #include #include -#include +#include "input_events.h" /** * Key processor trait. @@ -37,12 +37,12 @@ struct sc_key_processor_ops { * Ctrl+v on the device. */ void - (*process_key)(struct sc_key_processor *kp, const SDL_KeyboardEvent *event, - uint64_t ack_to_wait); + (*process_key)(struct sc_key_processor *kp, + const struct sc_key_event *event, uint64_t ack_to_wait); void (*process_text)(struct sc_key_processor *kp, - const SDL_TextInputEvent *event); + const struct sc_text_event *event); }; #endif From 9460bdd87ba76f03ff27e98973073a6acd8ed306 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 29 Dec 2021 16:24:20 +0100 Subject: [PATCH 0930/2244] Use scrcpy input events for mouse processors Pass scrcpy input events instead of SDL input events to mouse processors. These events represent exactly what mouse processors need, abstracted from any visual orientation and scaling applied on the SDL window. This makes the mouse processors independent of the "screen" instance, and the implementation source code independent of the SDL API. --- app/src/input_manager.c | 101 ++++++++++++++++++++- app/src/mouse_inject.c | 152 +++++++++++++------------------- app/src/mouse_inject.h | 4 +- app/src/scrcpy.c | 2 +- app/src/trait/mouse_processor.h | 14 +-- 5 files changed, 169 insertions(+), 104 deletions(-) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 03d19b0e..ba6a4e2a 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -30,6 +30,44 @@ sc_action_from_sdl_keyboard_type(uint32_t type) { return SC_ACTION_UP; } +static inline enum sc_action +sc_action_from_sdl_mousebutton_type(uint32_t type) { + assert(type == SDL_MOUSEBUTTONDOWN || type == SDL_MOUSEBUTTONUP); + if (type == SDL_MOUSEBUTTONDOWN) { + return SC_ACTION_DOWN; + } + return SC_ACTION_UP; +} + +static inline enum sc_touch_action +sc_touch_action_from_sdl(uint32_t type) { + assert(type == SDL_FINGERMOTION || type == SDL_FINGERDOWN || + type == SDL_FINGERUP); + if (type == SDL_FINGERMOTION) { + return SC_TOUCH_ACTION_MOVE; + } + if (type == SDL_FINGERDOWN) { + return SC_TOUCH_ACTION_DOWN; + } + return SC_TOUCH_ACTION_UP; +} + +static inline enum sc_mouse_button +sc_mouse_button_from_sdl(uint8_t button) { + if (button >= SDL_BUTTON_LEFT && button <= SDL_BUTTON_X2) { + // SC_MOUSE_BUTTON_* constants are initialized from SDL_BUTTON(index) + return SDL_BUTTON(button); + } + + return SC_MOUSE_BUTTON_UNKNOWN; +} + +static inline uint8_t +sc_mouse_buttons_state_from_sdl(uint32_t buttons_state) { + assert(buttons_state < 0x100); // fits in uint8_t + return buttons_state; +} + #define SC_SDL_SHORTCUT_MODS_MASK (KMOD_CTRL | KMOD_ALT | KMOD_GUI) static inline uint16_t @@ -591,7 +629,16 @@ input_manager_process_mouse_motion(struct input_manager *im, return; } - im->mp->ops->process_mouse_motion(im->mp, event); + struct sc_mouse_motion_event evt = { + .position = { + .screen_size = im->screen->frame_size, + .point = screen_convert_window_to_frame_coords(im->screen, + event->x, event->y), + }, + .buttons_state = sc_mouse_buttons_state_from_sdl(event->state), + }; + + im->mp->ops->process_mouse_motion(im->mp, &evt); if (im->vfinger_down) { struct sc_point mouse = @@ -605,7 +652,25 @@ input_manager_process_mouse_motion(struct input_manager *im, static void input_manager_process_touch(struct input_manager *im, const SDL_TouchFingerEvent *event) { - im->mp->ops->process_touch(im->mp, event); + int dw; + int dh; + SDL_GL_GetDrawableSize(im->screen->window, &dw, &dh); + + // SDL touch event coordinates are normalized in the range [0; 1] + int32_t x = event->x * dw; + int32_t y = event->y * dh; + + struct sc_touch_event evt = { + .position = { + .screen_size = im->screen->frame_size, + .point = screen_convert_drawable_to_frame_coords(im->screen, x, y), + }, + .action = sc_touch_action_from_sdl(event->type), + .pointer_id = event->fingerId, + .pressure = event->pressure, + }; + + im->mp->ops->process_touch(im->mp, &evt); } static void @@ -665,7 +730,20 @@ input_manager_process_mouse_button(struct input_manager *im, return; } - im->mp->ops->process_mouse_button(im->mp, event); + uint32_t sdl_buttons_state = SDL_GetMouseState(NULL, NULL); + + struct sc_mouse_click_event evt = { + .position = { + .screen_size = im->screen->frame_size, + .point = screen_convert_window_to_frame_coords(im->screen, event->x, + event->y), + }, + .action = sc_action_from_sdl_mousebutton_type(event->type), + .button = sc_mouse_button_from_sdl(event->button), + .buttons_state = sc_mouse_buttons_state_from_sdl(sdl_buttons_state), + }; + + im->mp->ops->process_mouse_click(im->mp, &evt); // Pinch-to-zoom simulation. // @@ -696,7 +774,22 @@ input_manager_process_mouse_button(struct input_manager *im, static void input_manager_process_mouse_wheel(struct input_manager *im, const SDL_MouseWheelEvent *event) { - im->mp->ops->process_mouse_wheel(im->mp, event); + // 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 sc_mouse_scroll_event evt = { + .position = { + .screen_size = im->screen->frame_size, + .point = screen_convert_window_to_frame_coords(im->screen, + mouse_x, mouse_y), + }, + .hscroll = event->x, + .vscroll = event->y, + }; + + im->mp->ops->process_mouse_scroll(im->mp, &evt); } bool diff --git a/app/src/mouse_inject.c b/app/src/mouse_inject.c index 8f7e363d..a4d2e1a7 100644 --- a/app/src/mouse_inject.c +++ b/app/src/mouse_inject.c @@ -1,11 +1,11 @@ #include "mouse_inject.h" #include -#include #include "android/input.h" #include "control_msg.h" #include "controller.h" +#include "input_events.h" #include "util/intmap.h" #include "util/log.h" @@ -15,29 +15,29 @@ static enum android_motionevent_buttons convert_mouse_buttons(uint32_t state) { enum android_motionevent_buttons buttons = 0; - if (state & SDL_BUTTON_LMASK) { + if (state & SC_MOUSE_BUTTON_LEFT) { buttons |= AMOTION_EVENT_BUTTON_PRIMARY; } - if (state & SDL_BUTTON_RMASK) { + if (state & SC_MOUSE_BUTTON_RIGHT) { buttons |= AMOTION_EVENT_BUTTON_SECONDARY; } - if (state & SDL_BUTTON_MMASK) { + if (state & SC_MOUSE_BUTTON_MIDDLE) { buttons |= AMOTION_EVENT_BUTTON_TERTIARY; } - if (state & SDL_BUTTON_X1MASK) { + if (state & SC_MOUSE_BUTTON_X1) { buttons |= AMOTION_EVENT_BUTTON_BACK; } - if (state & SDL_BUTTON_X2MASK) { + if (state & SC_MOUSE_BUTTON_X2) { buttons |= AMOTION_EVENT_BUTTON_FORWARD; } return buttons; } static bool -convert_mouse_action(SDL_EventType from, enum android_motionevent_action *to) { +convert_mouse_action(enum sc_action from, enum android_motionevent_action *to) { static const struct sc_intmap_entry actions[] = { - {SDL_MOUSEBUTTONDOWN, AMOTION_EVENT_ACTION_DOWN}, - {SDL_MOUSEBUTTONUP, AMOTION_EVENT_ACTION_UP}, + {SC_ACTION_DOWN, AMOTION_EVENT_ACTION_DOWN}, + {SC_ACTION_UP, AMOTION_EVENT_ACTION_UP}, }; const struct sc_intmap_entry *entry = SC_INTMAP_FIND_ENTRY(actions, from); @@ -50,11 +50,12 @@ convert_mouse_action(SDL_EventType from, enum android_motionevent_action *to) { } static bool -convert_touch_action(SDL_EventType from, enum android_motionevent_action *to) { +convert_touch_action(enum sc_touch_action from, + enum android_motionevent_action *to) { static const struct sc_intmap_entry actions[] = { - {SDL_FINGERMOTION, AMOTION_EVENT_ACTION_MOVE}, - {SDL_FINGERDOWN, AMOTION_EVENT_ACTION_DOWN}, - {SDL_FINGERUP, AMOTION_EVENT_ACTION_UP}, + {SC_TOUCH_ACTION_MOVE, AMOTION_EVENT_ACTION_MOVE}, + {SC_TOUCH_ACTION_DOWN, AMOTION_EVENT_ACTION_DOWN}, + {SC_TOUCH_ACTION_UP, AMOTION_EVENT_ACTION_UP}, }; const struct sc_intmap_entry *entry = SC_INTMAP_FIND_ENTRY(actions, from); @@ -67,99 +68,73 @@ convert_touch_action(SDL_EventType from, enum android_motionevent_action *to) { } static bool -convert_mouse_motion(const SDL_MouseMotionEvent *from, struct screen *screen, - struct control_msg *to) { - to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT; - 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 = - 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); +convert_mouse_motion(const struct sc_mouse_motion_event *event, + struct control_msg *msg) { + msg->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT; + msg->inject_touch_event.action = AMOTION_EVENT_ACTION_MOVE; + msg->inject_touch_event.pointer_id = POINTER_ID_MOUSE; + msg->inject_touch_event.position = event->position; + msg->inject_touch_event.pressure = 1.f; + msg->inject_touch_event.buttons = + convert_mouse_buttons(event->buttons_state); return true; } static bool -convert_touch(const SDL_TouchFingerEvent *from, struct screen *screen, - struct control_msg *to) { - to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT; +convert_touch(const struct sc_touch_event *event, + struct control_msg *msg) { + msg->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT; - if (!convert_touch_action(from->type, &to->inject_touch_event.action)) { + if (!convert_touch_action(event->action, &msg->inject_touch_event.action)) { return false; } - to->inject_touch_event.pointer_id = from->fingerId; - to->inject_touch_event.position.screen_size = screen->frame_size; + msg->inject_touch_event.pointer_id = event->pointer_id; + msg->inject_touch_event.position = event->position; + msg->inject_touch_event.pressure = event->pressure; + msg->inject_touch_event.buttons = 0; - 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 * dw; - int32_t y = from->y * dh; - to->inject_touch_event.position.point = - screen_convert_drawable_to_frame_coords(screen, x, y); - - to->inject_touch_event.pressure = from->pressure; - to->inject_touch_event.buttons = 0; return true; } static bool -convert_mouse_button(const SDL_MouseButtonEvent *from, struct screen *screen, - struct control_msg *to) { - to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT; +convert_mouse_click(const struct sc_mouse_click_event *event, + struct control_msg *msg) { + msg->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT; - if (!convert_mouse_action(from->type, &to->inject_touch_event.action)) { + if (!convert_mouse_action(event->action, &msg->inject_touch_event.action)) { return false; } - 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_window_to_frame_coords(screen, from->x, from->y); - 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)); + msg->inject_touch_event.pointer_id = POINTER_ID_MOUSE; + msg->inject_touch_event.position = event->position; + msg->inject_touch_event.pressure = event->action == SC_ACTION_DOWN + ? 1.f : 0.f; + msg->inject_touch_event.buttons = + convert_mouse_buttons(event->buttons_state); return true; } 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 sc_position position = { - .screen_size = screen->frame_size, - .point = screen_convert_window_to_frame_coords(screen, - mouse_x, mouse_y), - }; - - to->type = CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT; - - to->inject_scroll_event.position = position; - to->inject_scroll_event.hscroll = from->x; - to->inject_scroll_event.vscroll = from->y; +convert_mouse_scroll(const struct sc_mouse_scroll_event *event, + struct control_msg *msg) { + msg->type = CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT; + msg->inject_scroll_event.position = event->position; + msg->inject_scroll_event.hscroll = event->hscroll; + msg->inject_scroll_event.vscroll = event->vscroll; return true; } static void sc_mouse_processor_process_mouse_motion(struct sc_mouse_processor *mp, - const SDL_MouseMotionEvent *event) { + const struct sc_mouse_motion_event *event) { struct sc_mouse_inject *mi = DOWNCAST(mp); struct control_msg msg; - if (!convert_mouse_motion(event, mi->screen, &msg)) { + if (!convert_mouse_motion(event, &msg)) { return; } @@ -170,11 +145,11 @@ sc_mouse_processor_process_mouse_motion(struct sc_mouse_processor *mp, static void sc_mouse_processor_process_touch(struct sc_mouse_processor *mp, - const SDL_TouchFingerEvent *event) { + const struct sc_touch_event *event) { struct sc_mouse_inject *mi = DOWNCAST(mp); struct control_msg msg; - if (convert_touch(event, mi->screen, &msg)) { + if (convert_touch(event, &msg)) { if (!controller_push_msg(mi->controller, &msg)) { LOGW("Could not request 'inject touch event'"); } @@ -182,42 +157,41 @@ sc_mouse_processor_process_touch(struct sc_mouse_processor *mp, } static void -sc_mouse_processor_process_mouse_button(struct sc_mouse_processor *mp, - const SDL_MouseButtonEvent *event) { +sc_mouse_processor_process_mouse_click(struct sc_mouse_processor *mp, + const struct sc_mouse_click_event *event) { struct sc_mouse_inject *mi = DOWNCAST(mp); struct control_msg msg; - if (convert_mouse_button(event, mi->screen, &msg)) { + if (convert_mouse_click(event, &msg)) { if (!controller_push_msg(mi->controller, &msg)) { - LOGW("Could not request 'inject mouse button event'"); + LOGW("Could not request 'inject mouse click event'"); } } } static void -sc_mouse_processor_process_mouse_wheel(struct sc_mouse_processor *mp, - const SDL_MouseWheelEvent *event) { +sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp, + const struct sc_mouse_scroll_event *event) { struct sc_mouse_inject *mi = DOWNCAST(mp); struct control_msg msg; - if (convert_mouse_wheel(event, mi->screen, &msg)) { + if (convert_mouse_scroll(event, &msg)) { if (!controller_push_msg(mi->controller, &msg)) { - LOGW("Could not request 'inject mouse wheel event'"); + LOGW("Could not request 'inject mouse scroll event'"); } } } void -sc_mouse_inject_init(struct sc_mouse_inject *mi, struct controller *controller, - struct screen *screen) { +sc_mouse_inject_init(struct sc_mouse_inject *mi, + struct controller *controller) { mi->controller = controller; - mi->screen = screen; static const struct sc_mouse_processor_ops ops = { .process_mouse_motion = sc_mouse_processor_process_mouse_motion, .process_touch = sc_mouse_processor_process_touch, - .process_mouse_button = sc_mouse_processor_process_mouse_button, - .process_mouse_wheel = sc_mouse_processor_process_mouse_wheel, + .process_mouse_click = sc_mouse_processor_process_mouse_click, + .process_mouse_scroll = sc_mouse_processor_process_mouse_scroll, }; mi->mouse_processor.ops = &ops; diff --git a/app/src/mouse_inject.h b/app/src/mouse_inject.h index 7dcf7e83..50591e2b 100644 --- a/app/src/mouse_inject.h +++ b/app/src/mouse_inject.h @@ -13,11 +13,9 @@ struct sc_mouse_inject { struct sc_mouse_processor mouse_processor; // mouse processor trait struct controller *controller; - struct screen *screen; }; void -sc_mouse_inject_init(struct sc_mouse_inject *mi, struct controller *controller, - struct screen *screen); +sc_mouse_inject_init(struct sc_mouse_inject *mi, struct controller *controller); #endif diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 3a271ee5..51eb19fb 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -578,7 +578,7 @@ aoa_hid_end: kp = &s->keyboard_inject.key_processor; } - sc_mouse_inject_init(&s->mouse_inject, &s->controller, &s->screen); + sc_mouse_inject_init(&s->mouse_inject, &s->controller); mp = &s->mouse_inject.mouse_processor; } diff --git a/app/src/trait/mouse_processor.h b/app/src/trait/mouse_processor.h index f3548574..e06545bf 100644 --- a/app/src/trait/mouse_processor.h +++ b/app/src/trait/mouse_processor.h @@ -6,7 +6,7 @@ #include #include -#include +#include "input_events.h" /** * Mouse processor trait. @@ -21,19 +21,19 @@ struct sc_mouse_processor { struct sc_mouse_processor_ops { void (*process_mouse_motion)(struct sc_mouse_processor *mp, - const SDL_MouseMotionEvent *event); + const struct sc_mouse_motion_event *event); void (*process_touch)(struct sc_mouse_processor *mp, - const SDL_TouchFingerEvent *event); + const struct sc_touch_event *event); void - (*process_mouse_button)(struct sc_mouse_processor *mp, - const SDL_MouseButtonEvent *event); + (*process_mouse_click)(struct sc_mouse_processor *mp, + const struct sc_mouse_click_event *event); void - (*process_mouse_wheel)(struct sc_mouse_processor *mp, - const SDL_MouseWheelEvent *event); + (*process_mouse_scroll)(struct sc_mouse_processor *mp, + const struct sc_mouse_scroll_event *event); }; #endif From a1f2f5fbd3591be594e1b70fe08f05e7e562c123 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 29 Dec 2021 16:55:01 +0100 Subject: [PATCH 0931/2244] Make some event conversions infallible When the implementation handles all possible input values, it may never fail. --- app/src/keyboard_inject.c | 24 +++------ app/src/mouse_inject.c | 100 +++++++++++++------------------------- 2 files changed, 42 insertions(+), 82 deletions(-) diff --git a/app/src/keyboard_inject.c b/app/src/keyboard_inject.c index 968a3c4f..76357d85 100644 --- a/app/src/keyboard_inject.c +++ b/app/src/keyboard_inject.c @@ -12,20 +12,13 @@ /** Downcast key processor to sc_keyboard_inject */ #define DOWNCAST(KP) container_of(KP, struct sc_keyboard_inject, key_processor) -static bool -convert_keycode_action(enum sc_action from, enum android_keyevent_action *to) { - static const struct sc_intmap_entry actions[] = { - {SC_ACTION_DOWN, AKEY_EVENT_ACTION_DOWN}, - {SC_ACTION_UP, AKEY_EVENT_ACTION_UP}, - }; - - const struct sc_intmap_entry *entry = SC_INTMAP_FIND_ENTRY(actions, from); - if (entry) { - *to = entry->value; - return true; +static enum android_keyevent_action +convert_keycode_action(enum sc_action action) { + if (action == SC_ACTION_DOWN) { + return AKEY_EVENT_ACTION_DOWN; } - - return false; + assert(action == SC_ACTION_UP); + return AKEY_EVENT_ACTION_UP; } static bool @@ -257,15 +250,12 @@ convert_input_key(const struct sc_key_event *event, struct control_msg *msg, enum sc_key_inject_mode key_inject_mode, uint32_t repeat) { msg->type = CONTROL_MSG_TYPE_INJECT_KEYCODE; - if (!convert_keycode_action(event->action, &msg->inject_keycode.action)) { - return false; - } - if (!convert_keycode(event->keycode, &msg->inject_keycode.keycode, event->mods_state, key_inject_mode)) { return false; } + msg->inject_keycode.action = convert_keycode_action(event->action); msg->inject_keycode.repeat = repeat; msg->inject_keycode.metastate = convert_meta_state(event->mods_state); diff --git a/app/src/mouse_inject.c b/app/src/mouse_inject.c index a4d2e1a7..99b0b858 100644 --- a/app/src/mouse_inject.c +++ b/app/src/mouse_inject.c @@ -33,41 +33,29 @@ convert_mouse_buttons(uint32_t state) { return buttons; } -static bool -convert_mouse_action(enum sc_action from, enum android_motionevent_action *to) { - static const struct sc_intmap_entry actions[] = { - {SC_ACTION_DOWN, AMOTION_EVENT_ACTION_DOWN}, - {SC_ACTION_UP, AMOTION_EVENT_ACTION_UP}, - }; - - const struct sc_intmap_entry *entry = SC_INTMAP_FIND_ENTRY(actions, from); - if (entry) { - *to = entry->value; - return true; +static enum android_motionevent_action +convert_mouse_action(enum sc_action action) { + if (action == SC_ACTION_DOWN) { + return AMOTION_EVENT_ACTION_DOWN; } - - return false; + assert(action == SC_ACTION_UP); + return AMOTION_EVENT_ACTION_UP; } -static bool -convert_touch_action(enum sc_touch_action from, - enum android_motionevent_action *to) { - static const struct sc_intmap_entry actions[] = { - {SC_TOUCH_ACTION_MOVE, AMOTION_EVENT_ACTION_MOVE}, - {SC_TOUCH_ACTION_DOWN, AMOTION_EVENT_ACTION_DOWN}, - {SC_TOUCH_ACTION_UP, AMOTION_EVENT_ACTION_UP}, - }; - - const struct sc_intmap_entry *entry = SC_INTMAP_FIND_ENTRY(actions, from); - if (entry) { - *to = entry->value; - return true; +static enum android_motionevent_action +convert_touch_action(enum sc_touch_action action) { + switch (action) { + case SC_TOUCH_ACTION_MOVE: + return AMOTION_EVENT_ACTION_MOVE; + case SC_TOUCH_ACTION_DOWN: + return AMOTION_EVENT_ACTION_DOWN; + default: + assert(action == SC_TOUCH_ACTION_UP); + return AMOTION_EVENT_ACTION_UP; } - - return false; } -static bool +static void convert_mouse_motion(const struct sc_mouse_motion_event *event, struct control_msg *msg) { msg->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT; @@ -77,55 +65,39 @@ convert_mouse_motion(const struct sc_mouse_motion_event *event, msg->inject_touch_event.pressure = 1.f; msg->inject_touch_event.buttons = convert_mouse_buttons(event->buttons_state); - - return true; } -static bool +static void convert_touch(const struct sc_touch_event *event, struct control_msg *msg) { msg->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT; - - if (!convert_touch_action(event->action, &msg->inject_touch_event.action)) { - return false; - } - + msg->inject_touch_event.action = convert_touch_action(event->action); msg->inject_touch_event.pointer_id = event->pointer_id; msg->inject_touch_event.position = event->position; msg->inject_touch_event.pressure = event->pressure; msg->inject_touch_event.buttons = 0; - - return true; } -static bool +static void convert_mouse_click(const struct sc_mouse_click_event *event, struct control_msg *msg) { msg->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT; - - if (!convert_mouse_action(event->action, &msg->inject_touch_event.action)) { - return false; - } - + msg->inject_touch_event.action = convert_mouse_action(event->action); msg->inject_touch_event.pointer_id = POINTER_ID_MOUSE; msg->inject_touch_event.position = event->position; msg->inject_touch_event.pressure = event->action == SC_ACTION_DOWN ? 1.f : 0.f; msg->inject_touch_event.buttons = convert_mouse_buttons(event->buttons_state); - - return true; } -static bool +static void convert_mouse_scroll(const struct sc_mouse_scroll_event *event, struct control_msg *msg) { msg->type = CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT; msg->inject_scroll_event.position = event->position; msg->inject_scroll_event.hscroll = event->hscroll; msg->inject_scroll_event.vscroll = event->vscroll; - - return true; } static void @@ -134,9 +106,7 @@ sc_mouse_processor_process_mouse_motion(struct sc_mouse_processor *mp, struct sc_mouse_inject *mi = DOWNCAST(mp); struct control_msg msg; - if (!convert_mouse_motion(event, &msg)) { - return; - } + convert_mouse_motion(event, &msg); if (!controller_push_msg(mi->controller, &msg)) { LOGW("Could not request 'inject mouse motion event'"); @@ -149,10 +119,10 @@ sc_mouse_processor_process_touch(struct sc_mouse_processor *mp, struct sc_mouse_inject *mi = DOWNCAST(mp); struct control_msg msg; - if (convert_touch(event, &msg)) { - if (!controller_push_msg(mi->controller, &msg)) { - LOGW("Could not request 'inject touch event'"); - } + convert_touch(event, &msg); + + if (!controller_push_msg(mi->controller, &msg)) { + LOGW("Could not request 'inject touch event'"); } } @@ -162,10 +132,10 @@ sc_mouse_processor_process_mouse_click(struct sc_mouse_processor *mp, struct sc_mouse_inject *mi = DOWNCAST(mp); struct control_msg msg; - if (convert_mouse_click(event, &msg)) { - if (!controller_push_msg(mi->controller, &msg)) { - LOGW("Could not request 'inject mouse click event'"); - } + convert_mouse_click(event, &msg); + + if (!controller_push_msg(mi->controller, &msg)) { + LOGW("Could not request 'inject mouse click event'"); } } @@ -175,10 +145,10 @@ sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp, struct sc_mouse_inject *mi = DOWNCAST(mp); struct control_msg msg; - if (convert_mouse_scroll(event, &msg)) { - if (!controller_push_msg(mi->controller, &msg)) { - LOGW("Could not request 'inject mouse scroll event'"); - } + convert_mouse_scroll(event, &msg); + + if (!controller_push_msg(mi->controller, &msg)) { + LOGW("Could not request 'inject mouse scroll event'"); } } From 96e0e8974065312305642cbd2bc203d5f48422cd Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 29 Dec 2021 17:10:12 +0100 Subject: [PATCH 0932/2244] Simplify mouse injection implementation The static functions are now so simple that they become unnecessary: the control message may be initialized directly instead. --- app/src/mouse_inject.c | 91 ++++++++++++++++++------------------------ 1 file changed, 38 insertions(+), 53 deletions(-) diff --git a/app/src/mouse_inject.c b/app/src/mouse_inject.c index 99b0b858..887fc0b1 100644 --- a/app/src/mouse_inject.c +++ b/app/src/mouse_inject.c @@ -55,58 +55,21 @@ convert_touch_action(enum sc_touch_action action) { } } -static void -convert_mouse_motion(const struct sc_mouse_motion_event *event, - struct control_msg *msg) { - msg->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT; - msg->inject_touch_event.action = AMOTION_EVENT_ACTION_MOVE; - msg->inject_touch_event.pointer_id = POINTER_ID_MOUSE; - msg->inject_touch_event.position = event->position; - msg->inject_touch_event.pressure = 1.f; - msg->inject_touch_event.buttons = - convert_mouse_buttons(event->buttons_state); -} - -static void -convert_touch(const struct sc_touch_event *event, - struct control_msg *msg) { - msg->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT; - msg->inject_touch_event.action = convert_touch_action(event->action); - msg->inject_touch_event.pointer_id = event->pointer_id; - msg->inject_touch_event.position = event->position; - msg->inject_touch_event.pressure = event->pressure; - msg->inject_touch_event.buttons = 0; -} - -static void -convert_mouse_click(const struct sc_mouse_click_event *event, - struct control_msg *msg) { - msg->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT; - msg->inject_touch_event.action = convert_mouse_action(event->action); - msg->inject_touch_event.pointer_id = POINTER_ID_MOUSE; - msg->inject_touch_event.position = event->position; - msg->inject_touch_event.pressure = event->action == SC_ACTION_DOWN - ? 1.f : 0.f; - msg->inject_touch_event.buttons = - convert_mouse_buttons(event->buttons_state); -} - -static void -convert_mouse_scroll(const struct sc_mouse_scroll_event *event, - struct control_msg *msg) { - msg->type = CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT; - msg->inject_scroll_event.position = event->position; - msg->inject_scroll_event.hscroll = event->hscroll; - msg->inject_scroll_event.vscroll = event->vscroll; -} - static void sc_mouse_processor_process_mouse_motion(struct sc_mouse_processor *mp, const struct sc_mouse_motion_event *event) { struct sc_mouse_inject *mi = DOWNCAST(mp); - struct control_msg msg; - convert_mouse_motion(event, &msg); + struct control_msg msg = { + .type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, + .inject_touch_event = { + .action = AMOTION_EVENT_ACTION_MOVE, + .pointer_id = POINTER_ID_MOUSE, + .position = event->position, + .pressure = 1.f, + .buttons = convert_mouse_buttons(event->buttons_state), + }, + }; if (!controller_push_msg(mi->controller, &msg)) { LOGW("Could not request 'inject mouse motion event'"); @@ -118,8 +81,16 @@ sc_mouse_processor_process_touch(struct sc_mouse_processor *mp, const struct sc_touch_event *event) { struct sc_mouse_inject *mi = DOWNCAST(mp); - struct control_msg msg; - convert_touch(event, &msg); + struct control_msg msg = { + .type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, + .inject_touch_event = { + .action = convert_touch_action(event->action), + .pointer_id = event->pointer_id, + .position = event->position, + .pressure = event->pressure, + .buttons = 0, + }, + }; if (!controller_push_msg(mi->controller, &msg)) { LOGW("Could not request 'inject touch event'"); @@ -131,8 +102,16 @@ sc_mouse_processor_process_mouse_click(struct sc_mouse_processor *mp, const struct sc_mouse_click_event *event) { struct sc_mouse_inject *mi = DOWNCAST(mp); - struct control_msg msg; - convert_mouse_click(event, &msg); + struct control_msg msg = { + .type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, + .inject_touch_event = { + .action = convert_mouse_action(event->action), + .pointer_id = POINTER_ID_MOUSE, + .position = event->position, + .pressure = event->action == SC_ACTION_DOWN ? 1.f : 0.f, + .buttons = convert_mouse_buttons(event->buttons_state), + }, + }; if (!controller_push_msg(mi->controller, &msg)) { LOGW("Could not request 'inject mouse click event'"); @@ -144,8 +123,14 @@ sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp, const struct sc_mouse_scroll_event *event) { struct sc_mouse_inject *mi = DOWNCAST(mp); - struct control_msg msg; - convert_mouse_scroll(event, &msg); + struct control_msg msg = { + .type = CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT, + .inject_scroll_event = { + .position = event->position, + .hscroll = event->hscroll, + .vscroll = event->vscroll, + }, + }; if (!controller_push_msg(mi->controller, &msg)) { LOGW("Could not request 'inject mouse scroll event'"); From 3c15cbdaf89d352c0b2e79debe9ae04a8100e8b1 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 29 Dec 2021 23:20:35 +0100 Subject: [PATCH 0933/2244] Reorder mouse processor ops Group the mouse events callbacks before the touch event callback. --- app/src/mouse_inject.c | 44 ++++++++++++++++----------------- app/src/trait/mouse_processor.h | 8 +++--- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/app/src/mouse_inject.c b/app/src/mouse_inject.c index 887fc0b1..1d14509d 100644 --- a/app/src/mouse_inject.c +++ b/app/src/mouse_inject.c @@ -76,27 +76,6 @@ sc_mouse_processor_process_mouse_motion(struct sc_mouse_processor *mp, } } -static void -sc_mouse_processor_process_touch(struct sc_mouse_processor *mp, - const struct sc_touch_event *event) { - struct sc_mouse_inject *mi = DOWNCAST(mp); - - struct control_msg msg = { - .type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, - .inject_touch_event = { - .action = convert_touch_action(event->action), - .pointer_id = event->pointer_id, - .position = event->position, - .pressure = event->pressure, - .buttons = 0, - }, - }; - - if (!controller_push_msg(mi->controller, &msg)) { - LOGW("Could not request 'inject touch event'"); - } -} - static void sc_mouse_processor_process_mouse_click(struct sc_mouse_processor *mp, const struct sc_mouse_click_event *event) { @@ -137,6 +116,27 @@ sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp, } } +static void +sc_mouse_processor_process_touch(struct sc_mouse_processor *mp, + const struct sc_touch_event *event) { + struct sc_mouse_inject *mi = DOWNCAST(mp); + + struct control_msg msg = { + .type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, + .inject_touch_event = { + .action = convert_touch_action(event->action), + .pointer_id = event->pointer_id, + .position = event->position, + .pressure = event->pressure, + .buttons = 0, + }, + }; + + if (!controller_push_msg(mi->controller, &msg)) { + LOGW("Could not request 'inject touch event'"); + } +} + void sc_mouse_inject_init(struct sc_mouse_inject *mi, struct controller *controller) { @@ -144,9 +144,9 @@ sc_mouse_inject_init(struct sc_mouse_inject *mi, static const struct sc_mouse_processor_ops ops = { .process_mouse_motion = sc_mouse_processor_process_mouse_motion, - .process_touch = sc_mouse_processor_process_touch, .process_mouse_click = sc_mouse_processor_process_mouse_click, .process_mouse_scroll = sc_mouse_processor_process_mouse_scroll, + .process_touch = sc_mouse_processor_process_touch, }; mi->mouse_processor.ops = &ops; diff --git a/app/src/trait/mouse_processor.h b/app/src/trait/mouse_processor.h index e06545bf..b08d15bb 100644 --- a/app/src/trait/mouse_processor.h +++ b/app/src/trait/mouse_processor.h @@ -23,10 +23,6 @@ struct sc_mouse_processor_ops { (*process_mouse_motion)(struct sc_mouse_processor *mp, const struct sc_mouse_motion_event *event); - void - (*process_touch)(struct sc_mouse_processor *mp, - const struct sc_touch_event *event); - void (*process_mouse_click)(struct sc_mouse_processor *mp, const struct sc_mouse_click_event *event); @@ -34,6 +30,10 @@ struct sc_mouse_processor_ops { void (*process_mouse_scroll)(struct sc_mouse_processor *mp, const struct sc_mouse_scroll_event *event); + + void + (*process_touch)(struct sc_mouse_processor *mp, + const struct sc_touch_event *event); }; #endif From 63e29b1782f18606984550c151a41209c4c809af Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 29 Dec 2021 23:52:08 +0100 Subject: [PATCH 0934/2244] Apply buttons mask if not --forward-all-clicks If --forward-all-clicks is not set, then only left clicks are forwarded. For consistency, also mask the buttons state in other events. --- app/src/input_manager.c | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index ba6a4e2a..4dc8c4eb 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -63,9 +63,19 @@ sc_mouse_button_from_sdl(uint8_t button) { } static inline uint8_t -sc_mouse_buttons_state_from_sdl(uint32_t buttons_state) { +sc_mouse_buttons_state_from_sdl(uint32_t buttons_state, + bool forward_all_clicks) { assert(buttons_state < 0x100); // fits in uint8_t - return buttons_state; + + uint8_t mask = SC_MOUSE_BUTTON_LEFT; + if (forward_all_clicks) { + mask |= SC_MOUSE_BUTTON_RIGHT + | SC_MOUSE_BUTTON_MIDDLE + | SC_MOUSE_BUTTON_X1 + | SC_MOUSE_BUTTON_X2; + } + + return buttons_state & mask; } #define SC_SDL_SHORTCUT_MODS_MASK (KMOD_CTRL | KMOD_ALT | KMOD_GUI) @@ -616,6 +626,7 @@ input_manager_process_key(struct input_manager *im, static void input_manager_process_mouse_motion(struct input_manager *im, const SDL_MouseMotionEvent *event) { + uint32_t mask = SDL_BUTTON_LMASK; if (im->forward_all_clicks) { mask |= SDL_BUTTON_MMASK | SDL_BUTTON_RMASK; @@ -635,7 +646,9 @@ input_manager_process_mouse_motion(struct input_manager *im, .point = screen_convert_window_to_frame_coords(im->screen, event->x, event->y), }, - .buttons_state = sc_mouse_buttons_state_from_sdl(event->state), + .buttons_state = + sc_mouse_buttons_state_from_sdl(event->state, + im->forward_all_clicks), }; im->mp->ops->process_mouse_motion(im->mp, &evt); @@ -740,7 +753,9 @@ input_manager_process_mouse_button(struct input_manager *im, }, .action = sc_action_from_sdl_mousebutton_type(event->type), .button = sc_mouse_button_from_sdl(event->button), - .buttons_state = sc_mouse_buttons_state_from_sdl(sdl_buttons_state), + .buttons_state = + sc_mouse_buttons_state_from_sdl(sdl_buttons_state, + im->forward_all_clicks), }; im->mp->ops->process_mouse_click(im->mp, &evt); From bc674721dcc9b5b5461443c1b519333b4585fa7d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 30 Dec 2021 15:03:39 +0100 Subject: [PATCH 0935/2244] Make process_text() optional Not all key processors support text injection (HID keyboard does not support it). Instead of providing a dummy op function, set it to NULL and check on the caller side before calling it. --- app/src/hid_keyboard.c | 13 +++---------- app/src/input_manager.c | 6 ++++++ app/src/trait/key_processor.h | 9 ++++++++- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/app/src/hid_keyboard.c b/app/src/hid_keyboard.c index be5f8a19..4a0e64ba 100644 --- a/app/src/hid_keyboard.c +++ b/app/src/hid_keyboard.c @@ -387,15 +387,6 @@ sc_key_processor_process_key(struct sc_key_processor *kp, } } -static void -sc_key_processor_process_text(struct sc_key_processor *kp, - const struct sc_text_event *event) { - (void) kp; - (void) event; - - // Never forward text input via HID (all the keys are injected separately) -} - bool sc_hid_keyboard_init(struct sc_hid_keyboard *kb, struct sc_aoa *aoa) { kb->aoa = aoa; @@ -415,7 +406,9 @@ sc_hid_keyboard_init(struct sc_hid_keyboard *kb, struct sc_aoa *aoa) { static const struct sc_key_processor_ops ops = { .process_key = sc_key_processor_process_key, - .process_text = sc_key_processor_process_text, + // Never forward text input via HID (all the keys are injected + // separately) + .process_text = NULL, }; // Clipboard synchronization is requested over the control socket, while HID diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 4dc8c4eb..aa0708c0 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -384,6 +384,11 @@ rotate_client_right(struct screen *screen) { static void input_manager_process_text_input(struct input_manager *im, const SDL_TextInputEvent *event) { + if (!im->kp->ops->process_text) { + // The key processor does not support text input + return; + } + if (is_shortcut_mod(im, SDL_GetModState())) { // A shortcut must never generate text events return; @@ -620,6 +625,7 @@ input_manager_process_key(struct input_manager *im, .mods_state = sc_mods_state_from_sdl(event->keysym.mod), }; + assert(im->kp->ops->process_key); im->kp->ops->process_key(im->kp, &evt, ack_to_wait); } diff --git a/app/src/trait/key_processor.h b/app/src/trait/key_processor.h index 045e5e7b..8c51b11d 100644 --- a/app/src/trait/key_processor.h +++ b/app/src/trait/key_processor.h @@ -29,17 +29,24 @@ struct sc_key_processor { struct sc_key_processor_ops { /** - * Process the keyboard event + * Process a keyboard event * * The `sequence` number (if different from `SC_SEQUENCE_INVALID`) indicates * the acknowledgement number to wait for before injecting this event. * This allows to ensure that the device clipboard is set before injecting * Ctrl+v on the device. + * + * This function is mandatory. */ void (*process_key)(struct sc_key_processor *kp, const struct sc_key_event *event, uint64_t ack_to_wait); + /** + * Process an input text + * + * This function is optional. + */ void (*process_text)(struct sc_key_processor *kp, const struct sc_text_event *event); From 57f1655d4bb18452615f59d6bb8058bc8d5adfc7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 30 Dec 2021 15:08:42 +0100 Subject: [PATCH 0936/2244] Make some mouse processors ops optional Do not force all mouse processors to implement scroll events or touch events. --- app/src/input_manager.c | 12 ++++++++++++ app/src/trait/mouse_processor.h | 20 ++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index aa0708c0..dfd78484 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -657,6 +657,7 @@ input_manager_process_mouse_motion(struct input_manager *im, im->forward_all_clicks), }; + assert(im->mp->ops->process_mouse_motion); im->mp->ops->process_mouse_motion(im->mp, &evt); if (im->vfinger_down) { @@ -671,6 +672,11 @@ input_manager_process_mouse_motion(struct input_manager *im, static void input_manager_process_touch(struct input_manager *im, const SDL_TouchFingerEvent *event) { + if (!im->mp->ops->process_touch) { + // The mouse processor does not support touch events + return; + } + int dw; int dh; SDL_GL_GetDrawableSize(im->screen->window, &dw, &dh); @@ -764,6 +770,7 @@ input_manager_process_mouse_button(struct input_manager *im, im->forward_all_clicks), }; + assert(im->mp->ops->process_mouse_click); im->mp->ops->process_mouse_click(im->mp, &evt); // Pinch-to-zoom simulation. @@ -795,6 +802,11 @@ input_manager_process_mouse_button(struct input_manager *im, static void input_manager_process_mouse_wheel(struct input_manager *im, const SDL_MouseWheelEvent *event) { + if (!im->mp->ops->process_mouse_scroll) { + // The mouse processor does not support scroll events + return; + } + // mouse_x and mouse_y are expressed in pixels relative to the window int mouse_x; int mouse_y; diff --git a/app/src/trait/mouse_processor.h b/app/src/trait/mouse_processor.h index b08d15bb..0252d2c6 100644 --- a/app/src/trait/mouse_processor.h +++ b/app/src/trait/mouse_processor.h @@ -19,18 +19,38 @@ struct sc_mouse_processor { }; struct sc_mouse_processor_ops { + /** + * Process a mouse motion event + * + * This function is mandatory. + */ void (*process_mouse_motion)(struct sc_mouse_processor *mp, const struct sc_mouse_motion_event *event); + /** + * Process a mouse click event + * + * This function is mandatory. + */ void (*process_mouse_click)(struct sc_mouse_processor *mp, const struct sc_mouse_click_event *event); + /** + * Process a mouse scroll event + * + * This function is optional. + */ void (*process_mouse_scroll)(struct sc_mouse_processor *mp, const struct sc_mouse_scroll_event *event); + /** + * Process a touch event + * + * This function is optional. + */ void (*process_touch)(struct sc_mouse_processor *mp, const struct sc_touch_event *event); From cca3c953dae05493f7a076b6d8b1de6bf22d56f9 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 1 Jan 2022 23:59:20 +0100 Subject: [PATCH 0937/2244] Enable virtual finger only on left click The pinch-to-zoom feature must only be enabled with Ctrl+left_click. --- app/src/input_manager.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index dfd78484..34bab246 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -783,8 +783,9 @@ input_manager_process_mouse_button(struct input_manager *im, // 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)) { + if (event->button == SDL_BUTTON_LEFT && + ((down && !im->vfinger_down && CTRL_PRESSED) || + (!down && im->vfinger_down))) { struct sc_point mouse = screen_convert_window_to_frame_coords(im->screen, event->x, event->y); From a9d23400cdb763703a6d757b0c445e7700271abb Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 4 Jan 2022 15:11:33 +0100 Subject: [PATCH 0938/2244] Remove unused enum value requiring SDL 2.0.18 Refs b8fed50639fe3bf8e02a8cc2409494cd6bf30fb7 Fixes #2924 --- app/src/input_events.h | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/input_events.h b/app/src/input_events.h index b40995e6..48c51ee7 100644 --- a/app/src/input_events.h +++ b/app/src/input_events.h @@ -54,7 +54,6 @@ enum sc_mod { SC_MOD_NUM = KMOD_NUM, SC_MOD_CAPS = KMOD_CAPS, - SC_MOD_SCROLL = KMOD_SCROLL, }; enum sc_action { From 2b34e1224e97e21ca04ce751bf513d85fbcc4a45 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 31 Dec 2021 16:15:41 +0100 Subject: [PATCH 0939/2244] Use separate struct for input manager params This avoids to directly pass the options instance (which contains more data than strictly necessary), and limit the number of parameters for the init function. --- app/src/input_manager.c | 28 +++++++++++++--------------- app/src/input_manager.h | 19 +++++++++++++++---- app/src/scrcpy.c | 15 +++++++++++++-- 3 files changed, 41 insertions(+), 21 deletions(-) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 34bab246..ce138012 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -121,24 +121,22 @@ is_shortcut_mod(struct input_manager *im, uint16_t sdl_mod) { } void -input_manager_init(struct input_manager *im, struct controller *controller, - struct screen *screen, struct sc_key_processor *kp, - struct sc_mouse_processor *mp, - const struct scrcpy_options *options) { - assert(!options->control || (kp && kp->ops)); - assert(!options->control || (mp && mp->ops)); +input_manager_init(struct input_manager *im, + const struct input_manager_params *params) { + assert(!params->control || (params->kp && params->kp->ops)); + assert(!params->control || (params->mp && params->mp->ops)); - im->controller = controller; - im->screen = screen; - im->kp = kp; - im->mp = mp; + im->controller = params->controller; + im->screen = params->screen; + im->kp = params->kp; + im->mp = params->mp; - im->control = options->control; - im->forward_all_clicks = options->forward_all_clicks; - im->legacy_paste = options->legacy_paste; - im->clipboard_autosync = options->clipboard_autosync; + im->control = params->control; + im->forward_all_clicks = params->forward_all_clicks; + im->legacy_paste = params->legacy_paste; + im->clipboard_autosync = params->clipboard_autosync; - const struct sc_shortcut_mods *shortcut_mods = &options->shortcut_mods; + const struct sc_shortcut_mods *shortcut_mods = params->shortcut_mods; assert(shortcut_mods->count); assert(shortcut_mods->count < SC_MAX_SHORTCUT_MODS); for (unsigned i = 0; i < shortcut_mods->count; ++i) { diff --git a/app/src/input_manager.h b/app/src/input_manager.h index 5e02b457..38d0d703 100644 --- a/app/src/input_manager.h +++ b/app/src/input_manager.h @@ -43,11 +43,22 @@ struct input_manager { uint64_t next_sequence; // used for request acknowledgements }; +struct input_manager_params { + struct controller *controller; + struct screen *screen; + struct sc_key_processor *kp; + struct sc_mouse_processor *mp; + + bool control; + bool forward_all_clicks; + bool legacy_paste; + bool clipboard_autosync; + const struct sc_shortcut_mods *shortcut_mods; +}; + void -input_manager_init(struct input_manager *im, struct controller *controller, - struct screen *screen, struct sc_key_processor *kp, - struct sc_mouse_processor *mp, - const struct scrcpy_options *options); +input_manager_init(struct input_manager *im, + const struct input_manager_params *params); bool input_manager_handle_event(struct input_manager *im, SDL_Event *event); diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 51eb19fb..972e2d99 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -582,8 +582,19 @@ aoa_hid_end: mp = &s->mouse_inject.mouse_processor; } - input_manager_init(&s->input_manager, &s->controller, &s->screen, kp, mp, - options); + struct input_manager_params im_params = { + .controller = &s->controller, + .screen = &s->screen, + .kp = kp, + .mp = mp, + .control = options->control, + .forward_all_clicks = options->forward_all_clicks, + .legacy_paste = options->legacy_paste, + .clipboard_autosync = options->clipboard_autosync, + .shortcut_mods = &options->shortcut_mods, + }; + + input_manager_init(&s->input_manager, &im_params); ret = event_loop(s, options); LOGD("quit..."); From 6102a0b5bb5b7c8473c407592707539be48da19e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 31 Dec 2021 16:32:07 +0100 Subject: [PATCH 0940/2244] Move input_manager into screen The input_manager is strongly tied to the screen, it could not work independently of the specific screen implementation. To implement a user-friendly HID mouse behavior, some SDL events will need to be handled both by the screen and by the input manager. For example, a click must typically be handled by the input_manager so that it is forwarded to the device, but in HID mouse mode, the first click should be handled by the screen to capture the mouse (enable relative mouse mode). Make the input_manager a descendant of the screen, so that the screen decides what to do on SDL events. Concretely, replace this structure hierarchy: +- struct scrcpy +- struct input_manager +- struct screen by this one: +- struct scrcpy +- struct screen +- struct input_manager --- app/src/input_manager.c | 1 + app/src/input_manager.h | 1 - app/src/scrcpy.c | 127 ++++++++++++++++++---------------------- app/src/screen.c | 16 ++++- app/src/screen.h | 15 +++++ 5 files changed, 87 insertions(+), 73 deletions(-) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index ce138012..ec91787a 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -4,6 +4,7 @@ #include #include "input_events.h" +#include "screen.h" #include "util/log.h" static inline uint16_t diff --git a/app/src/input_manager.h b/app/src/input_manager.h index 38d0d703..088406f6 100644 --- a/app/src/input_manager.h +++ b/app/src/input_manager.h @@ -10,7 +10,6 @@ #include "controller.h" #include "fps_counter.h" #include "options.h" -#include "screen.h" #include "trait/key_processor.h" #include "trait/mouse_processor.h" diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 972e2d99..5370f448 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -17,7 +17,6 @@ #include "decoder.h" #include "events.h" #include "file_handler.h" -#include "input_manager.h" #ifdef HAVE_AOA_HID # include "hid_keyboard.h" #endif @@ -57,7 +56,6 @@ struct scrcpy { #endif }; struct sc_mouse_inject mouse_inject; - struct input_manager input_manager; }; static inline void @@ -189,11 +187,6 @@ handle_event(struct scrcpy *s, const struct scrcpy_options *options, } bool consumed = screen_handle_event(&s->screen, event); - if (consumed) { - goto end; - } - - consumed = input_manager_handle_event(&s->input_manager, event); (void) consumed; end: @@ -450,6 +443,9 @@ scrcpy(struct scrcpy_options *options) { stream_add_sink(&s->stream, &rec->packet_sink); } + struct sc_key_processor *kp = NULL; + struct sc_mouse_processor *mp = NULL; + if (options->control) { #ifdef HAVE_AOA_HID if (options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_HID) { @@ -481,59 +477,7 @@ scrcpy(struct scrcpy_options *options) { LOGW("Could not request 'set screen power mode'"); } } - } - if (options->display) { - const char *window_title = - options->window_title ? options->window_title : info->device_name; - - struct screen_params screen_params = { - .window_title = window_title, - .frame_size = info->frame_size, - .always_on_top = options->always_on_top, - .window_x = options->window_x, - .window_y = options->window_y, - .window_width = options->window_width, - .window_height = options->window_height, - .window_borderless = options->window_borderless, - .rotation = options->rotation, - .mipmaps = options->mipmaps, - .fullscreen = options->fullscreen, - .buffering_time = options->display_buffer, - }; - - if (!screen_init(&s->screen, &screen_params)) { - goto end; - } - screen_initialized = true; - - decoder_add_sink(&s->decoder, &s->screen.frame_sink); - } - -#ifdef HAVE_V4L2 - if (options->v4l2_device) { - if (!sc_v4l2_sink_init(&s->v4l2_sink, options->v4l2_device, - info->frame_size, options->v4l2_buffer)) { - goto end; - } - - decoder_add_sink(&s->decoder, &s->v4l2_sink.frame_sink); - - v4l2_sink_initialized = true; - } -#endif - - // now we consumed the header values, the socket receives the video stream - // start the stream - if (!stream_start(&s->stream)) { - goto end; - } - stream_started = true; - - struct sc_key_processor *kp = NULL; - struct sc_mouse_processor *mp = NULL; - - if (options->control) { #ifdef HAVE_AOA_HID if (options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_HID) { bool aoa_hid_ok = false; @@ -582,19 +526,60 @@ aoa_hid_end: mp = &s->mouse_inject.mouse_processor; } - struct input_manager_params im_params = { - .controller = &s->controller, - .screen = &s->screen, - .kp = kp, - .mp = mp, - .control = options->control, - .forward_all_clicks = options->forward_all_clicks, - .legacy_paste = options->legacy_paste, - .clipboard_autosync = options->clipboard_autosync, - .shortcut_mods = &options->shortcut_mods, - }; + if (options->display) { + const char *window_title = + options->window_title ? options->window_title : info->device_name; - input_manager_init(&s->input_manager, &im_params); + struct screen_params screen_params = { + .controller = &s->controller, + .kp = kp, + .mp = mp, + .control = options->control, + .forward_all_clicks = options->forward_all_clicks, + .legacy_paste = options->legacy_paste, + .clipboard_autosync = options->clipboard_autosync, + .shortcut_mods = &options->shortcut_mods, + .window_title = window_title, + .frame_size = info->frame_size, + .always_on_top = options->always_on_top, + .window_x = options->window_x, + .window_y = options->window_y, + .window_width = options->window_width, + .window_height = options->window_height, + .window_borderless = options->window_borderless, + .rotation = options->rotation, + .mipmaps = options->mipmaps, + .fullscreen = options->fullscreen, + .buffering_time = options->display_buffer, + }; + + if (!screen_init(&s->screen, &screen_params)) { + goto end; + } + screen_initialized = true; + + decoder_add_sink(&s->decoder, &s->screen.frame_sink); + } + +#ifdef HAVE_V4L2 + if (options->v4l2_device) { + if (!sc_v4l2_sink_init(&s->v4l2_sink, options->v4l2_device, + info->frame_size, options->v4l2_buffer)) { + goto end; + } + + decoder_add_sink(&s->decoder, &s->v4l2_sink.frame_sink); + + v4l2_sink_initialized = true; + } +#endif + + // now we consumed the header values, the socket receives the video stream + // start the stream + if (!stream_start(&s->stream)) { + goto end; + } + stream_started = true; ret = event_loop(s, options); LOGD("quit..."); diff --git a/app/src/screen.c b/app/src/screen.c index 0cb09a4b..9ad81cb8 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -470,6 +470,20 @@ screen_init(struct screen *screen, const struct screen_params *params) { goto error_destroy_texture; } + struct input_manager_params im_params = { + .controller = params->controller, + .screen = screen, + .kp = params->kp, + .mp = params->mp, + .control = params->control, + .forward_all_clicks = params->forward_all_clicks, + .legacy_paste = params->legacy_paste, + .clipboard_autosync = params->clipboard_autosync, + .shortcut_mods = params->shortcut_mods, + }; + + input_manager_init(&screen->im, &im_params); + // 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 @@ -773,7 +787,7 @@ screen_handle_event(struct screen *screen, SDL_Event *event) { return true; } - return false; + return input_manager_handle_event(&screen->im, event); } struct sc_point diff --git a/app/src/screen.h b/app/src/screen.h index b82bf631..bc7696f1 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -7,10 +7,14 @@ #include #include +#include "controller.h" #include "coords.h" #include "fps_counter.h" +#include "input_manager.h" #include "opengl.h" +#include "trait/key_processor.h" #include "trait/frame_sink.h" +#include "trait/mouse_processor.h" #include "video_buffer.h" struct screen { @@ -20,6 +24,7 @@ struct screen { bool open; // track the open/close state to assert correct behavior #endif + struct input_manager im; struct sc_video_buffer vb; struct fps_counter fps_counter; @@ -50,6 +55,16 @@ struct screen { }; struct screen_params { + struct controller *controller; + struct sc_key_processor *kp; + struct sc_mouse_processor *mp; + + bool control; + bool forward_all_clicks; + bool legacy_paste; + bool clipboard_autosync; + const struct sc_shortcut_mods *shortcut_mods; + const char *window_title; struct sc_size frame_size; bool always_on_top; From 5ce1ccde8518295ddd67179e1ed658f618bea296 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 1 Jan 2022 19:38:27 +0100 Subject: [PATCH 0941/2244] Reorder controller and HID initialization This allows to merge two "#ifdef HAVE_AOA_HID" blocks to simplify. --- app/src/scrcpy.c | 48 +++++++++++++++++++++++------------------------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 5370f448..fad7049b 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -455,34 +455,10 @@ scrcpy(struct scrcpy_options *options) { } acksync = &s->acksync; - } -#endif - if (!controller_init(&s->controller, s->server.control_socket, - acksync)) { - goto end; - } - controller_initialized = true; - if (!controller_start(&s->controller)) { - goto end; - } - controller_started = true; - - if (options->turn_screen_off) { - struct control_msg msg; - msg.type = CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE; - msg.set_screen_power_mode.mode = SCREEN_POWER_MODE_OFF; - - if (!controller_push_msg(&s->controller, &msg)) { - LOGW("Could not request 'set screen power mode'"); - } - } - -#ifdef HAVE_AOA_HID - if (options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_HID) { bool aoa_hid_ok = false; - bool ok = sc_aoa_init(&s->aoa, serial, acksync); + ok = sc_aoa_init(&s->aoa, serial, acksync); if (!ok) { goto aoa_hid_end; } @@ -524,6 +500,28 @@ aoa_hid_end: sc_mouse_inject_init(&s->mouse_inject, &s->controller); mp = &s->mouse_inject.mouse_processor; + + if (!controller_init(&s->controller, s->server.control_socket, + acksync)) { + goto end; + } + controller_initialized = true; + + if (!controller_start(&s->controller)) { + goto end; + } + controller_started = true; + + if (options->turn_screen_off) { + struct control_msg msg; + msg.type = CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE; + msg.set_screen_power_mode.mode = SCREEN_POWER_MODE_OFF; + + if (!controller_push_msg(&s->controller, &msg)) { + LOGW("Could not request 'set screen power mode'"); + } + } + } if (options->display) { From f04812fc714f2fdb6b5ebadabc1007cc4895f70d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 1 Jan 2022 19:45:50 +0100 Subject: [PATCH 0942/2244] Remove duplicate boolean The AOA initialization state is already tracked by aoa_hid_initialized. --- app/src/scrcpy.c | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index fad7049b..52799da4 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -456,8 +456,6 @@ scrcpy(struct scrcpy_options *options) { acksync = &s->acksync; - bool aoa_hid_ok = false; - ok = sc_aoa_init(&s->aoa, serial, acksync); if (!ok) { goto aoa_hid_end; @@ -474,13 +472,12 @@ scrcpy(struct scrcpy_options *options) { goto aoa_hid_end; } - aoa_hid_ok = true; kp = &s->keyboard_hid.key_processor; aoa_hid_initialized = true; aoa_hid_end: - if (!aoa_hid_ok) { + if (!aoa_hid_initialized) { LOGE("Failed to enable HID over AOA, " "fallback to default keyboard injection method " "(-K/--hid-keyboard ignored)"); From 7121a0dc5310182cc1b003e40d36bd81bc1f9471 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 1 Jan 2022 19:47:47 +0100 Subject: [PATCH 0943/2244] Destroy acksync immediately on error If AOA or HID keyboard may not be initialized for some reason, acksync is useless. --- app/src/scrcpy.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 52799da4..6b8074e9 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -454,25 +454,27 @@ scrcpy(struct scrcpy_options *options) { goto end; } - acksync = &s->acksync; - ok = sc_aoa_init(&s->aoa, serial, acksync); if (!ok) { + sc_acksync_destroy(&s->acksync); goto aoa_hid_end; } if (!sc_hid_keyboard_init(&s->keyboard_hid, &s->aoa)) { + sc_acksync_destroy(&s->acksync); sc_aoa_destroy(&s->aoa); goto aoa_hid_end; } if (!sc_aoa_start(&s->aoa)) { + sc_acksync_destroy(&s->acksync); sc_hid_keyboard_destroy(&s->keyboard_hid); sc_aoa_destroy(&s->aoa); goto aoa_hid_end; } kp = &s->keyboard_hid.key_processor; + acksync = &s->acksync; aoa_hid_initialized = true; From 924375487e63a5a9114182015a68aedef2f52d14 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 31 Dec 2021 10:38:05 +0100 Subject: [PATCH 0944/2244] Pass buttons state in scroll events A scroll event might be produced when a mouse button is pressed (for example when scrolling while selecting a text). For consistency, pass the actual buttons state (instead of 0). In practice, it seems that this use case does not work properly with Android event injection, but it will work with HID mouse. --- app/src/control_msg.c | 8 +++++--- app/src/control_msg.h | 1 + app/src/input_events.h | 1 + app/src/input_manager.c | 4 +++- app/src/mouse_inject.c | 1 + app/tests/test_control_msg_serialize.c | 4 +++- .../main/java/com/genymobile/scrcpy/ControlMessage.java | 3 ++- .../java/com/genymobile/scrcpy/ControlMessageReader.java | 5 +++-- .../src/main/java/com/genymobile/scrcpy/Controller.java | 6 +++--- .../com/genymobile/scrcpy/ControlMessageReaderTest.java | 2 ++ 10 files changed, 24 insertions(+), 11 deletions(-) diff --git a/app/src/control_msg.c b/app/src/control_msg.c index 6ccdc054..75c74628 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -119,7 +119,8 @@ control_msg_serialize(const struct control_msg *msg, unsigned char *buf) { (uint32_t) msg->inject_scroll_event.hscroll); buffer_write32be(&buf[17], (uint32_t) msg->inject_scroll_event.vscroll); - return 21; + buffer_write32be(&buf[21], msg->inject_scroll_event.buttons); + return 25; case CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON: buf[1] = msg->inject_keycode.action; return 2; @@ -192,11 +193,12 @@ control_msg_log(const struct control_msg *msg) { } case CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT: LOG_CMSG("scroll position=%" PRIi32 ",%" PRIi32 " hscroll=%" PRIi32 - " vscroll=%" PRIi32, + " vscroll=%" PRIi32 " buttons=%06lx", msg->inject_scroll_event.position.point.x, msg->inject_scroll_event.position.point.y, msg->inject_scroll_event.hscroll, - msg->inject_scroll_event.vscroll); + msg->inject_scroll_event.vscroll, + (long) msg->inject_scroll_event.buttons); break; case CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON: LOG_CMSG("back-or-screen-on %s", diff --git a/app/src/control_msg.h b/app/src/control_msg.h index 7f3235d7..0eadd4f2 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -70,6 +70,7 @@ struct control_msg { struct sc_position position; int32_t hscroll; int32_t vscroll; + enum android_motionevent_buttons buttons; } inject_scroll_event; struct { enum android_keyevent_action action; // action for the BACK key diff --git a/app/src/input_events.h b/app/src/input_events.h index 48c51ee7..c27a7a47 100644 --- a/app/src/input_events.h +++ b/app/src/input_events.h @@ -360,6 +360,7 @@ struct sc_mouse_scroll_event { struct sc_position position; int32_t hscroll; int32_t vscroll; + uint8_t buttons_state; // bitwise-OR of sc_mouse_button values }; struct sc_mouse_motion_event { diff --git a/app/src/input_manager.c b/app/src/input_manager.c index ec91787a..64922f28 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -810,7 +810,7 @@ input_manager_process_mouse_wheel(struct input_manager *im, // 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); + uint32_t buttons = SDL_GetMouseState(&mouse_x, &mouse_y); struct sc_mouse_scroll_event evt = { .position = { @@ -820,6 +820,8 @@ input_manager_process_mouse_wheel(struct input_manager *im, }, .hscroll = event->x, .vscroll = event->y, + .buttons_state = + sc_mouse_buttons_state_from_sdl(buttons, im->forward_all_clicks), }; im->mp->ops->process_mouse_scroll(im->mp, &evt); diff --git a/app/src/mouse_inject.c b/app/src/mouse_inject.c index 1d14509d..38bfa404 100644 --- a/app/src/mouse_inject.c +++ b/app/src/mouse_inject.c @@ -108,6 +108,7 @@ sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp, .position = event->position, .hscroll = event->hscroll, .vscroll = event->vscroll, + .buttons = convert_mouse_buttons(event->buttons_state), }, }; diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c index 42b72b59..d1f0f161 100644 --- a/app/tests/test_control_msg_serialize.c +++ b/app/tests/test_control_msg_serialize.c @@ -126,12 +126,13 @@ static void test_serialize_inject_scroll_event(void) { }, .hscroll = 1, .vscroll = -1, + .buttons = 1, }, }; unsigned char buf[CONTROL_MSG_MAX_SIZE]; size_t size = control_msg_serialize(&msg, buf); - assert(size == 21); + assert(size == 25); const unsigned char expected[] = { CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT, @@ -139,6 +140,7 @@ static void test_serialize_inject_scroll_event(void) { 0x04, 0x38, 0x07, 0x80, // 1080 1920 0x00, 0x00, 0x00, 0x01, // 1 0xFF, 0xFF, 0xFF, 0xFF, // -1 + 0x00, 0x00, 0x00, 0x01, // 1 }; 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 63ba0fa3..99eb805f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java @@ -71,12 +71,13 @@ public final class ControlMessage { return msg; } - public static ControlMessage createInjectScrollEvent(Position position, int hScroll, int vScroll) { + public static ControlMessage createInjectScrollEvent(Position position, int hScroll, int vScroll, int buttons) { ControlMessage msg = new ControlMessage(); msg.type = TYPE_INJECT_SCROLL_EVENT; msg.position = position; msg.hScroll = hScroll; msg.vScroll = vScroll; + msg.buttons = buttons; return msg; } diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java index f09ed26f..24dc5e50 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java @@ -10,7 +10,7 @@ public class ControlMessageReader { 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 INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 24; static final int BACK_OR_SCREEN_ON_LENGTH = 1; static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1; static final int GET_CLIPBOARD_LENGTH = 1; @@ -154,7 +154,8 @@ public class ControlMessageReader { Position position = readPosition(buffer); int hScroll = buffer.getInt(); int vScroll = buffer.getInt(); - return ControlMessage.createInjectScrollEvent(position, hScroll, vScroll); + int buttons = buffer.getInt(); + return ControlMessage.createInjectScrollEvent(position, hScroll, vScroll, buttons); } private ControlMessage parseBackOrScreenOnEvent() { diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index 9246004a..481c512f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -98,7 +98,7 @@ public class Controller { break; case ControlMessage.TYPE_INJECT_SCROLL_EVENT: if (device.supportsInputEvents()) { - injectScroll(msg.getPosition(), msg.getHScroll(), msg.getVScroll()); + injectScroll(msg.getPosition(), msg.getHScroll(), msg.getVScroll(), msg.getButtons()); } break; case ControlMessage.TYPE_BACK_OR_SCREEN_ON: @@ -221,7 +221,7 @@ public class Controller { return device.injectEvent(event, Device.INJECT_MODE_ASYNC); } - private boolean injectScroll(Position position, int hScroll, int vScroll) { + private boolean injectScroll(Position position, int hScroll, int vScroll, int buttons) { long now = SystemClock.uptimeMillis(); Point point = device.getPhysicalPoint(position); if (point == null) { @@ -239,7 +239,7 @@ public class Controller { coords.setAxisValue(MotionEvent.AXIS_VSCROLL, vScroll); MotionEvent event = MotionEvent - .obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, 0, 1f, 1f, DEFAULT_DEVICE_ID, 0, + .obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, InputDevice.SOURCE_MOUSE, 0); return device.injectEvent(event, Device.INJECT_MODE_ASYNC); } diff --git a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java index 5e79d4f0..2a4ffe75 100644 --- a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java @@ -128,6 +128,7 @@ public class ControlMessageReaderTest { dos.writeShort(1920); dos.writeInt(1); dos.writeInt(-1); + dos.writeInt(1); byte[] packet = bos.toByteArray(); @@ -144,6 +145,7 @@ public class ControlMessageReaderTest { Assert.assertEquals(1920, event.getPosition().getScreenSize().getHeight()); Assert.assertEquals(1, event.getHScroll()); Assert.assertEquals(-1, event.getVScroll()); + Assert.assertEquals(1, event.getButtons()); } @Test From b5855e5deb7482f96e1a0217218442ecdf21e3ab Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 30 Dec 2021 15:46:00 +0100 Subject: [PATCH 0945/2244] Add relative mode flag to mouse processors The default mouse injection works in absolute mode: it forwards clicks at a specific position on screen. To support HID mouse, add a flag to indicate that the mouse processor works in relative mode: it forwards mouse motion vectors, without any absolute reference to the screen. --- app/src/input_manager.c | 10 ++++++++++ app/src/mouse_inject.c | 2 ++ app/src/trait/mouse_processor.h | 7 +++++++ 3 files changed, 19 insertions(+) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 64922f28..5cfb5b7e 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -659,7 +659,11 @@ input_manager_process_mouse_motion(struct input_manager *im, assert(im->mp->ops->process_mouse_motion); im->mp->ops->process_mouse_motion(im->mp, &evt); + // vfinger must never be used in relative mode + assert(!im->mp->relative_mode || !im->vfinger_down); + if (im->vfinger_down) { + assert(!im->mp->relative_mode); // assert one more time struct sc_point mouse = screen_convert_window_to_frame_coords(im->screen, event->x, event->y); @@ -772,6 +776,12 @@ input_manager_process_mouse_button(struct input_manager *im, assert(im->mp->ops->process_mouse_click); im->mp->ops->process_mouse_click(im->mp, &evt); + if (im->mp->relative_mode) { + assert(!im->vfinger_down); // vfinger must not be used in relative mode + // No pinch-to-zoom simulation + return; + } + // Pinch-to-zoom simulation. // // If Ctrl is hold when the left-click button is pressed, then diff --git a/app/src/mouse_inject.c b/app/src/mouse_inject.c index 38bfa404..dcb4f611 100644 --- a/app/src/mouse_inject.c +++ b/app/src/mouse_inject.c @@ -151,4 +151,6 @@ sc_mouse_inject_init(struct sc_mouse_inject *mi, }; mi->mouse_processor.ops = &ops; + + mi->mouse_processor.relative_mode = false; } diff --git a/app/src/trait/mouse_processor.h b/app/src/trait/mouse_processor.h index 0252d2c6..6e0b596e 100644 --- a/app/src/trait/mouse_processor.h +++ b/app/src/trait/mouse_processor.h @@ -16,6 +16,13 @@ */ struct sc_mouse_processor { const struct sc_mouse_processor_ops *ops; + + /** + * If set, the mouse processor works in relative mode (the absolute + * position is irrelevant). In particular, it indicates that the mouse + * pointer must be "captured" by the UI. + */ + bool relative_mode; }; struct sc_mouse_processor_ops { From 643293752dc92c2e175b6ad1c8ebb75ffc195a3d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 30 Dec 2021 00:05:30 +0100 Subject: [PATCH 0946/2244] Provide relative mouse motion vector in event This will allow the mouse processor to handle relative motion easily. --- app/src/input_events.h | 2 ++ app/src/input_manager.c | 2 ++ 2 files changed, 4 insertions(+) diff --git a/app/src/input_events.h b/app/src/input_events.h index c27a7a47..4a4cf356 100644 --- a/app/src/input_events.h +++ b/app/src/input_events.h @@ -365,6 +365,8 @@ struct sc_mouse_scroll_event { struct sc_mouse_motion_event { struct sc_position position; + int32_t xrel; + int32_t yrel; uint8_t buttons_state; // bitwise-OR of sc_mouse_button values }; diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 5cfb5b7e..cc7883d9 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -651,6 +651,8 @@ input_manager_process_mouse_motion(struct input_manager *im, .point = screen_convert_window_to_frame_coords(im->screen, event->x, event->y), }, + .xrel = event->xrel, + .yrel = event->yrel, .buttons_state = sc_mouse_buttons_state_from_sdl(event->state, im->forward_all_clicks), From 40fca82b6002662c43a054079c0b8e0cbaa7d692 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 2 Jan 2022 00:11:00 +0100 Subject: [PATCH 0947/2244] Forward all motion events to mouse processors The decision to not send motion events when no click is pressed is specific to Android mouse injection. Other mouse processors (e.g. for HID mouse) will need to receive all events. --- app/src/input_manager.c | 8 -------- app/src/mouse_inject.c | 5 +++++ 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index cc7883d9..e407a541 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -632,14 +632,6 @@ static void input_manager_process_mouse_motion(struct input_manager *im, const SDL_MouseMotionEvent *event) { - uint32_t mask = SDL_BUTTON_LMASK; - if (im->forward_all_clicks) { - mask |= SDL_BUTTON_MMASK | SDL_BUTTON_RMASK; - } - if (!(event->state & mask)) { - // do not send motion events when no click is pressed - return; - } if (event->which == SDL_TOUCH_MOUSEID) { // simulated from touch events, so it's a duplicate return; diff --git a/app/src/mouse_inject.c b/app/src/mouse_inject.c index dcb4f611..8a4a0531 100644 --- a/app/src/mouse_inject.c +++ b/app/src/mouse_inject.c @@ -58,6 +58,11 @@ convert_touch_action(enum sc_touch_action action) { static void sc_mouse_processor_process_mouse_motion(struct sc_mouse_processor *mp, const struct sc_mouse_motion_event *event) { + if (!event->buttons_state) { + // Do not send motion events when no click is pressed + return; + } + struct sc_mouse_inject *mi = DOWNCAST(mp); struct control_msg msg = { From 17d01b5bf7832d4c65435e6076a1c75f7aec66be Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 31 Dec 2021 18:52:34 +0100 Subject: [PATCH 0948/2244] Add UI/UX support for relative mouse mode In relative mouse mode, the mouse pointer must be "captured" from the computer. Toggle (disable/enable) relative mouse mode using any of the hardcoded capture keys: - left-Alt - left-Super - right-Super These capture keys do not conflict with shortcuts, since a shortcut is always a combination of the MOD key and some other key, while the capture key triggers an action only if it is pressed and released alone. The relative mouse mode is also automatically enabled on any click in the window, and automatically disabled on focus lost (it is possible to lose focus even without the mouse). --- app/src/screen.c | 78 ++++++++++++++++++++++++++++++++++++++++++++++++ app/src/screen.h | 5 ++++ 2 files changed, 83 insertions(+) diff --git a/app/src/screen.c b/app/src/screen.c index 9ad81cb8..9c9f80bc 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -156,6 +156,17 @@ get_initial_optimal_size(struct sc_size content_size, uint16_t req_width, return window_size; } +static inline void +screen_capture_mouse(struct screen *screen, bool capture) { + if (SDL_SetRelativeMouseMode(capture)) { + LOGE("Could not set relative mouse mode to %s: %s", + capture ? "true" : "false", SDL_GetError()); + return; + } + + screen->mouse_captured = capture; +} + static void screen_update_content_rect(struct screen *screen) { int dw; @@ -354,6 +365,8 @@ screen_init(struct screen *screen, const struct screen_params *params) { screen->fullscreen = false; screen->maximized = false; screen->event_failed = false; + screen->mouse_captured = false; + screen->mouse_capture_key_pressed = 0; static const struct sc_video_buffer_callbacks cbs = { .on_new_frame = sc_video_buffer_on_new_frame, @@ -741,6 +754,11 @@ screen_resize_to_pixel_perfect(struct screen *screen) { content_size.height); } +static inline bool +screen_is_mouse_capture_key(SDL_Keycode key) { + return key == SDLK_LALT || key == SDLK_LGUI || key == SDLK_RGUI; +} + bool screen_handle_event(struct screen *screen, SDL_Event *event) { switch (event->type) { @@ -783,8 +801,68 @@ screen_handle_event(struct screen *screen, SDL_Event *event) { apply_pending_resize(screen); screen_render(screen, true); break; + case SDL_WINDOWEVENT_FOCUS_LOST: + if (screen->im.mp->relative_mode) { + screen_capture_mouse(screen, false); + } + break; } return true; + case SDL_KEYDOWN: + if (screen->im.mp->relative_mode) { + SDL_Keycode key = event->key.keysym.sym; + if (screen_is_mouse_capture_key(key)) { + if (!screen->mouse_capture_key_pressed) { + screen->mouse_capture_key_pressed = key; + return true; + } else { + // Another mouse capture key has been pressed, cancel + // mouse (un)capture + screen->mouse_capture_key_pressed = 0; + // Do not return, the event must be forwarded to the + // input manager + } + } + } + break; + case SDL_KEYUP: + if (screen->im.mp->relative_mode) { + SDL_Keycode key = event->key.keysym.sym; + SDL_Keycode cap = screen->mouse_capture_key_pressed; + screen->mouse_capture_key_pressed = 0; + if (key == cap) { + // A mouse capture key has been pressed then released: + // toggle the capture mouse mode + screen_capture_mouse(screen, !screen->mouse_captured); + return true; + } + // Do not return, the event must be forwarded to the input + // manager + } + break; + case SDL_MOUSEWHEEL: + case SDL_MOUSEMOTION: + case SDL_MOUSEBUTTONDOWN: + if (screen->im.mp->relative_mode && !screen->mouse_captured) { + // Do not forward to input manager, the mouse will be captured + // on SDL_MOUSEBUTTONUP + return true; + } + break; + case SDL_FINGERMOTION: + case SDL_FINGERDOWN: + case SDL_FINGERUP: + if (screen->im.mp->relative_mode) { + // Touch events are not compatible with relative mode + // (coordinates are not relative) + return true; + } + break; + case SDL_MOUSEBUTTONUP: + if (screen->im.mp->relative_mode && !screen->mouse_captured) { + screen_capture_mouse(screen, true); + return true; + } } return input_manager_handle_event(&screen->im, event); diff --git a/app/src/screen.h b/app/src/screen.h index bc7696f1..f0e15a7d 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -51,6 +51,11 @@ struct screen { bool event_failed; // in case SDL_PushEvent() returned an error + bool mouse_captured; // only relevant in relative mouse mode + // To enable/disable mouse capture, a mouse capture key (LALT, LGUI or + // RGUI) must be pressed. This variable tracks the pressed capture key. + SDL_Keycode mouse_capture_key_pressed; + AVFrame *frame; }; From aee1b397905ae9108fe239c9e3bb41ef33d0f43f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 28 Dec 2021 11:50:24 +0100 Subject: [PATCH 0949/2244] Add CLAMP() macro --- app/src/common.h | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/common.h b/app/src/common.h index accbc615..ce9ccad4 100644 --- a/app/src/common.h +++ b/app/src/common.h @@ -7,6 +7,7 @@ #define ARRAY_LEN(a) (sizeof(a) / sizeof(a[0])) #define MIN(X,Y) (X) < (Y) ? (X) : (Y) #define MAX(X,Y) (X) > (Y) ? (X) : (Y) +#define CLAMP(V,X,Y) MIN( MAX((V),(X)), (Y) ) #define container_of(ptr, type, member) \ ((type *) (((char *) (ptr)) - offsetof(type, member))) From ed2e45ee29099cb6656782067f28376f65f94289 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 1 Jan 2022 20:00:33 +0100 Subject: [PATCH 0950/2244] Refactor AOA/HID keyboard initialization This paves the way to add support for HID mouse initialization. --- app/src/scrcpy.c | 43 +++++++++++++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 6b8074e9..5907c91e 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -329,6 +329,7 @@ scrcpy(struct scrcpy_options *options) { bool stream_started = false; #ifdef HAVE_AOA_HID bool aoa_hid_initialized = false; + bool hid_keyboard_initialized = false; #endif bool controller_initialized = false; bool controller_started = false; @@ -448,40 +449,52 @@ scrcpy(struct scrcpy_options *options) { if (options->control) { #ifdef HAVE_AOA_HID - if (options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_HID) { + bool use_hid_keyboard = + options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_HID; + if (use_hid_keyboard) { bool ok = sc_acksync_init(&s->acksync); if (!ok) { goto end; } - ok = sc_aoa_init(&s->aoa, serial, acksync); + ok = sc_aoa_init(&s->aoa, serial, &s->acksync); if (!ok) { + LOGE("Failed to enable HID over AOA"); sc_acksync_destroy(&s->acksync); goto aoa_hid_end; } - if (!sc_hid_keyboard_init(&s->keyboard_hid, &s->aoa)) { + if (use_hid_keyboard) { + if (sc_hid_keyboard_init(&s->keyboard_hid, &s->aoa)) { + hid_keyboard_initialized = true; + kp = &s->keyboard_hid.key_processor; + } else { + LOGE("Could not initialize HID keyboard"); + } + } + + bool need_aoa = hid_keyboard_initialized; + + if (!need_aoa || !sc_aoa_start(&s->aoa)) { sc_acksync_destroy(&s->acksync); sc_aoa_destroy(&s->aoa); goto aoa_hid_end; } - if (!sc_aoa_start(&s->aoa)) { - sc_acksync_destroy(&s->acksync); - sc_hid_keyboard_destroy(&s->keyboard_hid); - sc_aoa_destroy(&s->aoa); - goto aoa_hid_end; - } - - kp = &s->keyboard_hid.key_processor; acksync = &s->acksync; aoa_hid_initialized = true; aoa_hid_end: if (!aoa_hid_initialized) { - LOGE("Failed to enable HID over AOA, " - "fallback to default keyboard injection method " + if (hid_keyboard_initialized) { + sc_hid_keyboard_destroy(&s->keyboard_hid); + hid_keyboard_initialized = false; + } + } + + if (use_hid_keyboard && !hid_keyboard_initialized) { + LOGE("Fallback to default keyboard injection method " "(-K/--hid-keyboard ignored)"); options->keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_INJECT; } @@ -590,7 +603,9 @@ end: // end-of-stream #ifdef HAVE_AOA_HID if (aoa_hid_initialized) { - sc_hid_keyboard_destroy(&s->keyboard_hid); + if (hid_keyboard_initialized) { + sc_hid_keyboard_destroy(&s->keyboard_hid); + } sc_aoa_stop(&s->aoa); } if (acksync) { From cba84f69993e3d7bdbb0e44493b70c0a14689487 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 26 Dec 2021 22:32:51 +0100 Subject: [PATCH 0951/2244] Add support for HID mouse --- app/meson.build | 1 + app/scrcpy.1 | 14 +++ app/src/cli.c | 24 +++- app/src/hid_mouse.c | 267 ++++++++++++++++++++++++++++++++++++++++++++ app/src/hid_mouse.h | 23 ++++ app/src/options.h | 6 + app/src/scrcpy.c | 42 ++++++- 7 files changed, 371 insertions(+), 6 deletions(-) create mode 100644 app/src/hid_mouse.c create mode 100644 app/src/hid_mouse.h diff --git a/app/meson.build b/app/meson.build index 38393f0c..cee261bb 100644 --- a/app/meson.build +++ b/app/meson.build @@ -77,6 +77,7 @@ if aoa_hid_support src += [ 'src/aoa_hid.c', 'src/hid_keyboard.c', + 'src/hid_mouse.c', ] endif diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 971c8a19..4d02982c 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -96,6 +96,8 @@ The keyboard layout must be configured (once and for all) on the device, via Set However, the option is only available when the HID keyboard is enabled (or a physical keyboard is connected). +Also see \fB\-\-hid\-mouse\fR. + .TP .B \-\-legacy\-paste Inject computer clipboard text as a sequence of key events on Ctrl+v (like MOD+Shift+v). @@ -120,6 +122,18 @@ Limit both the width and height of the video to \fIvalue\fR. The other dimension Default is 0 (unlimited). +.TP +.B \-M, \-\-hid\-mouse +Simulate a physical mouse by using HID over AOAv2. + +In this mode, the computer mouse is captured to control the device directly (relative mouse mode). + +LAlt, LSuper or RSuper toggle the capture mode, to give control of the mouse back to the computer. + +It may only work over USB, and is currently only supported on Linux. + +Also see \fB\-\-hid\-keyboard\fR. + .TP .B \-\-no\-clipboard\-autosync By default, scrcpy automatically synchronizes the computer clipboard to the device clipboard before injecting Ctrl+v, and the device clipboard to the computer clipboard whenever it changes. diff --git a/app/src/cli.c b/app/src/cli.c index ec53e5ec..3fcf94eb 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -178,7 +178,8 @@ static const struct sc_option options[] = { "directly: `adb shell am start -a " "android.settings.HARD_KEYBOARD_SETTINGS`.\n" "However, the option is only available when the HID keyboard " - "is enabled (or a physical keyboard is connected).", + "is enabled (or a physical keyboard is connected).\n" + "Also see --hid-mouse.", }, { .shortopt = 'h', @@ -214,6 +215,18 @@ static const struct sc_option options[] = { .text = "Limit the frame rate of screen capture (officially supported " "since Android 10, but may work on earlier versions).", }, + { + .shortopt = 'M', + .longopt = "hid-mouse", + .text = "Simulate a physical mouse by using HID over AOAv2.\n" + "In this mode, the computer mouse is captured to control the " + "device directly (relative mouse mode).\n" + "LAlt, LSuper or RSuper toggle the capture mode, to give " + "control of the mouse back to the computer.\n" + "It may only work over USB, and is currently only supported " + "on Linux.\n" + "Also see --hid-keyboard.", + }, { .shortopt = 'm', .longopt = "max-size", @@ -1315,6 +1328,15 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; } break; + case 'M': +#ifdef HAVE_AOA_HID + opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_HID; +#else + LOGE("HID over AOA (-M/--hid-mouse) is not supported on this" + "platform. It is only available on Linux."); + return false; +#endif + break; case OPT_LOCK_VIDEO_ORIENTATION: if (!parse_lock_video_orientation(optarg, &opts->lock_video_orientation)) { diff --git a/app/src/hid_mouse.c b/app/src/hid_mouse.c new file mode 100644 index 00000000..0e26c7c3 --- /dev/null +++ b/app/src/hid_mouse.c @@ -0,0 +1,267 @@ +#include "hid_mouse.h" + +#include + +#include "input_events.h" +#include "util/log.h" + +/** Downcast mouse processor to hid_mouse */ +#define DOWNCAST(MP) container_of(MP, struct sc_hid_mouse, mouse_processor) + +#define HID_MOUSE_ACCESSORY_ID 2 + +// 1 byte for buttons + padding, 1 byte for X position, 1 byte for Y position +#define HID_MOUSE_EVENT_SIZE 4 + +/** + * Mouse descriptor from the specification: + * + * + * Appendix E (p71): §E.10 Report Descriptor (Mouse) + * + * The usage tags (like Wheel) are listed in "HID Usage Tables": + * + * §4 Generic Desktop Page (0x01) (p26) + */ +static const unsigned char mouse_report_desc[] = { + // Usage Page (Generic Desktop) + 0x05, 0x01, + // Usage (Mouse) + 0x09, 0x02, + + // Collection (Application) + 0xA1, 0x01, + + // Usage (Pointer) + 0x09, 0x01, + + // Collection (Physical) + 0xA1, 0x00, + + // Usage Page (Buttons) + 0x05, 0x09, + + // Usage Minimum (1) + 0x19, 0x01, + // Usage Maximum (5) + 0x29, 0x05, + // Logical Minimum (0) + 0x15, 0x00, + // Logical Maximum (1) + 0x25, 0x01, + // Report Count (5) + 0x95, 0x05, + // Report Size (1) + 0x75, 0x01, + // Input (Data, Variable, Absolute): 5 buttons bits + 0x81, 0x02, + + // Report Count (1) + 0x95, 0x01, + // Report Size (3) + 0x75, 0x03, + // Input (Constant): 3 bits padding + 0x81, 0x01, + + // Usage Page (Generic Desktop) + 0x05, 0x01, + // Usage (X) + 0x09, 0x30, + // Usage (Y) + 0x09, 0x31, + // Usage (Wheel) + 0x09, 0x38, + // Local Minimum (-127) + 0x15, 0x81, + // Local Maximum (127) + 0x25, 0x7F, + // Report Size (8) + 0x75, 0x08, + // Report Count (3) + 0x95, 0x03, + // Input (Data, Variable, Relative): 3 position bytes (X, Y, Wheel) + 0x81, 0x06, + + // End Collection + 0xC0, + + // End Collection + 0xC0, +}; + +/** + * A mouse HID event is 3 bytes long: + * + * - byte 0: buttons state + * - byte 1: relative x motion (signed byte from -127 to 127) + * - byte 2: relative y motion (signed byte from -127 to 127) + * + * 7 6 5 4 3 2 1 0 + * +---------------+ + * byte 0: |0 0 0 . . . . .| buttons state + * +---------------+ + * ^ ^ ^ ^ ^ + * | | | | `- left button + * | | | `--- right button + * | | `----- middle button + * | `------- button 4 + * `--------- button 5 + * + * +---------------+ + * byte 1: |. . . . . . . .| relative x motion + * +---------------+ + * byte 2: |. . . . . . . .| relative y motion + * +---------------+ + * byte 3: |. . . . . . . .| wheel motion (-1, 0 or 1) + * +---------------+ + * + * As an example, here is the report for a motion of (x=5, y=-4) with left + * button pressed: + * + * +---------------+ + * |0 0 0 0 0 0 0 1| left button pressed + * +---------------+ + * |0 0 0 0 0 1 0 1| horizontal motion (x = 5) + * +---------------+ + * |1 1 1 1 1 1 0 0| relative y motion (y = -4) + * +---------------+ + * |0 0 0 0 0 0 0 0| wheel motion + * +---------------+ + */ + +static bool +sc_hid_mouse_event_init(struct sc_hid_event *hid_event) { + unsigned char *buffer = calloc(1, HID_MOUSE_EVENT_SIZE); + if (!buffer) { + LOG_OOM(); + return false; + } + + sc_hid_event_init(hid_event, HID_MOUSE_ACCESSORY_ID, buffer, + HID_MOUSE_EVENT_SIZE); + return true; +} + +static unsigned char +buttons_state_to_hid_buttons(uint8_t buttons_state) { + unsigned char c = 0; + if (buttons_state & SC_MOUSE_BUTTON_LEFT) { + c |= 1 << 0; + } + if (buttons_state & SC_MOUSE_BUTTON_RIGHT) { + c |= 1 << 1; + } + if (buttons_state & SC_MOUSE_BUTTON_MIDDLE) { + c |= 1 << 2; + } + if (buttons_state & SC_MOUSE_BUTTON_X1) { + c |= 1 << 3; + } + if (buttons_state & SC_MOUSE_BUTTON_X2) { + c |= 1 << 4; + } + return c; +} + +static void +sc_mouse_processor_process_mouse_motion(struct sc_mouse_processor *mp, + const struct sc_mouse_motion_event *event) { + struct sc_hid_mouse *mouse = DOWNCAST(mp); + + struct sc_hid_event hid_event; + if (!sc_hid_mouse_event_init(&hid_event)) { + return; + } + + unsigned char *buffer = hid_event.buffer; + buffer[0] = buttons_state_to_hid_buttons(event->buttons_state); + buffer[1] = CLAMP(event->xrel, -127, 127); + buffer[2] = CLAMP(event->yrel, -127, 127); + buffer[3] = 0; // wheel coordinates only used for scrolling + + if (!sc_aoa_push_hid_event(mouse->aoa, &hid_event)) { + sc_hid_event_destroy(&hid_event); + LOGW("Could request HID event"); + } +} + +static void +sc_mouse_processor_process_mouse_click(struct sc_mouse_processor *mp, + const struct sc_mouse_click_event *event) { + struct sc_hid_mouse *mouse = DOWNCAST(mp); + + struct sc_hid_event hid_event; + if (!sc_hid_mouse_event_init(&hid_event)) { + return; + } + + unsigned char *buffer = hid_event.buffer; + buffer[0] = buttons_state_to_hid_buttons(event->buttons_state); + buffer[1] = 0; // no x motion + buffer[2] = 0; // no y motion + buffer[3] = 0; // wheel coordinates only used for scrolling + + if (!sc_aoa_push_hid_event(mouse->aoa, &hid_event)) { + sc_hid_event_destroy(&hid_event); + LOGW("Could request HID event"); + } +} + +static void +sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp, + const struct sc_mouse_scroll_event *event) { + struct sc_hid_mouse *mouse = DOWNCAST(mp); + + struct sc_hid_event hid_event; + if (!sc_hid_mouse_event_init(&hid_event)) { + return; + } + + unsigned char *buffer = hid_event.buffer; + buffer[0] = 0; // buttons state irrelevant (and unknown) + buffer[1] = 0; // no x motion + buffer[2] = 0; // no y motion + // In practice, vscroll is always -1, 0 or 1, but in theory other values + // are possible + buffer[3] = CLAMP(event->vscroll, -127, 127); + // Horizontal scrolling ignored + + if (!sc_aoa_push_hid_event(mouse->aoa, &hid_event)) { + sc_hid_event_destroy(&hid_event); + LOGW("Could request HID event"); + } +} + +bool +sc_hid_mouse_init(struct sc_hid_mouse *mouse, struct sc_aoa *aoa) { + mouse->aoa = aoa; + + bool ok = sc_aoa_setup_hid(aoa, HID_MOUSE_ACCESSORY_ID, mouse_report_desc, + ARRAY_LEN(mouse_report_desc)); + if (!ok) { + LOGW("Register HID mouse failed"); + return false; + } + + static const struct sc_mouse_processor_ops ops = { + .process_mouse_motion = sc_mouse_processor_process_mouse_motion, + .process_mouse_click = sc_mouse_processor_process_mouse_click, + .process_mouse_scroll = sc_mouse_processor_process_mouse_scroll, + // Touch events not supported (coordinates are not relative) + .process_touch = NULL, + }; + + mouse->mouse_processor.ops = &ops; + + mouse->mouse_processor.relative_mode = true; + + return true; +} + +void +sc_hid_mouse_destroy(struct sc_hid_mouse *mouse) { + bool ok = sc_aoa_unregister_hid(mouse->aoa, HID_MOUSE_ACCESSORY_ID); + if (!ok) { + LOGW("Could not unregister HID"); + } +} diff --git a/app/src/hid_mouse.h b/app/src/hid_mouse.h new file mode 100644 index 00000000..2819b1ff --- /dev/null +++ b/app/src/hid_mouse.h @@ -0,0 +1,23 @@ +#ifndef HID_MOUSE_H +#define HID_MOUSE_H + +#include "common.h" + +#include + +#include "aoa_hid.h" +#include "trait/mouse_processor.h" + +struct sc_hid_mouse { + struct sc_mouse_processor mouse_processor; // mouse processor trait + + struct sc_aoa *aoa; +}; + +bool +sc_hid_mouse_init(struct sc_hid_mouse *mouse, struct sc_aoa *aoa); + +void +sc_hid_mouse_destroy(struct sc_hid_mouse *mouse); + +#endif diff --git a/app/src/options.h b/app/src/options.h index 533f4a3b..b2c69664 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -38,6 +38,11 @@ enum sc_keyboard_input_mode { SC_KEYBOARD_INPUT_MODE_HID, }; +enum sc_mouse_input_mode { + SC_MOUSE_INPUT_MODE_INJECT, + SC_MOUSE_INPUT_MODE_HID, +}; + enum sc_key_inject_mode { // Inject special keys, letters and space as key events. // Inject numbers and punctuation as text events. @@ -90,6 +95,7 @@ struct scrcpy_options { enum sc_log_level log_level; enum sc_record_format record_format; enum sc_keyboard_input_mode keyboard_input_mode; + enum sc_mouse_input_mode mouse_input_mode; struct sc_port_range port_range; uint32_t tunnel_host; uint16_t tunnel_port; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 5907c91e..3e50aaca 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -19,6 +19,7 @@ #include "file_handler.h" #ifdef HAVE_AOA_HID # include "hid_keyboard.h" +# include "hid_mouse.h" #endif #include "keyboard_inject.h" #include "mouse_inject.h" @@ -55,7 +56,12 @@ struct scrcpy { struct sc_hid_keyboard keyboard_hid; #endif }; - struct sc_mouse_inject mouse_inject; + union { + struct sc_mouse_inject mouse_inject; +#ifdef HAVE_AOA_HID + struct sc_hid_mouse mouse_hid; +#endif + }; }; static inline void @@ -330,6 +336,7 @@ scrcpy(struct scrcpy_options *options) { #ifdef HAVE_AOA_HID bool aoa_hid_initialized = false; bool hid_keyboard_initialized = false; + bool hid_mouse_initialized = false; #endif bool controller_initialized = false; bool controller_started = false; @@ -451,7 +458,9 @@ scrcpy(struct scrcpy_options *options) { #ifdef HAVE_AOA_HID bool use_hid_keyboard = options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_HID; - if (use_hid_keyboard) { + bool use_hid_mouse = + options->mouse_input_mode == SC_MOUSE_INPUT_MODE_HID; + if (use_hid_keyboard || use_hid_mouse) { bool ok = sc_acksync_init(&s->acksync); if (!ok) { goto end; @@ -473,7 +482,16 @@ scrcpy(struct scrcpy_options *options) { } } - bool need_aoa = hid_keyboard_initialized; + if (use_hid_mouse) { + if (sc_hid_mouse_init(&s->mouse_hid, &s->aoa)) { + hid_mouse_initialized = true; + mp = &s->mouse_hid.mouse_processor; + } else { + LOGE("Could not initialized HID mouse"); + } + } + + bool need_aoa = hid_keyboard_initialized || hid_mouse_initialized; if (!need_aoa || !sc_aoa_start(&s->aoa)) { sc_acksync_destroy(&s->acksync); @@ -491,6 +509,10 @@ aoa_hid_end: sc_hid_keyboard_destroy(&s->keyboard_hid); hid_keyboard_initialized = false; } + if (hid_mouse_initialized) { + sc_hid_mouse_destroy(&s->mouse_hid); + hid_mouse_initialized = false; + } } if (use_hid_keyboard && !hid_keyboard_initialized) { @@ -498,9 +520,16 @@ aoa_hid_end: "(-K/--hid-keyboard ignored)"); options->keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_INJECT; } + + if (use_hid_mouse && !hid_mouse_initialized) { + LOGE("Fallback to default mouse injection method " + "(-M/--hid-mouse ignored)"); + options->mouse_input_mode = SC_MOUSE_INPUT_MODE_INJECT; + } } #else assert(options->keyboard_input_mode != SC_KEYBOARD_INPUT_MODE_HID); + assert(options->mouse_input_mode != SC_MOUSE_INPUT_MODE_HID); #endif // keyboard_input_mode may have been reset if HID mode failed @@ -510,8 +539,11 @@ aoa_hid_end: kp = &s->keyboard_inject.key_processor; } - sc_mouse_inject_init(&s->mouse_inject, &s->controller); - mp = &s->mouse_inject.mouse_processor; + // mouse_input_mode may have been reset if HID mode failed + if (options->mouse_input_mode == SC_MOUSE_INPUT_MODE_INJECT) { + sc_mouse_inject_init(&s->mouse_inject, &s->controller); + mp = &s->mouse_inject.mouse_processor; + } if (!controller_init(&s->controller, s->server.control_socket, acksync)) { From 43aff4af73261a46ee291d5e8fbfd70c7d9cd0ea Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 2 Jan 2022 00:37:53 +0100 Subject: [PATCH 0952/2244] Document HID mouse in README --- README.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/README.md b/README.md index 8a177406..edf48cfd 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,8 @@ Its features include: - device screen [as a webcam (V4L2)](#v4l2loopback) (Linux-only) - [physical keyboard simulation (HID)](#physical-keyboard-simulation-hid) (Linux-only) + - [physical mouse simulation (HID)](#physical-mouse-simulation-hid) + (Linux-only) - and more… ## Requirements @@ -815,6 +817,35 @@ a physical keyboard is connected). [Physical keyboard]: https://github.com/Genymobile/scrcpy/pull/2632#issuecomment-923756915 +#### Physical mouse simulation (HID) + +Similarly to the physical keyboard simulation, it is possible to simulate a +physical mouse. Likewise, it only works if the device is connected by USB, and +is currently only supported on Linux. + +By default, scrcpy uses Android mouse events injection, using absolute +coordinates. By simulating a physical mouse, a mouse pointer appears on the +Android device, and relative mouse motion, clicks and scrolls are injected. + +To enable this mode: + +```bash +scrcpy --hid-mouse +scrcpy -M # short version +``` + +You could also add `--forward-all-clicks` to [forward all mouse +buttons][forward_all_clicks]. + +[forward_all_clicks]: #right-click-and-middle-click + +When this mode is enabled, the computer mouse is "captured" (the mouse pointer +disappears from the computer and appears on the Android device instead). + +Special capture keys, either Alt or Super, toggle +(disable or enable) the mouse capture. Use one of them to give the control of +the mouse back to the computer. + #### Text injection preference From 75655194fb08d8ea92cd72ca42d6bd0bea191f05 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 14 Jan 2022 20:55:44 +0100 Subject: [PATCH 0953/2244] Do not pass scrcpy_options to keyboard inject The components should be configurable independently of the global scrcpy_options instance: their configuration could be provided separately, like it is the case for example for some screen parameters. For consistency, keyboard injection should not depend on scrcpy_options. --- app/src/keyboard_inject.c | 7 ++++--- app/src/keyboard_inject.h | 3 ++- app/src/scrcpy.c | 3 ++- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/app/src/keyboard_inject.c b/app/src/keyboard_inject.c index 76357d85..b6dd69fb 100644 --- a/app/src/keyboard_inject.c +++ b/app/src/keyboard_inject.c @@ -325,10 +325,11 @@ sc_key_processor_process_text(struct sc_key_processor *kp, void sc_keyboard_inject_init(struct sc_keyboard_inject *ki, struct controller *controller, - const struct scrcpy_options *options) { + enum sc_key_inject_mode key_inject_mode, + bool forward_key_repeat) { ki->controller = controller; - ki->key_inject_mode = options->key_inject_mode; - ki->forward_key_repeat = options->forward_key_repeat; + ki->key_inject_mode = key_inject_mode; + ki->forward_key_repeat = forward_key_repeat; ki->repeat = 0; diff --git a/app/src/keyboard_inject.h b/app/src/keyboard_inject.h index edd5b1ba..9b25425a 100644 --- a/app/src/keyboard_inject.h +++ b/app/src/keyboard_inject.h @@ -25,6 +25,7 @@ struct sc_keyboard_inject { void sc_keyboard_inject_init(struct sc_keyboard_inject *ki, struct controller *controller, - const struct scrcpy_options *options); + enum sc_key_inject_mode key_inject_mode, + bool forward_key_repeat); #endif diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 3e50aaca..474b007b 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -535,7 +535,8 @@ aoa_hid_end: // keyboard_input_mode may have been reset if HID mode failed if (options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_INJECT) { sc_keyboard_inject_init(&s->keyboard_inject, &s->controller, - options); + options->key_inject_mode, + options->forward_key_repeat); kp = &s->keyboard_inject.key_processor; } From a6644e831bbd005af1a92f6614bf901764d6eac8 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 14 Jan 2022 20:57:03 +0100 Subject: [PATCH 0954/2244] Fix code style Limit to 80 chars. --- app/src/aoa_hid.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/aoa_hid.c b/app/src/aoa_hid.c index abf74cf9..9de918bc 100644 --- a/app/src/aoa_hid.c +++ b/app/src/aoa_hid.c @@ -343,8 +343,8 @@ run_aoa_thread(void *data) { if (ack_to_wait != SC_SEQUENCE_INVALID) { LOGD("Waiting ack from server sequence=%" PRIu64_, ack_to_wait); - // Do not block the loop indefinitely if the ack never comes (it should - // never happen) + // Do not block the loop indefinitely if the ack never comes (it + // should never happen) sc_tick deadline = sc_tick_now() + SC_TICK_FROM_MS(500); enum sc_acksync_wait_result result = sc_acksync_wait(aoa->acksync, ack_to_wait, deadline); From 2a0c2e5e99ac5479c2b7ec867f21957deb1c3fbe Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 14 Jan 2022 22:17:30 +0100 Subject: [PATCH 0955/2244] Use sc_ prefix for screen --- app/src/input_manager.c | 43 +++++++------- app/src/input_manager.h | 4 +- app/src/scrcpy.c | 16 +++--- app/src/screen.c | 123 ++++++++++++++++++++-------------------- app/src/screen.h | 34 +++++------ 5 files changed, 112 insertions(+), 108 deletions(-) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index e407a541..450f0059 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -369,15 +369,15 @@ rotate_device(struct controller *controller) { } static void -rotate_client_left(struct screen *screen) { +rotate_client_left(struct sc_screen *screen) { unsigned new_rotation = (screen->rotation + 1) % 4; - screen_set_rotation(screen, new_rotation); + sc_screen_set_rotation(screen, new_rotation); } static void -rotate_client_right(struct screen *screen) { +rotate_client_right(struct sc_screen *screen) { unsigned new_rotation = (screen->rotation + 3) % 4; - screen_set_rotation(screen, new_rotation); + sc_screen_set_rotation(screen, new_rotation); } static void @@ -544,17 +544,17 @@ input_manager_process_key(struct input_manager *im, return; case SDLK_f: if (!shift && !repeat && down) { - screen_switch_fullscreen(im->screen); + sc_screen_switch_fullscreen(im->screen); } return; case SDLK_w: if (!shift && !repeat && down) { - screen_resize_to_fit(im->screen); + sc_screen_resize_to_fit(im->screen); } return; case SDLK_g: if (!shift && !repeat && down) { - screen_resize_to_pixel_perfect(im->screen); + sc_screen_resize_to_pixel_perfect(im->screen); } return; case SDLK_i: @@ -640,8 +640,9 @@ input_manager_process_mouse_motion(struct input_manager *im, struct sc_mouse_motion_event evt = { .position = { .screen_size = im->screen->frame_size, - .point = screen_convert_window_to_frame_coords(im->screen, - event->x, event->y), + .point = sc_screen_convert_window_to_frame_coords(im->screen, + event->x, + event->y), }, .xrel = event->xrel, .yrel = event->yrel, @@ -659,8 +660,8 @@ input_manager_process_mouse_motion(struct input_manager *im, if (im->vfinger_down) { assert(!im->mp->relative_mode); // assert one more time struct sc_point mouse = - screen_convert_window_to_frame_coords(im->screen, event->x, - event->y); + sc_screen_convert_window_to_frame_coords(im->screen, event->x, + event->y); struct sc_point vfinger = inverse_point(mouse, im->screen->frame_size); simulate_virtual_finger(im, AMOTION_EVENT_ACTION_MOVE, vfinger); } @@ -685,7 +686,8 @@ input_manager_process_touch(struct input_manager *im, struct sc_touch_event evt = { .position = { .screen_size = im->screen->frame_size, - .point = screen_convert_drawable_to_frame_coords(im->screen, x, y), + .point = + sc_screen_convert_drawable_to_frame_coords(im->screen, x, y), }, .action = sc_touch_action_from_sdl(event->type), .pointer_id = event->fingerId, @@ -734,13 +736,13 @@ input_manager_process_mouse_button(struct input_manager *im, if (event->button == SDL_BUTTON_LEFT && event->clicks == 2) { int32_t x = event->x; int32_t y = event->y; - screen_hidpi_scale_coords(im->screen, &x, &y); + sc_screen_hidpi_scale_coords(im->screen, &x, &y); SDL_Rect *r = &im->screen->rect; bool outside = x < r->x || x >= r->x + r->w || y < r->y || y >= r->y + r->h; if (outside) { if (down) { - screen_resize_to_fit(im->screen); + sc_screen_resize_to_fit(im->screen); } return; } @@ -757,8 +759,9 @@ input_manager_process_mouse_button(struct input_manager *im, struct sc_mouse_click_event evt = { .position = { .screen_size = im->screen->frame_size, - .point = screen_convert_window_to_frame_coords(im->screen, event->x, - event->y), + .point = sc_screen_convert_window_to_frame_coords(im->screen, + event->x, + event->y), }, .action = sc_action_from_sdl_mousebutton_type(event->type), .button = sc_mouse_button_from_sdl(event->button), @@ -790,8 +793,8 @@ input_manager_process_mouse_button(struct input_manager *im, ((down && !im->vfinger_down && CTRL_PRESSED) || (!down && im->vfinger_down))) { struct sc_point mouse = - screen_convert_window_to_frame_coords(im->screen, event->x, - event->y); + sc_screen_convert_window_to_frame_coords(im->screen, event->x, + event->y); struct sc_point vfinger = inverse_point(mouse, im->screen->frame_size); enum android_motionevent_action action = down ? AMOTION_EVENT_ACTION_DOWN @@ -819,8 +822,8 @@ input_manager_process_mouse_wheel(struct input_manager *im, struct sc_mouse_scroll_event evt = { .position = { .screen_size = im->screen->frame_size, - .point = screen_convert_window_to_frame_coords(im->screen, - mouse_x, mouse_y), + .point = sc_screen_convert_window_to_frame_coords(im->screen, + mouse_x, mouse_y), }, .hscroll = event->x, .vscroll = event->y, diff --git a/app/src/input_manager.h b/app/src/input_manager.h index 088406f6..4b5e7967 100644 --- a/app/src/input_manager.h +++ b/app/src/input_manager.h @@ -15,7 +15,7 @@ struct input_manager { struct controller *controller; - struct screen *screen; + struct sc_screen *screen; struct sc_key_processor *kp; struct sc_mouse_processor *mp; @@ -44,7 +44,7 @@ struct input_manager { struct input_manager_params { struct controller *controller; - struct screen *screen; + struct sc_screen *screen; struct sc_key_processor *kp; struct sc_mouse_processor *mp; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 474b007b..9237c59b 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -36,7 +36,7 @@ struct scrcpy { struct sc_server server; - struct screen screen; + struct sc_screen screen; struct stream stream; struct decoder decoder; struct recorder recorder; @@ -192,7 +192,7 @@ handle_event(struct scrcpy *s, const struct scrcpy_options *options, } } - bool consumed = screen_handle_event(&s->screen, event); + bool consumed = sc_screen_handle_event(&s->screen, event); (void) consumed; end: @@ -573,7 +573,7 @@ aoa_hid_end: const char *window_title = options->window_title ? options->window_title : info->device_name; - struct screen_params screen_params = { + struct sc_screen_params screen_params = { .controller = &s->controller, .kp = kp, .mp = mp, @@ -596,7 +596,7 @@ aoa_hid_end: .buffering_time = options->display_buffer, }; - if (!screen_init(&s->screen, &screen_params)) { + if (!sc_screen_init(&s->screen, &screen_params)) { goto end; } screen_initialized = true; @@ -629,7 +629,7 @@ aoa_hid_end: // Close the window immediately on closing, because screen_destroy() may // only be called once the stream thread is joined (it may take time) - screen_hide_window(&s->screen); + sc_screen_hide_window(&s->screen); end: // The stream is not stopped explicitly, because it will stop by itself on @@ -652,7 +652,7 @@ end: file_handler_stop(&s->file_handler); } if (screen_initialized) { - screen_interrupt(&s->screen); + sc_screen_interrupt(&s->screen); } if (server_started) { @@ -682,8 +682,8 @@ end: // Destroy the screen only after the stream is guaranteed to be finished, // because otherwise the screen could receive new frames after destruction if (screen_initialized) { - screen_join(&s->screen); - screen_destroy(&s->screen); + sc_screen_join(&s->screen); + sc_screen_destroy(&s->screen); } if (controller_started) { diff --git a/app/src/screen.c b/app/src/screen.c index 9c9f80bc..3fca6f21 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -12,7 +12,7 @@ #define DISPLAY_MARGINS 96 -#define DOWNCAST(SINK) container_of(SINK, struct screen, frame_sink) +#define DOWNCAST(SINK) container_of(SINK, struct sc_screen, frame_sink) static inline struct sc_size get_rotated_size(struct sc_size size, int rotation) { @@ -29,7 +29,7 @@ get_rotated_size(struct sc_size size, int rotation) { // get the window size in a struct sc_size static struct sc_size -get_window_size(const struct screen *screen) { +get_window_size(const struct sc_screen *screen) { int width; int height; SDL_GetWindowSize(screen->window, &width, &height); @@ -41,7 +41,7 @@ get_window_size(const struct screen *screen) { } static struct sc_point -get_window_position(const struct screen *screen) { +get_window_position(const struct sc_screen *screen) { int x; int y; SDL_GetWindowPosition(screen->window, &x, &y); @@ -54,7 +54,7 @@ get_window_position(const struct screen *screen) { // set the window size to be applied when fullscreen is disabled static void -set_window_size(struct screen *screen, struct sc_size new_size) { +set_window_size(struct sc_screen *screen, struct sc_size new_size) { assert(!screen->fullscreen); assert(!screen->maximized); SDL_SetWindowSize(screen->window, new_size.width, new_size.height); @@ -157,7 +157,7 @@ get_initial_optimal_size(struct sc_size content_size, uint16_t req_width, } static inline void -screen_capture_mouse(struct screen *screen, bool capture) { +sc_screen_capture_mouse(struct sc_screen *screen, bool capture) { if (SDL_SetRelativeMouseMode(capture)) { LOGE("Could not set relative mouse mode to %s: %s", capture ? "true" : "false", SDL_GetError()); @@ -168,7 +168,7 @@ screen_capture_mouse(struct screen *screen, bool capture) { } static void -screen_update_content_rect(struct screen *screen) { +sc_screen_update_content_rect(struct sc_screen *screen) { int dw; int dh; SDL_GL_GetDrawableSize(screen->window, &dw, &dh); @@ -205,7 +205,7 @@ screen_update_content_rect(struct screen *screen) { } static inline SDL_Texture * -create_texture(struct screen *screen) { +create_texture(struct sc_screen *screen) { SDL_Renderer *renderer = screen->renderer; struct sc_size size = screen->frame_size; SDL_Texture *texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_YV12, @@ -236,9 +236,9 @@ create_texture(struct screen *screen) { // Set the update_content_rect flag if the window or content size may have // changed, so that the content rectangle is recomputed static void -screen_render(struct screen *screen, bool update_content_rect) { +sc_screen_render(struct sc_screen *screen, bool update_content_rect) { if (update_content_rect) { - screen_update_content_rect(screen); + sc_screen_update_content_rect(screen); } SDL_RenderClear(screen->renderer); @@ -282,20 +282,20 @@ screen_render(struct screen *screen, bool update_content_rect) { // static int event_watcher(void *data, SDL_Event *event) { - struct screen *screen = data; + struct sc_screen *screen = data; if (event->type == SDL_WINDOWEVENT && 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, true); + sc_screen_render(screen, true); } return 0; } #endif static bool -screen_frame_sink_open(struct sc_frame_sink *sink) { - struct screen *screen = DOWNCAST(sink); +sc_screen_frame_sink_open(struct sc_frame_sink *sink) { + struct sc_screen *screen = DOWNCAST(sink); (void) screen; #ifndef NDEBUG screen->open = true; @@ -306,8 +306,8 @@ screen_frame_sink_open(struct sc_frame_sink *sink) { } static void -screen_frame_sink_close(struct sc_frame_sink *sink) { - struct screen *screen = DOWNCAST(sink); +sc_screen_frame_sink_close(struct sc_frame_sink *sink) { + struct sc_screen *screen = DOWNCAST(sink); (void) screen; #ifndef NDEBUG screen->open = false; @@ -317,8 +317,8 @@ screen_frame_sink_close(struct sc_frame_sink *sink) { } static bool -screen_frame_sink_push(struct sc_frame_sink *sink, const AVFrame *frame) { - struct screen *screen = DOWNCAST(sink); +sc_screen_frame_sink_push(struct sc_frame_sink *sink, const AVFrame *frame) { + struct sc_screen *screen = DOWNCAST(sink); return sc_video_buffer_push(&screen->vb, frame); } @@ -326,7 +326,7 @@ static void sc_video_buffer_on_new_frame(struct sc_video_buffer *vb, bool previous_skipped, void *userdata) { (void) vb; - struct screen *screen = userdata; + struct sc_screen *screen = userdata; // event_failed implies previous_skipped (the previous frame may not have // been consumed if the event was not sent) @@ -359,7 +359,8 @@ sc_video_buffer_on_new_frame(struct sc_video_buffer *vb, bool previous_skipped, } bool -screen_init(struct screen *screen, const struct screen_params *params) { +sc_screen_init(struct sc_screen *screen, + const struct sc_screen_params *params) { screen->resize_pending = false; screen->has_frame = false; screen->fullscreen = false; @@ -502,10 +503,10 @@ screen_init(struct screen *screen, const struct screen_params *params) { // different HiDPI scaling are connected SDL_SetWindowSize(screen->window, window_size.width, window_size.height); - screen_update_content_rect(screen); + sc_screen_update_content_rect(screen); if (params->fullscreen) { - screen_switch_fullscreen(screen); + sc_screen_switch_fullscreen(screen); } #ifdef CONTINUOUS_RESIZING_WORKAROUND @@ -513,9 +514,9 @@ screen_init(struct screen *screen, const struct screen_params *params) { #endif static const struct sc_frame_sink_ops ops = { - .open = screen_frame_sink_open, - .close = screen_frame_sink_close, - .push = screen_frame_sink_push, + .open = sc_screen_frame_sink_open, + .close = sc_screen_frame_sink_close, + .push = sc_screen_frame_sink_push, }; screen->frame_sink.ops = &ops; @@ -544,29 +545,29 @@ error_destroy_video_buffer: } static void -screen_show_window(struct screen *screen) { +sc_screen_show_window(struct sc_screen *screen) { SDL_ShowWindow(screen->window); } void -screen_hide_window(struct screen *screen) { +sc_screen_hide_window(struct sc_screen *screen) { SDL_HideWindow(screen->window); } void -screen_interrupt(struct screen *screen) { +sc_screen_interrupt(struct sc_screen *screen) { sc_video_buffer_stop(&screen->vb); fps_counter_interrupt(&screen->fps_counter); } void -screen_join(struct screen *screen) { +sc_screen_join(struct sc_screen *screen) { sc_video_buffer_join(&screen->vb); fps_counter_join(&screen->fps_counter); } void -screen_destroy(struct screen *screen) { +sc_screen_destroy(struct sc_screen *screen) { #ifndef NDEBUG assert(!screen->open); #endif @@ -579,7 +580,7 @@ screen_destroy(struct screen *screen) { } static void -resize_for_content(struct screen *screen, struct sc_size old_content_size, +resize_for_content(struct sc_screen *screen, struct sc_size old_content_size, struct sc_size new_content_size) { struct sc_size window_size = get_window_size(screen); struct sc_size target_size = { @@ -593,7 +594,7 @@ resize_for_content(struct screen *screen, struct sc_size old_content_size, } static void -set_content_size(struct screen *screen, struct sc_size new_content_size) { +set_content_size(struct sc_screen *screen, struct sc_size new_content_size) { if (!screen->fullscreen && !screen->maximized) { resize_for_content(screen, screen->content_size, new_content_size); } else if (!screen->resize_pending) { @@ -607,7 +608,7 @@ set_content_size(struct screen *screen, struct sc_size new_content_size) { } static void -apply_pending_resize(struct screen *screen) { +apply_pending_resize(struct sc_screen *screen) { assert(!screen->fullscreen); assert(!screen->maximized); if (screen->resize_pending) { @@ -618,7 +619,7 @@ apply_pending_resize(struct screen *screen) { } void -screen_set_rotation(struct screen *screen, unsigned rotation) { +sc_screen_set_rotation(struct sc_screen *screen, unsigned rotation) { assert(rotation < 4); if (rotation == screen->rotation) { return; @@ -632,12 +633,12 @@ screen_set_rotation(struct screen *screen, unsigned rotation) { screen->rotation = rotation; LOGI("Display rotation set to %u", rotation); - screen_render(screen, true); + sc_screen_render(screen, true); } // recreate the texture and resize the window if the frame size has changed static bool -prepare_for_frame(struct screen *screen, struct sc_size new_frame_size) { +prepare_for_frame(struct sc_screen *screen, struct sc_size new_frame_size) { if (screen->frame_size.width != new_frame_size.width || screen->frame_size.height != new_frame_size.height) { // frame dimension changed, destroy texture @@ -649,7 +650,7 @@ prepare_for_frame(struct screen *screen, struct sc_size new_frame_size) { get_rotated_size(new_frame_size, screen->rotation); set_content_size(screen, new_content_size); - screen_update_content_rect(screen); + sc_screen_update_content_rect(screen); LOGI("New texture: %" PRIu16 "x%" PRIu16, screen->frame_size.width, screen->frame_size.height); @@ -665,7 +666,7 @@ prepare_for_frame(struct screen *screen, struct sc_size new_frame_size) { // write the frame into the texture static void -update_texture(struct screen *screen, const AVFrame *frame) { +update_texture(struct sc_screen *screen, const AVFrame *frame) { SDL_UpdateYUVTexture(screen->texture, NULL, frame->data[0], frame->linesize[0], frame->data[1], frame->linesize[1], @@ -679,7 +680,7 @@ update_texture(struct screen *screen, const AVFrame *frame) { } static bool -screen_update_frame(struct screen *screen) { +sc_screen_update_frame(struct sc_screen *screen) { av_frame_unref(screen->frame); sc_video_buffer_consume(&screen->vb, screen->frame); AVFrame *frame = screen->frame; @@ -692,12 +693,12 @@ screen_update_frame(struct screen *screen) { } update_texture(screen, frame); - screen_render(screen, false); + sc_screen_render(screen, false); return true; } void -screen_switch_fullscreen(struct screen *screen) { +sc_screen_switch_fullscreen(struct sc_screen *screen) { uint32_t new_mode = screen->fullscreen ? 0 : SDL_WINDOW_FULLSCREEN_DESKTOP; if (SDL_SetWindowFullscreen(screen->window, new_mode)) { LOGW("Could not switch fullscreen mode: %s", SDL_GetError()); @@ -710,11 +711,11 @@ screen_switch_fullscreen(struct screen *screen) { } LOGD("Switched to %s mode", screen->fullscreen ? "fullscreen" : "windowed"); - screen_render(screen, true); + sc_screen_render(screen, true); } void -screen_resize_to_fit(struct screen *screen) { +sc_screen_resize_to_fit(struct sc_screen *screen) { if (screen->fullscreen || screen->maximized) { return; } @@ -738,7 +739,7 @@ screen_resize_to_fit(struct screen *screen) { } void -screen_resize_to_pixel_perfect(struct screen *screen) { +sc_screen_resize_to_pixel_perfect(struct sc_screen *screen) { if (screen->fullscreen) { return; } @@ -755,20 +756,20 @@ screen_resize_to_pixel_perfect(struct screen *screen) { } static inline bool -screen_is_mouse_capture_key(SDL_Keycode key) { +sc_screen_is_mouse_capture_key(SDL_Keycode key) { return key == SDLK_LALT || key == SDLK_LGUI || key == SDLK_RGUI; } bool -screen_handle_event(struct screen *screen, SDL_Event *event) { +sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event) { switch (event->type) { case EVENT_NEW_FRAME: if (!screen->has_frame) { screen->has_frame = true; // this is the very first frame, show the window - screen_show_window(screen); + sc_screen_show_window(screen); } - bool ok = screen_update_frame(screen); + bool ok = sc_screen_update_frame(screen); if (!ok) { LOGW("Frame update failed\n"); } @@ -780,10 +781,10 @@ screen_handle_event(struct screen *screen, SDL_Event *event) { } switch (event->window.event) { case SDL_WINDOWEVENT_EXPOSED: - screen_render(screen, true); + sc_screen_render(screen, true); break; case SDL_WINDOWEVENT_SIZE_CHANGED: - screen_render(screen, true); + sc_screen_render(screen, true); break; case SDL_WINDOWEVENT_MAXIMIZED: screen->maximized = true; @@ -799,11 +800,11 @@ screen_handle_event(struct screen *screen, SDL_Event *event) { } screen->maximized = false; apply_pending_resize(screen); - screen_render(screen, true); + sc_screen_render(screen, true); break; case SDL_WINDOWEVENT_FOCUS_LOST: if (screen->im.mp->relative_mode) { - screen_capture_mouse(screen, false); + sc_screen_capture_mouse(screen, false); } break; } @@ -811,7 +812,7 @@ screen_handle_event(struct screen *screen, SDL_Event *event) { case SDL_KEYDOWN: if (screen->im.mp->relative_mode) { SDL_Keycode key = event->key.keysym.sym; - if (screen_is_mouse_capture_key(key)) { + if (sc_screen_is_mouse_capture_key(key)) { if (!screen->mouse_capture_key_pressed) { screen->mouse_capture_key_pressed = key; return true; @@ -833,7 +834,7 @@ screen_handle_event(struct screen *screen, SDL_Event *event) { if (key == cap) { // A mouse capture key has been pressed then released: // toggle the capture mouse mode - screen_capture_mouse(screen, !screen->mouse_captured); + sc_screen_capture_mouse(screen, !screen->mouse_captured); return true; } // Do not return, the event must be forwarded to the input @@ -860,7 +861,7 @@ screen_handle_event(struct screen *screen, SDL_Event *event) { break; case SDL_MOUSEBUTTONUP: if (screen->im.mp->relative_mode && !screen->mouse_captured) { - screen_capture_mouse(screen, true); + sc_screen_capture_mouse(screen, true); return true; } } @@ -869,8 +870,8 @@ screen_handle_event(struct screen *screen, SDL_Event *event) { } struct sc_point -screen_convert_drawable_to_frame_coords(struct screen *screen, - int32_t x, int32_t y) { +sc_screen_convert_drawable_to_frame_coords(struct sc_screen *screen, + int32_t x, int32_t y) { unsigned rotation = screen->rotation; assert(rotation < 4); @@ -906,14 +907,14 @@ screen_convert_drawable_to_frame_coords(struct screen *screen, } struct sc_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); +sc_screen_convert_window_to_frame_coords(struct sc_screen *screen, + int32_t x, int32_t y) { + sc_screen_hidpi_scale_coords(screen, &x, &y); + return sc_screen_convert_drawable_to_frame_coords(screen, x, y); } void -screen_hidpi_scale_coords(struct screen *screen, int32_t *x, int32_t *y) { +sc_screen_hidpi_scale_coords(struct sc_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); diff --git a/app/src/screen.h b/app/src/screen.h index f0e15a7d..b3240ae0 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -17,7 +17,7 @@ #include "trait/mouse_processor.h" #include "video_buffer.h" -struct screen { +struct sc_screen { struct sc_frame_sink frame_sink; // frame sink trait #ifndef NDEBUG @@ -59,7 +59,7 @@ struct screen { AVFrame *frame; }; -struct screen_params { +struct sc_screen_params { struct controller *controller; struct sc_key_processor *kp; struct sc_mouse_processor *mp; @@ -91,65 +91,65 @@ struct screen_params { // initialize screen, create window, renderer and texture (window is hidden) bool -screen_init(struct screen *screen, const struct screen_params *params); +sc_screen_init(struct sc_screen *screen, const struct sc_screen_params *params); // request to interrupt any inner thread // must be called before screen_join() void -screen_interrupt(struct screen *screen); +sc_screen_interrupt(struct sc_screen *screen); // join any inner thread void -screen_join(struct screen *screen); +sc_screen_join(struct sc_screen *screen); // destroy window, renderer and texture (if any) void -screen_destroy(struct screen *screen); +sc_screen_destroy(struct sc_screen *screen); // hide the window // // It is used to hide the window immediately on closing without waiting for // screen_destroy() void -screen_hide_window(struct screen *screen); +sc_screen_hide_window(struct sc_screen *screen); // switch the fullscreen mode void -screen_switch_fullscreen(struct screen *screen); +sc_screen_switch_fullscreen(struct sc_screen *screen); // resize window to optimal size (remove black borders) void -screen_resize_to_fit(struct screen *screen); +sc_screen_resize_to_fit(struct sc_screen *screen); // resize window to 1:1 (pixel-perfect) void -screen_resize_to_pixel_perfect(struct screen *screen); +sc_screen_resize_to_pixel_perfect(struct sc_screen *screen); // set the display rotation (0, 1, 2 or 3, x90 degrees counterclockwise) void -screen_set_rotation(struct screen *screen, unsigned rotation); +sc_screen_set_rotation(struct sc_screen *screen, unsigned rotation); // react to SDL events bool -screen_handle_event(struct screen *screen, SDL_Event *event); +sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event); // convert point from window coordinates to frame coordinates // x and y are expressed in pixels struct sc_point -screen_convert_window_to_frame_coords(struct screen *screen, - int32_t x, int32_t y); +sc_screen_convert_window_to_frame_coords(struct sc_screen *screen, + int32_t x, int32_t y); // convert point from drawable coordinates to frame coordinates // x and y are expressed in pixels struct sc_point -screen_convert_drawable_to_frame_coords(struct screen *screen, - int32_t x, int32_t y); +sc_screen_convert_drawable_to_frame_coords(struct sc_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); +sc_screen_hidpi_scale_coords(struct sc_screen *screen, int32_t *x, int32_t *y); #endif From 5f7ddff8ae5e906b3c0835e649ca536ff69316b3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 14 Jan 2022 22:17:30 +0100 Subject: [PATCH 0956/2244] Use sc_ prefix for input_manager --- app/src/input_manager.c | 46 ++++++++++++++++++++--------------------- app/src/input_manager.h | 10 ++++----- app/src/screen.c | 6 +++--- app/src/screen.h | 2 +- 4 files changed, 32 insertions(+), 32 deletions(-) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 450f0059..6eac46aa 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -106,7 +106,7 @@ to_sdl_mod(unsigned shortcut_mod) { } static bool -is_shortcut_mod(struct input_manager *im, uint16_t sdl_mod) { +is_shortcut_mod(struct sc_input_manager *im, uint16_t sdl_mod) { // keep only the relevant modifier keys sdl_mod &= SC_SDL_SHORTCUT_MODS_MASK; @@ -122,8 +122,8 @@ is_shortcut_mod(struct input_manager *im, uint16_t sdl_mod) { } void -input_manager_init(struct input_manager *im, - const struct input_manager_params *params) { +sc_input_manager_init(struct sc_input_manager *im, + const struct sc_input_manager_params *params) { assert(!params->control || (params->kp && params->kp->ops)); assert(!params->control || (params->mp && params->mp->ops)); @@ -381,8 +381,8 @@ rotate_client_right(struct sc_screen *screen) { } static void -input_manager_process_text_input(struct input_manager *im, - const SDL_TextInputEvent *event) { +sc_input_manager_process_text_input(struct sc_input_manager *im, + const SDL_TextInputEvent *event) { if (!im->kp->ops->process_text) { // The key processor does not support text input return; @@ -401,7 +401,7 @@ input_manager_process_text_input(struct input_manager *im, } static bool -simulate_virtual_finger(struct input_manager *im, +simulate_virtual_finger(struct sc_input_manager *im, enum android_motionevent_action action, struct sc_point point) { bool up = action == AMOTION_EVENT_ACTION_UP; @@ -431,8 +431,8 @@ inverse_point(struct sc_point point, struct sc_size size) { } static void -input_manager_process_key(struct input_manager *im, - const SDL_KeyboardEvent *event) { +sc_input_manager_process_key(struct sc_input_manager *im, + const SDL_KeyboardEvent *event) { // control: indicates the state of the command-line option --no-control bool control = im->control; @@ -629,8 +629,8 @@ input_manager_process_key(struct input_manager *im, } static void -input_manager_process_mouse_motion(struct input_manager *im, - const SDL_MouseMotionEvent *event) { +sc_input_manager_process_mouse_motion(struct sc_input_manager *im, + const SDL_MouseMotionEvent *event) { if (event->which == SDL_TOUCH_MOUSEID) { // simulated from touch events, so it's a duplicate @@ -668,8 +668,8 @@ input_manager_process_mouse_motion(struct input_manager *im, } static void -input_manager_process_touch(struct input_manager *im, - const SDL_TouchFingerEvent *event) { +sc_input_manager_process_touch(struct sc_input_manager *im, + const SDL_TouchFingerEvent *event) { if (!im->mp->ops->process_touch) { // The mouse processor does not support touch events return; @@ -698,8 +698,8 @@ input_manager_process_touch(struct input_manager *im, } static void -input_manager_process_mouse_button(struct input_manager *im, - const SDL_MouseButtonEvent *event) { +sc_input_manager_process_mouse_button(struct sc_input_manager *im, + const SDL_MouseButtonEvent *event) { bool control = im->control; if (event->which == SDL_TOUCH_MOUSEID) { @@ -807,8 +807,8 @@ input_manager_process_mouse_button(struct input_manager *im, } static void -input_manager_process_mouse_wheel(struct input_manager *im, - const SDL_MouseWheelEvent *event) { +sc_input_manager_process_mouse_wheel(struct sc_input_manager *im, + const SDL_MouseWheelEvent *event) { if (!im->mp->ops->process_mouse_scroll) { // The mouse processor does not support scroll events return; @@ -835,42 +835,42 @@ input_manager_process_mouse_wheel(struct input_manager *im, } bool -input_manager_handle_event(struct input_manager *im, SDL_Event *event) { +sc_input_manager_handle_event(struct sc_input_manager *im, SDL_Event *event) { switch (event->type) { case SDL_TEXTINPUT: if (!im->control) { return true; } - input_manager_process_text_input(im, &event->text); + sc_input_manager_process_text_input(im, &event->text); return true; case SDL_KEYDOWN: 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(im, &event->key); + sc_input_manager_process_key(im, &event->key); return true; case SDL_MOUSEMOTION: if (!im->control) { break; } - input_manager_process_mouse_motion(im, &event->motion); + sc_input_manager_process_mouse_motion(im, &event->motion); return true; case SDL_MOUSEWHEEL: if (!im->control) { break; } - input_manager_process_mouse_wheel(im, &event->wheel); + sc_input_manager_process_mouse_wheel(im, &event->wheel); return true; case SDL_MOUSEBUTTONDOWN: 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(im, &event->button); + sc_input_manager_process_mouse_button(im, &event->button); return true; case SDL_FINGERMOTION: case SDL_FINGERDOWN: case SDL_FINGERUP: - input_manager_process_touch(im, &event->tfinger); + sc_input_manager_process_touch(im, &event->tfinger); return true; } diff --git a/app/src/input_manager.h b/app/src/input_manager.h index 4b5e7967..9eaea23c 100644 --- a/app/src/input_manager.h +++ b/app/src/input_manager.h @@ -13,7 +13,7 @@ #include "trait/key_processor.h" #include "trait/mouse_processor.h" -struct input_manager { +struct sc_input_manager { struct controller *controller; struct sc_screen *screen; @@ -42,7 +42,7 @@ struct input_manager { uint64_t next_sequence; // used for request acknowledgements }; -struct input_manager_params { +struct sc_input_manager_params { struct controller *controller; struct sc_screen *screen; struct sc_key_processor *kp; @@ -56,10 +56,10 @@ struct input_manager_params { }; void -input_manager_init(struct input_manager *im, - const struct input_manager_params *params); +sc_input_manager_init(struct sc_input_manager *im, + const struct sc_input_manager_params *params); bool -input_manager_handle_event(struct input_manager *im, SDL_Event *event); +sc_input_manager_handle_event(struct sc_input_manager *im, SDL_Event *event); #endif diff --git a/app/src/screen.c b/app/src/screen.c index 3fca6f21..a2796278 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -484,7 +484,7 @@ sc_screen_init(struct sc_screen *screen, goto error_destroy_texture; } - struct input_manager_params im_params = { + struct sc_input_manager_params im_params = { .controller = params->controller, .screen = screen, .kp = params->kp, @@ -496,7 +496,7 @@ sc_screen_init(struct sc_screen *screen, .shortcut_mods = params->shortcut_mods, }; - input_manager_init(&screen->im, &im_params); + sc_input_manager_init(&screen->im, &im_params); // Reset the window size to trigger a SIZE_CHANGED event, to workaround // HiDPI issues with some SDL renderers when several displays having @@ -866,7 +866,7 @@ sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event) { } } - return input_manager_handle_event(&screen->im, event); + return sc_input_manager_handle_event(&screen->im, event); } struct sc_point diff --git a/app/src/screen.h b/app/src/screen.h index b3240ae0..b1a8a58e 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -24,7 +24,7 @@ struct sc_screen { bool open; // track the open/close state to assert correct behavior #endif - struct input_manager im; + struct sc_input_manager im; struct sc_video_buffer vb; struct fps_counter fps_counter; From 3a4d5c7f18de27de450191a4b5401ad94f9d7285 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 14 Jan 2022 22:17:30 +0100 Subject: [PATCH 0957/2244] Use sc_ prefix for controller --- app/src/controller.c | 22 +++++++-------- app/src/controller.h | 22 +++++++-------- app/src/input_manager.c | 58 +++++++++++++++++++-------------------- app/src/input_manager.h | 4 +-- app/src/keyboard_inject.c | 6 ++-- app/src/keyboard_inject.h | 4 +-- app/src/mouse_inject.c | 10 +++---- app/src/mouse_inject.h | 5 ++-- app/src/scrcpy.c | 16 +++++------ app/src/screen.h | 2 +- 10 files changed, 75 insertions(+), 74 deletions(-) diff --git a/app/src/controller.c b/app/src/controller.c index 10eceaf2..531b5fb9 100644 --- a/app/src/controller.c +++ b/app/src/controller.c @@ -5,8 +5,8 @@ #include "util/log.h" bool -controller_init(struct controller *controller, sc_socket control_socket, - struct sc_acksync *acksync) { +sc_controller_init(struct sc_controller *controller, sc_socket control_socket, + struct sc_acksync *acksync) { cbuf_init(&controller->queue); bool ok = receiver_init(&controller->receiver, control_socket, acksync); @@ -34,7 +34,7 @@ controller_init(struct controller *controller, sc_socket control_socket, } void -controller_destroy(struct controller *controller) { +sc_controller_destroy(struct sc_controller *controller) { sc_cond_destroy(&controller->msg_cond); sc_mutex_destroy(&controller->mutex); @@ -47,8 +47,8 @@ controller_destroy(struct controller *controller) { } bool -controller_push_msg(struct controller *controller, - const struct control_msg *msg) { +sc_controller_push_msg(struct sc_controller *controller, + const struct control_msg *msg) { if (sc_get_log_level() <= SC_LOG_LEVEL_VERBOSE) { control_msg_log(msg); } @@ -64,7 +64,7 @@ controller_push_msg(struct controller *controller, } static bool -process_msg(struct controller *controller, const struct control_msg *msg) { +process_msg(struct sc_controller *controller, const struct control_msg *msg) { static unsigned char serialized_msg[CONTROL_MSG_MAX_SIZE]; size_t length = control_msg_serialize(msg, serialized_msg); if (!length) { @@ -77,7 +77,7 @@ process_msg(struct controller *controller, const struct control_msg *msg) { static int run_controller(void *data) { - struct controller *controller = data; + struct sc_controller *controller = data; for (;;) { sc_mutex_lock(&controller->mutex); @@ -106,7 +106,7 @@ run_controller(void *data) { } bool -controller_start(struct controller *controller) { +sc_controller_start(struct sc_controller *controller) { LOGD("Starting controller thread"); bool ok = sc_thread_create(&controller->thread, run_controller, @@ -117,7 +117,7 @@ controller_start(struct controller *controller) { } if (!receiver_start(&controller->receiver)) { - controller_stop(controller); + sc_controller_stop(controller); sc_thread_join(&controller->thread, NULL); return false; } @@ -126,7 +126,7 @@ controller_start(struct controller *controller) { } void -controller_stop(struct controller *controller) { +sc_controller_stop(struct sc_controller *controller) { sc_mutex_lock(&controller->mutex); controller->stopped = true; sc_cond_signal(&controller->msg_cond); @@ -134,7 +134,7 @@ controller_stop(struct controller *controller) { } void -controller_join(struct controller *controller) { +sc_controller_join(struct sc_controller *controller) { sc_thread_join(&controller->thread, NULL); receiver_join(&controller->receiver); } diff --git a/app/src/controller.h b/app/src/controller.h index 00267878..06ceb782 100644 --- a/app/src/controller.h +++ b/app/src/controller.h @@ -12,36 +12,36 @@ #include "util/net.h" #include "util/thread.h" -struct control_msg_queue CBUF(struct control_msg, 64); +struct sc_control_msg_queue CBUF(struct control_msg, 64); -struct controller { +struct sc_controller { sc_socket control_socket; sc_thread thread; sc_mutex mutex; sc_cond msg_cond; bool stopped; - struct control_msg_queue queue; + struct sc_control_msg_queue queue; struct receiver receiver; }; bool -controller_init(struct controller *controller, sc_socket control_socket, - struct sc_acksync *acksync); +sc_controller_init(struct sc_controller *controller, sc_socket control_socket, + struct sc_acksync *acksync); void -controller_destroy(struct controller *controller); +sc_controller_destroy(struct sc_controller *controller); bool -controller_start(struct controller *controller); +sc_controller_start(struct sc_controller *controller); void -controller_stop(struct controller *controller); +sc_controller_stop(struct sc_controller *controller); void -controller_join(struct controller *controller); +sc_controller_join(struct sc_controller *controller); bool -controller_push_msg(struct controller *controller, - const struct control_msg *msg); +sc_controller_push_msg(struct sc_controller *controller, + const struct control_msg *msg); #endif diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 6eac46aa..5b355701 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -157,7 +157,7 @@ sc_input_manager_init(struct sc_input_manager *im, } static void -send_keycode(struct controller *controller, enum android_keycode keycode, +send_keycode(struct sc_controller *controller, enum android_keycode keycode, enum sc_action action, const char *name) { // send DOWN event struct control_msg msg; @@ -169,50 +169,50 @@ send_keycode(struct controller *controller, enum android_keycode keycode, msg.inject_keycode.metastate = 0; msg.inject_keycode.repeat = 0; - if (!controller_push_msg(controller, &msg)) { + if (!sc_controller_push_msg(controller, &msg)) { LOGW("Could not request 'inject %s'", name); } } static inline void -action_home(struct controller *controller, enum sc_action action) { +action_home(struct sc_controller *controller, enum sc_action action) { send_keycode(controller, AKEYCODE_HOME, action, "HOME"); } static inline void -action_back(struct controller *controller, enum sc_action action) { +action_back(struct sc_controller *controller, enum sc_action action) { send_keycode(controller, AKEYCODE_BACK, action, "BACK"); } static inline void -action_app_switch(struct controller *controller, enum sc_action action) { +action_app_switch(struct sc_controller *controller, enum sc_action action) { send_keycode(controller, AKEYCODE_APP_SWITCH, action, "APP_SWITCH"); } static inline void -action_power(struct controller *controller, enum sc_action action) { +action_power(struct sc_controller *controller, enum sc_action action) { send_keycode(controller, AKEYCODE_POWER, action, "POWER"); } static inline void -action_volume_up(struct controller *controller, enum sc_action action) { +action_volume_up(struct sc_controller *controller, enum sc_action action) { send_keycode(controller, AKEYCODE_VOLUME_UP, action, "VOLUME_UP"); } static inline void -action_volume_down(struct controller *controller, enum sc_action action) { +action_volume_down(struct sc_controller *controller, enum sc_action action) { send_keycode(controller, AKEYCODE_VOLUME_DOWN, action, "VOLUME_DOWN"); } static inline void -action_menu(struct controller *controller, enum sc_action action) { +action_menu(struct sc_controller *controller, enum sc_action action) { send_keycode(controller, AKEYCODE_MENU, action, "MENU"); } // turn the screen on if it was off, press BACK otherwise // If the screen is off, it is turned on only on ACTION_DOWN static void -press_back_or_turn_screen_on(struct controller *controller, +press_back_or_turn_screen_on(struct sc_controller *controller, enum sc_action action) { struct control_msg msg; msg.type = CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON; @@ -220,49 +220,49 @@ press_back_or_turn_screen_on(struct controller *controller, ? AKEY_EVENT_ACTION_DOWN : AKEY_EVENT_ACTION_UP; - if (!controller_push_msg(controller, &msg)) { + if (!sc_controller_push_msg(controller, &msg)) { LOGW("Could not request 'press back or turn screen on'"); } } static void -expand_notification_panel(struct controller *controller) { +expand_notification_panel(struct sc_controller *controller) { struct control_msg msg; msg.type = CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL; - if (!controller_push_msg(controller, &msg)) { + if (!sc_controller_push_msg(controller, &msg)) { LOGW("Could not request 'expand notification panel'"); } } static void -expand_settings_panel(struct controller *controller) { +expand_settings_panel(struct sc_controller *controller) { struct control_msg msg; msg.type = CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL; - if (!controller_push_msg(controller, &msg)) { + if (!sc_controller_push_msg(controller, &msg)) { LOGW("Could not request 'expand settings panel'"); } } static void -collapse_panels(struct controller *controller) { +collapse_panels(struct sc_controller *controller) { struct control_msg msg; msg.type = CONTROL_MSG_TYPE_COLLAPSE_PANELS; - if (!controller_push_msg(controller, &msg)) { + if (!sc_controller_push_msg(controller, &msg)) { LOGW("Could not request 'collapse notification panel'"); } } static bool -get_device_clipboard(struct controller *controller, +get_device_clipboard(struct sc_controller *controller, enum get_clipboard_copy_key copy_key) { struct control_msg msg; msg.type = CONTROL_MSG_TYPE_GET_CLIPBOARD; msg.get_clipboard.copy_key = copy_key; - if (!controller_push_msg(controller, &msg)) { + if (!sc_controller_push_msg(controller, &msg)) { LOGW("Could not request 'get device clipboard'"); return false; } @@ -271,7 +271,7 @@ get_device_clipboard(struct controller *controller, } static bool -set_device_clipboard(struct controller *controller, bool paste, +set_device_clipboard(struct sc_controller *controller, bool paste, uint64_t sequence) { char *text = SDL_GetClipboardText(); if (!text) { @@ -292,7 +292,7 @@ set_device_clipboard(struct controller *controller, bool paste, msg.set_clipboard.text = text_dup; msg.set_clipboard.paste = paste; - if (!controller_push_msg(controller, &msg)) { + if (!sc_controller_push_msg(controller, &msg)) { free(text_dup); LOGW("Could not request 'set device clipboard'"); return false; @@ -302,13 +302,13 @@ set_device_clipboard(struct controller *controller, bool paste, } static void -set_screen_power_mode(struct controller *controller, +set_screen_power_mode(struct sc_controller *controller, enum screen_power_mode mode) { struct control_msg msg; msg.type = CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE; msg.set_screen_power_mode.mode = mode; - if (!controller_push_msg(controller, &msg)) { + if (!sc_controller_push_msg(controller, &msg)) { LOGW("Could not request 'set screen power mode'"); } } @@ -330,7 +330,7 @@ switch_fps_counter_state(struct fps_counter *fps_counter) { } static void -clipboard_paste(struct controller *controller) { +clipboard_paste(struct sc_controller *controller) { char *text = SDL_GetClipboardText(); if (!text) { LOGW("Could not get clipboard text: %s", SDL_GetError()); @@ -352,18 +352,18 @@ clipboard_paste(struct controller *controller) { struct control_msg msg; msg.type = CONTROL_MSG_TYPE_INJECT_TEXT; msg.inject_text.text = text_dup; - if (!controller_push_msg(controller, &msg)) { + if (!sc_controller_push_msg(controller, &msg)) { free(text_dup); LOGW("Could not request 'paste clipboard'"); } } static void -rotate_device(struct controller *controller) { +rotate_device(struct sc_controller *controller) { struct control_msg msg; msg.type = CONTROL_MSG_TYPE_ROTATE_DEVICE; - if (!controller_push_msg(controller, &msg)) { + if (!sc_controller_push_msg(controller, &msg)) { LOGW("Could not request device rotation"); } } @@ -415,7 +415,7 @@ simulate_virtual_finger(struct sc_input_manager *im, msg.inject_touch_event.pressure = up ? 0.0f : 1.0f; msg.inject_touch_event.buttons = 0; - if (!controller_push_msg(im->controller, &msg)) { + if (!sc_controller_push_msg(im->controller, &msg)) { LOGW("Could not request 'inject virtual finger event'"); return false; } @@ -436,7 +436,7 @@ sc_input_manager_process_key(struct sc_input_manager *im, // control: indicates the state of the command-line option --no-control bool control = im->control; - struct controller *controller = im->controller; + struct sc_controller *controller = im->controller; SDL_Keycode keycode = event->keysym.sym; uint16_t mod = event->keysym.mod; diff --git a/app/src/input_manager.h b/app/src/input_manager.h index 9eaea23c..5d552ee2 100644 --- a/app/src/input_manager.h +++ b/app/src/input_manager.h @@ -14,7 +14,7 @@ #include "trait/mouse_processor.h" struct sc_input_manager { - struct controller *controller; + struct sc_controller *controller; struct sc_screen *screen; struct sc_key_processor *kp; @@ -43,7 +43,7 @@ struct sc_input_manager { }; struct sc_input_manager_params { - struct controller *controller; + struct sc_controller *controller; struct sc_screen *screen; struct sc_key_processor *kp; struct sc_mouse_processor *mp; diff --git a/app/src/keyboard_inject.c b/app/src/keyboard_inject.c index b6dd69fb..9c141a37 100644 --- a/app/src/keyboard_inject.c +++ b/app/src/keyboard_inject.c @@ -284,7 +284,7 @@ sc_key_processor_process_key(struct sc_key_processor *kp, struct control_msg msg; if (convert_input_key(event, &msg, ki->key_inject_mode, ki->repeat)) { - if (!controller_push_msg(ki->controller, &msg)) { + if (!sc_controller_push_msg(ki->controller, &msg)) { LOGW("Could not request 'inject keycode'"); } } @@ -316,7 +316,7 @@ sc_key_processor_process_text(struct sc_key_processor *kp, LOGW("Could not strdup input text"); return; } - if (!controller_push_msg(ki->controller, &msg)) { + if (!sc_controller_push_msg(ki->controller, &msg)) { free(msg.inject_text.text); LOGW("Could not request 'inject text'"); } @@ -324,7 +324,7 @@ sc_key_processor_process_text(struct sc_key_processor *kp, void sc_keyboard_inject_init(struct sc_keyboard_inject *ki, - struct controller *controller, + struct sc_controller *controller, enum sc_key_inject_mode key_inject_mode, bool forward_key_repeat) { ki->controller = controller; diff --git a/app/src/keyboard_inject.h b/app/src/keyboard_inject.h index 9b25425a..b7781c1f 100644 --- a/app/src/keyboard_inject.h +++ b/app/src/keyboard_inject.h @@ -12,7 +12,7 @@ struct sc_keyboard_inject { struct sc_key_processor key_processor; // key processor trait - struct controller *controller; + struct sc_controller *controller; // SDL reports repeated events as a boolean, but Android expects the actual // number of repetitions. This variable keeps track of the count. @@ -24,7 +24,7 @@ struct sc_keyboard_inject { void sc_keyboard_inject_init(struct sc_keyboard_inject *ki, - struct controller *controller, + struct sc_controller *controller, enum sc_key_inject_mode key_inject_mode, bool forward_key_repeat); diff --git a/app/src/mouse_inject.c b/app/src/mouse_inject.c index 8a4a0531..106c215f 100644 --- a/app/src/mouse_inject.c +++ b/app/src/mouse_inject.c @@ -76,7 +76,7 @@ sc_mouse_processor_process_mouse_motion(struct sc_mouse_processor *mp, }, }; - if (!controller_push_msg(mi->controller, &msg)) { + if (!sc_controller_push_msg(mi->controller, &msg)) { LOGW("Could not request 'inject mouse motion event'"); } } @@ -97,7 +97,7 @@ sc_mouse_processor_process_mouse_click(struct sc_mouse_processor *mp, }, }; - if (!controller_push_msg(mi->controller, &msg)) { + if (!sc_controller_push_msg(mi->controller, &msg)) { LOGW("Could not request 'inject mouse click event'"); } } @@ -117,7 +117,7 @@ sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp, }, }; - if (!controller_push_msg(mi->controller, &msg)) { + if (!sc_controller_push_msg(mi->controller, &msg)) { LOGW("Could not request 'inject mouse scroll event'"); } } @@ -138,14 +138,14 @@ sc_mouse_processor_process_touch(struct sc_mouse_processor *mp, }, }; - if (!controller_push_msg(mi->controller, &msg)) { + if (!sc_controller_push_msg(mi->controller, &msg)) { LOGW("Could not request 'inject touch event'"); } } void sc_mouse_inject_init(struct sc_mouse_inject *mi, - struct controller *controller) { + struct sc_controller *controller) { mi->controller = controller; static const struct sc_mouse_processor_ops ops = { diff --git a/app/src/mouse_inject.h b/app/src/mouse_inject.h index 50591e2b..59a6a5d8 100644 --- a/app/src/mouse_inject.h +++ b/app/src/mouse_inject.h @@ -12,10 +12,11 @@ struct sc_mouse_inject { struct sc_mouse_processor mouse_processor; // mouse processor trait - struct controller *controller; + struct sc_controller *controller; }; void -sc_mouse_inject_init(struct sc_mouse_inject *mi, struct controller *controller); +sc_mouse_inject_init(struct sc_mouse_inject *mi, + struct sc_controller *controller); #endif diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 9237c59b..e1869eec 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -43,7 +43,7 @@ struct scrcpy { #ifdef HAVE_V4L2 struct sc_v4l2_sink v4l2_sink; #endif - struct controller controller; + struct sc_controller controller; struct file_handler file_handler; #ifdef HAVE_AOA_HID struct sc_aoa aoa; @@ -546,13 +546,13 @@ aoa_hid_end: mp = &s->mouse_inject.mouse_processor; } - if (!controller_init(&s->controller, s->server.control_socket, - acksync)) { + if (!sc_controller_init(&s->controller, s->server.control_socket, + acksync)) { goto end; } controller_initialized = true; - if (!controller_start(&s->controller)) { + if (!sc_controller_start(&s->controller)) { goto end; } controller_started = true; @@ -562,7 +562,7 @@ aoa_hid_end: msg.type = CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE; msg.set_screen_power_mode.mode = SCREEN_POWER_MODE_OFF; - if (!controller_push_msg(&s->controller, &msg)) { + if (!sc_controller_push_msg(&s->controller, &msg)) { LOGW("Could not request 'set screen power mode'"); } } @@ -646,7 +646,7 @@ end: } #endif if (controller_started) { - controller_stop(&s->controller); + sc_controller_stop(&s->controller); } if (file_handler_initialized) { file_handler_stop(&s->file_handler); @@ -687,10 +687,10 @@ end: } if (controller_started) { - controller_join(&s->controller); + sc_controller_join(&s->controller); } if (controller_initialized) { - controller_destroy(&s->controller); + sc_controller_destroy(&s->controller); } if (recorder_initialized) { diff --git a/app/src/screen.h b/app/src/screen.h index b1a8a58e..4810de31 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -60,7 +60,7 @@ struct sc_screen { }; struct sc_screen_params { - struct controller *controller; + struct sc_controller *controller; struct sc_key_processor *kp; struct sc_mouse_processor *mp; From afa4a1b728d6f130acff23c0e3fb4609bcb8a6b2 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 14 Jan 2022 22:17:30 +0100 Subject: [PATCH 0958/2244] Use sc_ prefix for control_msg --- app/src/control_msg.c | 6 +-- app/src/control_msg.h | 12 +++--- app/src/controller.c | 17 ++++---- app/src/controller.h | 4 +- app/src/input_manager.c | 22 +++++----- app/src/keyboard_inject.c | 6 +-- app/src/mouse_inject.c | 8 ++-- app/src/scrcpy.c | 2 +- app/tests/test_control_msg_serialize.c | 56 +++++++++++++------------- 9 files changed, 67 insertions(+), 66 deletions(-) diff --git a/app/src/control_msg.c b/app/src/control_msg.c index 75c74628..d093183a 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -89,7 +89,7 @@ to_fixed_point_16(float f) { } size_t -control_msg_serialize(const struct control_msg *msg, unsigned char *buf) { +sc_control_msg_serialize(const struct sc_control_msg *msg, unsigned char *buf) { buf[0] = msg->type; switch (msg->type) { case CONTROL_MSG_TYPE_INJECT_KEYCODE: @@ -151,7 +151,7 @@ control_msg_serialize(const struct control_msg *msg, unsigned char *buf) { } void -control_msg_log(const struct control_msg *msg) { +sc_control_msg_log(const struct sc_control_msg *msg) { #define LOG_CMSG(fmt, ...) LOGV("input: " fmt, ## __VA_ARGS__) switch (msg->type) { case CONTROL_MSG_TYPE_INJECT_KEYCODE: @@ -237,7 +237,7 @@ control_msg_log(const struct control_msg *msg) { } void -control_msg_destroy(struct control_msg *msg) { +sc_control_msg_destroy(struct sc_control_msg *msg) { switch (msg->type) { case CONTROL_MSG_TYPE_INJECT_TEXT: free(msg->inject_text.text); diff --git a/app/src/control_msg.h b/app/src/control_msg.h index 0eadd4f2..2ac96064 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -20,7 +20,7 @@ #define POINTER_ID_MOUSE UINT64_C(-1) #define POINTER_ID_VIRTUAL_FINGER UINT64_C(-2) -enum control_msg_type { +enum sc_control_msg_type { CONTROL_MSG_TYPE_INJECT_KEYCODE, CONTROL_MSG_TYPE_INJECT_TEXT, CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, @@ -47,8 +47,8 @@ enum get_clipboard_copy_key { GET_CLIPBOARD_COPY_KEY_CUT, }; -struct control_msg { - enum control_msg_type type; +struct sc_control_msg { + enum sc_control_msg_type type; union { struct { enum android_keyevent_action action; @@ -93,12 +93,12 @@ struct control_msg { // 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); +sc_control_msg_serialize(const struct sc_control_msg *msg, unsigned char *buf); void -control_msg_log(const struct control_msg *msg); +sc_control_msg_log(const struct sc_control_msg *msg); void -control_msg_destroy(struct control_msg *msg); +sc_control_msg_destroy(struct sc_control_msg *msg); #endif diff --git a/app/src/controller.c b/app/src/controller.c index 531b5fb9..9135d967 100644 --- a/app/src/controller.c +++ b/app/src/controller.c @@ -38,9 +38,9 @@ sc_controller_destroy(struct sc_controller *controller) { sc_cond_destroy(&controller->msg_cond); sc_mutex_destroy(&controller->mutex); - struct control_msg msg; + struct sc_control_msg msg; while (cbuf_take(&controller->queue, &msg)) { - control_msg_destroy(&msg); + sc_control_msg_destroy(&msg); } receiver_destroy(&controller->receiver); @@ -48,9 +48,9 @@ sc_controller_destroy(struct sc_controller *controller) { bool sc_controller_push_msg(struct sc_controller *controller, - const struct control_msg *msg) { + const struct sc_control_msg *msg) { if (sc_get_log_level() <= SC_LOG_LEVEL_VERBOSE) { - control_msg_log(msg); + sc_control_msg_log(msg); } sc_mutex_lock(&controller->mutex); @@ -64,9 +64,10 @@ sc_controller_push_msg(struct sc_controller *controller, } static bool -process_msg(struct sc_controller *controller, const struct control_msg *msg) { +process_msg(struct sc_controller *controller, + const struct sc_control_msg *msg) { static unsigned char serialized_msg[CONTROL_MSG_MAX_SIZE]; - size_t length = control_msg_serialize(msg, serialized_msg); + size_t length = sc_control_msg_serialize(msg, serialized_msg); if (!length) { return false; } @@ -89,14 +90,14 @@ run_controller(void *data) { sc_mutex_unlock(&controller->mutex); break; } - struct control_msg msg; + struct sc_control_msg msg; bool non_empty = cbuf_take(&controller->queue, &msg); assert(non_empty); (void) non_empty; sc_mutex_unlock(&controller->mutex); bool ok = process_msg(controller, &msg); - control_msg_destroy(&msg); + sc_control_msg_destroy(&msg); if (!ok) { LOGD("Could not write msg to socket"); break; diff --git a/app/src/controller.h b/app/src/controller.h index 06ceb782..f8bc7c02 100644 --- a/app/src/controller.h +++ b/app/src/controller.h @@ -12,7 +12,7 @@ #include "util/net.h" #include "util/thread.h" -struct sc_control_msg_queue CBUF(struct control_msg, 64); +struct sc_control_msg_queue CBUF(struct sc_control_msg, 64); struct sc_controller { sc_socket control_socket; @@ -42,6 +42,6 @@ sc_controller_join(struct sc_controller *controller); bool sc_controller_push_msg(struct sc_controller *controller, - const struct control_msg *msg); + const struct sc_control_msg *msg); #endif diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 5b355701..9eee6e6c 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -160,7 +160,7 @@ static void send_keycode(struct sc_controller *controller, enum android_keycode keycode, enum sc_action action, const char *name) { // send DOWN event - struct control_msg msg; + struct sc_control_msg msg; msg.type = CONTROL_MSG_TYPE_INJECT_KEYCODE; msg.inject_keycode.action = action == SC_ACTION_DOWN ? AKEY_EVENT_ACTION_DOWN @@ -214,7 +214,7 @@ action_menu(struct sc_controller *controller, enum sc_action action) { static void press_back_or_turn_screen_on(struct sc_controller *controller, enum sc_action action) { - struct control_msg msg; + struct sc_control_msg msg; msg.type = CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON; msg.back_or_screen_on.action = action == SC_ACTION_DOWN ? AKEY_EVENT_ACTION_DOWN @@ -227,7 +227,7 @@ press_back_or_turn_screen_on(struct sc_controller *controller, static void expand_notification_panel(struct sc_controller *controller) { - struct control_msg msg; + struct sc_control_msg msg; msg.type = CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL; if (!sc_controller_push_msg(controller, &msg)) { @@ -237,7 +237,7 @@ expand_notification_panel(struct sc_controller *controller) { static void expand_settings_panel(struct sc_controller *controller) { - struct control_msg msg; + struct sc_control_msg msg; msg.type = CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL; if (!sc_controller_push_msg(controller, &msg)) { @@ -247,7 +247,7 @@ expand_settings_panel(struct sc_controller *controller) { static void collapse_panels(struct sc_controller *controller) { - struct control_msg msg; + struct sc_control_msg msg; msg.type = CONTROL_MSG_TYPE_COLLAPSE_PANELS; if (!sc_controller_push_msg(controller, &msg)) { @@ -258,7 +258,7 @@ collapse_panels(struct sc_controller *controller) { static bool get_device_clipboard(struct sc_controller *controller, enum get_clipboard_copy_key copy_key) { - struct control_msg msg; + struct sc_control_msg msg; msg.type = CONTROL_MSG_TYPE_GET_CLIPBOARD; msg.get_clipboard.copy_key = copy_key; @@ -286,7 +286,7 @@ set_device_clipboard(struct sc_controller *controller, bool paste, return false; } - struct control_msg msg; + struct sc_control_msg msg; msg.type = CONTROL_MSG_TYPE_SET_CLIPBOARD; msg.set_clipboard.sequence = sequence; msg.set_clipboard.text = text_dup; @@ -304,7 +304,7 @@ set_device_clipboard(struct sc_controller *controller, bool paste, static void set_screen_power_mode(struct sc_controller *controller, enum screen_power_mode mode) { - struct control_msg msg; + struct sc_control_msg msg; msg.type = CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE; msg.set_screen_power_mode.mode = mode; @@ -349,7 +349,7 @@ clipboard_paste(struct sc_controller *controller) { return; } - struct control_msg msg; + struct sc_control_msg msg; msg.type = CONTROL_MSG_TYPE_INJECT_TEXT; msg.inject_text.text = text_dup; if (!sc_controller_push_msg(controller, &msg)) { @@ -360,7 +360,7 @@ clipboard_paste(struct sc_controller *controller) { static void rotate_device(struct sc_controller *controller) { - struct control_msg msg; + struct sc_control_msg msg; msg.type = CONTROL_MSG_TYPE_ROTATE_DEVICE; if (!sc_controller_push_msg(controller, &msg)) { @@ -406,7 +406,7 @@ simulate_virtual_finger(struct sc_input_manager *im, struct sc_point point) { bool up = action == AMOTION_EVENT_ACTION_UP; - struct control_msg msg; + struct sc_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; diff --git a/app/src/keyboard_inject.c b/app/src/keyboard_inject.c index 9c141a37..7276d325 100644 --- a/app/src/keyboard_inject.c +++ b/app/src/keyboard_inject.c @@ -246,7 +246,7 @@ convert_meta_state(uint16_t mod) { } static bool -convert_input_key(const struct sc_key_event *event, struct control_msg *msg, +convert_input_key(const struct sc_key_event *event, struct sc_control_msg *msg, enum sc_key_inject_mode key_inject_mode, uint32_t repeat) { msg->type = CONTROL_MSG_TYPE_INJECT_KEYCODE; @@ -282,7 +282,7 @@ sc_key_processor_process_key(struct sc_key_processor *kp, ki->repeat = 0; } - struct control_msg msg; + struct sc_control_msg msg; if (convert_input_key(event, &msg, ki->key_inject_mode, ki->repeat)) { if (!sc_controller_push_msg(ki->controller, &msg)) { LOGW("Could not request 'inject keycode'"); @@ -309,7 +309,7 @@ sc_key_processor_process_text(struct sc_key_processor *kp, } } - struct control_msg msg; + struct sc_control_msg msg; msg.type = CONTROL_MSG_TYPE_INJECT_TEXT; msg.inject_text.text = strdup(event->text); if (!msg.inject_text.text) { diff --git a/app/src/mouse_inject.c b/app/src/mouse_inject.c index 106c215f..855aaa9f 100644 --- a/app/src/mouse_inject.c +++ b/app/src/mouse_inject.c @@ -65,7 +65,7 @@ sc_mouse_processor_process_mouse_motion(struct sc_mouse_processor *mp, struct sc_mouse_inject *mi = DOWNCAST(mp); - struct control_msg msg = { + struct sc_control_msg msg = { .type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, .inject_touch_event = { .action = AMOTION_EVENT_ACTION_MOVE, @@ -86,7 +86,7 @@ sc_mouse_processor_process_mouse_click(struct sc_mouse_processor *mp, const struct sc_mouse_click_event *event) { struct sc_mouse_inject *mi = DOWNCAST(mp); - struct control_msg msg = { + struct sc_control_msg msg = { .type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, .inject_touch_event = { .action = convert_mouse_action(event->action), @@ -107,7 +107,7 @@ sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp, const struct sc_mouse_scroll_event *event) { struct sc_mouse_inject *mi = DOWNCAST(mp); - struct control_msg msg = { + struct sc_control_msg msg = { .type = CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT, .inject_scroll_event = { .position = event->position, @@ -127,7 +127,7 @@ sc_mouse_processor_process_touch(struct sc_mouse_processor *mp, const struct sc_touch_event *event) { struct sc_mouse_inject *mi = DOWNCAST(mp); - struct control_msg msg = { + struct sc_control_msg msg = { .type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, .inject_touch_event = { .action = convert_touch_action(event->action), diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index e1869eec..86fdf984 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -558,7 +558,7 @@ aoa_hid_end: controller_started = true; if (options->turn_screen_off) { - struct control_msg msg; + struct sc_control_msg msg; msg.type = CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE; msg.set_screen_power_mode.mode = SCREEN_POWER_MODE_OFF; diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c index d1f0f161..95a54a98 100644 --- a/app/tests/test_control_msg_serialize.c +++ b/app/tests/test_control_msg_serialize.c @@ -6,7 +6,7 @@ #include "control_msg.h" static void test_serialize_inject_keycode(void) { - struct control_msg msg = { + struct sc_control_msg msg = { .type = CONTROL_MSG_TYPE_INJECT_KEYCODE, .inject_keycode = { .action = AKEY_EVENT_ACTION_UP, @@ -17,7 +17,7 @@ static void test_serialize_inject_keycode(void) { }; unsigned char buf[CONTROL_MSG_MAX_SIZE]; - size_t size = control_msg_serialize(&msg, buf); + size_t size = sc_control_msg_serialize(&msg, buf); assert(size == 14); const unsigned char expected[] = { @@ -31,7 +31,7 @@ static void test_serialize_inject_keycode(void) { } static void test_serialize_inject_text(void) { - struct control_msg msg = { + struct sc_control_msg msg = { .type = CONTROL_MSG_TYPE_INJECT_TEXT, .inject_text = { .text = "hello, world!", @@ -39,7 +39,7 @@ static void test_serialize_inject_text(void) { }; unsigned char buf[CONTROL_MSG_MAX_SIZE]; - size_t size = control_msg_serialize(&msg, buf); + size_t size = sc_control_msg_serialize(&msg, buf); assert(size == 18); const unsigned char expected[] = { @@ -51,7 +51,7 @@ static void test_serialize_inject_text(void) { } static void test_serialize_inject_text_long(void) { - struct control_msg msg; + struct sc_control_msg msg; msg.type = CONTROL_MSG_TYPE_INJECT_TEXT; char text[CONTROL_MSG_INJECT_TEXT_MAX_LENGTH + 1]; memset(text, 'a', CONTROL_MSG_INJECT_TEXT_MAX_LENGTH); @@ -59,7 +59,7 @@ static void test_serialize_inject_text_long(void) { msg.inject_text.text = text; unsigned char buf[CONTROL_MSG_MAX_SIZE]; - size_t size = control_msg_serialize(&msg, buf); + size_t size = sc_control_msg_serialize(&msg, buf); assert(size == 5 + CONTROL_MSG_INJECT_TEXT_MAX_LENGTH); unsigned char expected[5 + CONTROL_MSG_INJECT_TEXT_MAX_LENGTH]; @@ -74,7 +74,7 @@ static void test_serialize_inject_text_long(void) { } static void test_serialize_inject_touch_event(void) { - struct control_msg msg = { + struct sc_control_msg msg = { .type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, .inject_touch_event = { .action = AMOTION_EVENT_ACTION_DOWN, @@ -95,7 +95,7 @@ static void test_serialize_inject_touch_event(void) { }; unsigned char buf[CONTROL_MSG_MAX_SIZE]; - size_t size = control_msg_serialize(&msg, buf); + size_t size = sc_control_msg_serialize(&msg, buf); assert(size == 28); const unsigned char expected[] = { @@ -111,7 +111,7 @@ static void test_serialize_inject_touch_event(void) { } static void test_serialize_inject_scroll_event(void) { - struct control_msg msg = { + struct sc_control_msg msg = { .type = CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT, .inject_scroll_event = { .position = { @@ -131,7 +131,7 @@ static void test_serialize_inject_scroll_event(void) { }; unsigned char buf[CONTROL_MSG_MAX_SIZE]; - size_t size = control_msg_serialize(&msg, buf); + size_t size = sc_control_msg_serialize(&msg, buf); assert(size == 25); const unsigned char expected[] = { @@ -146,7 +146,7 @@ static void test_serialize_inject_scroll_event(void) { } static void test_serialize_back_or_screen_on(void) { - struct control_msg msg = { + struct sc_control_msg msg = { .type = CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON, .back_or_screen_on = { .action = AKEY_EVENT_ACTION_UP, @@ -154,7 +154,7 @@ static void test_serialize_back_or_screen_on(void) { }; unsigned char buf[CONTROL_MSG_MAX_SIZE]; - size_t size = control_msg_serialize(&msg, buf); + size_t size = sc_control_msg_serialize(&msg, buf); assert(size == 2); const unsigned char expected[] = { @@ -165,12 +165,12 @@ static void test_serialize_back_or_screen_on(void) { } static void test_serialize_expand_notification_panel(void) { - struct control_msg msg = { + struct sc_control_msg msg = { .type = CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL, }; unsigned char buf[CONTROL_MSG_MAX_SIZE]; - size_t size = control_msg_serialize(&msg, buf); + size_t size = sc_control_msg_serialize(&msg, buf); assert(size == 1); const unsigned char expected[] = { @@ -180,12 +180,12 @@ static void test_serialize_expand_notification_panel(void) { } static void test_serialize_expand_settings_panel(void) { - struct control_msg msg = { + struct sc_control_msg msg = { .type = CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL, }; unsigned char buf[CONTROL_MSG_MAX_SIZE]; - size_t size = control_msg_serialize(&msg, buf); + size_t size = sc_control_msg_serialize(&msg, buf); assert(size == 1); const unsigned char expected[] = { @@ -195,12 +195,12 @@ static void test_serialize_expand_settings_panel(void) { } static void test_serialize_collapse_panels(void) { - struct control_msg msg = { + struct sc_control_msg msg = { .type = CONTROL_MSG_TYPE_COLLAPSE_PANELS, }; unsigned char buf[CONTROL_MSG_MAX_SIZE]; - size_t size = control_msg_serialize(&msg, buf); + size_t size = sc_control_msg_serialize(&msg, buf); assert(size == 1); const unsigned char expected[] = { @@ -210,7 +210,7 @@ static void test_serialize_collapse_panels(void) { } static void test_serialize_get_clipboard(void) { - struct control_msg msg = { + struct sc_control_msg msg = { .type = CONTROL_MSG_TYPE_GET_CLIPBOARD, .get_clipboard = { .copy_key = GET_CLIPBOARD_COPY_KEY_COPY, @@ -218,7 +218,7 @@ static void test_serialize_get_clipboard(void) { }; unsigned char buf[CONTROL_MSG_MAX_SIZE]; - size_t size = control_msg_serialize(&msg, buf); + size_t size = sc_control_msg_serialize(&msg, buf); assert(size == 2); const unsigned char expected[] = { @@ -229,7 +229,7 @@ static void test_serialize_get_clipboard(void) { } static void test_serialize_set_clipboard(void) { - struct control_msg msg = { + struct sc_control_msg msg = { .type = CONTROL_MSG_TYPE_SET_CLIPBOARD, .set_clipboard = { .sequence = UINT64_C(0x0102030405060708), @@ -239,7 +239,7 @@ static void test_serialize_set_clipboard(void) { }; unsigned char buf[CONTROL_MSG_MAX_SIZE]; - size_t size = control_msg_serialize(&msg, buf); + size_t size = sc_control_msg_serialize(&msg, buf); assert(size == 27); const unsigned char expected[] = { @@ -253,7 +253,7 @@ static void test_serialize_set_clipboard(void) { } static void test_serialize_set_clipboard_long(void) { - struct control_msg msg = { + struct sc_control_msg msg = { .type = CONTROL_MSG_TYPE_SET_CLIPBOARD, .set_clipboard = { .sequence = UINT64_C(0x0102030405060708), @@ -268,7 +268,7 @@ static void test_serialize_set_clipboard_long(void) { msg.set_clipboard.text = text; unsigned char buf[CONTROL_MSG_MAX_SIZE]; - size_t size = control_msg_serialize(&msg, buf); + size_t size = sc_control_msg_serialize(&msg, buf); assert(size == CONTROL_MSG_MAX_SIZE); unsigned char expected[CONTROL_MSG_MAX_SIZE] = { @@ -287,7 +287,7 @@ static void test_serialize_set_clipboard_long(void) { } static void test_serialize_set_screen_power_mode(void) { - struct control_msg msg = { + struct sc_control_msg msg = { .type = CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE, .set_screen_power_mode = { .mode = SCREEN_POWER_MODE_NORMAL, @@ -295,7 +295,7 @@ static void test_serialize_set_screen_power_mode(void) { }; unsigned char buf[CONTROL_MSG_MAX_SIZE]; - size_t size = control_msg_serialize(&msg, buf); + size_t size = sc_control_msg_serialize(&msg, buf); assert(size == 2); const unsigned char expected[] = { @@ -306,12 +306,12 @@ static void test_serialize_set_screen_power_mode(void) { } static void test_serialize_rotate_device(void) { - struct control_msg msg = { + struct sc_control_msg msg = { .type = CONTROL_MSG_TYPE_ROTATE_DEVICE, }; unsigned char buf[CONTROL_MSG_MAX_SIZE]; - size_t size = control_msg_serialize(&msg, buf); + size_t size = sc_control_msg_serialize(&msg, buf); assert(size == 1); const unsigned char expected[] = { From 1c71bd16bec1db0788e72a7c6b02f80ed40f1601 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 15 Jan 2022 22:57:38 +0100 Subject: [PATCH 0959/2244] Use constant string for known booleans Boolean options explicitly passed to the server are statically known. --- app/src/server.c | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/app/src/server.c b/app/src/server.c index ab5439ad..ea7e5377 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -188,7 +188,6 @@ execute_server(struct sc_server *server, } \ cmd[count++] = p; \ } -#define STRBOOL(v) (v ? "true" : "false") ADD_PARAM("log_level=%s", log_level_to_server_string(params->log_level)); ADD_PARAM("bit_rate=%" PRIu32, params->bit_rate); @@ -204,23 +203,23 @@ execute_server(struct sc_server *server, params->lock_video_orientation); } if (server->tunnel.forward) { - ADD_PARAM("tunnel_forward=%s", STRBOOL(server->tunnel.forward)); + ADD_PARAM("tunnel_forward=true"); } if (params->crop) { ADD_PARAM("crop=%s", params->crop); } if (!params->control) { // By default, control is true - ADD_PARAM("control=%s", STRBOOL(params->control)); + ADD_PARAM("control=false"); } if (params->display_id) { ADD_PARAM("display_id=%" PRIu32, params->display_id); } if (params->show_touches) { - ADD_PARAM("show_touches=%s", STRBOOL(params->show_touches)); + ADD_PARAM("show_touches=true"); } if (params->stay_awake) { - ADD_PARAM("stay_awake=%s", STRBOOL(params->stay_awake)); + ADD_PARAM("stay_awake=true"); } if (params->codec_options) { ADD_PARAM("codec_options=%s", params->codec_options); @@ -229,11 +228,11 @@ execute_server(struct sc_server *server, ADD_PARAM("encoder_name=%s", params->encoder_name); } if (params->power_off_on_close) { - ADD_PARAM("power_off_on_close=%s", STRBOOL(params->power_off_on_close)); + ADD_PARAM("power_off_on_close=true"); } if (!params->clipboard_autosync) { // By default, clipboard_autosync is true - ADD_PARAM("clipboard_autosync=%s", STRBOOL(params->clipboard_autosync)); + ADD_PARAM("clipboard_autosync=false"); } #undef ADD_PARAM From 60bf133ac28c76bcbef79e6610f34c6b66e5e82e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 15 Jan 2022 23:04:37 +0100 Subject: [PATCH 0960/2244] Add final modifier to ScreenEncoder fields These fields are only set from the constructor. --- .../main/java/com/genymobile/scrcpy/ScreenEncoder.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index f98c53d0..ce6b556a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -30,11 +30,11 @@ public class ScreenEncoder implements Device.RotationListener { private final AtomicBoolean rotationChanged = new AtomicBoolean(); private final ByteBuffer headerBuffer = ByteBuffer.allocate(12); - private String encoderName; - private List codecOptions; - private int bitRate; - private int maxFps; - private boolean sendFrameMeta; + private final String encoderName; + private final List codecOptions; + private final int bitRate; + private final int maxFps; + private final boolean sendFrameMeta; private long ptsOrigin; public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, List codecOptions, String encoderName) { From 5e8fa56e7a8b13b7c9c6d0d4ee56636c76b821d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernhard=20Rosenkr=C3=A4nzer?= Date: Sun, 16 Jan 2022 01:45:36 +0100 Subject: [PATCH 0961/2244] Fix build with ffmpeg 5.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #2948 Signed-off-by: Bernhard Rosenkränzer Signed-off-by: Romain Vimont --- app/src/decoder.h | 1 + app/src/icon.c | 3 ++- app/src/stream.c | 3 +-- app/src/stream.h | 1 + app/src/v4l2_sink.h | 5 +++-- 5 files changed, 8 insertions(+), 5 deletions(-) diff --git a/app/src/decoder.h b/app/src/decoder.h index 257f751a..e2972cb1 100644 --- a/app/src/decoder.h +++ b/app/src/decoder.h @@ -6,6 +6,7 @@ #include "trait/packet_sink.h" #include +#include #include #define DECODER_MAX_SINKS 2 diff --git a/app/src/icon.c b/app/src/icon.c index 1d670242..e709678f 100644 --- a/app/src/icon.c +++ b/app/src/icon.c @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -85,7 +86,7 @@ decode_image(const char *path) { AVCodecParameters *params = ctx->streams[stream]->codecpar; - AVCodec *codec = avcodec_find_decoder(params->codec_id); + const AVCodec *codec = avcodec_find_decoder(params->codec_id); if (!codec) { LOGE("Could not find image decoder"); goto close_input; diff --git a/app/src/stream.c b/app/src/stream.c index f8d73a27..c873c4ad 100644 --- a/app/src/stream.c +++ b/app/src/stream.c @@ -1,7 +1,6 @@ #include "stream.h" #include -#include #include #include @@ -192,7 +191,7 @@ static int run_stream(void *data) { struct stream *stream = data; - AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264); + const AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264); if (!codec) { LOGE("H.264 decoder not found"); goto end; diff --git a/app/src/stream.h b/app/src/stream.h index 362bc4a7..bdcefe39 100644 --- a/app/src/stream.h +++ b/app/src/stream.h @@ -5,6 +5,7 @@ #include #include +#include #include #include "trait/packet_sink.h" diff --git a/app/src/v4l2_sink.h b/app/src/v4l2_sink.h index 8737a607..339a61f2 100644 --- a/app/src/v4l2_sink.h +++ b/app/src/v4l2_sink.h @@ -3,13 +3,14 @@ #include "common.h" +#include +#include + #include "coords.h" #include "trait/frame_sink.h" #include "video_buffer.h" #include "util/tick.h" -#include - struct sc_v4l2_sink { struct sc_frame_sink frame_sink; // frame sink trait From 479abc8c7723d2d05b7c5027528acd91946a328c Mon Sep 17 00:00:00 2001 From: Tobias Preuss Date: Fri, 14 Jan 2022 11:07:15 +0100 Subject: [PATCH 0962/2244] Reference Windows USB driver for Google devices PR #2945 Signed-off-by: Romain Vimont --- FAQ.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/FAQ.md b/FAQ.md index d5f0e3ee..399f3504 100644 --- a/FAQ.md +++ b/FAQ.md @@ -43,10 +43,11 @@ Check [stackoverflow][device-unauthorized]. Check that you correctly enabled [adb debugging][enable-adb]. -If your device is not detected, you may need some [drivers] (on Windows). +If your device is not detected, you may need some [drivers] (on Windows). There is a separate [USB driver for Google devices][google-usb-driver]. [enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling [drivers]: https://developer.android.com/studio/run/oem-usb.html +[google-usb-driver]: https://developer.android.com/studio/run/win-usb ### Several devices connected From 37c7827d463ba99f87aa87553b661946113d0412 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 17 Jan 2022 19:00:39 +0100 Subject: [PATCH 0963/2244] Simplify ffmpeg dependencies Makefile The fact that the current prebuilt FFmpeg is split into two separate zipfiles is an implementation detail. Use a single Makefile recipe for both files. PR #2952 --- prebuilt-deps/Makefile | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/prebuilt-deps/Makefile b/prebuilt-deps/Makefile index fa986978..fe6d8217 100644 --- a/prebuilt-deps/Makefile +++ b/prebuilt-deps/Makefile @@ -1,30 +1,24 @@ .PHONY: prepare-win32 prepare-win64 \ - prepare-ffmpeg-shared-win32 \ - prepare-ffmpeg-dev-win32 \ - prepare-ffmpeg-shared-win64 \ - prepare-ffmpeg-dev-win64 \ + prepare-ffmpeg-win32 \ + prepare-ffmpeg-win64 \ prepare-sdl2 \ prepare-adb -prepare-win32: prepare-sdl2 prepare-ffmpeg-shared-win32 prepare-ffmpeg-dev-win32 prepare-adb -prepare-win64: prepare-sdl2 prepare-ffmpeg-shared-win64 prepare-ffmpeg-dev-win64 prepare-adb +prepare-win32: prepare-sdl2 prepare-ffmpeg-win32 prepare-adb +prepare-win64: prepare-sdl2 prepare-ffmpeg-win64 prepare-adb -prepare-ffmpeg-shared-win32: +prepare-ffmpeg-win32: @./prepare-dep https://github.com/Genymobile/scrcpy/releases/download/v1.16/ffmpeg-4.3.1-win32-shared.zip \ 357af9901a456f4dcbacd107e83a934d344c9cb07ddad8aaf80612eeab7d26d2 \ ffmpeg-4.3.1-win32-shared - -prepare-ffmpeg-dev-win32: @./prepare-dep https://github.com/Genymobile/scrcpy/releases/download/v1.16/ffmpeg-4.3.1-win32-dev.zip \ 230efb08e9bcf225bd474da29676c70e591fc94d8790a740ca801408fddcb78b \ ffmpeg-4.3.1-win32-dev -prepare-ffmpeg-shared-win64: +prepare-ffmpeg-win64: @./prepare-dep https://github.com/Genymobile/scrcpy/releases/download/v1.16/ffmpeg-4.3.1-win64-shared.zip \ dd29b7f92f48dead4dd940492c7509138c0f99db445076d0a597007298a79940 \ ffmpeg-4.3.1-win64-shared - -prepare-ffmpeg-dev-win64: @./prepare-dep https://github.com/Genymobile/scrcpy/releases/download/v1.16/ffmpeg-4.3.1-win64-dev.zip \ 2e8038242cf8e1bd095c2978f196ff0462b122cc6ef7e74626a6af15459d8b81 \ ffmpeg-4.3.1-win64-dev From a2495c5ef192e86f4dc5204e4e250f3b6cc56ef4 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 17 Jan 2022 19:04:18 +0100 Subject: [PATCH 0964/2244] Use symlink to simplify Windows ffmpeg dependency The FFmpeg dependency is downloaded from two separate zipfiles. Symlink include/ to expose everything from a single directory, to simplify the meson script. PR #2952 --- app/meson.build | 7 +++---- cross_win32.txt | 3 +-- cross_win64.txt | 3 +-- prebuilt-deps/Makefile | 2 ++ 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/app/meson.build b/app/meson.build index cee261bb..39334a3d 100644 --- a/app/meson.build +++ b/app/meson.build @@ -118,10 +118,9 @@ else include_directories: include_directories(sdl2_include_dir) ) - prebuilt_ffmpeg_shared = meson.get_cross_property('prebuilt_ffmpeg_shared') - prebuilt_ffmpeg_dev = meson.get_cross_property('prebuilt_ffmpeg_dev') - ffmpeg_bin_dir = meson.current_source_dir() + '/../prebuilt-deps/' + prebuilt_ffmpeg_shared + '/bin' - ffmpeg_include_dir = '../prebuilt-deps/' + prebuilt_ffmpeg_dev + '/include' + prebuilt_ffmpeg = meson.get_cross_property('prebuilt_ffmpeg') + ffmpeg_bin_dir = meson.current_source_dir() + '/../prebuilt-deps/' + prebuilt_ffmpeg + '/bin' + ffmpeg_include_dir = '../prebuilt-deps/' + prebuilt_ffmpeg + '/include' ffmpeg = declare_dependency( dependencies: [ cc.find_library('avcodec-58', dirs: ffmpeg_bin_dir), diff --git a/cross_win32.txt b/cross_win32.txt index b0e43622..045cb5c6 100644 --- a/cross_win32.txt +++ b/cross_win32.txt @@ -16,6 +16,5 @@ cpu = 'i686' endian = 'little' [properties] -prebuilt_ffmpeg_shared = 'ffmpeg-4.3.1-win32-shared' -prebuilt_ffmpeg_dev = 'ffmpeg-4.3.1-win32-dev' +prebuilt_ffmpeg = 'ffmpeg-4.3.1-win32-shared' prebuilt_sdl2 = 'SDL2-2.0.18/i686-w64-mingw32' diff --git a/cross_win64.txt b/cross_win64.txt index 6625c0cf..d48c137a 100644 --- a/cross_win64.txt +++ b/cross_win64.txt @@ -16,6 +16,5 @@ cpu = 'x86_64' endian = 'little' [properties] -prebuilt_ffmpeg_shared = 'ffmpeg-4.3.1-win64-shared' -prebuilt_ffmpeg_dev = 'ffmpeg-4.3.1-win64-dev' +prebuilt_ffmpeg = 'ffmpeg-4.3.1-win64-shared' prebuilt_sdl2 = 'SDL2-2.0.18/x86_64-w64-mingw32' diff --git a/prebuilt-deps/Makefile b/prebuilt-deps/Makefile index fe6d8217..41b7cf2a 100644 --- a/prebuilt-deps/Makefile +++ b/prebuilt-deps/Makefile @@ -14,6 +14,7 @@ prepare-ffmpeg-win32: @./prepare-dep https://github.com/Genymobile/scrcpy/releases/download/v1.16/ffmpeg-4.3.1-win32-dev.zip \ 230efb08e9bcf225bd474da29676c70e591fc94d8790a740ca801408fddcb78b \ ffmpeg-4.3.1-win32-dev + ln -sf ../ffmpeg-4.3.1-win32-dev/include ffmpeg-4.3.1-win32-shared/ prepare-ffmpeg-win64: @./prepare-dep https://github.com/Genymobile/scrcpy/releases/download/v1.16/ffmpeg-4.3.1-win64-shared.zip \ @@ -22,6 +23,7 @@ prepare-ffmpeg-win64: @./prepare-dep https://github.com/Genymobile/scrcpy/releases/download/v1.16/ffmpeg-4.3.1-win64-dev.zip \ 2e8038242cf8e1bd095c2978f196ff0462b122cc6ef7e74626a6af15459d8b81 \ ffmpeg-4.3.1-win64-dev + ln -sf ../ffmpeg-4.3.1-win64-dev/include ffmpeg-4.3.1-win64-shared/ prepare-sdl2: @./prepare-dep https://libsdl.org/release/SDL2-devel-2.0.18-mingw.tar.gz \ From b3ff1f6b3b6629a0ed55e759178b3e3378572cc4 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 17 Jan 2022 19:11:23 +0100 Subject: [PATCH 0965/2244] Upgrade FFmpeg (5.0) for Windows 64-bit Use FFmpeg win64 binaries from gyan.dev (referenced from ffmpeg.org): - https://www.gyan.dev/ffmpeg/builds/ - https://ffmpeg.org/download.html#build-windows Keep the old FFmpeg prebuilt binaries (4.3.1) for win32 builds. Fixes #1753 Refs #1838 Refs #2583 PR #2952 Co-authored-by: Yu-Chen Lin Co-authored-by: nkh0472 Signed-off-by: Romain Vimont --- app/meson.build | 12 +++++++++--- cross_win32.txt | 3 +++ cross_win64.txt | 5 ++++- prebuilt-deps/Makefile | 11 ++++------- prebuilt-deps/prepare-dep | 3 +++ release.mk | 10 +++++----- 6 files changed, 28 insertions(+), 16 deletions(-) diff --git a/app/meson.build b/app/meson.build index 39334a3d..319dd0b6 100644 --- a/app/meson.build +++ b/app/meson.build @@ -121,11 +121,17 @@ else prebuilt_ffmpeg = meson.get_cross_property('prebuilt_ffmpeg') ffmpeg_bin_dir = meson.current_source_dir() + '/../prebuilt-deps/' + prebuilt_ffmpeg + '/bin' ffmpeg_include_dir = '../prebuilt-deps/' + prebuilt_ffmpeg + '/include' + + # ffmpeg versions are different for win32 and win64 builds + ffmpeg_avcodec = meson.get_cross_property('ffmpeg_avcodec') + ffmpeg_avformat = meson.get_cross_property('ffmpeg_avformat') + ffmpeg_avutil = meson.get_cross_property('ffmpeg_avutil') + ffmpeg = declare_dependency( dependencies: [ - cc.find_library('avcodec-58', dirs: ffmpeg_bin_dir), - cc.find_library('avformat-58', dirs: ffmpeg_bin_dir), - cc.find_library('avutil-56', dirs: ffmpeg_bin_dir), + cc.find_library(ffmpeg_avcodec, dirs: ffmpeg_bin_dir), + cc.find_library(ffmpeg_avformat, dirs: ffmpeg_bin_dir), + cc.find_library(ffmpeg_avutil, dirs: ffmpeg_bin_dir), ], include_directories: include_directories(ffmpeg_include_dir) ) diff --git a/cross_win32.txt b/cross_win32.txt index 045cb5c6..826c044f 100644 --- a/cross_win32.txt +++ b/cross_win32.txt @@ -16,5 +16,8 @@ cpu = 'i686' endian = 'little' [properties] +ffmpeg_avcodec = 'avcodec-58' +ffmpeg_avformat = 'avformat-58' +ffmpeg_avutil = 'avutil-56' prebuilt_ffmpeg = 'ffmpeg-4.3.1-win32-shared' prebuilt_sdl2 = 'SDL2-2.0.18/i686-w64-mingw32' diff --git a/cross_win64.txt b/cross_win64.txt index d48c137a..2e049fdb 100644 --- a/cross_win64.txt +++ b/cross_win64.txt @@ -16,5 +16,8 @@ cpu = 'x86_64' endian = 'little' [properties] -prebuilt_ffmpeg = 'ffmpeg-4.3.1-win64-shared' +ffmpeg_avcodec = 'avcodec-59' +ffmpeg_avformat = 'avformat-59' +ffmpeg_avutil = 'avutil-57' +prebuilt_ffmpeg = 'ffmpeg-5.0-full_build-shared' prebuilt_sdl2 = 'SDL2-2.0.18/x86_64-w64-mingw32' diff --git a/prebuilt-deps/Makefile b/prebuilt-deps/Makefile index 41b7cf2a..93e8c48d 100644 --- a/prebuilt-deps/Makefile +++ b/prebuilt-deps/Makefile @@ -7,6 +7,7 @@ prepare-win32: prepare-sdl2 prepare-ffmpeg-win32 prepare-adb prepare-win64: prepare-sdl2 prepare-ffmpeg-win64 prepare-adb +# Use old FFmpeg version for win32, there are no new prebuilts prepare-ffmpeg-win32: @./prepare-dep https://github.com/Genymobile/scrcpy/releases/download/v1.16/ffmpeg-4.3.1-win32-shared.zip \ 357af9901a456f4dcbacd107e83a934d344c9cb07ddad8aaf80612eeab7d26d2 \ @@ -17,13 +18,9 @@ prepare-ffmpeg-win32: ln -sf ../ffmpeg-4.3.1-win32-dev/include ffmpeg-4.3.1-win32-shared/ prepare-ffmpeg-win64: - @./prepare-dep https://github.com/Genymobile/scrcpy/releases/download/v1.16/ffmpeg-4.3.1-win64-shared.zip \ - dd29b7f92f48dead4dd940492c7509138c0f99db445076d0a597007298a79940 \ - ffmpeg-4.3.1-win64-shared - @./prepare-dep https://github.com/Genymobile/scrcpy/releases/download/v1.16/ffmpeg-4.3.1-win64-dev.zip \ - 2e8038242cf8e1bd095c2978f196ff0462b122cc6ef7e74626a6af15459d8b81 \ - ffmpeg-4.3.1-win64-dev - ln -sf ../ffmpeg-4.3.1-win64-dev/include ffmpeg-4.3.1-win64-shared/ + @./prepare-dep https://github.com/GyanD/codexffmpeg/releases/download/5.0/ffmpeg-5.0-full_build-shared.7z \ + e5900f6cecd4c438d398bd2fc308736c10b857cd8dd61c11bcfb05bff5d1211a \ + ffmpeg-5.0-full_build-shared prepare-sdl2: @./prepare-dep https://libsdl.org/release/SDL2-devel-2.0.18-mingw.tar.gz \ diff --git a/prebuilt-deps/prepare-dep b/prebuilt-deps/prepare-dep index f152e6cf..a95b9bb6 100755 --- a/prebuilt-deps/prepare-dep +++ b/prebuilt-deps/prepare-dep @@ -34,6 +34,9 @@ extract() { elif [[ "$file" == *.tar.gz ]] then tar xf "$file" + elif [[ "$file" == *.7z ]] + then + 7z x "$file" else echo "Unsupported file: $file" return 1 diff --git a/release.mk b/release.mk index 6414f079..aa26f02d 100644 --- a/release.mk +++ b/release.mk @@ -110,11 +110,11 @@ dist-win64: build-server build-win64 cp data/scrcpy-console.bat "$(DIST)/$(WIN64_TARGET_DIR)" cp data/scrcpy-noconsole.vbs "$(DIST)/$(WIN64_TARGET_DIR)" cp data/icon.png "$(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/ffmpeg-5.0-full_build-shared/bin/avutil-57.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-5.0-full_build-shared/bin/avcodec-59.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-5.0-full_build-shared/bin/avformat-59.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-5.0-full_build-shared/bin/swresample-4.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-5.0-full_build-shared/bin/swscale-6.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)/" From b7a06278fee0dbfbb18b651ad563d9fb0797c8bc Mon Sep 17 00:00:00 2001 From: Thomas Rebele Date: Sat, 15 Jan 2022 16:27:51 +0100 Subject: [PATCH 0966/2244] Fix NoSuchMethodException for injectInputEvent() Some devices with modified ROMs expose a different signature for injectInputEvent(). Fixes #2250 PR #2946 Signed-off-by: Romain Vimont --- .../com/genymobile/scrcpy/wrappers/InputManager.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) 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 e17b5a17..dc7bc8da 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java @@ -16,6 +16,7 @@ public final class InputManager { private final IInterface manager; private Method injectInputEventMethod; + boolean alternativeInjectInputEventMethod; private static Method setDisplayIdMethod; @@ -25,7 +26,12 @@ public final class InputManager { private Method getInjectInputEventMethod() throws NoSuchMethodException { if (injectInputEventMethod == null) { - injectInputEventMethod = manager.getClass().getMethod("injectInputEvent", InputEvent.class, int.class); + try { + injectInputEventMethod = manager.getClass().getMethod("injectInputEvent", InputEvent.class, int.class); + } catch (NoSuchMethodException e) { + injectInputEventMethod = manager.getClass().getMethod("injectInputEvent", InputEvent.class, int.class, int.class); + alternativeInjectInputEventMethod = true; + } } return injectInputEventMethod; } @@ -33,6 +39,10 @@ public final class InputManager { public boolean injectInputEvent(InputEvent inputEvent, int mode) { try { Method method = getInjectInputEventMethod(); + if (alternativeInjectInputEventMethod) { + // See + return (boolean) method.invoke(manager, inputEvent, mode, 0); + } return (boolean) method.invoke(manager, inputEvent, mode); } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { Ln.e("Could not invoke method", e); From 117fe32626892c57a432ccd19347d36ae83661bd Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 21 Jan 2022 18:36:44 +0100 Subject: [PATCH 0967/2244] Fix visibility modifier Refs b7a06278fee0dbfbb18b651ad563d9fb0797c8bc --- .../main/java/com/genymobile/scrcpy/wrappers/InputManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 dc7bc8da..61168993 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java @@ -16,7 +16,7 @@ public final class InputManager { private final IInterface manager; private Method injectInputEventMethod; - boolean alternativeInjectInputEventMethod; + private boolean alternativeInjectInputEventMethod; private static Method setDisplayIdMethod; From 162043911e1d742489bf836113fe0797191a5160 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 15 Jan 2022 22:13:50 +0100 Subject: [PATCH 0968/2244] Compute screen size without DisplayInfo instance Use the actual rotation and size values directly. This will allow to automatically change the maxSize value on MediaCodec error. PR #2947 --- server/src/main/java/com/genymobile/scrcpy/Device.java | 3 ++- server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java | 5 +---- 2 files changed, 3 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 ba833a06..419e996e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -69,7 +69,8 @@ public final class Device { int displayInfoFlags = displayInfo.getFlags(); - screenInfo = ScreenInfo.computeScreenInfo(displayInfo, options.getCrop(), options.getMaxSize(), options.getLockVideoOrientation()); + screenInfo = ScreenInfo.computeScreenInfo(displayInfo.getRotation(), displayInfo.getSize(), options.getCrop(), options.getMaxSize(), + options.getLockVideoOrientation()); layerStack = displayInfo.getLayerStack(); SERVICE_MANAGER.getWindowManager().registerRotationWatcher(new IRotationWatcher.Stub() { diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java b/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java index c27322ef..8e5b401f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java @@ -80,15 +80,12 @@ public final class ScreenInfo { return new ScreenInfo(newContentRect, newUnlockedVideoSize, newDeviceRotation, lockedVideoOrientation); } - public static ScreenInfo computeScreenInfo(DisplayInfo displayInfo, Rect crop, int maxSize, int lockedVideoOrientation) { - int rotation = displayInfo.getRotation(); - + public static ScreenInfo computeScreenInfo(int rotation, Size deviceSize, Rect crop, int maxSize, int lockedVideoOrientation) { if (lockedVideoOrientation == Device.LOCK_VIDEO_ORIENTATION_INITIAL) { // The user requested to lock the video orientation to the current orientation lockedVideoOrientation = rotation; } - Size deviceSize = displayInfo.getSize(); Rect contentRect = new Rect(0, 0, deviceSize.getWidth(), deviceSize.getHeight()); if (crop != null) { if (rotation % 2 != 0) { // 180s preserve dimensions From 723faa5dee2915086c3b07fd1bfb6d541680b042 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 15 Jan 2022 22:17:38 +0100 Subject: [PATCH 0969/2244] Remember Device parameters This will allow to reuse them to recreate a ScreenInfo instance in order to change the maxSize value on MediaCodec error. PR #2947 --- .../src/main/java/com/genymobile/scrcpy/Device.java | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index 419e996e..591460b6 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -42,6 +42,11 @@ public final class Device { void onClipboardTextChanged(String text); } + private final Size deviceSize; + private final Rect crop; + private final int maxSize; + private final int lockVideoOrientation; + private ScreenInfo screenInfo; private RotationListener rotationListener; private ClipboardListener clipboardListener; @@ -69,8 +74,12 @@ public final class Device { int displayInfoFlags = displayInfo.getFlags(); - screenInfo = ScreenInfo.computeScreenInfo(displayInfo.getRotation(), displayInfo.getSize(), options.getCrop(), options.getMaxSize(), - options.getLockVideoOrientation()); + deviceSize = displayInfo.getSize(); + crop = options.getCrop(); + maxSize = options.getMaxSize(); + lockVideoOrientation = options.getLockVideoOrientation(); + + screenInfo = ScreenInfo.computeScreenInfo(displayInfo.getRotation(), deviceSize, crop, maxSize, lockVideoOrientation); layerStack = displayInfo.getLayerStack(); SERVICE_MANAGER.getWindowManager().registerRotationWatcher(new IRotationWatcher.Stub() { From 26b4104844fb9516be13ff1f2be34e2945090e79 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 15 Jan 2022 22:47:27 +0100 Subject: [PATCH 0970/2244] Downsize on error Some devices are not able to encode at the device screen definition. Instead of just failing, try with a lower definition on any MediaCodec error. PR #2947 --- .../java/com/genymobile/scrcpy/Device.java | 7 ++++- .../com/genymobile/scrcpy/ScreenEncoder.java | 27 +++++++++++++++++++ 2 files changed, 33 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 591460b6..763a7fad 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -44,7 +44,7 @@ public final class Device { private final Size deviceSize; private final Rect crop; - private final int maxSize; + private int maxSize; private final int lockVideoOrientation; private ScreenInfo screenInfo; @@ -133,6 +133,11 @@ public final class Device { } } + public synchronized void setMaxSize(int newMaxSize) { + maxSize = newMaxSize; + screenInfo = ScreenInfo.computeScreenInfo(screenInfo.getReverseVideoRotation(), deviceSize, crop, newMaxSize, lockVideoOrientation); + } + public synchronized ScreenInfo getScreenInfo() { return screenInfo; } diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index ce6b556a..ba601a22 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -25,6 +25,9 @@ public class ScreenEncoder implements Device.RotationListener { 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"; + // Keep the values in descending order + private static final int[] MAX_SIZE_FALLBACK = {2560, 1920, 1600, 1280, 1024, 800}; + private static final int NO_PTS = -1; private final AtomicBoolean rotationChanged = new AtomicBoolean(); @@ -91,6 +94,18 @@ public class ScreenEncoder implements Device.RotationListener { alive = encode(codec, fd); // do not call stop() on exception, it would trigger an IllegalStateException codec.stop(); + } catch (Exception e) { + Ln.e("Encoding error: " + e.getClass().getName() + ": " + e.getMessage()); + int newMaxSize = chooseMaxSizeFallback(screenInfo.getVideoSize()); + if (newMaxSize == 0) { + // Definitively fail + throw e; + } + + // Retry with a smaller device size + Ln.i("Retrying with -m" + newMaxSize + "..."); + device.setMaxSize(newMaxSize); + alive = true; } finally { destroyDisplay(display); codec.release(); @@ -102,6 +117,18 @@ public class ScreenEncoder implements Device.RotationListener { } } + private static int chooseMaxSizeFallback(Size failedSize) { + int currentMaxSize = Math.max(failedSize.getWidth(), failedSize.getHeight()); + for (int value : MAX_SIZE_FALLBACK) { + if (value < currentMaxSize) { + // We found a smaller value to reduce the video size + return value; + } + } + // No fallback, fail definitively + return 0; + } + private boolean encode(MediaCodec codec, FileDescriptor fd) throws IOException { boolean eof = false; MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); From 15bf27afdddc64c2887085e075398f71d2748d06 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 15 Jan 2022 23:01:14 +0100 Subject: [PATCH 0971/2244] Make auto-downsize on error optional Add --no-downsize-on-error option to disable attempts to use a lower definition on MediaCodec error. PR #2947 --- app/scrcpy.1 | 6 ++++++ app/src/cli.c | 11 +++++++++++ app/src/options.c | 1 + app/src/options.h | 1 + app/src/scrcpy.c | 1 + app/src/server.c | 4 ++++ app/src/server.h | 1 + .../src/main/java/com/genymobile/scrcpy/Options.java | 9 +++++++++ .../java/com/genymobile/scrcpy/ScreenEncoder.java | 10 +++++++++- .../src/main/java/com/genymobile/scrcpy/Server.java | 6 +++++- 10 files changed, 48 insertions(+), 2 deletions(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 4d02982c..37431a91 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -140,6 +140,12 @@ By default, scrcpy automatically synchronizes the computer clipboard to the devi This option disables this automatic synchronization. +.TP +.B \-\-no\-downsize\-on\-error +By default, on MediaCodec error, scrcpy automatically tries again with a lower definition. + +This option disables this behavior. + .TP .B \-n, \-\-no\-control Disable device control (mirror the device in read\-only). diff --git a/app/src/cli.c b/app/src/cli.c index 3fcf94eb..76feb130 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -52,6 +52,7 @@ #define OPT_NO_CLIPBOARD_AUTOSYNC 1032 #define OPT_TCPIP 1033 #define OPT_RAW_KEY_EVENTS 1034 +#define OPT_NO_DOWNSIZE_ON_ERROR 1035 struct sc_option { char shortopt; @@ -236,6 +237,13 @@ static const struct sc_option options[] = { "is preserved.\n" "Default is 0 (unlimited).", }, + { + .longopt_id = OPT_NO_DOWNSIZE_ON_ERROR, + .longopt = "no-downsize-on-error", + .text = "By default, on MediaCodec error, scrcpy automatically tries " + "again with a lower definition.\n" + "This option disables this behavior.", + }, { .longopt_id = OPT_NO_CLIPBOARD_AUTOSYNC, .longopt = "no-clipboard-autosync", @@ -1489,6 +1497,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], opts->tcpip = true; opts->tcpip_dst = optarg; break; + case OPT_NO_DOWNSIZE_ON_ERROR: + opts->downsize_on_error = false; + break; case OPT_V4L2_SINK: #ifdef HAVE_V4L2 opts->v4l2_device = optarg; diff --git a/app/src/options.c b/app/src/options.c index 7a5b71af..b8560406 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -54,6 +54,7 @@ const struct scrcpy_options scrcpy_options_default = { .legacy_paste = false, .power_off_on_close = false, .clipboard_autosync = true, + .downsize_on_error = true, .tcpip = false, .tcpip_dst = NULL, }; diff --git a/app/src/options.h b/app/src/options.h index b2c69664..99f03d40 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -129,6 +129,7 @@ struct scrcpy_options { bool legacy_paste; bool power_off_on_close; bool clipboard_autosync; + bool downsize_on_error; bool tcpip; const char *tcpip_dst; }; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 86fdf984..e55fef80 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -364,6 +364,7 @@ scrcpy(struct scrcpy_options *options) { .force_adb_forward = options->force_adb_forward, .power_off_on_close = options->power_off_on_close, .clipboard_autosync = options->clipboard_autosync, + .downsize_on_error = options->downsize_on_error, .tcpip = options->tcpip, .tcpip_dst = options->tcpip_dst, }; diff --git a/app/src/server.c b/app/src/server.c index ea7e5377..b62a74f1 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -234,6 +234,10 @@ execute_server(struct sc_server *server, // By default, clipboard_autosync is true ADD_PARAM("clipboard_autosync=false"); } + if (!params->downsize_on_error) { + // By default, downsize_on_error is true + ADD_PARAM("downsize_on_error=false"); + } #undef ADD_PARAM #undef STRBOOL diff --git a/app/src/server.h b/app/src/server.h index 8ea20dc7..89cdc2f4 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -42,6 +42,7 @@ struct sc_server_params { bool force_adb_forward; bool power_off_on_close; bool clipboard_autosync; + bool downsize_on_error; bool tcpip; const char *tcpip_dst; }; diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 1ac17176..ef7d8572 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -21,6 +21,7 @@ public class Options { private String encoderName; private boolean powerOffScreenOnClose; private boolean clipboardAutosync = true; + private boolean downsizeOnError = true; public Ln.Level getLogLevel() { return logLevel; @@ -149,4 +150,12 @@ public class Options { public void setClipboardAutosync(boolean clipboardAutosync) { this.clipboardAutosync = clipboardAutosync; } + + public boolean getDownsizeOnError() { + return downsizeOnError; + } + + public void setDownsizeOnError(boolean downsizeOnError) { + this.downsizeOnError = downsizeOnError; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index ba601a22..10ba9fac 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -38,14 +38,17 @@ public class ScreenEncoder implements Device.RotationListener { private final int bitRate; private final int maxFps; private final boolean sendFrameMeta; + private final boolean downsizeOnError; private long ptsOrigin; - public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, List codecOptions, String encoderName) { + public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, List codecOptions, String encoderName, + boolean downsizeOnError) { this.sendFrameMeta = sendFrameMeta; this.bitRate = bitRate; this.maxFps = maxFps; this.codecOptions = codecOptions; this.encoderName = encoderName; + this.downsizeOnError = downsizeOnError; } @Override @@ -96,6 +99,11 @@ public class ScreenEncoder implements Device.RotationListener { codec.stop(); } catch (Exception e) { Ln.e("Encoding error: " + e.getClass().getName() + ": " + e.getMessage()); + if (!downsizeOnError) { + // Fail immediately + throw e; + } + int newMaxSize = chooseMaxSizeFallback(screenInfo.getVideoSize()); if (newMaxSize == 0) { // Definitively fail diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 4f9575ae..21e3fd92 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -70,7 +70,7 @@ public final class Server { try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward, control)) { ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps(), codecOptions, - options.getEncoderName()); + options.getEncoderName(), options.getDownsizeOnError()); Thread controllerThread = null; Thread deviceMessageSenderThread = null; @@ -237,6 +237,10 @@ public final class Server { boolean clipboardAutosync = Boolean.parseBoolean(value); options.setClipboardAutosync(clipboardAutosync); break; + case "downsize_on_error": + boolean downsizeOnError = Boolean.parseBoolean(value); + options.setDownsizeOnError(downsizeOnError); + break; default: Ln.w("Unknown server option: " + key); break; From 0ec64baad437cc11d7fe7e046adb74eb2cdd9eb7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 15 Jan 2022 22:47:31 +0100 Subject: [PATCH 0972/2244] Remove MediaCodec error suggestion fix Now that scrcpy attempts with a lower definition on any MediaCodec error (or the user explicitly requests to disable auto-downsizing), the suggestion is unnecessary. PR #2947 --- .../src/main/java/com/genymobile/scrcpy/Server.java | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 21e3fd92..9d7a62e3 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -1,7 +1,6 @@ package com.genymobile.scrcpy; import android.graphics.Rect; -import android.media.MediaCodec; import android.media.MediaCodecInfo; import android.os.BatteryManager; import android.os.Build; @@ -267,16 +266,6 @@ public final class Server { } private static void suggestFix(Throwable e) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - if (e instanceof MediaCodec.CodecException) { - MediaCodec.CodecException mce = (MediaCodec.CodecException) e; - if (mce.getErrorCode() == 0xfffffc0e) { - Ln.e("The hardware encoder is not able to encode at the given definition."); - Ln.e("Try with a lower definition:"); - Ln.e(" scrcpy -m 1024"); - } - } - } if (e instanceof InvalidDisplayIdException) { InvalidDisplayIdException idie = (InvalidDisplayIdException) e; int[] displayIds = idie.getAvailableDisplayIds(); From 8fa9e6b01a4b5ee715cc6ba51d95ce8522ccc99c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 15 Jan 2022 23:17:52 +0100 Subject: [PATCH 0973/2244] Mention auto-downsize feature in FAQ PR #2947 --- FAQ.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/FAQ.md b/FAQ.md index d5f0e3ee..43ba39af 100644 --- a/FAQ.md +++ b/FAQ.md @@ -219,6 +219,9 @@ scrcpy -m 1024 scrcpy -m 800 ``` +Since scrcpy v1.22, scrcpy automatically tries again with a lower definition +before failing. This behavior can be disabled with `--no-downsize-on-error`. + You could also try another [encoder](README.md#encoder). From 4fb61ac83de7c99edcd1febd9635d822b3d0a075 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 16 Jan 2022 15:27:23 +0100 Subject: [PATCH 0974/2244] Fix screen comments The position fields accept SC_WINDOW_POSITION_UNDEFINED, not the size fields. PR #2947 --- app/src/screen.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/screen.h b/app/src/screen.h index 4810de31..9f65a5f6 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -74,10 +74,10 @@ struct sc_screen_params { struct sc_size frame_size; bool always_on_top; - int16_t window_x; - int16_t window_y; - uint16_t window_width; // accepts SC_WINDOW_POSITION_UNDEFINED - uint16_t window_height; // accepts SC_WINDOW_POSITION_UNDEFINED + int16_t window_x; // accepts SC_WINDOW_POSITION_UNDEFINED + int16_t window_y; // accepts SC_WINDOW_POSITION_UNDEFINED + uint16_t window_width; + uint16_t window_height; bool window_borderless; From fa30f9806aa928da5010c7110e6f0fafb5220eaf Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 16 Jan 2022 15:47:11 +0100 Subject: [PATCH 0975/2244] Move "show window" call on first frame Show the window only after the actual frame size is known (and if no error has occurred). This will allow to properly position and size the window when the size of the first frame is different from the size initially announced by the server. PR #2947 --- app/src/screen.c | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/app/src/screen.c b/app/src/screen.c index a2796278..78ab91d9 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -693,6 +693,12 @@ sc_screen_update_frame(struct sc_screen *screen) { } update_texture(screen, frame); + if (!screen->has_frame) { + screen->has_frame = true; + // this is the very first frame, show the window + sc_screen_show_window(screen); + } + sc_screen_render(screen, false); return true; } @@ -763,17 +769,13 @@ sc_screen_is_mouse_capture_key(SDL_Keycode key) { bool sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event) { switch (event->type) { - case EVENT_NEW_FRAME: - if (!screen->has_frame) { - screen->has_frame = true; - // this is the very first frame, show the window - sc_screen_show_window(screen); - } + case EVENT_NEW_FRAME: { bool ok = sc_screen_update_frame(screen); if (!ok) { LOGW("Frame update failed\n"); } return true; + } case SDL_WINDOWEVENT: if (!screen->has_frame) { // Do nothing From 75c5dc6859d29e2fb4d273e6c261288b802e16c0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 16 Jan 2022 15:46:37 +0100 Subject: [PATCH 0976/2244] Position and size the window on first frame The optimal initial size was computed from the expected dimensions, sent immediately by the server before encoding any video frame. However, the actual frame size may be different, for example when the device encoder does not support the requested size. To always handle this case properly, position and size the window only once the first frame size is known. PR #2947 --- app/src/screen.c | 50 ++++++++++++++++++++++++++---------------------- app/src/screen.h | 9 +++++++++ 2 files changed, 36 insertions(+), 23 deletions(-) diff --git a/app/src/screen.c b/app/src/screen.c index 78ab91d9..a357eecf 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -369,6 +369,12 @@ sc_screen_init(struct sc_screen *screen, screen->mouse_captured = false; screen->mouse_capture_key_pressed = 0; + screen->req.x = params->window_x; + screen->req.y = params->window_y; + screen->req.width = params->window_width; + screen->req.height = params->window_height; + screen->req.fullscreen = params->fullscreen; + static const struct sc_video_buffer_callbacks cbs = { .on_new_frame = sc_video_buffer_on_new_frame, }; @@ -397,9 +403,6 @@ sc_screen_init(struct sc_screen *screen, get_rotated_size(screen->frame_size, screen->rotation); screen->content_size = content_size; - struct sc_size window_size = - get_initial_optimal_size(content_size,params->window_width, - params->window_height); uint32_t window_flags = SDL_WINDOW_HIDDEN | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI; @@ -410,13 +413,9 @@ sc_screen_init(struct sc_screen *screen, window_flags |= SDL_WINDOW_BORDERLESS; } - int x = params->window_x != SC_WINDOW_POSITION_UNDEFINED - ? params->window_x : (int) SDL_WINDOWPOS_UNDEFINED; - int y = params->window_y != SC_WINDOW_POSITION_UNDEFINED - ? params->window_y : (int) SDL_WINDOWPOS_UNDEFINED; - screen->window = SDL_CreateWindow(params->window_title, x, y, - window_size.width, window_size.height, - window_flags); + // The window will be positioned and sized on first video frame + screen->window = + SDL_CreateWindow(params->window_title, 0, 0, 0, 0, window_flags); if (!screen->window) { LOGC("Could not create window: %s", SDL_GetError()); goto error_destroy_fps_counter; @@ -498,17 +497,6 @@ sc_screen_init(struct sc_screen *screen, sc_input_manager_init(&screen->im, &im_params); - // 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); - - sc_screen_update_content_rect(screen); - - if (params->fullscreen) { - sc_screen_switch_fullscreen(screen); - } - #ifdef CONTINUOUS_RESIZING_WORKAROUND SDL_AddEventWatch(event_watcher, screen); #endif @@ -545,7 +533,23 @@ error_destroy_video_buffer: } static void -sc_screen_show_window(struct sc_screen *screen) { +sc_screen_show_initial_window(struct sc_screen *screen) { + int x = screen->req.x != SC_WINDOW_POSITION_UNDEFINED + ? screen->req.x : (int) SDL_WINDOWPOS_CENTERED; + int y = screen->req.y != SC_WINDOW_POSITION_UNDEFINED + ? screen->req.y : (int) SDL_WINDOWPOS_CENTERED; + + struct sc_size window_size = + get_initial_optimal_size(screen->content_size, screen->req.width, + screen->req.height); + + set_window_size(screen, window_size); + SDL_SetWindowPosition(screen->window, x, y); + + if (screen->req.fullscreen) { + sc_screen_switch_fullscreen(screen); + } + SDL_ShowWindow(screen->window); } @@ -696,7 +700,7 @@ sc_screen_update_frame(struct sc_screen *screen) { if (!screen->has_frame) { screen->has_frame = true; // this is the very first frame, show the window - sc_screen_show_window(screen); + sc_screen_show_initial_window(screen); } sc_screen_render(screen, false); diff --git a/app/src/screen.h b/app/src/screen.h index 9f65a5f6..3e6c1031 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -28,6 +28,15 @@ struct sc_screen { struct sc_video_buffer vb; struct fps_counter fps_counter; + // The initial requested window properties + struct { + int16_t x; + int16_t y; + uint16_t width; + uint16_t height; + bool fullscreen; + } req; + SDL_Window *window; SDL_Renderer *renderer; SDL_Texture *texture; From 3a0ba7d0a439c0933f74e2433122c15cb7ee7318 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 17 Jan 2022 20:16:03 +0100 Subject: [PATCH 0977/2244] Disable downsizing on error if V4L2 is enabled V4L2 device is created with the initial device size, it does not support resizing. PR #2947 --- app/src/cli.c | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index 76feb130..69177aef 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -1545,11 +1545,18 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; } - if (opts->v4l2_device && opts->lock_video_orientation - == SC_LOCK_VIDEO_ORIENTATION_UNLOCKED) { - LOGI("Video orientation is locked for v4l2 sink. " - "See --lock-video-orientation."); - opts->lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_INITIAL; + if (opts->v4l2_device) { + if (opts->lock_video_orientation == + SC_LOCK_VIDEO_ORIENTATION_UNLOCKED) { + LOGI("Video orientation is locked for v4l2 sink. " + "See --lock-video-orientation."); + opts->lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_INITIAL; + } + + // V4L2 could not handle size change. + // Do not log because downsizing on error is the default behavior, + // not an explicit request from the user. + opts->downsize_on_error = false; } if (opts->v4l2_buffer && !opts->v4l2_device) { From 2eb6fe7d81c15912a2252f229d317ae03f618a33 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 17 Jan 2022 20:22:33 +0100 Subject: [PATCH 0978/2244] Downsize on error only before the first frame The purpose of automatic downscaling on error is to make mirroring work by just starting scrcpy without an explicit -m value, even if the encoder could not encode at the screen definition. It is only useful when we detect an encoding failure before the first frame. Downsizing later could be surprising, so disable it. PR #2947 --- .../src/main/java/com/genymobile/scrcpy/ScreenEncoder.java | 5 ++++- 1 file changed, 4 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 10ba9fac..06f06a9d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -41,6 +41,8 @@ public class ScreenEncoder implements Device.RotationListener { private final boolean downsizeOnError; private long ptsOrigin; + private boolean firstFrameSent; + public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, List codecOptions, String encoderName, boolean downsizeOnError) { this.sendFrameMeta = sendFrameMeta; @@ -99,7 +101,7 @@ public class ScreenEncoder implements Device.RotationListener { codec.stop(); } catch (Exception e) { Ln.e("Encoding error: " + e.getClass().getName() + ": " + e.getMessage()); - if (!downsizeOnError) { + if (!downsizeOnError || firstFrameSent) { // Fail immediately throw e; } @@ -157,6 +159,7 @@ public class ScreenEncoder implements Device.RotationListener { } IO.writeFully(fd, codecBuffer); + firstFrameSent = true; } } finally { if (outputBufferId >= 0) { From 262506c733ba5fb63ea08ef1a7cb7a667a741ccf Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 21 Jan 2022 21:50:31 +0100 Subject: [PATCH 0979/2244] Limit retry-on-error to IllegalStateException MediaCodec errors always trigger IllegalStateException or a subtype (like MediaCodec.CodecException). In practice, this avoids to retry if the error is caused by an IOException when writing the video packet to the socket. --- server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java | 2 +- 1 file changed, 1 insertion(+), 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 06f06a9d..e4e87c72 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -99,7 +99,7 @@ public class ScreenEncoder implements Device.RotationListener { alive = encode(codec, fd); // do not call stop() on exception, it would trigger an IllegalStateException codec.stop(); - } catch (Exception e) { + } catch (IllegalStateException e) { Ln.e("Encoding error: " + e.getClass().getName() + ": " + e.getMessage()); if (!downsizeOnError || firstFrameSent) { // Fail immediately From b066dc0bbfc608d52706dc59e826c75d7ad46fef Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 21 Jan 2022 19:10:27 +0100 Subject: [PATCH 0980/2244] Rename file_handler to sc_file_pusher Rename handler to pusher ("handler" is too generic), and add sc_ prefix. --- app/meson.build | 2 +- app/src/file_handler.c | 178 ----------------------------------------- app/src/file_handler.h | 60 -------------- app/src/file_pusher.c | 178 +++++++++++++++++++++++++++++++++++++++++ app/src/file_pusher.h | 59 ++++++++++++++ app/src/scrcpy.c | 30 +++---- 6 files changed, 253 insertions(+), 254 deletions(-) delete mode 100644 app/src/file_handler.c delete mode 100644 app/src/file_handler.h create mode 100644 app/src/file_pusher.c create mode 100644 app/src/file_pusher.h diff --git a/app/meson.build b/app/meson.build index 319dd0b6..88b8ef8c 100644 --- a/app/meson.build +++ b/app/meson.build @@ -11,7 +11,7 @@ src = [ 'src/decoder.c', 'src/device_msg.c', 'src/icon.c', - 'src/file_handler.c', + 'src/file_pusher.c', 'src/fps_counter.c', 'src/frame_buffer.c', 'src/input_manager.c', diff --git a/app/src/file_handler.c b/app/src/file_handler.c deleted file mode 100644 index 95d230ae..00000000 --- a/app/src/file_handler.c +++ /dev/null @@ -1,178 +0,0 @@ -#include "file_handler.h" - -#include -#include - -#include "adb.h" -#include "util/log.h" -#include "util/process_intr.h" - -#define DEFAULT_PUSH_TARGET "/sdcard/Download/" - -static void -file_handler_request_destroy(struct file_handler_request *req) { - free(req->file); -} - -bool -file_handler_init(struct file_handler *file_handler, const char *serial, - const char *push_target) { - assert(serial); - - cbuf_init(&file_handler->queue); - - bool ok = sc_mutex_init(&file_handler->mutex); - if (!ok) { - return false; - } - - ok = sc_cond_init(&file_handler->event_cond); - if (!ok) { - sc_mutex_destroy(&file_handler->mutex); - return false; - } - - ok = sc_intr_init(&file_handler->intr); - if (!ok) { - sc_cond_destroy(&file_handler->event_cond); - sc_mutex_destroy(&file_handler->mutex); - return false; - } - - file_handler->serial = strdup(serial); - if (!file_handler->serial) { - LOG_OOM(); - sc_intr_destroy(&file_handler->intr); - sc_cond_destroy(&file_handler->event_cond); - sc_mutex_destroy(&file_handler->mutex); - return false; - } - - // lazy initialization - file_handler->initialized = false; - - file_handler->stopped = false; - - file_handler->push_target = push_target ? push_target : DEFAULT_PUSH_TARGET; - - return true; -} - -void -file_handler_destroy(struct file_handler *file_handler) { - sc_cond_destroy(&file_handler->event_cond); - sc_mutex_destroy(&file_handler->mutex); - sc_intr_destroy(&file_handler->intr); - free(file_handler->serial); - - struct file_handler_request req; - while (cbuf_take(&file_handler->queue, &req)) { - file_handler_request_destroy(&req); - } -} - -bool -file_handler_request(struct file_handler *file_handler, - file_handler_action_t action, char *file) { - // start file_handler if it's used for the first time - if (!file_handler->initialized) { - if (!file_handler_start(file_handler)) { - return false; - } - file_handler->initialized = true; - } - - LOGI("Request to %s %s", action == ACTION_INSTALL_APK ? "install" : "push", - file); - struct file_handler_request req = { - .action = action, - .file = file, - }; - - sc_mutex_lock(&file_handler->mutex); - bool was_empty = cbuf_is_empty(&file_handler->queue); - bool res = cbuf_push(&file_handler->queue, req); - if (was_empty) { - sc_cond_signal(&file_handler->event_cond); - } - sc_mutex_unlock(&file_handler->mutex); - return res; -} - -static int -run_file_handler(void *data) { - struct file_handler *file_handler = data; - struct sc_intr *intr = &file_handler->intr; - - const char *serial = file_handler->serial; - assert(serial); - - const char *push_target = file_handler->push_target; - assert(push_target); - - for (;;) { - sc_mutex_lock(&file_handler->mutex); - while (!file_handler->stopped && cbuf_is_empty(&file_handler->queue)) { - sc_cond_wait(&file_handler->event_cond, &file_handler->mutex); - } - if (file_handler->stopped) { - // stop immediately, do not process further events - sc_mutex_unlock(&file_handler->mutex); - break; - } - struct file_handler_request req; - bool non_empty = cbuf_take(&file_handler->queue, &req); - assert(non_empty); - (void) non_empty; - sc_mutex_unlock(&file_handler->mutex); - - if (req.action == ACTION_INSTALL_APK) { - LOGI("Installing %s...", req.file); - bool ok = adb_install(intr, serial, req.file, 0); - if (ok) { - LOGI("%s successfully installed", req.file); - } else { - LOGE("Failed to install %s", req.file); - } - } else { - LOGI("Pushing %s...", req.file); - bool ok = adb_push(intr, serial, req.file, push_target, 0); - if (ok) { - LOGI("%s successfully pushed to %s", req.file, push_target); - } else { - LOGE("Failed to push %s to %s", req.file, push_target); - } - } - - file_handler_request_destroy(&req); - } - return 0; -} - -bool -file_handler_start(struct file_handler *file_handler) { - LOGD("Starting file_handler thread"); - - bool ok = sc_thread_create(&file_handler->thread, run_file_handler, - "scrcpy-file", file_handler); - if (!ok) { - LOGC("Could not start file_handler thread"); - return false; - } - - return true; -} - -void -file_handler_stop(struct file_handler *file_handler) { - sc_mutex_lock(&file_handler->mutex); - file_handler->stopped = true; - sc_cond_signal(&file_handler->event_cond); - sc_intr_interrupt(&file_handler->intr); - sc_mutex_unlock(&file_handler->mutex); -} - -void -file_handler_join(struct file_handler *file_handler) { - sc_thread_join(&file_handler->thread, NULL); -} diff --git a/app/src/file_handler.h b/app/src/file_handler.h deleted file mode 100644 index 4c0313cc..00000000 --- a/app/src/file_handler.h +++ /dev/null @@ -1,60 +0,0 @@ -#ifndef FILE_HANDLER_H -#define FILE_HANDLER_H - -#include "common.h" - -#include - -#include "adb.h" -#include "util/cbuf.h" -#include "util/thread.h" -#include "util/intr.h" - -typedef enum { - ACTION_INSTALL_APK, - ACTION_PUSH_FILE, -} file_handler_action_t; - -struct file_handler_request { - file_handler_action_t action; - char *file; -}; - -struct file_handler_request_queue CBUF(struct file_handler_request, 16); - -struct file_handler { - char *serial; - const char *push_target; - sc_thread thread; - sc_mutex mutex; - sc_cond event_cond; - bool stopped; - bool initialized; - struct file_handler_request_queue queue; - - struct sc_intr intr; -}; - -bool -file_handler_init(struct file_handler *file_handler, const char *serial, - const char *push_target); - -void -file_handler_destroy(struct file_handler *file_handler); - -bool -file_handler_start(struct file_handler *file_handler); - -void -file_handler_stop(struct file_handler *file_handler); - -void -file_handler_join(struct file_handler *file_handler); - -// take ownership of file, and will free() it -bool -file_handler_request(struct file_handler *file_handler, - file_handler_action_t action, - char *file); - -#endif diff --git a/app/src/file_pusher.c b/app/src/file_pusher.c new file mode 100644 index 00000000..738e3616 --- /dev/null +++ b/app/src/file_pusher.c @@ -0,0 +1,178 @@ +#include "file_pusher.h" + +#include +#include + +#include "adb.h" +#include "util/log.h" +#include "util/process_intr.h" + +#define DEFAULT_PUSH_TARGET "/sdcard/Download/" + +static void +sc_file_pusher_request_destroy(struct sc_file_pusher_request *req) { + free(req->file); +} + +bool +sc_file_pusher_init(struct sc_file_pusher *fp, const char *serial, + const char *push_target) { + assert(serial); + + cbuf_init(&fp->queue); + + bool ok = sc_mutex_init(&fp->mutex); + if (!ok) { + return false; + } + + ok = sc_cond_init(&fp->event_cond); + if (!ok) { + sc_mutex_destroy(&fp->mutex); + return false; + } + + ok = sc_intr_init(&fp->intr); + if (!ok) { + sc_cond_destroy(&fp->event_cond); + sc_mutex_destroy(&fp->mutex); + return false; + } + + fp->serial = strdup(serial); + if (!fp->serial) { + LOG_OOM(); + sc_intr_destroy(&fp->intr); + sc_cond_destroy(&fp->event_cond); + sc_mutex_destroy(&fp->mutex); + return false; + } + + // lazy initialization + fp->initialized = false; + + fp->stopped = false; + + fp->push_target = push_target ? push_target : DEFAULT_PUSH_TARGET; + + return true; +} + +void +sc_file_pusher_destroy(struct sc_file_pusher *fp) { + sc_cond_destroy(&fp->event_cond); + sc_mutex_destroy(&fp->mutex); + sc_intr_destroy(&fp->intr); + free(fp->serial); + + struct sc_file_pusher_request req; + while (cbuf_take(&fp->queue, &req)) { + sc_file_pusher_request_destroy(&req); + } +} + +bool +sc_file_pusher_request(struct sc_file_pusher *fp, + enum sc_file_pusher_action action, char *file) { + // start file_pusher if it's used for the first time + if (!fp->initialized) { + if (!sc_file_pusher_start(fp)) { + return false; + } + fp->initialized = true; + } + + LOGI("Request to %s %s", action == SC_FILE_PUSHER_ACTION_INSTALL_APK + ? "install" : "push", + file); + struct sc_file_pusher_request req = { + .action = action, + .file = file, + }; + + sc_mutex_lock(&fp->mutex); + bool was_empty = cbuf_is_empty(&fp->queue); + bool res = cbuf_push(&fp->queue, req); + if (was_empty) { + sc_cond_signal(&fp->event_cond); + } + sc_mutex_unlock(&fp->mutex); + return res; +} + +static int +run_file_pusher(void *data) { + struct sc_file_pusher *fp = data; + struct sc_intr *intr = &fp->intr; + + const char *serial = fp->serial; + assert(serial); + + const char *push_target = fp->push_target; + assert(push_target); + + for (;;) { + sc_mutex_lock(&fp->mutex); + while (!fp->stopped && cbuf_is_empty(&fp->queue)) { + sc_cond_wait(&fp->event_cond, &fp->mutex); + } + if (fp->stopped) { + // stop immediately, do not process further events + sc_mutex_unlock(&fp->mutex); + break; + } + struct sc_file_pusher_request req; + bool non_empty = cbuf_take(&fp->queue, &req); + assert(non_empty); + (void) non_empty; + sc_mutex_unlock(&fp->mutex); + + if (req.action == SC_FILE_PUSHER_ACTION_INSTALL_APK) { + LOGI("Installing %s...", req.file); + bool ok = adb_install(intr, serial, req.file, 0); + if (ok) { + LOGI("%s successfully installed", req.file); + } else { + LOGE("Failed to install %s", req.file); + } + } else { + LOGI("Pushing %s...", req.file); + bool ok = adb_push(intr, serial, req.file, push_target, 0); + if (ok) { + LOGI("%s successfully pushed to %s", req.file, push_target); + } else { + LOGE("Failed to push %s to %s", req.file, push_target); + } + } + + sc_file_pusher_request_destroy(&req); + } + return 0; +} + +bool +sc_file_pusher_start(struct sc_file_pusher *fp) { + LOGD("Starting file_pusher thread"); + + bool ok = sc_thread_create(&fp->thread, run_file_pusher, "scrcpy-file", fp); + if (!ok) { + LOGC("Could not start file_pusher thread"); + return false; + } + + return true; +} + +void +sc_file_pusher_stop(struct sc_file_pusher *fp) { + sc_mutex_lock(&fp->mutex); + fp->stopped = true; + sc_cond_signal(&fp->event_cond); + sc_intr_interrupt(&fp->intr); + sc_mutex_unlock(&fp->mutex); +} + +void +sc_file_pusher_join(struct sc_file_pusher *fp) { + sc_thread_join(&fp->thread, NULL); +} diff --git a/app/src/file_pusher.h b/app/src/file_pusher.h new file mode 100644 index 00000000..56b107a0 --- /dev/null +++ b/app/src/file_pusher.h @@ -0,0 +1,59 @@ +#ifndef SC_FILE_PUSHER_H +#define SC_FILE_PUSHER_H + +#include "common.h" + +#include + +#include "adb.h" +#include "util/cbuf.h" +#include "util/thread.h" +#include "util/intr.h" + +enum sc_file_pusher_action { + SC_FILE_PUSHER_ACTION_INSTALL_APK, + SC_FILE_PUSHER_ACTION_PUSH_FILE, +}; + +struct sc_file_pusher_request { + enum sc_file_pusher_action action; + char *file; +}; + +struct sc_file_pusher_request_queue CBUF(struct sc_file_pusher_request, 16); + +struct sc_file_pusher { + char *serial; + const char *push_target; + sc_thread thread; + sc_mutex mutex; + sc_cond event_cond; + bool stopped; + bool initialized; + struct sc_file_pusher_request_queue queue; + + struct sc_intr intr; +}; + +bool +sc_file_pusher_init(struct sc_file_pusher *fp, const char *serial, + const char *push_target); + +void +sc_file_pusher_destroy(struct sc_file_pusher *fp); + +bool +sc_file_pusher_start(struct sc_file_pusher *fp); + +void +sc_file_pusher_stop(struct sc_file_pusher *fp); + +void +sc_file_pusher_join(struct sc_file_pusher *fp); + +// take ownership of file, and will free() it +bool +sc_file_pusher_request(struct sc_file_pusher *fp, + enum sc_file_pusher_action action, char *file); + +#endif diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index e55fef80..4e7ccd6f 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -16,7 +16,7 @@ #include "controller.h" #include "decoder.h" #include "events.h" -#include "file_handler.h" +#include "file_pusher.h" #ifdef HAVE_AOA_HID # include "hid_keyboard.h" # include "hid_mouse.h" @@ -44,7 +44,7 @@ struct scrcpy { struct sc_v4l2_sink v4l2_sink; #endif struct sc_controller controller; - struct file_handler file_handler; + struct sc_file_pusher file_pusher; #ifdef HAVE_AOA_HID struct sc_aoa aoa; // sequence/ack helper to synchronize clipboard and Ctrl+v via HID @@ -181,13 +181,13 @@ handle_event(struct scrcpy *s, const struct scrcpy_options *options, break; } - file_handler_action_t action; + enum sc_file_pusher_action action; if (is_apk(file)) { - action = ACTION_INSTALL_APK; + action = SC_FILE_PUSHER_ACTION_INSTALL_APK; } else { - action = ACTION_PUSH_FILE; + action = SC_FILE_PUSHER_ACTION_PUSH_FILE; } - file_handler_request(&s->file_handler, action, file); + sc_file_pusher_request(&s->file_pusher, action, file); goto end; } } @@ -327,7 +327,7 @@ scrcpy(struct scrcpy_options *options) { bool ret = false; bool server_started = false; - bool file_handler_initialized = false; + bool file_pusher_initialized = false; bool recorder_initialized = false; #ifdef HAVE_V4L2 bool v4l2_sink_initialized = false; @@ -408,11 +408,11 @@ scrcpy(struct scrcpy_options *options) { assert(serial); if (options->display && options->control) { - if (!file_handler_init(&s->file_handler, serial, - options->push_target)) { + if (!sc_file_pusher_init(&s->file_pusher, serial, + options->push_target)) { goto end; } - file_handler_initialized = true; + file_pusher_initialized = true; } struct decoder *dec = NULL; @@ -649,8 +649,8 @@ end: if (controller_started) { sc_controller_stop(&s->controller); } - if (file_handler_initialized) { - file_handler_stop(&s->file_handler); + if (file_pusher_initialized) { + sc_file_pusher_stop(&s->file_pusher); } if (screen_initialized) { sc_screen_interrupt(&s->screen); @@ -698,9 +698,9 @@ end: recorder_destroy(&s->recorder); } - if (file_handler_initialized) { - file_handler_join(&s->file_handler); - file_handler_destroy(&s->file_handler); + if (file_pusher_initialized) { + sc_file_pusher_join(&s->file_pusher); + sc_file_pusher_destroy(&s->file_pusher); } sc_server_destroy(&s->server); From 8e4e7d42f1d776dc8a6e14322fdda494c973135e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 21 Jan 2022 19:15:47 +0100 Subject: [PATCH 0981/2244] Fix leak on file pusher error If a file_push request fails, the allocated filename must be freed. --- app/src/scrcpy.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 4e7ccd6f..d628df51 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -187,7 +187,10 @@ handle_event(struct scrcpy *s, const struct scrcpy_options *options, } else { action = SC_FILE_PUSHER_ACTION_PUSH_FILE; } - sc_file_pusher_request(&s->file_pusher, action, file); + bool ok = sc_file_pusher_request(&s->file_pusher, action, file); + if (!ok) { + free(file); + } goto end; } } From ebef027c4f13e7cd30743525e845ad06b3660679 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 21 Jan 2022 19:16:32 +0100 Subject: [PATCH 0982/2244] Do not return status for event handling It is never read. Simplify. --- app/src/input_manager.c | 18 ++++++++---------- app/src/input_manager.h | 2 +- app/src/scrcpy.c | 3 +-- app/src/screen.c | 20 ++++++++++---------- app/src/screen.h | 2 +- 5 files changed, 21 insertions(+), 24 deletions(-) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 9eee6e6c..e1e8738c 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -834,45 +834,43 @@ sc_input_manager_process_mouse_wheel(struct sc_input_manager *im, im->mp->ops->process_mouse_scroll(im->mp, &evt); } -bool +void sc_input_manager_handle_event(struct sc_input_manager *im, SDL_Event *event) { switch (event->type) { case SDL_TEXTINPUT: if (!im->control) { - return true; + break; } sc_input_manager_process_text_input(im, &event->text); - return true; + break; case SDL_KEYDOWN: case SDL_KEYUP: // some key events do not interact with the device, so process the // event even if control is disabled sc_input_manager_process_key(im, &event->key); - return true; + break; case SDL_MOUSEMOTION: if (!im->control) { break; } sc_input_manager_process_mouse_motion(im, &event->motion); - return true; + break; case SDL_MOUSEWHEEL: if (!im->control) { break; } sc_input_manager_process_mouse_wheel(im, &event->wheel); - return true; + break; case SDL_MOUSEBUTTONDOWN: case SDL_MOUSEBUTTONUP: // some mouse events do not interact with the device, so process // the event even if control is disabled sc_input_manager_process_mouse_button(im, &event->button); - return true; + break; case SDL_FINGERMOTION: case SDL_FINGERDOWN: case SDL_FINGERUP: sc_input_manager_process_touch(im, &event->tfinger); - return true; + break; } - - return false; } diff --git a/app/src/input_manager.h b/app/src/input_manager.h index 5d552ee2..b28b110f 100644 --- a/app/src/input_manager.h +++ b/app/src/input_manager.h @@ -59,7 +59,7 @@ void sc_input_manager_init(struct sc_input_manager *im, const struct sc_input_manager_params *params); -bool +void sc_input_manager_handle_event(struct sc_input_manager *im, SDL_Event *event); #endif diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index d628df51..1c03b8c1 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -195,8 +195,7 @@ handle_event(struct scrcpy *s, const struct scrcpy_options *options, } } - bool consumed = sc_screen_handle_event(&s->screen, event); - (void) consumed; + sc_screen_handle_event(&s->screen, event); end: return EVENT_RESULT_CONTINUE; diff --git a/app/src/screen.c b/app/src/screen.c index a357eecf..b1abe985 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -770,7 +770,7 @@ sc_screen_is_mouse_capture_key(SDL_Keycode key) { return key == SDLK_LALT || key == SDLK_LGUI || key == SDLK_RGUI; } -bool +void sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event) { switch (event->type) { case EVENT_NEW_FRAME: { @@ -778,12 +778,12 @@ sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event) { if (!ok) { LOGW("Frame update failed\n"); } - return true; + return; } case SDL_WINDOWEVENT: if (!screen->has_frame) { // Do nothing - return true; + return; } switch (event->window.event) { case SDL_WINDOWEVENT_EXPOSED: @@ -814,14 +814,14 @@ sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event) { } break; } - return true; + return; case SDL_KEYDOWN: if (screen->im.mp->relative_mode) { SDL_Keycode key = event->key.keysym.sym; if (sc_screen_is_mouse_capture_key(key)) { if (!screen->mouse_capture_key_pressed) { screen->mouse_capture_key_pressed = key; - return true; + return; } else { // Another mouse capture key has been pressed, cancel // mouse (un)capture @@ -841,7 +841,7 @@ sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event) { // A mouse capture key has been pressed then released: // toggle the capture mouse mode sc_screen_capture_mouse(screen, !screen->mouse_captured); - return true; + return; } // Do not return, the event must be forwarded to the input // manager @@ -853,7 +853,7 @@ sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event) { if (screen->im.mp->relative_mode && !screen->mouse_captured) { // Do not forward to input manager, the mouse will be captured // on SDL_MOUSEBUTTONUP - return true; + return; } break; case SDL_FINGERMOTION: @@ -862,17 +862,17 @@ sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event) { if (screen->im.mp->relative_mode) { // Touch events are not compatible with relative mode // (coordinates are not relative) - return true; + return; } break; case SDL_MOUSEBUTTONUP: if (screen->im.mp->relative_mode && !screen->mouse_captured) { sc_screen_capture_mouse(screen, true); - return true; + return; } } - return sc_input_manager_handle_event(&screen->im, event); + sc_input_manager_handle_event(&screen->im, event); } struct sc_point diff --git a/app/src/screen.h b/app/src/screen.h index 3e6c1031..8af53a5e 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -139,7 +139,7 @@ void sc_screen_set_rotation(struct sc_screen *screen, unsigned rotation); // react to SDL events -bool +void sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event); // convert point from window coordinates to frame coordinates From 1ffe312369ee2b223c8a946e053ad70932bec173 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 21 Jan 2022 19:26:36 +0100 Subject: [PATCH 0983/2244] Handle file drop from input_manager A file is pushed (or an apk is installed) to the device on file drop. This behavior is specific to the screen and its input_manager. --- app/src/input_manager.c | 35 ++++++++++++++++++++++++++++++++ app/src/input_manager.h | 3 +++ app/src/scrcpy.c | 44 ++++++++--------------------------------- app/src/screen.c | 1 + app/src/screen.h | 1 + 5 files changed, 48 insertions(+), 36 deletions(-) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index e1e8738c..08531cac 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -128,6 +128,7 @@ sc_input_manager_init(struct sc_input_manager *im, assert(!params->control || (params->mp && params->mp->ops)); im->controller = params->controller; + im->fp = params->fp; im->screen = params->screen; im->kp = params->kp; im->mp = params->mp; @@ -834,6 +835,34 @@ sc_input_manager_process_mouse_wheel(struct sc_input_manager *im, im->mp->ops->process_mouse_scroll(im->mp, &evt); } +static bool +is_apk(const char *file) { + const char *ext = strrchr(file, '.'); + return ext && !strcmp(ext, ".apk"); +} + +static void +sc_input_manager_process_file(struct sc_input_manager *im, + const SDL_DropEvent *event) { + char *file = strdup(event->file); + SDL_free(event->file); + if (!file) { + LOG_OOM(); + return; + } + + enum sc_file_pusher_action action; + if (is_apk(file)) { + action = SC_FILE_PUSHER_ACTION_INSTALL_APK; + } else { + action = SC_FILE_PUSHER_ACTION_PUSH_FILE; + } + bool ok = sc_file_pusher_request(im->fp, action, file); + if (!ok) { + free(file); + } +} + void sc_input_manager_handle_event(struct sc_input_manager *im, SDL_Event *event) { switch (event->type) { @@ -872,5 +901,11 @@ sc_input_manager_handle_event(struct sc_input_manager *im, SDL_Event *event) { case SDL_FINGERUP: sc_input_manager_process_touch(im, &event->tfinger); break; + case SDL_DROPFILE: { + if (!im->control) { + break; + } + sc_input_manager_process_file(im, &event->drop); + } } } diff --git a/app/src/input_manager.h b/app/src/input_manager.h index b28b110f..377835bf 100644 --- a/app/src/input_manager.h +++ b/app/src/input_manager.h @@ -8,6 +8,7 @@ #include #include "controller.h" +#include "file_pusher.h" #include "fps_counter.h" #include "options.h" #include "trait/key_processor.h" @@ -15,6 +16,7 @@ struct sc_input_manager { struct sc_controller *controller; + struct sc_file_pusher *fp; struct sc_screen *screen; struct sc_key_processor *kp; @@ -44,6 +46,7 @@ struct sc_input_manager { struct sc_input_manager_params { struct sc_controller *controller; + struct sc_file_pusher *fp; struct sc_screen *screen; struct sc_key_processor *kp; struct sc_mouse_processor *mp; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 1c03b8c1..0552a057 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -148,12 +148,6 @@ sdl_configure(bool display, bool disable_screensaver) { } } -static bool -is_apk(const char *file) { - const char *ext = strrchr(file, '.'); - return ext && !strcmp(ext, ".apk"); -} - enum event_result { EVENT_RESULT_CONTINUE, EVENT_RESULT_STOPPED_BY_USER, @@ -161,8 +155,7 @@ enum event_result { }; static enum event_result -handle_event(struct scrcpy *s, const struct scrcpy_options *options, - SDL_Event *event) { +handle_event(struct scrcpy *s, SDL_Event *event) { switch (event->type) { case EVENT_STREAM_STOPPED: LOGD("Video stream stopped"); @@ -170,42 +163,17 @@ handle_event(struct scrcpy *s, const struct scrcpy_options *options, case SDL_QUIT: LOGD("User requested to quit"); return EVENT_RESULT_STOPPED_BY_USER; - case SDL_DROPFILE: { - if (!options->control) { - break; - } - char *file = strdup(event->drop.file); - SDL_free(event->drop.file); - if (!file) { - LOGW("Could not strdup drop filename\n"); - break; - } - - enum sc_file_pusher_action action; - if (is_apk(file)) { - action = SC_FILE_PUSHER_ACTION_INSTALL_APK; - } else { - action = SC_FILE_PUSHER_ACTION_PUSH_FILE; - } - bool ok = sc_file_pusher_request(&s->file_pusher, action, file); - if (!ok) { - free(file); - } - goto end; - } } sc_screen_handle_event(&s->screen, event); - -end: return EVENT_RESULT_CONTINUE; } static bool -event_loop(struct scrcpy *s, const struct scrcpy_options *options) { +event_loop(struct scrcpy *s) { SDL_Event event; while (SDL_WaitEvent(&event)) { - enum event_result result = handle_event(s, options, &event); + enum event_result result = handle_event(s, &event); switch (result) { case EVENT_RESULT_STOPPED_BY_USER: return true; @@ -409,11 +377,14 @@ scrcpy(struct scrcpy_options *options) { const char *serial = s->server.params.serial; assert(serial); + struct sc_file_pusher *fp = NULL; + if (options->display && options->control) { if (!sc_file_pusher_init(&s->file_pusher, serial, options->push_target)) { goto end; } + fp = &s->file_pusher; file_pusher_initialized = true; } @@ -578,6 +549,7 @@ aoa_hid_end: struct sc_screen_params screen_params = { .controller = &s->controller, + .fp = fp, .kp = kp, .mp = mp, .control = options->control, @@ -627,7 +599,7 @@ aoa_hid_end: } stream_started = true; - ret = event_loop(s, options); + ret = event_loop(s); LOGD("quit..."); // Close the window immediately on closing, because screen_destroy() may diff --git a/app/src/screen.c b/app/src/screen.c index b1abe985..92afafbb 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -485,6 +485,7 @@ sc_screen_init(struct sc_screen *screen, struct sc_input_manager_params im_params = { .controller = params->controller, + .fp = params->fp, .screen = screen, .kp = params->kp, .mp = params->mp, diff --git a/app/src/screen.h b/app/src/screen.h index 8af53a5e..677cf514 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -70,6 +70,7 @@ struct sc_screen { struct sc_screen_params { struct sc_controller *controller; + struct sc_file_pusher *fp; struct sc_key_processor *kp; struct sc_mouse_processor *mp; From 81ff7ebd065eaf6917905e64e4e7017e9f83fc3b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 21 Jan 2022 21:43:49 +0100 Subject: [PATCH 0984/2244] Simplify event loop Merge single event handling with the event loop function. --- app/src/scrcpy.c | 34 +++++++--------------------------- 1 file changed, 7 insertions(+), 27 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 0552a057..d7ba3670 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -148,39 +148,19 @@ sdl_configure(bool display, bool disable_screensaver) { } } -enum event_result { - EVENT_RESULT_CONTINUE, - EVENT_RESULT_STOPPED_BY_USER, - EVENT_RESULT_STOPPED_BY_EOS, -}; - -static enum event_result -handle_event(struct scrcpy *s, SDL_Event *event) { - switch (event->type) { - case EVENT_STREAM_STOPPED: - LOGD("Video stream stopped"); - return EVENT_RESULT_STOPPED_BY_EOS; - case SDL_QUIT: - LOGD("User requested to quit"); - return EVENT_RESULT_STOPPED_BY_USER; - } - - sc_screen_handle_event(&s->screen, event); - return EVENT_RESULT_CONTINUE; -} - static bool event_loop(struct scrcpy *s) { SDL_Event event; while (SDL_WaitEvent(&event)) { - enum event_result result = handle_event(s, &event); - switch (result) { - case EVENT_RESULT_STOPPED_BY_USER: - return true; - case EVENT_RESULT_STOPPED_BY_EOS: + switch (event.type) { + case EVENT_STREAM_STOPPED: LOGW("Device disconnected"); return false; - case EVENT_RESULT_CONTINUE: + case SDL_QUIT: + LOGD("User requested to quit"); + return true; + default: + sc_screen_handle_event(&s->screen, &event); break; } } From 0ec3361bc9711332a30725b03b9592a4c68f0248 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 23 Jan 2022 11:34:00 +0100 Subject: [PATCH 0985/2244] Fix crash on --no-control Relative mouse mode assumed that a mouse processor was always available, but this is not the case if --no-control is passed. --- app/src/screen.c | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/app/src/screen.c b/app/src/screen.c index 92afafbb..9393a92f 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -773,6 +773,9 @@ sc_screen_is_mouse_capture_key(SDL_Keycode key) { void sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event) { + // screen->im.mp may be NULL if --no-control + bool relative_mode = screen->im.mp && screen->im.mp->relative_mode; + switch (event->type) { case EVENT_NEW_FRAME: { bool ok = sc_screen_update_frame(screen); @@ -810,14 +813,14 @@ sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event) { sc_screen_render(screen, true); break; case SDL_WINDOWEVENT_FOCUS_LOST: - if (screen->im.mp->relative_mode) { + if (relative_mode) { sc_screen_capture_mouse(screen, false); } break; } return; case SDL_KEYDOWN: - if (screen->im.mp->relative_mode) { + if (relative_mode) { SDL_Keycode key = event->key.keysym.sym; if (sc_screen_is_mouse_capture_key(key)) { if (!screen->mouse_capture_key_pressed) { @@ -834,7 +837,7 @@ sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event) { } break; case SDL_KEYUP: - if (screen->im.mp->relative_mode) { + if (relative_mode) { SDL_Keycode key = event->key.keysym.sym; SDL_Keycode cap = screen->mouse_capture_key_pressed; screen->mouse_capture_key_pressed = 0; @@ -851,7 +854,7 @@ sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event) { case SDL_MOUSEWHEEL: case SDL_MOUSEMOTION: case SDL_MOUSEBUTTONDOWN: - if (screen->im.mp->relative_mode && !screen->mouse_captured) { + if (relative_mode && !screen->mouse_captured) { // Do not forward to input manager, the mouse will be captured // on SDL_MOUSEBUTTONUP return; @@ -860,14 +863,14 @@ sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event) { case SDL_FINGERMOTION: case SDL_FINGERDOWN: case SDL_FINGERUP: - if (screen->im.mp->relative_mode) { + if (relative_mode) { // Touch events are not compatible with relative mode // (coordinates are not relative) return; } break; case SDL_MOUSEBUTTONUP: - if (screen->im.mp->relative_mode && !screen->mouse_captured) { + if (relative_mode && !screen->mouse_captured) { sc_screen_capture_mouse(screen, true); return; } From 0b8e9263305a388faabafac83565322e18da0537 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 23 Jan 2022 12:02:35 +0100 Subject: [PATCH 0986/2244] Do not process finger events if no control If --no-control is passed, then im->mp is NULL, so processing touches would crash. --- app/src/input_manager.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 08531cac..c7290dd6 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -899,6 +899,9 @@ sc_input_manager_handle_event(struct sc_input_manager *im, SDL_Event *event) { case SDL_FINGERMOTION: case SDL_FINGERDOWN: case SDL_FINGERUP: + if (!im->control) { + break; + } sc_input_manager_process_touch(im, &event->tfinger); break; case SDL_DROPFILE: { From 557daf280e03932ab1289a140d7920f756b0ad1f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 23 Jan 2022 11:43:09 +0100 Subject: [PATCH 0987/2244] Pass NULL controller if control is disabled If --no-control is requested, then the controller instance is not initialized. However, its reference was still passed to screen and input_manager. Instead, pass NULL if no controller is available. --- app/src/scrcpy.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index d7ba3670..5c2114eb 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -405,6 +405,7 @@ scrcpy(struct scrcpy_options *options) { stream_add_sink(&s->stream, &rec->packet_sink); } + struct sc_controller *controller = NULL; struct sc_key_processor *kp = NULL; struct sc_mouse_processor *mp = NULL; @@ -510,6 +511,7 @@ aoa_hid_end: goto end; } controller_started = true; + controller = &s->controller; if (options->turn_screen_off) { struct sc_control_msg msg; @@ -528,7 +530,7 @@ aoa_hid_end: options->window_title ? options->window_title : info->device_name; struct sc_screen_params screen_params = { - .controller = &s->controller, + .controller = controller, .fp = fp, .kp = kp, .mp = mp, From 855819bbd83d9d8e06988357ac1b548b11a4a0db Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 23 Jan 2022 12:08:55 +0100 Subject: [PATCH 0988/2244] Remove redundant control boolean The controller is NULL if and only if control is disabled, so an additional control boolean is redundant. --- app/src/input_manager.c | 70 ++++++++++++++++++++--------------------- app/src/input_manager.h | 2 -- app/src/scrcpy.c | 4 ++- app/src/screen.c | 1 - app/src/screen.h | 1 - 5 files changed, 37 insertions(+), 41 deletions(-) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index c7290dd6..c520b1aa 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -124,8 +124,8 @@ is_shortcut_mod(struct sc_input_manager *im, uint16_t sdl_mod) { void sc_input_manager_init(struct sc_input_manager *im, const struct sc_input_manager_params *params) { - assert(!params->control || (params->kp && params->kp->ops)); - assert(!params->control || (params->mp && params->mp->ops)); + assert(!params->controller || (params->kp && params->kp->ops)); + assert(!params->controller || (params->mp && params->mp->ops)); im->controller = params->controller; im->fp = params->fp; @@ -133,7 +133,6 @@ sc_input_manager_init(struct sc_input_manager *im, im->kp = params->kp; im->mp = params->mp; - im->control = params->control; im->forward_all_clicks = params->forward_all_clicks; im->legacy_paste = params->legacy_paste; im->clipboard_autosync = params->clipboard_autosync; @@ -434,9 +433,7 @@ inverse_point(struct sc_point point, struct sc_size size) { static void sc_input_manager_process_key(struct sc_input_manager *im, const SDL_KeyboardEvent *event) { - // control: indicates the state of the command-line option --no-control - bool control = im->control; - + // controller is NULL if --no-control is requested struct sc_controller *controller = im->controller; SDL_Keycode keycode = event->keysym.sym; @@ -463,33 +460,33 @@ sc_input_manager_process_key(struct sc_input_manager *im, enum sc_action action = down ? SC_ACTION_DOWN : SC_ACTION_UP; switch (keycode) { case SDLK_h: - if (control && !shift && !repeat) { + if (controller && !shift && !repeat) { action_home(controller, action); } return; case SDLK_b: // fall-through case SDLK_BACKSPACE: - if (control && !shift && !repeat) { + if (controller && !shift && !repeat) { action_back(controller, action); } return; case SDLK_s: - if (control && !shift && !repeat) { + if (controller && !shift && !repeat) { action_app_switch(controller, action); } return; case SDLK_m: - if (control && !shift && !repeat) { + if (controller && !shift && !repeat) { action_menu(controller, action); } return; case SDLK_p: - if (control && !shift && !repeat) { + if (controller && !shift && !repeat) { action_power(controller, action); } return; case SDLK_o: - if (control && !repeat && down) { + if (controller && !repeat && down) { enum screen_power_mode mode = shift ? SCREEN_POWER_MODE_NORMAL : SCREEN_POWER_MODE_OFF; @@ -497,13 +494,13 @@ sc_input_manager_process_key(struct sc_input_manager *im, } return; case SDLK_DOWN: - if (control && !shift) { + if (controller && !shift) { // forward repeated events action_volume_down(controller, action); } return; case SDLK_UP: - if (control && !shift) { + if (controller && !shift) { // forward repeated events action_volume_up(controller, action); } @@ -519,19 +516,19 @@ sc_input_manager_process_key(struct sc_input_manager *im, } return; case SDLK_c: - if (control && !shift && !repeat && down) { + if (controller && !shift && !repeat && down) { get_device_clipboard(controller, GET_CLIPBOARD_COPY_KEY_COPY); } return; case SDLK_x: - if (control && !shift && !repeat && down) { + if (controller && !shift && !repeat && down) { get_device_clipboard(controller, GET_CLIPBOARD_COPY_KEY_CUT); } return; case SDLK_v: - if (control && !repeat && down) { + if (controller && !repeat && down) { if (shift || im->legacy_paste) { // inject the text as input events clipboard_paste(controller); @@ -564,7 +561,7 @@ sc_input_manager_process_key(struct sc_input_manager *im, } return; case SDLK_n: - if (control && !repeat && down) { + if (controller && !repeat && down) { if (shift) { collapse_panels(controller); } else if (im->key_repeat == 0) { @@ -575,7 +572,7 @@ sc_input_manager_process_key(struct sc_input_manager *im, } return; case SDLK_r: - if (control && !shift && !repeat && down) { + if (controller && !shift && !repeat && down) { rotate_device(controller); } return; @@ -584,7 +581,7 @@ sc_input_manager_process_key(struct sc_input_manager *im, return; } - if (!control) { + if (!controller) { return; } @@ -701,7 +698,7 @@ sc_input_manager_process_touch(struct sc_input_manager *im, static void sc_input_manager_process_mouse_button(struct sc_input_manager *im, const SDL_MouseButtonEvent *event) { - bool control = im->control; + struct sc_controller *controller = im->controller; if (event->which == SDL_TOUCH_MOUSEID) { // simulated from touch events, so it's a duplicate @@ -712,24 +709,24 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im, if (!im->forward_all_clicks) { enum sc_action action = down ? SC_ACTION_DOWN : SC_ACTION_UP; - if (control && event->button == SDL_BUTTON_X1) { - action_app_switch(im->controller, action); + if (controller && event->button == SDL_BUTTON_X1) { + action_app_switch(controller, action); return; } - if (control && event->button == SDL_BUTTON_X2 && down) { + if (controller && event->button == SDL_BUTTON_X2 && down) { if (event->clicks < 2) { - expand_notification_panel(im->controller); + expand_notification_panel(controller); } else { - expand_settings_panel(im->controller); + expand_settings_panel(controller); } return; } - if (control && event->button == SDL_BUTTON_RIGHT) { - press_back_or_turn_screen_on(im->controller, action); + if (controller && event->button == SDL_BUTTON_RIGHT) { + press_back_or_turn_screen_on(controller, action); return; } - if (control && event->button == SDL_BUTTON_MIDDLE) { - action_home(im->controller, action); + if (controller && event->button == SDL_BUTTON_MIDDLE) { + action_home(controller, action); return; } @@ -751,7 +748,7 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im, // otherwise, send the click event to the device } - if (!control) { + if (!controller) { return; } @@ -865,9 +862,10 @@ sc_input_manager_process_file(struct sc_input_manager *im, void sc_input_manager_handle_event(struct sc_input_manager *im, SDL_Event *event) { + bool control = im->controller; switch (event->type) { case SDL_TEXTINPUT: - if (!im->control) { + if (!control) { break; } sc_input_manager_process_text_input(im, &event->text); @@ -879,13 +877,13 @@ sc_input_manager_handle_event(struct sc_input_manager *im, SDL_Event *event) { sc_input_manager_process_key(im, &event->key); break; case SDL_MOUSEMOTION: - if (!im->control) { + if (!control) { break; } sc_input_manager_process_mouse_motion(im, &event->motion); break; case SDL_MOUSEWHEEL: - if (!im->control) { + if (!control) { break; } sc_input_manager_process_mouse_wheel(im, &event->wheel); @@ -899,13 +897,13 @@ sc_input_manager_handle_event(struct sc_input_manager *im, SDL_Event *event) { case SDL_FINGERMOTION: case SDL_FINGERDOWN: case SDL_FINGERUP: - if (!im->control) { + if (!control) { break; } sc_input_manager_process_touch(im, &event->tfinger); break; case SDL_DROPFILE: { - if (!im->control) { + if (!control) { break; } sc_input_manager_process_file(im, &event->drop); diff --git a/app/src/input_manager.h b/app/src/input_manager.h index 377835bf..f6c210e3 100644 --- a/app/src/input_manager.h +++ b/app/src/input_manager.h @@ -22,7 +22,6 @@ struct sc_input_manager { struct sc_key_processor *kp; struct sc_mouse_processor *mp; - bool control; bool forward_all_clicks; bool legacy_paste; bool clipboard_autosync; @@ -51,7 +50,6 @@ struct sc_input_manager_params { struct sc_key_processor *kp; struct sc_mouse_processor *mp; - bool control; bool forward_all_clicks; bool legacy_paste; bool clipboard_autosync; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 5c2114eb..92521f45 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -525,6 +525,9 @@ aoa_hid_end: } + // There is a controller if and only if control is enabled + assert(options->control == !!controller); + if (options->display) { const char *window_title = options->window_title ? options->window_title : info->device_name; @@ -534,7 +537,6 @@ aoa_hid_end: .fp = fp, .kp = kp, .mp = mp, - .control = options->control, .forward_all_clicks = options->forward_all_clicks, .legacy_paste = options->legacy_paste, .clipboard_autosync = options->clipboard_autosync, diff --git a/app/src/screen.c b/app/src/screen.c index 9393a92f..52dfae04 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -489,7 +489,6 @@ sc_screen_init(struct sc_screen *screen, .screen = screen, .kp = params->kp, .mp = params->mp, - .control = params->control, .forward_all_clicks = params->forward_all_clicks, .legacy_paste = params->legacy_paste, .clipboard_autosync = params->clipboard_autosync, diff --git a/app/src/screen.h b/app/src/screen.h index 677cf514..13bd4d99 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -74,7 +74,6 @@ struct sc_screen_params { struct sc_key_processor *kp; struct sc_mouse_processor *mp; - bool control; bool forward_all_clicks; bool legacy_paste; bool clipboard_autosync; From 7e35bfe382da9f66aefdd46f0610a420a0815f2c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 23 Jan 2022 12:11:53 +0100 Subject: [PATCH 0989/2244] Refactor if-blocks Group all conditions requiring a controller in a single if-block. --- app/src/input_manager.c | 40 +++++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index c520b1aa..28c74791 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -707,27 +707,29 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im, bool down = event->type == SDL_MOUSEBUTTONDOWN; if (!im->forward_all_clicks) { - enum sc_action action = down ? SC_ACTION_DOWN : SC_ACTION_UP; + if (controller) { + enum sc_action action = down ? SC_ACTION_DOWN : SC_ACTION_UP; - if (controller && event->button == SDL_BUTTON_X1) { - action_app_switch(controller, action); - return; - } - if (controller && event->button == SDL_BUTTON_X2 && down) { - if (event->clicks < 2) { - expand_notification_panel(controller); - } else { - expand_settings_panel(controller); + if (event->button == SDL_BUTTON_X1) { + action_app_switch(controller, action); + return; + } + if (event->button == SDL_BUTTON_X2 && down) { + if (event->clicks < 2) { + expand_notification_panel(controller); + } else { + expand_settings_panel(controller); + } + return; + } + if (event->button == SDL_BUTTON_RIGHT) { + press_back_or_turn_screen_on(controller, action); + return; + } + if (event->button == SDL_BUTTON_MIDDLE) { + action_home(controller, action); + return; } - return; - } - if (controller && event->button == SDL_BUTTON_RIGHT) { - press_back_or_turn_screen_on(controller, action); - return; - } - if (controller && event->button == SDL_BUTTON_MIDDLE) { - action_home(controller, action); - return; } // double-click on black borders resize to fit the device screen From 241a587e61706321a7f8767d90aaf9876037c2eb Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 23 Jan 2022 12:31:05 +0100 Subject: [PATCH 0990/2244] Fix missing HID mouse destructor call The destructor unregisters the HID mouse, so it was not reported as a leak, but it must still be called. --- app/src/scrcpy.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 92521f45..5f4455ba 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -598,6 +598,9 @@ end: if (hid_keyboard_initialized) { sc_hid_keyboard_destroy(&s->keyboard_hid); } + if (hid_mouse_initialized) { + sc_hid_mouse_destroy(&s->mouse_hid); + } sc_aoa_stop(&s->aoa); } if (acksync) { From 308a1f8192be145242919b153577d319497cd2fd Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 21 Jan 2022 21:55:41 +0100 Subject: [PATCH 0991/2244] Simplify error handling in sc_aoa_init() Use goto to avoid many repetitions. --- app/src/aoa_hid.c | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/app/src/aoa_hid.c b/app/src/aoa_hid.c index 9de918bc..25dbcc8e 100644 --- a/app/src/aoa_hid.c +++ b/app/src/aoa_hid.c @@ -131,31 +131,22 @@ sc_aoa_init(struct sc_aoa *aoa, const char *serial, } if (!sc_cond_init(&aoa->event_cond)) { - sc_mutex_destroy(&aoa->mutex); - return false; + goto error_destroy_mutex; } if (libusb_init(&aoa->usb_context) != LIBUSB_SUCCESS) { - sc_cond_destroy(&aoa->event_cond); - sc_mutex_destroy(&aoa->mutex); - return false; + goto error_destroy_cond; } aoa->usb_device = sc_aoa_find_usb_device(serial); if (!aoa->usb_device) { LOGW("USB device of serial %s not found", serial); - libusb_exit(aoa->usb_context); - sc_mutex_destroy(&aoa->mutex); - sc_cond_destroy(&aoa->event_cond); - return false; + goto error_exit_libusb; } if (sc_aoa_open_usb_handle(aoa->usb_device, &aoa->usb_handle) < 0) { LOGW("Open USB handle failed"); - libusb_unref_device(aoa->usb_device); - libusb_exit(aoa->usb_context); - sc_cond_destroy(&aoa->event_cond); - sc_mutex_destroy(&aoa->mutex); + goto error_unref_device; return false; } @@ -163,6 +154,16 @@ sc_aoa_init(struct sc_aoa *aoa, const char *serial, aoa->acksync = acksync; return true; + +error_unref_device: + libusb_unref_device(aoa->usb_device); +error_exit_libusb: + libusb_exit(aoa->usb_context); +error_destroy_cond: + sc_cond_destroy(&aoa->event_cond); +error_destroy_mutex: + sc_mutex_destroy(&aoa->mutex); + return false; } void From d41a46dc95d13922d7b1166d8454842402e67a92 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 21 Jan 2022 21:56:15 +0100 Subject: [PATCH 0992/2244] Handle libusb_get_device_descriptor() error The function libusb_get_device_descriptor() might return an error. Handle it. --- app/src/aoa_hid.c | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/src/aoa_hid.c b/app/src/aoa_hid.c index 25dbcc8e..a9460c3b 100644 --- a/app/src/aoa_hid.c +++ b/app/src/aoa_hid.c @@ -56,14 +56,13 @@ accept_device(libusb_device *device, const char *serial) { // devices available on the computer have permission restrictions struct libusb_device_descriptor desc; - libusb_get_device_descriptor(device, &desc); - - if (!desc.iSerialNumber) { + int result = libusb_get_device_descriptor(device, &desc); + if (result < 0 || !desc.iSerialNumber) { return false; } libusb_device_handle *handle; - int result = libusb_open(device, &handle); + result = libusb_open(device, &handle); if (result < 0) { return false; } From 1f65b1bf87d835bdd96475b27c1712cd7c63b00b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 23 Jan 2022 14:48:06 +0100 Subject: [PATCH 0993/2244] Remove inline hint There is no reason to request inlining here. --- 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 52dfae04..c2dbb9f4 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -156,7 +156,7 @@ get_initial_optimal_size(struct sc_size content_size, uint16_t req_width, return window_size; } -static inline void +static void sc_screen_capture_mouse(struct sc_screen *screen, bool capture) { if (SDL_SetRelativeMouseMode(capture)) { LOGE("Could not set relative mouse mode to %s: %s", From ac038f276eb5e7dcc6fac7fa7120da23a38bc0ec Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 23 Jan 2022 14:57:12 +0100 Subject: [PATCH 0994/2244] Add missing break statement This was harmless because this is the last "case" of the switch, but for consistency, add the missing break. --- app/src/screen.c | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/screen.c b/app/src/screen.c index c2dbb9f4..cdd84367 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -873,6 +873,7 @@ sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event) { sc_screen_capture_mouse(screen, true); return; } + break; } sc_input_manager_handle_event(&screen->im, event); From 8c7f0ed5ead5dc15dc10fb1b521b7ec91523c50e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 23 Jan 2022 16:07:15 +0100 Subject: [PATCH 0995/2244] Fix warning message Make the message consistent for HID keyboard and HID mouse. --- app/src/hid_mouse.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/hid_mouse.c b/app/src/hid_mouse.c index 0e26c7c3..c24b4a4a 100644 --- a/app/src/hid_mouse.c +++ b/app/src/hid_mouse.c @@ -262,6 +262,6 @@ void sc_hid_mouse_destroy(struct sc_hid_mouse *mouse) { bool ok = sc_aoa_unregister_hid(mouse->aoa, HID_MOUSE_ACCESSORY_ID); if (!ok) { - LOGW("Could not unregister HID"); + LOGW("Could not unregister HID mouse"); } } From 17c97820b2030759e8b7e431e7d353c30bcff3dc Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 23 Jan 2022 15:12:03 +0100 Subject: [PATCH 0996/2244] Never forward capture keys In relative mode, Alt and Super are "capture keys". Never forward them to the input manager, to avoid inconsistencies between UP and DOWN events. --- app/src/screen.c | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/app/src/screen.c b/app/src/screen.c index cdd84367..af433648 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -824,14 +824,13 @@ sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event) { if (sc_screen_is_mouse_capture_key(key)) { if (!screen->mouse_capture_key_pressed) { screen->mouse_capture_key_pressed = key; - return; } else { // Another mouse capture key has been pressed, cancel // mouse (un)capture screen->mouse_capture_key_pressed = 0; - // Do not return, the event must be forwarded to the - // input manager } + // Mouse capture keys are never forwarded to the device + return; } } break; @@ -840,14 +839,16 @@ sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event) { SDL_Keycode key = event->key.keysym.sym; SDL_Keycode cap = screen->mouse_capture_key_pressed; screen->mouse_capture_key_pressed = 0; - if (key == cap) { - // A mouse capture key has been pressed then released: - // toggle the capture mouse mode - sc_screen_capture_mouse(screen, !screen->mouse_captured); + if (sc_screen_is_mouse_capture_key(key)) { + if (key == cap) { + // A mouse capture key has been pressed then released: + // toggle the capture mouse mode + sc_screen_capture_mouse(screen, + !screen->mouse_captured); + } + // Mouse capture keys are never forwarded to the device return; } - // Do not return, the event must be forwarded to the input - // manager } break; case SDL_MOUSEWHEEL: From 4bf9c057fe00c397982bc610998f7f1d1fd39017 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 23 Jan 2022 21:23:31 +0100 Subject: [PATCH 0997/2244] Extract relative mode check to an inline function This will allow to reuse the condition in another function. --- app/src/screen.c | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/src/screen.c b/app/src/screen.c index af433648..31c63092 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -156,6 +156,12 @@ get_initial_optimal_size(struct sc_size content_size, uint16_t req_width, return window_size; } +static inline bool +sc_screen_is_relative_mode(struct sc_screen *screen) { + // screen->im.mp may be NULL if --no-control + return screen->im.mp && screen->im.mp->relative_mode; +} + static void sc_screen_capture_mouse(struct sc_screen *screen, bool capture) { if (SDL_SetRelativeMouseMode(capture)) { @@ -772,8 +778,7 @@ sc_screen_is_mouse_capture_key(SDL_Keycode key) { void sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event) { - // screen->im.mp may be NULL if --no-control - bool relative_mode = screen->im.mp && screen->im.mp->relative_mode; + bool relative_mode = sc_screen_is_relative_mode(screen); switch (event->type) { case EVENT_NEW_FRAME: { From 063d103dd6f959f6f1619f20e4e964a042c700ad Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 23 Jan 2022 21:30:30 +0100 Subject: [PATCH 0998/2244] Capture mouse on start for --hid-mouse If relative mode is enabled, capture the mouse immediately. --- app/src/screen.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/screen.c b/app/src/screen.c index 31c63092..c62920bc 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -707,6 +707,11 @@ sc_screen_update_frame(struct sc_screen *screen) { screen->has_frame = true; // this is the very first frame, show the window sc_screen_show_initial_window(screen); + + if (sc_screen_is_relative_mode(screen)) { + // Capture mouse on start + sc_screen_capture_mouse(screen, true); + } } sc_screen_render(screen, false); From a9429efa34b3cbdc57847cb37d3621b889522c7f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 23 Jan 2022 21:46:57 +0100 Subject: [PATCH 0999/2244] Fix downsize on error before first frame Retry with a lower definition if MediaCodec fails before the first frame, not the first packet. In practice, the first packet is a config packet without any frame, and MediaCodec might fail just after. Refs 2eb6fe7d81c15912a2252f229d317ae03f618a33 Refs #2963 --- .../src/main/java/com/genymobile/scrcpy/ScreenEncoder.java | 5 ++++- 1 file changed, 4 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 e4e87c72..4c23dd92 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -159,7 +159,10 @@ public class ScreenEncoder implements Device.RotationListener { } IO.writeFully(fd, codecBuffer); - firstFrameSent = true; + if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) == 0) { + // If this is not a config packet, then it contains a frame + firstFrameSent = true; + } } } finally { if (outputBufferId >= 0) { From 1ff69e21c28ad69f6a39219c5da4c5f77dbaf9f6 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 24 Jan 2022 08:54:09 +0100 Subject: [PATCH 1000/2244] Update error messages in FAQ With the recent versions, scrcpy first executes "adb get-serialno", so the adb error messages are not exactly the same. --- FAQ.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/FAQ.md b/FAQ.md index 399f3504..edcea3b0 100644 --- a/FAQ.md +++ b/FAQ.md @@ -12,7 +12,7 @@ Here are the common reported problems and their status. In that case, it will print this error: -> ERROR: "adb push" returned with value 1 +> ERROR: "adb get-serialno" returned with value 1 This is typically not a bug in _scrcpy_, but a problem in your environment. @@ -39,7 +39,7 @@ Check [stackoverflow][device-unauthorized]. ### Device not detected -> adb: error: failed to get feature set: no devices/emulators found +> error: no devices/emulators found Check that you correctly enabled [adb debugging][enable-adb]. @@ -54,7 +54,7 @@ If your device is not detected, you may need some [drivers] (on Windows). There If several devices are connected, you will encounter this error: -> adb: error: failed to get feature set: more than one device/emulator +> error: more than one device/emulator the identifier of the device you want to mirror must be provided: From ae8fdda09ec5730bf84e80261b7e09fe44680b8d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 24 Jan 2022 08:55:27 +0100 Subject: [PATCH 1001/2244] Improve FAQ explanations --- FAQ.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/FAQ.md b/FAQ.md index edcea3b0..270fcaf3 100644 --- a/FAQ.md +++ b/FAQ.md @@ -32,7 +32,16 @@ in the release, so it should work out-of-the-box. ### Device unauthorized -Check [stackoverflow][device-unauthorized]. + +> error: device unauthorized. +> This adb server's $ADB_VENDOR_KEYS is not set +> Try 'adb kill-server' if that seems wrong. +> Otherwise check for a confirmation dialog on your device. + +When connecting, a popup should open on the device. You must authorize USB +debugging. + +If it does not open, check [stackoverflow][device-unauthorized]. [device-unauthorized]: https://stackoverflow.com/questions/23081263/adb-android-device-unauthorized @@ -62,7 +71,7 @@ the identifier of the device you want to mirror must be provided: scrcpy -s 01234567890abcdef ``` -Note that if your device is connected over TCP/IP, you'll get this message: +Note that if your device is connected over TCP/IP, you might get this message: > adb: error: more than one device/emulator > ERROR: "adb reverse" returned with value 1 From e0bce1725bdb6b5f114051b9611d90f57050a77c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 24 Jan 2022 21:37:40 +0100 Subject: [PATCH 1002/2244] Fix header guard prefix --- app/src/hid_mouse.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/hid_mouse.h b/app/src/hid_mouse.h index 2819b1ff..b89f7795 100644 --- a/app/src/hid_mouse.h +++ b/app/src/hid_mouse.h @@ -1,5 +1,5 @@ -#ifndef HID_MOUSE_H -#define HID_MOUSE_H +#ifndef SC_HID_MOUSE_H +#define SC_HID_MOUSE_H #include "common.h" From 5d6076bffd9ffe03ce7299f7f3459f6d018fb8c7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 24 Jan 2022 21:38:30 +0100 Subject: [PATCH 1003/2244] Move misplaced break statements With ifdefs, the resulting code could contain both a return statement and a break. --- app/src/cli.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index 69177aef..21d13b44 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -1320,12 +1320,12 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], case 'K': #ifdef HAVE_AOA_HID opts->keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_HID; + break; #else LOGE("HID over AOA (-K/--hid-keyboard) is not supported on " "this platform. It is only available on Linux."); return false; #endif - break; case OPT_MAX_FPS: if (!parse_max_fps(optarg, &opts->max_fps)) { return false; @@ -1339,12 +1339,12 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], case 'M': #ifdef HAVE_AOA_HID opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_HID; + break; #else LOGE("HID over AOA (-M/--hid-mouse) is not supported on this" "platform. It is only available on Linux."); return false; #endif - break; case OPT_LOCK_VIDEO_ORIENTATION: if (!parse_lock_video_orientation(optarg, &opts->lock_video_orientation)) { @@ -1503,21 +1503,21 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], case OPT_V4L2_SINK: #ifdef HAVE_V4L2 opts->v4l2_device = optarg; + break; #else LOGE("V4L2 (--v4l2-sink) is only available on Linux."); return false; #endif - break; case OPT_V4L2_BUFFER: #ifdef HAVE_V4L2 if (!parse_buffering_time(optarg, &opts->v4l2_buffer)) { return false; } + break; #else LOGE("V4L2 (--v4l2-buffer) is only available on Linux."); return false; #endif - break; default: // getopt prints the error message on stderr return false; From ca516f4318100e93d8fa4a88e05f004bafd1dc91 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 24 Jan 2022 21:44:28 +0100 Subject: [PATCH 1004/2244] Refactor if-block in cli Several tests must be performed if opts->control is false. --- app/src/cli.c | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index 21d13b44..57e02df8 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -1591,14 +1591,15 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } } - if (!opts->control && opts->turn_screen_off) { - LOGE("Could not request to turn screen off if control is disabled"); - return false; - } - - if (!opts->control && opts->stay_awake) { - LOGE("Could not request to stay awake if control is disabled"); - return false; + if (!opts->control) { + if (opts->turn_screen_off) { + LOGE("Could not request to turn screen off if control is disabled"); + return false; + } + if (opts->stay_awake) { + LOGE("Could not request to stay awake if control is disabled"); + return false; + } } return true; From f289d206ea0e10cc052137781c39da862ebe2f73 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 24 Jan 2022 21:45:44 +0100 Subject: [PATCH 1005/2244] Disable more actions if --no-control If control is disabled, then do not enable "show touches" or automatically power off the device on close. --- app/src/cli.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/src/cli.c b/app/src/cli.c index 57e02df8..60492730 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -1600,6 +1600,14 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], LOGE("Could not request to stay awake if control is disabled"); return false; } + if (opts->show_touches) { + LOGE("Could not request to show touches if control is disabled"); + return false; + } + if (opts->power_off_on_close) { + LOGE("Could not request power off on close if control is disabled"); + return false; + } } return true; From 31a5d0c2bfab4d8f74b116d634f63a0f3a79889a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 26 Jan 2022 10:23:09 +0100 Subject: [PATCH 1006/2244] Move call to send device name and size This will allow to optionally disable it. PR #2971 --- .../java/com/genymobile/scrcpy/DesktopConnection.java | 9 +++------ server/src/main/java/com/genymobile/scrcpy/Server.java | 4 +++- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java b/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java index 40cb088c..ca20c1d8 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java +++ b/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java @@ -46,7 +46,7 @@ public final class DesktopConnection implements Closeable { return localSocket; } - public static DesktopConnection open(Device device, boolean tunnelForward, boolean control) throws IOException { + public static DesktopConnection open(boolean tunnelForward, boolean control) throws IOException { LocalSocket videoSocket; LocalSocket controlSocket = null; if (tunnelForward) { @@ -78,10 +78,7 @@ public final class DesktopConnection implements Closeable { } } - DesktopConnection connection = new DesktopConnection(videoSocket, controlSocket); - Size videoSize = device.getScreenInfo().getVideoSize(); - connection.send(Device.getDeviceName(), videoSize.getWidth(), videoSize.getHeight()); - return connection; + return new DesktopConnection(videoSocket, controlSocket); } public void close() throws IOException { @@ -95,7 +92,7 @@ public final class DesktopConnection implements Closeable { } } - private void send(String deviceName, int width, int height) throws IOException { + public void sendDeviceMeta(String deviceName, int width, int height) throws IOException { byte[] buffer = new byte[DEVICE_NAME_FIELD_LENGTH + 4]; byte[] deviceNameBytes = deviceName.getBytes(StandardCharsets.UTF_8); diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 9d7a62e3..6dc0b2c6 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -67,7 +67,9 @@ public final class Server { boolean tunnelForward = options.isTunnelForward(); boolean control = options.getControl(); - try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward, control)) { + try (DesktopConnection connection = DesktopConnection.open(tunnelForward, control)) { + Size videoSize = device.getScreenInfo().getVideoSize(); + connection.sendDeviceMeta(Device.getDeviceName(), videoSize.getWidth(), videoSize.getHeight()); ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps(), codecOptions, options.getEncoderName(), options.getDownsizeOnError()); From 6b21f4ae13e0644632d2bef4f14ff590b1fa5e18 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 26 Jan 2022 10:29:06 +0100 Subject: [PATCH 1007/2244] Reorder scrcpy-server options Move the options unused by the scrcpy client at the end. These options may be useful to use scrcpy-server directly (to get a raw H.264 stream for example). PR #2971 --- .../java/com/genymobile/scrcpy/Options.java | 20 ++++++++++--------- .../java/com/genymobile/scrcpy/Server.java | 8 ++++---- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index ef7d8572..236a93d7 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -12,7 +12,6 @@ public class Options { private int lockVideoOrientation = -1; private boolean tunnelForward; private Rect crop; - private boolean sendFrameMeta = true; // send PTS so that the client may record properly private boolean control = true; private int displayId; private boolean showTouches; @@ -23,6 +22,9 @@ public class Options { private boolean clipboardAutosync = true; private boolean downsizeOnError = true; + // Options not used by the scrcpy client, but useful to use scrcpy-server directly + private boolean sendFrameMeta = true; // send PTS so that the client may record properly + public Ln.Level getLogLevel() { return logLevel; } @@ -79,14 +81,6 @@ public class Options { this.crop = crop; } - public boolean getSendFrameMeta() { - return sendFrameMeta; - } - - public void setSendFrameMeta(boolean sendFrameMeta) { - this.sendFrameMeta = sendFrameMeta; - } - public boolean getControl() { return control; } @@ -158,4 +152,12 @@ public class Options { public void setDownsizeOnError(boolean downsizeOnError) { this.downsizeOnError = downsizeOnError; } + + public boolean getSendFrameMeta() { + return sendFrameMeta; + } + + public void setSendFrameMeta(boolean sendFrameMeta) { + this.sendFrameMeta = sendFrameMeta; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 6dc0b2c6..e690a45b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -201,10 +201,6 @@ public final class Server { Rect crop = parseCrop(value); options.setCrop(crop); break; - case "send_frame_meta": - boolean sendFrameMeta = Boolean.parseBoolean(value); - options.setSendFrameMeta(sendFrameMeta); - break; case "control": boolean control = Boolean.parseBoolean(value); options.setControl(control); @@ -242,6 +238,10 @@ public final class Server { boolean downsizeOnError = Boolean.parseBoolean(value); options.setDownsizeOnError(downsizeOnError); break; + case "send_frame_meta": + boolean sendFrameMeta = Boolean.parseBoolean(value); + options.setSendFrameMeta(sendFrameMeta); + break; default: Ln.w("Unknown server option: " + key); break; From 3ba32c2a0d57b708980df121caad84252f720699 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 26 Jan 2022 10:24:41 +0100 Subject: [PATCH 1008/2244] Add server option send_device_meta Similar to send_device_frame, this option allows to disable sending the device name and size on start. This is only useful when using the scrcpy-server alone to get a raw H.264 stream, without using the scrcpy client. PR #2971 --- .../src/main/java/com/genymobile/scrcpy/Options.java | 9 +++++++++ server/src/main/java/com/genymobile/scrcpy/Server.java | 10 ++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 236a93d7..9210b318 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -23,6 +23,7 @@ public class Options { private boolean downsizeOnError = true; // Options not used by the scrcpy client, but useful to use scrcpy-server directly + private boolean sendDeviceMeta = true; // send device name and size private boolean sendFrameMeta = true; // send PTS so that the client may record properly public Ln.Level getLogLevel() { @@ -153,6 +154,14 @@ public class Options { this.downsizeOnError = downsizeOnError; } + public boolean getSendDeviceMeta() { + return sendDeviceMeta; + } + + public void setSendDeviceMeta(boolean sendDeviceMeta) { + this.sendDeviceMeta = sendDeviceMeta; + } + public boolean getSendFrameMeta() { return sendFrameMeta; } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index e690a45b..20fd1c1b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -68,8 +68,10 @@ public final class Server { boolean control = options.getControl(); try (DesktopConnection connection = DesktopConnection.open(tunnelForward, control)) { - Size videoSize = device.getScreenInfo().getVideoSize(); - connection.sendDeviceMeta(Device.getDeviceName(), videoSize.getWidth(), videoSize.getHeight()); + if (options.getSendDeviceMeta()) { + Size videoSize = device.getScreenInfo().getVideoSize(); + connection.sendDeviceMeta(Device.getDeviceName(), videoSize.getWidth(), videoSize.getHeight()); + } ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps(), codecOptions, options.getEncoderName(), options.getDownsizeOnError()); @@ -238,6 +240,10 @@ public final class Server { boolean downsizeOnError = Boolean.parseBoolean(value); options.setDownsizeOnError(downsizeOnError); break; + case "send_device_meta": + boolean sendDeviceMeta = Boolean.parseBoolean(value); + options.setSendDeviceMeta(sendDeviceMeta); + break; case "send_frame_meta": boolean sendFrameMeta = Boolean.parseBoolean(value); options.setSendFrameMeta(sendFrameMeta); From 45a5e560dfeabed8b32482caad857424ff472518 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 26 Jan 2022 10:36:18 +0100 Subject: [PATCH 1009/2244] Add server option send_dummy_byte If set to false, no dummy byte is written to detect a connection error. PR #2971 --- .../java/com/genymobile/scrcpy/DesktopConnection.java | 8 +++++--- server/src/main/java/com/genymobile/scrcpy/Options.java | 9 +++++++++ server/src/main/java/com/genymobile/scrcpy/Server.java | 7 ++++++- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java b/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java index ca20c1d8..78728d81 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java +++ b/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java @@ -46,15 +46,17 @@ public final class DesktopConnection implements Closeable { return localSocket; } - public static DesktopConnection open(boolean tunnelForward, boolean control) throws IOException { + public static DesktopConnection open(boolean tunnelForward, boolean control, boolean sendDummyByte) throws IOException { LocalSocket videoSocket; LocalSocket controlSocket = null; if (tunnelForward) { LocalServerSocket localServerSocket = new LocalServerSocket(SOCKET_NAME); try { videoSocket = localServerSocket.accept(); - // send one byte so the client may read() to detect a connection error - videoSocket.getOutputStream().write(0); + if (sendDummyByte) { + // send one byte so the client may read() to detect a connection error + videoSocket.getOutputStream().write(0); + } if (control) { try { controlSocket = localServerSocket.accept(); diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 9210b318..92cf1e7a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -25,6 +25,7 @@ public class Options { // Options not used by the scrcpy client, but useful to use scrcpy-server directly private boolean sendDeviceMeta = true; // send device name and size private boolean sendFrameMeta = true; // send PTS so that the client may record properly + private boolean sendDummyByte = true; // write a byte on start to detect connection issues public Ln.Level getLogLevel() { return logLevel; @@ -169,4 +170,12 @@ public class Options { public void setSendFrameMeta(boolean sendFrameMeta) { this.sendFrameMeta = sendFrameMeta; } + + public boolean getSendDummyByte() { + return sendDummyByte; + } + + public void setSendDummyByte(boolean sendDummyByte) { + this.sendDummyByte = sendDummyByte; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 20fd1c1b..d9c22771 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -66,8 +66,9 @@ public final class Server { boolean tunnelForward = options.isTunnelForward(); boolean control = options.getControl(); + boolean sendDummyByte = options.getSendDummyByte(); - try (DesktopConnection connection = DesktopConnection.open(tunnelForward, control)) { + try (DesktopConnection connection = DesktopConnection.open(tunnelForward, control, sendDummyByte)) { if (options.getSendDeviceMeta()) { Size videoSize = device.getScreenInfo().getVideoSize(); connection.sendDeviceMeta(Device.getDeviceName(), videoSize.getWidth(), videoSize.getHeight()); @@ -248,6 +249,10 @@ public final class Server { boolean sendFrameMeta = Boolean.parseBoolean(value); options.setSendFrameMeta(sendFrameMeta); break; + case "send_dummy_byte": + boolean sendDummyByte = Boolean.parseBoolean(value); + options.setSendDummyByte(sendDummyByte); + break; default: Ln.w("Unknown server option: " + key); break; From 2faf9715bebe720ed75ce98f5f8c2b2b1532880a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 26 Jan 2022 10:47:08 +0100 Subject: [PATCH 1010/2244] Add server option raw_video_stream For convenience, this new option forces the 3 following options: - send_device_meta=false - send_frame_meta=false - send_dummy_byte=false This allows to send a raw H.264 stream on the video socket. Concretely: adb push scrcpy-server /data/local/tmp/scrcpy-server.jar adb forward tcp:1234 localabstract:scrcpy adb shell CLASSPATH=/data/local/tmp/scrcpy-server.jar \ app_process / com.genymobile.scrcpy.Server 1.21 \ raw_video_stream=true tunnel_forward=true control=false As soon as a client connects via TCP to localhost:1234, it will receive the raw H.264 stream. Refs #1419 comment PR #2971 --- server/src/main/java/com/genymobile/scrcpy/Server.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index d9c22771..68c6c02c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -253,6 +253,14 @@ public final class Server { boolean sendDummyByte = Boolean.parseBoolean(value); options.setSendDummyByte(sendDummyByte); break; + case "raw_video_stream": + boolean rawVideoStream = Boolean.parseBoolean(value); + if (rawVideoStream) { + options.setSendDeviceMeta(false); + options.setSendFrameMeta(false); + options.setSendDummyByte(false); + } + break; default: Ln.w("Unknown server option: " + key); break; From 9d2e00697e5b6c27def3791e55cee67b8dd395b5 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 26 Jan 2022 21:31:30 +0100 Subject: [PATCH 1011/2244] Use sc_ prefix for control_msg enums Refs afa4a1b728d6f130acff23c0e3fb4609bcb8a6b2 --- app/src/control_msg.c | 57 ++++++------ app/src/control_msg.h | 48 +++++----- app/src/controller.c | 2 +- app/src/input_manager.c | 38 ++++---- app/src/keyboard_inject.c | 4 +- app/src/mouse_inject.c | 8 +- app/src/scrcpy.c | 4 +- app/tests/test_control_msg_serialize.c | 124 ++++++++++++------------- 8 files changed, 141 insertions(+), 144 deletions(-) diff --git a/app/src/control_msg.c b/app/src/control_msg.c index d093183a..e8ae4681 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -92,19 +92,19 @@ size_t sc_control_msg_serialize(const struct sc_control_msg *msg, unsigned char *buf) { buf[0] = msg->type; switch (msg->type) { - case CONTROL_MSG_TYPE_INJECT_KEYCODE: + case SC_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.repeat); buffer_write32be(&buf[10], msg->inject_keycode.metastate); return 14; - case CONTROL_MSG_TYPE_INJECT_TEXT: { + case SC_CONTROL_MSG_TYPE_INJECT_TEXT: { size_t len = write_string(msg->inject_text.text, - CONTROL_MSG_INJECT_TEXT_MAX_LENGTH, &buf[1]); + SC_CONTROL_MSG_INJECT_TEXT_MAX_LENGTH, &buf[1]); return 1 + len; } - case CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT: + case SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT: buf[1] = msg->inject_touch_event.action; buffer_write64be(&buf[2], msg->inject_touch_event.pointer_id); write_position(&buf[10], &msg->inject_touch_event.position); @@ -113,7 +113,7 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, unsigned char *buf) { buffer_write16be(&buf[22], pressure); buffer_write32be(&buf[24], msg->inject_touch_event.buttons); return 28; - case CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT: + case SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT: write_position(&buf[1], &msg->inject_scroll_event.position); buffer_write32be(&buf[13], (uint32_t) msg->inject_scroll_event.hscroll); @@ -121,27 +121,26 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, unsigned char *buf) { (uint32_t) msg->inject_scroll_event.vscroll); buffer_write32be(&buf[21], msg->inject_scroll_event.buttons); return 25; - case CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON: + case SC_CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON: buf[1] = msg->inject_keycode.action; return 2; - case CONTROL_MSG_TYPE_GET_CLIPBOARD: + case SC_CONTROL_MSG_TYPE_GET_CLIPBOARD: buf[1] = msg->get_clipboard.copy_key; return 2; - case CONTROL_MSG_TYPE_SET_CLIPBOARD: { + case SC_CONTROL_MSG_TYPE_SET_CLIPBOARD: buffer_write64be(&buf[1], msg->set_clipboard.sequence); buf[9] = !!msg->set_clipboard.paste; size_t len = write_string(msg->set_clipboard.text, - CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH, + SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH, &buf[10]); return 10 + len; - } - case CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE: + case SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE: buf[1] = msg->set_screen_power_mode.mode; return 2; - case CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL: - case CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL: - case CONTROL_MSG_TYPE_COLLAPSE_PANELS: - case CONTROL_MSG_TYPE_ROTATE_DEVICE: + case SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL: + case SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL: + case SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS: + case SC_CONTROL_MSG_TYPE_ROTATE_DEVICE: // no additional data return 1; default: @@ -154,17 +153,17 @@ void sc_control_msg_log(const struct sc_control_msg *msg) { #define LOG_CMSG(fmt, ...) LOGV("input: " fmt, ## __VA_ARGS__) switch (msg->type) { - case CONTROL_MSG_TYPE_INJECT_KEYCODE: + case SC_CONTROL_MSG_TYPE_INJECT_KEYCODE: LOG_CMSG("key %-4s code=%d repeat=%" PRIu32 " meta=%06lx", KEYEVENT_ACTION_LABEL(msg->inject_keycode.action), (int) msg->inject_keycode.keycode, msg->inject_keycode.repeat, (long) msg->inject_keycode.metastate); break; - case CONTROL_MSG_TYPE_INJECT_TEXT: + case SC_CONTROL_MSG_TYPE_INJECT_TEXT: LOG_CMSG("text \"%s\"", msg->inject_text.text); break; - case CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT: { + case SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT: { int action = msg->inject_touch_event.action & AMOTION_EVENT_ACTION_MASK; uint64_t id = msg->inject_touch_event.pointer_id; @@ -191,7 +190,7 @@ sc_control_msg_log(const struct sc_control_msg *msg) { } break; } - case CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT: + case SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT: LOG_CMSG("scroll position=%" PRIi32 ",%" PRIi32 " hscroll=%" PRIi32 " vscroll=%" PRIi32 " buttons=%06lx", msg->inject_scroll_event.position.point.x, @@ -200,34 +199,34 @@ sc_control_msg_log(const struct sc_control_msg *msg) { msg->inject_scroll_event.vscroll, (long) msg->inject_scroll_event.buttons); break; - case CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON: + case SC_CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON: LOG_CMSG("back-or-screen-on %s", KEYEVENT_ACTION_LABEL(msg->inject_keycode.action)); break; - case CONTROL_MSG_TYPE_GET_CLIPBOARD: + case SC_CONTROL_MSG_TYPE_GET_CLIPBOARD: LOG_CMSG("get clipboard copy_key=%s", copy_key_labels[msg->get_clipboard.copy_key]); break; - case CONTROL_MSG_TYPE_SET_CLIPBOARD: + case SC_CONTROL_MSG_TYPE_SET_CLIPBOARD: LOG_CMSG("clipboard %" PRIu64_ " %s \"%s\"", msg->set_clipboard.sequence, msg->set_clipboard.paste ? "paste" : "nopaste", msg->set_clipboard.text); break; - case CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE: + case SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE: LOG_CMSG("power mode %s", SCREEN_POWER_MODE_LABEL(msg->set_screen_power_mode.mode)); break; - case CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL: + case SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL: LOG_CMSG("expand notification panel"); break; - case CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL: + case SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL: LOG_CMSG("expand settings panel"); break; - case CONTROL_MSG_TYPE_COLLAPSE_PANELS: + case SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS: LOG_CMSG("collapse panels"); break; - case CONTROL_MSG_TYPE_ROTATE_DEVICE: + case SC_CONTROL_MSG_TYPE_ROTATE_DEVICE: LOG_CMSG("rotate device"); break; default: @@ -239,10 +238,10 @@ sc_control_msg_log(const struct sc_control_msg *msg) { void sc_control_msg_destroy(struct sc_control_msg *msg) { switch (msg->type) { - case CONTROL_MSG_TYPE_INJECT_TEXT: + case SC_CONTROL_MSG_TYPE_INJECT_TEXT: free(msg->inject_text.text); break; - case CONTROL_MSG_TYPE_SET_CLIPBOARD: + case SC_CONTROL_MSG_TYPE_SET_CLIPBOARD: free(msg->set_clipboard.text); break; default: diff --git a/app/src/control_msg.h b/app/src/control_msg.h index 2ac96064..fbe203d4 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -11,40 +11,40 @@ #include "android/keycodes.h" #include "coords.h" -#define CONTROL_MSG_MAX_SIZE (1 << 18) // 256k +#define SC_CONTROL_MSG_MAX_SIZE (1 << 18) // 256k -#define CONTROL_MSG_INJECT_TEXT_MAX_LENGTH 300 +#define SC_CONTROL_MSG_INJECT_TEXT_MAX_LENGTH 300 // type: 1 byte; sequence: 8 bytes; paste flag: 1 byte; length: 4 bytes -#define CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH (CONTROL_MSG_MAX_SIZE - 14) +#define SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH (SC_CONTROL_MSG_MAX_SIZE - 14) #define POINTER_ID_MOUSE UINT64_C(-1) #define POINTER_ID_VIRTUAL_FINGER UINT64_C(-2) enum sc_control_msg_type { - CONTROL_MSG_TYPE_INJECT_KEYCODE, - CONTROL_MSG_TYPE_INJECT_TEXT, - CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, - CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT, - CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON, - CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL, - CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL, - CONTROL_MSG_TYPE_COLLAPSE_PANELS, - CONTROL_MSG_TYPE_GET_CLIPBOARD, - CONTROL_MSG_TYPE_SET_CLIPBOARD, - CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE, - CONTROL_MSG_TYPE_ROTATE_DEVICE, + SC_CONTROL_MSG_TYPE_INJECT_KEYCODE, + SC_CONTROL_MSG_TYPE_INJECT_TEXT, + SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, + SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT, + SC_CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON, + SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL, + SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL, + SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS, + SC_CONTROL_MSG_TYPE_GET_CLIPBOARD, + SC_CONTROL_MSG_TYPE_SET_CLIPBOARD, + SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE, + SC_CONTROL_MSG_TYPE_ROTATE_DEVICE, }; -enum screen_power_mode { +enum sc_screen_power_mode { // see - SCREEN_POWER_MODE_OFF = 0, - SCREEN_POWER_MODE_NORMAL = 2, + SC_SCREEN_POWER_MODE_OFF = 0, + SC_SCREEN_POWER_MODE_NORMAL = 2, }; -enum get_clipboard_copy_key { - GET_CLIPBOARD_COPY_KEY_NONE, - GET_CLIPBOARD_COPY_KEY_COPY, - GET_CLIPBOARD_COPY_KEY_CUT, +enum sc_copy_key { + SC_COPY_KEY_NONE, + SC_COPY_KEY_COPY, + SC_COPY_KEY_CUT, }; struct sc_control_msg { @@ -77,7 +77,7 @@ struct sc_control_msg { // screen may only be turned on on ACTION_DOWN } back_or_screen_on; struct { - enum get_clipboard_copy_key copy_key; + enum sc_copy_key copy_key; } get_clipboard; struct { uint64_t sequence; @@ -85,7 +85,7 @@ struct sc_control_msg { bool paste; } set_clipboard; struct { - enum screen_power_mode mode; + enum sc_screen_power_mode mode; } set_screen_power_mode; }; }; diff --git a/app/src/controller.c b/app/src/controller.c index 9135d967..626a5e30 100644 --- a/app/src/controller.c +++ b/app/src/controller.c @@ -66,7 +66,7 @@ sc_controller_push_msg(struct sc_controller *controller, static bool process_msg(struct sc_controller *controller, const struct sc_control_msg *msg) { - static unsigned char serialized_msg[CONTROL_MSG_MAX_SIZE]; + static unsigned char serialized_msg[SC_CONTROL_MSG_MAX_SIZE]; size_t length = sc_control_msg_serialize(msg, serialized_msg); if (!length) { return false; diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 28c74791..d623c5ae 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -161,7 +161,7 @@ send_keycode(struct sc_controller *controller, enum android_keycode keycode, enum sc_action action, const char *name) { // send DOWN event struct sc_control_msg msg; - msg.type = CONTROL_MSG_TYPE_INJECT_KEYCODE; + msg.type = SC_CONTROL_MSG_TYPE_INJECT_KEYCODE; msg.inject_keycode.action = action == SC_ACTION_DOWN ? AKEY_EVENT_ACTION_DOWN : AKEY_EVENT_ACTION_UP; @@ -215,7 +215,7 @@ static void press_back_or_turn_screen_on(struct sc_controller *controller, enum sc_action action) { struct sc_control_msg msg; - msg.type = CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON; + msg.type = SC_CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON; msg.back_or_screen_on.action = action == SC_ACTION_DOWN ? AKEY_EVENT_ACTION_DOWN : AKEY_EVENT_ACTION_UP; @@ -228,7 +228,7 @@ press_back_or_turn_screen_on(struct sc_controller *controller, static void expand_notification_panel(struct sc_controller *controller) { struct sc_control_msg msg; - msg.type = CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL; + msg.type = SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL; if (!sc_controller_push_msg(controller, &msg)) { LOGW("Could not request 'expand notification panel'"); @@ -238,7 +238,7 @@ expand_notification_panel(struct sc_controller *controller) { static void expand_settings_panel(struct sc_controller *controller) { struct sc_control_msg msg; - msg.type = CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL; + msg.type = SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL; if (!sc_controller_push_msg(controller, &msg)) { LOGW("Could not request 'expand settings panel'"); @@ -248,7 +248,7 @@ expand_settings_panel(struct sc_controller *controller) { static void collapse_panels(struct sc_controller *controller) { struct sc_control_msg msg; - msg.type = CONTROL_MSG_TYPE_COLLAPSE_PANELS; + msg.type = SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS; if (!sc_controller_push_msg(controller, &msg)) { LOGW("Could not request 'collapse notification panel'"); @@ -257,9 +257,9 @@ collapse_panels(struct sc_controller *controller) { static bool get_device_clipboard(struct sc_controller *controller, - enum get_clipboard_copy_key copy_key) { + enum sc_copy_key copy_key) { struct sc_control_msg msg; - msg.type = CONTROL_MSG_TYPE_GET_CLIPBOARD; + msg.type = SC_CONTROL_MSG_TYPE_GET_CLIPBOARD; msg.get_clipboard.copy_key = copy_key; if (!sc_controller_push_msg(controller, &msg)) { @@ -287,7 +287,7 @@ set_device_clipboard(struct sc_controller *controller, bool paste, } struct sc_control_msg msg; - msg.type = CONTROL_MSG_TYPE_SET_CLIPBOARD; + msg.type = SC_CONTROL_MSG_TYPE_SET_CLIPBOARD; msg.set_clipboard.sequence = sequence; msg.set_clipboard.text = text_dup; msg.set_clipboard.paste = paste; @@ -303,9 +303,9 @@ set_device_clipboard(struct sc_controller *controller, bool paste, static void set_screen_power_mode(struct sc_controller *controller, - enum screen_power_mode mode) { + enum sc_screen_power_mode mode) { struct sc_control_msg msg; - msg.type = CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE; + msg.type = SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE; msg.set_screen_power_mode.mode = mode; if (!sc_controller_push_msg(controller, &msg)) { @@ -350,7 +350,7 @@ clipboard_paste(struct sc_controller *controller) { } struct sc_control_msg msg; - msg.type = CONTROL_MSG_TYPE_INJECT_TEXT; + msg.type = SC_CONTROL_MSG_TYPE_INJECT_TEXT; msg.inject_text.text = text_dup; if (!sc_controller_push_msg(controller, &msg)) { free(text_dup); @@ -361,7 +361,7 @@ clipboard_paste(struct sc_controller *controller) { static void rotate_device(struct sc_controller *controller) { struct sc_control_msg msg; - msg.type = CONTROL_MSG_TYPE_ROTATE_DEVICE; + msg.type = SC_CONTROL_MSG_TYPE_ROTATE_DEVICE; if (!sc_controller_push_msg(controller, &msg)) { LOGW("Could not request device rotation"); @@ -407,7 +407,7 @@ simulate_virtual_finger(struct sc_input_manager *im, bool up = action == AMOTION_EVENT_ACTION_UP; struct sc_control_msg msg; - msg.type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT; + msg.type = SC_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; @@ -487,9 +487,9 @@ sc_input_manager_process_key(struct sc_input_manager *im, return; case SDLK_o: if (controller && !repeat && down) { - enum screen_power_mode mode = shift - ? SCREEN_POWER_MODE_NORMAL - : SCREEN_POWER_MODE_OFF; + enum sc_screen_power_mode mode = shift + ? SC_SCREEN_POWER_MODE_NORMAL + : SC_SCREEN_POWER_MODE_OFF; set_screen_power_mode(controller, mode); } return; @@ -517,14 +517,12 @@ sc_input_manager_process_key(struct sc_input_manager *im, return; case SDLK_c: if (controller && !shift && !repeat && down) { - get_device_clipboard(controller, - GET_CLIPBOARD_COPY_KEY_COPY); + get_device_clipboard(controller, SC_COPY_KEY_COPY); } return; case SDLK_x: if (controller && !shift && !repeat && down) { - get_device_clipboard(controller, - GET_CLIPBOARD_COPY_KEY_CUT); + get_device_clipboard(controller, SC_COPY_KEY_CUT); } return; case SDLK_v: diff --git a/app/src/keyboard_inject.c b/app/src/keyboard_inject.c index 7276d325..fe297310 100644 --- a/app/src/keyboard_inject.c +++ b/app/src/keyboard_inject.c @@ -248,7 +248,7 @@ convert_meta_state(uint16_t mod) { static bool convert_input_key(const struct sc_key_event *event, struct sc_control_msg *msg, enum sc_key_inject_mode key_inject_mode, uint32_t repeat) { - msg->type = CONTROL_MSG_TYPE_INJECT_KEYCODE; + msg->type = SC_CONTROL_MSG_TYPE_INJECT_KEYCODE; if (!convert_keycode(event->keycode, &msg->inject_keycode.keycode, event->mods_state, key_inject_mode)) { @@ -310,7 +310,7 @@ sc_key_processor_process_text(struct sc_key_processor *kp, } struct sc_control_msg msg; - msg.type = CONTROL_MSG_TYPE_INJECT_TEXT; + msg.type = SC_CONTROL_MSG_TYPE_INJECT_TEXT; msg.inject_text.text = strdup(event->text); if (!msg.inject_text.text) { LOGW("Could not strdup input text"); diff --git a/app/src/mouse_inject.c b/app/src/mouse_inject.c index 855aaa9f..2e89de9a 100644 --- a/app/src/mouse_inject.c +++ b/app/src/mouse_inject.c @@ -66,7 +66,7 @@ sc_mouse_processor_process_mouse_motion(struct sc_mouse_processor *mp, struct sc_mouse_inject *mi = DOWNCAST(mp); struct sc_control_msg msg = { - .type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, + .type = SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, .inject_touch_event = { .action = AMOTION_EVENT_ACTION_MOVE, .pointer_id = POINTER_ID_MOUSE, @@ -87,7 +87,7 @@ sc_mouse_processor_process_mouse_click(struct sc_mouse_processor *mp, struct sc_mouse_inject *mi = DOWNCAST(mp); struct sc_control_msg msg = { - .type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, + .type = SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, .inject_touch_event = { .action = convert_mouse_action(event->action), .pointer_id = POINTER_ID_MOUSE, @@ -108,7 +108,7 @@ sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp, struct sc_mouse_inject *mi = DOWNCAST(mp); struct sc_control_msg msg = { - .type = CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT, + .type = SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT, .inject_scroll_event = { .position = event->position, .hscroll = event->hscroll, @@ -128,7 +128,7 @@ sc_mouse_processor_process_touch(struct sc_mouse_processor *mp, struct sc_mouse_inject *mi = DOWNCAST(mp); struct sc_control_msg msg = { - .type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, + .type = SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, .inject_touch_event = { .action = convert_touch_action(event->action), .pointer_id = event->pointer_id, diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 5f4455ba..1534c772 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -515,8 +515,8 @@ aoa_hid_end: if (options->turn_screen_off) { struct sc_control_msg msg; - msg.type = CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE; - msg.set_screen_power_mode.mode = SCREEN_POWER_MODE_OFF; + msg.type = SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE; + msg.set_screen_power_mode.mode = SC_SCREEN_POWER_MODE_OFF; if (!sc_controller_push_msg(&s->controller, &msg)) { LOGW("Could not request 'set screen power mode'"); diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c index 95a54a98..72e3d87a 100644 --- a/app/tests/test_control_msg_serialize.c +++ b/app/tests/test_control_msg_serialize.c @@ -7,7 +7,7 @@ static void test_serialize_inject_keycode(void) { struct sc_control_msg msg = { - .type = CONTROL_MSG_TYPE_INJECT_KEYCODE, + .type = SC_CONTROL_MSG_TYPE_INJECT_KEYCODE, .inject_keycode = { .action = AKEY_EVENT_ACTION_UP, .keycode = AKEYCODE_ENTER, @@ -16,12 +16,12 @@ static void test_serialize_inject_keycode(void) { }, }; - unsigned char buf[CONTROL_MSG_MAX_SIZE]; + unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; size_t size = sc_control_msg_serialize(&msg, buf); assert(size == 14); const unsigned char expected[] = { - CONTROL_MSG_TYPE_INJECT_KEYCODE, + SC_CONTROL_MSG_TYPE_INJECT_KEYCODE, 0x01, // AKEY_EVENT_ACTION_UP 0x00, 0x00, 0x00, 0x42, // AKEYCODE_ENTER 0x00, 0x00, 0x00, 0X05, // repeat @@ -32,18 +32,18 @@ static void test_serialize_inject_keycode(void) { static void test_serialize_inject_text(void) { struct sc_control_msg msg = { - .type = CONTROL_MSG_TYPE_INJECT_TEXT, + .type = SC_CONTROL_MSG_TYPE_INJECT_TEXT, .inject_text = { .text = "hello, world!", }, }; - unsigned char buf[CONTROL_MSG_MAX_SIZE]; + unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; size_t size = sc_control_msg_serialize(&msg, buf); assert(size == 18); const unsigned char expected[] = { - CONTROL_MSG_TYPE_INJECT_TEXT, + SC_CONTROL_MSG_TYPE_INJECT_TEXT, 0x00, 0x00, 0x00, 0x0d, // text length 'h', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', '!', // text }; @@ -52,30 +52,30 @@ static void test_serialize_inject_text(void) { static void test_serialize_inject_text_long(void) { struct sc_control_msg msg; - msg.type = CONTROL_MSG_TYPE_INJECT_TEXT; - char text[CONTROL_MSG_INJECT_TEXT_MAX_LENGTH + 1]; - memset(text, 'a', CONTROL_MSG_INJECT_TEXT_MAX_LENGTH); - text[CONTROL_MSG_INJECT_TEXT_MAX_LENGTH] = '\0'; + msg.type = SC_CONTROL_MSG_TYPE_INJECT_TEXT; + char text[SC_CONTROL_MSG_INJECT_TEXT_MAX_LENGTH + 1]; + memset(text, 'a', SC_CONTROL_MSG_INJECT_TEXT_MAX_LENGTH); + text[SC_CONTROL_MSG_INJECT_TEXT_MAX_LENGTH] = '\0'; msg.inject_text.text = text; - unsigned char buf[CONTROL_MSG_MAX_SIZE]; + unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; size_t size = sc_control_msg_serialize(&msg, buf); - assert(size == 5 + CONTROL_MSG_INJECT_TEXT_MAX_LENGTH); + assert(size == 5 + SC_CONTROL_MSG_INJECT_TEXT_MAX_LENGTH); - unsigned char expected[5 + CONTROL_MSG_INJECT_TEXT_MAX_LENGTH]; - expected[0] = CONTROL_MSG_TYPE_INJECT_TEXT; + unsigned char expected[5 + SC_CONTROL_MSG_INJECT_TEXT_MAX_LENGTH]; + expected[0] = SC_CONTROL_MSG_TYPE_INJECT_TEXT; 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); + memset(&expected[5], 'a', SC_CONTROL_MSG_INJECT_TEXT_MAX_LENGTH); assert(!memcmp(buf, expected, sizeof(expected))); } static void test_serialize_inject_touch_event(void) { struct sc_control_msg msg = { - .type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, + .type = SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, .inject_touch_event = { .action = AMOTION_EVENT_ACTION_DOWN, .pointer_id = UINT64_C(0x1234567887654321), @@ -94,12 +94,12 @@ static void test_serialize_inject_touch_event(void) { }, }; - unsigned char buf[CONTROL_MSG_MAX_SIZE]; + unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; size_t size = sc_control_msg_serialize(&msg, buf); assert(size == 28); const unsigned char expected[] = { - CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, + SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, 0x00, // AKEY_EVENT_ACTION_DOWN 0x12, 0x34, 0x56, 0x78, 0x87, 0x65, 0x43, 0x21, // pointer id 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0xc8, // 100 200 @@ -112,7 +112,7 @@ static void test_serialize_inject_touch_event(void) { static void test_serialize_inject_scroll_event(void) { struct sc_control_msg msg = { - .type = CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT, + .type = SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT, .inject_scroll_event = { .position = { .point = { @@ -130,12 +130,12 @@ static void test_serialize_inject_scroll_event(void) { }, }; - unsigned char buf[CONTROL_MSG_MAX_SIZE]; + unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; size_t size = sc_control_msg_serialize(&msg, buf); assert(size == 25); const unsigned char expected[] = { - CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT, + SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT, 0x00, 0x00, 0x01, 0x04, 0x00, 0x00, 0x04, 0x02, // 260 1026 0x04, 0x38, 0x07, 0x80, // 1080 1920 0x00, 0x00, 0x00, 0x01, // 1 @@ -147,18 +147,18 @@ static void test_serialize_inject_scroll_event(void) { static void test_serialize_back_or_screen_on(void) { struct sc_control_msg msg = { - .type = CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON, + .type = SC_CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON, .back_or_screen_on = { .action = AKEY_EVENT_ACTION_UP, }, }; - unsigned char buf[CONTROL_MSG_MAX_SIZE]; + unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; size_t size = sc_control_msg_serialize(&msg, buf); assert(size == 2); const unsigned char expected[] = { - CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON, + SC_CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON, 0x01, // AKEY_EVENT_ACTION_UP }; assert(!memcmp(buf, expected, sizeof(expected))); @@ -166,71 +166,71 @@ static void test_serialize_back_or_screen_on(void) { static void test_serialize_expand_notification_panel(void) { struct sc_control_msg msg = { - .type = CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL, + .type = SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL, }; - unsigned char buf[CONTROL_MSG_MAX_SIZE]; + unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; size_t size = sc_control_msg_serialize(&msg, buf); assert(size == 1); const unsigned char expected[] = { - CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL, + SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL, }; assert(!memcmp(buf, expected, sizeof(expected))); } static void test_serialize_expand_settings_panel(void) { struct sc_control_msg msg = { - .type = CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL, + .type = SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL, }; - unsigned char buf[CONTROL_MSG_MAX_SIZE]; + unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; size_t size = sc_control_msg_serialize(&msg, buf); assert(size == 1); const unsigned char expected[] = { - CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL, + SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL, }; assert(!memcmp(buf, expected, sizeof(expected))); } static void test_serialize_collapse_panels(void) { struct sc_control_msg msg = { - .type = CONTROL_MSG_TYPE_COLLAPSE_PANELS, + .type = SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS, }; - unsigned char buf[CONTROL_MSG_MAX_SIZE]; + unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; size_t size = sc_control_msg_serialize(&msg, buf); assert(size == 1); const unsigned char expected[] = { - CONTROL_MSG_TYPE_COLLAPSE_PANELS, + SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS, }; assert(!memcmp(buf, expected, sizeof(expected))); } static void test_serialize_get_clipboard(void) { struct sc_control_msg msg = { - .type = CONTROL_MSG_TYPE_GET_CLIPBOARD, + .type = SC_CONTROL_MSG_TYPE_GET_CLIPBOARD, .get_clipboard = { - .copy_key = GET_CLIPBOARD_COPY_KEY_COPY, + .copy_key = SC_COPY_KEY_COPY, }, }; - unsigned char buf[CONTROL_MSG_MAX_SIZE]; + unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; size_t size = sc_control_msg_serialize(&msg, buf); assert(size == 2); const unsigned char expected[] = { - CONTROL_MSG_TYPE_GET_CLIPBOARD, - GET_CLIPBOARD_COPY_KEY_COPY, + SC_CONTROL_MSG_TYPE_GET_CLIPBOARD, + SC_COPY_KEY_COPY, }; assert(!memcmp(buf, expected, sizeof(expected))); } static void test_serialize_set_clipboard(void) { struct sc_control_msg msg = { - .type = CONTROL_MSG_TYPE_SET_CLIPBOARD, + .type = SC_CONTROL_MSG_TYPE_SET_CLIPBOARD, .set_clipboard = { .sequence = UINT64_C(0x0102030405060708), .paste = true, @@ -238,12 +238,12 @@ static void test_serialize_set_clipboard(void) { }, }; - unsigned char buf[CONTROL_MSG_MAX_SIZE]; + unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; size_t size = sc_control_msg_serialize(&msg, buf); assert(size == 27); const unsigned char expected[] = { - CONTROL_MSG_TYPE_SET_CLIPBOARD, + SC_CONTROL_MSG_TYPE_SET_CLIPBOARD, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, // sequence 1, // paste 0x00, 0x00, 0x00, 0x0d, // text length @@ -254,7 +254,7 @@ static void test_serialize_set_clipboard(void) { static void test_serialize_set_clipboard_long(void) { struct sc_control_msg msg = { - .type = CONTROL_MSG_TYPE_SET_CLIPBOARD, + .type = SC_CONTROL_MSG_TYPE_SET_CLIPBOARD, .set_clipboard = { .sequence = UINT64_C(0x0102030405060708), .paste = true, @@ -262,60 +262,60 @@ static void test_serialize_set_clipboard_long(void) { }, }; - char text[CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH + 1]; - memset(text, 'a', CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH); - text[CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH] = '\0'; + char text[SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH + 1]; + memset(text, 'a', SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH); + text[SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH] = '\0'; msg.set_clipboard.text = text; - unsigned char buf[CONTROL_MSG_MAX_SIZE]; + unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; size_t size = sc_control_msg_serialize(&msg, buf); - assert(size == CONTROL_MSG_MAX_SIZE); + assert(size == SC_CONTROL_MSG_MAX_SIZE); - unsigned char expected[CONTROL_MSG_MAX_SIZE] = { - CONTROL_MSG_TYPE_SET_CLIPBOARD, + unsigned char expected[SC_CONTROL_MSG_MAX_SIZE] = { + SC_CONTROL_MSG_TYPE_SET_CLIPBOARD, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, // sequence 1, // paste // text length - CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH >> 24, - (CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH >> 16) & 0xff, - (CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH >> 8) & 0xff, - CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH & 0xff, + SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH >> 24, + (SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH >> 16) & 0xff, + (SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH >> 8) & 0xff, + SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH & 0xff, }; - memset(expected + 14, 'a', CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH); + memset(expected + 14, 'a', SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH); assert(!memcmp(buf, expected, sizeof(expected))); } static void test_serialize_set_screen_power_mode(void) { struct sc_control_msg msg = { - .type = CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE, + .type = SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE, .set_screen_power_mode = { - .mode = SCREEN_POWER_MODE_NORMAL, + .mode = SC_SCREEN_POWER_MODE_NORMAL, }, }; - unsigned char buf[CONTROL_MSG_MAX_SIZE]; + unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; size_t size = sc_control_msg_serialize(&msg, buf); assert(size == 2); const unsigned char expected[] = { - CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE, - 0x02, // SCREEN_POWER_MODE_NORMAL + SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE, + 0x02, // SC_SCREEN_POWER_MODE_NORMAL }; assert(!memcmp(buf, expected, sizeof(expected))); } static void test_serialize_rotate_device(void) { struct sc_control_msg msg = { - .type = CONTROL_MSG_TYPE_ROTATE_DEVICE, + .type = SC_CONTROL_MSG_TYPE_ROTATE_DEVICE, }; - unsigned char buf[CONTROL_MSG_MAX_SIZE]; + unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; size_t size = sc_control_msg_serialize(&msg, buf); assert(size == 1); const unsigned char expected[] = { - CONTROL_MSG_TYPE_ROTATE_DEVICE, + SC_CONTROL_MSG_TYPE_ROTATE_DEVICE, }; assert(!memcmp(buf, expected, sizeof(expected))); } From c8dc1917f47a3f9b4ef6026b912c2792b69e676a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 27 Jan 2022 08:10:04 +0100 Subject: [PATCH 1012/2244] Do not restore power mode if --no-control This totally disables deferred cleanup when --no-control is passed. Refs f289d206ea0e10cc052137781c39da862ebe2f73 --- server/src/main/java/com/genymobile/scrcpy/Server.java | 4 +++- 1 file changed, 3 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 68c6c02c..8cf289cd 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -19,6 +19,7 @@ public final class Server { private static void initAndCleanUp(Options options) { boolean mustDisableShowTouchesOnCleanUp = false; int restoreStayOn = -1; + boolean restoreNormalPowerMode = options.getControl(); // only restore power mode if control is enabled if (options.getShowTouches() || options.getStayAwake()) { Settings settings = Device.getSettings(); if (options.getShowTouches()) { @@ -51,7 +52,8 @@ public final class Server { } try { - CleanUp.configure(options.getDisplayId(), restoreStayOn, mustDisableShowTouchesOnCleanUp, true, options.getPowerOffScreenOnClose()); + CleanUp.configure(options.getDisplayId(), restoreStayOn, mustDisableShowTouchesOnCleanUp, restoreNormalPowerMode, + options.getPowerOffScreenOnClose()); } catch (IOException e) { Ln.e("Could not configure cleanup", e); } From 50f4f1639c552f2195f1b99f67aeac884c0337a5 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 26 Jan 2022 09:57:24 +0100 Subject: [PATCH 1013/2244] Add a shorcut "open a terminal here" on Windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On Windows, the file explorer does not provide any "open a terminal here" shortcut. Add a bat file which just runs `cmd` to easily open a terminal directly in the scrcpy directory (so there is no need to `cd C:\path\…`). PR #2970 --- FAQ.md | 11 +++++++++-- data/open_a_terminal_here.bat | 1 + release.mk | 2 ++ 3 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 data/open_a_terminal_here.bat diff --git a/FAQ.md b/FAQ.md index 43ba39af..6468b32a 100644 --- a/FAQ.md +++ b/FAQ.md @@ -248,8 +248,15 @@ Caused by: java.lang.IllegalArgumentException: displayToken must not be null ## Command line on Windows -Some Windows users are not familiar with the command line. Here is how to open a -terminal and run `scrcpy` with arguments: +Since v1.22, a "shortcut" has been added to directly open a terminal in the +scrcpy directory. Double-click on `open_a_terminal_here.bat`, then type your +command. For example: + +``` +scrcpy --record file.mkv +``` + +You could also open a terminal and go to the scrcpy folder manually: 1. Press Windows+r, this opens a dialog box. 2. Type `cmd` and press Enter, this opens a terminal. diff --git a/data/open_a_terminal_here.bat b/data/open_a_terminal_here.bat new file mode 100644 index 00000000..24d557f3 --- /dev/null +++ b/data/open_a_terminal_here.bat @@ -0,0 +1 @@ +@cmd diff --git a/release.mk b/release.mk index aa26f02d..6b361e5b 100644 --- a/release.mk +++ b/release.mk @@ -93,6 +93,7 @@ dist-win32: build-server build-win32 cp data/scrcpy-console.bat "$(DIST)/$(WIN32_TARGET_DIR)" cp data/scrcpy-noconsole.vbs "$(DIST)/$(WIN32_TARGET_DIR)" cp data/icon.png "$(DIST)/$(WIN32_TARGET_DIR)" + cp data/open_a_terminal_here.bat "$(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)/" @@ -110,6 +111,7 @@ dist-win64: build-server build-win64 cp data/scrcpy-console.bat "$(DIST)/$(WIN64_TARGET_DIR)" cp data/scrcpy-noconsole.vbs "$(DIST)/$(WIN64_TARGET_DIR)" cp data/icon.png "$(DIST)/$(WIN64_TARGET_DIR)" + cp data/open_a_terminal_here.bat "$(DIST)/$(WIN64_TARGET_DIR)" cp prebuilt-deps/ffmpeg-5.0-full_build-shared/bin/avutil-57.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/ffmpeg-5.0-full_build-shared/bin/avcodec-59.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/ffmpeg-5.0-full_build-shared/bin/avformat-59.dll "$(DIST)/$(WIN64_TARGET_DIR)/" From 8e4d3beb01ba4623acc91deaaed2d10aef19f718 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 27 Jan 2022 12:26:43 +0100 Subject: [PATCH 1014/2244] Fix return value on adb commands error --- app/src/adb.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/adb.c b/app/src/adb.c index 598b331f..5bc4e4cc 100644 --- a/app/src/adb.c +++ b/app/src/adb.c @@ -425,7 +425,7 @@ adb_get_serialno(struct sc_intr *intr, unsigned flags) { } if (r == -1) { - return false; + return NULL; } sc_str_truncate(buf, r, " \r\n"); @@ -455,7 +455,7 @@ adb_get_device_ip(struct sc_intr *intr, const char *serial, unsigned flags) { } if (r == -1) { - return false; + return NULL; } assert((size_t) r <= sizeof(buf)); From 02b5e87802aa2dce4b8dbacf47dfded55688944b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 27 Jan 2022 15:30:09 +0100 Subject: [PATCH 1015/2244] Slightly reduce lock usage Locking the frame_buffer mutex to reference the input frame into the tmp_frame is unnecessary. This also fixes the missing unlock on error. --- app/src/frame_buffer.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/frame_buffer.c b/app/src/frame_buffer.c index d177d4fa..fc5e7084 100644 --- a/app/src/frame_buffer.c +++ b/app/src/frame_buffer.c @@ -51,8 +51,6 @@ swap_frames(AVFrame **lhs, AVFrame **rhs) { bool sc_frame_buffer_push(struct sc_frame_buffer *fb, const AVFrame *frame, bool *previous_frame_skipped) { - sc_mutex_lock(&fb->mutex); - // Use a temporary frame to preserve pending_frame in case of error. // tmp_frame is an empty frame, no need to call av_frame_unref() beforehand. int r = av_frame_ref(fb->tmp_frame, frame); @@ -61,6 +59,8 @@ sc_frame_buffer_push(struct sc_frame_buffer *fb, const AVFrame *frame, return false; } + sc_mutex_lock(&fb->mutex); + // Now that av_frame_ref() succeeded, we can replace the previous // pending_frame swap_frames(&fb->pending_frame, &fb->tmp_frame); From 4817cadd09aa391746343f9daa2a028a2fa19b6a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 27 Jan 2022 18:59:41 +0100 Subject: [PATCH 1016/2244] Fix code style Align function parameters. --- app/src/frame_buffer.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/frame_buffer.c b/app/src/frame_buffer.c index fc5e7084..5699b58f 100644 --- a/app/src/frame_buffer.c +++ b/app/src/frame_buffer.c @@ -50,7 +50,7 @@ swap_frames(AVFrame **lhs, AVFrame **rhs) { bool sc_frame_buffer_push(struct sc_frame_buffer *fb, const AVFrame *frame, - bool *previous_frame_skipped) { + bool *previous_frame_skipped) { // Use a temporary frame to preserve pending_frame in case of error. // tmp_frame is an empty frame, no need to call av_frame_unref() beforehand. int r = av_frame_ref(fb->tmp_frame, frame); From 34e19dcc57f35d8f152b1f5b0988800581af15df Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 27 Jan 2022 19:00:03 +0100 Subject: [PATCH 1017/2244] Upgrade SDL (2.0.20) for Windows Include the latest version of SDL in Windows releases. --- cross_win32.txt | 2 +- cross_win64.txt | 2 +- prebuilt-deps/Makefile | 6 +++--- release.mk | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cross_win32.txt b/cross_win32.txt index 826c044f..9bf974e3 100644 --- a/cross_win32.txt +++ b/cross_win32.txt @@ -20,4 +20,4 @@ ffmpeg_avcodec = 'avcodec-58' ffmpeg_avformat = 'avformat-58' ffmpeg_avutil = 'avutil-56' prebuilt_ffmpeg = 'ffmpeg-4.3.1-win32-shared' -prebuilt_sdl2 = 'SDL2-2.0.18/i686-w64-mingw32' +prebuilt_sdl2 = 'SDL2-2.0.20/i686-w64-mingw32' diff --git a/cross_win64.txt b/cross_win64.txt index 2e049fdb..763e12e9 100644 --- a/cross_win64.txt +++ b/cross_win64.txt @@ -20,4 +20,4 @@ ffmpeg_avcodec = 'avcodec-59' ffmpeg_avformat = 'avformat-59' ffmpeg_avutil = 'avutil-57' prebuilt_ffmpeg = 'ffmpeg-5.0-full_build-shared' -prebuilt_sdl2 = 'SDL2-2.0.18/x86_64-w64-mingw32' +prebuilt_sdl2 = 'SDL2-2.0.20/x86_64-w64-mingw32' diff --git a/prebuilt-deps/Makefile b/prebuilt-deps/Makefile index 93e8c48d..b40321b6 100644 --- a/prebuilt-deps/Makefile +++ b/prebuilt-deps/Makefile @@ -23,9 +23,9 @@ prepare-ffmpeg-win64: ffmpeg-5.0-full_build-shared prepare-sdl2: - @./prepare-dep https://libsdl.org/release/SDL2-devel-2.0.18-mingw.tar.gz \ - bbad7c6947f6ca3e05292f065852ed8b62f319fc5533047e7708769c4dbae394 \ - SDL2-2.0.18 + @./prepare-dep https://libsdl.org/release/SDL2-devel-2.0.20-mingw.tar.gz \ + 38094d82a857d6c62352e5c5cdec74948c5b4d25c59cbd298d6d233568976bd1 \ + SDL2-2.0.20 prepare-adb: @./prepare-dep https://dl.google.com/android/repository/platform-tools_r31.0.3-windows.zip \ diff --git a/release.mk b/release.mk index 6b361e5b..2f2fe9e1 100644 --- a/release.mk +++ b/release.mk @@ -102,7 +102,7 @@ dist-win32: build-server build-win32 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.18/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/SDL2-2.0.20/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/" dist-win64: build-server build-win64 mkdir -p "$(DIST)/$(WIN64_TARGET_DIR)" @@ -120,7 +120,7 @@ dist-win64: build-server build-win64 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.18/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/SDL2-2.0.20/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/" zip-win32: dist-win32 cd "$(DIST)/$(WIN32_TARGET_DIR)"; \ From c0de365f672110959cec8fca936bd6aeaa01b59d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 27 Jan 2022 19:01:30 +0100 Subject: [PATCH 1018/2244] Upgrade platform-tools (32.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 b40321b6..44ea6e1e 100644 --- a/prebuilt-deps/Makefile +++ b/prebuilt-deps/Makefile @@ -28,6 +28,6 @@ prepare-sdl2: SDL2-2.0.20 prepare-adb: - @./prepare-dep https://dl.google.com/android/repository/platform-tools_r31.0.3-windows.zip \ - 0f4b8fdd26af2c3733539d6eebb3c2ed499ea1d4bb1f4e0ecc2d6016961a6e24 \ + @./prepare-dep https://dl.google.com/android/repository/platform-tools_r32.0.0-windows.zip \ + 41f4c7512b32cbb3f8c624c20b56326abb692a6f169b03b4b63b6c5a6fdbb08c \ platform-tools From f51c53e913d4614a677f656d973fa69c496d585a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 27 Jan 2022 17:08:14 +0100 Subject: [PATCH 1019/2244] Mention "screen copy" in README Help users make sense of the app name :) --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index edf48cfd..9eb4bd21 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ scrcpy +_pronounced "**scr**een **c**o**py**"_ + [Read in another language](#translations) This application provides display and control of Android devices connected via From 86158130051d450a449a2e7bb20b0fcef1b62e80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Raiti?= <46955459+Secreto31126@users.noreply.github.com> Date: Sun, 23 Jan 2022 06:37:00 -0300 Subject: [PATCH 1020/2244] Update README.sp.md to v1.21 PR #2962 Signed-off-by: Romain Vimont --- README.md | 2 +- README.sp.md | 369 +++++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 301 insertions(+), 70 deletions(-) diff --git a/README.md b/README.md index c4b25f25..db0e2244 100644 --- a/README.md +++ b/README.md @@ -1051,7 +1051,7 @@ This README is available in other languages: - [日本語 (Japanese, `jp`) - v1.19](README.jp.md) - [한국어 (Korean, `ko`) - v1.11](README.ko.md) - [Português Brasileiro (Brazilian Portuguese, `pt-BR`) - v1.19](README.pt-br.md) -- [Español (Spanish, `sp`) - v1.17](README.sp.md) +- [Español (Spanish, `sp`) - v1.21](README.sp.md) - [简体中文 (Simplified Chinese, `zh-Hans`) - v1.20](README.zh-Hans.md) - [繁體中文 (Traditional Chinese, `zh-Hant`) - v1.15](README.zh-Hant.md) - [Turkish (Turkish, `tr`) - v1.18](README.tr.md) diff --git a/README.sp.md b/README.sp.md index 6f76a7be..4ab5ae25 100644 --- a/README.sp.md +++ b/README.sp.md @@ -1,24 +1,36 @@ Solo se garantiza que el archivo [README](README.md) original esté actualizado. -# scrcpy (v1.17) +# scrcpy (v1.21) -Esta aplicación proporciona imagen y control de un dispositivo Android conectado -por USB (o [por TCP/IP][article-tcpip]). No requiere acceso _root_. +scrcpy + +Esta aplicación proporciona control e imagen de un dispositivo Android conectado +por USB (o [por TCP/IP](#conexión)). No requiere acceso _root_. Compatible con _GNU/Linux_, _Windows_ y _macOS_. ![screenshot](assets/screenshot-debian-600.jpg) -Sus características principales son: - - - **ligero** (nativo, solo muestra la imagen del dispositivo) - - **desempeño** (30~60fps) - - **calidad** (1920×1080 o superior) - - **baja latencia** ([35~70ms][lowlatency]) - - **corto tiempo de inicio** (~1 segundo para mostrar la primera imagen) - - **no intrusivo** (no se deja nada instalado en el dispositivo) +Se enfoca en: + - **ser ligera**: aplicación nativa, solo muestra la imagen del dispositivo + - **rendimiento**: 30~120fps, dependiendo del dispositivo + - **calidad**: 1920×1080 o superior + - **baja latencia**: [35~70ms][lowlatency] + - **inicio rápido**: ~1 segundo para mostrar la primera imagen + - **no intrusivo**: no deja nada instalado en el dispositivo + - **beneficios**: sin cuentas, sin anuncios, no requiere acceso a internet + - **libertad**: software gratis y de código abierto [lowlatency]: https://github.com/Genymobile/scrcpy/pull/646 +Con la aplicación puede: + - [grabar la pantalla](#capturas-y-grabaciones) + - duplicar la imagen con [la pantalla apagada](#apagar-la-pantalla) + - [copiar y pegar](#copiar-y-pegar) en ambos sentidos + - [configurar la calidad](#configuración-de-captura) + - usar la pantalla del dispositivo [como webcam (V4L2)](#v4l2loopback) (solo en Linux) + - [emular un teclado físico (HID)](#emular-teclado-físico-hid) + (solo en Linux) + - y mucho más… ## Requisitos @@ -51,7 +63,7 @@ Construir desde la fuente: [BUILD] ([proceso simplificado][BUILD_simple]) ### Linux -En Debian (_test_ y _sid_ por ahora) y Ubuntu (20.04): +En Debian y Ubuntu: ``` apt install scrcpy @@ -125,7 +137,7 @@ Necesitarás `adb`, accesible desde `PATH`. Si aún no lo tienes: brew install android-platform-tools ``` -También está disponible en [MacPorts], que configurará el adb automáticamente: +También está disponible en [MacPorts], que configura el adb automáticamente: ```bash sudo port install scrcpy @@ -153,7 +165,7 @@ scrcpy --help ## Características -### Capturar configuración +### Configuración de captura #### Reducir la definición @@ -208,10 +220,11 @@ Si `--max-size` también está especificado, el cambio de tamaño es aplicado de Para fijar la rotación de la transmisión: ```bash -scrcpy --lock-video-orientation 0 # orientación normal -scrcpy --lock-video-orientation 1 # 90° contrarreloj -scrcpy --lock-video-orientation 2 # 180° -scrcpy --lock-video-orientation 3 # 90° sentido de las agujas del reloj +scrcpy --lock-video-orientation # orientación inicial +scrcpy --lock-video-orientation=0 # orientación normal +scrcpy --lock-video-orientation=1 # 90° contrarreloj +scrcpy --lock-video-orientation=2 # 180° +scrcpy --lock-video-orientation=3 # 90° sentido de las agujas del reloj ``` Esto afecta la rotación de la grabación. @@ -233,7 +246,10 @@ Para listar los codificadores disponibles, puedes pasar un nombre de codificador scrcpy --encoder _ ``` -### Grabación +### Capturas y grabaciones + + +#### Grabación Es posible grabar la pantalla mientras se transmite: @@ -250,17 +266,117 @@ scrcpy -Nr file.mkv # interrumpe la grabación con Ctrl+C ``` -"Skipped frames" son grabados, incluso si no son mostrados en tiempo real (por razones de desempeño). Los frames tienen _marcas de tiempo_ en el dispositivo, por lo que el "[packet delay +Los "skipped frames" son grabados, incluso si no se mostrados en tiempo real (por razones de desempeño). Los frames tienen _marcas de tiempo_ en el dispositivo, por lo que el "[packet delay variation]" no impacta el archivo grabado. [packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation +#### v4l2loopback + +En Linux se puede mandar el stream del video a un dispositivo loopback v4l2, por +lo que se puede abrir el dispositivo Android como una webcam con cualquier +programa compatible con v4l2. + +Se debe instalar el modulo `v4l2loopback`: + +```bash +sudo apt install v4l2loopback-dkms +``` + +Para crear un dispositivo v4l2: + +```bash +sudo modprobe v4l2loopback +``` + +Esto va a crear un nuevo dispositivo de video en `/dev/videoN`, donde `N` es un número +(hay más [opciones](https://github.com/umlaeute/v4l2loopback#options) disponibles +para crear múltiples dispositivos o usar un ID en específico). + +Para ver los dispositivos disponibles: + +```bash +# requiere el paquete v4l-utils +v4l2-ctl --list-devices +# simple pero generalmente suficiente +ls /dev/video* +``` + +Para iniciar scrcpy usando una fuente v4l2: + +```bash +scrcpy --v4l2-sink=/dev/videoN +scrcpy --v4l2-sink=/dev/videoN --no-display # deshabilita la transmisión de imagen +scrcpy --v4l2-sink=/dev/videoN -N # más corto +``` + +(reemplace `N` con el ID del dispositivo, compruebe con `ls /dev/video*`) + +Una vez habilitado, podés abrir el stream del video con una herramienta compatible con v4l2: + +```bash +ffplay -i /dev/videoN +vlc v4l2:///dev/videoN # VLC puede agregar un delay por buffering +``` + +Por ejemplo, podrías capturar el video usando [OBS]. + +[OBS]: https://obsproject.com/ + + +#### Buffering + +Es posible agregar buffering al video. Esto reduce el ruido en la imagen ("jitter") +pero aumenta la latencia (vea [#2464]). + +[#2464]: https://github.com/Genymobile/scrcpy/issues/2464 + +La opción de buffering está disponible para la transmisión de imagen: + +```bash +scrcpy --display-buffer=50 # agrega 50 ms de buffering a la imagen +``` + +y las fuentes V4L2: + +```bash +scrcpy --v4l2-buffer=500 # agrega 500 ms de buffering a la fuente v4l2 +``` + + ### Conexión -#### Inalámbrica +#### TCP/IP (Inalámbrica) -_Scrcpy_ usa `adb` para comunicarse con el dispositivo, y `adb` puede [conectarse] vía TCP/IP: +_Scrcpy_ usa `adb` para comunicarse con el dispositivo, y `adb` puede [conectarse] vía TCP/IP. +El dispositivo debe estar conectado a la misma red que la computadora: + +##### Automático + +La opción `--tcpip` permite configurar la conexión automáticamente. Hay 2 variables. + +Si el dispositivo (accesible en 192.168.1.1 para este ejemplo) ya está escuchando +en un puerto (generalmente 5555) esperando una conexión adb entrante, entonces corré: + +```bash +scrcpy --tcpip=192.168.1.1 # el puerto default es 5555 +scrcpy --tcpip=192.168.1.1:5555 +``` + +Si el dispositivo no tiene habilitado el modo adb TCP/IP (o si no sabés la dirección IP), +entonces conectá el dispositivo por USB y corré: + +```bash +scrcpy --tcpip # sin argumentos +``` + +El programa buscará automáticamente la IP del dispositivo, habilitará el modo TCP/IP, y +se conectará al dispositivo antes de comenzar a transmitir la imagen. + +##### Manual + +Como alternativa, se puede habilitar la conexión TCP/IP manualmente usando `adb`: 1. Conecta el dispositivo al mismo Wi-Fi que tu computadora. 2. Obtén la dirección IP del dispositivo, en Ajustes → Acerca del dispositivo → Estado, o ejecutando este comando: @@ -302,7 +418,7 @@ scrcpy -s 192.168.0.1:5555 # versión breve Puedes iniciar múltiples instancias de _scrcpy_ para múltiples dispositivos. -#### Autoiniciar al detectar dispositivo +#### Iniciar automáticamente al detectar dispositivo Puedes utilizar [AutoAdb]: @@ -312,37 +428,82 @@ autoadb scrcpy -s '{}' [AutoAdb]: https://github.com/rom1v/autoadb -#### Túnel SSH +#### Túneles -Para conectarse a un dispositivo remoto, es posible conectar un cliente local de `adb` a un servidor remoto `adb` (siempre y cuando utilicen la misma versión de protocolos _adb_): +Para conectarse a un dispositivo remoto, es posible conectar un cliente local `adb` a un servidor remoto `adb` (siempre y cuando utilicen la misma versión de protocolos _adb_). + +##### Servidor ADB remoto + +Para conectarse a un servidor ADB remoto, haz que el servidor escuche en todas las interfaces: ```bash -adb kill-server # cierra el servidor local adb en 5037 -ssh -CN -L5037:localhost:5037 -R27183:localhost:27183 your_remote_computer +adb kill-server +adb -a nodaemon server start # conserva este servidor abierto ``` -Desde otra terminal: +**Advertencia: todas las comunicaciones entre los clientes y el servidor ADB están desencriptadas.** + +Supondremos que este servidor se puede acceder desde 192.168.1.2. Entonces, desde otra +terminal, corré scrcpy: ```bash +export ADB_SERVER_SOCKET=tcp:192.168.1.2:5037 +scrcpy --tunnel-host=192.168.1.2 +``` + +Por default, scrcpy usa el puerto local que se usó para establecer el tunel +`adb forward` (típicamente `27183`, vea `--port`). También es posible forzar un +puerto diferente (puede resultar útil en situaciones más complejas, donde haya +múltiples redirecciones): + +``` +scrcpy --tunnel-port=1234 +``` + + +##### Túnel SSH + +Para comunicarse con un servidor ADB remoto de forma segura, es preferible usar un túnel SSH. + +Primero, asegurate que el servidor ADB está corriendo en la computadora remota: + +```bash +adb start-server +``` + +Después, establecé el túnel SSH: + +```bash +# local 5038 --> remoto 5037 +# local 27183 <-- remoto 27183 +ssh -CN -L5038:localhost:5037 -R27183:localhost:27183 your_remote_computer +# conserva este servidor abierto +``` + +Desde otra terminal, corré scrcpy: + +```bash +export ADB_SERVER_SOCKET=tcp:localhost:5038 scrcpy ``` Para evitar habilitar "remote port forwarding", puedes forzar una "forward connection" (nótese el argumento `-L` en vez de `-R`): ```bash -adb kill-server # cierra el servidor local adb en 5037 -ssh -CN -L5037:localhost:5037 -L27183:localhost:27183 your_remote_computer +# local 5038 --> remoto 5037 +# local 27183 --> remoto 27183 +ssh -CN -L5038:localhost:5037 -L27183:localhost:27183 your_remote_computer # conserva este servidor abierto ``` -Desde otra terminal: +Desde otra terminal, corré scrcpy: ```bash +export ADB_SERVER_SOCKET=tcp:localhost:5038 scrcpy --force-adb-forward ``` - Al igual que las conexiones inalámbricas, puede resultar útil reducir la calidad: ``` @@ -402,7 +563,7 @@ Se puede rotar la ventana: scrcpy --rotation 1 ``` -Los valores posibles son: +Los posibles valores son: - `0`: sin rotación - `1`: 90 grados contrarreloj - `2`: 180 grados @@ -416,7 +577,7 @@ Nótese que _scrcpy_ maneja 3 diferentes rotaciones: - `--rotation` (o MOD+/MOD+) rota solo el contenido de la imagen. Esto solo afecta a la imagen mostrada, no a la grabación. -### Otras opciones menores +### Otras opciones #### Solo lectura ("Read-only") @@ -479,14 +640,12 @@ scrcpy -Sw # versión breve ``` -#### Renderizar frames vencidos +#### Apagar al cerrar la aplicación -Por defecto, para minimizar la latencia, _scrcpy_ siempre renderiza el último frame disponible decodificado, e ignora cualquier frame anterior. - -Para forzar el renderizado de todos los frames (a costo de posible aumento de latencia), use: +Para apagar la pantalla del dispositivo al cerrar scrcpy: ```bash -scrcpy --render-expired-frames +scrcpy --power-off-on-close ``` #### Mostrar clicks @@ -548,6 +707,8 @@ Además, MOD+Shift+v permite inyectar el texto Algunos dispositivos no se comportan como es esperado al establecer el portapapeles programáticamente. La opción `--legacy-paste` está disponible para cambiar el comportamiento de Ctrl+v y MOD+v para que también inyecten el texto del portapapeles de la computadora como una secuencia de teclas (de la misma forma que MOD+Shift+v). +Para deshabilitar la auto-sincronización del portapapeles, use `--no-clipboard-autosync`. + #### Pellizcar para zoom Para simular "pinch-to-zoom": Ctrl+_click-y-mover_. @@ -556,6 +717,48 @@ Más precisamente, mantén Ctrl mientras presionas botón izquierdo. Concretamente, scrcpy genera clicks adicionales con un "dedo virtual" en la posición invertida respecto al centro de la pantalla. +#### Emular teclado físico (HID) + +Por default, scrcpy usa el sistema de Android para la injección de teclas o texto: +funciona en todas partes, pero está limitado a ASCII. + +En Linux, scrcpy puede emular un teclado USB físico en Android para proveer +una mejor experiencia al enviar _inputs_ (usando [USB HID vía AOAv2][hid-aoav2]): +deshabilita el teclado virtual y funciona para todos los caracteres y IME. + +[hid-aoav2]: https://source.android.com/devices/accessories/aoa2#hid-support + +Sin embargo, solo funciona si el dispositivo está conectado por USB, y por ahora +solo funciona en Linux. + +Para habilitar este modo: + +```bash +scrcpy --hid-keyboard +scrcpy -K # más corto +``` + +Si por alguna razón falla (por ejemplo si el dispositivo no está conectado vía +USB), automáticamente vuelve al modo default (un mensaje se escribirá en la consola). +Se puede usar los mismos argumentos en la línea de comandos tanto si se conecta con +USB o vía TCP/IP. + +En este modo, los _raw key events_ (_scancodes_) se envían al dispositivo, independientemente +del mapeo del teclado en el host. Por eso, si el diseño de tu teclado no concuerda, debe ser +configurado en el dispositivo Android, en Ajustes → Sistema → Idioma y Entrada de Texto +→ [Teclado Físico]. + +Se puede iniciar automáticamente en esta página de ajustes: + +```bash +adb shell am start -a android.settings.HARD_KEYBOARD_SETTINGS +``` + +Sin embargo, la opción solo está disponible cuando el teclado HID está activo +(o cuando se conecta un teclado físico). + +[Teclado Físico]: https://github.com/Genymobile/scrcpy/pull/2632#issuecomment-923756915 + #### Preferencias de inyección de texto @@ -573,13 +776,23 @@ scrcpy --prefer-text (Pero esto romperá el comportamiento del teclado en los juegos) +Por el contrario, se puede forzar scrcpy para siempre injectar _raw key events_: + +```bash +scrcpy --raw-key-events +``` + +Estas opciones no tienen efecto en los teclados HID (todos los _key events_ son enviados como +_scancodes_ en este modo). + [textevents]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-text-input [prefertext]: https://github.com/Genymobile/scrcpy/issues/650#issuecomment-512945343 #### Repetir tecla -Por defecto, mantener una tecla presionada genera múltiples _key events_. Esto puede causar problemas de desempeño en algunos juegos, donde estos eventos no tienen sentido de todos modos. +Por defecto, mantener una tecla presionada genera múltiples _key events_. Esto puede +causar problemas de desempeño en algunos juegos, donde estos eventos no tienen sentido de todos modos. Para evitar enviar _key events_ repetidos: @@ -587,6 +800,9 @@ Para evitar enviar _key events_ repetidos: scrcpy --no-key-repeat ``` +Estas opciones no tienen efecto en los teclados HID (Android maneja directamente +las repeticiones de teclas en este modo) + #### Botón derecho y botón del medio @@ -608,14 +824,15 @@ No hay respuesta visual, un mensaje se escribirá en la consola. #### Enviar archivos al dispositivo -Para enviar un archivo a `/sdcard/` en el dispositivo, arrastre y suelte un archivo (no APK) a la ventana de _scrcpy_. +Para enviar un archivo a `/sdcard/Download/` en el dispositivo, arrastre y suelte +un archivo (no APK) a la ventana de _scrcpy_. -No hay respuesta visual, un mensaje se escribirá en la consola. +No hay ninguna respuesta visual, un mensaje se escribirá en la consola. El directorio de destino puede ser modificado al iniciar: ```bash -scrcpy --push-target=/sdcard/Download/ +scrcpy --push-target=/sdcard/Movies/ ``` @@ -647,36 +864,48 @@ _[Super] es generalmente la tecla Windows o Cmd [Super]: https://en.wikipedia.org/wiki/Super_key_(keyboard_button) - | Acción | Atajo - | ------------------------------------------- |:----------------------------- - | Alterne entre pantalla compelta | MOD+f - | Rotar pantalla hacia la izquierda | MOD+ _(izquierda)_ - | Rotar pantalla hacia la derecha | MOD+ _(derecha)_ - | Ajustar ventana a 1:1 ("pixel-perfect") | MOD+g - | Ajustar ventana para quitar los bordes negros| MOD+w \| _Doble click¹_ - | Click en `INICIO` | MOD+h \| _Botón del medio_ - | Click en `RETROCEDER` | MOD+b \| _Botón derecho²_ - | Click en `CAMBIAR APLICACIÓN` | MOD+s - | Click en `MENÚ` (desbloquear pantalla) | MOD+m - | Click en `SUBIR VOLUMEN` | MOD+ _(arriba)_ - | Click en `BAJAR VOLUME` | MOD+ _(abajo)_ - | Click en `ENCENDIDO` | MOD+p - | Encendido | _Botón derecho²_ - | Apagar pantalla (manteniendo la transmisión)| MOD+o - | Encender pantalla | MOD+Shift+o - | Rotar pantalla del dispositivo | MOD+r - | Abrir panel de notificaciones | MOD+n - | Cerrar panel de notificaciones | MOD+Shift+n - | Copiar al portapapeles³ | MOD+c - | Cortar al portapapeles³ | MOD+x - | Synchronizar portapapeles y pegar³ | MOD+v - | inyectar texto del portapapeles de la PC | MOD+Shift+v + | Acción | Atajo + | ------------------------------------------- |:----------------------------- + | Alterne entre pantalla compelta | MOD+f + | Rotar pantalla hacia la izquierda | MOD+ _(izquierda)_ + | Rotar pantalla hacia la derecha | MOD+ _(derecha)_ + | Ajustar ventana a 1:1 ("pixel-perfect") | MOD+g + | Ajustar ventana para quitar los bordes negros| MOD+w \| _Doble click izquierdo¹_ + | Click en `INICIO` | MOD+h \| _Click medio_ + | Click en `RETROCEDER` | MOD+b \| _Click derecho²_ + | Click en `CAMBIAR APLICACIÓN` | MOD+s \| _Cuarto botón³_ + | Click en `MENÚ` (desbloquear pantalla)⁴ | MOD+m + | Click en `SUBIR VOLUMEN` | MOD+ _(arriba)_ + | Click en `BAJAR VOLUME` | MOD+ _(abajo)_ + | Click en `ENCENDIDO` | MOD+p + | Encendido | _Botón derecho²_ + | Apagar pantalla (manteniendo la transmisión) | MOD+o + | Encender pantalla | MOD+Shift+o + | Rotar pantalla del dispositivo | MOD+r + | Abrir panel de notificaciones | MOD+n \| _Quinto botón³_ + | Abrir panel de configuración | MOD+n+n \| _Doble quinto botón³_ + | Cerrar paneles | MOD+Shift+n + | Copiar al portapapeles⁵ | MOD+c + | Cortar al portapapeles⁵ | MOD+x + | Synchronizar portapapeles y pegar⁵ | MOD+v + | Inyectar texto del portapapeles de la PC | MOD+Shift+v | Habilitar/Deshabilitar contador de FPS (en stdout) | MOD+i - | Pellizcar para zoom | Ctrl+_click-y-mover_ + | Pellizcar para zoom | Ctrl+_click-y-mover_ + | Arrastrar y soltar un archivo (APK) | Instalar APK desde la computadora + | Arrastrar y soltar un archivo (no APK) | [Mover archivo al dispositivo](#enviar-archivos-al-dispositivo) _¹Doble click en los bordes negros para eliminarlos._ _²Botón derecho enciende la pantalla si estaba apagada, sino ejecuta RETROCEDER._ -_³Solo en Android >= 7._ +_³Cuarto y quinto botón del mouse, si tu mouse los tiene._ +_⁴Para las apps react-native en desarrollo, `MENU` activa el menú de desarrollo._ +_⁵Solo en Android >= 7._ + +Los shortcuts con teclas repetidas se ejecutan soltando y volviendo a apretar la tecla +por segunda vez. Por ejemplo, para ejecutar "Abrir panel de configuración": + + 1. Apretá y mantené apretado MOD. + 2. Después apretá dos veces la tecla n. + 3. Por último, soltá la tecla MOD. Todos los atajos Ctrl+_tecla_ son enviados al dispositivo para que sean manejados por la aplicación activa. @@ -691,6 +920,8 @@ ADB=/path/to/adb scrcpy Para sobreescribir el path del archivo `scrcpy-server`, configure el path en `SCRCPY_SERVER_PATH`. +Para sobreescribir el ícono, configure el path en `SCRCPY_ICON_PATH`. + ## ¿Por qué _scrcpy_? From b546c33eff4daa9a9b69a2c98de39e70fd08109e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 27 Jan 2022 21:12:46 +0100 Subject: [PATCH 1021/2244] Do not print scrcpy version twice on --version Refs 6da6d905c2aeac17255347037d44c32a62c4c504 --- app/src/main.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/src/main.c b/app/src/main.c index 690e4070..2fdde8e0 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -17,9 +17,7 @@ static void print_version(void) { - fprintf(stderr, "scrcpy %s\n\n", SCRCPY_VERSION); - - fprintf(stderr, "dependencies:\n"); + fprintf(stderr, "\ndependencies:\n"); fprintf(stderr, " - SDL %d.%d.%d\n", SDL_MAJOR_VERSION, SDL_MINOR_VERSION, SDL_PATCHLEVEL); fprintf(stderr, " - libavcodec %d.%d.%d\n", LIBAVCODEC_VERSION_MAJOR, From 8ea6fb1f0f4e2c06ecf51fa184d08a71663c0e7b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 27 Jan 2022 21:14:33 +0100 Subject: [PATCH 1022/2244] Print version on stdout Refs b25404ee4b6f2cbdd41992fa3e087dd8a73412c9 --- app/src/main.c | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/app/src/main.c b/app/src/main.c index 2fdde8e0..cbcef4a7 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -17,22 +17,22 @@ static void print_version(void) { - fprintf(stderr, "\ndependencies:\n"); - fprintf(stderr, " - SDL %d.%d.%d\n", SDL_MAJOR_VERSION, SDL_MINOR_VERSION, - SDL_PATCHLEVEL); - fprintf(stderr, " - libavcodec %d.%d.%d\n", LIBAVCODEC_VERSION_MAJOR, - LIBAVCODEC_VERSION_MINOR, - LIBAVCODEC_VERSION_MICRO); - fprintf(stderr, " - libavformat %d.%d.%d\n", LIBAVFORMAT_VERSION_MAJOR, - LIBAVFORMAT_VERSION_MINOR, - LIBAVFORMAT_VERSION_MICRO); - fprintf(stderr, " - libavutil %d.%d.%d\n", LIBAVUTIL_VERSION_MAJOR, - LIBAVUTIL_VERSION_MINOR, - LIBAVUTIL_VERSION_MICRO); + printf("\ndependencies:\n"); + printf(" - SDL %d.%d.%d\n", SDL_MAJOR_VERSION, SDL_MINOR_VERSION, + SDL_PATCHLEVEL); + printf(" - libavcodec %d.%d.%d\n", LIBAVCODEC_VERSION_MAJOR, + LIBAVCODEC_VERSION_MINOR, + LIBAVCODEC_VERSION_MICRO); + printf(" - libavformat %d.%d.%d\n", LIBAVFORMAT_VERSION_MAJOR, + LIBAVFORMAT_VERSION_MINOR, + LIBAVFORMAT_VERSION_MICRO); + printf(" - libavutil %d.%d.%d\n", LIBAVUTIL_VERSION_MAJOR, + LIBAVUTIL_VERSION_MINOR, + LIBAVUTIL_VERSION_MICRO); #ifdef HAVE_V4L2 - fprintf(stderr, " - libavdevice %d.%d.%d\n", LIBAVDEVICE_VERSION_MAJOR, - LIBAVDEVICE_VERSION_MINOR, - LIBAVDEVICE_VERSION_MICRO); + printf(" - libavdevice %d.%d.%d\n", LIBAVDEVICE_VERSION_MAJOR, + LIBAVDEVICE_VERSION_MINOR, + LIBAVDEVICE_VERSION_MICRO); #endif } From c996a6d462d7499b1421eabcfcadf0a58baae60e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 27 Jan 2022 23:22:43 +0100 Subject: [PATCH 1023/2244] Fix socket close race condition The server needs to interrupt the sockets on stop, but it must not close them while other threads may attempt to read from or write to them. In particular, the video_socket is read by the stream thread, and the control_socket is written by the controller and read by receiver. Therefore, close the socket only on sc_server_destroy(), which is called after all other threads are joined. Reported by TSAN on close: WARNING: ThreadSanitizer: data race (pid=3287612) Write of size 8 at 0x7ba000000080 by thread T1: #0 close ../../../../src/libsanitizer/tsan/tsan_interceptors_posix.cpp:1690 (libtsan.so.0+0x359d8) #1 net_close ../app/src/util/net.c:280 (scrcpy+0x23643) #2 run_server ../app/src/server.c:772 (scrcpy+0x20047) #3 (libSDL2-2.0.so.0+0x905a0) Previous read of size 8 at 0x7ba000000080 by thread T16: #0 recv ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:6603 (libtsan.so.0+0x4f4a6) #1 net_recv_all ../app/src/util/net.c:228 (scrcpy+0x234a9) #2 stream_recv_packet ../app/src/stream.c:33 (scrcpy+0x2045c) #3 run_stream ../app/src/stream.c:228 (scrcpy+0x21169) #4 (libSDL2-2.0.so.0+0x905a0) Refs ddb9396743072f97628fab168ef7fcd45a597b03 --- app/src/server.c | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/src/server.c b/app/src/server.c index b62a74f1..e5acef95 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -769,12 +769,10 @@ run_server(void *data) { // Interrupt sockets to wake up socket blocking calls on the server assert(server->video_socket != SC_SOCKET_NONE); net_interrupt(server->video_socket); - net_close(server->video_socket); if (server->control_socket != SC_SOCKET_NONE) { // There is no control_socket if --no-control is set net_interrupt(server->control_socket); - net_close(server->control_socket); } // Give some delay for the server to terminate properly @@ -830,6 +828,13 @@ sc_server_stop(struct sc_server *server) { void sc_server_destroy(struct sc_server *server) { + if (server->video_socket != SC_SOCKET_NONE) { + net_close(server->video_socket); + } + if (server->control_socket != SC_SOCKET_NONE) { + net_close(server->control_socket); + } + sc_server_params_destroy(&server->params); sc_intr_destroy(&server->intr); sc_cond_destroy(&server->cond_stopped); From 2762f5d18351d5e610a110d731867bd1c27248cf Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 24 Jan 2022 22:27:15 +0100 Subject: [PATCH 1024/2244] Move AOA/HID code to usb/ PR #2974 --- app/meson.build | 6 +++--- app/src/scrcpy.c | 9 +++++---- app/src/{ => usb}/aoa_hid.c | 0 app/src/{ => usb}/aoa_hid.h | 0 app/src/{ => usb}/hid_keyboard.c | 0 app/src/{ => usb}/hid_keyboard.h | 0 app/src/{ => usb}/hid_mouse.c | 0 app/src/{ => usb}/hid_mouse.h | 0 8 files changed, 8 insertions(+), 7 deletions(-) rename app/src/{ => usb}/aoa_hid.c (100%) rename app/src/{ => usb}/aoa_hid.h (100%) rename app/src/{ => usb}/hid_keyboard.c (100%) rename app/src/{ => usb}/hid_keyboard.h (100%) rename app/src/{ => usb}/hid_mouse.c (100%) rename app/src/{ => usb}/hid_mouse.h (100%) diff --git a/app/meson.build b/app/meson.build index 88b8ef8c..60888bb3 100644 --- a/app/meson.build +++ b/app/meson.build @@ -75,9 +75,9 @@ endif aoa_hid_support = host_machine.system() == 'linux' if aoa_hid_support src += [ - 'src/aoa_hid.c', - 'src/hid_keyboard.c', - 'src/hid_mouse.c', + 'src/usb/aoa_hid.c', + 'src/usb/hid_keyboard.c', + 'src/usb/hid_mouse.c', ] endif diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 1534c772..c1e20edb 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -17,16 +17,17 @@ #include "decoder.h" #include "events.h" #include "file_pusher.h" -#ifdef HAVE_AOA_HID -# include "hid_keyboard.h" -# include "hid_mouse.h" -#endif #include "keyboard_inject.h" #include "mouse_inject.h" #include "recorder.h" #include "screen.h" #include "server.h" #include "stream.h" +#ifdef HAVE_AOA_HID +# include "usb/aoa_hid.h" +# include "usb/hid_keyboard.h" +# include "usb/hid_mouse.h" +#endif #include "util/acksync.h" #include "util/log.h" #include "util/net.h" diff --git a/app/src/aoa_hid.c b/app/src/usb/aoa_hid.c similarity index 100% rename from app/src/aoa_hid.c rename to app/src/usb/aoa_hid.c diff --git a/app/src/aoa_hid.h b/app/src/usb/aoa_hid.h similarity index 100% rename from app/src/aoa_hid.h rename to app/src/usb/aoa_hid.h diff --git a/app/src/hid_keyboard.c b/app/src/usb/hid_keyboard.c similarity index 100% rename from app/src/hid_keyboard.c rename to app/src/usb/hid_keyboard.c diff --git a/app/src/hid_keyboard.h b/app/src/usb/hid_keyboard.h similarity index 100% rename from app/src/hid_keyboard.h rename to app/src/usb/hid_keyboard.h diff --git a/app/src/hid_mouse.c b/app/src/usb/hid_mouse.c similarity index 100% rename from app/src/hid_mouse.c rename to app/src/usb/hid_mouse.c diff --git a/app/src/hid_mouse.h b/app/src/usb/hid_mouse.h similarity index 100% rename from app/src/hid_mouse.h rename to app/src/usb/hid_mouse.h From d48d191262957402fa73e94d6eda15afce528a13 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 24 Jan 2022 22:29:07 +0100 Subject: [PATCH 1025/2244] Rename HAVE_AOA_HID to HAVE_USB The condition actually determines whether scrcpy can use libusb or not. PR #2974 --- app/meson.build | 8 ++++---- app/src/cli.c | 4 ++-- app/src/scrcpy.c | 16 ++++++++-------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/app/meson.build b/app/meson.build index 60888bb3..c26332e0 100644 --- a/app/meson.build +++ b/app/meson.build @@ -72,8 +72,8 @@ if v4l2_support src += [ 'src/v4l2_sink.c' ] endif -aoa_hid_support = host_machine.system() == 'linux' -if aoa_hid_support +usb_support = host_machine.system() == 'linux' +if usb_support src += [ 'src/usb/aoa_hid.c', 'src/usb/hid_keyboard.c', @@ -99,7 +99,7 @@ if not crossbuild_windows dependencies += dependency('libavdevice') endif - if aoa_hid_support + if usb_support dependencies += dependency('libusb-1.0') endif @@ -193,7 +193,7 @@ conf.set('SERVER_DEBUGGER_METHOD_NEW', get_option('server_debugger_method') == ' conf.set('HAVE_V4L2', v4l2_support) # enable HID over AOA support (linux only) -conf.set('HAVE_AOA_HID', aoa_hid_support) +conf.set('HAVE_USB', usb_support) configure_file(configuration: conf, output: 'config.h') diff --git a/app/src/cli.c b/app/src/cli.c index 60492730..34c3103a 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -1318,7 +1318,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], args->help = true; break; case 'K': -#ifdef HAVE_AOA_HID +#ifdef HAVE_USB opts->keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_HID; break; #else @@ -1337,7 +1337,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } break; case 'M': -#ifdef HAVE_AOA_HID +#ifdef HAVE_USB opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_HID; break; #else diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index c1e20edb..9321cf47 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -23,7 +23,7 @@ #include "screen.h" #include "server.h" #include "stream.h" -#ifdef HAVE_AOA_HID +#ifdef HAVE_USB # include "usb/aoa_hid.h" # include "usb/hid_keyboard.h" # include "usb/hid_mouse.h" @@ -46,20 +46,20 @@ struct scrcpy { #endif struct sc_controller controller; struct sc_file_pusher file_pusher; -#ifdef HAVE_AOA_HID +#ifdef HAVE_USB struct sc_aoa aoa; // sequence/ack helper to synchronize clipboard and Ctrl+v via HID struct sc_acksync acksync; #endif union { struct sc_keyboard_inject keyboard_inject; -#ifdef HAVE_AOA_HID +#ifdef HAVE_USB struct sc_hid_keyboard keyboard_hid; #endif }; union { struct sc_mouse_inject mouse_inject; -#ifdef HAVE_AOA_HID +#ifdef HAVE_USB struct sc_hid_mouse mouse_hid; #endif }; @@ -284,7 +284,7 @@ scrcpy(struct scrcpy_options *options) { bool v4l2_sink_initialized = false; #endif bool stream_started = false; -#ifdef HAVE_AOA_HID +#ifdef HAVE_USB bool aoa_hid_initialized = false; bool hid_keyboard_initialized = false; bool hid_mouse_initialized = false; @@ -411,7 +411,7 @@ scrcpy(struct scrcpy_options *options) { struct sc_mouse_processor *mp = NULL; if (options->control) { -#ifdef HAVE_AOA_HID +#ifdef HAVE_USB bool use_hid_keyboard = options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_HID; bool use_hid_mouse = @@ -594,7 +594,7 @@ aoa_hid_end: end: // The stream is not stopped explicitly, because it will stop by itself on // end-of-stream -#ifdef HAVE_AOA_HID +#ifdef HAVE_USB if (aoa_hid_initialized) { if (hid_keyboard_initialized) { sc_hid_keyboard_destroy(&s->keyboard_hid); @@ -635,7 +635,7 @@ end: } #endif -#ifdef HAVE_AOA_HID +#ifdef HAVE_USB if (aoa_hid_initialized) { sc_aoa_join(&s->aoa); sc_aoa_destroy(&s->aoa); From 1d6f9952ee290564bce5876183e691dbc7b01055 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 24 Jan 2022 22:56:12 +0100 Subject: [PATCH 1026/2244] Extract USB handling from AOA The AOA code handled both USB initialization and AOA commands/events. Extract USB-related code to a separate file and structure. PR #2974 --- app/meson.build | 1 + app/src/usb/aoa_hid.c | 99 +++---------------------------------- app/src/usb/aoa_hid.h | 5 +- app/src/usb/usb.c | 112 ++++++++++++++++++++++++++++++++++++++++++ app/src/usb/usb.h | 21 ++++++++ 5 files changed, 143 insertions(+), 95 deletions(-) create mode 100644 app/src/usb/usb.c create mode 100644 app/src/usb/usb.h diff --git a/app/meson.build b/app/meson.build index c26332e0..5497281a 100644 --- a/app/meson.build +++ b/app/meson.build @@ -78,6 +78,7 @@ if usb_support 'src/usb/aoa_hid.c', 'src/usb/hid_keyboard.c', 'src/usb/hid_mouse.c', + 'src/usb/usb.c', ] endif diff --git a/app/src/usb/aoa_hid.c b/app/src/usb/aoa_hid.c index a9460c3b..b03808ae 100644 --- a/app/src/usb/aoa_hid.c +++ b/app/src/usb/aoa_hid.c @@ -50,74 +50,6 @@ log_libusb_error(enum libusb_error errcode) { LOGW("libusb error: %s", libusb_strerror(errcode)); } -static bool -accept_device(libusb_device *device, const char *serial) { - // do not log any USB error in this function, it is expected that many USB - // devices available on the computer have permission restrictions - - struct libusb_device_descriptor desc; - int result = libusb_get_device_descriptor(device, &desc); - if (result < 0 || !desc.iSerialNumber) { - return false; - } - - libusb_device_handle *handle; - result = libusb_open(device, &handle); - if (result < 0) { - return false; - } - - char buffer[128]; - result = libusb_get_string_descriptor_ascii(handle, desc.iSerialNumber, - (unsigned char *) buffer, - sizeof(buffer)); - libusb_close(handle); - if (result < 0) { - return false; - } - - buffer[sizeof(buffer) - 1] = '\0'; // just in case - - // accept the device if its serial matches - return !strcmp(buffer, serial); -} - -static libusb_device * -sc_aoa_find_usb_device(const char *serial) { - if (!serial) { - return NULL; - } - - libusb_device **list; - libusb_device *result = NULL; - ssize_t count = libusb_get_device_list(NULL, &list); - if (count < 0) { - log_libusb_error((enum libusb_error) count); - return NULL; - } - - for (size_t i = 0; i < (size_t) count; ++i) { - libusb_device *device = list[i]; - - if (accept_device(device, serial)) { - result = libusb_ref_device(device); - break; - } - } - libusb_free_device_list(list, 1); - return result; -} - -static int -sc_aoa_open_usb_handle(libusb_device *device, libusb_device_handle **handle) { - int result = libusb_open(device, handle); - if (result < 0) { - log_libusb_error((enum libusb_error) result); - return result; - } - return 0; -} - bool sc_aoa_init(struct sc_aoa *aoa, const char *serial, struct sc_acksync *acksync) { @@ -133,31 +65,16 @@ sc_aoa_init(struct sc_aoa *aoa, const char *serial, goto error_destroy_mutex; } - if (libusb_init(&aoa->usb_context) != LIBUSB_SUCCESS) { + bool ok = sc_usb_init(&aoa->usb, serial); + if (!ok) { goto error_destroy_cond; } - aoa->usb_device = sc_aoa_find_usb_device(serial); - if (!aoa->usb_device) { - LOGW("USB device of serial %s not found", serial); - goto error_exit_libusb; - } - - if (sc_aoa_open_usb_handle(aoa->usb_device, &aoa->usb_handle) < 0) { - LOGW("Open USB handle failed"); - goto error_unref_device; - return false; - } - aoa->stopped = false; aoa->acksync = acksync; return true; -error_unref_device: - libusb_unref_device(aoa->usb_device); -error_exit_libusb: - libusb_exit(aoa->usb_context); error_destroy_cond: sc_cond_destroy(&aoa->event_cond); error_destroy_mutex: @@ -173,9 +90,7 @@ sc_aoa_destroy(struct sc_aoa *aoa) { sc_hid_event_destroy(&event); } - libusb_close(aoa->usb_handle); - libusb_unref_device(aoa->usb_device); - libusb_exit(aoa->usb_context); + sc_usb_destroy(&aoa->usb); sc_cond_destroy(&aoa->event_cond); sc_mutex_destroy(&aoa->mutex); } @@ -192,7 +107,7 @@ sc_aoa_register_hid(struct sc_aoa *aoa, uint16_t accessory_id, uint16_t index = report_desc_size; unsigned char *buffer = NULL; uint16_t length = 0; - int result = libusb_control_transfer(aoa->usb_handle, request_type, request, + int result = libusb_control_transfer(aoa->usb.handle, request_type, request, value, index, buffer, length, DEFAULT_TIMEOUT); if (result < 0) { @@ -228,7 +143,7 @@ sc_aoa_set_hid_report_desc(struct sc_aoa *aoa, uint16_t accessory_id, // libusb_control_transfer expects a pointer to non-const unsigned char *buffer = (unsigned char *) report_desc; uint16_t length = report_desc_size; - int result = libusb_control_transfer(aoa->usb_handle, request_type, request, + int result = libusb_control_transfer(aoa->usb.handle, request_type, request, value, index, buffer, length, DEFAULT_TIMEOUT); if (result < 0) { @@ -270,7 +185,7 @@ sc_aoa_send_hid_event(struct sc_aoa *aoa, const struct sc_hid_event *event) { uint16_t index = 0; unsigned char *buffer = event->buffer; uint16_t length = event->size; - int result = libusb_control_transfer(aoa->usb_handle, request_type, request, + int result = libusb_control_transfer(aoa->usb.handle, request_type, request, value, index, buffer, length, DEFAULT_TIMEOUT); if (result < 0) { @@ -292,7 +207,7 @@ sc_aoa_unregister_hid(struct sc_aoa *aoa, const uint16_t accessory_id) { uint16_t index = 0; unsigned char *buffer = NULL; uint16_t length = 0; - int result = libusb_control_transfer(aoa->usb_handle, request_type, request, + int result = libusb_control_transfer(aoa->usb.handle, request_type, request, value, index, buffer, length, DEFAULT_TIMEOUT); if (result < 0) { diff --git a/app/src/usb/aoa_hid.h b/app/src/usb/aoa_hid.h index e8fb9708..0ce78626 100644 --- a/app/src/usb/aoa_hid.h +++ b/app/src/usb/aoa_hid.h @@ -6,6 +6,7 @@ #include +#include "usb.h" #include "util/acksync.h" #include "util/cbuf.h" #include "util/thread.h" @@ -29,9 +30,7 @@ sc_hid_event_destroy(struct sc_hid_event *hid_event); struct sc_hid_event_queue CBUF(struct sc_hid_event, 64); struct sc_aoa { - libusb_context *usb_context; - libusb_device *usb_device; - libusb_device_handle *usb_handle; + struct sc_usb usb; sc_thread thread; sc_mutex mutex; sc_cond event_cond; diff --git a/app/src/usb/usb.c b/app/src/usb/usb.c new file mode 100644 index 00000000..e933fdbc --- /dev/null +++ b/app/src/usb/usb.c @@ -0,0 +1,112 @@ +#include "usb.h" + +#include + +#include "util/log.h" + +static inline void +log_libusb_error(enum libusb_error errcode) { + LOGW("libusb error: %s", libusb_strerror(errcode)); +} + +static bool +accept_device(libusb_device *device, const char *serial) { + // Do not log any USB error in this function, it is expected that many USB + // devices available on the computer have permission restrictions + + struct libusb_device_descriptor desc; + int result = libusb_get_device_descriptor(device, &desc); + if (result < 0 || !desc.iSerialNumber) { + return false; + } + + libusb_device_handle *handle; + result = libusb_open(device, &handle); + if (result < 0) { + return false; + } + + char buffer[128]; + result = libusb_get_string_descriptor_ascii(handle, desc.iSerialNumber, + (unsigned char *) buffer, + sizeof(buffer)); + libusb_close(handle); + if (result < 0) { + return false; + } + + buffer[sizeof(buffer) - 1] = '\0'; // just in case + + // Accept the device if its serial matches + return !strcmp(buffer, serial); +} + +static libusb_device * +sc_usb_find_device(const char *serial) { + if (!serial) { + return NULL; + } + + libusb_device **list; + libusb_device *result = NULL; + ssize_t count = libusb_get_device_list(NULL, &list); + if (count < 0) { + log_libusb_error((enum libusb_error) count); + return NULL; + } + + for (size_t i = 0; i < (size_t) count; ++i) { + libusb_device *device = list[i]; + + if (accept_device(device, serial)) { + result = libusb_ref_device(device); + break; + } + } + + libusb_free_device_list(list, 1); + return result; +} + +static libusb_device_handle * +sc_usb_open_handle(libusb_device *device) { + libusb_device_handle *handle; + int result = libusb_open(device, &handle); + if (result < 0) { + log_libusb_error((enum libusb_error) result); + return NULL; + } + return handle; + } + +bool +sc_usb_init(struct sc_usb *usb, const char *serial) { + // There is only one device, initialize the context here + if (libusb_init(&usb->context) != LIBUSB_SUCCESS) { + return false; + } + + usb->device = sc_usb_find_device(serial); + if (!usb->device) { + LOGW("USB device %s not found", serial); + libusb_exit(usb->context); + return false; + } + + usb->handle = sc_usb_open_handle(usb->device); + if (!usb->handle) { + LOGW("Could not open USB device %s", serial); + libusb_unref_device(usb->device); + libusb_exit(usb->context); + return false; + } + + return true; +} + +void +sc_usb_destroy(struct sc_usb *usb) { + libusb_close(usb->handle); + libusb_unref_device(usb->device); + libusb_exit(usb->context); +} diff --git a/app/src/usb/usb.h b/app/src/usb/usb.h new file mode 100644 index 00000000..5743626d --- /dev/null +++ b/app/src/usb/usb.h @@ -0,0 +1,21 @@ +#ifndef SC_USB_H +#define SC_USB_H + +#include "common.h" + +#include +#include + +struct sc_usb { + libusb_context *context; + libusb_device *device; + libusb_device_handle *handle; +}; + +bool +sc_usb_init(struct sc_usb *usb, const char *serial); + +void +sc_usb_destroy(struct sc_usb *usb); + +#endif From 48e3ff284f538c470e0bf15cc91b8c3430304bf4 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 26 Jan 2022 21:38:28 +0100 Subject: [PATCH 1027/2244] Make serial mandatory for sc_usb In practice, it is already mandatory. PR #2974 --- app/src/usb/usb.c | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/src/usb/usb.c b/app/src/usb/usb.c index e933fdbc..ebd218dc 100644 --- a/app/src/usb/usb.c +++ b/app/src/usb/usb.c @@ -43,10 +43,6 @@ accept_device(libusb_device *device, const char *serial) { static libusb_device * sc_usb_find_device(const char *serial) { - if (!serial) { - return NULL; - } - libusb_device **list; libusb_device *result = NULL; ssize_t count = libusb_get_device_list(NULL, &list); @@ -81,6 +77,8 @@ sc_usb_open_handle(libusb_device *device) { bool sc_usb_init(struct sc_usb *usb, const char *serial) { + assert(serial); + // There is only one device, initialize the context here if (libusb_init(&usb->context) != LIBUSB_SUCCESS) { return false; From adda47b0f72eb18fa0abc68c67acd435eccdd310 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 24 Jan 2022 23:11:42 +0100 Subject: [PATCH 1028/2244] Move sc_usb out of sc_aoa This will allow to initialize a USB device separately and pass it to sc_aoa. PR #2974 --- app/src/scrcpy.c | 14 +++++++++++++- app/src/usb/aoa_hid.c | 34 ++++++++++++---------------------- app/src/usb/aoa_hid.h | 4 ++-- 3 files changed, 27 insertions(+), 25 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 9321cf47..0c7966ac 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -27,6 +27,7 @@ # include "usb/aoa_hid.h" # include "usb/hid_keyboard.h" # include "usb/hid_mouse.h" +# include "usb/usb.h" #endif #include "util/acksync.h" #include "util/log.h" @@ -47,6 +48,7 @@ struct scrcpy { struct sc_controller controller; struct sc_file_pusher file_pusher; #ifdef HAVE_USB + struct sc_usb usb; struct sc_aoa aoa; // sequence/ack helper to synchronize clipboard and Ctrl+v via HID struct sc_acksync acksync; @@ -422,9 +424,17 @@ scrcpy(struct scrcpy_options *options) { goto end; } - ok = sc_aoa_init(&s->aoa, serial, &s->acksync); + ok = sc_usb_init(&s->usb, serial); + if (!ok) { + LOGE("Failed to initialized USB device"); + sc_acksync_destroy(&s->acksync); + goto aoa_hid_end; + } + + ok = sc_aoa_init(&s->aoa, &s->usb, &s->acksync); if (!ok) { LOGE("Failed to enable HID over AOA"); + sc_usb_destroy(&s->usb); sc_acksync_destroy(&s->acksync); goto aoa_hid_end; } @@ -451,6 +461,7 @@ scrcpy(struct scrcpy_options *options) { if (!need_aoa || !sc_aoa_start(&s->aoa)) { sc_acksync_destroy(&s->acksync); + sc_usb_destroy(&s->usb); sc_aoa_destroy(&s->aoa); goto aoa_hid_end; } @@ -639,6 +650,7 @@ end: if (aoa_hid_initialized) { sc_aoa_join(&s->aoa); sc_aoa_destroy(&s->aoa); + sc_usb_destroy(&s->usb); } #endif diff --git a/app/src/usb/aoa_hid.c b/app/src/usb/aoa_hid.c index b03808ae..0925d9e4 100644 --- a/app/src/usb/aoa_hid.c +++ b/app/src/usb/aoa_hid.c @@ -51,7 +51,7 @@ log_libusb_error(enum libusb_error errcode) { } bool -sc_aoa_init(struct sc_aoa *aoa, const char *serial, +sc_aoa_init(struct sc_aoa *aoa, struct sc_usb *usb, struct sc_acksync *acksync) { assert(acksync); @@ -62,24 +62,15 @@ sc_aoa_init(struct sc_aoa *aoa, const char *serial, } if (!sc_cond_init(&aoa->event_cond)) { - goto error_destroy_mutex; - } - - bool ok = sc_usb_init(&aoa->usb, serial); - if (!ok) { - goto error_destroy_cond; + sc_mutex_destroy(&aoa->mutex); + return false; } aoa->stopped = false; aoa->acksync = acksync; + aoa->usb = usb; return true; - -error_destroy_cond: - sc_cond_destroy(&aoa->event_cond); -error_destroy_mutex: - sc_mutex_destroy(&aoa->mutex); - return false; } void @@ -90,7 +81,6 @@ sc_aoa_destroy(struct sc_aoa *aoa) { sc_hid_event_destroy(&event); } - sc_usb_destroy(&aoa->usb); sc_cond_destroy(&aoa->event_cond); sc_mutex_destroy(&aoa->mutex); } @@ -107,8 +97,8 @@ sc_aoa_register_hid(struct sc_aoa *aoa, uint16_t accessory_id, uint16_t index = report_desc_size; unsigned char *buffer = NULL; uint16_t length = 0; - int result = libusb_control_transfer(aoa->usb.handle, request_type, request, - value, index, buffer, length, + int result = libusb_control_transfer(aoa->usb->handle, request_type, + request, value, index, buffer, length, DEFAULT_TIMEOUT); if (result < 0) { log_libusb_error((enum libusb_error) result); @@ -143,8 +133,8 @@ sc_aoa_set_hid_report_desc(struct sc_aoa *aoa, uint16_t accessory_id, // libusb_control_transfer expects a pointer to non-const unsigned char *buffer = (unsigned char *) report_desc; uint16_t length = report_desc_size; - int result = libusb_control_transfer(aoa->usb.handle, request_type, request, - value, index, buffer, length, + int result = libusb_control_transfer(aoa->usb->handle, request_type, + request, value, index, buffer, length, DEFAULT_TIMEOUT); if (result < 0) { log_libusb_error((enum libusb_error) result); @@ -185,8 +175,8 @@ sc_aoa_send_hid_event(struct sc_aoa *aoa, const struct sc_hid_event *event) { uint16_t index = 0; unsigned char *buffer = event->buffer; uint16_t length = event->size; - int result = libusb_control_transfer(aoa->usb.handle, request_type, request, - value, index, buffer, length, + int result = libusb_control_transfer(aoa->usb->handle, request_type, + request, value, index, buffer, length, DEFAULT_TIMEOUT); if (result < 0) { log_libusb_error((enum libusb_error) result); @@ -207,8 +197,8 @@ sc_aoa_unregister_hid(struct sc_aoa *aoa, const uint16_t accessory_id) { uint16_t index = 0; unsigned char *buffer = NULL; uint16_t length = 0; - int result = libusb_control_transfer(aoa->usb.handle, request_type, request, - value, index, buffer, length, + int result = libusb_control_transfer(aoa->usb->handle, request_type, + request, value, index, buffer, length, DEFAULT_TIMEOUT); if (result < 0) { log_libusb_error((enum libusb_error) result); diff --git a/app/src/usb/aoa_hid.h b/app/src/usb/aoa_hid.h index 0ce78626..d785a0e9 100644 --- a/app/src/usb/aoa_hid.h +++ b/app/src/usb/aoa_hid.h @@ -30,7 +30,7 @@ sc_hid_event_destroy(struct sc_hid_event *hid_event); struct sc_hid_event_queue CBUF(struct sc_hid_event, 64); struct sc_aoa { - struct sc_usb usb; + struct sc_usb *usb; sc_thread thread; sc_mutex mutex; sc_cond event_cond; @@ -41,7 +41,7 @@ struct sc_aoa { }; bool -sc_aoa_init(struct sc_aoa *aoa, const char *serial, struct sc_acksync *acksync); +sc_aoa_init(struct sc_aoa *aoa, struct sc_usb *usb, struct sc_acksync *acksync); void sc_aoa_destroy(struct sc_aoa *aoa); From b779eca8d37896072ddc5017f1c63a3056515869 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 26 Jan 2022 21:40:46 +0100 Subject: [PATCH 1029/2244] Remove libusb_device field It is possible to retrieve the device instance from the handle via libusb_get_device(), so we don't need to reference the device one more time. PR #2974 --- app/src/usb/usb.c | 9 ++++----- app/src/usb/usb.h | 1 - 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/app/src/usb/usb.c b/app/src/usb/usb.c index ebd218dc..44d1d489 100644 --- a/app/src/usb/usb.c +++ b/app/src/usb/usb.c @@ -84,17 +84,17 @@ sc_usb_init(struct sc_usb *usb, const char *serial) { return false; } - usb->device = sc_usb_find_device(serial); - if (!usb->device) { + libusb_device *device = sc_usb_find_device(serial); + if (!device) { LOGW("USB device %s not found", serial); libusb_exit(usb->context); return false; } - usb->handle = sc_usb_open_handle(usb->device); + usb->handle = sc_usb_open_handle(device); + libusb_unref_device(device); if (!usb->handle) { LOGW("Could not open USB device %s", serial); - libusb_unref_device(usb->device); libusb_exit(usb->context); return false; } @@ -105,6 +105,5 @@ sc_usb_init(struct sc_usb *usb, const char *serial) { void sc_usb_destroy(struct sc_usb *usb) { libusb_close(usb->handle); - libusb_unref_device(usb->device); libusb_exit(usb->context); } diff --git a/app/src/usb/usb.h b/app/src/usb/usb.h index 5743626d..8ee3eb9f 100644 --- a/app/src/usb/usb.h +++ b/app/src/usb/usb.h @@ -8,7 +8,6 @@ struct sc_usb { libusb_context *context; - libusb_device *device; libusb_device_handle *handle; }; From 2114f48185ff69e8b2849c75d4b77c9352a3b6f6 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 26 Jan 2022 21:42:26 +0100 Subject: [PATCH 1030/2244] Find device with USB context An explicit context was used everywhere except for listing the devices. PR #2974 --- app/src/usb/usb.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/usb/usb.c b/app/src/usb/usb.c index 44d1d489..321d745c 100644 --- a/app/src/usb/usb.c +++ b/app/src/usb/usb.c @@ -42,10 +42,10 @@ accept_device(libusb_device *device, const char *serial) { } static libusb_device * -sc_usb_find_device(const char *serial) { +sc_usb_find_device(struct sc_usb *usb, const char *serial) { libusb_device **list; libusb_device *result = NULL; - ssize_t count = libusb_get_device_list(NULL, &list); + ssize_t count = libusb_get_device_list(usb->context, &list); if (count < 0) { log_libusb_error((enum libusb_error) count); return NULL; @@ -84,7 +84,7 @@ sc_usb_init(struct sc_usb *usb, const char *serial) { return false; } - libusb_device *device = sc_usb_find_device(serial); + libusb_device *device = sc_usb_find_device(usb, serial); if (!device) { LOGW("USB device %s not found", serial); libusb_exit(usb->context); From bbef426a4bb69dc96fbd8c1075c0e569d71fdff4 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 25 Jan 2022 19:10:23 +0100 Subject: [PATCH 1031/2244] Split USB initialization and connection This will allow to execute other USB calls (retrieving the device list for example) before connecting to the selected device. PR #2974 --- app/src/scrcpy.c | 15 +++++++++++++-- app/src/usb/usb.c | 23 +++++++++++++---------- app/src/usb/usb.h | 8 +++++++- 3 files changed, 33 insertions(+), 13 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 0c7966ac..3de683db 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -424,9 +424,17 @@ scrcpy(struct scrcpy_options *options) { goto end; } - ok = sc_usb_init(&s->usb, serial); + ok = sc_usb_init(&s->usb); if (!ok) { - LOGE("Failed to initialized USB device"); + LOGE("Failed to initialize USB"); + sc_acksync_destroy(&s->acksync); + goto aoa_hid_end; + } + + ok = sc_usb_connect(&s->usb, serial); + if (!ok) { + LOGE("Failed to connect to USB device %s", serial); + sc_usb_destroy(&s->usb); sc_acksync_destroy(&s->acksync); goto aoa_hid_end; } @@ -434,6 +442,7 @@ scrcpy(struct scrcpy_options *options) { ok = sc_aoa_init(&s->aoa, &s->usb, &s->acksync); if (!ok) { LOGE("Failed to enable HID over AOA"); + sc_usb_disconnect(&s->usb); sc_usb_destroy(&s->usb); sc_acksync_destroy(&s->acksync); goto aoa_hid_end; @@ -461,6 +470,7 @@ scrcpy(struct scrcpy_options *options) { if (!need_aoa || !sc_aoa_start(&s->aoa)) { sc_acksync_destroy(&s->acksync); + sc_usb_disconnect(&s->usb); sc_usb_destroy(&s->usb); sc_aoa_destroy(&s->aoa); goto aoa_hid_end; @@ -650,6 +660,7 @@ end: if (aoa_hid_initialized) { sc_aoa_join(&s->aoa); sc_aoa_destroy(&s->aoa); + sc_usb_disconnect(&s->usb); sc_usb_destroy(&s->usb); } #endif diff --git a/app/src/usb/usb.c b/app/src/usb/usb.c index 321d745c..64f30353 100644 --- a/app/src/usb/usb.c +++ b/app/src/usb/usb.c @@ -76,18 +76,23 @@ sc_usb_open_handle(libusb_device *device) { } bool -sc_usb_init(struct sc_usb *usb, const char *serial) { - assert(serial); +sc_usb_init(struct sc_usb *usb) { + usb->handle = NULL; + return libusb_init(&usb->context) == LIBUSB_SUCCESS; +} - // There is only one device, initialize the context here - if (libusb_init(&usb->context) != LIBUSB_SUCCESS) { - return false; - } +void +sc_usb_destroy(struct sc_usb *usb) { + libusb_exit(usb->context); +} + +bool +sc_usb_connect(struct sc_usb *usb, const char *serial) { + assert(serial); libusb_device *device = sc_usb_find_device(usb, serial); if (!device) { LOGW("USB device %s not found", serial); - libusb_exit(usb->context); return false; } @@ -95,7 +100,6 @@ sc_usb_init(struct sc_usb *usb, const char *serial) { libusb_unref_device(device); if (!usb->handle) { LOGW("Could not open USB device %s", serial); - libusb_exit(usb->context); return false; } @@ -103,7 +107,6 @@ sc_usb_init(struct sc_usb *usb, const char *serial) { } void -sc_usb_destroy(struct sc_usb *usb) { +sc_usb_disconnect(struct sc_usb *usb) { libusb_close(usb->handle); - libusb_exit(usb->context); } diff --git a/app/src/usb/usb.h b/app/src/usb/usb.h index 8ee3eb9f..e487a32c 100644 --- a/app/src/usb/usb.h +++ b/app/src/usb/usb.h @@ -12,9 +12,15 @@ struct sc_usb { }; bool -sc_usb_init(struct sc_usb *usb, const char *serial); +sc_usb_init(struct sc_usb *usb); void sc_usb_destroy(struct sc_usb *usb); +bool +sc_usb_connect(struct sc_usb *usb, const char *serial); + +void +sc_usb_disconnect(struct sc_usb *usb); + #endif From 1ab3692f3dec0f28e2f228d604a037f1a801d337 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 25 Jan 2022 19:40:51 +0100 Subject: [PATCH 1032/2244] Add util function to read USB descriptor string Use it from accept_device() to simplify (at the cost an additional allocation for each serial, but it is not important). It will also be useful in other functions in further commits. PR #2974 --- app/src/usb/usb.c | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/app/src/usb/usb.c b/app/src/usb/usb.c index 64f30353..e2bbaee1 100644 --- a/app/src/usb/usb.c +++ b/app/src/usb/usb.c @@ -9,6 +9,26 @@ log_libusb_error(enum libusb_error errcode) { LOGW("libusb error: %s", libusb_strerror(errcode)); } +static char * +read_string(libusb_device_handle *handle, uint8_t desc_index) { + char buffer[128]; + int result = + libusb_get_string_descriptor_ascii(handle, desc_index, + (unsigned char *) buffer, + sizeof(buffer)); + if (result < 0) { + return NULL; + } + + assert((size_t) result <= sizeof(buffer)); + + // When non-negative, 'result' contains the number of bytes written + char *s = malloc(result + 1); + memcpy(s, buffer, result); + s[result] = '\0'; + return s; +} + static bool accept_device(libusb_device *device, const char *serial) { // Do not log any USB error in this function, it is expected that many USB @@ -26,19 +46,15 @@ accept_device(libusb_device *device, const char *serial) { return false; } - char buffer[128]; - result = libusb_get_string_descriptor_ascii(handle, desc.iSerialNumber, - (unsigned char *) buffer, - sizeof(buffer)); + char *device_serial = read_string(handle, desc.iSerialNumber); libusb_close(handle); - if (result < 0) { + if (!device_serial) { return false; } - buffer[sizeof(buffer) - 1] = '\0'; // just in case - - // Accept the device if its serial matches - return !strcmp(buffer, serial); + bool matches = !strcmp(serial, device_serial); + free(device_serial); + return matches; } static libusb_device * From 0ee9e2ff51baa4826283b521053a383550b93ea1 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 25 Jan 2022 21:11:32 +0100 Subject: [PATCH 1033/2244] Expose function to find a USB device The device was automatically found by sc_usb_connect(). Instead, expose a function to find a device from a serial, and let the caller connect to the device found (if any). This will allow to list all devices first, then select one device to connect to. PR #2974 --- app/src/scrcpy.c | 12 +++++++++++- app/src/usb/usb.c | 16 ++++------------ app/src/usb/usb.h | 5 ++++- 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 3de683db..9039eede 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -431,7 +431,17 @@ scrcpy(struct scrcpy_options *options) { goto aoa_hid_end; } - ok = sc_usb_connect(&s->usb, serial); + assert(serial); + libusb_device *device = sc_usb_find_device(&s->usb, serial); + if (!device) { + LOGE("Could not find USB device %s", serial); + sc_usb_destroy(&s->usb); + sc_acksync_destroy(&s->acksync); + goto aoa_hid_end; + } + + ok = sc_usb_connect(&s->usb, device); + libusb_unref_device(device); if (!ok) { LOGE("Failed to connect to USB device %s", serial); sc_usb_destroy(&s->usb); diff --git a/app/src/usb/usb.c b/app/src/usb/usb.c index e2bbaee1..459aefc0 100644 --- a/app/src/usb/usb.c +++ b/app/src/usb/usb.c @@ -57,8 +57,10 @@ accept_device(libusb_device *device, const char *serial) { return matches; } -static libusb_device * +libusb_device * sc_usb_find_device(struct sc_usb *usb, const char *serial) { + assert(serial); + libusb_device **list; libusb_device *result = NULL; ssize_t count = libusb_get_device_list(usb->context, &list); @@ -103,19 +105,9 @@ sc_usb_destroy(struct sc_usb *usb) { } bool -sc_usb_connect(struct sc_usb *usb, const char *serial) { - assert(serial); - - libusb_device *device = sc_usb_find_device(usb, serial); - if (!device) { - LOGW("USB device %s not found", serial); - return false; - } - +sc_usb_connect(struct sc_usb *usb, libusb_device *device) { usb->handle = sc_usb_open_handle(device); - libusb_unref_device(device); if (!usb->handle) { - LOGW("Could not open USB device %s", serial); return false; } diff --git a/app/src/usb/usb.h b/app/src/usb/usb.h index e487a32c..0ab7ce90 100644 --- a/app/src/usb/usb.h +++ b/app/src/usb/usb.h @@ -17,8 +17,11 @@ sc_usb_init(struct sc_usb *usb); void sc_usb_destroy(struct sc_usb *usb); +libusb_device * +sc_usb_find_device(struct sc_usb *usb, const char *serial); + bool -sc_usb_connect(struct sc_usb *usb, const char *serial); +sc_usb_connect(struct sc_usb *usb, libusb_device *device); void sc_usb_disconnect(struct sc_usb *usb); From d8b37fe189d5cacf9bac8162d25c58c1a6890a6c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 26 Jan 2022 22:02:24 +0100 Subject: [PATCH 1034/2244] Wrap libusb_device Introduce a structure to wrap a libusb_device and expose its descriptor data read during discovery. PR #2974 --- app/src/scrcpy.c | 13 +++++++++---- app/src/usb/usb.c | 47 +++++++++++++++++++++++++++++++++++------------ app/src/usb/usb.h | 17 +++++++++++++++-- 3 files changed, 59 insertions(+), 18 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 9039eede..7d58b0f1 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -432,16 +432,21 @@ scrcpy(struct scrcpy_options *options) { } assert(serial); - libusb_device *device = sc_usb_find_device(&s->usb, serial); - if (!device) { + struct sc_usb_device usb_device; + ok = sc_usb_find_device(&s->usb, serial, &usb_device); + if (!ok) { LOGE("Could not find USB device %s", serial); sc_usb_destroy(&s->usb); sc_acksync_destroy(&s->acksync); goto aoa_hid_end; } - ok = sc_usb_connect(&s->usb, device); - libusb_unref_device(device); + LOGI("USB device: %s (%04" PRIx16 ":%04" PRIx16 ") %s %s", + usb_device.serial, usb_device.vid, usb_device.pid, + usb_device.manufacturer, usb_device.product); + + ok = sc_usb_connect(&s->usb, usb_device.device); + sc_usb_device_destroy(&usb_device); if (!ok) { LOGE("Failed to connect to USB device %s", serial); sc_usb_destroy(&s->usb); diff --git a/app/src/usb/usb.c b/app/src/usb/usb.c index 459aefc0..9d5f35be 100644 --- a/app/src/usb/usb.c +++ b/app/src/usb/usb.c @@ -30,7 +30,8 @@ read_string(libusb_device_handle *handle, uint8_t desc_index) { } static bool -accept_device(libusb_device *device, const char *serial) { +accept_device(libusb_device *device, const char *serial, + struct sc_usb_device *out) { // Do not log any USB error in this function, it is expected that many USB // devices available on the computer have permission restrictions @@ -47,39 +48,61 @@ accept_device(libusb_device *device, const char *serial) { } char *device_serial = read_string(handle, desc.iSerialNumber); - libusb_close(handle); if (!device_serial) { + libusb_close(handle); return false; } bool matches = !strcmp(serial, device_serial); - free(device_serial); - return matches; + if (!matches) { + free(device_serial); + libusb_close(handle); + return false; + } + + out->device = libusb_ref_device(device); + out->serial = device_serial; + out->vid = desc.idVendor; + out->pid = desc.idProduct; + out->manufacturer = read_string(handle, desc.iManufacturer); + out->product = read_string(handle, desc.iProduct); + + libusb_close(handle); + + return true; } -libusb_device * -sc_usb_find_device(struct sc_usb *usb, const char *serial) { +void +sc_usb_device_destroy(struct sc_usb_device *usb_device) { + libusb_unref_device(usb_device->device); + free(usb_device->serial); + free(usb_device->manufacturer); + free(usb_device->product); +} + +bool +sc_usb_find_device(struct sc_usb *usb, const char *serial, + struct sc_usb_device *out) { assert(serial); libusb_device **list; - libusb_device *result = NULL; ssize_t count = libusb_get_device_list(usb->context, &list); if (count < 0) { log_libusb_error((enum libusb_error) count); - return NULL; + return false; } for (size_t i = 0; i < (size_t) count; ++i) { libusb_device *device = list[i]; - if (accept_device(device, serial)) { - result = libusb_ref_device(device); - break; + if (accept_device(device, serial, out)) { + libusb_free_device_list(list, 1); + return true; } } libusb_free_device_list(list, 1); - return result; + return false; } static libusb_device_handle * diff --git a/app/src/usb/usb.h b/app/src/usb/usb.h index 0ab7ce90..ea6e5514 100644 --- a/app/src/usb/usb.h +++ b/app/src/usb/usb.h @@ -11,14 +11,27 @@ struct sc_usb { libusb_device_handle *handle; }; +struct sc_usb_device { + libusb_device *device; + char *serial; + char *manufacturer; + char *product; + uint16_t vid; + uint16_t pid; +}; + +void +sc_usb_device_destroy(struct sc_usb_device *usb_device); + bool sc_usb_init(struct sc_usb *usb); void sc_usb_destroy(struct sc_usb *usb); -libusb_device * -sc_usb_find_device(struct sc_usb *usb, const char *serial); +bool +sc_usb_find_device(struct sc_usb *usb, const char *serial, + struct sc_usb_device *out); bool sc_usb_connect(struct sc_usb *usb, libusb_device *device); From 1c17f57c101fc08fd54c99b5c6c947414f5f35be Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 26 Jan 2022 22:08:55 +0100 Subject: [PATCH 1035/2244] Find a list of devices instead of a single one Several devices may match the requested serial, but above all, this paves the way to list all devices (when no serial is provided). PR #2974 --- app/src/scrcpy.c | 27 +++++++++++++++++++-------- app/src/usb/usb.c | 25 ++++++++++++++++--------- app/src/usb/usb.h | 9 ++++++--- 3 files changed, 41 insertions(+), 20 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 7d58b0f1..1c84b428 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -432,21 +432,32 @@ scrcpy(struct scrcpy_options *options) { } assert(serial); - struct sc_usb_device usb_device; - ok = sc_usb_find_device(&s->usb, serial, &usb_device); - if (!ok) { + struct sc_usb_device usb_devices[16]; + ssize_t count = sc_usb_find_devices(&s->usb, serial, usb_devices, + ARRAY_LEN(usb_devices)); + if (count <= 0) { LOGE("Could not find USB device %s", serial); sc_usb_destroy(&s->usb); sc_acksync_destroy(&s->acksync); goto aoa_hid_end; } - LOGI("USB device: %s (%04" PRIx16 ":%04" PRIx16 ") %s %s", - usb_device.serial, usb_device.vid, usb_device.pid, - usb_device.manufacturer, usb_device.product); + if (count > 1) { + LOGE("Multiple (%d) devices with serial %s", (int) count, serial); + sc_usb_device_destroy_all(usb_devices, count); + sc_usb_destroy(&s->usb); + sc_acksync_destroy(&s->acksync); + goto aoa_hid_end; + } - ok = sc_usb_connect(&s->usb, usb_device.device); - sc_usb_device_destroy(&usb_device); + struct sc_usb_device *usb_device = &usb_devices[0]; + + LOGI("USB device: %s (%04" PRIx16 ":%04" PRIx16 ") %s %s", + usb_device->serial, usb_device->vid, usb_device->pid, + usb_device->manufacturer, usb_device->product); + + ok = sc_usb_connect(&s->usb, usb_device->device); + sc_usb_device_destroy(usb_device); if (!ok) { LOGE("Failed to connect to USB device %s", serial); sc_usb_destroy(&s->usb); diff --git a/app/src/usb/usb.c b/app/src/usb/usb.c index 9d5f35be..6add7b2e 100644 --- a/app/src/usb/usb.c +++ b/app/src/usb/usb.c @@ -80,29 +80,36 @@ sc_usb_device_destroy(struct sc_usb_device *usb_device) { free(usb_device->product); } -bool -sc_usb_find_device(struct sc_usb *usb, const char *serial, - struct sc_usb_device *out) { +void +sc_usb_device_destroy_all(struct sc_usb_device *usb_devices, size_t count) { + for (size_t i = 0; i < count; ++i) { + sc_usb_device_destroy(&usb_devices[i]); + } +} + +ssize_t +sc_usb_find_devices(struct sc_usb *usb, const char *serial, + struct sc_usb_device *devices, size_t len) { assert(serial); libusb_device **list; ssize_t count = libusb_get_device_list(usb->context, &list); if (count < 0) { log_libusb_error((enum libusb_error) count); - return false; + return -1; } - for (size_t i = 0; i < (size_t) count; ++i) { + size_t idx = 0; + for (size_t i = 0; i < (size_t) count && idx < len; ++i) { libusb_device *device = list[i]; - if (accept_device(device, serial, out)) { - libusb_free_device_list(list, 1); - return true; + if (accept_device(device, serial, &devices[idx])) { + ++idx; } } libusb_free_device_list(list, 1); - return false; + return idx; } static libusb_device_handle * diff --git a/app/src/usb/usb.h b/app/src/usb/usb.h index ea6e5514..a271d034 100644 --- a/app/src/usb/usb.h +++ b/app/src/usb/usb.h @@ -23,15 +23,18 @@ struct sc_usb_device { void sc_usb_device_destroy(struct sc_usb_device *usb_device); +void +sc_usb_device_destroy_all(struct sc_usb_device *usb_devices, size_t count); + bool sc_usb_init(struct sc_usb *usb); void sc_usb_destroy(struct sc_usb *usb); -bool -sc_usb_find_device(struct sc_usb *usb, const char *serial, - struct sc_usb_device *out); +ssize_t +sc_usb_find_devices(struct sc_usb *usb, const char *serial, + struct sc_usb_device *devices, size_t len); bool sc_usb_connect(struct sc_usb *usb, libusb_device *device); From 8fc9dca8cb4d35fb329826fffa00e1403eb7ed43 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 26 Jan 2022 22:15:23 +0100 Subject: [PATCH 1036/2244] Make serial optional to find USB devices If no serial is provided, then list all available USB devices (which can be open and having a serial). PR #2974 --- app/src/usb/usb.c | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/app/src/usb/usb.c b/app/src/usb/usb.c index 6add7b2e..8140b674 100644 --- a/app/src/usb/usb.c +++ b/app/src/usb/usb.c @@ -53,11 +53,14 @@ accept_device(libusb_device *device, const char *serial, return false; } - bool matches = !strcmp(serial, device_serial); - if (!matches) { - free(device_serial); - libusb_close(handle); - return false; + if (serial) { + // Filter by serial + bool matches = !strcmp(serial, device_serial); + if (!matches) { + free(device_serial); + libusb_close(handle); + return false; + } } out->device = libusb_ref_device(device); @@ -90,8 +93,6 @@ sc_usb_device_destroy_all(struct sc_usb_device *usb_devices, size_t count) { ssize_t sc_usb_find_devices(struct sc_usb *usb, const char *serial, struct sc_usb_device *devices, size_t len) { - assert(serial); - libusb_device **list; ssize_t count = libusb_get_device_list(usb->context, &list); if (count < 0) { From 37987b822e8f0f57f27c867ef6c573f98791b5bd Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 22 Jan 2022 10:52:13 +0100 Subject: [PATCH 1037/2244] Make acksync optional for AOA initialization Acksync is used to delay HID events until some request (in practice, device clipboard synchronization) is acknowledged by the device. This mechanism will not be necessary for OTG mode. PR #2974 --- app/src/usb/aoa_hid.c | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/app/src/usb/aoa_hid.c b/app/src/usb/aoa_hid.c index 0925d9e4..6e3bd2e7 100644 --- a/app/src/usb/aoa_hid.c +++ b/app/src/usb/aoa_hid.c @@ -53,8 +53,6 @@ log_libusb_error(enum libusb_error errcode) { bool sc_aoa_init(struct sc_aoa *aoa, struct sc_usb *usb, struct sc_acksync *acksync) { - assert(acksync); - cbuf_init(&aoa->queue); if (!sc_mutex_init(&aoa->mutex)) { @@ -248,6 +246,11 @@ run_aoa_thread(void *data) { if (ack_to_wait != SC_SEQUENCE_INVALID) { LOGD("Waiting ack from server sequence=%" PRIu64_, ack_to_wait); + + // If some events have ack_to_wait set, then sc_aoa must have been + // initialized with a non NULL acksync + assert(aoa->acksync); + // Do not block the loop indefinitely if the ack never comes (it // should never happen) sc_tick deadline = sc_tick_now() + SC_TICK_FROM_MS(500); @@ -294,7 +297,9 @@ sc_aoa_stop(struct sc_aoa *aoa) { sc_cond_signal(&aoa->event_cond); sc_mutex_unlock(&aoa->mutex); - sc_acksync_interrupt(aoa->acksync); + if (aoa->acksync) { + sc_acksync_interrupt(aoa->acksync); + } } void From 1a03206e36b6065b421b3e7635a48211cdf0ce4a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 24 Jan 2022 20:02:05 +0100 Subject: [PATCH 1038/2244] Detect USB device disconnection The device disconnection is detected when the video socket closes. In order to introduce an OTG mode (HID events) without mirroring (and without server), we must be able to detect USB device disconnection. This feature will only be used in OTG mode. PR #2974 --- app/src/scrcpy.c | 4 +- app/src/usb/usb.c | 115 +++++++++++++++++++++++++++++++++++++++++++++- app/src/usb/usb.h | 26 ++++++++++- 3 files changed, 142 insertions(+), 3 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 1c84b428..5f67f7f8 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -456,7 +456,7 @@ scrcpy(struct scrcpy_options *options) { usb_device->serial, usb_device->vid, usb_device->pid, usb_device->manufacturer, usb_device->product); - ok = sc_usb_connect(&s->usb, usb_device->device); + ok = sc_usb_connect(&s->usb, usb_device->device, NULL, NULL); sc_usb_device_destroy(usb_device); if (!ok) { LOGE("Failed to connect to USB device %s", serial); @@ -650,6 +650,7 @@ end: sc_hid_mouse_destroy(&s->mouse_hid); } sc_aoa_stop(&s->aoa); + sc_usb_stop(&s->usb); } if (acksync) { sc_acksync_destroy(acksync); @@ -686,6 +687,7 @@ end: if (aoa_hid_initialized) { sc_aoa_join(&s->aoa); sc_aoa_destroy(&s->aoa); + sc_usb_join(&s->usb); sc_usb_disconnect(&s->usb); sc_usb_destroy(&s->usb); } diff --git a/app/src/usb/usb.c b/app/src/usb/usb.c index 8140b674..729df7f0 100644 --- a/app/src/usb/usb.c +++ b/app/src/usb/usb.c @@ -135,13 +135,111 @@ sc_usb_destroy(struct sc_usb *usb) { libusb_exit(usb->context); } +static int +sc_usb_libusb_callback(libusb_context *ctx, libusb_device *device, + libusb_hotplug_event event, void *userdata) { + (void) ctx; + (void) device; + (void) event; + + struct sc_usb *usb = userdata; + + libusb_device *dev = libusb_get_device(usb->handle); + assert(dev); + if (dev != device) { + // Not the connected device + return 0; + } + + assert(usb->cbs && usb->cbs->on_disconnected); + usb->cbs->on_disconnected(usb, usb->cbs_userdata); + + // Do not automatically deregister the callback by returning 1. Instead, + // manually deregister to interrupt libusb_handle_events() from the libusb + // event thread: + return 0; +} + +static int +run_libusb_event_handler(void *data) { + struct sc_usb *usb = data; + while (!atomic_load(&usb->stopped)) { + // Interrupted by events or by libusb_hotplug_deregister_callback() + libusb_handle_events(usb->context); + } + return 0; +} + +static bool +sc_usb_register_callback(struct sc_usb *usb) { + if (!libusb_has_capability(LIBUSB_CAP_HAS_HOTPLUG)) { + LOGW("libusb does not have hotplug capability"); + return false; + } + + libusb_device *device = libusb_get_device(usb->handle); + assert(device); + + struct libusb_device_descriptor desc; + int result = libusb_get_device_descriptor(device, &desc); + if (result < 0) { + log_libusb_error((enum libusb_error) result); + LOGW("Could not read USB device descriptor"); + return false; + } + + int events = LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT; + int flags = LIBUSB_HOTPLUG_NO_FLAGS; + int vendor_id = desc.idVendor; + int product_id = desc.idProduct; + int dev_class = LIBUSB_HOTPLUG_MATCH_ANY; + result = libusb_hotplug_register_callback(usb->context, events, flags, + vendor_id, product_id, dev_class, + sc_usb_libusb_callback, usb, + &usb->callback_handle); + if (result < 0) { + log_libusb_error((enum libusb_error) result); + LOGW("Could not register USB callback"); + return false; + } + + usb->has_callback_handle = true; + return true; +} + bool -sc_usb_connect(struct sc_usb *usb, libusb_device *device) { +sc_usb_connect(struct sc_usb *usb, libusb_device *device, + const struct sc_usb_callbacks *cbs, void *cbs_userdata) { usb->handle = sc_usb_open_handle(device); if (!usb->handle) { return false; } + usb->has_callback_handle = false; + usb->has_libusb_event_thread = false; + + // If cbs is set, then cbs->on_disconnected must be set + assert(!cbs || cbs->on_disconnected); + usb->cbs = cbs; + usb->cbs_userdata = cbs_userdata; + + if (cbs) { + atomic_init(&usb->stopped, false); + if (sc_usb_register_callback(usb)) { + // Create a thread to process libusb events, so that device + // disconnection could be detected immediately + usb->has_libusb_event_thread = + sc_thread_create(&usb->libusb_event_thread, + run_libusb_event_handler, "scrcpy-usbev", usb); + if (!usb->has_libusb_event_thread) { + LOGW("Libusb event thread handler could not be created, USB " + "device disconnection might not be detected immediately"); + } + } else { + LOGW("Could not register USB device disconnection callback"); + } + } + return true; } @@ -149,3 +247,18 @@ void sc_usb_disconnect(struct sc_usb *usb) { libusb_close(usb->handle); } + +void +sc_usb_stop(struct sc_usb *usb) { + if (usb->has_callback_handle) { + atomic_store(&usb->stopped, true); + libusb_hotplug_deregister_callback(usb->context, usb->callback_handle); + } +} + +void +sc_usb_join(struct sc_usb *usb) { + if (usb->has_libusb_event_thread) { + sc_thread_join(&usb->libusb_event_thread, NULL); + } +} diff --git a/app/src/usb/usb.h b/app/src/usb/usb.h index a271d034..eda7c2f9 100644 --- a/app/src/usb/usb.h +++ b/app/src/usb/usb.h @@ -6,9 +6,26 @@ #include #include +#include "util/thread.h" + struct sc_usb { libusb_context *context; libusb_device_handle *handle; + + const struct sc_usb_callbacks *cbs; + void *cbs_userdata; + + bool has_callback_handle; + libusb_hotplug_callback_handle callback_handle; + + bool has_libusb_event_thread; + sc_thread libusb_event_thread; + + atomic_bool stopped; // only used if cbs != NULL +}; + +struct sc_usb_callbacks { + void (*on_disconnected)(struct sc_usb *usb, void *userdata); }; struct sc_usb_device { @@ -37,9 +54,16 @@ sc_usb_find_devices(struct sc_usb *usb, const char *serial, struct sc_usb_device *devices, size_t len); bool -sc_usb_connect(struct sc_usb *usb, libusb_device *device); +sc_usb_connect(struct sc_usb *usb, libusb_device *device, + const struct sc_usb_callbacks *cbs, void *cbs_userdata); void sc_usb_disconnect(struct sc_usb *usb); +void +sc_usb_stop(struct sc_usb *usb); + +void +sc_usb_join(struct sc_usb *usb); + #endif From 36aaf702791b77d65b7b1bf87ee58a808cdf70d0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 23 Jan 2022 15:35:46 +0100 Subject: [PATCH 1039/2244] Move input event helpers Input events helpers to convert from SDL events to scrcpy events were implemented in input_manager. To reuse them for OTG mode, move them to input_events.h. PR #2974 --- app/src/input_events.h | 72 +++++++++++++++++++++++++++++++++++++++++ app/src/input_manager.c | 72 ----------------------------------------- 2 files changed, 72 insertions(+), 72 deletions(-) diff --git a/app/src/input_events.h b/app/src/input_events.h index 4a4cf356..9bf3c421 100644 --- a/app/src/input_events.h +++ b/app/src/input_events.h @@ -377,4 +377,76 @@ struct sc_touch_event { float pressure; }; +static inline uint16_t +sc_mods_state_from_sdl(uint16_t mods_state) { + return mods_state; +} + +static inline enum sc_keycode +sc_keycode_from_sdl(SDL_Keycode keycode) { + return (enum sc_keycode) keycode; +} + +static inline enum sc_scancode +sc_scancode_from_sdl(SDL_Scancode scancode) { + return (enum sc_scancode) scancode; +} + +static inline enum sc_action +sc_action_from_sdl_keyboard_type(uint32_t type) { + assert(type == SDL_KEYDOWN || type == SDL_KEYUP); + if (type == SDL_KEYDOWN) { + return SC_ACTION_DOWN; + } + return SC_ACTION_UP; +} + +static inline enum sc_action +sc_action_from_sdl_mousebutton_type(uint32_t type) { + assert(type == SDL_MOUSEBUTTONDOWN || type == SDL_MOUSEBUTTONUP); + if (type == SDL_MOUSEBUTTONDOWN) { + return SC_ACTION_DOWN; + } + return SC_ACTION_UP; +} + +static inline enum sc_touch_action +sc_touch_action_from_sdl(uint32_t type) { + assert(type == SDL_FINGERMOTION || type == SDL_FINGERDOWN || + type == SDL_FINGERUP); + if (type == SDL_FINGERMOTION) { + return SC_TOUCH_ACTION_MOVE; + } + if (type == SDL_FINGERDOWN) { + return SC_TOUCH_ACTION_DOWN; + } + return SC_TOUCH_ACTION_UP; +} + +static inline enum sc_mouse_button +sc_mouse_button_from_sdl(uint8_t button) { + if (button >= SDL_BUTTON_LEFT && button <= SDL_BUTTON_X2) { + // SC_MOUSE_BUTTON_* constants are initialized from SDL_BUTTON(index) + return SDL_BUTTON(button); + } + + return SC_MOUSE_BUTTON_UNKNOWN; +} + +static inline uint8_t +sc_mouse_buttons_state_from_sdl(uint32_t buttons_state, + bool forward_all_clicks) { + assert(buttons_state < 0x100); // fits in uint8_t + + uint8_t mask = SC_MOUSE_BUTTON_LEFT; + if (forward_all_clicks) { + mask |= SC_MOUSE_BUTTON_RIGHT + | SC_MOUSE_BUTTON_MIDDLE + | SC_MOUSE_BUTTON_X1 + | SC_MOUSE_BUTTON_X2; + } + + return buttons_state & mask; +} + #endif diff --git a/app/src/input_manager.c b/app/src/input_manager.c index d623c5ae..63842d35 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -7,78 +7,6 @@ #include "screen.h" #include "util/log.h" -static inline uint16_t -sc_mods_state_from_sdl(uint16_t mods_state) { - return mods_state; -} - -static inline enum sc_keycode -sc_keycode_from_sdl(SDL_Keycode keycode) { - return (enum sc_keycode) keycode; -} - -static inline enum sc_scancode -sc_scancode_from_sdl(SDL_Scancode scancode) { - return (enum sc_scancode) scancode; -} - -static inline enum sc_action -sc_action_from_sdl_keyboard_type(uint32_t type) { - assert(type == SDL_KEYDOWN || type == SDL_KEYUP); - if (type == SDL_KEYDOWN) { - return SC_ACTION_DOWN; - } - return SC_ACTION_UP; -} - -static inline enum sc_action -sc_action_from_sdl_mousebutton_type(uint32_t type) { - assert(type == SDL_MOUSEBUTTONDOWN || type == SDL_MOUSEBUTTONUP); - if (type == SDL_MOUSEBUTTONDOWN) { - return SC_ACTION_DOWN; - } - return SC_ACTION_UP; -} - -static inline enum sc_touch_action -sc_touch_action_from_sdl(uint32_t type) { - assert(type == SDL_FINGERMOTION || type == SDL_FINGERDOWN || - type == SDL_FINGERUP); - if (type == SDL_FINGERMOTION) { - return SC_TOUCH_ACTION_MOVE; - } - if (type == SDL_FINGERDOWN) { - return SC_TOUCH_ACTION_DOWN; - } - return SC_TOUCH_ACTION_UP; -} - -static inline enum sc_mouse_button -sc_mouse_button_from_sdl(uint8_t button) { - if (button >= SDL_BUTTON_LEFT && button <= SDL_BUTTON_X2) { - // SC_MOUSE_BUTTON_* constants are initialized from SDL_BUTTON(index) - return SDL_BUTTON(button); - } - - return SC_MOUSE_BUTTON_UNKNOWN; -} - -static inline uint8_t -sc_mouse_buttons_state_from_sdl(uint32_t buttons_state, - bool forward_all_clicks) { - assert(buttons_state < 0x100); // fits in uint8_t - - uint8_t mask = SC_MOUSE_BUTTON_LEFT; - if (forward_all_clicks) { - mask |= SC_MOUSE_BUTTON_RIGHT - | SC_MOUSE_BUTTON_MIDDLE - | SC_MOUSE_BUTTON_X1 - | SC_MOUSE_BUTTON_X2; - } - - return buttons_state & mask; -} - #define SC_SDL_SHORTCUT_MODS_MASK (KMOD_CTRL | KMOD_ALT | KMOD_GUI) static inline uint16_t From 91418c79ab3b7056c21ef6c64f903704c8f00a52 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 22 Jan 2022 11:09:41 +0100 Subject: [PATCH 1040/2244] Add OTG mode Add an option --otg to run scrcpy with only physical keyboard and mouse simulation (HID over AOA), without mirroring and without requiring adb. To avoid adding complexity into the scrcpy initialization and screen implementation, OTG mode is implemented totally separately, with a separate window. PR #2974 --- app/meson.build | 2 + app/scrcpy.1 | 12 ++ app/src/cli.c | 61 ++++++++++ app/src/events.h | 1 + app/src/main.c | 10 +- app/src/options.c | 3 + app/src/options.h | 3 + app/src/usb/scrcpy_otg.c | 213 ++++++++++++++++++++++++++++++++ app/src/usb/scrcpy_otg.h | 12 ++ app/src/usb/screen_otg.c | 254 +++++++++++++++++++++++++++++++++++++++ app/src/usb/screen_otg.h | 46 +++++++ 11 files changed, 615 insertions(+), 2 deletions(-) create mode 100644 app/src/usb/scrcpy_otg.c create mode 100644 app/src/usb/scrcpy_otg.h create mode 100644 app/src/usb/screen_otg.c create mode 100644 app/src/usb/screen_otg.h diff --git a/app/meson.build b/app/meson.build index 5497281a..80a0d9d2 100644 --- a/app/meson.build +++ b/app/meson.build @@ -78,6 +78,8 @@ if usb_support 'src/usb/aoa_hid.c', 'src/usb/hid_keyboard.c', 'src/usb/hid_mouse.c', + 'src/usb/scrcpy_otg.c', + 'src/usb/screen_otg.c', 'src/usb/usb.c', ] endif diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 37431a91..e8e74f98 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -162,6 +162,18 @@ Do not forward repeated key events when a key is held down. .B \-\-no\-mipmaps If the renderer is OpenGL 3.0+ or OpenGL ES 2.0+, then mipmaps are automatically generated to improve downscaling quality. This option disables the generation of mipmaps. +.TP +.B \-\-otg +Run in OTG mode: simulate physical keyboard and mouse, as if the computer keyboard and mouse were plugged directly to the device via an OTG cable. + +In this mode, adb (USB debugging) is not necessary, and mirroring is disabled. + +LAlt, LSuper or RSuper toggle the mouse capture mode, to give control of the mouse back to the computer. + +It may only work over USB, and is currently only supported on Linux. + +See \fB\-\-hid\-keyboard\fR and \fB\-\-hid\-mouse\fR. + .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 34c3103a..28462efd 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -53,6 +53,7 @@ #define OPT_TCPIP 1033 #define OPT_RAW_KEY_EVENTS 1034 #define OPT_NO_DOWNSIZE_ON_ERROR 1035 +#define OPT_OTG 1036 struct sc_option { char shortopt; @@ -276,6 +277,20 @@ static const struct sc_option options[] = { "mipmaps are automatically generated to improve downscaling " "quality. This option disables the generation of mipmaps.", }, + { + .longopt_id = OPT_OTG, + .longopt = "otg", + .text = "Run in OTG mode: simulate physical keyboard and mouse, " + "as if the computer keyboard and mouse were plugged directly " + "to the device via an OTG cable.\n" + "In this mode, adb (USB debugging) is not necessary, and " + "mirroring is disabled.\n" + "LAlt, LSuper or RSuper toggle the mouse capture mode, to give " + "control of the mouse back to the computer.\n" + "It may only work over USB, and is currently only supported " + "on Linux.\n" + "See --hid-keyboard and --hid-mouse.", + }, { .shortopt = 'p', .longopt = "port", @@ -1500,6 +1515,15 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], case OPT_NO_DOWNSIZE_ON_ERROR: opts->downsize_on_error = false; break; + case OPT_OTG: +#ifdef HAVE_USB + opts->otg = true; + break; +#else + LOGE("OTG mode (--otg) is not supported on this platform. It " + "is only available on Linux."); + return false; +#endif case OPT_V4L2_SINK: #ifdef HAVE_V4L2 opts->v4l2_device = optarg; @@ -1610,6 +1634,43 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } } +#ifdef HAVE_USB + if (opts->otg) { + // OTG mode is compatible with only very few options. + // Only report obvious errors. + if (opts->record_filename) { + LOGE("OTG mode: could not record"); + return false; + } + if (opts->turn_screen_off) { + LOGE("OTG mode: could not turn screen off"); + return false; + } + if (opts->stay_awake) { + LOGE("OTG mode: could not stay awake"); + return false; + } + if (opts->show_touches) { + LOGE("OTG mode: could not request to show touches"); + return false; + } + if (opts->power_off_on_close) { + LOGE("OTG mode: could not request power off on close"); + return false; + } + if (opts->display_id) { + LOGE("OTG mode: could not select display"); + return false; + } +#ifdef HAVE_V4L2 + if (opts->v4l2_device) { + LOGE("OTG mode: could not sink to V4L2 device"); + return false; + } +#endif + } +#endif + return true; } diff --git a/app/src/events.h b/app/src/events.h index abe1a72c..3c14f96e 100644 --- a/app/src/events.h +++ b/app/src/events.h @@ -2,3 +2,4 @@ #define EVENT_STREAM_STOPPED (SDL_USEREVENT + 1) #define EVENT_SERVER_CONNECTION_FAILED (SDL_USEREVENT + 2) #define EVENT_SERVER_CONNECTED (SDL_USEREVENT + 3) +#define EVENT_USB_DEVICE_DISCONNECTED (SDL_USEREVENT + 4) diff --git a/app/src/main.c b/app/src/main.c index cbcef4a7..8a8c029c 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -13,6 +13,7 @@ #include "cli.h" #include "options.h" #include "scrcpy.h" +#include "usb/scrcpy_otg.h" #include "util/log.h" static void @@ -88,9 +89,14 @@ main(int argc, char *argv[]) { return 1; } - int res = scrcpy(&args.opts) ? 0 : 1; +#ifdef HAVE_USB + bool ok = args.opts.otg ? scrcpy_otg(&args.opts) + : scrcpy(&args.opts); +#else + bool ok = scrcpy(&args.opts); +#endif avformat_network_deinit(); // ignore failure - return res; + return ok ? 0 : 1; } diff --git a/app/src/options.c b/app/src/options.c index b8560406..c94f798d 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -37,6 +37,9 @@ const struct scrcpy_options scrcpy_options_default = { .display_id = 0, .display_buffer = 0, .v4l2_buffer = 0, +#ifdef HAVE_USB + .otg = false, +#endif .show_touches = false, .fullscreen = false, .always_on_top = false, diff --git a/app/src/options.h b/app/src/options.h index 99f03d40..1591a065 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -112,6 +112,9 @@ struct scrcpy_options { uint32_t display_id; sc_tick display_buffer; sc_tick v4l2_buffer; +#ifdef HAVE_USB + bool otg; +#endif bool show_touches; bool fullscreen; bool always_on_top; diff --git a/app/src/usb/scrcpy_otg.c b/app/src/usb/scrcpy_otg.c new file mode 100644 index 00000000..0a28eda4 --- /dev/null +++ b/app/src/usb/scrcpy_otg.c @@ -0,0 +1,213 @@ +#include "scrcpy_otg.h" + +#include + +#include "events.h" +#include "screen_otg.h" +#include "util/log.h" + +struct scrcpy_otg { + struct sc_usb usb; + struct sc_aoa aoa; + struct sc_hid_keyboard keyboard; + struct sc_hid_mouse mouse; + + struct sc_screen_otg screen_otg; +}; + +static void +sc_usb_on_disconnected(struct sc_usb *usb, void *userdata) { + (void) usb; + (void) userdata; + + SDL_Event event; + event.type = EVENT_USB_DEVICE_DISCONNECTED; + int ret = SDL_PushEvent(&event); + if (ret < 0) { + LOGE("Could not post USB disconnection event: %s", SDL_GetError()); + } +} + +static bool +event_loop(struct scrcpy_otg *s) { + SDL_Event event; + while (SDL_WaitEvent(&event)) { + switch (event.type) { + case EVENT_USB_DEVICE_DISCONNECTED: + LOGW("Device disconnected"); + return false; + case SDL_QUIT: + LOGD("User requested to quit"); + return true; + default: + sc_screen_otg_handle_event(&s->screen_otg, &event); + break; + } + } + return false; +} + +bool +scrcpy_otg(struct scrcpy_options *options) { + static struct scrcpy_otg scrcpy_otg; + struct scrcpy_otg *s = &scrcpy_otg; + + const char *serial = options->serial; + + // Minimal SDL initialization + if (SDL_Init(SDL_INIT_EVENTS)) { + LOGC("Could not initialize SDL: %s", SDL_GetError()); + return false; + } + + atexit(SDL_Quit); + + bool ret = false; + + struct sc_hid_keyboard *keyboard = NULL; + struct sc_hid_mouse *mouse = NULL; + bool usb_device_initialized = false; + bool usb_connected = false; + bool aoa_started = false; + bool aoa_initialized = false; + + static const struct sc_usb_callbacks cbs = { + .on_disconnected = sc_usb_on_disconnected, + }; + bool ok = sc_usb_init(&s->usb); + if (!ok) { + return false; + } + + struct sc_usb_device usb_devices[16]; + ssize_t count = sc_usb_find_devices(&s->usb, serial, usb_devices, + ARRAY_LEN(usb_devices)); + if (count < 0) { + LOGE("Could not list USB devices"); + goto end; + } + + if (count == 0) { + if (serial) { + LOGE("Could not find USB device %s", serial); + } else { + LOGE("Could not find any USB device"); + } + goto end; + } + + if (count > 1) { + if (serial) { + LOGE("Multiple (%d) USB devices with serial %s:", (int) count, + serial); + } else { + LOGE("Multiple (%d) USB devices:", (int) count); + } + for (size_t i = 0; i < (size_t) count; ++i) { + struct sc_usb_device *d = &usb_devices[i]; + LOGE(" %-18s (%04" PRIx16 ":%04" PRIx16 ") %s %s", + d->serial, d->vid, d->pid, d->manufacturer, d->product); + } + if (!serial) { + LOGE("Specify the device via -s or --serial"); + } + sc_usb_device_destroy_all(usb_devices, count); + goto end; + } + usb_device_initialized = true; + + struct sc_usb_device *usb_device = &usb_devices[0]; + + LOGI("USB device: %s (%04" PRIx16 ":%04" PRIx16 ") %s %s", + usb_device->serial, usb_device->vid, usb_device->pid, + usb_device->manufacturer, usb_device->product); + + ok = sc_usb_connect(&s->usb, usb_device->device, &cbs, NULL); + if (!ok) { + goto end; + } + usb_connected = true; + + ok = sc_aoa_init(&s->aoa, &s->usb, NULL); + if (!ok) { + goto end; + } + aoa_initialized = true; + + ok = sc_hid_keyboard_init(&s->keyboard, &s->aoa); + if (!ok) { + goto end; + } + keyboard = &s->keyboard; + + ok = sc_hid_mouse_init(&s->mouse, &s->aoa); + if (!ok) { + goto end; + } + mouse = &s->mouse; + + ok = sc_aoa_start(&s->aoa); + if (!ok) { + goto end; + } + aoa_started = true; + + const char *window_title = options->window_title; + if (!window_title) { + window_title = usb_device->product ? usb_device->product : "scrcpy"; + } + + struct sc_screen_otg_params params = { + .keyboard = keyboard, + .mouse = mouse, + .window_title = window_title, + .always_on_top = options->always_on_top, + .window_x = options->window_x, + .window_y = options->window_y, + .window_borderless = options->window_borderless, + }; + + ok = sc_screen_otg_init(&s->screen_otg, ¶ms); + if (!ok) { + goto end; + } + + // usb_device not needed anymore + sc_usb_device_destroy(usb_device); + usb_device_initialized = false; + + ret = event_loop(s); + LOGD("quit..."); + +end: + if (aoa_started) { + sc_aoa_stop(&s->aoa); + } + sc_usb_stop(&s->usb); + + if (mouse) { + sc_hid_mouse_destroy(&s->mouse); + } + if (keyboard) { + sc_hid_keyboard_destroy(&s->keyboard); + } + + if (aoa_initialized) { + sc_aoa_join(&s->aoa); + sc_aoa_destroy(&s->aoa); + } + + sc_usb_join(&s->usb); + + if (usb_connected) { + sc_usb_disconnect(&s->usb); + } + + if (usb_device_initialized) { + sc_usb_device_destroy(usb_device); + } + + sc_usb_destroy(&s->usb); + + return ret; +} diff --git a/app/src/usb/scrcpy_otg.h b/app/src/usb/scrcpy_otg.h new file mode 100644 index 00000000..24b9cde8 --- /dev/null +++ b/app/src/usb/scrcpy_otg.h @@ -0,0 +1,12 @@ +#ifndef SCRCPY_OTG_H +#define SCRCPY_OTG_H + +#include "common.h" + +#include +#include "options.h" + +bool +scrcpy_otg(struct scrcpy_options *options); + +#endif diff --git a/app/src/usb/screen_otg.c b/app/src/usb/screen_otg.c new file mode 100644 index 00000000..a4b37264 --- /dev/null +++ b/app/src/usb/screen_otg.c @@ -0,0 +1,254 @@ +#include "screen_otg.h" + +#include "icon.h" +#include "options.h" +#include "util/log.h" + +static void +sc_screen_otg_capture_mouse(struct sc_screen_otg *screen, bool capture) { + if (SDL_SetRelativeMouseMode(capture)) { + LOGE("Could not set relative mouse mode to %s: %s", + capture ? "true" : "false", SDL_GetError()); + return; + } + + screen->mouse_captured = capture; +} + +static void +sc_screen_otg_render(struct sc_screen_otg *screen) { + SDL_RenderClear(screen->renderer); + if (screen->texture) { + SDL_RenderCopy(screen->renderer, screen->texture, NULL, NULL); + } + SDL_RenderPresent(screen->renderer); +} + +bool +sc_screen_otg_init(struct sc_screen_otg *screen, + const struct sc_screen_otg_params *params) { + screen->keyboard = params->keyboard; + screen->mouse = params->mouse; + + screen->mouse_captured = false; + screen->mouse_capture_key_pressed = 0; + + const char *title = params->window_title; + assert(title); + + int x = params->window_x != SC_WINDOW_POSITION_UNDEFINED + ? params->window_x : (int) SDL_WINDOWPOS_UNDEFINED; + int y = params->window_y != SC_WINDOW_POSITION_UNDEFINED + ? params->window_y : (int) SDL_WINDOWPOS_UNDEFINED; + int width = 256; + int height = 256; + + uint32_t window_flags = 0; + if (params->always_on_top) { + window_flags |= SDL_WINDOW_ALWAYS_ON_TOP; + } + if (params->window_borderless) { + window_flags |= SDL_WINDOW_BORDERLESS; + } + + screen->window = SDL_CreateWindow(title, x, y, width, height, window_flags); + if (!screen->window) { + LOGE("Could not create window: %s", SDL_GetError()); + return false; + } + + screen->renderer = SDL_CreateRenderer(screen->window, -1, 0); + if (!screen->renderer) { + LOGE("Could not create renderer: %s", SDL_GetError()); + goto error_destroy_window; + } + + SDL_Surface *icon = scrcpy_icon_load(); + + if (icon) { + SDL_SetWindowIcon(screen->window, icon); + + screen->texture = SDL_CreateTextureFromSurface(screen->renderer, icon); + scrcpy_icon_destroy(icon); + if (!screen->texture) { + goto error_destroy_renderer; + } + } else { + screen->texture = NULL; + LOGW("Could not load icon"); + } + + // Capture mouse on start + sc_screen_otg_capture_mouse(screen, true); + + return true; + +error_destroy_window: + SDL_DestroyWindow(screen->window); +error_destroy_renderer: + SDL_DestroyRenderer(screen->renderer); + + return false; +} + +void +sc_screen_otg_destroy(struct sc_screen_otg *screen) { + if (screen->texture) { + SDL_DestroyTexture(screen->texture); + } + SDL_DestroyRenderer(screen->renderer); + SDL_DestroyWindow(screen->window); +} + +static inline bool +sc_screen_otg_is_mouse_capture_key(SDL_Keycode key) { + return key == SDLK_LALT || key == SDLK_LGUI || key == SDLK_RGUI; +} + +static void +sc_screen_otg_process_key(struct sc_screen_otg *screen, + const SDL_KeyboardEvent *event) { + assert(screen->keyboard); + struct sc_key_processor *kp = &screen->keyboard->key_processor; + + struct sc_key_event evt = { + .action = sc_action_from_sdl_keyboard_type(event->type), + .keycode = sc_keycode_from_sdl(event->keysym.sym), + .scancode = sc_scancode_from_sdl(event->keysym.scancode), + .repeat = event->repeat, + .mods_state = sc_mods_state_from_sdl(event->keysym.mod), + }; + + assert(kp->ops->process_key); + kp->ops->process_key(kp, &evt, SC_SEQUENCE_INVALID); +} + +static void +sc_screen_otg_process_mouse_motion(struct sc_screen_otg *screen, + const SDL_MouseMotionEvent *event) { + assert(screen->mouse); + struct sc_mouse_processor *mp = &screen->mouse->mouse_processor; + + struct sc_mouse_motion_event evt = { + // .position not used for HID events + .xrel = event->xrel, + .yrel = event->yrel, + .buttons_state = sc_mouse_buttons_state_from_sdl(event->state, true), + }; + + assert(mp->ops->process_mouse_motion); + mp->ops->process_mouse_motion(mp, &evt); +} + +static void +sc_screen_otg_process_mouse_button(struct sc_screen_otg *screen, + const SDL_MouseButtonEvent *event) { + assert(screen->mouse); + struct sc_mouse_processor *mp = &screen->mouse->mouse_processor; + + uint32_t sdl_buttons_state = SDL_GetMouseState(NULL, NULL); + + struct sc_mouse_click_event evt = { + // .position not used for HID events + .action = sc_action_from_sdl_mousebutton_type(event->type), + .button = sc_mouse_button_from_sdl(event->button), + .buttons_state = + sc_mouse_buttons_state_from_sdl(sdl_buttons_state, true), + }; + + assert(mp->ops->process_mouse_click); + mp->ops->process_mouse_click(mp, &evt); +} + +static void +sc_screen_otg_process_mouse_wheel(struct sc_screen_otg *screen, + const SDL_MouseWheelEvent *event) { + assert(screen->mouse); + struct sc_mouse_processor *mp = &screen->mouse->mouse_processor; + + uint32_t sdl_buttons_state = SDL_GetMouseState(NULL, NULL); + + struct sc_mouse_scroll_event evt = { + // .position not used for HID events + .hscroll = event->x, + .vscroll = event->y, + .buttons_state = + sc_mouse_buttons_state_from_sdl(sdl_buttons_state, true), + }; + + assert(mp->ops->process_mouse_scroll); + mp->ops->process_mouse_scroll(mp, &evt); +} + +void +sc_screen_otg_handle_event(struct sc_screen_otg *screen, SDL_Event *event) { + switch (event->type) { + case SDL_WINDOWEVENT: + switch (event->window.event) { + case SDL_WINDOWEVENT_EXPOSED: + sc_screen_otg_render(screen); + break; + case SDL_WINDOWEVENT_FOCUS_LOST: + sc_screen_otg_capture_mouse(screen, false); + break; + } + return; + case SDL_KEYDOWN: { + SDL_Keycode key = event->key.keysym.sym; + if (sc_screen_otg_is_mouse_capture_key(key)) { + if (!screen->mouse_capture_key_pressed) { + screen->mouse_capture_key_pressed = key; + } else { + // Another mouse capture key has been pressed, cancel mouse + // (un)capture + screen->mouse_capture_key_pressed = 0; + } + // Mouse capture keys are never forwarded to the device + return; + } + + sc_screen_otg_process_key(screen, &event->key); + break; + } + case SDL_KEYUP: { + SDL_Keycode key = event->key.keysym.sym; + SDL_Keycode cap = screen->mouse_capture_key_pressed; + screen->mouse_capture_key_pressed = 0; + if (sc_screen_otg_is_mouse_capture_key(key)) { + if (key == cap) { + // A mouse capture key has been pressed then released: + // toggle the capture mouse mode + sc_screen_otg_capture_mouse(screen, + !screen->mouse_captured); + } + // Mouse capture keys are never forwarded to the device + return; + } + + sc_screen_otg_process_key(screen, &event->key); + break; + } + case SDL_MOUSEMOTION: + if (screen->mouse_captured) { + sc_screen_otg_process_mouse_motion(screen, &event->motion); + } + break; + case SDL_MOUSEBUTTONDOWN: + if (screen->mouse_captured) { + sc_screen_otg_process_mouse_button(screen, &event->button); + } + break; + case SDL_MOUSEBUTTONUP: + if (screen->mouse_captured) { + sc_screen_otg_process_mouse_button(screen, &event->button); + } else { + sc_screen_otg_capture_mouse(screen, true); + } + break; + case SDL_MOUSEWHEEL: + if (screen->mouse_captured) { + sc_screen_otg_process_mouse_wheel(screen, &event->wheel); + } + break; + } +} diff --git a/app/src/usb/screen_otg.h b/app/src/usb/screen_otg.h new file mode 100644 index 00000000..3fa1c4ad --- /dev/null +++ b/app/src/usb/screen_otg.h @@ -0,0 +1,46 @@ +#ifndef SC_SCREEN_OTG_H +#define SC_SCREEN_OTG_H + +#include "common.h" + +#include +#include + +#include "hid_keyboard.h" +#include "hid_mouse.h" + +struct sc_screen_otg { + struct sc_hid_keyboard *keyboard; + struct sc_hid_mouse *mouse; + + SDL_Window *window; + SDL_Renderer *renderer; + SDL_Texture *texture; + + // See equivalent mechanism in screen.h + bool mouse_captured; + SDL_Keycode mouse_capture_key_pressed; +}; + +struct sc_screen_otg_params { + struct sc_hid_keyboard *keyboard; + struct sc_hid_mouse *mouse; + + const char *window_title; + bool always_on_top; + int16_t window_x; // accepts SC_WINDOW_POSITION_UNDEFINED + int16_t window_y; // accepts SC_WINDOW_POSITION_UNDEFINED + bool window_borderless; +}; + +bool +sc_screen_otg_init(struct sc_screen_otg *screen, + const struct sc_screen_otg_params *params); + +void +sc_screen_otg_destroy(struct sc_screen_otg *screen); + +void +sc_screen_otg_handle_event(struct sc_screen_otg *screen, SDL_Event *event); + +#endif From c5be0d6438c14a059fc4b581b2645fce6b6666cd Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 26 Jan 2022 22:30:57 +0100 Subject: [PATCH 1041/2244] Document OTG mode in README PR #2974 --- README.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/README.md b/README.md index 9eb4bd21..af0e54f7 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ Its features include: (Linux-only) - [physical mouse simulation (HID)](#physical-mouse-simulation-hid) (Linux-only) + - [OTG mode](#otg) (Linux-only) - and more… ## Requirements @@ -849,6 +850,26 @@ Special capture keys, either Alt or Super, toggle the mouse back to the computer. +#### OTG + +It is possible to run _scrcpy_ with only physical keyboard and mouse simulation +(HID), as if the computer keyboard and mouse were plugged directly to the device +via an OTG cable. + +In this mode, _adb_ (USB debugging) is not necessary, and mirroring is disabled. + +To enable OTG mode: + +```bash +scrcpy --otg +# Pass the serial if several USB devices are available +scrcpy --otg -s 0123456789abcdef +``` + +Like `--hid-keyboard` and `--hid-mouse`, it only works if the device is +connected by USB, and is currently only supported on Linux. + + #### Text injection preference There are two kinds of [events][textevents] generated when typing text: From ea68a003a2e5d584a19f83d81d1aeb86690f6102 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 26 Jan 2022 22:50:10 +0100 Subject: [PATCH 1042/2244] Make HID keyboard and mouse optional in OTG mode Allow to only enable HID keyboard or HID mouse: scrcpy --otg -K # keyboard only scrcpy --otg -M # mouse only scrcpy --otg -KM # keyboard and mouse scrcpy --otg # keyboard and mouse PR #2974 --- README.md | 10 +++++ app/scrcpy.1 | 2 + app/src/cli.c | 2 + app/src/usb/scrcpy_otg.c | 33 ++++++++++----- app/src/usb/screen_otg.c | 87 +++++++++++++++++++++++----------------- 5 files changed, 88 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index af0e54f7..8509dd71 100644 --- a/README.md +++ b/README.md @@ -866,6 +866,16 @@ scrcpy --otg scrcpy --otg -s 0123456789abcdef ``` +It is possible to enable only HID keyboard or HID mouse: + +```bash +scrcpy --otg --hid-keyboard # keyboard only +scrcpy --otg --hid-mouse # mouse only +scrcpy --otg --hid-keyboard --hid-mouse # keyboard and mouse +# for convenience, enable both by default +scrcpy --otg # keyboard and mouse +``` + Like `--hid-keyboard` and `--hid-mouse`, it only works if the device is connected by USB, and is currently only supported on Linux. diff --git a/app/scrcpy.1 b/app/scrcpy.1 index e8e74f98..0f618cb4 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -170,6 +170,8 @@ In this mode, adb (USB debugging) is not necessary, and mirroring is disabled. LAlt, LSuper or RSuper toggle the mouse capture mode, to give control of the mouse back to the computer. +If any of \fB\-\-hid\-keyboard\fR or \fB\-\-hid\-mouse\fR is set, only enable keyboard or mouse respectively, otherwise enable both. + It may only work over USB, and is currently only supported on Linux. See \fB\-\-hid\-keyboard\fR and \fB\-\-hid\-mouse\fR. diff --git a/app/src/cli.c b/app/src/cli.c index 28462efd..5587b2d8 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -287,6 +287,8 @@ static const struct sc_option options[] = { "mirroring is disabled.\n" "LAlt, LSuper or RSuper toggle the mouse capture mode, to give " "control of the mouse back to the computer.\n" + "If any of --hid-keyboard or --hid-mouse is set, only enable " + "keyboard or mouse respectively, otherwise enable both." "It may only work over USB, and is currently only supported " "on Linux.\n" "See --hid-keyboard and --hid-mouse.", diff --git a/app/src/usb/scrcpy_otg.c b/app/src/usb/scrcpy_otg.c index 0a28eda4..f2e5d549 100644 --- a/app/src/usb/scrcpy_otg.c +++ b/app/src/usb/scrcpy_otg.c @@ -134,17 +134,32 @@ scrcpy_otg(struct scrcpy_options *options) { } aoa_initialized = true; - ok = sc_hid_keyboard_init(&s->keyboard, &s->aoa); - if (!ok) { - goto end; - } - keyboard = &s->keyboard; + bool enable_keyboard = + options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_HID; + bool enable_mouse = + options->mouse_input_mode == SC_MOUSE_INPUT_MODE_HID; - ok = sc_hid_mouse_init(&s->mouse, &s->aoa); - if (!ok) { - goto end; + // If neither --hid-keyboard or --hid-mouse is passed, enable both + if (!enable_keyboard && !enable_mouse) { + enable_keyboard = true; + enable_mouse = true; + } + + if (enable_keyboard) { + ok = sc_hid_keyboard_init(&s->keyboard, &s->aoa); + if (!ok) { + goto end; + } + keyboard = &s->keyboard; + } + + if (enable_mouse) { + ok = sc_hid_mouse_init(&s->mouse, &s->aoa); + if (!ok) { + goto end; + } + mouse = &s->mouse; } - mouse = &s->mouse; ok = sc_aoa_start(&s->aoa); if (!ok) { diff --git a/app/src/usb/screen_otg.c b/app/src/usb/screen_otg.c index a4b37264..cda0da5e 100644 --- a/app/src/usb/screen_otg.c +++ b/app/src/usb/screen_otg.c @@ -6,6 +6,7 @@ static void sc_screen_otg_capture_mouse(struct sc_screen_otg *screen, bool capture) { + assert(screen->mouse); if (SDL_SetRelativeMouseMode(capture)) { LOGE("Could not set relative mouse mode to %s: %s", capture ? "true" : "false", SDL_GetError()); @@ -78,8 +79,10 @@ sc_screen_otg_init(struct sc_screen_otg *screen, LOGW("Could not load icon"); } - // Capture mouse on start - sc_screen_otg_capture_mouse(screen, true); + if (screen->mouse) { + // Capture mouse on start + sc_screen_otg_capture_mouse(screen, true); + } return true; @@ -189,64 +192,74 @@ sc_screen_otg_handle_event(struct sc_screen_otg *screen, SDL_Event *event) { sc_screen_otg_render(screen); break; case SDL_WINDOWEVENT_FOCUS_LOST: - sc_screen_otg_capture_mouse(screen, false); + if (screen->mouse) { + sc_screen_otg_capture_mouse(screen, false); + } break; } return; - case SDL_KEYDOWN: { - SDL_Keycode key = event->key.keysym.sym; - if (sc_screen_otg_is_mouse_capture_key(key)) { - if (!screen->mouse_capture_key_pressed) { - screen->mouse_capture_key_pressed = key; - } else { - // Another mouse capture key has been pressed, cancel mouse - // (un)capture - screen->mouse_capture_key_pressed = 0; + case SDL_KEYDOWN: + if (screen->mouse) { + SDL_Keycode key = event->key.keysym.sym; + if (sc_screen_otg_is_mouse_capture_key(key)) { + if (!screen->mouse_capture_key_pressed) { + screen->mouse_capture_key_pressed = key; + } else { + // Another mouse capture key has been pressed, cancel + // mouse (un)capture + screen->mouse_capture_key_pressed = 0; + } + // Mouse capture keys are never forwarded to the device + return; } - // Mouse capture keys are never forwarded to the device - return; } - sc_screen_otg_process_key(screen, &event->key); + if (screen->keyboard) { + sc_screen_otg_process_key(screen, &event->key); + } break; - } - case SDL_KEYUP: { - SDL_Keycode key = event->key.keysym.sym; - SDL_Keycode cap = screen->mouse_capture_key_pressed; - screen->mouse_capture_key_pressed = 0; - if (sc_screen_otg_is_mouse_capture_key(key)) { - if (key == cap) { - // A mouse capture key has been pressed then released: - // toggle the capture mouse mode - sc_screen_otg_capture_mouse(screen, - !screen->mouse_captured); + case SDL_KEYUP: + if (screen->mouse) { + SDL_Keycode key = event->key.keysym.sym; + SDL_Keycode cap = screen->mouse_capture_key_pressed; + screen->mouse_capture_key_pressed = 0; + if (sc_screen_otg_is_mouse_capture_key(key)) { + if (key == cap) { + // A mouse capture key has been pressed then released: + // toggle the capture mouse mode + sc_screen_otg_capture_mouse(screen, + !screen->mouse_captured); + } + // Mouse capture keys are never forwarded to the device + return; } - // Mouse capture keys are never forwarded to the device - return; } - sc_screen_otg_process_key(screen, &event->key); + if (screen->keyboard) { + sc_screen_otg_process_key(screen, &event->key); + } break; - } case SDL_MOUSEMOTION: - if (screen->mouse_captured) { + if (screen->mouse && screen->mouse_captured) { sc_screen_otg_process_mouse_motion(screen, &event->motion); } break; case SDL_MOUSEBUTTONDOWN: - if (screen->mouse_captured) { + if (screen->mouse && screen->mouse_captured) { sc_screen_otg_process_mouse_button(screen, &event->button); } break; case SDL_MOUSEBUTTONUP: - if (screen->mouse_captured) { - sc_screen_otg_process_mouse_button(screen, &event->button); - } else { - sc_screen_otg_capture_mouse(screen, true); + if (screen->mouse) { + if (screen->mouse_captured) { + sc_screen_otg_process_mouse_button(screen, &event->button); + } else { + sc_screen_otg_capture_mouse(screen, true); + } } break; case SDL_MOUSEWHEEL: - if (screen->mouse_captured) { + if (screen->mouse && screen->mouse_captured) { sc_screen_otg_process_mouse_wheel(screen, &event->wheel); } break; From 5508c635cb11b61df9bfc3d9e5e603492100b067 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 27 Jan 2022 23:32:37 +0100 Subject: [PATCH 1043/2244] Enable mouse focus clickthrough in OTG mode A single click on the window must both give focus and capture the mouse. PR #2974 --- app/src/usb/scrcpy_otg.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/usb/scrcpy_otg.c b/app/src/usb/scrcpy_otg.c index f2e5d549..e27a3605 100644 --- a/app/src/usb/scrcpy_otg.c +++ b/app/src/usb/scrcpy_otg.c @@ -62,6 +62,10 @@ scrcpy_otg(struct scrcpy_options *options) { atexit(SDL_Quit); + if (!SDL_SetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1")) { + LOGW("Could not enable mouse focus clickthrough"); + } + bool ret = false; struct sc_hid_keyboard *keyboard = NULL; From 80bec708523ea7b8cefc9f74057611e8ac499b89 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 28 Jan 2022 09:05:36 +0100 Subject: [PATCH 1044/2244] Add helper to log Windows system errors It will help to log errors returned by GetLastError() or WSAGetLastError(): - - - Always log the errors in English to be able to read them in bug reports. --- app/src/util/log.c | 24 ++++++++++++++++++++++++ app/src/util/log.h | 6 ++++++ app/src/util/net.c | 9 +-------- 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/app/src/util/log.c b/app/src/util/log.c index a285fffb..d8d7cd70 100644 --- a/app/src/util/log.c +++ b/app/src/util/log.c @@ -1,5 +1,8 @@ #include "log.h" +#if _WIN32 +# include +#endif #include static SDL_LogPriority @@ -51,3 +54,24 @@ sc_get_log_level(void) { SDL_LogPriority sdl_log = SDL_LogGetPriority(SDL_LOG_CATEGORY_APPLICATION); return log_level_sdl_to_sc(sdl_log); } + +#ifdef _WIN32 +bool +sc_log_windows_error(const char *prefix, int error) { + assert(prefix); + + char *message; + DWORD flags = FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM; + DWORD lang_id = MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US); + int ret = + FormatMessage(flags, NULL, error, lang_id, (char *) &message, 0, NULL); + if (ret <= 0) { + return false; + } + + // Note: message already contains a trailing '\n' + LOGE("%s: [%d] %s", prefix, error, message); + LocalFree(message); + return true; +} +#endif diff --git a/app/src/util/log.h b/app/src/util/log.h index 231e0846..e3efdbe5 100644 --- a/app/src/util/log.h +++ b/app/src/util/log.h @@ -26,4 +26,10 @@ sc_set_log_level(enum sc_log_level level); enum sc_log_level sc_get_log_level(void); +#ifdef _WIN32 +// Log system error (typically returned by GetLastError() or similar) +bool +sc_log_windows_error(const char *prefix, int error); +#endif + #endif diff --git a/app/src/util/net.c b/app/src/util/net.c index 565db2e9..b18566be 100644 --- a/app/src/util/net.c +++ b/app/src/util/net.c @@ -117,14 +117,7 @@ set_cloexec_flag(sc_raw_socket raw_sock) { static void net_perror(const char *s) { #ifdef _WIN32 - int error = WSAGetLastError(); - char *wsa_message; - FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, - NULL, error, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), - (char *) &wsa_message, 0, NULL); - // no explicit '\n', wsa_message already contains a trailing '\n' - fprintf(stderr, "%s: [%d] %s", s, error, wsa_message); - LocalFree(wsa_message); + sc_log_windows_error(s, WSAGetLastError()); #else perror(s); #endif From b8d7f36ba37c153aa1516392e08afdbe6386e46b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 29 Jan 2022 08:05:59 +0100 Subject: [PATCH 1045/2244] Fix SC_EXIT_CODE_NONE value The exit code on windows is stored in a DWORD, an unsigned long: Use the max value of this type for SC_EXIT_CODE_NONE. --- app/src/util/process.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/util/process.h b/app/src/util/process.h index 17c09bc5..90920620 100644 --- a/app/src/util/process.h +++ b/app/src/util/process.h @@ -15,7 +15,7 @@ // # define SC_PRIsizet "Iu" # define SC_PROCESS_NONE NULL -# define SC_EXIT_CODE_NONE -1u // max value as unsigned +# define SC_EXIT_CODE_NONE -1UL // max value as unsigned long typedef HANDLE sc_pid; typedef DWORD sc_exit_code; typedef HANDLE sc_pipe; From eaba6136334e5fb50d3276ca7405b77e48c8f29e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 29 Jan 2022 11:46:39 +0100 Subject: [PATCH 1046/2244] Revert "Upgrade platform-tools (32.0.0) for Windows" This reverts commit c0de365f672110959cec8fca936bd6aeaa01b59d. The new adb.exe crashes. Refs #2981 comment --- prebuilt-deps/Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/prebuilt-deps/Makefile b/prebuilt-deps/Makefile index 44ea6e1e..b40321b6 100644 --- a/prebuilt-deps/Makefile +++ b/prebuilt-deps/Makefile @@ -28,6 +28,6 @@ prepare-sdl2: SDL2-2.0.20 prepare-adb: - @./prepare-dep https://dl.google.com/android/repository/platform-tools_r32.0.0-windows.zip \ - 41f4c7512b32cbb3f8c624c20b56326abb692a6f169b03b4b63b6c5a6fdbb08c \ + @./prepare-dep https://dl.google.com/android/repository/platform-tools_r31.0.3-windows.zip \ + 0f4b8fdd26af2c3733539d6eebb3c2ed499ea1d4bb1f4e0ecc2d6016961a6e24 \ platform-tools From 38cdcdda50de8872230921fb5681bcd4665bc1e6 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 29 Jan 2022 14:08:49 +0100 Subject: [PATCH 1047/2244] Improve prebuilt system This aims to fix two issues with the previous implementation: 1. the whole content of downloaded archives were extracted, while only few files are necessary; 2. the archives were extracted in the prebuild-deps/ directory as is. As a consequence of (2), the actual directory name relied on the root directory of the archive. For adb, this root directory was always "platform-tools", so when bumping the adb version, the target directory already existed and the dependency was not upgraded (the old one had to be removed manually). Expose common function to download a file and check its checksum, but let the custom script for each dependency extract only the needed files and reorganize the content if necessary. --- app/meson.build | 10 ++--- cross_win32.txt | 2 +- cross_win64.txt | 2 +- prebuilt-deps/.gitignore | 5 +-- prebuilt-deps/Makefile | 33 --------------- prebuilt-deps/common | 22 ++++++++++ prebuilt-deps/prepare-adb.sh | 32 ++++++++++++++ prebuilt-deps/prepare-dep | 61 --------------------------- prebuilt-deps/prepare-ffmpeg-win32.sh | 45 ++++++++++++++++++++ prebuilt-deps/prepare-ffmpeg-win64.sh | 35 +++++++++++++++ prebuilt-deps/prepare-sdl.sh | 32 ++++++++++++++ release.mk | 44 ++++++++++--------- 12 files changed, 198 insertions(+), 125 deletions(-) delete mode 100644 prebuilt-deps/Makefile create mode 100755 prebuilt-deps/common create mode 100755 prebuilt-deps/prepare-adb.sh delete mode 100755 prebuilt-deps/prepare-dep create mode 100755 prebuilt-deps/prepare-ffmpeg-win32.sh create mode 100755 prebuilt-deps/prepare-ffmpeg-win64.sh create mode 100755 prebuilt-deps/prepare-sdl.sh diff --git a/app/meson.build b/app/meson.build index 80a0d9d2..7f675035 100644 --- a/app/meson.build +++ b/app/meson.build @@ -109,9 +109,9 @@ if not crossbuild_windows else # cross-compile mingw32 build (from Linux to Windows) prebuilt_sdl2 = meson.get_cross_property('prebuilt_sdl2') - sdl2_bin_dir = meson.current_source_dir() + '/../prebuilt-deps/' + prebuilt_sdl2 + '/bin' - sdl2_lib_dir = meson.current_source_dir() + '/../prebuilt-deps/' + prebuilt_sdl2 + '/lib' - sdl2_include_dir = '../prebuilt-deps/' + prebuilt_sdl2 + '/include' + sdl2_bin_dir = meson.current_source_dir() + '/../prebuilt-deps/data/' + prebuilt_sdl2 + '/bin' + sdl2_lib_dir = meson.current_source_dir() + '/../prebuilt-deps/data/' + prebuilt_sdl2 + '/lib' + sdl2_include_dir = '../prebuilt-deps/data/' + prebuilt_sdl2 + '/include' sdl2 = declare_dependency( dependencies: [ @@ -122,8 +122,8 @@ else ) prebuilt_ffmpeg = meson.get_cross_property('prebuilt_ffmpeg') - ffmpeg_bin_dir = meson.current_source_dir() + '/../prebuilt-deps/' + prebuilt_ffmpeg + '/bin' - ffmpeg_include_dir = '../prebuilt-deps/' + prebuilt_ffmpeg + '/include' + ffmpeg_bin_dir = meson.current_source_dir() + '/../prebuilt-deps/data/' + prebuilt_ffmpeg + '/bin' + ffmpeg_include_dir = '../prebuilt-deps/data/' + prebuilt_ffmpeg + '/include' # ffmpeg versions are different for win32 and win64 builds ffmpeg_avcodec = meson.get_cross_property('ffmpeg_avcodec') diff --git a/cross_win32.txt b/cross_win32.txt index 9bf974e3..db448a00 100644 --- a/cross_win32.txt +++ b/cross_win32.txt @@ -19,5 +19,5 @@ endian = 'little' ffmpeg_avcodec = 'avcodec-58' ffmpeg_avformat = 'avformat-58' ffmpeg_avutil = 'avutil-56' -prebuilt_ffmpeg = 'ffmpeg-4.3.1-win32-shared' +prebuilt_ffmpeg = 'ffmpeg-win32-4.3.1' prebuilt_sdl2 = 'SDL2-2.0.20/i686-w64-mingw32' diff --git a/cross_win64.txt b/cross_win64.txt index 763e12e9..9d169a71 100644 --- a/cross_win64.txt +++ b/cross_win64.txt @@ -19,5 +19,5 @@ endian = 'little' ffmpeg_avcodec = 'avcodec-59' ffmpeg_avformat = 'avformat-59' ffmpeg_avutil = 'avutil-57' -prebuilt_ffmpeg = 'ffmpeg-5.0-full_build-shared' +prebuilt_ffmpeg = 'ffmpeg-win64-5.0' prebuilt_sdl2 = 'SDL2-2.0.20/x86_64-w64-mingw32' diff --git a/prebuilt-deps/.gitignore b/prebuilt-deps/.gitignore index 934bc04c..3af0ccb6 100644 --- a/prebuilt-deps/.gitignore +++ b/prebuilt-deps/.gitignore @@ -1,4 +1 @@ -* -!/.gitignore -!/Makefile -!/prepare-dep +/data diff --git a/prebuilt-deps/Makefile b/prebuilt-deps/Makefile deleted file mode 100644 index b40321b6..00000000 --- a/prebuilt-deps/Makefile +++ /dev/null @@ -1,33 +0,0 @@ -.PHONY: prepare-win32 prepare-win64 \ - prepare-ffmpeg-win32 \ - prepare-ffmpeg-win64 \ - prepare-sdl2 \ - prepare-adb - -prepare-win32: prepare-sdl2 prepare-ffmpeg-win32 prepare-adb -prepare-win64: prepare-sdl2 prepare-ffmpeg-win64 prepare-adb - -# Use old FFmpeg version for win32, there are no new prebuilts -prepare-ffmpeg-win32: - @./prepare-dep https://github.com/Genymobile/scrcpy/releases/download/v1.16/ffmpeg-4.3.1-win32-shared.zip \ - 357af9901a456f4dcbacd107e83a934d344c9cb07ddad8aaf80612eeab7d26d2 \ - ffmpeg-4.3.1-win32-shared - @./prepare-dep https://github.com/Genymobile/scrcpy/releases/download/v1.16/ffmpeg-4.3.1-win32-dev.zip \ - 230efb08e9bcf225bd474da29676c70e591fc94d8790a740ca801408fddcb78b \ - ffmpeg-4.3.1-win32-dev - ln -sf ../ffmpeg-4.3.1-win32-dev/include ffmpeg-4.3.1-win32-shared/ - -prepare-ffmpeg-win64: - @./prepare-dep https://github.com/GyanD/codexffmpeg/releases/download/5.0/ffmpeg-5.0-full_build-shared.7z \ - e5900f6cecd4c438d398bd2fc308736c10b857cd8dd61c11bcfb05bff5d1211a \ - ffmpeg-5.0-full_build-shared - -prepare-sdl2: - @./prepare-dep https://libsdl.org/release/SDL2-devel-2.0.20-mingw.tar.gz \ - 38094d82a857d6c62352e5c5cdec74948c5b4d25c59cbd298d6d233568976bd1 \ - SDL2-2.0.20 - -prepare-adb: - @./prepare-dep https://dl.google.com/android/repository/platform-tools_r31.0.3-windows.zip \ - 0f4b8fdd26af2c3733539d6eebb3c2ed499ea1d4bb1f4e0ecc2d6016961a6e24 \ - platform-tools diff --git a/prebuilt-deps/common b/prebuilt-deps/common new file mode 100755 index 00000000..c97f7de4 --- /dev/null +++ b/prebuilt-deps/common @@ -0,0 +1,22 @@ +PREBUILT_DATA_DIR=data + +checksum() { + local file="$1" + local sum="$2" + echo "$file: verifying checksum..." + echo "$sum $file" | sha256sum -c +} + +get_file() { + local url="$1" + local file="$2" + local sum="$3" + if [[ -f "$file" ]] + then + echo "$file: found" + else + echo "$file: not found, downloading..." + wget "$url" -O "$file" + fi + checksum "$file" "$sum" +} diff --git a/prebuilt-deps/prepare-adb.sh b/prebuilt-deps/prepare-adb.sh new file mode 100755 index 00000000..2e7997e1 --- /dev/null +++ b/prebuilt-deps/prepare-adb.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +set -e +DIR=$(dirname ${BASH_SOURCE[0]}) +cd "$DIR" +. common +mkdir -p "$PREBUILT_DATA_DIR" +cd "$PREBUILT_DATA_DIR" + +DEP_DIR=platform-tools-31.0.3 + +FILENAME=platform-tools_r31.0.3-windows.zip +SHA256SUM=0f4b8fdd26af2c3733539d6eebb3c2ed499ea1d4bb1f4e0ecc2d6016961a6e24 + +if [[ -d "$DEP_DIR" ]] +then + echo "$DEP_DIR" found + exit 0 +fi + +get_file "https://dl.google.com/android/repository/$FILENAME" \ + "$FILENAME" "$SHA256SUM" + +mkdir "$DEP_DIR" +cd "$DEP_DIR" + +ZIP_PREFIX=platform-tools +unzip "../$FILENAME" \ + "$ZIP_PREFIX"/AdbWinApi.dll \ + "$ZIP_PREFIX"/AdbWinUsbApi.dll \ + "$ZIP_PREFIX"/adb.exe +mv "$ZIP_PREFIX"/* . +rmdir "$ZIP_PREFIX" diff --git a/prebuilt-deps/prepare-dep b/prebuilt-deps/prepare-dep deleted file mode 100755 index a95b9bb6..00000000 --- a/prebuilt-deps/prepare-dep +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env bash -set -e -url="$1" -sum="$2" -dir="$3" - -checksum() { - local file="$1" - local sum="$2" - echo "$file: verifying checksum..." - echo "$sum $file" | sha256sum -c -} - -get_file() { - local url="$1" - local file="$2" - local sum="$3" - if [[ -f "$file" ]] - then - echo "$file: found" - else - echo "$file: not found, downloading..." - wget "$url" -O "$file" - fi - checksum "$file" "$sum" -} - -extract() { - local file="$1" - echo "Extracting $file..." - if [[ "$file" == *.zip ]] - then - unzip -q "$file" - elif [[ "$file" == *.tar.gz ]] - then - tar xf "$file" - elif [[ "$file" == *.7z ]] - then - 7z x "$file" - else - echo "Unsupported file: $file" - return 1 - fi -} - -get_dep() { - local url="$1" - local sum="$2" - local dir="$3" - local file="${url##*/}" - if [[ -d "$dir" ]] - then - echo "$dir: found" - else - echo "$dir: not found" - get_file "$url" "$file" "$sum" - extract "$file" - fi -} - -get_dep "$url" "$sum" "$dir" diff --git a/prebuilt-deps/prepare-ffmpeg-win32.sh b/prebuilt-deps/prepare-ffmpeg-win32.sh new file mode 100755 index 00000000..2a6a3841 --- /dev/null +++ b/prebuilt-deps/prepare-ffmpeg-win32.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +set -e +DIR=$(dirname ${BASH_SOURCE[0]}) +cd "$DIR" +. common +mkdir -p "$PREBUILT_DATA_DIR" +cd "$PREBUILT_DATA_DIR" + +DEP_DIR=ffmpeg-win32-4.3.1 + +FILENAME_SHARED=ffmpeg-4.3.1-win32-shared.zip +SHA256SUM_SHARED=357af9901a456f4dcbacd107e83a934d344c9cb07ddad8aaf80612eeab7d26d2 + +FILENAME_DEV=ffmpeg-4.3.1-win32-dev.zip +SHA256SUM_DEV=230efb08e9bcf225bd474da29676c70e591fc94d8790a740ca801408fddcb78b + +if [[ -d "$DEP_DIR" ]] +then + echo "$DEP_DIR" found + exit 0 +fi + +get_file "https://github.com/Genymobile/scrcpy/releases/download/v1.16/$FILENAME_SHARED" \ + "$FILENAME_SHARED" "$SHA256SUM_SHARED" +get_file "https://github.com/Genymobile/scrcpy/releases/download/v1.16/$FILENAME_DEV" \ + "$FILENAME_DEV" "$SHA256SUM_DEV" + +mkdir "$DEP_DIR" +cd "$DEP_DIR" + +ZIP_PREFIX_SHARED=ffmpeg-4.3.1-win32-shared +unzip "../$FILENAME_SHARED" \ + "$ZIP_PREFIX_SHARED"/bin/avutil-56.dll \ + "$ZIP_PREFIX_SHARED"/bin/avcodec-58.dll \ + "$ZIP_PREFIX_SHARED"/bin/avformat-58.dll \ + "$ZIP_PREFIX_SHARED"/bin/swresample-3.dll \ + "$ZIP_PREFIX_SHARED"/bin/swscale-5.dll + +ZIP_PREFIX_DEV=ffmpeg-4.3.1-win32-dev +unzip "../$FILENAME_DEV" \ + "$ZIP_PREFIX_DEV/include/*" + +mv "$ZIP_PREFIX_SHARED"/* . +mv "$ZIP_PREFIX_DEV"/* . +rmdir "$ZIP_PREFIX_SHARED" "$ZIP_PREFIX_DEV" diff --git a/prebuilt-deps/prepare-ffmpeg-win64.sh b/prebuilt-deps/prepare-ffmpeg-win64.sh new file mode 100755 index 00000000..a62eb261 --- /dev/null +++ b/prebuilt-deps/prepare-ffmpeg-win64.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +set -e +DIR=$(dirname ${BASH_SOURCE[0]}) +cd "$DIR" +. common +mkdir -p "$PREBUILT_DATA_DIR" +cd "$PREBUILT_DATA_DIR" + +DEP_DIR=ffmpeg-win64-5.0 + +FILENAME=ffmpeg-5.0-full_build-shared.7z +SHA256SUM=e5900f6cecd4c438d398bd2fc308736c10b857cd8dd61c11bcfb05bff5d1211a + +if [[ -d "$DEP_DIR" ]] +then + echo "$DEP_DIR" found + exit 0 +fi + +get_file "https://github.com/GyanD/codexffmpeg/releases/download/5.0/$FILENAME" \ + "$FILENAME" "$SHA256SUM" + +mkdir "$DEP_DIR" +cd "$DEP_DIR" + +ZIP_PREFIX=ffmpeg-5.0-full_build-shared +7z x "../$FILENAME" \ + "$ZIP_PREFIX"/bin/avutil-57.dll \ + "$ZIP_PREFIX"/bin/avcodec-59.dll \ + "$ZIP_PREFIX"/bin/avformat-59.dll \ + "$ZIP_PREFIX"/bin/swresample-4.dll \ + "$ZIP_PREFIX"/bin/swscale-6.dll \ + "$ZIP_PREFIX"/include +mv "$ZIP_PREFIX"/* . +rmdir "$ZIP_PREFIX" diff --git a/prebuilt-deps/prepare-sdl.sh b/prebuilt-deps/prepare-sdl.sh new file mode 100755 index 00000000..c414c854 --- /dev/null +++ b/prebuilt-deps/prepare-sdl.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +set -e +DIR=$(dirname ${BASH_SOURCE[0]}) +cd "$DIR" +. common +mkdir -p "$PREBUILT_DATA_DIR" +cd "$PREBUILT_DATA_DIR" + +DEP_DIR=SDL2-2.0.20 + +FILENAME=SDL2-devel-2.0.20-mingw.tar.gz +SHA256SUM=38094d82a857d6c62352e5c5cdec74948c5b4d25c59cbd298d6d233568976bd1 + +if [[ -d "$DEP_DIR" ]] +then + echo "$DEP_DIR" found + exit 0 +fi + +get_file "https://libsdl.org/release/$FILENAME" "$FILENAME" "$SHA256SUM" + +mkdir "$DEP_DIR" +cd "$DEP_DIR" + +TAR_PREFIX="$DEP_DIR" # root directory inside the tar has the same name +tar xf "../$FILENAME" --strip-components=1 \ + "$TAR_PREFIX"/i686-w64-mingw32/bin/SDL2.dll \ + "$TAR_PREFIX"/i686-w64-mingw32/include/ \ + "$TAR_PREFIX"/i686-w64-mingw32/lib/ \ + "$TAR_PREFIX"/x86_64-w64-mingw32/bin/SDL2.dll \ + "$TAR_PREFIX"/x86_64-w64-mingw32/include/ \ + "$TAR_PREFIX"/x86_64-w64-mingw32/lib/ \ diff --git a/release.mk b/release.mk index 2f2fe9e1..37b8d5c5 100644 --- a/release.mk +++ b/release.mk @@ -63,7 +63,9 @@ build-server: ninja -C "$(SERVER_BUILD_DIR)" prepare-deps-win32: - -$(MAKE) -C prebuilt-deps prepare-win32 + @prebuilt-deps/prepare-adb.sh + @prebuilt-deps/prepare-sdl.sh + @prebuilt-deps/prepare-ffmpeg-win32.sh build-win32: prepare-deps-win32 [ -d "$(WIN32_BUILD_DIR)" ] || ( mkdir "$(WIN32_BUILD_DIR)" && \ @@ -75,7 +77,9 @@ build-win32: prepare-deps-win32 ninja -C "$(WIN32_BUILD_DIR)" prepare-deps-win64: - -$(MAKE) -C prebuilt-deps prepare-win64 + @prebuilt-deps/prepare-adb.sh + @prebuilt-deps/prepare-sdl.sh + @prebuilt-deps/prepare-ffmpeg-win64.sh build-win64: prepare-deps-win64 [ -d "$(WIN64_BUILD_DIR)" ] || ( mkdir "$(WIN64_BUILD_DIR)" && \ @@ -94,15 +98,15 @@ dist-win32: build-server build-win32 cp data/scrcpy-noconsole.vbs "$(DIST)/$(WIN32_TARGET_DIR)" cp data/icon.png "$(DIST)/$(WIN32_TARGET_DIR)" cp data/open_a_terminal_here.bat "$(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)/" - cp prebuilt-deps/SDL2-2.0.20/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/data/ffmpeg-win32-4.3.1/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/data/ffmpeg-win32-4.3.1/bin/avcodec-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/data/ffmpeg-win32-4.3.1/bin/avformat-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/data/ffmpeg-win32-4.3.1/bin/swresample-3.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/data/ffmpeg-win32-4.3.1/bin/swscale-5.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/data/platform-tools-31.0.3/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/data/platform-tools-31.0.3/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/data/platform-tools-31.0.3/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/data/SDL2-2.0.20/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/" dist-win64: build-server build-win64 mkdir -p "$(DIST)/$(WIN64_TARGET_DIR)" @@ -112,15 +116,15 @@ dist-win64: build-server build-win64 cp data/scrcpy-noconsole.vbs "$(DIST)/$(WIN64_TARGET_DIR)" cp data/icon.png "$(DIST)/$(WIN64_TARGET_DIR)" cp data/open_a_terminal_here.bat "$(DIST)/$(WIN64_TARGET_DIR)" - cp prebuilt-deps/ffmpeg-5.0-full_build-shared/bin/avutil-57.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-5.0-full_build-shared/bin/avcodec-59.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-5.0-full_build-shared/bin/avformat-59.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-5.0-full_build-shared/bin/swresample-4.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-5.0-full_build-shared/bin/swscale-6.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)/" - cp prebuilt-deps/SDL2-2.0.20/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/data/ffmpeg-win64-5.0/bin/avutil-57.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/data/ffmpeg-win64-5.0/bin/avcodec-59.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/data/ffmpeg-win64-5.0/bin/avformat-59.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/data/ffmpeg-win64-5.0/bin/swresample-4.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/data/ffmpeg-win64-5.0/bin/swscale-6.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/data/platform-tools-31.0.3/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/data/platform-tools-31.0.3/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/data/platform-tools-31.0.3/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/data/SDL2-2.0.20/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/" zip-win32: dist-win32 cd "$(DIST)/$(WIN32_TARGET_DIR)"; \ From 334f46995a92df381df4c0ce818c3a533eaa203c Mon Sep 17 00:00:00 2001 From: Cccc_owo Date: Fri, 28 Jan 2022 16:09:03 +0800 Subject: [PATCH 1048/2244] Update README.zh-Hans.md to v1.21 PR #2978 Signed-off-by: Romain Vimont --- README.md | 2 +- README.zh-Hans.md | 143 +++++++++++++++++++++++++++++++++++----------- 2 files changed, 110 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index db0e2244..01ed0991 100644 --- a/README.md +++ b/README.md @@ -1052,7 +1052,7 @@ This README is available in other languages: - [한국어 (Korean, `ko`) - v1.11](README.ko.md) - [Português Brasileiro (Brazilian Portuguese, `pt-BR`) - v1.19](README.pt-br.md) - [Español (Spanish, `sp`) - v1.21](README.sp.md) -- [简体中文 (Simplified Chinese, `zh-Hans`) - v1.20](README.zh-Hans.md) +- [简体中文 (Simplified Chinese, `zh-Hans`) - v1.21](README.zh-Hans.md) - [繁體中文 (Traditional Chinese, `zh-Hant`) - v1.15](README.zh-Hant.md) - [Turkish (Turkish, `tr`) - v1.18](README.tr.md) diff --git a/README.zh-Hans.md b/README.zh-Hans.md index b96d6d5a..69c6e7c0 100644 --- a/README.zh-Hans.md +++ b/README.zh-Hans.md @@ -1,14 +1,14 @@ _Only the original [README](README.md) is guaranteed to be up-to-date._ -只有原版的[README](README.md)会保持最新。 +_只有原版的 [README](README.md)是保证最新的。_ -Current version is based on [65b023a] +Current version is based on [8615813] -本文根据[65b023a]进行翻译。 +本文根据[8615813]进行翻译。 -[65b023a]: https://github.com/Genymobile/scrcpy/blob/65b023ac6d586593193fd5290f65e25603b68e02/README.md +[8615813]: https://github.com/Genymobile/scrcpy/blob/86158130051d450a449a2e7bb20b0fcef1b62e80/README.md -# scrcpy (v1.20) +# scrcpy (v1.21) scrcpy @@ -68,12 +68,18 @@ Current version is based on [65b023a] ### Linux -在 Debian (目前仅支持 _testing_ 和 _sid_ 分支) 和Ubuntu (20.04) 上: +在 Debian 和 Ubuntu 上: ``` apt install scrcpy ``` +在 Arch Linux 上: + +``` +pacman -S scrcpy +``` + 我们也提供 [Snap] 包: [`scrcpy`][snap-link]。 [snap-link]: https://snapstats.org/snaps/scrcpy @@ -85,11 +91,6 @@ apt install scrcpy [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 @@ -343,9 +344,32 @@ scrcpy --v4l2-buffer=500 # 为 v4l2 漏增加 500 毫秒的缓冲 ### 连接 -#### 无线 +#### TCP/IP (无线) -_Scrcpy_ 使用 `adb` 与设备通信,并且 `adb` 支持通过 TCP/IP [连接]到设备: +_Scrcpy_ 使用 `adb` 与设备通信,并且 `adb` 支持通过 TCP/IP [连接]到设备(设备必须连接与电脑相同的网络)。 + +##### 自动配置 + +参数 `--tcpip` 允许自动配置连接。这里有两种方式。 + +对于传入的 adb 连接,如果设备(在这个例子中以192.168.1.1为可用地址)已经监听了一个端口(通常是5555),运行: + +```bash +scrcpy --tcpip=192.168.1.1 # 默认端口是5555 +scrcpy --tcpip=192.168.1.1:5555 +``` + +如果adb TCP/IP(无线) 模式在某些设备上不被启用(或者你不知道IP地址),用USB连接设备,然后运行: + +```bash +scrcpy --tcpip # 无需参数 +``` + +这将会自动寻找设备IP地址,启用TCP/IP模式,然后在启动之前连接到设备。 + +##### 手动配置 + +或者,可以通过 `adb` 使用手动启用 TCP/IP 连接: 1. 将设备和电脑连接至同一 Wi-Fi。 2. 打开 设置 → 关于手机 → 状态信息,获取设备的 IP 地址,也可以执行以下的命令: @@ -354,12 +378,12 @@ _Scrcpy_ 使用 `adb` 与设备通信,并且 `adb` 支持通过 TCP/IP [连接 adb shell ip route | awk '{print $9}' ``` -3. 启用设备的网络 adb 功能: `adb tcpip 5555`。 +3. 启用设备的网络 adb 功能:`adb tcpip 5555`。 4. 断开设备的 USB 连接。 5. 连接到您的设备:`adb connect DEVICE_IP:5555` _(将 `DEVICE_IP` 替换为设备 IP)_。 6. 正常运行 `scrcpy`。 -可能降低码率和分辨率会更好一些: +降低比特率和分辨率可能很有用: ```bash scrcpy --bit-rate 2M --max-size 800 @@ -397,33 +421,75 @@ autoadb scrcpy -s '{}' [AutoAdb]: https://github.com/rom1v/autoadb -#### SSH 隧道 +#### 隧道 -要远程连接到设备,可以将本地的 adb 客户端连接到远程的 adb 服务端 (需要两端的 _adb_ 协议版本相同): +要远程连接到设备,可以将本地的 adb 客户端连接到远程的 adb 服务端 (需要两端的 _adb_ 协议版本相同)。 + +##### 远程ADB服务器 + +要连接到一个远程ADB服务器,让服务器在所有接口上监听: ```bash -adb kill-server # 关闭本地 5037 端口上的 adb 服务端 -ssh -CN -L5037:localhost:5037 -R27183:localhost:27183 your_remote_computer +adb kill-server +adb -a nodaemon server start # 保持该窗口开启 ``` -在另一个终端: +**警告:所有客户端与ADB服务器的交流都是未加密的。** + +假设此服务器可在 192.168.1.2 访问。 然后,从另一个终端,运行 scrcpy: ```bash +export ADB_SERVER_SOCKET=tcp:192.168.1.2:5037 +scrcpy --tunnel-host=192.168.1.2 +``` + +默认情况下,scrcpy使用用于 `adb forward` 隧道建立的本地端口(通常是 `27183`,见 `--port` )。它也可以强制使用一个不同的隧道端口(当涉及更多的重定向时,这在更复杂的情况下可能很有用): + +``` +scrcpy --tunnel-port=1234 +``` + + +##### SSH 隧道 + +为了安全地与远程ADB服务器通信,最好使用SSH隧道。 + +首先,确保ADB服务器正在远程计算机上运行: + +```bash +adb start-server +``` + +然后,建立一个SSH隧道: + +```bash +# 本地 5038 --> 远程 5037 +# 本地 27183 <-- 远程 27183 +ssh -CN -L5038:localhost:5037 -R27183:localhost:27183 your_remote_computer +# 保持该窗口开启 +``` + +在另一个终端上,运行scrcpy: + +```bash +export ADB_SERVER_SOCKET=tcp:localhost:5038 scrcpy ``` -若要不使用远程端口转发,可以强制使用正向连接 (注意 `-L` 和 `-R` 的区别): +若要不使用远程端口转发,可以强制使用正向连接(注意是 `-L` 而不是 `-R` ): ```bash -adb kill-server # 关闭本地 5037 端口上的 adb 服务端 -ssh -CN -L5037:localhost:5037 -L27183:localhost:27183 your_remote_computer +# 本地 5038 --> 远程 5037 +# 本地 27183 <-- 远程 27183 +ssh -CN -L5038:localhost:5037 -L27183:localhost:27183 your_remote_computer # 保持该窗口开启 ``` -在另一个终端: +在另一个终端上,运行scrcpy: ```bash +export ADB_SERVER_SOCKET=tcp:localhost:5038 scrcpy --force-adb-forward ``` @@ -441,7 +507,7 @@ scrcpy -b2M -m800 --max-fps 15 窗口的标题默认为设备型号。可以通过如下命令修改: ```bash -scrcpy --window-title 'My device' +scrcpy --window-title "我的设备" ``` #### 位置和大小 @@ -630,6 +696,8 @@ scrcpy --disable-screensaver 一些设备不支持通过程序设置剪贴板。通过 `--legacy-paste` 选项可以修改 Ctrl+vMOD+v 的工作方式,使它们通过按键事件 (同 MOD+Shift+v) 来注入电脑剪贴板内容。 +要禁用自动剪贴板同步功能,使用`--no-clipboard-autosync`。 + #### 双指缩放 模拟“双指缩放”:Ctrl+_按住并移动鼠标_。 @@ -659,11 +727,11 @@ scrcpy -K # 简写 在这种模式下,原始按键事件 (扫描码) 被发送给设备,而与宿主机按键映射无关。因此,若键盘布局不匹配,需要在 Android 设备上进行配置,具体为 设置 → 系统 → 语言和输入法 → [实体键盘]。 -[Physical keyboard]: https://github.com/Genymobile/scrcpy/pull/2632#issuecomment-923756915 +[实体键盘]: https://github.com/Genymobile/scrcpy/pull/2632#issuecomment-923756915 #### 文本注入偏好 -打字的时候,系统会产生两种[事件][textevents]: +输入文字的时候,系统会产生两种[事件][textevents]: - _按键事件_ ,代表一个按键被按下或松开。 - _文本事件_ ,代表一个字符被输入。 @@ -675,7 +743,13 @@ scrcpy -K # 简写 scrcpy --prefer-text ``` -(这会导致键盘在游戏中工作不正常) +(但这会导致键盘在游戏中工作不正常) + +相反,您可以强制始终注入原始按键事件: + +```bash +scrcpy --raw-key-events +``` 该选项不影响 HID 键盘 (该模式下,所有按键都发送为扫描码)。 @@ -765,7 +839,7 @@ _[Super] 键通常是指 WindowsCmd 键。 | 点按 `主屏幕` | MOD+h \| _中键_ | 点按 `返回` | MOD+b \| _右键²_ | 点按 `切换应用` | MOD+s \| _第4键³_ - | 点按 `菜单` (解锁屏幕) | MOD+m + | 点按 `菜单` (解锁屏幕)⁴ | MOD+m | 点按 `音量+` | MOD+ _(上箭头)_ | 点按 `音量-` | MOD+ _(下箭头)_ | 点按 `电源` | MOD+p @@ -776,9 +850,9 @@ _[Super] 键通常是指 WindowsCmd 键。 | 展开通知面板 | MOD+n \| _第5键³_ | 展开设置面板 | MOD+n+n \| _双击第5键³_ | 收起通知面板 | MOD+Shift+n - | 复制到剪贴板⁴ | MOD+c - | 剪切到剪贴板⁴ | MOD+x - | 同步剪贴板并粘贴⁴ | MOD+v + | 复制到剪贴板⁵ | MOD+c + | 剪切到剪贴板⁵ | MOD+x + | 同步剪贴板并粘贴⁵ | MOD+v | 注入电脑剪贴板文本 | MOD+Shift+v | 打开/关闭FPS显示 (至标准输出) | MOD+i | 捏拉缩放 | Ctrl+_按住并移动鼠标_ @@ -788,7 +862,8 @@ _[Super] 键通常是指 WindowsCmd 键。 _¹双击黑边可以去除黑边。_ _²点击鼠标右键将在屏幕熄灭时点亮屏幕,其余情况则视为按下返回键 。_ _³鼠标的第4键和第5键。_ -_⁴需要安卓版本 Android >= 7。_ +_⁴对于开发中的 react-native 应用程序,`MENU` 触发开发菜单。_ +_⁵需要安卓版本 Android >= 7。_ 有重复按键的快捷键通过松开再按下一个按键来进行,如“展开设置面板”: @@ -816,7 +891,7 @@ ADB=/path/to/adb scrcpy 一个同事让我找出一个和 [gnirehtet] 一样难以发音的名字。 -[`strcpy`] 复制一个 **str**ing (字符串); `scrcpy` 复制一个 **scr**een (屏幕)。 +[`strcpy`] 源于 **str**ing (字符串); `scrcpy` 源于 **scr**een (屏幕)。 [gnirehtet]: https://github.com/Genymobile/gnirehtet [`strcpy`]: http://man7.org/linux/man-pages/man3/strcpy.3.html From 64a09513ae8a3941952e6e82969193f4cf393e81 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 29 Jan 2022 15:44:52 +0100 Subject: [PATCH 1049/2244] Bump version to 1.22 --- app/scrcpy-windows.rc | 2 +- meson.build | 2 +- server/build.gradle | 4 ++-- server/build_without_gradle.sh | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/scrcpy-windows.rc b/app/scrcpy-windows.rc index 7525d092..b130dc6a 100644 --- a/app/scrcpy-windows.rc +++ b/app/scrcpy-windows.rc @@ -13,7 +13,7 @@ BEGIN VALUE "LegalCopyright", "Romain Vimont, Genymobile" VALUE "OriginalFilename", "scrcpy.exe" VALUE "ProductName", "scrcpy" - VALUE "ProductVersion", "1.21" + VALUE "ProductVersion", "1.22" END END BLOCK "VarFileInfo" diff --git a/meson.build b/meson.build index 0b4ed174..b4b59353 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('scrcpy', 'c', - version: '1.21', + version: '1.22', meson_version: '>= 0.48', default_options: [ 'c_std=c11', diff --git a/server/build.gradle b/server/build.gradle index 1f939a1a..f262b02d 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -6,8 +6,8 @@ android { applicationId "com.genymobile.scrcpy" minSdkVersion 21 targetSdkVersion 31 - versionCode 12100 - versionName "1.21" + versionCode 12200 + versionName "1.22" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index ab5e19e4..ecf7b604 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.21 +SCRCPY_VERSION_NAME=1.22 PLATFORM=${ANDROID_PLATFORM:-31} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-31.0.0} From f4c7044b46ae28eb64cb5e1a15c9649a44023c70 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 29 Jan 2022 16:13:50 +0100 Subject: [PATCH 1050/2244] Update links to v1.22 --- BUILD.md | 6 +++--- README.md | 8 ++++---- install_release.sh | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/BUILD.md b/BUILD.md index c9473f27..1e713b93 100644 --- a/BUILD.md +++ b/BUILD.md @@ -270,10 +270,10 @@ install` must be run as root)._ #### Option 2: Use prebuilt server - - [`scrcpy-server-v1.21`][direct-scrcpy-server] - _(SHA-256: dbcccab523ee26796e55ea33652649e4b7af498edae9aa75e4d4d7869c0ab848)_ + - [`scrcpy-server-v1.22`][direct-scrcpy-server] + _(SHA-256: c05d273eec7533c0e106282e0254cf04e7f5e8f0c2920ca39448865fab2a419b)_ -[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.21/scrcpy-server-v1.21 +[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.22/scrcpy-server-v1.22 Download the prebuilt server somewhere, and specify its path during the Meson configuration: diff --git a/README.md b/README.md index feea032d..8306e62e 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# scrcpy (v1.21) +# scrcpy (v1.22) scrcpy @@ -108,10 +108,10 @@ process][BUILD_simple]). For Windows, for simplicity, a prebuilt archive with all the dependencies (including `adb`) is available: - - [`scrcpy-win64-v1.21.zip`][direct-win64] - _(SHA-256: fdab0c1421353b592a9bbcebd6e252675eadccca65cca8105686feaa9c1ded53)_ + - [`scrcpy-win64-v1.22.zip`][direct-win64] + _(SHA-256: ce4d9b8cc761e29862c4a72d8ad6f538bdd1f1831d15fd1f36633cd3b403db82)_ -[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.21/scrcpy-win64-v1.21.zip +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.22/scrcpy-win64-v1.22.zip It is also available in [Chocolatey]: diff --git a/install_release.sh b/install_release.sh index 2a59a6f1..69dfefdc 100755 --- a/install_release.sh +++ b/install_release.sh @@ -2,8 +2,8 @@ set -e BUILDDIR=build-auto -PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v1.21/scrcpy-server-v1.21 -PREBUILT_SERVER_SHA256=dbcccab523ee26796e55ea33652649e4b7af498edae9aa75e4d4d7869c0ab848 +PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v1.22/scrcpy-server-v1.22 +PREBUILT_SERVER_SHA256=c05d273eec7533c0e106282e0254cf04e7f5e8f0c2920ca39448865fab2a419b echo "[scrcpy] Downloading prebuilt server..." wget "$PREBUILT_SERVER_URL" -O scrcpy-server From a86deab3d4dcf7471b493ba82365aa284e1f74b8 Mon Sep 17 00:00:00 2001 From: Cccc_owo <47687154+Cccc-owo@users.noreply.github.com> Date: Sun, 30 Jan 2022 10:43:12 +0800 Subject: [PATCH 1051/2244] Update README.zh-Hans.md to v1.22 PR #2989 Signed-off-by: Romain Vimont --- README.md | 2 +- README.zh-Hans.md | 63 +++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 59 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 8306e62e..e8ddf907 100644 --- a/README.md +++ b/README.md @@ -1116,7 +1116,7 @@ This README is available in other languages: - [한국어 (Korean, `ko`) - v1.11](README.ko.md) - [Português Brasileiro (Brazilian Portuguese, `pt-BR`) - v1.19](README.pt-br.md) - [Español (Spanish, `sp`) - v1.21](README.sp.md) -- [简体中文 (Simplified Chinese, `zh-Hans`) - v1.21](README.zh-Hans.md) +- [简体中文 (Simplified Chinese, `zh-Hans`) - v1.22](README.zh-Hans.md) - [繁體中文 (Traditional Chinese, `zh-Hant`) - v1.15](README.zh-Hant.md) - [Turkish (Turkish, `tr`) - v1.18](README.tr.md) diff --git a/README.zh-Hans.md b/README.zh-Hans.md index acdf9a7e..caf964b3 100644 --- a/README.zh-Hans.md +++ b/README.zh-Hans.md @@ -2,16 +2,18 @@ _Only the original [README](README.md) is guaranteed to be up-to-date._ _只有原版的 [README](README.md)是保证最新的。_ -Current version is based on [8615813] +Current version is based on [f4c7044] -本文根据[8615813]进行翻译。 +本文根据[f4c7044]进行翻译。 -[8615813]: https://github.com/Genymobile/scrcpy/blob/86158130051d450a449a2e7bb20b0fcef1b62e80/README.md +[f4c7044]: https://github.com/Genymobile/scrcpy/blob/f4c7044b46ae28eb64cb5e1a15c9649a44023c70/README.md -# scrcpy (v1.21) +# scrcpy (v1.22) scrcpy +_发音为 "**scr**een **c**o**py**"_ + 本应用程序可以显示并控制通过 USB (或 [TCP/IP][article-tcpip]) 连接的安卓设备,且不需要任何 _root_ 权限。本程序支持 _GNU/Linux_, _Windows_ 和 _macOS_。 ![screenshot](assets/screenshot-debian-600.jpg) @@ -36,6 +38,8 @@ Current version is based on [8615813] - [可配置显示质量](#采集设置) - 以设备屏幕[作为摄像头(V4L2)](#v4l2loopback) (仅限 Linux) - [模拟物理键盘 (HID)](#物理键盘模拟-hid) (仅限 Linux) + - [物理鼠标模拟 (HID)](#物理鼠标模拟-hid) (仅限 Linux) + - [OTG模式](#otg) (仅限 Linux) - 更多 …… ## 系统要求 @@ -362,7 +366,7 @@ scrcpy --tcpip=192.168.1.1:5555 如果adb TCP/IP(无线) 模式在某些设备上不被启用(或者你不知道IP地址),用USB连接设备,然后运行: ```bash -scrcpy --tcpip # 无需参数 +scrcpy --tcpip # 无需其他参数 ``` 这将会自动寻找设备IP地址,启用TCP/IP模式,然后在启动之前连接到设备。 @@ -729,6 +733,55 @@ scrcpy -K # 简写 [实体键盘]: https://github.com/Genymobile/scrcpy/pull/2632#issuecomment-923756915 +#### 物理鼠标模拟 (HID) + +与物理键盘模拟类似,可以模拟一个物理鼠标。 同样,它仅在设备通过 USB 连接时才有效,并且目前仅在 Linux 上受支持。 + +默认情况下,scrcpy 使用 Android 鼠标事件注入,使用绝对坐标。 通过模拟物理鼠标,在Android设备上出现鼠标指针,并注入鼠标相对运动、点击和滚动。 + +启用此模式: + +```bash +scrcpy --hid-mouse +scrcpy -M # 简写 +``` + +您还可以将 `--forward-all-clicks` 添加到 [转发所有点击][forward_all_clicks]. + +[forward_all_clicks]: #右键和中键 + +启用此模式后,计算机鼠标将被“捕获”(鼠标指针从计算机上消失并出现在 Android 设备上)。 + +特殊的捕获键,AltSuper,切换(禁用或启用)鼠标捕获。 使用其中之一将鼠标的控制权交还给计算机。 + + +#### OTG + +可以仅使用物理键盘和鼠标模拟 (HID) 运行 _scrcpy_,就好像计算机键盘和鼠标通过 OTG 线直接插入设备一样。 + +在这个模式下,_adb_ (USB 调试)是不必要的,且镜像被禁用。 + +启用 OTG 模式: + +```bash +scrcpy --otg +# 如果有多个 USB 设备可用,则通过序列号选择 +scrcpy --otg -s 0123456789abcdef +``` + +只开启 HID 键盘 或 HID 鼠标 是可行的: + +```bash +scrcpy --otg --hid-keyboard # 只开启 HID 键盘 +scrcpy --otg --hid-mouse # 只开启 HID 鼠标 +scrcpy --otg --hid-keyboard --hid-mouse # 开启 HID 键盘 和 HID 鼠标 +# 为了方便,默认两者都开启 +scrcpy --otg # 开启 HID 键盘 和 HID 鼠标 +``` + +像 `--hid-keyboard` 和 `--hid-mouse` 一样,它只在设备通过 USB 连接时才有效,且目前仅在 Linux 上支持。 + + #### 文本注入偏好 输入文字的时候,系统会产生两种[事件][textevents]: From a1967b4dfdc01e70a29e7d679f425ab413c90aa4 Mon Sep 17 00:00:00 2001 From: Cccc_owo <47687154+Cccc-owo@users.noreply.github.com> Date: Sun, 30 Jan 2022 11:02:16 +0800 Subject: [PATCH 1052/2244] Update FAQ.zh-Hans.md to v1.22 PR #2989 Signed-off-by: Romain Vimont --- FAQ.md | 2 +- FAQ.zh-Hans.md | 74 ++++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 60 insertions(+), 16 deletions(-) diff --git a/FAQ.md b/FAQ.md index 6b76190d..5829136d 100644 --- a/FAQ.md +++ b/FAQ.md @@ -305,4 +305,4 @@ This FAQ is available in other languages: - [Italiano (Italiano, `it`) - v1.19](FAQ.it.md) - [한국어 (Korean, `ko`) - v1.11](FAQ.ko.md) - - [简体中文 (Simplified Chinese, `zh-Hans`) - v1.18](FAQ.zh-Hans.md) + - [简体中文 (Simplified Chinese, `zh-Hans`) - v1.22](FAQ.zh-Hans.md) diff --git a/FAQ.zh-Hans.md b/FAQ.zh-Hans.md index 136b5f2e..23674eed 100644 --- a/FAQ.zh-Hans.md +++ b/FAQ.zh-Hans.md @@ -1,7 +1,12 @@ -只有原版的[FAQ](FAQ.md)会保持更新。 -本文根据[d6aaa5]翻译。 +_Only the original [FAQ.md](FAQ.md) is guaranteed to be up-to-date._ -[d6aaa5]:https://github.com/Genymobile/scrcpy/blob/d6aaa5bf9aa3710660c683b6e3e0ed971ee44af5/FAQ.md +_只有原版的 [FAQ.md](FAQ.md)是保证最新的。_ + +Current version is based on [28054cd] + +本文根据[28054cd]进行翻译。 + +[28054cd]: https://github.com/Genymobile/scrcpy/blob/28054cd471f848733e11372c9d745cd5d71e6ce7/FAQ.md # 常见问题 @@ -9,11 +14,11 @@ ## `adb` 相关问题 -`scrcpy` 执行 `adb` 命令来初始化和设备之间的连接。如果`adb` 执行失败了, scrcpy 就无法工作。 +`scrcpy` 执行 `adb` 命令来初始化和设备之间的连接。如果 `adb` 执行失败了, scrcpy 就无法工作。 在这种情况中,将会输出这个错误: -> ERROR: "adb push" returned with value 1 +> ERROR: "adb get-serialno" returned with value 1 这通常不是 _scrcpy_ 的bug,而是你的环境的问题。 @@ -33,28 +38,37 @@ adb devices ### 设备未授权 -参见这里 [stackoverflow][device-unauthorized]. + +> error: device unauthorized. +> This adb server's $ADB_VENDOR_KEYS is not set +> Try 'adb kill-server' if that seems wrong. +> Otherwise check for a confirmation dialog on your device. + +连接时,在设备上应该会打开一个弹出窗口。 您必须授权 USB 调试。 + +如果没有打开,参见[stackoverflow][device-unauthorized]. [device-unauthorized]: https://stackoverflow.com/questions/23081263/adb-android-device-unauthorized ### 未检测到设备 -> adb: error: failed to get feature set: no devices/emulators found +> error: no devices/emulators found 确认已经正确启用 [adb debugging][enable-adb]. -如果你的设备没有被检测到,你可能需要一些[驱动][drivers] (在 Windows上). +如果你的设备没有被检测到,你可能需要一些[驱动][drivers] (在 Windows上)。这里有一个单独的 [适用于Google设备的USB驱动][google-usb-driver]. [enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling [drivers]: https://developer.android.com/studio/run/oem-usb.html +[google-usb-driver]: https://developer.android.com/studio/run/win-usb ### 已连接多个设备 如果连接了多个设备,您将遇到以下错误: -> adb: error: failed to get feature set: more than one device/emulator +> error: more than one device/emulator 必须提供要镜像的设备的标识符: @@ -90,19 +104,19 @@ scrcpy ### 设备断开连接 如果 _scrcpy_ 在警告“设备连接断开”的情况下自动中止,那就意味着`adb`连接已经断开了。 -请尝试使用另一条USB线或者电脑上的另一个USB接口。 -请参看 [#281] 和 [#283]。 + +请尝试使用另一条USB线或者电脑上的另一个USB接口。请参看 [#281] 和 [#283]。 [#281]: https://github.com/Genymobile/scrcpy/issues/281 [#283]: https://github.com/Genymobile/scrcpy/issues/283 + ## 控制相关问题 ### 鼠标和键盘不起作用 在某些设备上,您可能需要启用一个选项以允许 [模拟输入][simulating input]。 - 在开发者选项中,打开: > **USB调试 (安全设置)** @@ -115,10 +129,12 @@ scrcpy 可输入的文本[被限制为ASCII字符][text-input]。也可以用一些小技巧输入一些[带重音符号的字符][accented-characters],但是仅此而已。参见[#37]。 +自 Linux 上的 scrcpy v1.20 之后,可以模拟[物理键盘][hid] (HID)。 [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 +[hid]: README.md#physical-keyboard-simulation-hid ## 客户端相关问题 @@ -129,7 +145,6 @@ scrcpy [#40]: https://github.com/Genymobile/scrcpy/issues/40 - 为了提升降尺度的质量,如果渲染器是OpenGL并且支持mip映射,就会自动开启三线性过滤。 在Windows上,你可能希望强制使用OpenGL: @@ -177,6 +192,7 @@ scrcpy ## 崩溃 ### 异常 + 可能有很多原因。一个常见的原因是您的设备无法按给定清晰度进行编码: > ``` @@ -204,12 +220,40 @@ scrcpy -m 1024 scrcpy -m 800 ``` +自 scrcpy v1.22以来,scrcpy 会自动在失败前以更低的分辨率重试。这种行为可以用`--no-downsize-on-error`关闭。 + 你也可以尝试另一种 [编码器](README.md#encoder)。 +如果您在 Android 12 上遇到此异常,则只需升级到 scrcpy >= 1.18 (见 [#2129]): + +``` +> ERROR: Exception on thread Thread[main,5,main] +java.lang.AssertionError: java.lang.reflect.InvocationTargetException + at com.genymobile.scrcpy.wrappers.SurfaceControl.setDisplaySurface(SurfaceControl.java:75) + ... +Caused by: java.lang.reflect.InvocationTargetException + at java.lang.reflect.Method.invoke(Native Method) + at com.genymobile.scrcpy.wrappers.SurfaceControl.setDisplaySurface(SurfaceControl.java:73) + ... 7 more +Caused by: java.lang.IllegalArgumentException: displayToken must not be null + at android.view.SurfaceControl$Transaction.setDisplaySurface(SurfaceControl.java:3067) + at android.view.SurfaceControl.setDisplaySurface(SurfaceControl.java:2147) + ... 9 more +``` + +[#2129]: https://github.com/Genymobile/scrcpy/issues/2129 + + ## Windows命令行 -一些Windows用户不熟悉命令行。以下是如何打开终端并带参数执行`scrcpy`: +从 v1.22 开始,增加了一个“快捷方式”,可以直接在 scrcpy 目录打开一个终端。双击`open_a_terminal_here.bat`,然后输入你的命令。 例如: + +``` +scrcpy --record file.mkv +``` + +您也可以打开终端并手动转到 scrcpy 文件夹: 1. 按下 Windows+r,打开一个对话框。 2. 输入 `cmd` 并按 Enter,这样就打开了一个终端。 @@ -233,7 +277,7 @@ scrcpy -m 800 scrcpy --prefer-text --turn-screen-off --stay-awake ``` -然后双击刚刚创建的文件。 +然后只需双击刚刚创建的文件。 你也可以编辑 `scrcpy-console.bat` 或者 `scrcpy-noconsole.vbs`(的副本)来添加参数。 From 22d9f0faf4fe22e6316450041585a2cdd04d0eca Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 1 Feb 2022 21:09:02 +0100 Subject: [PATCH 1053/2244] Fix comment typo --- app/src/adb_parser.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/adb_parser.c b/app/src/adb_parser.c index 5ac21ede..d660aaab 100644 --- a/app/src/adb_parser.c +++ b/app/src/adb_parser.c @@ -8,7 +8,7 @@ static char * sc_adb_parse_device_ip_from_line(char *line, size_t len) { - // One line from "ip route" looks lile: + // One line from "ip route" looks like: // "192.168.1.0/24 dev wlan0 proto kernel scope link src 192.168.1.x" // Get the location of the device name (index of "wlan0" in the example) From c8d0f5cdeb75cde4f5cff9a75daf000f15077acc Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 1 Feb 2022 21:28:26 +0100 Subject: [PATCH 1054/2244] Fix sc_str_truncate() documentation The function was initially implemented to truncate lines, but was later generalized to accept custom delimiters. The whole documentation has not been updated accordingly. Refs 9619ade706824a92ea387bdc9d0d27816bf79da5 --- app/src/util/str.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/util/str.h b/app/src/util/str.h index b81764ef..dfe0cb30 100644 --- a/app/src/util/str.h +++ b/app/src/util/str.h @@ -106,10 +106,10 @@ sc_str_wrap_lines(const char *input, unsigned columns, unsigned indent); /** * Truncate the data after any of the characters from `endchars` * - * An '\0' is always written at the end of the data, even if no newline - * character is encountered. + * An '\0' is always written at the end of the data string, even if no + * character from `endchars` is encountered. * - * Return the size of the resulting line. + * Return the size of the resulting string (as strlen() would return). */ size_t sc_str_truncate(char *data, size_t len, const char *endchars); From 9b4360b6b8e1ba4234f5b11b8217379faafcb3b7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 1 Feb 2022 21:35:02 +0100 Subject: [PATCH 1055/2244] Add warning in function documentation The function parsing "ip route" output modifies the input buffer to tokenize in place. This must be mentioned in the function documentation. --- app/src/adb_parser.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/adb_parser.h b/app/src/adb_parser.h index 79f26631..ed7363d1 100644 --- a/app/src/adb_parser.h +++ b/app/src/adb_parser.h @@ -7,6 +7,8 @@ /** * Parse the ip from the output of `adb shell ip route` + * + * Warning: this function modifies the buffer for optimization purposes. */ char * sc_adb_parse_device_ip_from_output(char *buf, size_t buf_len); From f02f2135cdd03f174bba6ce18ee7840469111378 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 1 Feb 2022 21:36:37 +0100 Subject: [PATCH 1056/2244] Fix include for standard library header --- app/src/adb_parser.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/adb_parser.h b/app/src/adb_parser.h index ed7363d1..bce5126c 100644 --- a/app/src/adb_parser.h +++ b/app/src/adb_parser.h @@ -3,7 +3,7 @@ #include "common.h" -#include "stddef.h" +#include /** * Parse the ip from the output of `adb shell ip route` From c0a75ca7462b6c22847e8cefe5c25aa410591b79 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 29 Jan 2022 23:10:24 +0100 Subject: [PATCH 1057/2244] Downscale and retry also on early MediaCodec error The new retry mechanism with a lower definition only worked if the error occurred during encode(). For example: java.lang.IllegalStateException at android.media.MediaCodec.native_dequeueOutputBuffer(Native Method) at android.media.MediaCodec.dequeueOutputBuffer(MediaCodec.java:3452) at com.genymobile.scrcpy.ScreenEncoder.encode(ScreenEncoder.java:114) at com.genymobile.scrcpy.ScreenEncoder.internalStreamScreen(ScreenEncoder.java:95) at com.genymobile.scrcpy.ScreenEncoder.streamScreen(ScreenEncoder.java:61) at com.genymobile.scrcpy.Server.scrcpy(Server.java:80) at com.genymobile.scrcpy.Server.main(Server.java:255) However, MediaCodec may also fail before encoding, during configure() or start(). For example: android.media.MediaCodec$CodecException: Error 0xfffffc0e at android.media.MediaCodec.native_configure(Native Method) at android.media.MediaCodec.configure(MediaCodec.java:1956) at android.media.MediaCodec.configure(MediaCodec.java:1885) at com.genymobile.scrcpy.ScreenEncoder.configure(ScreenEncoder.java:158) at com.genymobile.scrcpy.ScreenEncoder.streamScreen(ScreenEncoder.java:68) at com.genymobile.scrcpy.Server.scrcpy(Server.java:28) at com.genymobile.scrcpy.Server.main(Server.java:110) Also downscale and retry in these cases. Refs #2947 Refs #2988 PR #2990 --- .../com/genymobile/scrcpy/ScreenEncoder.java | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index 4c23dd92..79efc17c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -89,13 +89,15 @@ public class ScreenEncoder implements Device.RotationListener { 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, layerStack); - codec.start(); + + Surface surface = null; try { + configure(codec, format); + surface = codec.createInputSurface(); + setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack); + codec.start(); + alive = encode(codec, fd); // do not call stop() on exception, it would trigger an IllegalStateException codec.stop(); @@ -119,7 +121,9 @@ public class ScreenEncoder implements Device.RotationListener { } finally { destroyDisplay(display); codec.release(); - surface.release(); + if (surface != null) { + surface.release(); + } } } while (alive); } finally { From 0bf7e4ddc42dc73d43666f58dd10edf89c62ae66 Mon Sep 17 00:00:00 2001 From: CennoxX Date: Wed, 2 Feb 2022 11:49:34 +0100 Subject: [PATCH 1058/2244] Add missing spaces in help PR #2994 Signed-off-by: Romain Vimont --- app/src/cli.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index 5587b2d8..7567a10b 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -288,7 +288,7 @@ static const struct sc_option options[] = { "LAlt, LSuper or RSuper toggle the mouse capture mode, to give " "control of the mouse back to the computer.\n" "If any of --hid-keyboard or --hid-mouse is set, only enable " - "keyboard or mouse respectively, otherwise enable both." + "keyboard or mouse respectively, otherwise enable both.\n" "It may only work over USB, and is currently only supported " "on Linux.\n" "See --hid-keyboard and --hid-mouse.", @@ -309,7 +309,7 @@ static const struct sc_option options[] = { { .longopt_id = OPT_PREFER_TEXT, .longopt = "prefer-text", - .text = "Inject alpha characters and space as text events instead of" + .text = "Inject alpha characters and space as text events instead of " "key events.\n" "This avoids issues when combining multiple keys to enter a " "special character, but breaks the expected behavior of alpha " From 0080d0b0ff5a8b91430e12942ba8bb8e66e9e84b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 2 Feb 2022 19:27:41 +0100 Subject: [PATCH 1059/2244] Use sc_ prefix for decoder --- app/src/decoder.c | 55 ++++++++++++++++++++++++----------------------- app/src/decoder.h | 14 ++++++------ app/src/scrcpy.c | 10 ++++----- 3 files changed, 40 insertions(+), 39 deletions(-) diff --git a/app/src/decoder.c b/app/src/decoder.c index a20986dd..9424ec1d 100644 --- a/app/src/decoder.c +++ b/app/src/decoder.c @@ -9,10 +9,10 @@ #include "util/log.h" /** Downcast packet_sink to decoder */ -#define DOWNCAST(SINK) container_of(SINK, struct decoder, packet_sink) +#define DOWNCAST(SINK) container_of(SINK, struct sc_decoder, packet_sink) static void -decoder_close_first_sinks(struct decoder *decoder, unsigned count) { +sc_decoder_close_first_sinks(struct sc_decoder *decoder, unsigned count) { while (count) { struct sc_frame_sink *sink = decoder->sinks[--count]; sink->ops->close(sink); @@ -20,17 +20,17 @@ decoder_close_first_sinks(struct decoder *decoder, unsigned count) { } static inline void -decoder_close_sinks(struct decoder *decoder) { - decoder_close_first_sinks(decoder, decoder->sink_count); +sc_decoder_close_sinks(struct sc_decoder *decoder) { + sc_decoder_close_first_sinks(decoder, decoder->sink_count); } static bool -decoder_open_sinks(struct decoder *decoder) { +sc_decoder_open_sinks(struct sc_decoder *decoder) { for (unsigned i = 0; i < decoder->sink_count; ++i) { struct sc_frame_sink *sink = decoder->sinks[i]; if (!sink->ops->open(sink)) { LOGE("Could not open frame sink %d", i); - decoder_close_first_sinks(decoder, i); + sc_decoder_close_first_sinks(decoder, i); return false; } } @@ -39,7 +39,7 @@ decoder_open_sinks(struct decoder *decoder) { } static bool -decoder_open(struct decoder *decoder, const AVCodec *codec) { +sc_decoder_open(struct sc_decoder *decoder, const AVCodec *codec) { decoder->codec_ctx = avcodec_alloc_context3(codec); if (!decoder->codec_ctx) { LOG_OOM(); @@ -62,7 +62,7 @@ decoder_open(struct decoder *decoder, const AVCodec *codec) { return false; } - if (!decoder_open_sinks(decoder)) { + if (!sc_decoder_open_sinks(decoder)) { LOGE("Could not open decoder sinks"); av_frame_free(&decoder->frame); avcodec_close(decoder->codec_ctx); @@ -74,15 +74,15 @@ decoder_open(struct decoder *decoder, const AVCodec *codec) { } static void -decoder_close(struct decoder *decoder) { - decoder_close_sinks(decoder); +sc_decoder_close(struct sc_decoder *decoder) { + sc_decoder_close_sinks(decoder); av_frame_free(&decoder->frame); avcodec_close(decoder->codec_ctx); avcodec_free_context(&decoder->codec_ctx); } static bool -push_frame_to_sinks(struct decoder *decoder, const AVFrame *frame) { +push_frame_to_sinks(struct sc_decoder *decoder, const AVFrame *frame) { for (unsigned i = 0; i < decoder->sink_count; ++i) { struct sc_frame_sink *sink = decoder->sinks[i]; if (!sink->ops->push(sink, frame)) { @@ -95,7 +95,7 @@ push_frame_to_sinks(struct decoder *decoder, const AVFrame *frame) { } static bool -decoder_push(struct decoder *decoder, const AVPacket *packet) { +sc_decoder_push(struct sc_decoder *decoder, const AVPacket *packet) { bool is_config = packet->pts == AV_NOPTS_VALUE; if (is_config) { // nothing to do @@ -124,39 +124,40 @@ decoder_push(struct decoder *decoder, const AVPacket *packet) { } static bool -decoder_packet_sink_open(struct sc_packet_sink *sink, const AVCodec *codec) { - struct decoder *decoder = DOWNCAST(sink); - return decoder_open(decoder, codec); +sc_decoder_packet_sink_open(struct sc_packet_sink *sink, const AVCodec *codec) { + struct sc_decoder *decoder = DOWNCAST(sink); + return sc_decoder_open(decoder, codec); } static void -decoder_packet_sink_close(struct sc_packet_sink *sink) { - struct decoder *decoder = DOWNCAST(sink); - decoder_close(decoder); +sc_decoder_packet_sink_close(struct sc_packet_sink *sink) { + struct sc_decoder *decoder = DOWNCAST(sink); + sc_decoder_close(decoder); } static bool -decoder_packet_sink_push(struct sc_packet_sink *sink, const AVPacket *packet) { - struct decoder *decoder = DOWNCAST(sink); - return decoder_push(decoder, packet); +sc_decoder_packet_sink_push(struct sc_packet_sink *sink, + const AVPacket *packet) { + struct sc_decoder *decoder = DOWNCAST(sink); + return sc_decoder_push(decoder, packet); } void -decoder_init(struct decoder *decoder) { +sc_decoder_init(struct sc_decoder *decoder) { decoder->sink_count = 0; static const struct sc_packet_sink_ops ops = { - .open = decoder_packet_sink_open, - .close = decoder_packet_sink_close, - .push = decoder_packet_sink_push, + .open = sc_decoder_packet_sink_open, + .close = sc_decoder_packet_sink_close, + .push = sc_decoder_packet_sink_push, }; decoder->packet_sink.ops = &ops; } void -decoder_add_sink(struct decoder *decoder, struct sc_frame_sink *sink) { - assert(decoder->sink_count < DECODER_MAX_SINKS); +sc_decoder_add_sink(struct sc_decoder *decoder, struct sc_frame_sink *sink) { + assert(decoder->sink_count < SC_DECODER_MAX_SINKS); assert(sink); assert(sink->ops); decoder->sinks[decoder->sink_count++] = sink; diff --git a/app/src/decoder.h b/app/src/decoder.h index e2972cb1..16adc5ec 100644 --- a/app/src/decoder.h +++ b/app/src/decoder.h @@ -1,5 +1,5 @@ -#ifndef DECODER_H -#define DECODER_H +#ifndef SC_DECODER_H +#define SC_DECODER_H #include "common.h" @@ -9,12 +9,12 @@ #include #include -#define DECODER_MAX_SINKS 2 +#define SC_DECODER_MAX_SINKS 2 -struct decoder { +struct sc_decoder { struct sc_packet_sink packet_sink; // packet sink trait - struct sc_frame_sink *sinks[DECODER_MAX_SINKS]; + struct sc_frame_sink *sinks[SC_DECODER_MAX_SINKS]; unsigned sink_count; AVCodecContext *codec_ctx; @@ -22,9 +22,9 @@ struct decoder { }; void -decoder_init(struct decoder *decoder); +sc_decoder_init(struct sc_decoder *decoder); void -decoder_add_sink(struct decoder *decoder, struct sc_frame_sink *sink); +sc_decoder_add_sink(struct sc_decoder *decoder, struct sc_frame_sink *sink); #endif diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 5f67f7f8..9efae799 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -40,7 +40,7 @@ struct scrcpy { struct sc_server server; struct sc_screen screen; struct stream stream; - struct decoder decoder; + struct sc_decoder decoder; struct recorder recorder; #ifdef HAVE_V4L2 struct sc_v4l2_sink v4l2_sink; @@ -371,13 +371,13 @@ scrcpy(struct scrcpy_options *options) { file_pusher_initialized = true; } - struct decoder *dec = NULL; + struct sc_decoder *dec = NULL; bool needs_decoder = options->display; #ifdef HAVE_V4L2 needs_decoder |= !!options->v4l2_device; #endif if (needs_decoder) { - decoder_init(&s->decoder); + sc_decoder_init(&s->decoder); dec = &s->decoder; } @@ -608,7 +608,7 @@ aoa_hid_end: } screen_initialized = true; - decoder_add_sink(&s->decoder, &s->screen.frame_sink); + sc_decoder_add_sink(&s->decoder, &s->screen.frame_sink); } #ifdef HAVE_V4L2 @@ -618,7 +618,7 @@ aoa_hid_end: goto end; } - decoder_add_sink(&s->decoder, &s->v4l2_sink.frame_sink); + sc_decoder_add_sink(&s->decoder, &s->v4l2_sink.frame_sink); v4l2_sink_initialized = true; } From 4ee62abe1d7fc14fa4812ed40cf7e2d6577ba962 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 2 Feb 2022 19:37:28 +0100 Subject: [PATCH 1060/2244] Use sc_ prefix for recorder --- app/src/recorder.c | 94 +++++++++++++++++++++++----------------------- app/src/recorder.h | 23 ++++++------ app/src/scrcpy.c | 14 +++---- 3 files changed, 67 insertions(+), 64 deletions(-) diff --git a/app/src/recorder.c b/app/src/recorder.c index 74cfce07..2a82e172 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -9,7 +9,7 @@ #include "util/str.h" /** Downcast packet_sink to recorder */ -#define DOWNCAST(SINK) container_of(SINK, struct recorder, packet_sink) +#define DOWNCAST(SINK) container_of(SINK, struct sc_recorder, packet_sink) static const AVRational SCRCPY_TIME_BASE = {1, 1000000}; // timestamps in us @@ -30,9 +30,9 @@ find_muxer(const char *name) { return oformat; } -static struct record_packet * -record_packet_new(const AVPacket *packet) { - struct record_packet *rec = malloc(sizeof(*rec)); +static struct sc_record_packet * +sc_record_packet_new(const AVPacket *packet) { + struct sc_record_packet *rec = malloc(sizeof(*rec)); if (!rec) { LOG_OOM(); return NULL; @@ -54,22 +54,22 @@ record_packet_new(const AVPacket *packet) { } static void -record_packet_delete(struct record_packet *rec) { +sc_record_packet_delete(struct sc_record_packet *rec) { av_packet_free(&rec->packet); free(rec); } static void -recorder_queue_clear(struct recorder_queue *queue) { +sc_recorder_queue_clear(struct sc_recorder_queue *queue) { while (!sc_queue_is_empty(queue)) { - struct record_packet *rec; + struct sc_record_packet *rec; sc_queue_take(queue, next, &rec); - record_packet_delete(rec); + sc_record_packet_delete(rec); } } static const char * -recorder_get_format_name(enum sc_record_format format) { +sc_recorder_get_format_name(enum sc_record_format format) { switch (format) { case SC_RECORD_FORMAT_MP4: return "mp4"; case SC_RECORD_FORMAT_MKV: return "matroska"; @@ -78,7 +78,7 @@ recorder_get_format_name(enum sc_record_format format) { } static bool -recorder_write_header(struct recorder *recorder, const AVPacket *packet) { +sc_recorder_write_header(struct sc_recorder *recorder, const AVPacket *packet) { AVStream *ostream = recorder->ctx->streams[0]; uint8_t *extradata = av_malloc(packet->size * sizeof(uint8_t)); @@ -103,19 +103,19 @@ recorder_write_header(struct recorder *recorder, const AVPacket *packet) { } static void -recorder_rescale_packet(struct recorder *recorder, AVPacket *packet) { +sc_recorder_rescale_packet(struct sc_recorder *recorder, AVPacket *packet) { AVStream *ostream = recorder->ctx->streams[0]; av_packet_rescale_ts(packet, SCRCPY_TIME_BASE, ostream->time_base); } static bool -recorder_write(struct recorder *recorder, AVPacket *packet) { +sc_recorder_write(struct sc_recorder *recorder, AVPacket *packet) { if (!recorder->header_written) { if (packet->pts != AV_NOPTS_VALUE) { LOGE("The first packet is not a config packet"); return false; } - bool ok = recorder_write_header(recorder, packet); + bool ok = sc_recorder_write_header(recorder, packet); if (!ok) { return false; } @@ -128,13 +128,13 @@ recorder_write(struct recorder *recorder, AVPacket *packet) { return true; } - recorder_rescale_packet(recorder, packet); + sc_recorder_rescale_packet(recorder, packet); return av_write_frame(recorder->ctx, packet) >= 0; } static int run_recorder(void *data) { - struct recorder *recorder = data; + struct sc_recorder *recorder = data; for (;;) { sc_mutex_lock(&recorder->mutex); @@ -148,29 +148,29 @@ run_recorder(void *data) { if (recorder->stopped && sc_queue_is_empty(&recorder->queue)) { sc_mutex_unlock(&recorder->mutex); - struct record_packet *last = recorder->previous; + struct sc_record_packet *last = recorder->previous; if (last) { // assign an arbitrary duration to the last packet last->packet->duration = 100000; - bool ok = recorder_write(recorder, last->packet); + bool ok = sc_recorder_write(recorder, last->packet); if (!ok) { // failing to write the last frame is not very serious, no // future frame may depend on it, so the resulting file // will still be valid LOGW("Could not record last packet"); } - record_packet_delete(last); + sc_record_packet_delete(last); } break; } - struct record_packet *rec; + struct sc_record_packet *rec; sc_queue_take(&recorder->queue, next, &rec); sc_mutex_unlock(&recorder->mutex); // recorder->previous is only written from this thread, no need to lock - struct record_packet *previous = recorder->previous; + struct sc_record_packet *previous = recorder->previous; recorder->previous = rec; if (!previous) { @@ -186,15 +186,15 @@ run_recorder(void *data) { rec->packet->pts - previous->packet->pts; } - bool ok = recorder_write(recorder, previous->packet); - record_packet_delete(previous); + bool ok = sc_recorder_write(recorder, previous->packet); + sc_record_packet_delete(previous); if (!ok) { LOGE("Could not record packet"); sc_mutex_lock(&recorder->mutex); recorder->failed = true; // discard pending packets - recorder_queue_clear(&recorder->queue); + sc_recorder_queue_clear(&recorder->queue); sc_mutex_unlock(&recorder->mutex); break; } @@ -216,7 +216,7 @@ run_recorder(void *data) { if (recorder->failed) { LOGE("Recording failed to %s", recorder->filename); } else { - const char *format_name = recorder_get_format_name(recorder->format); + const char *format_name = sc_recorder_get_format_name(recorder->format); LOGI("Recording complete to %s file: %s", format_name, recorder->filename); } @@ -227,7 +227,7 @@ run_recorder(void *data) { } static bool -recorder_open(struct recorder *recorder, const AVCodec *input_codec) { +sc_recorder_open(struct sc_recorder *recorder, const AVCodec *input_codec) { bool ok = sc_mutex_init(&recorder->mutex); if (!ok) { return false; @@ -244,7 +244,7 @@ recorder_open(struct recorder *recorder, const AVCodec *input_codec) { recorder->header_written = false; recorder->previous = NULL; - const char *format_name = recorder_get_format_name(recorder->format); + const char *format_name = sc_recorder_get_format_name(recorder->format); assert(format_name); const AVOutputFormat *format = find_muxer(format_name); if (!format) { @@ -311,7 +311,7 @@ error_mutex_destroy: } static void -recorder_close(struct recorder *recorder) { +sc_recorder_close(struct sc_recorder *recorder) { sc_mutex_lock(&recorder->mutex); recorder->stopped = true; sc_cond_signal(&recorder->queue_cond); @@ -326,7 +326,7 @@ recorder_close(struct recorder *recorder) { } static bool -recorder_push(struct recorder *recorder, const AVPacket *packet) { +sc_recorder_push(struct sc_recorder *recorder, const AVPacket *packet) { sc_mutex_lock(&recorder->mutex); assert(!recorder->stopped); @@ -336,7 +336,7 @@ recorder_push(struct recorder *recorder, const AVPacket *packet) { return false; } - struct record_packet *rec = record_packet_new(packet); + struct sc_record_packet *rec = sc_record_packet_new(packet); if (!rec) { LOG_OOM(); sc_mutex_unlock(&recorder->mutex); @@ -351,28 +351,30 @@ recorder_push(struct recorder *recorder, const AVPacket *packet) { } static bool -recorder_packet_sink_open(struct sc_packet_sink *sink, const AVCodec *codec) { - struct recorder *recorder = DOWNCAST(sink); - return recorder_open(recorder, codec); +sc_recorder_packet_sink_open(struct sc_packet_sink *sink, + const AVCodec *codec) { + struct sc_recorder *recorder = DOWNCAST(sink); + return sc_recorder_open(recorder, codec); } static void -recorder_packet_sink_close(struct sc_packet_sink *sink) { - struct recorder *recorder = DOWNCAST(sink); - recorder_close(recorder); +sc_recorder_packet_sink_close(struct sc_packet_sink *sink) { + struct sc_recorder *recorder = DOWNCAST(sink); + sc_recorder_close(recorder); } static bool -recorder_packet_sink_push(struct sc_packet_sink *sink, const AVPacket *packet) { - struct recorder *recorder = DOWNCAST(sink); - return recorder_push(recorder, packet); +sc_recorder_packet_sink_push(struct sc_packet_sink *sink, + const AVPacket *packet) { + struct sc_recorder *recorder = DOWNCAST(sink); + return sc_recorder_push(recorder, packet); } bool -recorder_init(struct recorder *recorder, - const char *filename, - enum sc_record_format format, - struct sc_size declared_frame_size) { +sc_recorder_init(struct sc_recorder *recorder, + const char *filename, + enum sc_record_format format, + struct sc_size declared_frame_size) { recorder->filename = strdup(filename); if (!recorder->filename) { LOG_OOM(); @@ -383,9 +385,9 @@ recorder_init(struct recorder *recorder, recorder->declared_frame_size = declared_frame_size; static const struct sc_packet_sink_ops ops = { - .open = recorder_packet_sink_open, - .close = recorder_packet_sink_close, - .push = recorder_packet_sink_push, + .open = sc_recorder_packet_sink_open, + .close = sc_recorder_packet_sink_close, + .push = sc_recorder_packet_sink_push, }; recorder->packet_sink.ops = &ops; @@ -394,6 +396,6 @@ recorder_init(struct recorder *recorder, } void -recorder_destroy(struct recorder *recorder) { +sc_recorder_destroy(struct sc_recorder *recorder) { free(recorder->filename); } diff --git a/app/src/recorder.h b/app/src/recorder.h index 27ea5526..373278e6 100644 --- a/app/src/recorder.h +++ b/app/src/recorder.h @@ -1,5 +1,5 @@ -#ifndef RECORDER_H -#define RECORDER_H +#ifndef SC_RECORDER_H +#define SC_RECORDER_H #include "common.h" @@ -12,14 +12,14 @@ #include "util/queue.h" #include "util/thread.h" -struct record_packet { +struct sc_record_packet { AVPacket *packet; - struct record_packet *next; + struct sc_record_packet *next; }; -struct recorder_queue SC_QUEUE(struct record_packet); +struct sc_recorder_queue SC_QUEUE(struct sc_record_packet); -struct recorder { +struct sc_recorder { struct sc_packet_sink packet_sink; // packet sink trait char *filename; @@ -33,20 +33,21 @@ struct recorder { sc_cond queue_cond; bool stopped; // set on recorder_close() bool failed; // set on packet write failure - struct recorder_queue queue; + struct sc_recorder_queue queue; // we can write a packet only once we received the next one so that we can // set its duration (next_pts - current_pts) // "previous" is only accessed from the recorder thread, so it does not // need to be protected by the mutex - struct record_packet *previous; + struct sc_record_packet *previous; }; bool -recorder_init(struct recorder *recorder, const char *filename, - enum sc_record_format format, struct sc_size declared_frame_size); +sc_recorder_init(struct sc_recorder *recorder, const char *filename, + enum sc_record_format format, + struct sc_size declared_frame_size); void -recorder_destroy(struct recorder *recorder); +sc_recorder_destroy(struct sc_recorder *recorder); #endif diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 9efae799..91ee0a2a 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -41,7 +41,7 @@ struct scrcpy { struct sc_screen screen; struct stream stream; struct sc_decoder decoder; - struct recorder recorder; + struct sc_recorder recorder; #ifdef HAVE_V4L2 struct sc_v4l2_sink v4l2_sink; #endif @@ -381,12 +381,12 @@ scrcpy(struct scrcpy_options *options) { dec = &s->decoder; } - struct recorder *rec = NULL; + struct sc_recorder *rec = NULL; if (options->record_filename) { - if (!recorder_init(&s->recorder, - options->record_filename, - options->record_format, - info->frame_size)) { + if (!sc_recorder_init(&s->recorder, + options->record_filename, + options->record_format, + info->frame_size)) { goto end; } rec = &s->recorder; @@ -708,7 +708,7 @@ end: } if (recorder_initialized) { - recorder_destroy(&s->recorder); + sc_recorder_destroy(&s->recorder); } if (file_pusher_initialized) { From 7dec225cebbf208cf1d607bd45fb49e3a9fa1167 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 2 Feb 2022 20:51:07 +0100 Subject: [PATCH 1061/2244] Rename stream to sc_demuxer For consistency with recorder and decoder, name the component which demuxes a "demuxer". And add the missing sc_ prefix. --- app/meson.build | 2 +- app/src/{stream.c => demuxer.c} | 142 ++++++++++++++++---------------- app/src/demuxer.h | 51 ++++++++++++ app/src/scrcpy.c | 38 ++++----- 4 files changed, 142 insertions(+), 91 deletions(-) rename app/src/{stream.c => demuxer.c} (55%) create mode 100644 app/src/demuxer.h diff --git a/app/meson.build b/app/meson.build index 7f675035..1d31a442 100644 --- a/app/meson.build +++ b/app/meson.build @@ -9,6 +9,7 @@ src = [ 'src/control_msg.c', 'src/controller.c', 'src/decoder.c', + 'src/demuxer.c', 'src/device_msg.c', 'src/icon.c', 'src/file_pusher.c', @@ -24,7 +25,6 @@ src = [ 'src/scrcpy.c', 'src/screen.c', 'src/server.c', - 'src/stream.c', 'src/video_buffer.c', 'src/util/acksync.c', 'src/util/file.c', diff --git a/app/src/stream.c b/app/src/demuxer.c similarity index 55% rename from app/src/stream.c rename to app/src/demuxer.c index c873c4ad..3dd62491 100644 --- a/app/src/stream.c +++ b/app/src/demuxer.c @@ -1,4 +1,4 @@ -#include "stream.h" +#include "demuxer.h" #include #include @@ -16,7 +16,7 @@ #define NO_PTS UINT64_C(-1) static bool -stream_recv_packet(struct stream *stream, AVPacket *packet) { +sc_demuxer_recv_packet(struct sc_demuxer *demuxer, AVPacket *packet) { // The video stream contains raw packets, without time information. When we // record, we retrieve the timestamps separately, from a "meta" header // added by the server before each raw packet. @@ -30,7 +30,7 @@ stream_recv_packet(struct stream *stream, AVPacket *packet) { // It is followed by bytes containing the packet/frame. uint8_t header[HEADER_SIZE]; - ssize_t r = net_recv_all(stream->socket, header, HEADER_SIZE); + ssize_t r = net_recv_all(demuxer->socket, header, HEADER_SIZE); if (r < HEADER_SIZE) { return false; } @@ -45,7 +45,7 @@ stream_recv_packet(struct stream *stream, AVPacket *packet) { return false; } - r = net_recv_all(stream->socket, packet->data, len); + r = net_recv_all(demuxer->socket, packet->data, len); if (r < 0 || ((uint32_t) r) < len) { av_packet_unref(packet); return false; @@ -57,9 +57,9 @@ stream_recv_packet(struct stream *stream, AVPacket *packet) { } static bool -push_packet_to_sinks(struct stream *stream, const AVPacket *packet) { - for (unsigned i = 0; i < stream->sink_count; ++i) { - struct sc_packet_sink *sink = stream->sinks[i]; +push_packet_to_sinks(struct sc_demuxer *demuxer, const AVPacket *packet) { + for (unsigned i = 0; i < demuxer->sink_count; ++i) { + struct sc_packet_sink *sink = demuxer->sinks[i]; if (!sink->ops->push(sink, packet)) { LOGE("Could not send config packet to sink %d", i); return false; @@ -70,12 +70,12 @@ push_packet_to_sinks(struct stream *stream, const AVPacket *packet) { } static bool -stream_parse(struct stream *stream, AVPacket *packet) { +sc_demuxer_parse(struct sc_demuxer *demuxer, AVPacket *packet) { uint8_t *in_data = packet->data; int in_len = packet->size; uint8_t *out_data = NULL; int out_len = 0; - int r = av_parser_parse2(stream->parser, stream->codec_ctx, + int r = av_parser_parse2(demuxer->parser, demuxer->codec_ctx, &out_data, &out_len, in_data, in_len, AV_NOPTS_VALUE, AV_NOPTS_VALUE, -1); @@ -84,13 +84,13 @@ stream_parse(struct stream *stream, AVPacket *packet) { (void) r; assert(out_len == in_len); - if (stream->parser->key_frame == 1) { + if (demuxer->parser->key_frame == 1) { packet->flags |= AV_PKT_FLAG_KEY; } packet->dts = packet->pts; - bool ok = push_packet_to_sinks(stream, packet); + bool ok = push_packet_to_sinks(demuxer, packet); if (!ok) { LOGE("Could not process packet"); return false; @@ -100,57 +100,57 @@ stream_parse(struct stream *stream, AVPacket *packet) { } static bool -stream_push_packet(struct stream *stream, AVPacket *packet) { +sc_demuxer_push_packet(struct sc_demuxer *demuxer, AVPacket *packet) { bool is_config = packet->pts == AV_NOPTS_VALUE; // A config packet must not be decoded immediately (it contains no // frame); instead, it must be concatenated with the future data packet. - if (stream->pending || is_config) { + if (demuxer->pending || is_config) { size_t offset; - if (stream->pending) { - offset = stream->pending->size; - if (av_grow_packet(stream->pending, packet->size)) { + if (demuxer->pending) { + offset = demuxer->pending->size; + if (av_grow_packet(demuxer->pending, packet->size)) { LOG_OOM(); return false; } } else { offset = 0; - stream->pending = av_packet_alloc(); - if (!stream->pending) { + demuxer->pending = av_packet_alloc(); + if (!demuxer->pending) { LOG_OOM(); return false; } - if (av_new_packet(stream->pending, packet->size)) { + if (av_new_packet(demuxer->pending, packet->size)) { LOG_OOM(); - av_packet_free(&stream->pending); + av_packet_free(&demuxer->pending); return false; } } - memcpy(stream->pending->data + offset, packet->data, packet->size); + memcpy(demuxer->pending->data + offset, packet->data, packet->size); if (!is_config) { // prepare the concat packet to send to the decoder - stream->pending->pts = packet->pts; - stream->pending->dts = packet->dts; - stream->pending->flags = packet->flags; - packet = stream->pending; + demuxer->pending->pts = packet->pts; + demuxer->pending->dts = packet->dts; + demuxer->pending->flags = packet->flags; + packet = demuxer->pending; } } if (is_config) { // config packet - bool ok = push_packet_to_sinks(stream, packet); + bool ok = push_packet_to_sinks(demuxer, packet); if (!ok) { return false; } } else { // data packet - bool ok = stream_parse(stream, packet); + bool ok = sc_demuxer_parse(demuxer, packet); - if (stream->pending) { + if (demuxer->pending) { // the pending packet must be discarded (consumed or error) - av_packet_free(&stream->pending); + av_packet_free(&demuxer->pending); } if (!ok) { @@ -161,25 +161,25 @@ stream_push_packet(struct stream *stream, AVPacket *packet) { } static void -stream_close_first_sinks(struct stream *stream, unsigned count) { +sc_demuxer_close_first_sinks(struct sc_demuxer *demuxer, unsigned count) { while (count) { - struct sc_packet_sink *sink = stream->sinks[--count]; + struct sc_packet_sink *sink = demuxer->sinks[--count]; sink->ops->close(sink); } } static inline void -stream_close_sinks(struct stream *stream) { - stream_close_first_sinks(stream, stream->sink_count); +sc_demuxer_close_sinks(struct sc_demuxer *demuxer) { + sc_demuxer_close_first_sinks(demuxer, demuxer->sink_count); } static bool -stream_open_sinks(struct stream *stream, const AVCodec *codec) { - for (unsigned i = 0; i < stream->sink_count; ++i) { - struct sc_packet_sink *sink = stream->sinks[i]; +sc_demuxer_open_sinks(struct sc_demuxer *demuxer, const AVCodec *codec) { + for (unsigned i = 0; i < demuxer->sink_count; ++i) { + struct sc_packet_sink *sink = demuxer->sinks[i]; if (!sink->ops->open(sink, codec)) { LOGE("Could not open packet sink %d", i); - stream_close_first_sinks(stream, i); + sc_demuxer_close_first_sinks(demuxer, i); return false; } } @@ -188,8 +188,8 @@ stream_open_sinks(struct stream *stream, const AVCodec *codec) { } static int -run_stream(void *data) { - struct stream *stream = data; +run_demuxer(void *data) { + struct sc_demuxer *demuxer = data; const AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264); if (!codec) { @@ -197,26 +197,26 @@ run_stream(void *data) { goto end; } - stream->codec_ctx = avcodec_alloc_context3(codec); - if (!stream->codec_ctx) { + demuxer->codec_ctx = avcodec_alloc_context3(codec); + if (!demuxer->codec_ctx) { LOG_OOM(); goto end; } - if (!stream_open_sinks(stream, codec)) { - LOGE("Could not open stream sinks"); + if (!sc_demuxer_open_sinks(demuxer, codec)) { + LOGE("Could not open demuxer sinks"); goto finally_free_codec_ctx; } - stream->parser = av_parser_init(AV_CODEC_ID_H264); - if (!stream->parser) { + demuxer->parser = av_parser_init(AV_CODEC_ID_H264); + if (!demuxer->parser) { LOGE("Could not initialize parser"); goto finally_close_sinks; } // We must only pass complete frames to av_parser_parse2()! // It's more complicated, but this allows to reduce the latency by 1 frame! - stream->parser->flags |= PARSER_FLAG_COMPLETE_FRAMES; + demuxer->parser->flags |= PARSER_FLAG_COMPLETE_FRAMES; AVPacket *packet = av_packet_alloc(); if (!packet) { @@ -225,13 +225,13 @@ run_stream(void *data) { } for (;;) { - bool ok = stream_recv_packet(stream, packet); + bool ok = sc_demuxer_recv_packet(demuxer, packet); if (!ok) { // end of stream break; } - ok = stream_push_packet(stream, packet); + ok = sc_demuxer_push_packet(demuxer, packet); av_packet_unref(packet); if (!ok) { // cannot process packet (error already logged) @@ -241,58 +241,58 @@ run_stream(void *data) { LOGD("End of frames"); - if (stream->pending) { - av_packet_free(&stream->pending); + if (demuxer->pending) { + av_packet_free(&demuxer->pending); } av_packet_free(&packet); finally_close_parser: - av_parser_close(stream->parser); + av_parser_close(demuxer->parser); finally_close_sinks: - stream_close_sinks(stream); + sc_demuxer_close_sinks(demuxer); finally_free_codec_ctx: - avcodec_free_context(&stream->codec_ctx); + avcodec_free_context(&demuxer->codec_ctx); end: - stream->cbs->on_eos(stream, stream->cbs_userdata); + demuxer->cbs->on_eos(demuxer, demuxer->cbs_userdata); return 0; } void -stream_init(struct stream *stream, sc_socket socket, - const struct stream_callbacks *cbs, void *cbs_userdata) { - stream->socket = socket; - stream->pending = NULL; - stream->sink_count = 0; +sc_demuxer_init(struct sc_demuxer *demuxer, sc_socket socket, + const struct sc_demuxer_callbacks *cbs, void *cbs_userdata) { + demuxer->socket = socket; + demuxer->pending = NULL; + demuxer->sink_count = 0; assert(cbs && cbs->on_eos); - stream->cbs = cbs; - stream->cbs_userdata = cbs_userdata; + demuxer->cbs = cbs; + demuxer->cbs_userdata = cbs_userdata; } void -stream_add_sink(struct stream *stream, struct sc_packet_sink *sink) { - assert(stream->sink_count < STREAM_MAX_SINKS); +sc_demuxer_add_sink(struct sc_demuxer *demuxer, struct sc_packet_sink *sink) { + assert(demuxer->sink_count < SC_DEMUXER_MAX_SINKS); assert(sink); assert(sink->ops); - stream->sinks[stream->sink_count++] = sink; + demuxer->sinks[demuxer->sink_count++] = sink; } bool -stream_start(struct stream *stream) { - LOGD("Starting stream thread"); +sc_demuxer_start(struct sc_demuxer *demuxer) { + LOGD("Starting demuxer thread"); - bool ok = - sc_thread_create(&stream->thread, run_stream, "scrcpy-stream", stream); + bool ok = sc_thread_create(&demuxer->thread, run_demuxer, "scrcpy-demuxer", + demuxer); if (!ok) { - LOGC("Could not start stream thread"); + LOGC("Could not start demuxer thread"); return false; } return true; } void -stream_join(struct stream *stream) { - sc_thread_join(&stream->thread, NULL); +sc_demuxer_join(struct sc_demuxer *demuxer) { + sc_thread_join(&demuxer->thread, NULL); } diff --git a/app/src/demuxer.h b/app/src/demuxer.h new file mode 100644 index 00000000..11e20ad6 --- /dev/null +++ b/app/src/demuxer.h @@ -0,0 +1,51 @@ +#ifndef SC_DEMUXER_H +#define SC_DEMUXER_H + +#include "common.h" + +#include +#include +#include +#include + +#include "trait/packet_sink.h" +#include "util/net.h" +#include "util/thread.h" + +#define SC_DEMUXER_MAX_SINKS 2 + +struct sc_demuxer { + sc_socket socket; + sc_thread thread; + + struct sc_packet_sink *sinks[SC_DEMUXER_MAX_SINKS]; + unsigned sink_count; + + AVCodecContext *codec_ctx; + AVCodecParserContext *parser; + // successive packets may need to be concatenated, until a non-config + // packet is available + AVPacket *pending; + + const struct sc_demuxer_callbacks *cbs; + void *cbs_userdata; +}; + +struct sc_demuxer_callbacks { + void (*on_eos)(struct sc_demuxer *demuxer, void *userdata); +}; + +void +sc_demuxer_init(struct sc_demuxer *demuxer, sc_socket socket, + const struct sc_demuxer_callbacks *cbs, void *cbs_userdata); + +void +sc_demuxer_add_sink(struct sc_demuxer *demuxer, struct sc_packet_sink *sink); + +bool +sc_demuxer_start(struct sc_demuxer *demuxer); + +void +sc_demuxer_join(struct sc_demuxer *demuxer); + +#endif diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 91ee0a2a..270fe2b3 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -15,6 +15,7 @@ #include "controller.h" #include "decoder.h" +#include "demuxer.h" #include "events.h" #include "file_pusher.h" #include "keyboard_inject.h" @@ -22,7 +23,6 @@ #include "recorder.h" #include "screen.h" #include "server.h" -#include "stream.h" #ifdef HAVE_USB # include "usb/aoa_hid.h" # include "usb/hid_keyboard.h" @@ -39,7 +39,7 @@ struct scrcpy { struct sc_server server; struct sc_screen screen; - struct stream stream; + struct sc_demuxer demuxer; struct sc_decoder decoder; struct sc_recorder recorder; #ifdef HAVE_V4L2 @@ -231,8 +231,8 @@ av_log_callback(void *avcl, int level, const char *fmt, va_list vl) { } static void -stream_on_eos(struct stream *stream, void *userdata) { - (void) stream; +sc_demuxer_on_eos(struct sc_demuxer *demuxer, void *userdata) { + (void) demuxer; (void) userdata; PUSH_EVENT(EVENT_STREAM_STOPPED); @@ -285,7 +285,7 @@ scrcpy(struct scrcpy_options *options) { #ifdef HAVE_V4L2 bool v4l2_sink_initialized = false; #endif - bool stream_started = false; + bool demuxer_started = false; #ifdef HAVE_USB bool aoa_hid_initialized = false; bool hid_keyboard_initialized = false; @@ -395,17 +395,17 @@ scrcpy(struct scrcpy_options *options) { av_log_set_callback(av_log_callback); - static const struct stream_callbacks stream_cbs = { - .on_eos = stream_on_eos, + static const struct sc_demuxer_callbacks demuxer_cbs = { + .on_eos = sc_demuxer_on_eos, }; - stream_init(&s->stream, s->server.video_socket, &stream_cbs, NULL); + sc_demuxer_init(&s->demuxer, s->server.video_socket, &demuxer_cbs, NULL); if (dec) { - stream_add_sink(&s->stream, &dec->packet_sink); + sc_demuxer_add_sink(&s->demuxer, &dec->packet_sink); } if (rec) { - stream_add_sink(&s->stream, &rec->packet_sink); + sc_demuxer_add_sink(&s->demuxer, &rec->packet_sink); } struct sc_controller *controller = NULL; @@ -625,21 +625,21 @@ aoa_hid_end: #endif // now we consumed the header values, the socket receives the video stream - // start the stream - if (!stream_start(&s->stream)) { + // start the demuxer + if (!sc_demuxer_start(&s->demuxer)) { goto end; } - stream_started = true; + demuxer_started = true; ret = event_loop(s); LOGD("quit..."); // Close the window immediately on closing, because screen_destroy() may - // only be called once the stream thread is joined (it may take time) + // only be called once the demuxer thread is joined (it may take time) sc_screen_hide_window(&s->screen); end: - // The stream is not stopped explicitly, because it will stop by itself on + // The demuxer is not stopped explicitly, because it will stop by itself on // end-of-stream #ifdef HAVE_USB if (aoa_hid_initialized) { @@ -671,10 +671,10 @@ end: sc_server_stop(&s->server); } - // now that the sockets are shutdown, the stream and controller are + // now that the sockets are shutdown, the demuxer and controller are // interrupted, we can join them - if (stream_started) { - stream_join(&s->stream); + if (demuxer_started) { + sc_demuxer_join(&s->demuxer); } #ifdef HAVE_V4L2 @@ -693,7 +693,7 @@ end: } #endif - // Destroy the screen only after the stream is guaranteed to be finished, + // Destroy the screen only after the demuxer is guaranteed to be finished, // because otherwise the screen could receive new frames after destruction if (screen_initialized) { sc_screen_join(&s->screen); From c460243ce2c21cfd89535ec7cd301684e8f3ee18 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 2 Feb 2022 21:03:24 +0100 Subject: [PATCH 1062/2244] Simplify demuxer Call the same push_packet_to_sinks() in all cases, and make sc_demuxer_parse() return void. --- app/src/demuxer.c | 42 ++++++++++++++++-------------------------- 1 file changed, 16 insertions(+), 26 deletions(-) diff --git a/app/src/demuxer.c b/app/src/demuxer.c index 3dd62491..c683dfe0 100644 --- a/app/src/demuxer.c +++ b/app/src/demuxer.c @@ -69,7 +69,7 @@ push_packet_to_sinks(struct sc_demuxer *demuxer, const AVPacket *packet) { return true; } -static bool +static void sc_demuxer_parse(struct sc_demuxer *demuxer, AVPacket *packet) { uint8_t *in_data = packet->data; int in_len = packet->size; @@ -89,14 +89,6 @@ sc_demuxer_parse(struct sc_demuxer *demuxer, AVPacket *packet) { } packet->dts = packet->pts; - - bool ok = push_packet_to_sinks(demuxer, packet); - if (!ok) { - LOGE("Could not process packet"); - return false; - } - - return true; } static bool @@ -138,25 +130,23 @@ sc_demuxer_push_packet(struct sc_demuxer *demuxer, AVPacket *packet) { } } - if (is_config) { - // config packet - bool ok = push_packet_to_sinks(demuxer, packet); - if (!ok) { - return false; - } - } else { + if (!is_config) { // data packet - bool ok = sc_demuxer_parse(demuxer, packet); - - if (demuxer->pending) { - // the pending packet must be discarded (consumed or error) - av_packet_free(&demuxer->pending); - } - - if (!ok) { - return false; - } + sc_demuxer_parse(demuxer, packet); } + + bool ok = push_packet_to_sinks(demuxer, packet); + + if (!is_config && demuxer->pending) { + // the pending packet must be discarded (consumed or error) + av_packet_free(&demuxer->pending); + } + + if (!ok) { + LOGE("Could not process packet"); + return false; + } + return true; } From 7810ca61b0022500a9b19b23fb1b163c032a90ec Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 3 Feb 2022 22:46:24 +0100 Subject: [PATCH 1063/2244] Move ADB code to adb/ --- app/meson.build | 8 ++++---- app/src/{ => adb}/adb.c | 0 app/src/{ => adb}/adb.h | 0 app/src/{ => adb}/adb_parser.c | 0 app/src/{ => adb}/adb_parser.h | 0 app/src/{ => adb}/adb_tunnel.c | 0 app/src/{ => adb}/adb_tunnel.h | 0 app/src/file_pusher.c | 2 +- app/src/file_pusher.h | 1 - app/src/server.c | 2 +- app/src/server.h | 3 +-- app/tests/test_adb_parser.c | 2 +- 12 files changed, 8 insertions(+), 10 deletions(-) rename app/src/{ => adb}/adb.c (100%) rename app/src/{ => adb}/adb.h (100%) rename app/src/{ => adb}/adb_parser.c (100%) rename app/src/{ => adb}/adb_parser.h (100%) rename app/src/{ => adb}/adb_tunnel.c (100%) rename app/src/{ => adb}/adb_tunnel.h (100%) diff --git a/app/meson.build b/app/meson.build index 1d31a442..b2c0bc55 100644 --- a/app/meson.build +++ b/app/meson.build @@ -1,8 +1,8 @@ src = [ 'src/main.c', - 'src/adb.c', - 'src/adb_parser.c', - 'src/adb_tunnel.c', + 'src/adb/adb.c', + 'src/adb/adb_parser.c', + 'src/adb/adb_tunnel.c', 'src/cli.c', 'src/clock.c', 'src/compat.c', @@ -221,7 +221,7 @@ if get_option('buildtype') == 'debug' tests = [ ['test_adb_parser', [ 'tests/test_adb_parser.c', - 'src/adb_parser.c', + 'src/adb/adb_parser.c', 'src/util/str.c', 'src/util/strbuf.c', ]], diff --git a/app/src/adb.c b/app/src/adb/adb.c similarity index 100% rename from app/src/adb.c rename to app/src/adb/adb.c diff --git a/app/src/adb.h b/app/src/adb/adb.h similarity index 100% rename from app/src/adb.h rename to app/src/adb/adb.h diff --git a/app/src/adb_parser.c b/app/src/adb/adb_parser.c similarity index 100% rename from app/src/adb_parser.c rename to app/src/adb/adb_parser.c diff --git a/app/src/adb_parser.h b/app/src/adb/adb_parser.h similarity index 100% rename from app/src/adb_parser.h rename to app/src/adb/adb_parser.h diff --git a/app/src/adb_tunnel.c b/app/src/adb/adb_tunnel.c similarity index 100% rename from app/src/adb_tunnel.c rename to app/src/adb/adb_tunnel.c diff --git a/app/src/adb_tunnel.h b/app/src/adb/adb_tunnel.h similarity index 100% rename from app/src/adb_tunnel.h rename to app/src/adb/adb_tunnel.h diff --git a/app/src/file_pusher.c b/app/src/file_pusher.c index 738e3616..78ea48c2 100644 --- a/app/src/file_pusher.c +++ b/app/src/file_pusher.c @@ -3,7 +3,7 @@ #include #include -#include "adb.h" +#include "adb/adb.h" #include "util/log.h" #include "util/process_intr.h" diff --git a/app/src/file_pusher.h b/app/src/file_pusher.h index 56b107a0..0d934d6c 100644 --- a/app/src/file_pusher.h +++ b/app/src/file_pusher.h @@ -5,7 +5,6 @@ #include -#include "adb.h" #include "util/cbuf.h" #include "util/thread.h" #include "util/intr.h" diff --git a/app/src/server.c b/app/src/server.c index e5acef95..b7a28949 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -7,7 +7,7 @@ #include #include -#include "adb.h" +#include "adb/adb.h" #include "util/file.h" #include "util/log.h" #include "util/net_intr.h" diff --git a/app/src/server.h b/app/src/server.h index 89cdc2f4..1dff19a7 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -7,8 +7,7 @@ #include #include -#include "adb.h" -#include "adb_tunnel.h" +#include "adb/adb_tunnel.h" #include "coords.h" #include "options.h" #include "util/intr.h" diff --git a/app/tests/test_adb_parser.c b/app/tests/test_adb_parser.c index cb3abb0e..51e7d6d2 100644 --- a/app/tests/test_adb_parser.c +++ b/app/tests/test_adb_parser.c @@ -2,7 +2,7 @@ #include -#include "adb_parser.h" +#include "adb/adb_parser.h" static void test_get_ip_single_line() { char ip_route[] = "192.168.1.0/24 dev wlan0 proto kernel scope link src " From 9e3902f30c9132d275b5bd1a58cb174b7c96b3e9 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 3 Feb 2022 23:04:01 +0100 Subject: [PATCH 1064/2244] Use sc_ prefix for adb --- app/src/adb/adb.c | 83 +++++++++++++++++++++------------------- app/src/adb/adb.h | 46 +++++++++++----------- app/src/adb/adb_tunnel.c | 17 ++++---- app/src/file_pusher.c | 4 +- app/src/server.c | 16 ++++---- 5 files changed, 85 insertions(+), 81 deletions(-) diff --git a/app/src/adb/adb.c b/app/src/adb/adb.c index 5bc4e4cc..215fc6be 100644 --- a/app/src/adb/adb.c +++ b/app/src/adb/adb.c @@ -158,7 +158,8 @@ process_check_success_intr(struct sc_intr *intr, sc_pid pid, const char *name, } static const char ** -adb_create_argv(const char *serial, const char *const adb_cmd[], size_t len) { +sc_adb_create_argv(const char *serial, const char *const adb_cmd[], + size_t len) { const char **argv = malloc((len + 4) * sizeof(*argv)); if (!argv) { LOG_OOM(); @@ -181,9 +182,9 @@ adb_create_argv(const char *serial, const char *const adb_cmd[], size_t len) { } static sc_pid -adb_execute_p(const char *serial, const char *const adb_cmd[], size_t len, - unsigned flags, sc_pipe *pout) { - const char **argv = adb_create_argv(serial, adb_cmd, len); +sc_adb_execute_p(const char *serial, const char *const adb_cmd[], size_t len, + unsigned flags, sc_pipe *pout) { + const char **argv = sc_adb_create_argv(serial, adb_cmd, len); if (!argv) { return SC_PROCESS_NONE; } @@ -211,63 +212,63 @@ adb_execute_p(const char *serial, const char *const adb_cmd[], size_t len, } sc_pid -adb_execute(const char *serial, const char *const adb_cmd[], size_t len, - unsigned flags) { - return adb_execute_p(serial, adb_cmd, len, flags, NULL); +sc_adb_execute(const char *serial, const char *const adb_cmd[], size_t len, + unsigned flags) { + return sc_adb_execute_p(serial, adb_cmd, len, flags, NULL); } bool -adb_forward(struct sc_intr *intr, const char *serial, uint16_t local_port, - const char *device_socket_name, unsigned flags) { +sc_adb_forward(struct sc_intr *intr, const char *serial, uint16_t local_port, + const char *device_socket_name, unsigned flags) { char local[4 + 5 + 1]; // tcp:PORT char remote[108 + 14 + 1]; // localabstract:NAME sprintf(local, "tcp:%" PRIu16, local_port); snprintf(remote, sizeof(remote), "localabstract:%s", device_socket_name); const char *const adb_cmd[] = {"forward", local, remote}; - sc_pid pid = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd), flags); + sc_pid pid = sc_adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd), flags); return process_check_success_intr(intr, pid, "adb forward", flags); } bool -adb_forward_remove(struct sc_intr *intr, const char *serial, - uint16_t local_port, unsigned flags) { +sc_adb_forward_remove(struct sc_intr *intr, const char *serial, + uint16_t local_port, unsigned flags) { char local[4 + 5 + 1]; // tcp:PORT sprintf(local, "tcp:%" PRIu16, local_port); const char *const adb_cmd[] = {"forward", "--remove", local}; - sc_pid pid = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd), flags); + sc_pid pid = sc_adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd), flags); return process_check_success_intr(intr, pid, "adb forward --remove", flags); } bool -adb_reverse(struct sc_intr *intr, const char *serial, - const char *device_socket_name, uint16_t local_port, - unsigned flags) { +sc_adb_reverse(struct sc_intr *intr, const char *serial, + const char *device_socket_name, uint16_t local_port, + unsigned flags) { char local[4 + 5 + 1]; // tcp:PORT char remote[108 + 14 + 1]; // localabstract:NAME sprintf(local, "tcp:%" PRIu16, local_port); snprintf(remote, sizeof(remote), "localabstract:%s", device_socket_name); const char *const adb_cmd[] = {"reverse", remote, local}; - sc_pid pid = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd), flags); + sc_pid pid = sc_adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd), flags); return process_check_success_intr(intr, pid, "adb reverse", flags); } bool -adb_reverse_remove(struct sc_intr *intr, const char *serial, - const char *device_socket_name, unsigned flags) { +sc_adb_reverse_remove(struct sc_intr *intr, const char *serial, + const char *device_socket_name, unsigned flags) { char remote[108 + 14 + 1]; // localabstract:NAME snprintf(remote, sizeof(remote), "localabstract:%s", device_socket_name); const char *const adb_cmd[] = {"reverse", "--remove", remote}; - sc_pid pid = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd), flags); + sc_pid pid = sc_adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd), flags); return process_check_success_intr(intr, pid, "adb reverse --remove", flags); } bool -adb_push(struct sc_intr *intr, const char *serial, const char *local, - const char *remote, unsigned flags) { +sc_adb_push(struct sc_intr *intr, const char *serial, const char *local, + const char *remote, unsigned flags) { #ifdef __WINDOWS__ // Windows will parse the string, so the paths must be quoted // (see sys/win/command.c) @@ -283,7 +284,7 @@ adb_push(struct sc_intr *intr, const char *serial, const char *local, #endif const char *const adb_cmd[] = {"push", local, remote}; - sc_pid pid = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd), flags); + sc_pid pid = sc_adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd), flags); #ifdef __WINDOWS__ free((void *) remote); @@ -294,8 +295,8 @@ adb_push(struct sc_intr *intr, const char *serial, const char *local, } bool -adb_install(struct sc_intr *intr, const char *serial, const char *local, - unsigned flags) { +sc_adb_install(struct sc_intr *intr, const char *serial, const char *local, + unsigned flags) { #ifdef __WINDOWS__ // Windows will parse the string, so the local name must be quoted // (see sys/win/command.c) @@ -306,7 +307,7 @@ adb_install(struct sc_intr *intr, const char *serial, const char *local, #endif const char *const adb_cmd[] = {"install", "-r", local}; - sc_pid pid = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd), flags); + sc_pid pid = sc_adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd), flags); #ifdef __WINDOWS__ free((void *) local); @@ -316,22 +317,23 @@ adb_install(struct sc_intr *intr, const char *serial, const char *local, } bool -adb_tcpip(struct sc_intr *intr, const char *serial, uint16_t port, - unsigned flags) { +sc_adb_tcpip(struct sc_intr *intr, const char *serial, uint16_t port, + unsigned flags) { char port_string[5 + 1]; sprintf(port_string, "%" PRIu16, port); const char *const adb_cmd[] = {"tcpip", port_string}; - sc_pid pid = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd), flags); + sc_pid pid = sc_adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd), flags); return process_check_success_intr(intr, pid, "adb tcpip", flags); } bool -adb_connect(struct sc_intr *intr, const char *ip_port, unsigned flags) { +sc_adb_connect(struct sc_intr *intr, const char *ip_port, unsigned flags) { const char *const adb_cmd[] = {"connect", ip_port}; sc_pipe pout; - sc_pid pid = adb_execute_p(NULL, adb_cmd, ARRAY_LEN(adb_cmd), flags, &pout); + sc_pid pid = + sc_adb_execute_p(NULL, adb_cmd, ARRAY_LEN(adb_cmd), flags, &pout); if (pid == SC_PROCESS_NONE) { LOGE("Could not execute \"adb connect\""); return false; @@ -364,23 +366,23 @@ adb_connect(struct sc_intr *intr, const char *ip_port, unsigned flags) { } bool -adb_disconnect(struct sc_intr *intr, const char *ip_port, unsigned flags) { +sc_adb_disconnect(struct sc_intr *intr, const char *ip_port, unsigned flags) { const char *const adb_cmd[] = {"disconnect", ip_port}; size_t len = ip_port ? ARRAY_LEN(adb_cmd) : ARRAY_LEN(adb_cmd) - 1; - sc_pid pid = adb_execute(NULL, adb_cmd, len, flags); + sc_pid pid = sc_adb_execute(NULL, adb_cmd, len, flags); return process_check_success_intr(intr, pid, "adb disconnect", flags); } char * -adb_getprop(struct sc_intr *intr, const char *serial, const char *prop, - unsigned flags) { +sc_adb_getprop(struct sc_intr *intr, const char *serial, const char *prop, + unsigned flags) { const char *const adb_cmd[] = {"shell", "getprop", prop}; sc_pipe pout; sc_pid pid = - adb_execute_p(serial, adb_cmd, ARRAY_LEN(adb_cmd), flags, &pout); + sc_adb_execute_p(serial, adb_cmd, ARRAY_LEN(adb_cmd), flags, &pout); if (pid == SC_PROCESS_NONE) { LOGE("Could not execute \"adb getprop\""); return NULL; @@ -405,11 +407,12 @@ adb_getprop(struct sc_intr *intr, const char *serial, const char *prop, } char * -adb_get_serialno(struct sc_intr *intr, unsigned flags) { +sc_adb_get_serialno(struct sc_intr *intr, unsigned flags) { const char *const adb_cmd[] = {"get-serialno"}; sc_pipe pout; - sc_pid pid = adb_execute_p(NULL, adb_cmd, ARRAY_LEN(adb_cmd), flags, &pout); + sc_pid pid = + sc_adb_execute_p(NULL, adb_cmd, ARRAY_LEN(adb_cmd), flags, &pout); if (pid == SC_PROCESS_NONE) { LOGE("Could not execute \"adb get-serialno\""); return NULL; @@ -434,11 +437,11 @@ adb_get_serialno(struct sc_intr *intr, unsigned flags) { } char * -adb_get_device_ip(struct sc_intr *intr, const char *serial, unsigned flags) { +sc_adb_get_device_ip(struct sc_intr *intr, const char *serial, unsigned flags) { const char *const cmd[] = {"shell", "ip", "route"}; sc_pipe pout; - sc_pid pid = adb_execute_p(serial, cmd, ARRAY_LEN(cmd), flags, &pout); + sc_pid pid = sc_adb_execute_p(serial, cmd, ARRAY_LEN(cmd), flags, &pout); if (pid == SC_PROCESS_NONE) { LOGD("Could not execute \"ip route\""); return NULL; diff --git a/app/src/adb/adb.h b/app/src/adb/adb.h index 4d1278cf..34fd7866 100644 --- a/app/src/adb/adb.h +++ b/app/src/adb/adb.h @@ -15,40 +15,40 @@ #define SC_ADB_SILENT (SC_ADB_NO_STDOUT | SC_ADB_NO_STDERR | SC_ADB_NO_LOGERR) sc_pid -adb_execute(const char *serial, const char *const adb_cmd[], size_t len, - unsigned flags); +sc_adb_execute(const char *serial, const char *const adb_cmd[], size_t len, + unsigned flags); bool -adb_forward(struct sc_intr *intr, const char *serial, uint16_t local_port, - const char *device_socket_name, unsigned flags); +sc_adb_forward(struct sc_intr *intr, const char *serial, uint16_t local_port, + const char *device_socket_name, unsigned flags); bool -adb_forward_remove(struct sc_intr *intr, const char *serial, - uint16_t local_port, unsigned flags); +sc_adb_forward_remove(struct sc_intr *intr, const char *serial, + uint16_t local_port, unsigned flags); bool -adb_reverse(struct sc_intr *intr, const char *serial, - const char *device_socket_name, uint16_t local_port, - unsigned flags); +sc_adb_reverse(struct sc_intr *intr, const char *serial, + const char *device_socket_name, uint16_t local_port, + unsigned flags); bool -adb_reverse_remove(struct sc_intr *intr, const char *serial, - const char *device_socket_name, unsigned flags); +sc_adb_reverse_remove(struct sc_intr *intr, const char *serial, + const char *device_socket_name, unsigned flags); bool -adb_push(struct sc_intr *intr, const char *serial, const char *local, - const char *remote, unsigned flags); +sc_adb_push(struct sc_intr *intr, const char *serial, const char *local, + const char *remote, unsigned flags); bool -adb_install(struct sc_intr *intr, const char *serial, const char *local, - unsigned flags); +sc_adb_install(struct sc_intr *intr, const char *serial, const char *local, + unsigned flags); /** * Execute `adb tcpip ` */ bool -adb_tcpip(struct sc_intr *intr, const char *serial, uint16_t port, - unsigned flags); +sc_adb_tcpip(struct sc_intr *intr, const char *serial, uint16_t port, + unsigned flags); /** * Execute `adb connect ` @@ -56,7 +56,7 @@ adb_tcpip(struct sc_intr *intr, const char *serial, uint16_t port, * `ip_port` may not be NULL. */ bool -adb_connect(struct sc_intr *intr, const char *ip_port, unsigned flags); +sc_adb_connect(struct sc_intr *intr, const char *ip_port, unsigned flags); /** * Execute `adb disconnect []` @@ -65,14 +65,14 @@ adb_connect(struct sc_intr *intr, const char *ip_port, unsigned flags); * Otherwise, execute `adb disconnect `. */ bool -adb_disconnect(struct sc_intr *intr, const char *ip_port, unsigned flags); +sc_adb_disconnect(struct sc_intr *intr, const char *ip_port, unsigned flags); /** * Execute `adb getprop ` */ char * -adb_getprop(struct sc_intr *intr, const char *serial, const char *prop, - unsigned flags); +sc_adb_getprop(struct sc_intr *intr, const char *serial, const char *prop, + unsigned flags); /** * Execute `adb get-serialno` @@ -80,7 +80,7 @@ adb_getprop(struct sc_intr *intr, const char *serial, const char *prop, * Return the result, to be freed by the caller, or NULL on error. */ char * -adb_get_serialno(struct sc_intr *intr, unsigned flags); +sc_adb_get_serialno(struct sc_intr *intr, unsigned flags); /** * Attempt to retrieve the device IP @@ -89,6 +89,6 @@ adb_get_serialno(struct sc_intr *intr, unsigned flags); * caller, or NULL on error. */ char * -adb_get_device_ip(struct sc_intr *intr, const char *serial, unsigned flags); +sc_adb_get_device_ip(struct sc_intr *intr, const char *serial, unsigned flags); #endif diff --git a/app/src/adb/adb_tunnel.c b/app/src/adb/adb_tunnel.c index 00552597..c613bc2b 100644 --- a/app/src/adb/adb_tunnel.c +++ b/app/src/adb/adb_tunnel.c @@ -20,8 +20,8 @@ enable_tunnel_reverse_any_port(struct sc_adb_tunnel *tunnel, struct sc_port_range port_range) { uint16_t port = port_range.first; for (;;) { - if (!adb_reverse(intr, serial, SC_SOCKET_NAME, port, - SC_ADB_NO_STDOUT)) { + if (!sc_adb_reverse(intr, serial, SC_SOCKET_NAME, port, + SC_ADB_NO_STDOUT)) { // the command itself failed, it will fail on any port return false; } @@ -52,7 +52,7 @@ enable_tunnel_reverse_any_port(struct sc_adb_tunnel *tunnel, } // failure, disable tunnel and try another port - if (!adb_reverse_remove(intr, serial, SC_SOCKET_NAME, + if (!sc_adb_reverse_remove(intr, serial, SC_SOCKET_NAME, SC_ADB_NO_STDOUT)) { LOGW("Could not remove reverse tunnel on port %" PRIu16, port); } @@ -83,7 +83,8 @@ enable_tunnel_forward_any_port(struct sc_adb_tunnel *tunnel, uint16_t port = port_range.first; for (;;) { - if (adb_forward(intr, serial, port, SC_SOCKET_NAME, SC_ADB_NO_STDOUT)) { + if (sc_adb_forward(intr, serial, port, SC_SOCKET_NAME, + SC_ADB_NO_STDOUT)) { // success tunnel->local_port = port; tunnel->enabled = true; @@ -148,11 +149,11 @@ sc_adb_tunnel_close(struct sc_adb_tunnel *tunnel, struct sc_intr *intr, bool ret; if (tunnel->forward) { - ret = adb_forward_remove(intr, serial, tunnel->local_port, - SC_ADB_NO_STDOUT); + ret = sc_adb_forward_remove(intr, serial, tunnel->local_port, + SC_ADB_NO_STDOUT); } else { - ret = adb_reverse_remove(intr, serial, SC_SOCKET_NAME, - SC_ADB_NO_STDOUT); + ret = sc_adb_reverse_remove(intr, serial, SC_SOCKET_NAME, + SC_ADB_NO_STDOUT); assert(tunnel->server_socket != SC_SOCKET_NONE); if (!net_close(tunnel->server_socket)) { diff --git a/app/src/file_pusher.c b/app/src/file_pusher.c index 78ea48c2..cbbeb2d7 100644 --- a/app/src/file_pusher.c +++ b/app/src/file_pusher.c @@ -129,7 +129,7 @@ run_file_pusher(void *data) { if (req.action == SC_FILE_PUSHER_ACTION_INSTALL_APK) { LOGI("Installing %s...", req.file); - bool ok = adb_install(intr, serial, req.file, 0); + bool ok = sc_adb_install(intr, serial, req.file, 0); if (ok) { LOGI("%s successfully installed", req.file); } else { @@ -137,7 +137,7 @@ run_file_pusher(void *data) { } } else { LOGI("Pushing %s...", req.file); - bool ok = adb_push(intr, serial, req.file, push_target, 0); + bool ok = sc_adb_push(intr, serial, req.file, push_target, 0); if (ok) { LOGI("%s successfully pushed to %s", req.file, push_target); } else { diff --git a/app/src/server.c b/app/src/server.c index b7a28949..2770df05 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -114,7 +114,7 @@ push_server(struct sc_intr *intr, const char *serial) { free(server_path); return false; } - bool ok = adb_push(intr, serial, server_path, SC_DEVICE_SERVER_PATH, 0); + bool ok = sc_adb_push(intr, serial, server_path, SC_DEVICE_SERVER_PATH, 0); free(server_path); return ok; } @@ -254,7 +254,7 @@ execute_server(struct sc_server *server, // Then click on "Debug" #endif // Inherit both stdout and stderr (all server logs are printed to stdout) - pid = adb_execute(params->serial, cmd, count, 0); + pid = sc_adb_execute(params->serial, cmd, count, 0); end: for (unsigned i = dyn_idx; i < count; ++i) { @@ -501,7 +501,7 @@ sc_server_fill_serial(struct sc_server *server) { // device/emulator" error) if (!server->params.serial) { // The serial is owned by sc_server_params, and will be freed on destroy - server->params.serial = adb_get_serialno(&server->intr, 0); + server->params.serial = sc_adb_get_serialno(&server->intr, 0); if (!server->params.serial) { LOGE("Could not get device serial"); return false; @@ -519,7 +519,7 @@ is_tcpip_mode_enabled(struct sc_server *server) { const char *serial = server->params.serial; char *current_port = - adb_getprop(intr, serial, "service.adb.tcp.port", SC_ADB_SILENT); + sc_adb_getprop(intr, serial, "service.adb.tcp.port", SC_ADB_SILENT); if (!current_port) { return false; } @@ -580,7 +580,7 @@ sc_server_switch_to_tcpip(struct sc_server *server, char **out_ip_port) { struct sc_intr *intr = &server->intr; - char *ip = adb_get_device_ip(intr, serial, 0); + char *ip = sc_adb_get_device_ip(intr, serial, 0); if (!ip) { LOGE("Device IP not found"); return false; @@ -595,7 +595,7 @@ sc_server_switch_to_tcpip(struct sc_server *server, char **out_ip_port) { bool tcp_mode = is_tcpip_mode_enabled(server); if (!tcp_mode) { - bool ok = adb_tcpip(intr, serial, 5555, SC_ADB_NO_STDOUT); + bool ok = sc_adb_tcpip(intr, serial, 5555, SC_ADB_NO_STDOUT); if (!ok) { LOGE("Could not restart adbd in TCP/IP mode"); goto error; @@ -623,9 +623,9 @@ sc_server_connect_to_tcpip(struct sc_server *server, const char *ip_port) { struct sc_intr *intr = &server->intr; // Error expected if not connected, do not report any error - adb_disconnect(intr, ip_port, SC_ADB_SILENT); + sc_adb_disconnect(intr, ip_port, SC_ADB_SILENT); - bool ok = adb_connect(intr, ip_port, 0); + bool ok = sc_adb_connect(intr, ip_port, 0); if (!ok) { LOGE("Could not connect to %s", ip_port); return false; From bd3c93ae3dfa3dd1b41594450a20dc5611fef50a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 4 Feb 2022 08:37:36 +0100 Subject: [PATCH 1065/2244] Remove platform-tools installation suggestion On Windows, adb is provided in the release archive. Most missing adb issues come from users setting the ADB environment variable to an incorrect value (on all platforms). Suggesting to install platform-tools to solve the problem will just make things worse (there will be one more adb in yet another location). --- app/src/adb/adb.c | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/src/adb/adb.c b/app/src/adb/adb.c index 215fc6be..4810dcbd 100644 --- a/app/src/adb/adb.c +++ b/app/src/adb/adb.c @@ -77,9 +77,6 @@ show_adb_installation_msg() { } } #endif - - LOGI("You may download and install 'adb' from " - "https://developer.android.com/studio/releases/platform-tools"); } static void From 21106bd70a07696198982bbaf00b72558b1e58c0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 4 Feb 2022 09:11:07 +0100 Subject: [PATCH 1066/2244] Remove screensaver log If --disable-screensaver is passed, then screensaver is disabled, otherwise it is enabled. No need for a log. --- app/src/scrcpy.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 270fe2b3..5da6588c 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -143,10 +143,8 @@ sdl_configure(bool display, bool disable_screensaver) { } if (disable_screensaver) { - LOGD("Screensaver disabled"); SDL_DisableScreenSaver(); } else { - LOGD("Screensaver enabled"); SDL_EnableScreenSaver(); } } From f807131c0af4c77139fb7a7b3e67fa8000c3bde5 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 5 Feb 2022 10:08:51 +0100 Subject: [PATCH 1067/2244] Remove useless undef The #define was removed in 1c71bd16bec1db0788e72a7c6b02f80ed40f1601. --- app/src/server.c | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/server.c b/app/src/server.c index 2770df05..c4172d88 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -240,7 +240,6 @@ execute_server(struct sc_server *server, } #undef ADD_PARAM -#undef STRBOOL #ifdef SERVER_DEBUGGER LOGI("Server debugger waiting for a client on device port " From 6ca9825c0f54b2d8cd11c01c6faa970bfec96330 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 5 Feb 2022 10:11:13 +0100 Subject: [PATCH 1068/2244] Assert "adb disconnect" is called with an argument Calling "adb disconnect" without argument would disconnect every TCP/IP device. We must make sure scrcpy never does that. --- app/src/adb/adb.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/src/adb/adb.c b/app/src/adb/adb.c index 4810dcbd..b6084129 100644 --- a/app/src/adb/adb.c +++ b/app/src/adb/adb.c @@ -364,11 +364,10 @@ sc_adb_connect(struct sc_intr *intr, const char *ip_port, unsigned flags) { bool sc_adb_disconnect(struct sc_intr *intr, const char *ip_port, unsigned flags) { + assert(ip_port); const char *const adb_cmd[] = {"disconnect", ip_port}; - size_t len = ip_port ? ARRAY_LEN(adb_cmd) - : ARRAY_LEN(adb_cmd) - 1; - sc_pid pid = sc_adb_execute(NULL, adb_cmd, len, flags); + sc_pid pid = sc_adb_execute(NULL, adb_cmd, ARRAY_LEN(adb_cmd), flags); return process_check_success_intr(intr, pid, "adb disconnect", flags); } From 028e7afe32091a6073a1eb7854a47e13719a9156 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 5 Feb 2022 09:51:18 +0100 Subject: [PATCH 1069/2244] Assert non-NULL serial If no serial is passed, then the command would work if there is exactly one device connected, but will fail with multiple devices. To avoid such cases, ensure that a serial is always provided. --- app/src/adb/adb.c | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/app/src/adb/adb.c b/app/src/adb/adb.c index b6084129..c10c7496 100644 --- a/app/src/adb/adb.c +++ b/app/src/adb/adb.c @@ -221,6 +221,8 @@ sc_adb_forward(struct sc_intr *intr, const char *serial, uint16_t local_port, char remote[108 + 14 + 1]; // localabstract:NAME sprintf(local, "tcp:%" PRIu16, local_port); snprintf(remote, sizeof(remote), "localabstract:%s", device_socket_name); + + assert(serial); const char *const adb_cmd[] = {"forward", local, remote}; sc_pid pid = sc_adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd), flags); @@ -232,6 +234,8 @@ sc_adb_forward_remove(struct sc_intr *intr, const char *serial, uint16_t local_port, unsigned flags) { char local[4 + 5 + 1]; // tcp:PORT sprintf(local, "tcp:%" PRIu16, local_port); + + assert(serial); const char *const adb_cmd[] = {"forward", "--remove", local}; sc_pid pid = sc_adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd), flags); @@ -246,6 +250,8 @@ sc_adb_reverse(struct sc_intr *intr, const char *serial, char remote[108 + 14 + 1]; // localabstract:NAME sprintf(local, "tcp:%" PRIu16, local_port); snprintf(remote, sizeof(remote), "localabstract:%s", device_socket_name); + + assert(serial); const char *const adb_cmd[] = {"reverse", remote, local}; sc_pid pid = sc_adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd), flags); @@ -257,6 +263,8 @@ sc_adb_reverse_remove(struct sc_intr *intr, const char *serial, const char *device_socket_name, unsigned flags) { char remote[108 + 14 + 1]; // localabstract:NAME snprintf(remote, sizeof(remote), "localabstract:%s", device_socket_name); + + assert(serial); const char *const adb_cmd[] = {"reverse", "--remove", remote}; sc_pid pid = sc_adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd), flags); @@ -280,7 +288,9 @@ sc_adb_push(struct sc_intr *intr, const char *serial, const char *local, } #endif + assert(serial); const char *const adb_cmd[] = {"push", local, remote}; + sc_pid pid = sc_adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd), flags); #ifdef __WINDOWS__ @@ -303,7 +313,9 @@ sc_adb_install(struct sc_intr *intr, const char *serial, const char *local, } #endif + assert(serial); const char *const adb_cmd[] = {"install", "-r", local}; + sc_pid pid = sc_adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd), flags); #ifdef __WINDOWS__ @@ -318,6 +330,8 @@ sc_adb_tcpip(struct sc_intr *intr, const char *serial, uint16_t port, unsigned flags) { char port_string[5 + 1]; sprintf(port_string, "%" PRIu16, port); + + assert(serial); const char *const adb_cmd[] = {"tcpip", port_string}; sc_pid pid = sc_adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd), flags); @@ -374,6 +388,7 @@ sc_adb_disconnect(struct sc_intr *intr, const char *ip_port, unsigned flags) { char * sc_adb_getprop(struct sc_intr *intr, const char *serial, const char *prop, unsigned flags) { + assert(serial); const char *const adb_cmd[] = {"shell", "getprop", prop}; sc_pipe pout; @@ -434,6 +449,7 @@ sc_adb_get_serialno(struct sc_intr *intr, unsigned flags) { char * sc_adb_get_device_ip(struct sc_intr *intr, const char *serial, unsigned flags) { + assert(serial); const char *const cmd[] = {"shell", "ip", "route"}; sc_pipe pout; From ba30ca5c1edc53fca3f230a2fdc5dbb7648588a5 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 5 Feb 2022 09:21:53 +0100 Subject: [PATCH 1070/2244] Rename adb_command to adb_executable Semantically, a "command" refers to the whole command line argv (adb and its arguments). --- app/src/adb/adb.c | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/src/adb/adb.c b/app/src/adb/adb.c index c10c7496..44c197eb 100644 --- a/app/src/adb/adb.c +++ b/app/src/adb/adb.c @@ -11,16 +11,16 @@ #include "util/process_intr.h" #include "util/str.h" -static const char *adb_command; +static const char *adb_executable; static inline const char * -get_adb_command(void) { - if (!adb_command) { - adb_command = getenv("ADB"); - if (!adb_command) - adb_command = "adb"; +get_adb_executable(void) { + if (!adb_executable) { + adb_executable = getenv("ADB"); + if (!adb_executable) + adb_executable = "adb"; } - return adb_command; + return adb_executable; } // serialize argv to string "[arg1], [arg2], [arg3]" @@ -163,7 +163,7 @@ sc_adb_create_argv(const char *serial, const char *const adb_cmd[], return NULL; } - argv[0] = get_adb_command(); + argv[0] = get_adb_executable(); int i; if (serial) { argv[1] = "-s"; From 5e2bfccab4e8489da02c01a8fca1f568de50e6b7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 5 Feb 2022 09:37:00 +0100 Subject: [PATCH 1071/2244] Expose adb executable path publicly This will allow the caller to build the argv array directly. --- app/src/adb/adb.c | 6 +++--- app/src/adb/adb.h | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/src/adb/adb.c b/app/src/adb/adb.c index 44c197eb..ded0298b 100644 --- a/app/src/adb/adb.c +++ b/app/src/adb/adb.c @@ -13,8 +13,8 @@ static const char *adb_executable; -static inline const char * -get_adb_executable(void) { +const char * +sc_adb_get_executable(void) { if (!adb_executable) { adb_executable = getenv("ADB"); if (!adb_executable) @@ -163,7 +163,7 @@ sc_adb_create_argv(const char *serial, const char *const adb_cmd[], return NULL; } - argv[0] = get_adb_executable(); + argv[0] = sc_adb_get_executable(); int i; if (serial) { argv[1] = "-s"; diff --git a/app/src/adb/adb.h b/app/src/adb/adb.h index 34fd7866..8b8954f8 100644 --- a/app/src/adb/adb.h +++ b/app/src/adb/adb.h @@ -14,6 +14,9 @@ #define SC_ADB_SILENT (SC_ADB_NO_STDOUT | SC_ADB_NO_STDERR | SC_ADB_NO_LOGERR) +const char * +sc_adb_get_executable(void); + sc_pid sc_adb_execute(const char *serial, const char *const adb_cmd[], size_t len, unsigned flags); From 386cf7d7acf305827dc49c621be7a6b64e6633d2 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 5 Feb 2022 10:07:13 +0100 Subject: [PATCH 1072/2244] Build adb argv statically Now that providing a serial is mandatory for adb commands where it is relevant, the whole argv array may be built statically, without allocations at runtime. --- app/src/adb/adb.c | 111 ++++++++++++++++++++-------------------------- app/src/adb/adb.h | 3 +- app/src/server.c | 10 ++++- 3 files changed, 58 insertions(+), 66 deletions(-) diff --git a/app/src/adb/adb.c b/app/src/adb/adb.c index ded0298b..2aad516a 100644 --- a/app/src/adb/adb.c +++ b/app/src/adb/adb.c @@ -11,6 +11,18 @@ #include "util/process_intr.h" #include "util/str.h" +/* Convenience macro to expand: + * + * const char *const argv[] = + * SC_ADB_COMMAND("shell", "echo", "hello"); + * + * to: + * + * const char *const argv[] = + * { sc_adb_get_executable(), "shell", "echo", "hello", NULL }; + */ +#define SC_ADB_COMMAND(...) { sc_adb_get_executable(), __VA_ARGS__, NULL } + static const char *adb_executable; const char * @@ -154,38 +166,8 @@ process_check_success_intr(struct sc_intr *intr, sc_pid pid, const char *name, return ret; } -static const char ** -sc_adb_create_argv(const char *serial, const char *const adb_cmd[], - size_t len) { - const char **argv = malloc((len + 4) * sizeof(*argv)); - if (!argv) { - LOG_OOM(); - return NULL; - } - - argv[0] = sc_adb_get_executable(); - int i; - if (serial) { - argv[1] = "-s"; - argv[2] = serial; - i = 3; - } else { - i = 1; - } - - memcpy(&argv[i], adb_cmd, len * sizeof(const char *)); - argv[len + i] = NULL; - return argv; -} - static sc_pid -sc_adb_execute_p(const char *serial, const char *const adb_cmd[], size_t len, - unsigned flags, sc_pipe *pout) { - const char **argv = sc_adb_create_argv(serial, adb_cmd, len); - if (!argv) { - return SC_PROCESS_NONE; - } - +sc_adb_execute_p(const char *const argv[], unsigned flags, sc_pipe *pout) { unsigned process_flags = 0; if (flags & SC_ADB_NO_STDOUT) { process_flags |= SC_PROCESS_NO_STDOUT; @@ -204,14 +186,12 @@ sc_adb_execute_p(const char *serial, const char *const adb_cmd[], size_t len, pid = SC_PROCESS_NONE; } - free(argv); return pid; } sc_pid -sc_adb_execute(const char *serial, const char *const adb_cmd[], size_t len, - unsigned flags) { - return sc_adb_execute_p(serial, adb_cmd, len, flags, NULL); +sc_adb_execute(const char *const argv[], unsigned flags) { + return sc_adb_execute_p(argv, flags, NULL); } bool @@ -223,9 +203,10 @@ sc_adb_forward(struct sc_intr *intr, const char *serial, uint16_t local_port, snprintf(remote, sizeof(remote), "localabstract:%s", device_socket_name); assert(serial); - const char *const adb_cmd[] = {"forward", local, remote}; + const char *const argv[] = + SC_ADB_COMMAND("-s", serial, "forward", local, remote); - sc_pid pid = sc_adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd), flags); + sc_pid pid = sc_adb_execute(argv, flags); return process_check_success_intr(intr, pid, "adb forward", flags); } @@ -236,9 +217,10 @@ sc_adb_forward_remove(struct sc_intr *intr, const char *serial, sprintf(local, "tcp:%" PRIu16, local_port); assert(serial); - const char *const adb_cmd[] = {"forward", "--remove", local}; + const char *const argv[] = + SC_ADB_COMMAND("-s", serial, "forward", "--remove", local); - sc_pid pid = sc_adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd), flags); + sc_pid pid = sc_adb_execute(argv, flags); return process_check_success_intr(intr, pid, "adb forward --remove", flags); } @@ -250,11 +232,11 @@ sc_adb_reverse(struct sc_intr *intr, const char *serial, char remote[108 + 14 + 1]; // localabstract:NAME sprintf(local, "tcp:%" PRIu16, local_port); snprintf(remote, sizeof(remote), "localabstract:%s", device_socket_name); - assert(serial); - const char *const adb_cmd[] = {"reverse", remote, local}; + const char *const argv[] = + SC_ADB_COMMAND("-s", serial, "reverse", remote, local); - sc_pid pid = sc_adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd), flags); + sc_pid pid = sc_adb_execute(argv, flags); return process_check_success_intr(intr, pid, "adb reverse", flags); } @@ -265,9 +247,10 @@ sc_adb_reverse_remove(struct sc_intr *intr, const char *serial, snprintf(remote, sizeof(remote), "localabstract:%s", device_socket_name); assert(serial); - const char *const adb_cmd[] = {"reverse", "--remove", remote}; + const char *const argv[] = + SC_ADB_COMMAND("-s", serial, "reverse", "--remove", remote); - sc_pid pid = sc_adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd), flags); + sc_pid pid = sc_adb_execute(argv, flags); return process_check_success_intr(intr, pid, "adb reverse --remove", flags); } @@ -289,9 +272,10 @@ sc_adb_push(struct sc_intr *intr, const char *serial, const char *local, #endif assert(serial); - const char *const adb_cmd[] = {"push", local, remote}; + const char *const argv[] = + SC_ADB_COMMAND("-s", serial, "push", local, remote); - sc_pid pid = sc_adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd), flags); + sc_pid pid = sc_adb_execute(argv, flags); #ifdef __WINDOWS__ free((void *) remote); @@ -314,9 +298,10 @@ sc_adb_install(struct sc_intr *intr, const char *serial, const char *local, #endif assert(serial); - const char *const adb_cmd[] = {"install", "-r", local}; + const char *const argv[] = + SC_ADB_COMMAND("-s", serial, "install", "-r", local); - sc_pid pid = sc_adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd), flags); + sc_pid pid = sc_adb_execute(argv, flags); #ifdef __WINDOWS__ free((void *) local); @@ -332,19 +317,19 @@ sc_adb_tcpip(struct sc_intr *intr, const char *serial, uint16_t port, sprintf(port_string, "%" PRIu16, port); assert(serial); - const char *const adb_cmd[] = {"tcpip", port_string}; + const char *const argv[] = + SC_ADB_COMMAND("-s", serial, "tcpip", port_string); - sc_pid pid = sc_adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd), flags); + sc_pid pid = sc_adb_execute(argv, flags); return process_check_success_intr(intr, pid, "adb tcpip", flags); } bool sc_adb_connect(struct sc_intr *intr, const char *ip_port, unsigned flags) { - const char *const adb_cmd[] = {"connect", ip_port}; + const char *const argv[] = SC_ADB_COMMAND("connect", ip_port); sc_pipe pout; - sc_pid pid = - sc_adb_execute_p(NULL, adb_cmd, ARRAY_LEN(adb_cmd), flags, &pout); + sc_pid pid = sc_adb_execute_p(argv, flags, &pout); if (pid == SC_PROCESS_NONE) { LOGE("Could not execute \"adb connect\""); return false; @@ -379,9 +364,9 @@ sc_adb_connect(struct sc_intr *intr, const char *ip_port, unsigned flags) { bool sc_adb_disconnect(struct sc_intr *intr, const char *ip_port, unsigned flags) { assert(ip_port); - const char *const adb_cmd[] = {"disconnect", ip_port}; + const char *const argv[] = SC_ADB_COMMAND("disconnect", ip_port); - sc_pid pid = sc_adb_execute(NULL, adb_cmd, ARRAY_LEN(adb_cmd), flags); + sc_pid pid = sc_adb_execute(argv, flags); return process_check_success_intr(intr, pid, "adb disconnect", flags); } @@ -389,11 +374,11 @@ char * sc_adb_getprop(struct sc_intr *intr, const char *serial, const char *prop, unsigned flags) { assert(serial); - const char *const adb_cmd[] = {"shell", "getprop", prop}; + const char *const argv[] = + SC_ADB_COMMAND("-s", serial, "shell", "getprop", prop); sc_pipe pout; - sc_pid pid = - sc_adb_execute_p(serial, adb_cmd, ARRAY_LEN(adb_cmd), flags, &pout); + sc_pid pid = sc_adb_execute_p(argv, flags, &pout); if (pid == SC_PROCESS_NONE) { LOGE("Could not execute \"adb getprop\""); return NULL; @@ -419,11 +404,10 @@ sc_adb_getprop(struct sc_intr *intr, const char *serial, const char *prop, char * sc_adb_get_serialno(struct sc_intr *intr, unsigned flags) { - const char *const adb_cmd[] = {"get-serialno"}; + const char *const argv[] = SC_ADB_COMMAND("get-serialno"); sc_pipe pout; - sc_pid pid = - sc_adb_execute_p(NULL, adb_cmd, ARRAY_LEN(adb_cmd), flags, &pout); + sc_pid pid = sc_adb_execute_p(argv, flags, &pout); if (pid == SC_PROCESS_NONE) { LOGE("Could not execute \"adb get-serialno\""); return NULL; @@ -450,10 +434,11 @@ sc_adb_get_serialno(struct sc_intr *intr, unsigned flags) { char * sc_adb_get_device_ip(struct sc_intr *intr, const char *serial, unsigned flags) { assert(serial); - const char *const cmd[] = {"shell", "ip", "route"}; + const char *const argv[] = + SC_ADB_COMMAND("-s", serial, "shell", "ip", "route"); sc_pipe pout; - sc_pid pid = sc_adb_execute_p(serial, cmd, ARRAY_LEN(cmd), flags, &pout); + sc_pid pid = sc_adb_execute_p(argv, flags, &pout); if (pid == SC_PROCESS_NONE) { LOGD("Could not execute \"ip route\""); return NULL; diff --git a/app/src/adb/adb.h b/app/src/adb/adb.h index 8b8954f8..1a97c203 100644 --- a/app/src/adb/adb.h +++ b/app/src/adb/adb.h @@ -18,8 +18,7 @@ const char * sc_adb_get_executable(void); sc_pid -sc_adb_execute(const char *serial, const char *const adb_cmd[], size_t len, - unsigned flags); +sc_adb_execute(const char *const argv[], unsigned flags); bool sc_adb_forward(struct sc_intr *intr, const char *serial, uint16_t local_port, diff --git a/app/src/server.c b/app/src/server.c index c4172d88..abdaa6a4 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -157,8 +157,14 @@ execute_server(struct sc_server *server, const struct sc_server_params *params) { sc_pid pid = SC_PROCESS_NONE; + const char *serial = params->serial; + assert(serial); + const char *cmd[128]; unsigned count = 0; + cmd[count++] = sc_adb_get_executable(); + cmd[count++] = "-s"; + cmd[count++] = serial; cmd[count++] = "shell"; cmd[count++] = "CLASSPATH=" SC_DEVICE_SERVER_PATH; cmd[count++] = "app_process"; @@ -241,6 +247,8 @@ execute_server(struct sc_server *server, #undef ADD_PARAM + cmd[count++] = NULL; + #ifdef SERVER_DEBUGGER LOGI("Server debugger waiting for a client on device port " SERVER_DEBUGGER_PORT "..."); @@ -253,7 +261,7 @@ execute_server(struct sc_server *server, // Then click on "Debug" #endif // Inherit both stdout and stderr (all server logs are printed to stdout) - pid = sc_adb_execute(params->serial, cmd, count, 0); + pid = sc_adb_execute(cmd, 0); end: for (unsigned i = dyn_idx; i < count; ++i) { From 08f16a9dde12a5631ca3a24b888816465212963b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 4 Feb 2022 19:42:42 +0100 Subject: [PATCH 1073/2244] Simplify switch to TCPIP function Do not use an output parameter to return the value. Instead, return the actual ip:port string on success or NULL on error. --- app/src/server.c | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/app/src/server.c b/app/src/server.c index abdaa6a4..2d4f70b8 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -580,8 +580,8 @@ append_port_5555(const char *ip) { return ip_port; } -static bool -sc_server_switch_to_tcpip(struct sc_server *server, char **out_ip_port) { +static char * +sc_server_switch_to_tcpip(struct sc_server *server) { const char *serial = server->params.serial; assert(serial); @@ -590,13 +590,13 @@ sc_server_switch_to_tcpip(struct sc_server *server, char **out_ip_port) { char *ip = sc_adb_get_device_ip(intr, serial, 0); if (!ip) { LOGE("Device IP not found"); - return false; + return NULL; } char *ip_port = append_port_5555(ip); free(ip); if (!ip_port) { - return false; + return NULL; } bool tcp_mode = is_tcpip_mode_enabled(server); @@ -616,13 +616,11 @@ sc_server_switch_to_tcpip(struct sc_server *server, char **out_ip_port) { } } - *out_ip_port = ip_port; - - return true; + return ip_port; error: free(ip_port); - return false; + return NULL; } static bool @@ -687,8 +685,8 @@ sc_server_configure_tcpip(struct sc_server *server) { return true; } - bool ok = sc_server_switch_to_tcpip(server, &ip_port); - if (!ok) { + ip_port = sc_server_switch_to_tcpip(server); + if (!ip_port) { return false; } } From 5b3ae2cb2f95a333b3b90253ac959a34d51dd4ea Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 4 Feb 2022 20:49:35 +0100 Subject: [PATCH 1074/2244] Store actual serial in sc_server Before starting the server, the actual device serial (possibly its ip:port if it's over TCP/IP) must be known. A serial might be requested via -s/--serial (stored in the sc_server_params), but the actual serial may change afterwards: - if none is provided, then it is retrieved with "adb get-serialno"; - if --tcpip is requested, then the final serial will be the target ip:port. The requested serial was overwritten by the actual serial in the sc_server_params struct, which was a bit hacky. Instead, store a separate serial field in sc_server (and rename the one from sc_server_params to "req_serial" to avoid confusion). --- app/src/scrcpy.c | 4 +- app/src/server.c | 116 +++++++++++++++++++++++------------------------ app/src/server.h | 3 +- 3 files changed, 60 insertions(+), 63 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 5da6588c..c846ee18 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -296,7 +296,7 @@ scrcpy(struct scrcpy_options *options) { struct sc_acksync *acksync = NULL; struct sc_server_params params = { - .serial = options->serial, + .req_serial = options->serial, .log_level = options->log_level, .crop = options->crop, .port_range = options->port_range, @@ -355,7 +355,7 @@ scrcpy(struct scrcpy_options *options) { // It is necessarily initialized here, since the device is connected struct sc_server_info *info = &s->server.info; - const char *serial = s->server.params.serial; + const char *serial = s->server.serial; assert(serial); struct sc_file_pusher *fp = NULL; diff --git a/app/src/server.c b/app/src/server.c index 2d4f70b8..e8ae32af 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -65,7 +65,7 @@ get_server_path(void) { static void sc_server_params_destroy(struct sc_server_params *params) { // The server stores a copy of the params provided by the user - free((char *) params->serial); + free((char *) params->req_serial); free((char *) params->crop); free((char *) params->codec_options); free((char *) params->encoder_name); @@ -89,7 +89,7 @@ sc_server_params_copy(struct sc_server_params *dst, } \ } - COPY(serial); + COPY(req_serial); COPY(crop); COPY(codec_options); COPY(encoder_name); @@ -157,7 +157,7 @@ execute_server(struct sc_server *server, const struct sc_server_params *params) { sc_pid pid = SC_PROCESS_NONE; - const char *serial = params->serial; + const char *serial = server->serial; assert(serial); const char *cmd[128]; @@ -353,6 +353,7 @@ sc_server_init(struct sc_server *server, const struct sc_server_params *params, return false; } + server->serial = NULL; server->stopped = false; server->video_socket = SC_SOCKET_NONE; @@ -397,7 +398,9 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) { assert(tunnel->enabled); - const char *serial = server->params.serial; + const char *serial = server->serial; + assert(serial); + bool control = server->params.control; sc_socket video_socket = SC_SOCKET_NONE; @@ -500,30 +503,25 @@ sc_server_on_terminated(void *userdata) { LOGD("Server terminated"); } -static bool -sc_server_fill_serial(struct sc_server *server) { - // Retrieve the actual device immediately if not provided, so that all - // future adb commands are executed for this specific device, even if other - // devices are connected afterwards (without "more than one - // device/emulator" error) - if (!server->params.serial) { - // The serial is owned by sc_server_params, and will be freed on destroy - server->params.serial = sc_adb_get_serialno(&server->intr, 0); - if (!server->params.serial) { - LOGE("Could not get device serial"); - return false; +static char * +sc_server_read_serial(struct sc_server *server) { + char *serial; + if (server->params.req_serial) { + // The serial is already known + serial = strdup(server->params.req_serial); + if (!serial) { + LOG_OOM(); } - - LOGD("Device serial: %s", server->params.serial); + } else { + serial = sc_adb_get_serialno(&server->intr, 0); } - return true; + return serial; } static bool -is_tcpip_mode_enabled(struct sc_server *server) { +is_tcpip_mode_enabled(struct sc_server *server, const char *serial) { struct sc_intr *intr = &server->intr; - const char *serial = server->params.serial; char *current_port = sc_adb_getprop(intr, serial, "service.adb.tcp.port", SC_ADB_SILENT); @@ -538,9 +536,9 @@ is_tcpip_mode_enabled(struct sc_server *server) { } static bool -wait_tcpip_mode_enabled(struct sc_server *server, unsigned attempts, - sc_tick delay) { - if (is_tcpip_mode_enabled(server)) { +wait_tcpip_mode_enabled(struct sc_server *server, const char *serial, + unsigned attempts, sc_tick delay) { + if (is_tcpip_mode_enabled(server, serial)) { LOGI("TCP/IP mode enabled"); return true; } @@ -555,7 +553,7 @@ wait_tcpip_mode_enabled(struct sc_server *server, unsigned attempts, return false; } - if (is_tcpip_mode_enabled(server)) { + if (is_tcpip_mode_enabled(server, serial)) { LOGI("TCP/IP mode enabled"); return true; } @@ -581,12 +579,13 @@ append_port_5555(const char *ip) { } static char * -sc_server_switch_to_tcpip(struct sc_server *server) { - const char *serial = server->params.serial; +sc_server_switch_to_tcpip(struct sc_server *server, const char *serial) { assert(serial); struct sc_intr *intr = &server->intr; + LOGI("Switching device %s to TCP/IP...", serial); + char *ip = sc_adb_get_device_ip(intr, serial, 0); if (!ip) { LOGE("Device IP not found"); @@ -599,7 +598,7 @@ sc_server_switch_to_tcpip(struct sc_server *server) { return NULL; } - bool tcp_mode = is_tcpip_mode_enabled(server); + bool tcp_mode = is_tcpip_mode_enabled(server, serial); if (!tcp_mode) { bool ok = sc_adb_tcpip(intr, serial, 5555, SC_ADB_NO_STDOUT); @@ -610,7 +609,7 @@ sc_server_switch_to_tcpip(struct sc_server *server) { unsigned attempts = 40; sc_tick delay = SC_TICK_FROM_MS(250); - ok = wait_tcpip_mode_enabled(server, attempts, delay); + ok = wait_tcpip_mode_enabled(server, serial, attempts, delay); if (!ok) { goto error; } @@ -630,20 +629,14 @@ sc_server_connect_to_tcpip(struct sc_server *server, const char *ip_port) { // Error expected if not connected, do not report any error sc_adb_disconnect(intr, ip_port, SC_ADB_SILENT); + LOGI("Connecting to %s...", ip_port); + bool ok = sc_adb_connect(intr, ip_port, 0); if (!ok) { LOGE("Could not connect to %s", ip_port); return false; } - // Override the serial, owned by the sc_server_params - free((void *) server->params.serial); - server->params.serial = strdup(ip_port); - if (!server->params.serial) { - LOG_OOM(); - return false; - } - LOGI("Connected to %s", ip_port); return true; } @@ -657,7 +650,7 @@ sc_server_configure_tcpip(struct sc_server *server) { // If tcpip parameter is given, then it must connect to this address. // Therefore, the device is unknown, so serial is meaningless at this point. - assert(!params->serial || !params->tcpip_dst); + assert(!params->req_serial || !params->tcpip_dst); if (params->tcpip_dst) { // Append ":5555" if no port is present @@ -671,30 +664,32 @@ sc_server_configure_tcpip(struct sc_server *server) { } else { // The device IP address must be retrieved from the current // connected device - if (!sc_server_fill_serial(server)) { + char *serial = sc_server_read_serial(server); + if (!serial) { + LOGE("Could not get device serial"); return false; } // The serial is either the real serial when connected via USB, or // the IP:PORT when connected over TCP/IP. Only the latter contains // a colon. - bool is_already_tcpip = strchr(params->serial, ':'); + bool is_already_tcpip = strchr(serial, ':'); if (is_already_tcpip) { // Nothing to do - LOGI("Device already connected via TCP/IP: %s", params->serial); + LOGI("Device already connected via TCP/IP: %s", serial); + free(serial); return true; } - ip_port = sc_server_switch_to_tcpip(server); + ip_port = sc_server_switch_to_tcpip(server, serial); + free(serial); if (!ip_port) { return false; } } - // On success, this call changes params->serial - bool ok = sc_server_connect_to_tcpip(server, ip_port); - free(ip_port); - return ok; + server->serial = ip_port; + return sc_server_connect_to_tcpip(server, ip_port); } static int @@ -703,30 +698,30 @@ run_server(void *data) { const struct sc_server_params *params = &server->params; - if (params->serial) { - LOGD("Device serial: %s", params->serial); - } - if (params->tcpip) { - // params->serial may be changed after this call bool ok = sc_server_configure_tcpip(server); if (!ok) { goto error_connection_failed; } + assert(server->serial); + } else { + server->serial = sc_server_read_serial(server); + if (!server->serial) { + LOGD("Could not get device serial"); + goto error_connection_failed; + } } - // It is ok to call this function even if the device serial has been - // changed by switching over TCP/IP - if (!sc_server_fill_serial(server)) { - goto error_connection_failed; - } + const char *serial = server->serial; + assert(serial); + LOGD("Device serial: %s", serial); - bool ok = push_server(&server->intr, params->serial); + bool ok = push_server(&server->intr, serial); if (!ok) { goto error_connection_failed; } - ok = sc_adb_tunnel_open(&server->tunnel, &server->intr, params->serial, + ok = sc_adb_tunnel_open(&server->tunnel, &server->intr, serial, params->port_range, params->force_adb_forward); if (!ok) { goto error_connection_failed; @@ -735,7 +730,7 @@ run_server(void *data) { // server will connect to our server socket sc_pid pid = execute_server(server, params); if (pid == SC_PROCESS_NONE) { - sc_adb_tunnel_close(&server->tunnel, &server->intr, params->serial); + sc_adb_tunnel_close(&server->tunnel, &server->intr, serial); goto error_connection_failed; } @@ -747,7 +742,7 @@ run_server(void *data) { if (!ok) { sc_process_terminate(pid); sc_process_wait(pid, true); // ignore exit code - sc_adb_tunnel_close(&server->tunnel, &server->intr, params->serial); + sc_adb_tunnel_close(&server->tunnel, &server->intr, serial); goto error_connection_failed; } @@ -840,6 +835,7 @@ sc_server_destroy(struct sc_server *server) { net_close(server->control_socket); } + free(server->serial); sc_server_params_destroy(&server->params); sc_intr_destroy(&server->intr); sc_cond_destroy(&server->cond_stopped); diff --git a/app/src/server.h b/app/src/server.h index 1dff19a7..c2293118 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -22,7 +22,7 @@ struct sc_server_info { }; struct sc_server_params { - const char *serial; + const char *req_serial; enum sc_log_level log_level; const char *crop; const char *codec_options; @@ -49,6 +49,7 @@ struct sc_server_params { struct sc_server { // The internal allocated strings are copies owned by the server struct sc_server_params params; + char *serial; sc_thread thread; struct sc_server_info info; // initialized once connected From 5d6bd8f9cd69036b331bb1898fe7c6353777f592 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 6 Feb 2022 10:52:55 +0100 Subject: [PATCH 1075/2244] Fix adb device ip parsing The parser assumed that its input was a NUL-terminated string, but it was not the case: it is just the raw output of "adb devices ip route". In practice, it was harmless, since the output always ended with '\n' (which was replaced by '\0' on truncation), but it was incorrect nonetheless. Always write a '\0' at the end of the buffer, and explicitly parse as a NUL-terminated string. For that purpose, avoid the error-prone sc_str_truncate() util function. --- app/src/adb/adb.c | 11 +++++++---- app/src/adb/adb_parser.c | 27 +++++++++++++++++---------- app/src/adb/adb_parser.h | 4 +++- app/tests/test_adb_parser.c | 23 ++++++++++++++++------- 4 files changed, 43 insertions(+), 22 deletions(-) diff --git a/app/src/adb/adb.c b/app/src/adb/adb.c index 2aad516a..5a1ed25d 100644 --- a/app/src/adb/adb.c +++ b/app/src/adb/adb.c @@ -446,7 +446,7 @@ sc_adb_get_device_ip(struct sc_intr *intr, const char *serial, unsigned flags) { // "adb shell ip route" output should contain only a few lines char buf[1024]; - ssize_t r = sc_pipe_read_all_intr(intr, pid, pout, buf, sizeof(buf)); + ssize_t r = sc_pipe_read_all_intr(intr, pid, pout, buf, sizeof(buf) - 1); sc_pipe_close(pout); bool ok = process_check_success_intr(intr, pid, "ip route", flags); @@ -458,8 +458,8 @@ sc_adb_get_device_ip(struct sc_intr *intr, const char *serial, unsigned flags) { return NULL; } - assert((size_t) r <= sizeof(buf)); - if (r == sizeof(buf) && buf[sizeof(buf) - 1] != '\0') { + assert((size_t) r < sizeof(buf)); + if (r == sizeof(buf) - 1) { // The implementation assumes that the output of "ip route" fits in the // buffer in a single pass LOGW("Result of \"ip route\" does not fit in 1Kb. " @@ -467,5 +467,8 @@ sc_adb_get_device_ip(struct sc_intr *intr, const char *serial, unsigned flags) { return NULL; } - return sc_adb_parse_device_ip_from_output(buf, r); + // It is parsed as a NUL-terminated string + buf[r] = '\0'; + + return sc_adb_parse_device_ip_from_output(buf); } diff --git a/app/src/adb/adb_parser.c b/app/src/adb/adb_parser.c index d660aaab..2a0fd8da 100644 --- a/app/src/adb/adb_parser.c +++ b/app/src/adb/adb_parser.c @@ -7,7 +7,7 @@ #include "util/str.h" static char * -sc_adb_parse_device_ip_from_line(char *line, size_t len) { +sc_adb_parse_device_ip_from_line(char *line) { // One line from "ip route" looks like: // "192.168.1.0/24 dev wlan0 proto kernel scope link src 192.168.1.x" @@ -27,10 +27,12 @@ sc_adb_parse_device_ip_from_line(char *line, size_t len) { idx_ip += idx_dev_name; char *dev_name = &line[idx_dev_name]; - sc_str_truncate(dev_name, len - idx_dev_name + 1, " \t"); + size_t dev_name_len = strcspn(dev_name, " \t"); + dev_name[dev_name_len] = '\0'; char *ip = &line[idx_ip]; - sc_str_truncate(ip, len - idx_ip + 1, " \t"); + size_t ip_len = strcspn(ip, " \t"); + ip[ip_len] = '\0'; // Only consider lines where the device name starts with "wlan" if (strncmp(dev_name, "wlan", sizeof("wlan") - 1)) { @@ -42,23 +44,28 @@ sc_adb_parse_device_ip_from_line(char *line, size_t len) { } char * -sc_adb_parse_device_ip_from_output(char *buf, size_t buf_len) { +sc_adb_parse_device_ip_from_output(char *str) { size_t idx_line = 0; - while (idx_line < buf_len && buf[idx_line] != '\0') { - char *line = &buf[idx_line]; - size_t len = sc_str_truncate(line, buf_len - idx_line, "\n"); + while (str[idx_line] != '\0') { + char *line = &str[idx_line]; + size_t len = strcspn(line, "\n"); // The same, but without any trailing '\r' size_t line_len = sc_str_remove_trailing_cr(line, len); + line[line_len] = '\0'; - char *ip = sc_adb_parse_device_ip_from_line(line, line_len); + char *ip = sc_adb_parse_device_ip_from_line(line); if (ip) { // Found return ip; } - // The next line starts after the '\n' (replaced by `\0`) - idx_line += len + 1; + idx_line += len; + + if (str[idx_line] != '\0') { + // The next line starts after the '\n' + idx_line += 1; + } } return NULL; diff --git a/app/src/adb/adb_parser.h b/app/src/adb/adb_parser.h index bce5126c..7d116713 100644 --- a/app/src/adb/adb_parser.h +++ b/app/src/adb/adb_parser.h @@ -8,9 +8,11 @@ /** * Parse the ip from the output of `adb shell ip route` * + * The parameter must be a NUL-terminated string. + * * Warning: this function modifies the buffer for optimization purposes. */ char * -sc_adb_parse_device_ip_from_output(char *buf, size_t buf_len); +sc_adb_parse_device_ip_from_output(char *str); #endif diff --git a/app/tests/test_adb_parser.c b/app/tests/test_adb_parser.c index 51e7d6d2..749ce433 100644 --- a/app/tests/test_adb_parser.c +++ b/app/tests/test_adb_parser.c @@ -8,7 +8,7 @@ static void test_get_ip_single_line() { char ip_route[] = "192.168.1.0/24 dev wlan0 proto kernel scope link src " "192.168.12.34\r\r\n"; - char *ip = sc_adb_parse_device_ip_from_output(ip_route, sizeof(ip_route)); + char *ip = sc_adb_parse_device_ip_from_output(ip_route); assert(ip); assert(!strcmp(ip, "192.168.12.34")); free(ip); @@ -18,7 +18,7 @@ static void test_get_ip_single_line_without_eol() { char ip_route[] = "192.168.1.0/24 dev wlan0 proto kernel scope link src " "192.168.12.34"; - char *ip = sc_adb_parse_device_ip_from_output(ip_route, sizeof(ip_route)); + char *ip = sc_adb_parse_device_ip_from_output(ip_route); assert(ip); assert(!strcmp(ip, "192.168.12.34")); free(ip); @@ -28,7 +28,7 @@ static void test_get_ip_single_line_with_trailing_space() { char ip_route[] = "192.168.1.0/24 dev wlan0 proto kernel scope link src " "192.168.12.34 \n"; - char *ip = sc_adb_parse_device_ip_from_output(ip_route, sizeof(ip_route)); + char *ip = sc_adb_parse_device_ip_from_output(ip_route); assert(ip); assert(!strcmp(ip, "192.168.12.34")); free(ip); @@ -40,7 +40,7 @@ static void test_get_ip_multiline_first_ok() { "10.0.0.0/24 dev rmnet proto kernel scope link src " "10.0.0.2\r\n"; - char *ip = sc_adb_parse_device_ip_from_output(ip_route, sizeof(ip_route)); + char *ip = sc_adb_parse_device_ip_from_output(ip_route); assert(ip); assert(!strcmp(ip, "192.168.1.2")); free(ip); @@ -52,7 +52,7 @@ static void test_get_ip_multiline_second_ok() { "192.168.1.0/24 dev wlan0 proto kernel scope link src " "192.168.1.3\r\n"; - char *ip = sc_adb_parse_device_ip_from_output(ip_route, sizeof(ip_route)); + char *ip = sc_adb_parse_device_ip_from_output(ip_route); assert(ip); assert(!strcmp(ip, "192.168.1.3")); free(ip); @@ -62,7 +62,15 @@ static void test_get_ip_no_wlan() { char ip_route[] = "192.168.1.0/24 dev rmnet proto kernel scope link src " "192.168.12.34\r\r\n"; - char *ip = sc_adb_parse_device_ip_from_output(ip_route, sizeof(ip_route)); + char *ip = sc_adb_parse_device_ip_from_output(ip_route); + assert(!ip); +} + +static void test_get_ip_no_wlan_without_eol() { + char ip_route[] = "192.168.1.0/24 dev rmnet proto kernel scope link src " + "192.168.12.34"; + + char *ip = sc_adb_parse_device_ip_from_output(ip_route); assert(!ip); } @@ -70,7 +78,7 @@ static void test_get_ip_truncated() { char ip_route[] = "192.168.1.0/24 dev rmnet proto kernel scope link src " "\n"; - char *ip = sc_adb_parse_device_ip_from_output(ip_route, sizeof(ip_route)); + char *ip = sc_adb_parse_device_ip_from_output(ip_route); assert(!ip); } @@ -84,5 +92,6 @@ int main(int argc, char *argv[]) { test_get_ip_multiline_first_ok(); test_get_ip_multiline_second_ok(); test_get_ip_no_wlan(); + test_get_ip_no_wlan_without_eol(); test_get_ip_truncated(); } From 2ea12f73db333d0d6b01f92f2a092ada034c7f27 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 6 Feb 2022 12:17:39 +0100 Subject: [PATCH 1076/2244] Fix adb getprop parsing The function assumed that the raw output of "adb getprop" was a NUL-terminated string, but it is not the case. It this output did not end with a space or a new line character, then sc_str_truncate() would write '\0' over the last character. Even worse, if the output was empty, then sc_str_truncate() would write out-of-bounds. Avoid the error-prone sc_str_truncate() util function. --- app/src/adb/adb.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/src/adb/adb.c b/app/src/adb/adb.c index 5a1ed25d..2fd4b35d 100644 --- a/app/src/adb/adb.c +++ b/app/src/adb/adb.c @@ -385,7 +385,7 @@ sc_adb_getprop(struct sc_intr *intr, const char *serial, const char *prop, } char buf[128]; - ssize_t r = sc_pipe_read_all_intr(intr, pid, pout, buf, sizeof(buf)); + ssize_t r = sc_pipe_read_all_intr(intr, pid, pout, buf, sizeof(buf) - 1); sc_pipe_close(pout); bool ok = process_check_success_intr(intr, pid, "adb getprop", flags); @@ -397,7 +397,10 @@ sc_adb_getprop(struct sc_intr *intr, const char *serial, const char *prop, return NULL; } - sc_str_truncate(buf, r, " \r\n"); + assert((size_t) r < sizeof(buf)); + buf[r] = '\0'; + size_t len = strcspn(buf, " \r\n"); + buf[len] = '\0'; return strdup(buf); } From 8d540e83c707e249a72a88ef939a5b8395e36e87 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 6 Feb 2022 12:18:17 +0100 Subject: [PATCH 1077/2244] Fix adb get-serialno parsing The function assumed that the raw output of "adb get-serialno" was a NUL-terminated string, but it is not the case. It this output did not end with a space or a new line character, then sc_str_truncate() would write '\0' over the last character. Even worse, if the output was empty, then sc_str_truncate() would write out-of-bounds. Avoid the error-prone sc_str_truncate() util function. --- app/src/adb/adb.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/src/adb/adb.c b/app/src/adb/adb.c index 2fd4b35d..11c1b298 100644 --- a/app/src/adb/adb.c +++ b/app/src/adb/adb.c @@ -417,7 +417,7 @@ sc_adb_get_serialno(struct sc_intr *intr, unsigned flags) { } char buf[128]; - ssize_t r = sc_pipe_read_all_intr(intr, pid, pout, buf, sizeof(buf)); + ssize_t r = sc_pipe_read_all_intr(intr, pid, pout, buf, sizeof(buf) - 1); sc_pipe_close(pout); bool ok = process_check_success_intr(intr, pid, "adb get-serialno", flags); @@ -429,7 +429,10 @@ sc_adb_get_serialno(struct sc_intr *intr, unsigned flags) { return NULL; } - sc_str_truncate(buf, r, " \r\n"); + assert((size_t) r < sizeof(buf)); + buf[r] = '\0'; + size_t len = strcspn(buf, " \r\n"); + buf[len] = '\0'; return strdup(buf); } From 6d41c53b61dbd433a094d4b33e86c83e12e3399b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 6 Feb 2022 12:22:17 +0100 Subject: [PATCH 1078/2244] Fix adb connect parsing The function assumed that the raw output of "adb connect" was a NUL-terminated string, but it is not the case. It this output did not end with a space or a new line character, then sc_str_truncate() would write '\0' over the last character. Even worse, if the output was empty, then sc_str_truncate() would write out-of-bounds. Avoid the error-prone sc_str_truncate() util function. --- app/src/adb/adb.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/src/adb/adb.c b/app/src/adb/adb.c index 11c1b298..3cfc5a95 100644 --- a/app/src/adb/adb.c +++ b/app/src/adb/adb.c @@ -339,7 +339,7 @@ sc_adb_connect(struct sc_intr *intr, const char *ip_port, unsigned flags) { // case of failure. As a workaround, check if its output starts with // "connected". char buf[128]; - ssize_t r = sc_pipe_read_all_intr(intr, pid, pout, buf, sizeof(buf)); + ssize_t r = sc_pipe_read_all_intr(intr, pid, pout, buf, sizeof(buf) - 1); sc_pipe_close(pout); bool ok = process_check_success_intr(intr, pid, "adb connect", flags); @@ -351,11 +351,15 @@ sc_adb_connect(struct sc_intr *intr, const char *ip_port, unsigned flags) { return false; } + assert((size_t) r < sizeof(buf)); + buf[r] = '\0'; + ok = !strncmp("connected", buf, sizeof("connected") - 1); if (!ok && !(flags & SC_ADB_NO_STDERR)) { // "adb connect" also prints errors to stdout. Since we capture it, // re-print the error to stderr. - sc_str_truncate(buf, r, "\r\n"); + size_t len = strcspn(buf, "\r\n"); + buf[len] = '\0'; fprintf(stderr, "%s\n", buf); } return ok; From 137d2c97914fd9ac4b79869185c94c402d7719ab Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 6 Feb 2022 12:24:40 +0100 Subject: [PATCH 1079/2244] Remove confusing sc_str_truncate() This util function was error-prone: - it accepted a buffer as parameter (not necessarily a NUL-terminated string) and its length (including the NUL char, if any); - it wrote '\0' over the last character of the buffer, so the last character was lost if the buffer was not a NUL-terminated string, and even worse, it caused undefined behavior if the length was empty; - it returned the length of the resulting NUL-terminated string, which was inconsistent with the input buffer length. In addition, it was not necessarily optimal: - it wrote '\0' twice; - it required to know the buffer length, that is the input string length + 1, in advance. Remove this function, and let the client use strcspn() manually. --- app/src/util/str.c | 8 -------- app/src/util/str.h | 11 ----------- app/tests/test_str.c | 27 --------------------------- 3 files changed, 46 deletions(-) diff --git a/app/src/util/str.c b/app/src/util/str.c index 2d67f816..d78aa9d7 100644 --- a/app/src/util/str.c +++ b/app/src/util/str.c @@ -297,14 +297,6 @@ error: return NULL; } -size_t -sc_str_truncate(char *data, size_t len, const char *endchars) { - data[len - 1] = '\0'; - size_t idx = strcspn(data, endchars); - data[idx] = '\0'; - return idx; -} - ssize_t sc_str_index_of_column(const char *s, unsigned col, const char *seps) { size_t colidx = 0; diff --git a/app/src/util/str.h b/app/src/util/str.h index dfe0cb30..1736bd95 100644 --- a/app/src/util/str.h +++ b/app/src/util/str.h @@ -103,17 +103,6 @@ sc_str_from_wchars(const wchar_t *s); char * sc_str_wrap_lines(const char *input, unsigned columns, unsigned indent); -/** - * Truncate the data after any of the characters from `endchars` - * - * An '\0' is always written at the end of the data string, even if no - * character from `endchars` is encountered. - * - * Return the size of the resulting string (as strlen() would return). - */ -size_t -sc_str_truncate(char *data, size_t len, const char *endchars); - /** * Find the start of a column in a string * diff --git a/app/tests/test_str.c b/app/tests/test_str.c index cc3039e7..4fe8a1df 100644 --- a/app/tests/test_str.c +++ b/app/tests/test_str.c @@ -338,32 +338,6 @@ static void test_wrap_lines(void) { free(formatted); } -static void test_truncate(void) { - char s[] = "hello\nworld\n!"; - size_t len = sc_str_truncate(s, sizeof(s), "\n"); - - assert(len == 5); - assert(!strcmp("hello", s)); - - char s2[] = "hello\r\nworkd\r\n!"; - len = sc_str_truncate(s2, sizeof(s2), "\n\r"); - - assert(len == 5); - assert(!strcmp("hello", s)); - - char s3[] = "hello world\n!"; - len = sc_str_truncate(s3, sizeof(s3), " \n\r"); - - assert(len == 5); - assert(!strcmp("hello", s3)); - - char s4[] = "hello "; - len = sc_str_truncate(s4, sizeof(s4), " \n\r"); - - assert(len == 5); - assert(!strcmp("hello", s4)); -} - static void test_index_of_column(void) { assert(sc_str_index_of_column("a bc d", 0, " ") == 0); assert(sc_str_index_of_column("a bc d", 1, " ") == 2); @@ -417,7 +391,6 @@ int main(int argc, char *argv[]) { test_parse_integer_with_suffix(); test_strlist_contains(); test_wrap_lines(); - test_truncate(); test_index_of_column(); test_remove_trailing_cr(); return 0; From b0e04aa327141937f47175a86a10efc5cb11da8d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 7 Feb 2022 12:56:25 +0100 Subject: [PATCH 1080/2244] Remove log_libusb_error() This helper did not help a lot, and prevented the client to choose the log level and the prefix error message. --- app/src/usb/aoa_hid.c | 13 ++++--------- app/src/usb/usb.c | 16 +++++----------- 2 files changed, 9 insertions(+), 20 deletions(-) diff --git a/app/src/usb/aoa_hid.c b/app/src/usb/aoa_hid.c index 6e3bd2e7..ca10934a 100644 --- a/app/src/usb/aoa_hid.c +++ b/app/src/usb/aoa_hid.c @@ -45,11 +45,6 @@ sc_hid_event_destroy(struct sc_hid_event *hid_event) { free(hid_event->buffer); } -static inline void -log_libusb_error(enum libusb_error errcode) { - LOGW("libusb error: %s", libusb_strerror(errcode)); -} - bool sc_aoa_init(struct sc_aoa *aoa, struct sc_usb *usb, struct sc_acksync *acksync) { @@ -99,7 +94,7 @@ sc_aoa_register_hid(struct sc_aoa *aoa, uint16_t accessory_id, request, value, index, buffer, length, DEFAULT_TIMEOUT); if (result < 0) { - log_libusb_error((enum libusb_error) result); + LOGE("REGISTER_HID: libusb error: %s", libusb_strerror(result)); return false; } @@ -135,7 +130,7 @@ sc_aoa_set_hid_report_desc(struct sc_aoa *aoa, uint16_t accessory_id, request, value, index, buffer, length, DEFAULT_TIMEOUT); if (result < 0) { - log_libusb_error((enum libusb_error) result); + LOGE("SET_HID_REPORT_DESC: libusb error: %s", libusb_strerror(result)); return false; } @@ -177,7 +172,7 @@ sc_aoa_send_hid_event(struct sc_aoa *aoa, const struct sc_hid_event *event) { request, value, index, buffer, length, DEFAULT_TIMEOUT); if (result < 0) { - log_libusb_error((enum libusb_error) result); + LOGE("SEND_HID_EVENT: libusb error: %s", libusb_strerror(result)); return false; } @@ -199,7 +194,7 @@ sc_aoa_unregister_hid(struct sc_aoa *aoa, const uint16_t accessory_id) { request, value, index, buffer, length, DEFAULT_TIMEOUT); if (result < 0) { - log_libusb_error((enum libusb_error) result); + LOGE("UNREGISTER_HID: libusb error: %s", libusb_strerror(result)); return false; } diff --git a/app/src/usb/usb.c b/app/src/usb/usb.c index 729df7f0..143bca04 100644 --- a/app/src/usb/usb.c +++ b/app/src/usb/usb.c @@ -4,11 +4,6 @@ #include "util/log.h" -static inline void -log_libusb_error(enum libusb_error errcode) { - LOGW("libusb error: %s", libusb_strerror(errcode)); -} - static char * read_string(libusb_device_handle *handle, uint8_t desc_index) { char buffer[128]; @@ -96,7 +91,7 @@ sc_usb_find_devices(struct sc_usb *usb, const char *serial, libusb_device **list; ssize_t count = libusb_get_device_list(usb->context, &list); if (count < 0) { - log_libusb_error((enum libusb_error) count); + LOGE("List USB devices: libusb error: %s", libusb_strerror(count)); return -1; } @@ -118,7 +113,7 @@ sc_usb_open_handle(libusb_device *device) { libusb_device_handle *handle; int result = libusb_open(device, &handle); if (result < 0) { - log_libusb_error((enum libusb_error) result); + LOGE("Open device: libusb error: %s", libusb_strerror(result)); return NULL; } return handle; @@ -183,8 +178,7 @@ sc_usb_register_callback(struct sc_usb *usb) { struct libusb_device_descriptor desc; int result = libusb_get_device_descriptor(device, &desc); if (result < 0) { - log_libusb_error((enum libusb_error) result); - LOGW("Could not read USB device descriptor"); + LOGE("Device descriptor: libusb error: %s", libusb_strerror(result)); return false; } @@ -198,8 +192,8 @@ sc_usb_register_callback(struct sc_usb *usb) { sc_usb_libusb_callback, usb, &usb->callback_handle); if (result < 0) { - log_libusb_error((enum libusb_error) result); - LOGW("Could not register USB callback"); + LOGE("Register hotplog callback: libusb error: %s", + libusb_strerror(result)); return false; } From b60809a4da23f52c1efaebc2813d3a45d97a8ca5 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 7 Feb 2022 13:01:49 +0100 Subject: [PATCH 1081/2244] Inline USB device opening Such a separate function was useless. --- app/src/usb/usb.c | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/app/src/usb/usb.c b/app/src/usb/usb.c index 143bca04..1b5f4a5d 100644 --- a/app/src/usb/usb.c +++ b/app/src/usb/usb.c @@ -108,17 +108,6 @@ sc_usb_find_devices(struct sc_usb *usb, const char *serial, return idx; } -static libusb_device_handle * -sc_usb_open_handle(libusb_device *device) { - libusb_device_handle *handle; - int result = libusb_open(device, &handle); - if (result < 0) { - LOGE("Open device: libusb error: %s", libusb_strerror(result)); - return NULL; - } - return handle; - } - bool sc_usb_init(struct sc_usb *usb) { usb->handle = NULL; @@ -204,8 +193,9 @@ sc_usb_register_callback(struct sc_usb *usb) { bool sc_usb_connect(struct sc_usb *usb, libusb_device *device, const struct sc_usb_callbacks *cbs, void *cbs_userdata) { - usb->handle = sc_usb_open_handle(device); - if (!usb->handle) { + int result = libusb_open(device, &usb->handle); + if (result < 0) { + LOGE("Open device: libusb error: %s", libusb_strerror(result)); return false; } From f20137d2ac524dd9dc0c7073fbc0f5f4e266b371 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 7 Feb 2022 13:10:53 +0100 Subject: [PATCH 1082/2244] Improve USB device open log For consistency with "List USB devices", log "Open USB device". --- app/src/usb/usb.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/usb/usb.c b/app/src/usb/usb.c index 1b5f4a5d..f2dab658 100644 --- a/app/src/usb/usb.c +++ b/app/src/usb/usb.c @@ -195,7 +195,7 @@ sc_usb_connect(struct sc_usb *usb, libusb_device *device, const struct sc_usb_callbacks *cbs, void *cbs_userdata) { int result = libusb_open(device, &usb->handle); if (result < 0) { - LOGE("Open device: libusb error: %s", libusb_strerror(result)); + LOGE("Open USB device: libusb error: %s", libusb_strerror(result)); return false; } From 61b6324ee902f48da4c4e6a76b3123e8768b4a9f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 5 Feb 2022 14:06:03 +0100 Subject: [PATCH 1083/2244] Remove LOGC() It is not clear when to use LOGC() rather than LOGE(). Always use LOGE(). Moreover, enum sc_log_level has no "critical" log level. --- app/src/controller.c | 2 +- app/src/demuxer.c | 2 +- app/src/file_pusher.c | 2 +- app/src/receiver.c | 2 +- app/src/recorder.c | 2 +- app/src/scrcpy.c | 4 ++-- app/src/screen.c | 8 ++++---- app/src/sys/unix/process.c | 2 +- app/src/usb/aoa_hid.c | 2 +- app/src/usb/scrcpy_otg.c | 2 +- app/src/util/log.h | 3 +-- app/src/util/net.c | 2 +- app/src/util/thread.c | 12 ++++++------ app/src/v4l2_sink.c | 2 +- 14 files changed, 23 insertions(+), 24 deletions(-) diff --git a/app/src/controller.c b/app/src/controller.c index 626a5e30..cdf53edb 100644 --- a/app/src/controller.c +++ b/app/src/controller.c @@ -113,7 +113,7 @@ sc_controller_start(struct sc_controller *controller) { bool ok = sc_thread_create(&controller->thread, run_controller, "scrcpy-ctl", controller); if (!ok) { - LOGC("Could not start controller thread"); + LOGE("Could not start controller thread"); return false; } diff --git a/app/src/demuxer.c b/app/src/demuxer.c index c683dfe0..bebdb6d6 100644 --- a/app/src/demuxer.c +++ b/app/src/demuxer.c @@ -276,7 +276,7 @@ sc_demuxer_start(struct sc_demuxer *demuxer) { bool ok = sc_thread_create(&demuxer->thread, run_demuxer, "scrcpy-demuxer", demuxer); if (!ok) { - LOGC("Could not start demuxer thread"); + LOGE("Could not start demuxer thread"); return false; } return true; diff --git a/app/src/file_pusher.c b/app/src/file_pusher.c index cbbeb2d7..f6757870 100644 --- a/app/src/file_pusher.c +++ b/app/src/file_pusher.c @@ -156,7 +156,7 @@ sc_file_pusher_start(struct sc_file_pusher *fp) { bool ok = sc_thread_create(&fp->thread, run_file_pusher, "scrcpy-file", fp); if (!ok) { - LOGC("Could not start file_pusher thread"); + LOGE("Could not start file_pusher thread"); return false; } diff --git a/app/src/receiver.c b/app/src/receiver.c index 1e25536e..0376948d 100644 --- a/app/src/receiver.c +++ b/app/src/receiver.c @@ -114,7 +114,7 @@ receiver_start(struct receiver *receiver) { bool ok = sc_thread_create(&receiver->thread, run_receiver, "scrcpy-receiver", receiver); if (!ok) { - LOGC("Could not start receiver thread"); + LOGE("Could not start receiver thread"); return false; } diff --git a/app/src/recorder.c b/app/src/recorder.c index 2a82e172..b14b6050 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -290,7 +290,7 @@ sc_recorder_open(struct sc_recorder *recorder, const AVCodec *input_codec) { ok = sc_thread_create(&recorder->thread, run_recorder, "scrcpy-recorder", recorder); if (!ok) { - LOGC("Could not start recorder thread"); + LOGE("Could not start recorder thread"); goto error_avio_close; } diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index c846ee18..dc4ac25a 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -269,7 +269,7 @@ scrcpy(struct scrcpy_options *options) { // Minimal SDL initialization if (SDL_Init(SDL_INIT_EVENTS)) { - LOGC("Could not initialize SDL: %s", SDL_GetError()); + LOGE("Could not initialize SDL: %s", SDL_GetError()); return false; } @@ -341,7 +341,7 @@ scrcpy(struct scrcpy_options *options) { // Initialize SDL video in addition if display is enabled if (options->display && SDL_Init(SDL_INIT_VIDEO)) { - LOGC("Could not initialize SDL: %s", SDL_GetError()); + LOGE("Could not initialize SDL: %s", SDL_GetError()); goto end; } diff --git a/app/src/screen.c b/app/src/screen.c index c62920bc..98626909 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -423,14 +423,14 @@ sc_screen_init(struct sc_screen *screen, screen->window = SDL_CreateWindow(params->window_title, 0, 0, 0, 0, window_flags); if (!screen->window) { - LOGC("Could not create window: %s", SDL_GetError()); + LOGE("Could not create window: %s", SDL_GetError()); goto error_destroy_fps_counter; } screen->renderer = SDL_CreateRenderer(screen->window, -1, SDL_RENDERER_ACCELERATED); if (!screen->renderer) { - LOGC("Could not create renderer: %s", SDL_GetError()); + LOGE("Could not create renderer: %s", SDL_GetError()); goto error_destroy_window; } @@ -479,7 +479,7 @@ sc_screen_init(struct sc_screen *screen, params->frame_size.height); screen->texture = create_texture(screen); if (!screen->texture) { - LOGC("Could not create texture: %s", SDL_GetError()); + LOGE("Could not create texture: %s", SDL_GetError()); goto error_destroy_renderer; } @@ -666,7 +666,7 @@ prepare_for_frame(struct sc_screen *screen, struct sc_size new_frame_size) { screen->frame_size.width, screen->frame_size.height); screen->texture = create_texture(screen); if (!screen->texture) { - LOGC("Could not create texture: %s", SDL_GetError()); + LOGE("Could not create texture: %s", SDL_GetError()); return false; } } diff --git a/app/src/sys/unix/process.c b/app/src/sys/unix/process.c index cb82f3a9..cef227ed 100644 --- a/app/src/sys/unix/process.c +++ b/app/src/sys/unix/process.c @@ -176,7 +176,7 @@ sc_process_execute_p(const char *const argv[], sc_pid *pid, unsigned flags, bool sc_process_terminate(pid_t pid) { if (pid <= 0) { - LOGC("Requested to kill %d, this is an error. Please report the bug.\n", + LOGE("Requested to kill %d, this is an error. Please report the bug.\n", (int) pid); abort(); } diff --git a/app/src/usb/aoa_hid.c b/app/src/usb/aoa_hid.c index ca10934a..57296bfc 100644 --- a/app/src/usb/aoa_hid.c +++ b/app/src/usb/aoa_hid.c @@ -278,7 +278,7 @@ sc_aoa_start(struct sc_aoa *aoa) { bool ok = sc_thread_create(&aoa->thread, run_aoa_thread, "scrcpy-aoa", aoa); if (!ok) { - LOGC("Could not start AOA thread"); + LOGE("Could not start AOA thread"); return false; } diff --git a/app/src/usb/scrcpy_otg.c b/app/src/usb/scrcpy_otg.c index e27a3605..e39ab27a 100644 --- a/app/src/usb/scrcpy_otg.c +++ b/app/src/usb/scrcpy_otg.c @@ -56,7 +56,7 @@ scrcpy_otg(struct scrcpy_options *options) { // Minimal SDL initialization if (SDL_Init(SDL_INIT_EVENTS)) { - LOGC("Could not initialize SDL: %s", SDL_GetError()); + LOGE("Could not initialize SDL: %s", SDL_GetError()); return false; } diff --git a/app/src/util/log.h b/app/src/util/log.h index e3efdbe5..94787347 100644 --- a/app/src/util/log.h +++ b/app/src/util/log.h @@ -15,10 +15,9 @@ #define LOGI(...) SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__) #define LOGW(...) SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__) #define LOGE(...) SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__) -#define LOGC(...) SDL_LogCritical(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__) #define LOG_OOM() \ - LOGC("OOM: %s:%d %s()", __FILE__, __LINE__, __func__) + LOGE("OOM: %s:%d %s()", __FILE__, __LINE__, __func__) void sc_set_log_level(enum sc_log_level level); diff --git a/app/src/util/net.c b/app/src/util/net.c index b18566be..b1aa7445 100644 --- a/app/src/util/net.c +++ b/app/src/util/net.c @@ -33,7 +33,7 @@ net_init(void) { WSADATA wsa; int res = WSAStartup(MAKEWORD(2, 2), &wsa) < 0; if (res < 0) { - LOGC("WSAStartup failed with error %d", res); + LOGE("WSAStartup failed with error %d", res); return false; } #endif diff --git a/app/src/util/thread.c b/app/src/util/thread.c index c6e6b81e..0d098b3f 100644 --- a/app/src/util/thread.c +++ b/app/src/util/thread.c @@ -54,7 +54,7 @@ sc_mutex_lock(sc_mutex *mutex) { int r = SDL_LockMutex(mutex->mutex); #ifndef NDEBUG if (r) { - LOGC("Could not lock mutex: %s", SDL_GetError()); + LOGE("Could not lock mutex: %s", SDL_GetError()); abort(); } @@ -74,7 +74,7 @@ sc_mutex_unlock(sc_mutex *mutex) { int r = SDL_UnlockMutex(mutex->mutex); #ifndef NDEBUG if (r) { - LOGC("Could not lock mutex: %s", SDL_GetError()); + LOGE("Could not lock mutex: %s", SDL_GetError()); abort(); } #else @@ -118,7 +118,7 @@ sc_cond_wait(sc_cond *cond, sc_mutex *mutex) { int r = SDL_CondWait(cond->cond, mutex->mutex); #ifndef NDEBUG if (r) { - LOGC("Could not wait on condition: %s", SDL_GetError()); + LOGE("Could not wait on condition: %s", SDL_GetError()); abort(); } @@ -140,7 +140,7 @@ sc_cond_timedwait(sc_cond *cond, sc_mutex *mutex, sc_tick deadline) { int r = SDL_CondWaitTimeout(cond->cond, mutex->mutex, ms); #ifndef NDEBUG if (r < 0) { - LOGC("Could not wait on condition with timeout: %s", SDL_GetError()); + LOGE("Could not wait on condition with timeout: %s", SDL_GetError()); abort(); } @@ -156,7 +156,7 @@ sc_cond_signal(sc_cond *cond) { int r = SDL_CondSignal(cond->cond); #ifndef NDEBUG if (r) { - LOGC("Could not signal a condition: %s", SDL_GetError()); + LOGE("Could not signal a condition: %s", SDL_GetError()); abort(); } #else @@ -169,7 +169,7 @@ sc_cond_broadcast(sc_cond *cond) { int r = SDL_CondBroadcast(cond->cond); #ifndef NDEBUG if (r) { - LOGC("Could not broadcast a condition: %s", SDL_GetError()); + LOGE("Could not broadcast a condition: %s", SDL_GetError()); abort(); } #else diff --git a/app/src/v4l2_sink.c b/app/src/v4l2_sink.c index 7675fd92..9a0011f2 100644 --- a/app/src/v4l2_sink.c +++ b/app/src/v4l2_sink.c @@ -274,7 +274,7 @@ sc_v4l2_sink_open(struct sc_v4l2_sink *vs) { LOGD("Starting v4l2 thread"); ok = sc_thread_create(&vs->thread, run_v4l2_sink, "scrcpy-v4l2", vs); if (!ok) { - LOGC("Could not start v4l2 thread"); + LOGE("Could not start v4l2 thread"); goto error_av_packet_free; } From 6df2205cf3450feb3023990fecc7aba8d2982911 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 5 Feb 2022 14:18:12 +0100 Subject: [PATCH 1084/2244] Add generic LOG() macro with level parameter One log macro was provided for each log level (LOGV(), LOGD(), LOGI(), LOGW(), LOGE()). Add a generic macro LOG(LEVEL, ...) accepting a log level as parameter, so that it is possible to write logging wrappers. PR #3005 --- app/src/util/log.c | 10 ++++++++++ app/src/util/log.h | 4 ++++ 2 files changed, 14 insertions(+) diff --git a/app/src/util/log.c b/app/src/util/log.c index d8d7cd70..72cd2877 100644 --- a/app/src/util/log.c +++ b/app/src/util/log.c @@ -55,6 +55,16 @@ sc_get_log_level(void) { return log_level_sdl_to_sc(sdl_log); } +void +sc_log(enum sc_log_level level, const char *fmt, ...) { + SDL_LogPriority sdl_level = log_level_sc_to_sdl(level); + + va_list ap; + va_start(ap, fmt); + SDL_LogMessageV(SDL_LOG_CATEGORY_APPLICATION, sdl_level, fmt, ap); + va_end(ap); +} + #ifdef _WIN32 bool sc_log_windows_error(const char *prefix, int error) { diff --git a/app/src/util/log.h b/app/src/util/log.h index 94787347..6bd8506c 100644 --- a/app/src/util/log.h +++ b/app/src/util/log.h @@ -25,6 +25,10 @@ sc_set_log_level(enum sc_log_level level); enum sc_log_level sc_get_log_level(void); +void +sc_log(enum sc_log_level level, const char *fmt, ...); +#define LOG(LEVEL, ...) sc_log((LEVEL), __VA_ARGS__) + #ifdef _WIN32 // Log system error (typically returned by GetLastError() or similar) bool From 0eadf95a3e8382ad6f5aceb94eba9f1e2d4780e0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 5 Feb 2022 18:09:02 +0100 Subject: [PATCH 1085/2244] Rename function to destroy a list of USB devices Rename from "usb_device_" to "usb_devices_". PR #3005 --- app/src/scrcpy.c | 2 +- app/src/usb/scrcpy_otg.c | 2 +- app/src/usb/usb.c | 2 +- app/src/usb/usb.h | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index dc4ac25a..92159bba 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -442,7 +442,7 @@ scrcpy(struct scrcpy_options *options) { if (count > 1) { LOGE("Multiple (%d) devices with serial %s", (int) count, serial); - sc_usb_device_destroy_all(usb_devices, count); + sc_usb_devices_destroy_all(usb_devices, count); sc_usb_destroy(&s->usb); sc_acksync_destroy(&s->acksync); goto aoa_hid_end; diff --git a/app/src/usb/scrcpy_otg.c b/app/src/usb/scrcpy_otg.c index e39ab27a..34c16ce1 100644 --- a/app/src/usb/scrcpy_otg.c +++ b/app/src/usb/scrcpy_otg.c @@ -115,7 +115,7 @@ scrcpy_otg(struct scrcpy_options *options) { if (!serial) { LOGE("Specify the device via -s or --serial"); } - sc_usb_device_destroy_all(usb_devices, count); + sc_usb_devices_destroy_all(usb_devices, count); goto end; } usb_device_initialized = true; diff --git a/app/src/usb/usb.c b/app/src/usb/usb.c index f2dab658..d705f647 100644 --- a/app/src/usb/usb.c +++ b/app/src/usb/usb.c @@ -79,7 +79,7 @@ sc_usb_device_destroy(struct sc_usb_device *usb_device) { } void -sc_usb_device_destroy_all(struct sc_usb_device *usb_devices, size_t count) { +sc_usb_devices_destroy_all(struct sc_usb_device *usb_devices, size_t count) { for (size_t i = 0; i < count; ++i) { sc_usb_device_destroy(&usb_devices[i]); } diff --git a/app/src/usb/usb.h b/app/src/usb/usb.h index eda7c2f9..a8810b90 100644 --- a/app/src/usb/usb.h +++ b/app/src/usb/usb.h @@ -41,7 +41,7 @@ void sc_usb_device_destroy(struct sc_usb_device *usb_device); void -sc_usb_device_destroy_all(struct sc_usb_device *usb_devices, size_t count); +sc_usb_devices_destroy_all(struct sc_usb_device *usb_devices, size_t count); bool sc_usb_init(struct sc_usb *usb); From 8c50342fb28b71f3632ad53903dc10920f0fb1d8 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 5 Feb 2022 18:55:10 +0100 Subject: [PATCH 1086/2244] Move SC_PRIsizet to compat.h Define the printf format macro for size_t in compat.h so that it can be used from anywhere. PR #3005 --- app/src/compat.h | 2 ++ app/src/util/process.h | 3 --- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/src/compat.h b/app/src/compat.h index 311d617d..62435718 100644 --- a/app/src/compat.h +++ b/app/src/compat.h @@ -8,8 +8,10 @@ #ifndef __WIN32 # define PRIu64_ PRIu64 +# define SC_PRIsizet "zu" #else # define PRIu64_ "I64u" // Windows... +# define SC_PRIsizet "Iu" #endif // In ffmpeg/doc/APIchanges: diff --git a/app/src/util/process.h b/app/src/util/process.h index 90920620..4d9d1684 100644 --- a/app/src/util/process.h +++ b/app/src/util/process.h @@ -12,8 +12,6 @@ # include # include # define SC_PRIexitcode "lu" -// -# define SC_PRIsizet "Iu" # define SC_PROCESS_NONE NULL # define SC_EXIT_CODE_NONE -1UL // max value as unsigned long typedef HANDLE sc_pid; @@ -23,7 +21,6 @@ #else # include -# define SC_PRIsizet "zu" # define SC_PRIexitcode "d" # define SC_PROCESS_NONE -1 # define SC_EXIT_CODE_NONE -1 From b88c4aa75e73c0bbe981dd8f5e986c67d18abe22 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 5 Feb 2022 19:07:53 +0100 Subject: [PATCH 1087/2244] Add move-function for sc_usb_device Add a function to "move" a sc_usb_device into another instance. This will avoid unnecessary copies. PR #3005 --- app/src/usb/usb.c | 13 ++++++++++++- app/src/usb/usb.h | 12 ++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/app/src/usb/usb.c b/app/src/usb/usb.c index d705f647..cc9c78cc 100644 --- a/app/src/usb/usb.c +++ b/app/src/usb/usb.c @@ -72,12 +72,23 @@ accept_device(libusb_device *device, const char *serial, void sc_usb_device_destroy(struct sc_usb_device *usb_device) { - libusb_unref_device(usb_device->device); + if (usb_device->device) { + libusb_unref_device(usb_device->device); + } free(usb_device->serial); free(usb_device->manufacturer); free(usb_device->product); } +void +sc_usb_device_move(struct sc_usb_device *dst, struct sc_usb_device *src) { + *dst = *src; + src->device = NULL; + src->serial = NULL; + src->manufacturer = NULL; + src->product = NULL; +} + void sc_usb_devices_destroy_all(struct sc_usb_device *usb_devices, size_t count) { for (size_t i = 0; i < count; ++i) { diff --git a/app/src/usb/usb.h b/app/src/usb/usb.h index a8810b90..bed41cd6 100644 --- a/app/src/usb/usb.h +++ b/app/src/usb/usb.h @@ -40,6 +40,18 @@ struct sc_usb_device { void sc_usb_device_destroy(struct sc_usb_device *usb_device); +/** + * Move src to dst + * + * After this call, the content of src is undefined, except that + * sc_usb_device_destroy() can be called. + * + * This is useful to take a device from a list that will be destroyed, without + * making unnecessary copies. + */ +void +sc_usb_device_move(struct sc_usb_device *dst, struct sc_usb_device *src); + void sc_usb_devices_destroy_all(struct sc_usb_device *usb_devices, size_t count); From 61969aeb80093d0777c7716a61698cbdaf9ddd71 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 5 Feb 2022 19:12:59 +0100 Subject: [PATCH 1088/2244] Expose simple API to select a single USB device The caller just wants a single device. Handle all cases and error messages internally. PR #3005 --- app/src/scrcpy.c | 27 ++++++--------------- app/src/usb/scrcpy_otg.c | 51 +++++++--------------------------------- app/src/usb/usb.c | 45 ++++++++++++++++++++++++++++++++++- app/src/usb/usb.h | 6 ++--- 4 files changed, 63 insertions(+), 66 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 92159bba..d0aa2627 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -430,32 +430,19 @@ scrcpy(struct scrcpy_options *options) { } assert(serial); - struct sc_usb_device usb_devices[16]; - ssize_t count = sc_usb_find_devices(&s->usb, serial, usb_devices, - ARRAY_LEN(usb_devices)); - if (count <= 0) { - LOGE("Could not find USB device %s", serial); + struct sc_usb_device usb_device; + ok = sc_usb_select_device(&s->usb, serial, &usb_device); + if (!ok) { sc_usb_destroy(&s->usb); - sc_acksync_destroy(&s->acksync); goto aoa_hid_end; } - if (count > 1) { - LOGE("Multiple (%d) devices with serial %s", (int) count, serial); - sc_usb_devices_destroy_all(usb_devices, count); - sc_usb_destroy(&s->usb); - sc_acksync_destroy(&s->acksync); - goto aoa_hid_end; - } - - struct sc_usb_device *usb_device = &usb_devices[0]; - LOGI("USB device: %s (%04" PRIx16 ":%04" PRIx16 ") %s %s", - usb_device->serial, usb_device->vid, usb_device->pid, - usb_device->manufacturer, usb_device->product); + usb_device.serial, usb_device.vid, usb_device.pid, + usb_device.manufacturer, usb_device.product); - ok = sc_usb_connect(&s->usb, usb_device->device, NULL, NULL); - sc_usb_device_destroy(usb_device); + ok = sc_usb_connect(&s->usb, usb_device.device, NULL, NULL); + sc_usb_device_destroy(&usb_device); if (!ok) { LOGE("Failed to connect to USB device %s", serial); sc_usb_destroy(&s->usb); diff --git a/app/src/usb/scrcpy_otg.c b/app/src/usb/scrcpy_otg.c index 34c16ce1..9a7e3fe6 100644 --- a/app/src/usb/scrcpy_otg.c +++ b/app/src/usb/scrcpy_otg.c @@ -83,50 +83,17 @@ scrcpy_otg(struct scrcpy_options *options) { return false; } - struct sc_usb_device usb_devices[16]; - ssize_t count = sc_usb_find_devices(&s->usb, serial, usb_devices, - ARRAY_LEN(usb_devices)); - if (count < 0) { - LOGE("Could not list USB devices"); + struct sc_usb_device usb_device; + ok = sc_usb_select_device(&s->usb, serial, &usb_device); + if (!ok) { goto end; } - if (count == 0) { - if (serial) { - LOGE("Could not find USB device %s", serial); - } else { - LOGE("Could not find any USB device"); - } - goto end; - } - - if (count > 1) { - if (serial) { - LOGE("Multiple (%d) USB devices with serial %s:", (int) count, - serial); - } else { - LOGE("Multiple (%d) USB devices:", (int) count); - } - for (size_t i = 0; i < (size_t) count; ++i) { - struct sc_usb_device *d = &usb_devices[i]; - LOGE(" %-18s (%04" PRIx16 ":%04" PRIx16 ") %s %s", - d->serial, d->vid, d->pid, d->manufacturer, d->product); - } - if (!serial) { - LOGE("Specify the device via -s or --serial"); - } - sc_usb_devices_destroy_all(usb_devices, count); - goto end; - } - usb_device_initialized = true; - - struct sc_usb_device *usb_device = &usb_devices[0]; - LOGI("USB device: %s (%04" PRIx16 ":%04" PRIx16 ") %s %s", - usb_device->serial, usb_device->vid, usb_device->pid, - usb_device->manufacturer, usb_device->product); + usb_device.serial, usb_device.vid, usb_device.pid, + usb_device.manufacturer, usb_device.product); - ok = sc_usb_connect(&s->usb, usb_device->device, &cbs, NULL); + ok = sc_usb_connect(&s->usb, usb_device.device, &cbs, NULL); if (!ok) { goto end; } @@ -173,7 +140,7 @@ scrcpy_otg(struct scrcpy_options *options) { const char *window_title = options->window_title; if (!window_title) { - window_title = usb_device->product ? usb_device->product : "scrcpy"; + window_title = usb_device.product ? usb_device.product : "scrcpy"; } struct sc_screen_otg_params params = { @@ -192,7 +159,7 @@ scrcpy_otg(struct scrcpy_options *options) { } // usb_device not needed anymore - sc_usb_device_destroy(usb_device); + sc_usb_device_destroy(&usb_device); usb_device_initialized = false; ret = event_loop(s); @@ -223,7 +190,7 @@ end: } if (usb_device_initialized) { - sc_usb_device_destroy(usb_device); + sc_usb_device_destroy(&usb_device); } sc_usb_destroy(&s->usb); diff --git a/app/src/usb/usb.c b/app/src/usb/usb.c index cc9c78cc..f1e008cd 100644 --- a/app/src/usb/usb.c +++ b/app/src/usb/usb.c @@ -96,7 +96,7 @@ sc_usb_devices_destroy_all(struct sc_usb_device *usb_devices, size_t count) { } } -ssize_t +static ssize_t sc_usb_find_devices(struct sc_usb *usb, const char *serial, struct sc_usb_device *devices, size_t len) { libusb_device **list; @@ -119,6 +119,49 @@ sc_usb_find_devices(struct sc_usb *usb, const char *serial, return idx; } +bool +sc_usb_select_device(struct sc_usb *usb, const char *serial, + struct sc_usb_device *out_device) { + struct sc_usb_device usb_devices[16]; + ssize_t count = sc_usb_find_devices(usb, serial, usb_devices, + ARRAY_LEN(usb_devices)); + if (count == -1) { + LOGE("Could not list USB devices"); + return false; + } + + if (count == 0) { + if (serial) { + LOGE("Could not find USB device %s", serial); + } else { + LOGE("Could not find any USB device"); + } + return false; + } + + if (count > 1) { + if (serial) { + LOGE("Multiple (%" SC_PRIsizet ") USB devices with serial %s:", + count, serial); + } else { + LOGE("Multiple (%" SC_PRIsizet ") USB devices:", count); + } + for (size_t i = 0; i < (size_t) count; ++i) { + struct sc_usb_device *d = &usb_devices[i]; + LOGE(" %-18s (%04" PRIx16 ":%04" PRIx16 ") %s %s", + d->serial, d->vid, d->pid, d->manufacturer, d->product); + } + LOGE("Select a device via -s (--serial)"); + sc_usb_devices_destroy_all(usb_devices, count); + return false; + } + + assert(count == 1); + // Move usb_devices[0] into out_device (do not destroy usb_devices[0]) + *out_device = usb_devices[0]; + return true; +} + bool sc_usb_init(struct sc_usb *usb) { usb->handle = NULL; diff --git a/app/src/usb/usb.h b/app/src/usb/usb.h index bed41cd6..f0b62daf 100644 --- a/app/src/usb/usb.h +++ b/app/src/usb/usb.h @@ -61,9 +61,9 @@ sc_usb_init(struct sc_usb *usb); void sc_usb_destroy(struct sc_usb *usb); -ssize_t -sc_usb_find_devices(struct sc_usb *usb, const char *serial, - struct sc_usb_device *devices, size_t len); +bool +sc_usb_select_device(struct sc_usb *usb, const char *serial, + struct sc_usb_device *out_device); bool sc_usb_connect(struct sc_usb *usb, libusb_device *device, From 700503df6c4df77867f6172bc7a344a56bb91481 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 5 Feb 2022 19:34:45 +0100 Subject: [PATCH 1089/2244] List and select USB devices separately List all USB devices in a first step, then select the matching one(s). This allows to report a user-friendly log message containing the list of devices, with the matching one(s) highlighted. PR #3005 --- app/src/usb/usb.c | 104 +++++++++++++++++++++++++++++++--------------- app/src/usb/usb.h | 1 + 2 files changed, 72 insertions(+), 33 deletions(-) diff --git a/app/src/usb/usb.c b/app/src/usb/usb.c index f1e008cd..aec263cd 100644 --- a/app/src/usb/usb.c +++ b/app/src/usb/usb.c @@ -25,8 +25,7 @@ read_string(libusb_device_handle *handle, uint8_t desc_index) { } static bool -accept_device(libusb_device *device, const char *serial, - struct sc_usb_device *out) { +sc_usb_read_device(libusb_device *device, struct sc_usb_device *out) { // Do not log any USB error in this function, it is expected that many USB // devices available on the computer have permission restrictions @@ -48,22 +47,13 @@ accept_device(libusb_device *device, const char *serial, return false; } - if (serial) { - // Filter by serial - bool matches = !strcmp(serial, device_serial); - if (!matches) { - free(device_serial); - libusb_close(handle); - return false; - } - } - out->device = libusb_ref_device(device); out->serial = device_serial; out->vid = desc.idVendor; out->pid = desc.idProduct; out->manufacturer = read_string(handle, desc.iManufacturer); out->product = read_string(handle, desc.iProduct); + out->selected = false; libusb_close(handle); @@ -97,8 +87,8 @@ sc_usb_devices_destroy_all(struct sc_usb_device *usb_devices, size_t count) { } static ssize_t -sc_usb_find_devices(struct sc_usb *usb, const char *serial, - struct sc_usb_device *devices, size_t len) { +sc_usb_list_devices(struct sc_usb *usb, struct sc_usb_device *devices, + size_t len) { libusb_device **list; ssize_t count = libusb_get_device_list(usb->context, &list); if (count < 0) { @@ -110,7 +100,7 @@ sc_usb_find_devices(struct sc_usb *usb, const char *serial, for (size_t i = 0; i < (size_t) count && idx < len; ++i) { libusb_device *device = list[i]; - if (accept_device(device, serial, &devices[idx])) { + if (sc_usb_read_device(device, &devices[idx])) { ++idx; } } @@ -119,46 +109,94 @@ sc_usb_find_devices(struct sc_usb *usb, const char *serial, return idx; } +static bool +sc_usb_accept_device(const struct sc_usb_device *device, const char *serial) { + if (!serial) { + return true; + } + + return !strcmp(serial, device->serial); +} + +static size_t +sc_usb_devices_select(struct sc_usb_device *devices, size_t len, + const char *serial, size_t *idx_out) { + size_t count = 0; + for (size_t i = 0; i < len; ++i) { + struct sc_usb_device *device = &devices[i]; + device->selected = sc_usb_accept_device(device, serial); + if (device->selected) { + if (idx_out && !count) { + *idx_out = i; + } + ++count; + } + } + + return count; +} + +static void +sc_usb_devices_log(enum sc_log_level level, struct sc_usb_device *devices, + size_t count) { + for (size_t i = 0; i < count; ++i) { + struct sc_usb_device *d = &devices[i]; + const char *selection = d->selected ? "-->" : " "; + LOG(level, " %s %-18s (%04" PRIx16 ":%04" PRIx16 ") %s %s", + selection, d->serial, d->vid, d->pid, d->manufacturer, d->product); + } +} + bool sc_usb_select_device(struct sc_usb *usb, const char *serial, struct sc_usb_device *out_device) { struct sc_usb_device usb_devices[16]; - ssize_t count = sc_usb_find_devices(usb, serial, usb_devices, - ARRAY_LEN(usb_devices)); + ssize_t count = + sc_usb_list_devices(usb, usb_devices, ARRAY_LEN(usb_devices)); if (count == -1) { LOGE("Could not list USB devices"); return false; } if (count == 0) { - if (serial) { - LOGE("Could not find USB device %s", serial); - } else { - LOGE("Could not find any USB device"); - } + LOGE("Could not find any USB device"); return false; } - if (count > 1) { + size_t sel_idx; // index of the single matching device if sel_count == 1 + size_t sel_count = + sc_usb_devices_select(usb_devices, count, serial, &sel_idx); + + if (sel_count == 0) { + // if count > 0 && sel_count == 0, then necessarily a serial is provided + assert(serial); + LOGE("Could not find USB device %s", serial); + sc_usb_devices_log(SC_LOG_LEVEL_ERROR, usb_devices, count); + sc_usb_devices_destroy_all(usb_devices, count); + return false; + } + + if (sel_count > 1) { if (serial) { LOGE("Multiple (%" SC_PRIsizet ") USB devices with serial %s:", - count, serial); + sel_count, serial); } else { - LOGE("Multiple (%" SC_PRIsizet ") USB devices:", count); - } - for (size_t i = 0; i < (size_t) count; ++i) { - struct sc_usb_device *d = &usb_devices[i]; - LOGE(" %-18s (%04" PRIx16 ":%04" PRIx16 ") %s %s", - d->serial, d->vid, d->pid, d->manufacturer, d->product); + LOGE("Multiple (%" SC_PRIsizet ") USB devices:", sel_count); } LOGE("Select a device via -s (--serial)"); sc_usb_devices_destroy_all(usb_devices, count); return false; } - assert(count == 1); - // Move usb_devices[0] into out_device (do not destroy usb_devices[0]) - *out_device = usb_devices[0]; + assert(sel_count == 1); // sel_idx is valid only if sel_count == 1 + struct sc_usb_device *device = &usb_devices[sel_idx]; + + LOGD("USB device found:"); + sc_usb_devices_log(SC_LOG_LEVEL_DEBUG, usb_devices, count); + + // Move device into out_device (do not destroy device) + sc_usb_device_move(out_device, device); + sc_usb_devices_destroy_all(usb_devices, count); return true; } diff --git a/app/src/usb/usb.h b/app/src/usb/usb.h index f0b62daf..d264a536 100644 --- a/app/src/usb/usb.h +++ b/app/src/usb/usb.h @@ -35,6 +35,7 @@ struct sc_usb_device { char *product; uint16_t vid; uint16_t pid; + bool selected; }; void From 85ff70fc95b9b6a10f3aa834db0d2d1efcbbce0a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 5 Feb 2022 21:20:12 +0100 Subject: [PATCH 1090/2244] Refactor device configuration Depending on the parameters passed to scrcpy, either the initial device serial is necessary or not. Reorganize to simplify the logic. PR #3005 --- app/src/server.c | 110 ++++++++++++++++++++++++++--------------------- 1 file changed, 60 insertions(+), 50 deletions(-) diff --git a/app/src/server.c b/app/src/server.c index e8ae32af..b0b596ad 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -641,51 +641,37 @@ sc_server_connect_to_tcpip(struct sc_server *server, const char *ip_port) { return true; } +static bool +sc_server_configure_tcpip_known_address(struct sc_server *server, + const char *addr) { + // Append ":5555" if no port is present + bool contains_port = strchr(addr, ':'); + char *ip_port = contains_port ? strdup(addr) : append_port_5555(addr); + if (!ip_port) { + LOG_OOM(); + return false; + } + + server->serial = ip_port; + return sc_server_connect_to_tcpip(server, ip_port); +} static bool -sc_server_configure_tcpip(struct sc_server *server) { - char *ip_port; +sc_server_configure_tcpip_unknown_address(struct sc_server *server, + const char *serial) { + // The serial is either the real serial when connected via USB, or + // the IP:PORT when connected over TCP/IP. Only the latter contains + // a colon. + bool is_already_tcpip = strchr(serial, ':'); + if (is_already_tcpip) { + // Nothing to do + LOGI("Device already connected via TCP/IP: %s", serial); + return true; + } - const struct sc_server_params *params = &server->params; - - // If tcpip parameter is given, then it must connect to this address. - // Therefore, the device is unknown, so serial is meaningless at this point. - assert(!params->req_serial || !params->tcpip_dst); - - if (params->tcpip_dst) { - // Append ":5555" if no port is present - bool contains_port = strchr(params->tcpip_dst, ':'); - ip_port = contains_port ? strdup(params->tcpip_dst) - : append_port_5555(params->tcpip_dst); - if (!ip_port) { - LOG_OOM(); - return false; - } - } else { - // The device IP address must be retrieved from the current - // connected device - char *serial = sc_server_read_serial(server); - if (!serial) { - LOGE("Could not get device serial"); - return false; - } - - // The serial is either the real serial when connected via USB, or - // the IP:PORT when connected over TCP/IP. Only the latter contains - // a colon. - bool is_already_tcpip = strchr(serial, ':'); - if (is_already_tcpip) { - // Nothing to do - LOGI("Device already connected via TCP/IP: %s", serial); - free(serial); - return true; - } - - ip_port = sc_server_switch_to_tcpip(server, serial); - free(serial); - if (!ip_port) { - return false; - } + char *ip_port = sc_server_switch_to_tcpip(server, serial); + if (!ip_port) { + return false; } server->serial = ip_port; @@ -698,16 +684,40 @@ run_server(void *data) { const struct sc_server_params *params = &server->params; - if (params->tcpip) { - bool ok = sc_server_configure_tcpip(server); - if (!ok) { + // params->tcpip_dst implies params->tcpip + assert(!params->tcpip_dst || params->tcpip); + + // If tcpip_dst parameter is given, then it must connect to this address. + // Therefore, the device is unknown, so serial is meaningless at this point. + assert(!params->req_serial || !params->tcpip_dst); + + // A device must be selected via a serial in all cases except when --tcpip= + // is called with a parameter (in that case, the device may initially not + // exist, and scrcpy will execute "adb connect"). + bool need_initial_serial = !params->tcpip_dst; + + bool ok; + if (need_initial_serial) { + char *serial = sc_server_read_serial(server); + if (!serial) { + LOGE("Could not get device serial"); goto error_connection_failed; } - assert(server->serial); + + if (params->tcpip) { + assert(!params->tcpip_dst); + ok = sc_server_configure_tcpip_unknown_address(server, serial); + free(serial); + if (!ok) { + goto error_connection_failed; + } + assert(server->serial); + } else { + server->serial = serial; + } } else { - server->serial = sc_server_read_serial(server); - if (!server->serial) { - LOGD("Could not get device serial"); + ok = sc_server_configure_tcpip_known_address(server, params->tcpip_dst); + if (!ok) { goto error_connection_failed; } } @@ -716,7 +726,7 @@ run_server(void *data) { assert(serial); LOGD("Device serial: %s", serial); - bool ok = push_server(&server->intr, serial); + ok = push_server(&server->intr, serial); if (!ok) { goto error_connection_failed; } From 4389de1c239c524f2a3aed2c985893dd3de74824 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 5 Feb 2022 22:32:40 +0100 Subject: [PATCH 1091/2244] Add adb devices parser Add a parser of `adb device -l` output, to extract a list of devices with their serial, state and model. PR #3005 --- app/meson.build | 2 + app/src/adb/adb_device.c | 26 ++++++ app/src/adb/adb_device.h | 33 ++++++++ app/src/adb/adb_parser.c | 159 +++++++++++++++++++++++++++++++++++- app/src/adb/adb_parser.h | 13 +++ app/tests/test_adb_parser.c | 159 ++++++++++++++++++++++++++++++++++++ 6 files changed, 391 insertions(+), 1 deletion(-) create mode 100644 app/src/adb/adb_device.c create mode 100644 app/src/adb/adb_device.h diff --git a/app/meson.build b/app/meson.build index b2c0bc55..a9b39b1e 100644 --- a/app/meson.build +++ b/app/meson.build @@ -1,6 +1,7 @@ src = [ 'src/main.c', 'src/adb/adb.c', + 'src/adb/adb_device.c', 'src/adb/adb_parser.c', 'src/adb/adb_tunnel.c', 'src/cli.c', @@ -221,6 +222,7 @@ if get_option('buildtype') == 'debug' tests = [ ['test_adb_parser', [ 'tests/test_adb_parser.c', + 'src/adb/adb_device.c', 'src/adb/adb_parser.c', 'src/util/str.c', 'src/util/strbuf.c', diff --git a/app/src/adb/adb_device.c b/app/src/adb/adb_device.c new file mode 100644 index 00000000..b6ff16a7 --- /dev/null +++ b/app/src/adb/adb_device.c @@ -0,0 +1,26 @@ +#include "adb_device.h" + +#include + +void +sc_adb_device_destroy(struct sc_adb_device *device) { + free(device->serial); + free(device->state); + free(device->model); +} + +void +sc_adb_device_move(struct sc_adb_device *dst, struct sc_adb_device *src) { + *dst = *src; + src->serial = NULL; + src->state = NULL; + src->model = NULL; +} + +void +sc_adb_devices_destroy_all(struct sc_adb_device *devices, size_t count) { + for (size_t i = 0; i < count; ++i) { + sc_adb_device_destroy(&devices[i]); + } +} + diff --git a/app/src/adb/adb_device.h b/app/src/adb/adb_device.h new file mode 100644 index 00000000..11b46c0c --- /dev/null +++ b/app/src/adb/adb_device.h @@ -0,0 +1,33 @@ +#ifndef SC_ADB_DEVICE_H +#define SC_ADB_DEVICE_H + +#include "common.h" + +#include +#include + +struct sc_adb_device { + char *serial; + char *state; + char *model; +}; + +void +sc_adb_device_destroy(struct sc_adb_device *device); + +/** + * Move src to dst + * + * After this call, the content of src is undefined, except that + * sc_adb_device_destroy() can be called. + * + * This is useful to take a device from a list that will be destroyed, without + * making unnecessary copies. + */ +void +sc_adb_device_move(struct sc_adb_device *dst, struct sc_adb_device *src); + +void +sc_adb_devices_destroy_all(struct sc_adb_device *devices, size_t count); + +#endif diff --git a/app/src/adb/adb_parser.c b/app/src/adb/adb_parser.c index 2a0fd8da..d41e1bcd 100644 --- a/app/src/adb/adb_parser.c +++ b/app/src/adb/adb_parser.c @@ -1,11 +1,168 @@ #include "adb_parser.h" #include +#include #include #include "util/log.h" #include "util/str.h" +bool +sc_adb_parse_device(char *line, struct sc_adb_device *device) { + // One device line looks like: + // "0123456789abcdef device usb:2-1 product:MyProduct model:MyModel " + // "device:MyDevice transport_id:1" + + if (line[0] == '*') { + // Garbage lines printed by adb daemon while starting start with a '*' + return false; + } + + if (!strncmp("adb server", line, sizeof("adb server") - 1)) { + // Ignore lines starting with "adb server": + // adb server version (41) doesn't match this client (39); killing... + return false; + } + + char *s = line; // cursor in the line + + // After the serial: + // - "adb devices" writes a single '\t' + // - "adb devices -l" writes multiple spaces + // For flexibility, accept both. + size_t serial_len = strcspn(s, " \t"); + if (!serial_len) { + // empty serial + return false; + } + bool eol = s[serial_len] == '\0'; + if (eol) { + // serial alone is unexpected + return false; + } + s[serial_len] = '\0'; + char *serial = s; + s += serial_len + 1; + // After the serial, there might be several spaces + s += strspn(s, " \t"); // consume all separators + + size_t state_len = strcspn(s, " "); + if (!state_len) { + // empty state + return false; + } + eol = s[state_len] == '\0'; + s[state_len] = '\0'; + char *state = s; + + char *model = NULL; + if (!eol) { + s += state_len + 1; + + // Iterate over all properties "key:value key:value ..." + for (;;) { + size_t token_len = strcspn(s, " "); + if (!token_len) { + break; + } + eol = s[token_len] == '\0'; + s[token_len] = '\0'; + char *token = s; + + if (!strncmp("model:", token, sizeof("model:") - 1)) { + model = &token[sizeof("model:") - 1]; + // We only need the model + break; + } + + if (eol) { + break; + } else { + s+= token_len + 1; + } + } + } + + device->serial = strdup(serial); + if (!device->serial) { + return false; + } + + device->state = strdup(state); + if (!device->state) { + free(device->serial); + return false; + } + + if (model) { + device->model = strdup(model); + if (!device->model) { + LOG_OOM(); + // model is optional, do not fail + } + } else { + device->model = NULL; + } + + return true; +} + +ssize_t +sc_adb_parse_devices(char *str, struct sc_adb_device *devices, + size_t devices_len) { + size_t dev_count = 0; + +#define HEADER "List of devices attached" +#define HEADER_LEN (sizeof(HEADER) - 1) + bool header_found = false; + + size_t idx_line = 0; + while (str[idx_line] != '\0') { + char *line = &str[idx_line]; + size_t len = strcspn(line, "\n"); + + // The next line starts after the '\n' (replaced by `\0`) + idx_line += len; + + if (str[idx_line] != '\0') { + // The next line starts after the '\n' + ++idx_line; + } + + if (!header_found) { + if (!strncmp(line, HEADER, HEADER_LEN)) { + header_found = true; + } + // Skip everything until the header, there might be garbage lines + // related to daemon starting before + continue; + } + + // The line, but without any trailing '\r' + size_t line_len = sc_str_remove_trailing_cr(line, len); + line[line_len] = '\0'; + + bool ok = sc_adb_parse_device(line, &devices[dev_count]); + if (!ok) { + continue; + } + + ++dev_count; + + assert(dev_count <= devices_len); + if (dev_count == devices_len) { + // Max number of devices reached + break; + } + } + + if (!header_found) { + return -1; + } + + return dev_count; +} + static char * sc_adb_parse_device_ip_from_line(char *line) { // One line from "ip route" looks like: @@ -64,7 +221,7 @@ sc_adb_parse_device_ip_from_output(char *str) { if (str[idx_line] != '\0') { // The next line starts after the '\n' - idx_line += 1; + ++idx_line; } } diff --git a/app/src/adb/adb_parser.h b/app/src/adb/adb_parser.h index 7d116713..65493a2e 100644 --- a/app/src/adb/adb_parser.h +++ b/app/src/adb/adb_parser.h @@ -5,6 +5,19 @@ #include +#include "adb_device.h" + +/** + * Parse the available devices from the output of `adb devices` + * + * The parameter must be a NUL-terminated string. + * + * Warning: this function modifies the buffer for optimization purposes. + */ +ssize_t +sc_adb_parse_devices(char *str, struct sc_adb_device *devices, + size_t devices_len); + /** * Parse the ip from the output of `adb shell ip route` * diff --git a/app/tests/test_adb_parser.c b/app/tests/test_adb_parser.c index 749ce433..990418c0 100644 --- a/app/tests/test_adb_parser.c +++ b/app/tests/test_adb_parser.c @@ -2,8 +2,158 @@ #include +#include "adb/adb_device.h" #include "adb/adb_parser.h" +static void test_adb_devices() { + char output[] = + "List of devices attached\n" + "0123456789abcdef device usb:2-1 product:MyProduct model:MyModel " + "device:MyDevice transport_id:1\n" + "192.168.1.1:5555 device product:MyWifiProduct model:MyWifiModel " + "device:MyWifiDevice trandport_id:2\n"; + + struct sc_adb_device devices[16]; + ssize_t count = sc_adb_parse_devices(output, devices, ARRAY_LEN(devices)); + assert(count == 2); + + struct sc_adb_device *device = &devices[0]; + assert(!strcmp("0123456789abcdef", device->serial)); + assert(!strcmp("device", device->state)); + assert(!strcmp("MyModel", device->model)); + + device = &devices[1]; + assert(!strcmp("192.168.1.1:5555", device->serial)); + assert(!strcmp("device", device->state)); + assert(!strcmp("MyWifiModel", device->model)); + + sc_adb_devices_destroy_all(devices, count); +} + +static void test_adb_devices_cr() { + char output[] = + "List of devices attached\r\n" + "0123456789abcdef device usb:2-1 product:MyProduct model:MyModel " + "device:MyDevice transport_id:1\r\n" + "192.168.1.1:5555 device product:MyWifiProduct model:MyWifiModel " + "device:MyWifiDevice trandport_id:2\r\n"; + + struct sc_adb_device devices[16]; + ssize_t count = sc_adb_parse_devices(output, devices, ARRAY_LEN(devices)); + assert(count == 2); + + struct sc_adb_device *device = &devices[0]; + assert(!strcmp("0123456789abcdef", device->serial)); + assert(!strcmp("device", device->state)); + assert(!strcmp("MyModel", device->model)); + + device = &devices[1]; + assert(!strcmp("192.168.1.1:5555", device->serial)); + assert(!strcmp("device", device->state)); + assert(!strcmp("MyWifiModel", device->model)); + + sc_adb_devices_destroy_all(devices, count); +} + +static void test_adb_devices_daemon_start() { + char output[] = + "* daemon not running; starting now at tcp:5037\n" + "* daemon started successfully\n" + "List of devices attached\n" + "0123456789abcdef device usb:2-1 product:MyProduct model:MyModel " + "device:MyDevice transport_id:1\n"; + + struct sc_adb_device devices[16]; + ssize_t count = sc_adb_parse_devices(output, devices, ARRAY_LEN(devices)); + assert(count == 1); + + struct sc_adb_device *device = &devices[0]; + assert(!strcmp("0123456789abcdef", device->serial)); + assert(!strcmp("device", device->state)); + assert(!strcmp("MyModel", device->model)); + + sc_adb_device_destroy(device); +} + +static void test_adb_devices_daemon_start_mixed() { + char output[] = + "List of devices attached\n" + "adb server version (41) doesn't match this client (39); killing...\n" + "* daemon started successfully *\n" + "0123456789abcdef unauthorized usb:1-1\n" + "87654321 device usb:2-1 product:MyProduct model:MyModel " + "device:MyDevice\n"; + + struct sc_adb_device devices[16]; + ssize_t count = sc_adb_parse_devices(output, devices, ARRAY_LEN(devices)); + assert(count == 2); + + struct sc_adb_device *device = &devices[0]; + assert(!strcmp("0123456789abcdef", device->serial)); + assert(!strcmp("unauthorized", device->state)); + fprintf(stderr, "==== [%s]\n", device->model); + assert(!device->model); + + device = &devices[1]; + assert(!strcmp("87654321", device->serial)); + assert(!strcmp("device", device->state)); + assert(!strcmp("MyModel", device->model)); + + sc_adb_devices_destroy_all(devices, count); +} + +static void test_adb_devices_without_eol() { + char output[] = + "List of devices attached\n" + "0123456789abcdef device usb:2-1 product:MyProduct model:MyModel " + "device:MyDevice transport_id:1"; + struct sc_adb_device devices[16]; + ssize_t count = sc_adb_parse_devices(output, devices, ARRAY_LEN(devices)); + assert(count == 1); + + struct sc_adb_device *device = &devices[0]; + assert(!strcmp("0123456789abcdef", device->serial)); + assert(!strcmp("device", device->state)); + assert(!strcmp("MyModel", device->model)); + + sc_adb_device_destroy(device); +} + +static void test_adb_devices_without_header() { + char output[] = + "0123456789abcdef device usb:2-1 product:MyProduct model:MyModel " + "device:MyDevice transport_id:1\n"; + struct sc_adb_device devices[16]; + ssize_t count = sc_adb_parse_devices(output, devices, ARRAY_LEN(devices)); + assert(count == -1); +} + +static void test_adb_devices_corrupted() { + char output[] = + "List of devices attached\n" + "corrupted_garbage\n"; + struct sc_adb_device devices[16]; + ssize_t count = sc_adb_parse_devices(output, devices, ARRAY_LEN(devices)); + assert(count == 0); +} + +static void test_adb_devices_spaces() { + char output[] = + "List of devices attached\n" + "0123456789abcdef unauthorized usb:1-4 transport_id:3\n"; + + struct sc_adb_device devices[16]; + ssize_t count = sc_adb_parse_devices(output, devices, ARRAY_LEN(devices)); + assert(count == 1); + + struct sc_adb_device *device = &devices[0]; + assert(!strcmp("0123456789abcdef", device->serial)); + assert(!strcmp("unauthorized", device->state)); + assert(!device->model); + + sc_adb_device_destroy(device); +} + static void test_get_ip_single_line() { char ip_route[] = "192.168.1.0/24 dev wlan0 proto kernel scope link src " "192.168.12.34\r\r\n"; @@ -86,6 +236,15 @@ int main(int argc, char *argv[]) { (void) argc; (void) argv; + test_adb_devices(); + test_adb_devices_cr(); + test_adb_devices_daemon_start(); + test_adb_devices_daemon_start_mixed(); + test_adb_devices_without_eol(); + test_adb_devices_without_header(); + test_adb_devices_corrupted(); + test_adb_devices_spaces(); + test_get_ip_single_line(); test_get_ip_single_line_without_eol(); test_get_ip_single_line_with_trailing_space(); From 02d46b226210e407d940ea95f16047985f883174 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 6 Feb 2022 15:02:29 +0100 Subject: [PATCH 1092/2244] Expose function to test if a serial is TCP/IP In practice, it just tests if the serial contains a ':', which is sufficient to distinguish ip:port from a real USB serial. PR #3005 --- app/src/adb/adb.c | 5 +++++ app/src/adb/adb.h | 9 +++++++++ app/src/server.c | 5 +---- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/app/src/adb/adb.c b/app/src/adb/adb.c index 3cfc5a95..b00bd620 100644 --- a/app/src/adb/adb.c +++ b/app/src/adb/adb.c @@ -482,3 +482,8 @@ sc_adb_get_device_ip(struct sc_intr *intr, const char *serial, unsigned flags) { return sc_adb_parse_device_ip_from_output(buf); } + +bool +sc_adb_is_serial_tcpip(const char *serial) { + return strchr(serial, ':'); +} diff --git a/app/src/adb/adb.h b/app/src/adb/adb.h index 1a97c203..18a11438 100644 --- a/app/src/adb/adb.h +++ b/app/src/adb/adb.h @@ -93,4 +93,13 @@ sc_adb_get_serialno(struct sc_intr *intr, unsigned flags); char * sc_adb_get_device_ip(struct sc_intr *intr, const char *serial, unsigned flags); +/** + * Indicate if the serial represents an IP address + * + * In practice, it just returns true if and only if it contains a ':', which is + * sufficient to distinguish an ip:port from a real USB serial. + */ +bool +sc_adb_is_serial_tcpip(const char *serial); + #endif diff --git a/app/src/server.c b/app/src/server.c index b0b596ad..3dbda0e9 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -659,10 +659,7 @@ sc_server_configure_tcpip_known_address(struct sc_server *server, static bool sc_server_configure_tcpip_unknown_address(struct sc_server *server, const char *serial) { - // The serial is either the real serial when connected via USB, or - // the IP:PORT when connected over TCP/IP. Only the latter contains - // a colon. - bool is_already_tcpip = strchr(serial, ':'); + bool is_already_tcpip = sc_adb_is_serial_tcpip(serial); if (is_already_tcpip) { // Nothing to do LOGI("Device already connected via TCP/IP: %s", serial); From 4692d13179fc244333975769c3236ed5fc8fcbd2 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 6 Feb 2022 15:04:00 +0100 Subject: [PATCH 1093/2244] Expose simple API to select a single adb device Select an adb device from the output of `adb device -l`. PR #3005 --- app/src/adb/adb.c | 160 +++++++++++++++++++++++++++++++++++++++ app/src/adb/adb.h | 10 +++ app/src/adb/adb_device.h | 1 + app/src/adb/adb_parser.c | 2 + app/src/server.c | 34 +++------ app/src/usb/usb.c | 1 + 6 files changed, 186 insertions(+), 22 deletions(-) diff --git a/app/src/adb/adb.c b/app/src/adb/adb.c index b00bd620..ef316884 100644 --- a/app/src/adb/adb.c +++ b/app/src/adb/adb.c @@ -374,6 +374,166 @@ sc_adb_disconnect(struct sc_intr *intr, const char *ip_port, unsigned flags) { return process_check_success_intr(intr, pid, "adb disconnect", flags); } +static ssize_t +sc_adb_list_devices(struct sc_intr *intr, unsigned flags, + struct sc_adb_device *devices, size_t len) { + const char *const argv[] = SC_ADB_COMMAND("devices", "-l"); + + sc_pipe pout; + sc_pid pid = sc_adb_execute_p(argv, flags, &pout); + if (pid == SC_PROCESS_NONE) { + LOGE("Could not execute \"adb devices -l\""); + return -1; + } + + char buf[4096]; + ssize_t r = sc_pipe_read_all_intr(intr, pid, pout, buf, sizeof(buf) - 1); + sc_pipe_close(pout); + + bool ok = process_check_success_intr(intr, pid, "adb devices -l", flags); + if (!ok) { + return -1; + } + + if (r == -1) { + return -1; + } + + assert((size_t) r < sizeof(buf)); + if (r == sizeof(buf) - 1) { + // The implementation assumes that the output of "adb devices -l" fits + // in the buffer in a single pass + LOGW("Result of \"adb devices -l\" does not fit in 4Kb. " + "Please report an issue.\n"); + return -1; + } + + // It is parsed as a NUL-terminated string + buf[r] = '\0'; + + // List all devices to the output list directly + return sc_adb_parse_devices(buf, devices, len); +} + +static bool +sc_adb_accept_device(const struct sc_adb_device *device, const char *serial) { + if (!serial) { + return true; + } + + return !strcmp(serial, device->serial); +} + +static size_t +sc_adb_devices_select(struct sc_adb_device *devices, size_t len, + const char *serial, size_t *idx_out) { + size_t count = 0; + for (size_t i = 0; i < len; ++i) { + struct sc_adb_device *device = &devices[i]; + device->selected = sc_adb_accept_device(device, serial); + if (device->selected) { + if (idx_out && !count) { + *idx_out = i; + } + ++count; + } + } + + return count; +} + +static void +sc_adb_devices_log(enum sc_log_level level, struct sc_adb_device *devices, + size_t count) { + for (size_t i = 0; i < count; ++i) { + struct sc_adb_device *d = &devices[i]; + const char *selection = d->selected ? "-->" : " "; + const char *type = sc_adb_is_serial_tcpip(d->serial) ? "(tcpip)" + : " (usb)"; + LOG(level, " %s %s %-20s %16s %s", + selection, type, d->serial, d->state, d->model ? d->model : ""); + } +} + +static bool +sc_adb_device_check_state(struct sc_adb_device *device, + struct sc_adb_device *devices, size_t count) { + const char *state = device->state; + + if (!strcmp("device", state)) { + return true; + } + + if (!strcmp("unauthorized", state)) { + LOGE("Device is unauthorized:"); + sc_adb_devices_log(SC_LOG_LEVEL_ERROR, devices, count); + LOGE("A popup should open on the device to request authorization."); + LOGE("Check the FAQ: " + ""); + } + + return false; +} + +bool +sc_adb_select_device(struct sc_intr *intr, const char *serial, unsigned flags, + struct sc_adb_device *out_device) { + struct sc_adb_device devices[16]; + ssize_t count = + sc_adb_list_devices(intr, flags, devices, ARRAY_LEN(devices)); + if (count == -1) { + LOGE("Could not list ADB devices"); + return false; + } + + if (count == 0) { + LOGE("Could not find any ADB device"); + return false; + } + + size_t sel_idx; // index of the single matching device if sel_count == 1 + size_t sel_count = sc_adb_devices_select(devices, count, serial, &sel_idx); + + if (sel_count == 0) { + // if count > 0 && sel_count == 0, then necessarily a serial is provided + assert(serial); + LOGE("Could not find ADB device %s", serial); + sc_adb_devices_log(SC_LOG_LEVEL_ERROR, devices, count); + sc_adb_devices_destroy_all(devices, count); + return false; + } + + if (sel_count > 1) { + if (serial) { + LOGE("Multiple (%" SC_PRIsizet ") ADB devices with serial %s:", + sel_count, serial); + } else { + LOGE("Multiple (%" SC_PRIsizet ") ADB devices:", sel_count); + } + sc_adb_devices_log(SC_LOG_LEVEL_ERROR, devices, count); + LOGE("Select a device via -s (--serial)"); + sc_adb_devices_destroy_all(devices, count); + return false; + } + + assert(sel_count == 1); // sel_idx is valid only if sel_count == 1 + struct sc_adb_device *device = &devices[sel_idx]; + + bool ok = sc_adb_device_check_state(device, devices, count); + if (!ok) { + sc_adb_devices_destroy_all(devices, count); + return false; + } + + LOGD("ADB device found:"); + sc_adb_devices_log(SC_LOG_LEVEL_DEBUG, devices, count); + + // Move devics into out_device (do not destroy device) + sc_adb_device_move(out_device, device); + sc_adb_devices_destroy_all(devices, count); + return true; +} + char * sc_adb_getprop(struct sc_intr *intr, const char *serial, const char *prop, unsigned flags) { diff --git a/app/src/adb/adb.h b/app/src/adb/adb.h index 18a11438..04e2c985 100644 --- a/app/src/adb/adb.h +++ b/app/src/adb/adb.h @@ -6,6 +6,7 @@ #include #include +#include "adb_device.h" #include "util/intr.h" #define SC_ADB_NO_STDOUT (1 << 0) @@ -69,6 +70,15 @@ sc_adb_connect(struct sc_intr *intr, const char *ip_port, unsigned flags); bool sc_adb_disconnect(struct sc_intr *intr, const char *ip_port, unsigned flags); +/** + * Execute `adb devices` and parse the result to select a device + * + * Return true if a single matching device is found, and write it to out_device. + */ +bool +sc_adb_select_device(struct sc_intr *intr, const char *serial, unsigned flags, + struct sc_adb_device *out_device); + /** * Execute `adb getprop ` */ diff --git a/app/src/adb/adb_device.h b/app/src/adb/adb_device.h index 11b46c0c..ed8362e9 100644 --- a/app/src/adb/adb_device.h +++ b/app/src/adb/adb_device.h @@ -10,6 +10,7 @@ struct sc_adb_device { char *serial; char *state; char *model; + bool selected; }; void diff --git a/app/src/adb/adb_parser.c b/app/src/adb/adb_parser.c index d41e1bcd..85e8ffaf 100644 --- a/app/src/adb/adb_parser.c +++ b/app/src/adb/adb_parser.c @@ -104,6 +104,8 @@ sc_adb_parse_device(char *line, struct sc_adb_device *device) { device->model = NULL; } + device->selected = false; + return true; } diff --git a/app/src/server.c b/app/src/server.c index 3dbda0e9..3264c4ee 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -503,22 +503,6 @@ sc_server_on_terminated(void *userdata) { LOGD("Server terminated"); } -static char * -sc_server_read_serial(struct sc_server *server) { - char *serial; - if (server->params.req_serial) { - // The serial is already known - serial = strdup(server->params.req_serial); - if (!serial) { - LOG_OOM(); - } - } else { - serial = sc_adb_get_serialno(&server->intr, 0); - } - - return serial; -} - static bool is_tcpip_mode_enabled(struct sc_server *server, const char *serial) { struct sc_intr *intr = &server->intr; @@ -695,22 +679,28 @@ run_server(void *data) { bool ok; if (need_initial_serial) { - char *serial = sc_server_read_serial(server); - if (!serial) { - LOGE("Could not get device serial"); + struct sc_adb_device device; + ok = sc_adb_select_device(&server->intr, params->req_serial, 0, + &device); + if (!ok) { goto error_connection_failed; } if (params->tcpip) { assert(!params->tcpip_dst); - ok = sc_server_configure_tcpip_unknown_address(server, serial); - free(serial); + ok = sc_server_configure_tcpip_unknown_address(server, + device.serial); + sc_adb_device_destroy(&device); if (!ok) { goto error_connection_failed; } assert(server->serial); } else { - server->serial = serial; + // "move" the device.serial without copy + server->serial = device.serial; + // the serial must not be freed by the destructor + device.serial = NULL; + sc_adb_device_destroy(&device); } } else { ok = sc_server_configure_tcpip_known_address(server, params->tcpip_dst); diff --git a/app/src/usb/usb.c b/app/src/usb/usb.c index aec263cd..9edb1866 100644 --- a/app/src/usb/usb.c +++ b/app/src/usb/usb.c @@ -183,6 +183,7 @@ sc_usb_select_device(struct sc_usb *usb, const char *serial, } else { LOGE("Multiple (%" SC_PRIsizet ") USB devices:", sel_count); } + sc_usb_devices_log(SC_LOG_LEVEL_ERROR, usb_devices, count); LOGE("Select a device via -s (--serial)"); sc_usb_devices_destroy_all(usb_devices, count); return false; From 0a619dc9efa358c03372e08192bda075ecbeb0a6 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 6 Feb 2022 15:11:35 +0100 Subject: [PATCH 1094/2244] Allow selecting a device from IP without port Since the previous commit, if a serial is given via -s/--serial (either a real USB serial or an IP:port), a device is selected if its serial matches exactly. In addition, if the user pass an IP without a port, then select any device with this IP, regardless of the port (so that "192.168.1.1" matches any "192.168.1.1:port"). This is also the default behavior of adb. PR #3005 --- app/src/adb/adb.c | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/app/src/adb/adb.c b/app/src/adb/adb.c index ef316884..baa0e406 100644 --- a/app/src/adb/adb.c +++ b/app/src/adb/adb.c @@ -421,6 +421,24 @@ sc_adb_accept_device(const struct sc_adb_device *device, const char *serial) { return true; } + char *device_serial_colon = strchr(device->serial, ':'); + if (device_serial_colon) { + // The device serial is an IP:port... + char *serial_colon = strchr(serial, ':'); + if (!serial_colon) { + // But the requested serial has no ':', so only consider the IP part + // of the device serial. This allows to use "192.168.1.1" to match + // any "192.168.1.1:port". + size_t serial_len = strlen(serial); + size_t device_ip_len = device_serial_colon - device->serial; + if (serial_len != device_ip_len) { + // They are not equal, they don't even have the same length + return false; + } + return !strncmp(serial, device->serial, device_ip_len); + } + } + return !strcmp(serial, device->serial); } From 9c545e8c29b743d908285ca17fa34187717936a3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 6 Feb 2022 15:22:01 +0100 Subject: [PATCH 1095/2244] Remove sc_adb_get_serialno() The device serial is now retrieved from `adb devices -l`, `adb get-serialno` is not called anymore. PR #3005 --- app/src/adb/adb.c | 32 -------------------------------- app/src/adb/adb.h | 8 -------- 2 files changed, 40 deletions(-) diff --git a/app/src/adb/adb.c b/app/src/adb/adb.c index baa0e406..2afe11c5 100644 --- a/app/src/adb/adb.c +++ b/app/src/adb/adb.c @@ -587,38 +587,6 @@ sc_adb_getprop(struct sc_intr *intr, const char *serial, const char *prop, return strdup(buf); } -char * -sc_adb_get_serialno(struct sc_intr *intr, unsigned flags) { - const char *const argv[] = SC_ADB_COMMAND("get-serialno"); - - sc_pipe pout; - sc_pid pid = sc_adb_execute_p(argv, flags, &pout); - if (pid == SC_PROCESS_NONE) { - LOGE("Could not execute \"adb get-serialno\""); - return NULL; - } - - char buf[128]; - ssize_t r = sc_pipe_read_all_intr(intr, pid, pout, buf, sizeof(buf) - 1); - sc_pipe_close(pout); - - bool ok = process_check_success_intr(intr, pid, "adb get-serialno", flags); - if (!ok) { - return NULL; - } - - if (r == -1) { - return NULL; - } - - assert((size_t) r < sizeof(buf)); - buf[r] = '\0'; - size_t len = strcspn(buf, " \r\n"); - buf[len] = '\0'; - - return strdup(buf); -} - char * sc_adb_get_device_ip(struct sc_intr *intr, const char *serial, unsigned flags) { assert(serial); diff --git a/app/src/adb/adb.h b/app/src/adb/adb.h index 04e2c985..ac9e15b0 100644 --- a/app/src/adb/adb.h +++ b/app/src/adb/adb.h @@ -86,14 +86,6 @@ char * sc_adb_getprop(struct sc_intr *intr, const char *serial, const char *prop, unsigned flags); -/** - * Execute `adb get-serialno` - * - * Return the result, to be freed by the caller, or NULL on error. - */ -char * -sc_adb_get_serialno(struct sc_intr *intr, unsigned flags); - /** * Attempt to retrieve the device IP * From 5ed13ef4776f41afd98df4cc22e27832d9c9bd3f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 6 Feb 2022 15:28:23 +0100 Subject: [PATCH 1096/2244] Execute adb start-server This does nothing if the adb daemon is already started, but allows to print any output/errors to the console. Otherwise, the daemon starting would occur during `adb devices`, which does not output to the console because the result is parsed. PR #3005 --- app/src/adb/adb.c | 8 ++++++++ app/src/adb/adb.h | 3 +++ app/src/server.c | 10 +++++++++- 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/app/src/adb/adb.c b/app/src/adb/adb.c index 2afe11c5..5b61788a 100644 --- a/app/src/adb/adb.c +++ b/app/src/adb/adb.c @@ -194,6 +194,14 @@ sc_adb_execute(const char *const argv[], unsigned flags) { return sc_adb_execute_p(argv, flags, NULL); } +bool +sc_adb_start_server(struct sc_intr *intr, unsigned flags) { + const char *const argv[] = SC_ADB_COMMAND("start-server"); + + sc_pid pid = sc_adb_execute(argv, flags); + return process_check_success_intr(intr, pid, "adb start-server", flags); +} + bool sc_adb_forward(struct sc_intr *intr, const char *serial, uint16_t local_port, const char *device_socket_name, unsigned flags) { diff --git a/app/src/adb/adb.h b/app/src/adb/adb.h index ac9e15b0..1381031f 100644 --- a/app/src/adb/adb.h +++ b/app/src/adb/adb.h @@ -21,6 +21,9 @@ sc_adb_get_executable(void); sc_pid sc_adb_execute(const char *const argv[], unsigned flags); +bool +sc_adb_start_server(struct sc_intr *intr, unsigned flags); + bool sc_adb_forward(struct sc_intr *intr, const char *serial, uint16_t local_port, const char *device_socket_name, unsigned flags); diff --git a/app/src/server.c b/app/src/server.c index 3264c4ee..f6e8b6df 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -665,6 +665,15 @@ run_server(void *data) { const struct sc_server_params *params = &server->params; + // Execute "adb start-server" before "adb devices" so that daemon starting + // output/errors is correctly printed in the console ("adb devices" output + // is parsed, so it is not output) + bool ok = sc_adb_start_server(&server->intr, 0); + if (!ok) { + LOGE("Could not start adb daemon"); + goto error_connection_failed; + } + // params->tcpip_dst implies params->tcpip assert(!params->tcpip_dst || params->tcpip); @@ -677,7 +686,6 @@ run_server(void *data) { // exist, and scrcpy will execute "adb connect"). bool need_initial_serial = !params->tcpip_dst; - bool ok; if (need_initial_serial) { struct sc_adb_device device; ok = sc_adb_select_device(&server->intr, params->req_serial, 0, From 146f65d7b2b046c2d9a218116b4483f1633b0322 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 6 Feb 2022 16:06:46 +0100 Subject: [PATCH 1097/2244] Introduce adb device selector Currently, a device is selected either from a specific serial, or if it is the only one connected. In order to support selecting the only device connected via USB or via TCP/IP separately, introduce a new selection structure. PR #3005 --- app/src/adb/adb.c | 98 +++++++++++++++++++++++++++++++---------------- app/src/adb/adb.h | 15 +++++++- app/src/server.c | 10 ++++- 3 files changed, 85 insertions(+), 38 deletions(-) diff --git a/app/src/adb/adb.c b/app/src/adb/adb.c index 5b61788a..2bfa9f1e 100644 --- a/app/src/adb/adb.c +++ b/app/src/adb/adb.c @@ -424,39 +424,49 @@ sc_adb_list_devices(struct sc_intr *intr, unsigned flags, } static bool -sc_adb_accept_device(const struct sc_adb_device *device, const char *serial) { - if (!serial) { - return true; - } - - char *device_serial_colon = strchr(device->serial, ':'); - if (device_serial_colon) { - // The device serial is an IP:port... - char *serial_colon = strchr(serial, ':'); - if (!serial_colon) { - // But the requested serial has no ':', so only consider the IP part - // of the device serial. This allows to use "192.168.1.1" to match - // any "192.168.1.1:port". - size_t serial_len = strlen(serial); - size_t device_ip_len = device_serial_colon - device->serial; - if (serial_len != device_ip_len) { - // They are not equal, they don't even have the same length - return false; +sc_adb_accept_device(const struct sc_adb_device *device, + const struct sc_adb_device_selector *selector) { + switch (selector->type) { + case SC_ADB_DEVICE_SELECT_ALL: + return true; + case SC_ADB_DEVICE_SELECT_SERIAL: + assert(selector->serial); + char *device_serial_colon = strchr(device->serial, ':'); + if (device_serial_colon) { + // The device serial is an IP:port... + char *serial_colon = strchr(selector->serial, ':'); + if (!serial_colon) { + // But the requested serial has no ':', so only consider + // the IP part of the device serial. This allows to use + // "192.168.1.1" to match any "192.168.1.1:port". + size_t serial_len = strlen(selector->serial); + size_t device_ip_len = device_serial_colon - device->serial; + if (serial_len != device_ip_len) { + // They are not equal, they don't even have the same + // length + return false; + } + return !strncmp(selector->serial, device->serial, + device_ip_len); + } } - return !strncmp(serial, device->serial, device_ip_len); - } + return !strcmp(selector->serial, device->serial); + default: + assert(!"Missing SC_ADB_DEVICE_SELECT_* handling"); + break; } - return !strcmp(serial, device->serial); + return false; } static size_t sc_adb_devices_select(struct sc_adb_device *devices, size_t len, - const char *serial, size_t *idx_out) { + const struct sc_adb_device_selector *selector, + size_t *idx_out) { size_t count = 0; for (size_t i = 0; i < len; ++i) { struct sc_adb_device *device = &devices[i]; - device->selected = sc_adb_accept_device(device, serial); + device->selected = sc_adb_accept_device(device, selector); if (device->selected) { if (idx_out && !count) { *idx_out = i; @@ -502,8 +512,9 @@ sc_adb_device_check_state(struct sc_adb_device *device, } bool -sc_adb_select_device(struct sc_intr *intr, const char *serial, unsigned flags, - struct sc_adb_device *out_device) { +sc_adb_select_device(struct sc_intr *intr, + const struct sc_adb_device_selector *selector, + unsigned flags, struct sc_adb_device *out_device) { struct sc_adb_device devices[16]; ssize_t count = sc_adb_list_devices(intr, flags, devices, ARRAY_LEN(devices)); @@ -518,23 +529,42 @@ sc_adb_select_device(struct sc_intr *intr, const char *serial, unsigned flags, } size_t sel_idx; // index of the single matching device if sel_count == 1 - size_t sel_count = sc_adb_devices_select(devices, count, serial, &sel_idx); + size_t sel_count = + sc_adb_devices_select(devices, count, selector, &sel_idx); if (sel_count == 0) { - // if count > 0 && sel_count == 0, then necessarily a serial is provided - assert(serial); - LOGE("Could not find ADB device %s", serial); + // if count > 0 && sel_count == 0, then necessarily a selection is + // requested + assert(selector->type != SC_ADB_DEVICE_SELECT_ALL); + + switch (selector->type) { + case SC_ADB_DEVICE_SELECT_SERIAL: + assert(selector->serial); + LOGE("Could not find ADB device %s:", selector->serial); + break; + default: + assert(!"Unexpected selector type"); + break; + } + sc_adb_devices_log(SC_LOG_LEVEL_ERROR, devices, count); sc_adb_devices_destroy_all(devices, count); return false; } if (sel_count > 1) { - if (serial) { - LOGE("Multiple (%" SC_PRIsizet ") ADB devices with serial %s:", - sel_count, serial); - } else { - LOGE("Multiple (%" SC_PRIsizet ") ADB devices:", sel_count); + switch (selector->type) { + case SC_ADB_DEVICE_SELECT_ALL: + LOGE("Multiple (%" SC_PRIsizet ") ADB devices:", sel_count); + break; + case SC_ADB_DEVICE_SELECT_SERIAL: + assert(selector->serial); + LOGE("Multiple (%" SC_PRIsizet ") ADB devices with serial %s:", + sel_count, selector->serial); + break; + default: + assert(!"Unexpected selector type"); + break; } sc_adb_devices_log(SC_LOG_LEVEL_ERROR, devices, count); LOGE("Select a device via -s (--serial)"); diff --git a/app/src/adb/adb.h b/app/src/adb/adb.h index 1381031f..dc302a85 100644 --- a/app/src/adb/adb.h +++ b/app/src/adb/adb.h @@ -18,6 +18,16 @@ const char * sc_adb_get_executable(void); +enum sc_adb_device_selector_type { + SC_ADB_DEVICE_SELECT_ALL, + SC_ADB_DEVICE_SELECT_SERIAL, +}; + +struct sc_adb_device_selector { + enum sc_adb_device_selector_type type; + const char *serial; +}; + sc_pid sc_adb_execute(const char *const argv[], unsigned flags); @@ -79,8 +89,9 @@ sc_adb_disconnect(struct sc_intr *intr, const char *ip_port, unsigned flags); * Return true if a single matching device is found, and write it to out_device. */ bool -sc_adb_select_device(struct sc_intr *intr, const char *serial, unsigned flags, - struct sc_adb_device *out_device); +sc_adb_select_device(struct sc_intr *intr, + const struct sc_adb_device_selector *selector, + unsigned flags, struct sc_adb_device *out_device); /** * Execute `adb getprop ` diff --git a/app/src/server.c b/app/src/server.c index f6e8b6df..c0c33a2c 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -687,9 +687,15 @@ run_server(void *data) { bool need_initial_serial = !params->tcpip_dst; if (need_initial_serial) { + struct sc_adb_device_selector selector; + if (params->req_serial) { + selector.type = SC_ADB_DEVICE_SELECT_SERIAL; + selector.serial = params->req_serial; + } else { + selector.type = SC_ADB_DEVICE_SELECT_ALL; + } struct sc_adb_device device; - ok = sc_adb_select_device(&server->intr, params->req_serial, 0, - &device); + ok = sc_adb_select_device(&server->intr, &selector, 0, &device); if (!ok) { goto error_connection_failed; } From 582161607e8cb2d6496b1962a5f2efa091ab3ccb Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 6 Feb 2022 18:40:18 +0100 Subject: [PATCH 1098/2244] Add option to select USB or TCP/IP devices If several devices are connected (as listed by `adb devices`), it was necessary to provide the explicit serial via -s/--serial. If only one device is connected via USB (respectively, via TCP/IP), it might be convenient to select it automatically. For this purpose, two new options are introduced: - -d/--select-usb: select the single device connected over USB - -e/--select-tcpip: select the single device connected over TCP/IP PR #3005 --- app/scrcpy.1 | 12 ++++++++++++ app/src/adb/adb.c | 21 ++++++++++++++++++++- app/src/adb/adb.h | 2 ++ app/src/cli.c | 30 ++++++++++++++++++++++++++++-- app/src/options.c | 2 ++ app/src/options.h | 2 ++ app/src/scrcpy.c | 2 ++ app/src/server.c | 9 +++++++++ app/src/server.h | 2 ++ 9 files changed, 79 insertions(+), 3 deletions(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 0f618cb4..317cec05 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -43,6 +43,12 @@ The values are expressed in the device natural orientation (typically, portrait .B \-\-max\-size value is computed on the cropped size. +.TP +.B \-d, \-\-select\-usb +Use USB device (if there is exactly one, like adb -d). + +Also see \fB\-e\fR (\fB\-\-select\-tcpip\fR). + .TP .BI "\-\-disable-screensaver" Disable screensaver while scrcpy is running. @@ -62,6 +68,12 @@ Add a buffering delay (in milliseconds) before displaying. This increases latenc Default is 0 (no buffering). +.TP +.B \-e, \-\-select\-tcpip +Use TCP/IP device (if there is exactly one, like adb -e). + +Also see \fB\-d\fR (\fB\-\-select\-usb\fR). + .TP .BI "\-\-encoder " name Use a specific MediaCodec encoder (must be a H.264 encoder). diff --git a/app/src/adb/adb.c b/app/src/adb/adb.c index 2bfa9f1e..e415eb4f 100644 --- a/app/src/adb/adb.c +++ b/app/src/adb/adb.c @@ -451,6 +451,10 @@ sc_adb_accept_device(const struct sc_adb_device *device, } } return !strcmp(selector->serial, device->serial); + case SC_ADB_DEVICE_SELECT_USB: + return !sc_adb_is_serial_tcpip(device->serial); + case SC_ADB_DEVICE_SELECT_TCPIP: + return sc_adb_is_serial_tcpip(device->serial); default: assert(!"Missing SC_ADB_DEVICE_SELECT_* handling"); break; @@ -542,6 +546,12 @@ sc_adb_select_device(struct sc_intr *intr, assert(selector->serial); LOGE("Could not find ADB device %s:", selector->serial); break; + case SC_ADB_DEVICE_SELECT_USB: + LOGE("Could not find any ADB device over USB:"); + break; + case SC_ADB_DEVICE_SELECT_TCPIP: + LOGE("Could not find any ADB device over TCP/IP:"); + break; default: assert(!"Unexpected selector type"); break; @@ -562,12 +572,21 @@ sc_adb_select_device(struct sc_intr *intr, LOGE("Multiple (%" SC_PRIsizet ") ADB devices with serial %s:", sel_count, selector->serial); break; + case SC_ADB_DEVICE_SELECT_USB: + LOGE("Multiple (%" SC_PRIsizet ") ADB devices over USB:", + sel_count); + break; + case SC_ADB_DEVICE_SELECT_TCPIP: + LOGE("Multiple (%" SC_PRIsizet ") ADB devices over TCP/IP:", + sel_count); + break; default: assert(!"Unexpected selector type"); break; } sc_adb_devices_log(SC_LOG_LEVEL_ERROR, devices, count); - LOGE("Select a device via -s (--serial)"); + LOGE("Select a device via -s (--serial), -d (--select-usb) or -e " + "(--select-tcpip)"); sc_adb_devices_destroy_all(devices, count); return false; } diff --git a/app/src/adb/adb.h b/app/src/adb/adb.h index dc302a85..6ea6e897 100644 --- a/app/src/adb/adb.h +++ b/app/src/adb/adb.h @@ -21,6 +21,8 @@ sc_adb_get_executable(void); enum sc_adb_device_selector_type { SC_ADB_DEVICE_SELECT_ALL, SC_ADB_DEVICE_SELECT_SERIAL, + SC_ADB_DEVICE_SELECT_USB, + SC_ADB_DEVICE_SELECT_TCPIP, }; struct sc_adb_device_selector { diff --git a/app/src/cli.c b/app/src/cli.c index 7567a10b..6e58feff 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -118,6 +118,12 @@ static const struct sc_option options[] = { "(typically, portrait for a phone, landscape for a tablet). " "Any --max-size value is cmoputed on the cropped size.", }, + { + .shortopt = 'd', + .longopt = "select-usb", + .text = "Use USB device (if there is exactly one, like adb -d).\n" + "Also see -e (--select-tcpip).", + }, { .longopt_id = OPT_DISABLE_SCREENSAVER, .longopt = "disable-screensaver", @@ -141,6 +147,12 @@ static const struct sc_option options[] = { "This increases latency to compensate for jitter.\n" "Default is 0 (no buffering).", }, + { + .shortopt = 'e', + .longopt = "select-tcpip", + .text = "Use TCP/IP device (if there is exactly one, like adb -e).\n" + "Also see -d (--select-usb).", + }, { .longopt_id = OPT_ENCODER_NAME, .longopt = "encoder", @@ -1320,6 +1332,12 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; } break; + case 'd': + opts->select_usb = true; + break; + case 'e': + opts->select_tcpip = true; + break; case 'f': opts->fullscreen = true; break; @@ -1559,8 +1577,16 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], // If a TCP/IP address is provided, then tcpip must be enabled assert(opts->tcpip || !opts->tcpip_dst); - if (opts->serial && opts->tcpip_dst) { - LOGE("Incompatible options: -s/--serial and --tcpip with an argument"); + unsigned selectors = !!opts->serial + + !!opts->tcpip_dst + + opts->select_tcpip + + opts->select_usb; + if (selectors > 1) { + LOGE("At most one device selector option may be passed, among:\n" + " --serial (-s)\n" + " --select-usb (-d)\n" + " --select-tcpip (-e)\n" + " --tcpip= (with an argument)"); return false; } diff --git a/app/src/options.c b/app/src/options.c index c94f798d..377c5590 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -60,4 +60,6 @@ const struct scrcpy_options scrcpy_options_default = { .downsize_on_error = true, .tcpip = false, .tcpip_dst = NULL, + .select_tcpip = false, + .select_usb = false, }; diff --git a/app/src/options.h b/app/src/options.h index 1591a065..44104734 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -135,6 +135,8 @@ struct scrcpy_options { bool downsize_on_error; bool tcpip; const char *tcpip_dst; + bool select_usb; + bool select_tcpip; }; extern const struct scrcpy_options scrcpy_options_default; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index d0aa2627..c3a481d0 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -297,6 +297,8 @@ scrcpy(struct scrcpy_options *options) { struct sc_server_params params = { .req_serial = options->serial, + .select_usb = options->select_usb, + .select_tcpip = options->select_tcpip, .log_level = options->log_level, .crop = options->crop, .port_range = options->port_range, diff --git a/app/src/server.c b/app/src/server.c index c0c33a2c..0de51c7e 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -687,10 +687,19 @@ run_server(void *data) { bool need_initial_serial = !params->tcpip_dst; if (need_initial_serial) { + // At most one of the 3 following parameters may be set + assert(!!params->req_serial + + params->select_usb + + params->select_tcpip <= 1); + struct sc_adb_device_selector selector; if (params->req_serial) { selector.type = SC_ADB_DEVICE_SELECT_SERIAL; selector.serial = params->req_serial; + } else if (params->select_usb) { + selector.type = SC_ADB_DEVICE_SELECT_USB; + } else if (params->select_tcpip) { + selector.type = SC_ADB_DEVICE_SELECT_TCPIP; } else { selector.type = SC_ADB_DEVICE_SELECT_ALL; } diff --git a/app/src/server.h b/app/src/server.h index c2293118..5818b43c 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -44,6 +44,8 @@ struct sc_server_params { bool downsize_on_error; bool tcpip; const char *tcpip_dst; + bool select_usb; + bool select_tcpip; }; struct sc_server { From dc5276b0e17cf7acd80726f3a31a8ac911f35e5e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 6 Feb 2022 18:45:02 +0100 Subject: [PATCH 1099/2244] Mention --select-usb and --select-tcpip in README PR #3005 --- README.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e8ddf907..dbbc4d71 100644 --- a/README.md +++ b/README.md @@ -422,7 +422,7 @@ scrcpy -b2M -m800 # short version #### Multi-devices -If several devices are listed in `adb devices`, you must specify the _serial_: +If several devices are listed in `adb devices`, you can specify the _serial_: ```bash scrcpy --serial 0123456789abcdef @@ -436,6 +436,19 @@ scrcpy --serial 192.168.0.1:5555 scrcpy -s 192.168.0.1:5555 # short version ``` +If only one device is connected via either USB or TCP/IP, it is possible to +select it automatically: + +```bash +# Select the only device connected via USB +scrcpy -d # like adb -d +scrcpy --select-usb # long version + +# Select the only device connected via TCP/IP +scrcpy -e # like adb -e +scrcpy --select-tcpip # long version +``` + You can start several instances of _scrcpy_ for several devices. #### Autostart on device connection From 29828aa3307c78f54a3169db156222ae91182468 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 9 Feb 2022 09:51:22 +0100 Subject: [PATCH 1100/2244] Log device opening errors during listing Without this log, the user would have no way to know that a USB device is rejected because it could not be opened (typically due to insufficient permissions). --- app/src/usb/usb.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/usb/usb.c b/app/src/usb/usb.c index 9edb1866..c7963726 100644 --- a/app/src/usb/usb.c +++ b/app/src/usb/usb.c @@ -38,6 +38,10 @@ sc_usb_read_device(libusb_device *device, struct sc_usb_device *out) { libusb_device_handle *handle; result = libusb_open(device, &handle); if (result < 0) { + // Log at debug level because it is expected that some non-Android USB + // devices present on the computer require special permissions + LOGD("Open USB device %04" PRIx16 ":%04" PRIx16 ": libusb error: %s", + desc.idVendor, desc.idProduct, libusb_strerror(result)); return false; } From 9477594f80d0851423d842993b7a658f33ace03b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 8 Feb 2022 20:59:38 +0100 Subject: [PATCH 1101/2244] Move version handling to a separate file This will avoid to include all dependencies headers from main.c. --- app/meson.build | 1 + app/src/main.c | 24 ++---------------------- app/src/version.c | 29 +++++++++++++++++++++++++++++ app/src/version.h | 9 +++++++++ 4 files changed, 41 insertions(+), 22 deletions(-) create mode 100644 app/src/version.c create mode 100644 app/src/version.h diff --git a/app/meson.build b/app/meson.build index a9b39b1e..6557c3b8 100644 --- a/app/meson.build +++ b/app/meson.build @@ -26,6 +26,7 @@ src = [ 'src/scrcpy.c', 'src/screen.c', 'src/server.c', + 'src/version.c', 'src/video_buffer.c', 'src/util/acksync.c', 'src/util/file.c', diff --git a/app/src/main.c b/app/src/main.c index 8a8c029c..2d1575f8 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -15,27 +15,7 @@ #include "scrcpy.h" #include "usb/scrcpy_otg.h" #include "util/log.h" - -static void -print_version(void) { - printf("\ndependencies:\n"); - printf(" - SDL %d.%d.%d\n", SDL_MAJOR_VERSION, SDL_MINOR_VERSION, - SDL_PATCHLEVEL); - printf(" - libavcodec %d.%d.%d\n", LIBAVCODEC_VERSION_MAJOR, - LIBAVCODEC_VERSION_MINOR, - LIBAVCODEC_VERSION_MICRO); - printf(" - libavformat %d.%d.%d\n", LIBAVFORMAT_VERSION_MAJOR, - LIBAVFORMAT_VERSION_MINOR, - LIBAVFORMAT_VERSION_MICRO); - printf(" - libavutil %d.%d.%d\n", LIBAVUTIL_VERSION_MAJOR, - LIBAVUTIL_VERSION_MINOR, - LIBAVUTIL_VERSION_MICRO); -#ifdef HAVE_V4L2 - printf(" - libavdevice %d.%d.%d\n", LIBAVDEVICE_VERSION_MAJOR, - LIBAVDEVICE_VERSION_MINOR, - LIBAVDEVICE_VERSION_MICRO); -#endif -} +#include "version.h" int main(int argc, char *argv[]) { @@ -71,7 +51,7 @@ main(int argc, char *argv[]) { } if (args.version) { - print_version(); + scrcpy_print_version(); return 0; } diff --git a/app/src/version.c b/app/src/version.c new file mode 100644 index 00000000..d0e93481 --- /dev/null +++ b/app/src/version.c @@ -0,0 +1,29 @@ +#include "version.h" + +#include +#include +#include +#ifdef HAVE_V4L2 +# include +#endif + +void +scrcpy_print_version(void) { + printf("\ndependencies:\n"); + printf(" - SDL %d.%d.%d\n", SDL_MAJOR_VERSION, SDL_MINOR_VERSION, + SDL_PATCHLEVEL); + printf(" - libavcodec %d.%d.%d\n", LIBAVCODEC_VERSION_MAJOR, + LIBAVCODEC_VERSION_MINOR, + LIBAVCODEC_VERSION_MICRO); + printf(" - libavformat %d.%d.%d\n", LIBAVFORMAT_VERSION_MAJOR, + LIBAVFORMAT_VERSION_MINOR, + LIBAVFORMAT_VERSION_MICRO); + printf(" - libavutil %d.%d.%d\n", LIBAVUTIL_VERSION_MAJOR, + LIBAVUTIL_VERSION_MINOR, + LIBAVUTIL_VERSION_MICRO); +#ifdef HAVE_V4L2 + printf(" - libavdevice %d.%d.%d\n", LIBAVDEVICE_VERSION_MAJOR, + LIBAVDEVICE_VERSION_MINOR, + LIBAVDEVICE_VERSION_MICRO); +#endif +} diff --git a/app/src/version.h b/app/src/version.h new file mode 100644 index 00000000..920360e8 --- /dev/null +++ b/app/src/version.h @@ -0,0 +1,9 @@ +#ifndef SC_VERSION_H +#define SC_VERSION_H + +#include "common.h" + +void +scrcpy_print_version(void); + +#endif From 9a546ef1afccb7457fea29ad8b51d8ae24137e46 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 8 Feb 2022 21:06:49 +0100 Subject: [PATCH 1102/2244] Print both compiled and linked versions of libs On --version, print both the version scrcpy had been compiled against, and the version linked at runtime. --- app/src/version.c | 58 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 43 insertions(+), 15 deletions(-) diff --git a/app/src/version.c b/app/src/version.c index d0e93481..40688aae 100644 --- a/app/src/version.c +++ b/app/src/version.c @@ -9,21 +9,49 @@ void scrcpy_print_version(void) { - printf("\ndependencies:\n"); - printf(" - SDL %d.%d.%d\n", SDL_MAJOR_VERSION, SDL_MINOR_VERSION, - SDL_PATCHLEVEL); - printf(" - libavcodec %d.%d.%d\n", LIBAVCODEC_VERSION_MAJOR, - LIBAVCODEC_VERSION_MINOR, - LIBAVCODEC_VERSION_MICRO); - printf(" - libavformat %d.%d.%d\n", LIBAVFORMAT_VERSION_MAJOR, - LIBAVFORMAT_VERSION_MINOR, - LIBAVFORMAT_VERSION_MICRO); - printf(" - libavutil %d.%d.%d\n", LIBAVUTIL_VERSION_MAJOR, - LIBAVUTIL_VERSION_MINOR, - LIBAVUTIL_VERSION_MICRO); + printf("\nDependencies (compiled / linked):\n"); + + SDL_version sdl; + SDL_GetVersion(&sdl); + printf(" - SDL: %u.%u.%u / %u.%u.%u\n", + SDL_MAJOR_VERSION, SDL_MINOR_VERSION, SDL_PATCHLEVEL, + (unsigned) sdl.major, (unsigned) sdl.minor, (unsigned) sdl.patch); + + unsigned avcodec = avcodec_version(); + printf(" - libavcodec: %u.%u.%u / %u.%u.%u\n", + LIBAVCODEC_VERSION_MAJOR, + LIBAVCODEC_VERSION_MINOR, + LIBAVCODEC_VERSION_MICRO, + AV_VERSION_MAJOR(avcodec), + AV_VERSION_MINOR(avcodec), + AV_VERSION_MICRO(avcodec)); + + unsigned avformat = avformat_version(); + printf(" - libavformat: %u.%u.%u / %u.%u.%u\n", + LIBAVFORMAT_VERSION_MAJOR, + LIBAVFORMAT_VERSION_MINOR, + LIBAVFORMAT_VERSION_MICRO, + AV_VERSION_MAJOR(avformat), + AV_VERSION_MINOR(avformat), + AV_VERSION_MICRO(avformat)); + + unsigned avutil = avutil_version(); + printf(" - libavutil: %u.%u.%u / %u.%u.%u\n", + LIBAVUTIL_VERSION_MAJOR, + LIBAVUTIL_VERSION_MINOR, + LIBAVUTIL_VERSION_MICRO, + AV_VERSION_MAJOR(avutil), + AV_VERSION_MINOR(avutil), + AV_VERSION_MICRO(avutil)); + #ifdef HAVE_V4L2 - printf(" - libavdevice %d.%d.%d\n", LIBAVDEVICE_VERSION_MAJOR, - LIBAVDEVICE_VERSION_MINOR, - LIBAVDEVICE_VERSION_MICRO); + unsigned avdevice = avdevice_version(); + printf(" - libavdevice: %u.%u.%u / %u.%u.%u\n", + LIBAVDEVICE_VERSION_MAJOR, + LIBAVDEVICE_VERSION_MINOR, + LIBAVDEVICE_VERSION_MICRO, + AV_VERSION_MAJOR(avdevice), + AV_VERSION_MINOR(avdevice), + AV_VERSION_MICRO(avdevice)); #endif } From f86df817f908a5fd5e7e6eaf9565ec76b5ead092 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 8 Feb 2022 21:28:55 +0100 Subject: [PATCH 1103/2244] Print libusb version on --version --- app/src/version.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/src/version.c b/app/src/version.c index 40688aae..90ea3334 100644 --- a/app/src/version.c +++ b/app/src/version.c @@ -6,6 +6,9 @@ #ifdef HAVE_V4L2 # include #endif +#ifdef HAVE_USB +# include +#endif void scrcpy_print_version(void) { @@ -54,4 +57,11 @@ scrcpy_print_version(void) { AV_VERSION_MINOR(avdevice), AV_VERSION_MICRO(avdevice)); #endif + +#ifdef HAVE_USB + const struct libusb_version *usb = libusb_get_version(); + // The compiled version may not be known + printf(" - libusb: - / %u.%u.%u\n", + (unsigned) usb->major, (unsigned) usb->minor, (unsigned) usb->micro); +#endif } From c00a31f1b063280a763b44b61a7cf4aab6576ff6 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 9 Feb 2022 18:16:34 +0100 Subject: [PATCH 1104/2244] Pass --buildtype=release as a single meson arg For consistency with the other arguments --- BUILD.md | 4 ++-- install_release.sh | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/BUILD.md b/BUILD.md index 1e713b93..f683be95 100644 --- a/BUILD.md +++ b/BUILD.md @@ -258,7 +258,7 @@ set ANDROID_SDK_ROOT=%LOCALAPPDATA%\Android\sdk Then, build: ```bash -meson x --buildtype release --strip -Db_lto=true +meson x --buildtype=release --strip -Db_lto=true ninja -Cx # DO NOT RUN AS ROOT ``` @@ -279,7 +279,7 @@ Download the prebuilt server somewhere, and specify its path during the Meson configuration: ```bash -meson x --buildtype release --strip -Db_lto=true \ +meson x --buildtype=release --strip -Db_lto=true \ -Dprebuilt_server=/path/to/scrcpy-server ninja -Cx # DO NOT RUN AS ROOT ``` diff --git a/install_release.sh b/install_release.sh index 69dfefdc..6dd71d25 100755 --- a/install_release.sh +++ b/install_release.sh @@ -12,7 +12,7 @@ echo "$PREBUILT_SERVER_SHA256 scrcpy-server" | sha256sum --check echo "[scrcpy] Building client..." rm -rf "$BUILDDIR" -meson "$BUILDDIR" --buildtype release --strip -Db_lto=true \ +meson "$BUILDDIR" --buildtype=release --strip -Db_lto=true \ -Dprebuilt_server=scrcpy-server cd "$BUILDDIR" ninja From 8498a2e8a669a28fb1078575a09b32e9f020658d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 9 Feb 2022 23:01:49 +0100 Subject: [PATCH 1105/2244] Reorder release.mk recipes Group prepare-deps for win32 and win64. --- release.mk | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/release.mk b/release.mk index 37b8d5c5..755e94d4 100644 --- a/release.mk +++ b/release.mk @@ -67,6 +67,11 @@ prepare-deps-win32: @prebuilt-deps/prepare-sdl.sh @prebuilt-deps/prepare-ffmpeg-win32.sh +prepare-deps-win64: + @prebuilt-deps/prepare-adb.sh + @prebuilt-deps/prepare-sdl.sh + @prebuilt-deps/prepare-ffmpeg-win64.sh + build-win32: prepare-deps-win32 [ -d "$(WIN32_BUILD_DIR)" ] || ( mkdir "$(WIN32_BUILD_DIR)" && \ meson "$(WIN32_BUILD_DIR)" \ @@ -76,11 +81,6 @@ build-win32: prepare-deps-win32 -Dportable=true ) ninja -C "$(WIN32_BUILD_DIR)" -prepare-deps-win64: - @prebuilt-deps/prepare-adb.sh - @prebuilt-deps/prepare-sdl.sh - @prebuilt-deps/prepare-ffmpeg-win64.sh - build-win64: prepare-deps-win64 [ -d "$(WIN64_BUILD_DIR)" ] || ( mkdir "$(WIN64_BUILD_DIR)" && \ meson "$(WIN64_BUILD_DIR)" \ From 8d583d36e259ba7f5f21d7a703cca73184200aa9 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 9 Feb 2022 23:10:38 +0100 Subject: [PATCH 1106/2244] Move prebuilt-deps/ to app/ The prebuilt dependencies are specific to the client app (not the server). This also avoids to reference the parent directory (../) from app/meson.build. --- app/meson.build | 10 ++-- .../prebuilt-deps}/.gitignore | 0 {prebuilt-deps => app/prebuilt-deps}/common | 0 .../prebuilt-deps}/prepare-adb.sh | 0 .../prebuilt-deps}/prepare-ffmpeg-win32.sh | 0 .../prebuilt-deps}/prepare-ffmpeg-win64.sh | 0 .../prebuilt-deps}/prepare-sdl.sh | 0 release.mk | 48 +++++++++---------- 8 files changed, 29 insertions(+), 29 deletions(-) rename {prebuilt-deps => app/prebuilt-deps}/.gitignore (100%) rename {prebuilt-deps => app/prebuilt-deps}/common (100%) rename {prebuilt-deps => app/prebuilt-deps}/prepare-adb.sh (100%) rename {prebuilt-deps => app/prebuilt-deps}/prepare-ffmpeg-win32.sh (100%) rename {prebuilt-deps => app/prebuilt-deps}/prepare-ffmpeg-win64.sh (100%) rename {prebuilt-deps => app/prebuilt-deps}/prepare-sdl.sh (100%) diff --git a/app/meson.build b/app/meson.build index 6557c3b8..123bcf62 100644 --- a/app/meson.build +++ b/app/meson.build @@ -111,9 +111,9 @@ if not crossbuild_windows else # cross-compile mingw32 build (from Linux to Windows) prebuilt_sdl2 = meson.get_cross_property('prebuilt_sdl2') - sdl2_bin_dir = meson.current_source_dir() + '/../prebuilt-deps/data/' + prebuilt_sdl2 + '/bin' - sdl2_lib_dir = meson.current_source_dir() + '/../prebuilt-deps/data/' + prebuilt_sdl2 + '/lib' - sdl2_include_dir = '../prebuilt-deps/data/' + prebuilt_sdl2 + '/include' + sdl2_bin_dir = meson.current_source_dir() + '/prebuilt-deps/data/' + prebuilt_sdl2 + '/bin' + sdl2_lib_dir = meson.current_source_dir() + '/prebuilt-deps/data/' + prebuilt_sdl2 + '/lib' + sdl2_include_dir = 'prebuilt-deps/data/' + prebuilt_sdl2 + '/include' sdl2 = declare_dependency( dependencies: [ @@ -124,8 +124,8 @@ else ) prebuilt_ffmpeg = meson.get_cross_property('prebuilt_ffmpeg') - ffmpeg_bin_dir = meson.current_source_dir() + '/../prebuilt-deps/data/' + prebuilt_ffmpeg + '/bin' - ffmpeg_include_dir = '../prebuilt-deps/data/' + prebuilt_ffmpeg + '/include' + ffmpeg_bin_dir = meson.current_source_dir() + '/prebuilt-deps/data/' + prebuilt_ffmpeg + '/bin' + ffmpeg_include_dir = 'prebuilt-deps/data/' + prebuilt_ffmpeg + '/include' # ffmpeg versions are different for win32 and win64 builds ffmpeg_avcodec = meson.get_cross_property('ffmpeg_avcodec') diff --git a/prebuilt-deps/.gitignore b/app/prebuilt-deps/.gitignore similarity index 100% rename from prebuilt-deps/.gitignore rename to app/prebuilt-deps/.gitignore diff --git a/prebuilt-deps/common b/app/prebuilt-deps/common similarity index 100% rename from prebuilt-deps/common rename to app/prebuilt-deps/common diff --git a/prebuilt-deps/prepare-adb.sh b/app/prebuilt-deps/prepare-adb.sh similarity index 100% rename from prebuilt-deps/prepare-adb.sh rename to app/prebuilt-deps/prepare-adb.sh diff --git a/prebuilt-deps/prepare-ffmpeg-win32.sh b/app/prebuilt-deps/prepare-ffmpeg-win32.sh similarity index 100% rename from prebuilt-deps/prepare-ffmpeg-win32.sh rename to app/prebuilt-deps/prepare-ffmpeg-win32.sh diff --git a/prebuilt-deps/prepare-ffmpeg-win64.sh b/app/prebuilt-deps/prepare-ffmpeg-win64.sh similarity index 100% rename from prebuilt-deps/prepare-ffmpeg-win64.sh rename to app/prebuilt-deps/prepare-ffmpeg-win64.sh diff --git a/prebuilt-deps/prepare-sdl.sh b/app/prebuilt-deps/prepare-sdl.sh similarity index 100% rename from prebuilt-deps/prepare-sdl.sh rename to app/prebuilt-deps/prepare-sdl.sh diff --git a/release.mk b/release.mk index 755e94d4..aff9bd89 100644 --- a/release.mk +++ b/release.mk @@ -63,14 +63,14 @@ build-server: ninja -C "$(SERVER_BUILD_DIR)" prepare-deps-win32: - @prebuilt-deps/prepare-adb.sh - @prebuilt-deps/prepare-sdl.sh - @prebuilt-deps/prepare-ffmpeg-win32.sh + @app/prebuilt-deps/prepare-adb.sh + @app/prebuilt-deps/prepare-sdl.sh + @app/prebuilt-deps/prepare-ffmpeg-win32.sh prepare-deps-win64: - @prebuilt-deps/prepare-adb.sh - @prebuilt-deps/prepare-sdl.sh - @prebuilt-deps/prepare-ffmpeg-win64.sh + @app/prebuilt-deps/prepare-adb.sh + @app/prebuilt-deps/prepare-sdl.sh + @app/prebuilt-deps/prepare-ffmpeg-win64.sh build-win32: prepare-deps-win32 [ -d "$(WIN32_BUILD_DIR)" ] || ( mkdir "$(WIN32_BUILD_DIR)" && \ @@ -98,15 +98,15 @@ dist-win32: build-server build-win32 cp data/scrcpy-noconsole.vbs "$(DIST)/$(WIN32_TARGET_DIR)" cp data/icon.png "$(DIST)/$(WIN32_TARGET_DIR)" cp data/open_a_terminal_here.bat "$(DIST)/$(WIN32_TARGET_DIR)" - cp prebuilt-deps/data/ffmpeg-win32-4.3.1/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp prebuilt-deps/data/ffmpeg-win32-4.3.1/bin/avcodec-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp prebuilt-deps/data/ffmpeg-win32-4.3.1/bin/avformat-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp prebuilt-deps/data/ffmpeg-win32-4.3.1/bin/swresample-3.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp prebuilt-deps/data/ffmpeg-win32-4.3.1/bin/swscale-5.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp prebuilt-deps/data/platform-tools-31.0.3/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/" - cp prebuilt-deps/data/platform-tools-31.0.3/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp prebuilt-deps/data/platform-tools-31.0.3/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp prebuilt-deps/data/SDL2-2.0.20/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-win32-4.3.1/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-win32-4.3.1/bin/avcodec-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-win32-4.3.1/bin/avformat-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-win32-4.3.1/bin/swresample-3.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-win32-4.3.1/bin/swscale-5.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/platform-tools-31.0.3/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/platform-tools-31.0.3/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/platform-tools-31.0.3/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/SDL2-2.0.20/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/" dist-win64: build-server build-win64 mkdir -p "$(DIST)/$(WIN64_TARGET_DIR)" @@ -116,15 +116,15 @@ dist-win64: build-server build-win64 cp data/scrcpy-noconsole.vbs "$(DIST)/$(WIN64_TARGET_DIR)" cp data/icon.png "$(DIST)/$(WIN64_TARGET_DIR)" cp data/open_a_terminal_here.bat "$(DIST)/$(WIN64_TARGET_DIR)" - cp prebuilt-deps/data/ffmpeg-win64-5.0/bin/avutil-57.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp prebuilt-deps/data/ffmpeg-win64-5.0/bin/avcodec-59.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp prebuilt-deps/data/ffmpeg-win64-5.0/bin/avformat-59.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp prebuilt-deps/data/ffmpeg-win64-5.0/bin/swresample-4.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp prebuilt-deps/data/ffmpeg-win64-5.0/bin/swscale-6.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp prebuilt-deps/data/platform-tools-31.0.3/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/" - cp prebuilt-deps/data/platform-tools-31.0.3/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp prebuilt-deps/data/platform-tools-31.0.3/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp prebuilt-deps/data/SDL2-2.0.20/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-win64-5.0/bin/avutil-57.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-win64-5.0/bin/avcodec-59.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-win64-5.0/bin/avformat-59.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-win64-5.0/bin/swresample-4.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-win64-5.0/bin/swscale-6.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/platform-tools-31.0.3/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/platform-tools-31.0.3/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/platform-tools-31.0.3/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/SDL2-2.0.20/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/" zip-win32: dist-win32 cd "$(DIST)/$(WIN32_TARGET_DIR)"; \ From 43ae418752b30738a7bd3fd5beb55b9476697022 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 10 Feb 2022 08:46:14 +0100 Subject: [PATCH 1107/2244] Fix USB device leak on connection error If sc_usb_connect() failed, then the sc_usb_device was never destroyed. The assignment was mistakenly removed by commit 61969aeb80093d0777c7716a61698cbdaf9ddd71. --- app/src/usb/scrcpy_otg.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/usb/scrcpy_otg.c b/app/src/usb/scrcpy_otg.c index 9a7e3fe6..d3a45679 100644 --- a/app/src/usb/scrcpy_otg.c +++ b/app/src/usb/scrcpy_otg.c @@ -89,6 +89,8 @@ scrcpy_otg(struct scrcpy_options *options) { goto end; } + usb_device_initialized = true; + LOGI("USB device: %s (%04" PRIx16 ":%04" PRIx16 ") %s %s", usb_device.serial, usb_device.vid, usb_device.pid, usb_device.manufacturer, usb_device.product); From 7848a387c8281cb156f0e0da6bbbb05cda31db22 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 9 Feb 2022 21:06:16 +0100 Subject: [PATCH 1108/2244] Do not duplicate relative mouse mode state The relative mouse mode is tracked by SDL, and accessible via SDL_GetRelativeMouseMode(). This is more robust in case SDL changes the relative mouse mode on its own. --- app/src/screen.c | 27 ++++++++++++++++----------- app/src/screen.h | 1 - app/src/usb/screen_otg.c | 32 ++++++++++++++++++-------------- app/src/usb/screen_otg.h | 1 - 4 files changed, 34 insertions(+), 27 deletions(-) diff --git a/app/src/screen.c b/app/src/screen.c index 98626909..9e9ff3ff 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -163,14 +163,21 @@ sc_screen_is_relative_mode(struct sc_screen *screen) { } static void -sc_screen_capture_mouse(struct sc_screen *screen, bool capture) { +sc_screen_set_mouse_capture(bool capture) { if (SDL_SetRelativeMouseMode(capture)) { LOGE("Could not set relative mouse mode to %s: %s", capture ? "true" : "false", SDL_GetError()); - return; } +} - screen->mouse_captured = capture; +static inline bool +sc_screen_get_mouse_capture(void) { + return SDL_GetRelativeMouseMode(); +} + +static inline void +sc_screen_toggle_mouse_capture(void) { + sc_screen_set_mouse_capture(!sc_screen_get_mouse_capture()); } static void @@ -372,7 +379,6 @@ sc_screen_init(struct sc_screen *screen, screen->fullscreen = false; screen->maximized = false; screen->event_failed = false; - screen->mouse_captured = false; screen->mouse_capture_key_pressed = 0; screen->req.x = params->window_x; @@ -710,7 +716,7 @@ sc_screen_update_frame(struct sc_screen *screen) { if (sc_screen_is_relative_mode(screen)) { // Capture mouse on start - sc_screen_capture_mouse(screen, true); + sc_screen_set_mouse_capture(true); } } @@ -823,7 +829,7 @@ sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event) { break; case SDL_WINDOWEVENT_FOCUS_LOST: if (relative_mode) { - sc_screen_capture_mouse(screen, false); + sc_screen_set_mouse_capture(false); } break; } @@ -853,8 +859,7 @@ sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event) { if (key == cap) { // A mouse capture key has been pressed then released: // toggle the capture mouse mode - sc_screen_capture_mouse(screen, - !screen->mouse_captured); + sc_screen_toggle_mouse_capture(); } // Mouse capture keys are never forwarded to the device return; @@ -864,7 +869,7 @@ sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event) { case SDL_MOUSEWHEEL: case SDL_MOUSEMOTION: case SDL_MOUSEBUTTONDOWN: - if (relative_mode && !screen->mouse_captured) { + if (relative_mode && !sc_screen_get_mouse_capture()) { // Do not forward to input manager, the mouse will be captured // on SDL_MOUSEBUTTONUP return; @@ -880,8 +885,8 @@ sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event) { } break; case SDL_MOUSEBUTTONUP: - if (relative_mode && !screen->mouse_captured) { - sc_screen_capture_mouse(screen, true); + if (relative_mode && !sc_screen_get_mouse_capture()) { + sc_screen_set_mouse_capture(true); return; } break; diff --git a/app/src/screen.h b/app/src/screen.h index 13bd4d99..0ddefc43 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -60,7 +60,6 @@ struct sc_screen { bool event_failed; // in case SDL_PushEvent() returned an error - bool mouse_captured; // only relevant in relative mouse mode // To enable/disable mouse capture, a mouse capture key (LALT, LGUI or // RGUI) must be pressed. This variable tracks the pressed capture key. SDL_Keycode mouse_capture_key_pressed; diff --git a/app/src/usb/screen_otg.c b/app/src/usb/screen_otg.c index cda0da5e..f32bc946 100644 --- a/app/src/usb/screen_otg.c +++ b/app/src/usb/screen_otg.c @@ -5,15 +5,21 @@ #include "util/log.h" static void -sc_screen_otg_capture_mouse(struct sc_screen_otg *screen, bool capture) { - assert(screen->mouse); +sc_screen_otg_set_mouse_capture(bool capture) { if (SDL_SetRelativeMouseMode(capture)) { LOGE("Could not set relative mouse mode to %s: %s", capture ? "true" : "false", SDL_GetError()); - return; } +} - screen->mouse_captured = capture; +static inline bool +sc_screen_otg_get_mouse_capture(void) { + return SDL_GetRelativeMouseMode(); +} + +static inline void +sc_screen_otg_toggle_mouse_capture(void) { + sc_screen_otg_set_mouse_capture(!sc_screen_otg_get_mouse_capture()); } static void @@ -31,7 +37,6 @@ sc_screen_otg_init(struct sc_screen_otg *screen, screen->keyboard = params->keyboard; screen->mouse = params->mouse; - screen->mouse_captured = false; screen->mouse_capture_key_pressed = 0; const char *title = params->window_title; @@ -81,7 +86,7 @@ sc_screen_otg_init(struct sc_screen_otg *screen, if (screen->mouse) { // Capture mouse on start - sc_screen_otg_capture_mouse(screen, true); + sc_screen_otg_set_mouse_capture(true); } return true; @@ -193,7 +198,7 @@ sc_screen_otg_handle_event(struct sc_screen_otg *screen, SDL_Event *event) { break; case SDL_WINDOWEVENT_FOCUS_LOST: if (screen->mouse) { - sc_screen_otg_capture_mouse(screen, false); + sc_screen_otg_set_mouse_capture(false); } break; } @@ -227,8 +232,7 @@ sc_screen_otg_handle_event(struct sc_screen_otg *screen, SDL_Event *event) { if (key == cap) { // A mouse capture key has been pressed then released: // toggle the capture mouse mode - sc_screen_otg_capture_mouse(screen, - !screen->mouse_captured); + sc_screen_otg_toggle_mouse_capture(); } // Mouse capture keys are never forwarded to the device return; @@ -240,26 +244,26 @@ sc_screen_otg_handle_event(struct sc_screen_otg *screen, SDL_Event *event) { } break; case SDL_MOUSEMOTION: - if (screen->mouse && screen->mouse_captured) { + if (screen->mouse && sc_screen_otg_get_mouse_capture()) { sc_screen_otg_process_mouse_motion(screen, &event->motion); } break; case SDL_MOUSEBUTTONDOWN: - if (screen->mouse && screen->mouse_captured) { + if (screen->mouse && sc_screen_otg_get_mouse_capture()) { sc_screen_otg_process_mouse_button(screen, &event->button); } break; case SDL_MOUSEBUTTONUP: if (screen->mouse) { - if (screen->mouse_captured) { + if (sc_screen_otg_get_mouse_capture()) { sc_screen_otg_process_mouse_button(screen, &event->button); } else { - sc_screen_otg_capture_mouse(screen, true); + sc_screen_otg_set_mouse_capture(true); } } break; case SDL_MOUSEWHEEL: - if (screen->mouse && screen->mouse_captured) { + if (screen->mouse && sc_screen_otg_get_mouse_capture()) { sc_screen_otg_process_mouse_wheel(screen, &event->wheel); } break; diff --git a/app/src/usb/screen_otg.h b/app/src/usb/screen_otg.h index 3fa1c4ad..0973ce59 100644 --- a/app/src/usb/screen_otg.h +++ b/app/src/usb/screen_otg.h @@ -18,7 +18,6 @@ struct sc_screen_otg { SDL_Texture *texture; // See equivalent mechanism in screen.h - bool mouse_captured; SDL_Keycode mouse_capture_key_pressed; }; From 4a95c08d56063db10a0456dcdafb1f683cc6be52 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 10 Feb 2022 07:37:33 +0100 Subject: [PATCH 1109/2244] Improve error message for unsupported usb hotplug --- app/src/usb/usb.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/usb/usb.c b/app/src/usb/usb.c index c7963726..07fb9619 100644 --- a/app/src/usb/usb.c +++ b/app/src/usb/usb.c @@ -254,7 +254,8 @@ run_libusb_event_handler(void *data) { static bool sc_usb_register_callback(struct sc_usb *usb) { if (!libusb_has_capability(LIBUSB_CAP_HAS_HOTPLUG)) { - LOGW("libusb does not have hotplug capability"); + LOGW("On this platform, libusb does not have hotplug capability; " + "device disconnection will not be detected properly"); return false; } From 29c163959cfe9b25a4f431e7a504905da5309cb7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 10 Feb 2022 09:06:29 +0100 Subject: [PATCH 1110/2244] Indent ifdef for clarity Make it explicit that the ifdef is an inner block. --- app/src/cli.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index 6e58feff..5251c47c 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -1690,12 +1690,12 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], LOGE("OTG mode: could not select display"); return false; } -#ifdef HAVE_V4L2 +# ifdef HAVE_V4L2 if (opts->v4l2_device) { LOGE("OTG mode: could not sink to V4L2 device"); return false; } -#endif +# endif } #endif From e3c2398aa208d9f85e6cd5474e3201b8b5f203f9 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 11 Feb 2022 19:31:01 +0100 Subject: [PATCH 1111/2244] Store packet flags in PTS most significant bits A special PTS value was used to encode a config packet. To prepare for adding more flags, use the most significant bits of the PTS field to store flags. --- app/src/demuxer.c | 22 +++++++++++++++---- .../com/genymobile/scrcpy/ScreenEncoder.java | 4 ++-- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/app/src/demuxer.c b/app/src/demuxer.c index bebdb6d6..c2f4a636 100644 --- a/app/src/demuxer.c +++ b/app/src/demuxer.c @@ -13,7 +13,10 @@ #define BUFSIZE 0x10000 #define HEADER_SIZE 12 -#define NO_PTS UINT64_C(-1) + +#define SC_PACKET_FLAG_CONFIG (UINT64_C(1) << 63) + +#define SC_PACKET_PTS_MASK (SC_PACKET_FLAG_CONFIG - 1) static bool sc_demuxer_recv_packet(struct sc_demuxer *demuxer, AVPacket *packet) { @@ -28,6 +31,14 @@ sc_demuxer_recv_packet(struct sc_demuxer *demuxer, AVPacket *packet) { // size // // It is followed by bytes containing the packet/frame. + // + // The most significant bits of the PTS are used for packet flags: + // + // byte 7 byte 6 byte 5 byte 4 byte 3 byte 2 byte 1 byte 0 + // C....... ........ ........ ........ ........ ........ ........ ........ + // ^<--------------------------------------------------------------------> + // | PTS + // `- config packet uint8_t header[HEADER_SIZE]; ssize_t r = net_recv_all(demuxer->socket, header, HEADER_SIZE); @@ -35,9 +46,8 @@ sc_demuxer_recv_packet(struct sc_demuxer *demuxer, AVPacket *packet) { return false; } - uint64_t pts = buffer_read64be(header); + uint64_t pts_flags = buffer_read64be(header); uint32_t len = buffer_read32be(&header[8]); - assert(pts == NO_PTS || (pts & 0x8000000000000000) == 0); assert(len); if (av_new_packet(packet, len)) { @@ -51,7 +61,11 @@ sc_demuxer_recv_packet(struct sc_demuxer *demuxer, AVPacket *packet) { return false; } - packet->pts = pts != NO_PTS ? (int64_t) pts : AV_NOPTS_VALUE; + if (pts_flags & SC_PACKET_FLAG_CONFIG) { + packet->pts = AV_NOPTS_VALUE; + } else { + packet->pts = pts_flags & SC_PACKET_PTS_MASK; + } return true; } diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index 79efc17c..a51a8a32 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -28,7 +28,7 @@ public class ScreenEncoder implements Device.RotationListener { // Keep the values in descending order private static final int[] MAX_SIZE_FALLBACK = {2560, 1920, 1600, 1280, 1024, 800}; - private static final int NO_PTS = -1; + private static final long PACKET_FLAG_CONFIG = 1L << 63; private final AtomicBoolean rotationChanged = new AtomicBoolean(); private final ByteBuffer headerBuffer = ByteBuffer.allocate(12); @@ -183,7 +183,7 @@ public class ScreenEncoder implements Device.RotationListener { long pts; if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) { - pts = NO_PTS; // non-media data packet + pts = PACKET_FLAG_CONFIG; // non-media data packet } else { if (ptsOrigin == 0) { ptsOrigin = bufferInfo.presentationTimeUs; From 67068e4e3d7ab17de425651848eecea530ac8b88 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 11 Feb 2022 19:33:35 +0100 Subject: [PATCH 1112/2244] Pass key frame flag from the device MediaCodec indicates when a packet is a key frame. Transmit it to the client. --- app/src/demuxer.c | 16 +++++++++++----- .../com/genymobile/scrcpy/ScreenEncoder.java | 4 ++++ 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/app/src/demuxer.c b/app/src/demuxer.c index c2f4a636..417abd27 100644 --- a/app/src/demuxer.c +++ b/app/src/demuxer.c @@ -15,8 +15,9 @@ #define HEADER_SIZE 12 #define SC_PACKET_FLAG_CONFIG (UINT64_C(1) << 63) +#define SC_PACKET_FLAG_KEY_FRAME (UINT64_C(1) << 62) -#define SC_PACKET_PTS_MASK (SC_PACKET_FLAG_CONFIG - 1) +#define SC_PACKET_PTS_MASK (SC_PACKET_FLAG_KEY_FRAME - 1) static bool sc_demuxer_recv_packet(struct sc_demuxer *demuxer, AVPacket *packet) { @@ -35,10 +36,11 @@ sc_demuxer_recv_packet(struct sc_demuxer *demuxer, AVPacket *packet) { // The most significant bits of the PTS are used for packet flags: // // byte 7 byte 6 byte 5 byte 4 byte 3 byte 2 byte 1 byte 0 - // C....... ........ ........ ........ ........ ........ ........ ........ - // ^<--------------------------------------------------------------------> - // | PTS - // `- config packet + // CK...... ........ ........ ........ ........ ........ ........ ........ + // ^^<-------------------------------------------------------------------> + // || PTS + // | `- config packet + // `-- key frame uint8_t header[HEADER_SIZE]; ssize_t r = net_recv_all(demuxer->socket, header, HEADER_SIZE); @@ -67,6 +69,10 @@ sc_demuxer_recv_packet(struct sc_demuxer *demuxer, AVPacket *packet) { packet->pts = pts_flags & SC_PACKET_PTS_MASK; } + if (pts_flags & SC_PACKET_FLAG_KEY_FRAME) { + packet->flags |= AV_PKT_FLAG_KEY; + } + return true; } diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index a51a8a32..f97206ec 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -29,6 +29,7 @@ public class ScreenEncoder implements Device.RotationListener { private static final int[] MAX_SIZE_FALLBACK = {2560, 1920, 1600, 1280, 1024, 800}; private static final long PACKET_FLAG_CONFIG = 1L << 63; + private static final long PACKET_FLAG_KEY_FRAME = 1L << 62; private final AtomicBoolean rotationChanged = new AtomicBoolean(); private final ByteBuffer headerBuffer = ByteBuffer.allocate(12); @@ -189,6 +190,9 @@ public class ScreenEncoder implements Device.RotationListener { ptsOrigin = bufferInfo.presentationTimeUs; } pts = bufferInfo.presentationTimeUs - ptsOrigin; + if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0) { + pts |= PACKET_FLAG_KEY_FRAME; + } } headerBuffer.putLong(pts); From 1c02b58412db2646ba6e57a7e305d26482b9bc0d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 11 Feb 2022 19:35:47 +0100 Subject: [PATCH 1113/2244] Remove sc_demuxer_parse() Now that the key frame flag is known, parsing the packet is useless. --- app/src/demuxer.c | 28 +--------------------------- 1 file changed, 1 insertion(+), 27 deletions(-) diff --git a/app/src/demuxer.c b/app/src/demuxer.c index 417abd27..96c0192a 100644 --- a/app/src/demuxer.c +++ b/app/src/demuxer.c @@ -73,6 +73,7 @@ sc_demuxer_recv_packet(struct sc_demuxer *demuxer, AVPacket *packet) { packet->flags |= AV_PKT_FLAG_KEY; } + packet->dts = packet->pts; return true; } @@ -89,28 +90,6 @@ push_packet_to_sinks(struct sc_demuxer *demuxer, const AVPacket *packet) { return true; } -static void -sc_demuxer_parse(struct sc_demuxer *demuxer, AVPacket *packet) { - uint8_t *in_data = packet->data; - int in_len = packet->size; - uint8_t *out_data = NULL; - int out_len = 0; - int r = av_parser_parse2(demuxer->parser, demuxer->codec_ctx, - &out_data, &out_len, in_data, in_len, - AV_NOPTS_VALUE, AV_NOPTS_VALUE, -1); - - // PARSER_FLAG_COMPLETE_FRAMES is set - assert(r == in_len); - (void) r; - assert(out_len == in_len); - - if (demuxer->parser->key_frame == 1) { - packet->flags |= AV_PKT_FLAG_KEY; - } - - packet->dts = packet->pts; -} - static bool sc_demuxer_push_packet(struct sc_demuxer *demuxer, AVPacket *packet) { bool is_config = packet->pts == AV_NOPTS_VALUE; @@ -150,11 +129,6 @@ sc_demuxer_push_packet(struct sc_demuxer *demuxer, AVPacket *packet) { } } - if (!is_config) { - // data packet - sc_demuxer_parse(demuxer, packet); - } - bool ok = push_packet_to_sinks(demuxer, packet); if (!is_config && demuxer->pending) { From 6fc388f3691ce888331eb04a60014df51df31923 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 11 Feb 2022 21:34:12 +0100 Subject: [PATCH 1114/2244] Remove unused BUFSIZE --- app/src/demuxer.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/demuxer.c b/app/src/demuxer.c index 96c0192a..30f76a99 100644 --- a/app/src/demuxer.c +++ b/app/src/demuxer.c @@ -10,8 +10,6 @@ #include "util/buffer_util.h" #include "util/log.h" -#define BUFSIZE 0x10000 - #define HEADER_SIZE 12 #define SC_PACKET_FLAG_CONFIG (UINT64_C(1) << 63) From 044acc2259f2898730ef1ccd4d780e69e3705614 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 11 Feb 2022 21:34:58 +0100 Subject: [PATCH 1115/2244] Rename HEADER_SIZE to SC_PACKET_HEADER_SIZE Prefix the constant for consistency. --- app/src/demuxer.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/demuxer.c b/app/src/demuxer.c index 30f76a99..2d0449c3 100644 --- a/app/src/demuxer.c +++ b/app/src/demuxer.c @@ -10,7 +10,7 @@ #include "util/buffer_util.h" #include "util/log.h" -#define HEADER_SIZE 12 +#define SC_PACKET_HEADER_SIZE 12 #define SC_PACKET_FLAG_CONFIG (UINT64_C(1) << 63) #define SC_PACKET_FLAG_KEY_FRAME (UINT64_C(1) << 62) @@ -40,9 +40,9 @@ sc_demuxer_recv_packet(struct sc_demuxer *demuxer, AVPacket *packet) { // | `- config packet // `-- key frame - uint8_t header[HEADER_SIZE]; - ssize_t r = net_recv_all(demuxer->socket, header, HEADER_SIZE); - if (r < HEADER_SIZE) { + uint8_t header[SC_PACKET_HEADER_SIZE]; + ssize_t r = net_recv_all(demuxer->socket, header, SC_PACKET_HEADER_SIZE); + if (r < SC_PACKET_HEADER_SIZE) { return false; } From 5c62f3419d252d10cd8c9cbb7c918b358b81f2d0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 12 Feb 2022 09:12:46 +0100 Subject: [PATCH 1116/2244] Rename buffer util functions with sc_ prefix --- app/src/control_msg.c | 30 +++++++++++++++--------------- app/src/demuxer.c | 4 ++-- app/src/device_msg.c | 4 ++-- app/src/util/buffer_util.h | 24 ++++++++++++------------ app/tests/test_buffer_util.c | 12 ++++++------ 5 files changed, 37 insertions(+), 37 deletions(-) diff --git a/app/src/control_msg.c b/app/src/control_msg.c index e8ae4681..a57d8cc2 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -63,17 +63,17 @@ static const char *const copy_key_labels[] = { static void write_position(uint8_t *buf, const struct sc_position *position) { - buffer_write32be(&buf[0], position->point.x); - buffer_write32be(&buf[4], position->point.y); - buffer_write16be(&buf[8], position->screen_size.width); - buffer_write16be(&buf[10], position->screen_size.height); + sc_write32be(&buf[0], position->point.x); + sc_write32be(&buf[4], position->point.y); + sc_write16be(&buf[8], position->screen_size.width); + sc_write16be(&buf[10], position->screen_size.height); } // write length (4 bytes) + string (non null-terminated) static size_t write_string(const char *utf8, size_t max_len, unsigned char *buf) { size_t len = sc_str_utf8_truncation_index(utf8, max_len); - buffer_write32be(buf, len); + sc_write32be(buf, len); memcpy(&buf[4], utf8, len); return 4 + len; } @@ -94,9 +94,9 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, unsigned char *buf) { switch (msg->type) { case SC_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.repeat); - buffer_write32be(&buf[10], msg->inject_keycode.metastate); + sc_write32be(&buf[2], msg->inject_keycode.keycode); + sc_write32be(&buf[6], msg->inject_keycode.repeat); + sc_write32be(&buf[10], msg->inject_keycode.metastate); return 14; case SC_CONTROL_MSG_TYPE_INJECT_TEXT: { size_t len = @@ -106,20 +106,20 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, unsigned char *buf) { } case SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT: buf[1] = msg->inject_touch_event.action; - buffer_write64be(&buf[2], msg->inject_touch_event.pointer_id); + sc_write64be(&buf[2], msg->inject_touch_event.pointer_id); write_position(&buf[10], &msg->inject_touch_event.position); uint16_t pressure = to_fixed_point_16(msg->inject_touch_event.pressure); - buffer_write16be(&buf[22], pressure); - buffer_write32be(&buf[24], msg->inject_touch_event.buttons); + sc_write16be(&buf[22], pressure); + sc_write32be(&buf[24], msg->inject_touch_event.buttons); return 28; case SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT: write_position(&buf[1], &msg->inject_scroll_event.position); - buffer_write32be(&buf[13], + sc_write32be(&buf[13], (uint32_t) msg->inject_scroll_event.hscroll); - buffer_write32be(&buf[17], + sc_write32be(&buf[17], (uint32_t) msg->inject_scroll_event.vscroll); - buffer_write32be(&buf[21], msg->inject_scroll_event.buttons); + sc_write32be(&buf[21], msg->inject_scroll_event.buttons); return 25; case SC_CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON: buf[1] = msg->inject_keycode.action; @@ -128,7 +128,7 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, unsigned char *buf) { buf[1] = msg->get_clipboard.copy_key; return 2; case SC_CONTROL_MSG_TYPE_SET_CLIPBOARD: - buffer_write64be(&buf[1], msg->set_clipboard.sequence); + sc_write64be(&buf[1], msg->set_clipboard.sequence); buf[9] = !!msg->set_clipboard.paste; size_t len = write_string(msg->set_clipboard.text, SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH, diff --git a/app/src/demuxer.c b/app/src/demuxer.c index 2d0449c3..35ea4c06 100644 --- a/app/src/demuxer.c +++ b/app/src/demuxer.c @@ -46,8 +46,8 @@ sc_demuxer_recv_packet(struct sc_demuxer *demuxer, AVPacket *packet) { return false; } - uint64_t pts_flags = buffer_read64be(header); - uint32_t len = buffer_read32be(&header[8]); + uint64_t pts_flags = sc_read64be(header); + uint32_t len = sc_read32be(&header[8]); assert(len); if (av_new_packet(packet, len)) { diff --git a/app/src/device_msg.c b/app/src/device_msg.c index 9a93036b..4b4a4974 100644 --- a/app/src/device_msg.c +++ b/app/src/device_msg.c @@ -18,7 +18,7 @@ device_msg_deserialize(const unsigned char *buf, size_t len, msg->type = buf[0]; switch (msg->type) { case DEVICE_MSG_TYPE_CLIPBOARD: { - size_t clipboard_len = buffer_read32be(&buf[1]); + size_t clipboard_len = sc_read32be(&buf[1]); if (clipboard_len > len - 5) { return 0; // not available } @@ -36,7 +36,7 @@ device_msg_deserialize(const unsigned char *buf, size_t len, return 5 + clipboard_len; } case DEVICE_MSG_TYPE_ACK_CLIPBOARD: { - uint64_t sequence = buffer_read64be(&buf[1]); + uint64_t sequence = sc_read64be(&buf[1]); msg->ack_clipboard.sequence = sequence; return 9; } diff --git a/app/src/util/buffer_util.h b/app/src/util/buffer_util.h index 337bb262..a5456abf 100644 --- a/app/src/util/buffer_util.h +++ b/app/src/util/buffer_util.h @@ -1,5 +1,5 @@ -#ifndef BUFFER_UTIL_H -#define BUFFER_UTIL_H +#ifndef SC_BUFFER_UTIL_H +#define SC_BUFFER_UTIL_H #include "common.h" @@ -7,13 +7,13 @@ #include static inline void -buffer_write16be(uint8_t *buf, uint16_t value) { +sc_write16be(uint8_t *buf, uint16_t value) { buf[0] = value >> 8; buf[1] = value; } static inline void -buffer_write32be(uint8_t *buf, uint32_t value) { +sc_write32be(uint8_t *buf, uint32_t value) { buf[0] = value >> 24; buf[1] = value >> 16; buf[2] = value >> 8; @@ -21,25 +21,25 @@ buffer_write32be(uint8_t *buf, uint32_t value) { } static inline void -buffer_write64be(uint8_t *buf, uint64_t value) { - buffer_write32be(buf, value >> 32); - buffer_write32be(&buf[4], (uint32_t) value); +sc_write64be(uint8_t *buf, uint64_t value) { + sc_write32be(buf, value >> 32); + sc_write32be(&buf[4], (uint32_t) value); } static inline uint16_t -buffer_read16be(const uint8_t *buf) { +sc_read16be(const uint8_t *buf) { return (buf[0] << 8) | buf[1]; } static inline uint32_t -buffer_read32be(const uint8_t *buf) { +sc_read32be(const uint8_t *buf) { return ((uint32_t) buf[0] << 24) | (buf[1] << 16) | (buf[2] << 8) | buf[3]; } static inline uint64_t -buffer_read64be(const uint8_t *buf) { - uint32_t msb = buffer_read32be(buf); - uint32_t lsb = buffer_read32be(&buf[4]); +sc_read64be(const uint8_t *buf) { + uint32_t msb = sc_read32be(buf); + uint32_t lsb = sc_read32be(&buf[4]); return ((uint64_t) msb << 32) | lsb; } diff --git a/app/tests/test_buffer_util.c b/app/tests/test_buffer_util.c index c7c13bdd..a9fec896 100644 --- a/app/tests/test_buffer_util.c +++ b/app/tests/test_buffer_util.c @@ -8,7 +8,7 @@ static void test_buffer_write16be(void) { uint16_t val = 0xABCD; uint8_t buf[2]; - buffer_write16be(buf, val); + sc_write16be(buf, val); assert(buf[0] == 0xAB); assert(buf[1] == 0xCD); @@ -18,7 +18,7 @@ static void test_buffer_write32be(void) { uint32_t val = 0xABCD1234; uint8_t buf[4]; - buffer_write32be(buf, val); + sc_write32be(buf, val); assert(buf[0] == 0xAB); assert(buf[1] == 0xCD); @@ -30,7 +30,7 @@ static void test_buffer_write64be(void) { uint64_t val = 0xABCD1234567890EF; uint8_t buf[8]; - buffer_write64be(buf, val); + sc_write64be(buf, val); assert(buf[0] == 0xAB); assert(buf[1] == 0xCD); @@ -45,7 +45,7 @@ static void test_buffer_write64be(void) { static void test_buffer_read16be(void) { uint8_t buf[2] = {0xAB, 0xCD}; - uint16_t val = buffer_read16be(buf); + uint16_t val = sc_read16be(buf); assert(val == 0xABCD); } @@ -53,7 +53,7 @@ static void test_buffer_read16be(void) { static void test_buffer_read32be(void) { uint8_t buf[4] = {0xAB, 0xCD, 0x12, 0x34}; - uint32_t val = buffer_read32be(buf); + uint32_t val = sc_read32be(buf); assert(val == 0xABCD1234); } @@ -62,7 +62,7 @@ static void test_buffer_read64be(void) { uint8_t buf[8] = {0xAB, 0xCD, 0x12, 0x34, 0x56, 0x78, 0x90, 0xEF}; - uint64_t val = buffer_read64be(buf); + uint64_t val = sc_read64be(buf); assert(val == 0xABCD1234567890EF); } From d0ab8c0e7beb33c3a2f31c735a56a58277038be1 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 12 Feb 2022 12:44:02 +0100 Subject: [PATCH 1117/2244] Fix double adb tunnel closing On error, close the adb tunnel only if it has not already been closed beforehand. --- app/src/server.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/server.c b/app/src/server.c index 0de51c7e..a4090f00 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -482,8 +482,10 @@ fail: } } - // Always leave this function with tunnel disabled - sc_adb_tunnel_close(tunnel, &server->intr, serial); + if (tunnel->enabled) { + // Always leave this function with tunnel disabled + sc_adb_tunnel_close(tunnel, &server->intr, serial); + } return false; } From cc27771dd18529fafc450529615c72582104991f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 12 Feb 2022 12:33:40 +0100 Subject: [PATCH 1118/2244] Add compilation flag for V4L2 feature This allows to disable V4L2 support on Linux to build without libavdevice. --- app/meson.build | 2 +- app/src/cli.c | 3 ++- meson_options.txt | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/meson.build b/app/meson.build index 123bcf62..ef7a467e 100644 --- a/app/meson.build +++ b/app/meson.build @@ -69,7 +69,7 @@ else endif endif -v4l2_support = host_machine.system() == 'linux' +v4l2_support = get_option('v4l2') and host_machine.system() == 'linux' if v4l2_support src += [ 'src/v4l2_sink.c' ] endif diff --git a/app/src/cli.c b/app/src/cli.c index 5251c47c..19e21324 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -1549,7 +1549,8 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], opts->v4l2_device = optarg; break; #else - LOGE("V4L2 (--v4l2-sink) is only available on Linux."); + LOGE("V4L2 (--v4l2-sink) is disabled (or unsupported on this " + "platform)."); return false; #endif case OPT_V4L2_BUFFER: diff --git a/meson_options.txt b/meson_options.txt index d64e357f..13227d8a 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -4,3 +4,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('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")') +option('v4l2', type: 'boolean', value: true, description: 'Enable V4L2 feature when supported') From ca9e1a05148d46793de0118a8cbcef62877df534 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 12 Feb 2022 12:38:40 +0100 Subject: [PATCH 1119/2244] Add compilation flag for USB features This allows to disable HID/OTG features on Linux to build without libusb. --- app/meson.build | 2 +- app/src/cli.c | 12 ++++++------ meson_options.txt | 1 + 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/app/meson.build b/app/meson.build index ef7a467e..9ee38d5f 100644 --- a/app/meson.build +++ b/app/meson.build @@ -74,7 +74,7 @@ if v4l2_support src += [ 'src/v4l2_sink.c' ] endif -usb_support = host_machine.system() == 'linux' +usb_support = get_option('usb') and host_machine.system() == 'linux' if usb_support src += [ 'src/usb/aoa_hid.c', diff --git a/app/src/cli.c b/app/src/cli.c index 19e21324..63f4a4ed 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -1357,8 +1357,8 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], opts->keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_HID; break; #else - LOGE("HID over AOA (-K/--hid-keyboard) is not supported on " - "this platform. It is only available on Linux."); + LOGE("HID over AOA (-K/--hid-keyboard) is disabled (or " + "unsupported on this platform)."); return false; #endif case OPT_MAX_FPS: @@ -1376,8 +1376,8 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_HID; break; #else - LOGE("HID over AOA (-M/--hid-mouse) is not supported on this" - "platform. It is only available on Linux."); + LOGE("HID over AOA (-M/--hid-mouse) is disabled (or " + "unsupported on this platform)."); return false; #endif case OPT_LOCK_VIDEO_ORIENTATION: @@ -1540,8 +1540,8 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], opts->otg = true; break; #else - LOGE("OTG mode (--otg) is not supported on this platform. It " - "is only available on Linux."); + LOGE("OTG mode (--otg) is disabled (or unsupported on this " + "platform)."); return false; #endif case OPT_V4L2_SINK: diff --git a/meson_options.txt b/meson_options.txt index 13227d8a..d1030694 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -5,3 +5,4 @@ option('portable', type: 'boolean', value: false, description: 'Use scrcpy-serve 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")') option('v4l2', type: 'boolean', value: true, description: 'Enable V4L2 feature when supported') +option('usb', type: 'boolean', value: true, description: 'Enable HID/OTG features when supported') From bb991f829cda06e024caa613ae91c34f4cc908b7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 13 Feb 2022 17:17:39 +0100 Subject: [PATCH 1120/2244] Fix order of options In alphabetic order, "no-clipboard-autosync" is before "no-downsize-on-error". --- app/src/cli.c | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index 63f4a4ed..d2a991d9 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -250,13 +250,6 @@ static const struct sc_option options[] = { "is preserved.\n" "Default is 0 (unlimited).", }, - { - .longopt_id = OPT_NO_DOWNSIZE_ON_ERROR, - .longopt = "no-downsize-on-error", - .text = "By default, on MediaCodec error, scrcpy automatically tries " - "again with a lower definition.\n" - "This option disables this behavior.", - }, { .longopt_id = OPT_NO_CLIPBOARD_AUTOSYNC, .longopt = "no-clipboard-autosync", @@ -266,6 +259,13 @@ static const struct sc_option options[] = { "it changes.\n" "This option disables this automatic synchronization." }, + { + .longopt_id = OPT_NO_DOWNSIZE_ON_ERROR, + .longopt = "no-downsize-on-error", + .text = "By default, on MediaCodec error, scrcpy automatically tries " + "again with a lower definition.\n" + "This option disables this behavior.", + }, { .shortopt = 'n', .longopt = "no-control", From ccbe370cc52c5cd4315aec0d153b4108e6237498 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 13 Feb 2022 17:17:01 +0100 Subject: [PATCH 1121/2244] Add --no-cleanup option It might be useful not to cleanup on exit, for example to leave the screen turned off, or keep the server binary on the device (via the server option "cleanup=false"). Fixes #1764 PR #3020 --- app/scrcpy.1 | 6 ++++++ app/src/cli.c | 12 ++++++++++++ app/src/options.c | 1 + app/src/options.h | 1 + app/src/scrcpy.c | 1 + app/src/server.c | 4 ++++ app/src/server.h | 1 + .../main/java/com/genymobile/scrcpy/Options.java | 9 +++++++++ .../main/java/com/genymobile/scrcpy/Server.java | 16 +++++++++++----- 9 files changed, 46 insertions(+), 5 deletions(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 317cec05..13a9a97a 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -146,6 +146,12 @@ It may only work over USB, and is currently only supported on Linux. Also see \fB\-\-hid\-keyboard\fR. +.TP +.B \-\-no\-cleanup +By default, scrcpy removes the server binary from the device and restores the device state (show touches, stay awake and power mode) on exit. + +This option disables this cleanup. + .TP .B \-\-no\-clipboard\-autosync By default, scrcpy automatically synchronizes the computer clipboard to the device clipboard before injecting Ctrl+v, and the device clipboard to the computer clipboard whenever it changes. diff --git a/app/src/cli.c b/app/src/cli.c index d2a991d9..eb28b1d7 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -54,6 +54,7 @@ #define OPT_RAW_KEY_EVENTS 1034 #define OPT_NO_DOWNSIZE_ON_ERROR 1035 #define OPT_OTG 1036 +#define OPT_NO_CLEANUP 1037 struct sc_option { char shortopt; @@ -250,6 +251,14 @@ static const struct sc_option options[] = { "is preserved.\n" "Default is 0 (unlimited).", }, + { + .longopt_id = OPT_NO_CLEANUP, + .longopt = "no-cleanup", + .text = "By default, scrcpy removes the server binary from the device " + "and restores the device state (show touches, stay awake and " + "power mode) on exit.\n" + "This option disables this cleanup." + }, { .longopt_id = OPT_NO_CLIPBOARD_AUTOSYNC, .longopt = "no-clipboard-autosync", @@ -1535,6 +1544,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], case OPT_NO_DOWNSIZE_ON_ERROR: opts->downsize_on_error = false; break; + case OPT_NO_CLEANUP: + opts->cleanup = false; + break; case OPT_OTG: #ifdef HAVE_USB opts->otg = true; diff --git a/app/src/options.c b/app/src/options.c index 377c5590..5a717df5 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -62,4 +62,5 @@ const struct scrcpy_options scrcpy_options_default = { .tcpip_dst = NULL, .select_tcpip = false, .select_usb = false, + .cleanup = true, }; diff --git a/app/src/options.h b/app/src/options.h index 44104734..f96edb22 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -137,6 +137,7 @@ struct scrcpy_options { const char *tcpip_dst; bool select_usb; bool select_tcpip; + bool cleanup; }; extern const struct scrcpy_options scrcpy_options_default; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index c3a481d0..8c4920d6 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -320,6 +320,7 @@ scrcpy(struct scrcpy_options *options) { .downsize_on_error = options->downsize_on_error, .tcpip = options->tcpip, .tcpip_dst = options->tcpip_dst, + .cleanup = options->cleanup, }; static const struct sc_server_callbacks cbs = { diff --git a/app/src/server.c b/app/src/server.c index a4090f00..c12b03d4 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -244,6 +244,10 @@ execute_server(struct sc_server *server, // By default, downsize_on_error is true ADD_PARAM("downsize_on_error=false"); } + if (!params->cleanup) { + // By default, cleanup is true + ADD_PARAM("cleanup=false"); + } #undef ADD_PARAM diff --git a/app/src/server.h b/app/src/server.h index 5818b43c..5f630ca8 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -46,6 +46,7 @@ struct sc_server_params { const char *tcpip_dst; bool select_usb; bool select_tcpip; + bool cleanup; }; struct sc_server { diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 92cf1e7a..4842b635 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -21,6 +21,7 @@ public class Options { private boolean powerOffScreenOnClose; private boolean clipboardAutosync = true; private boolean downsizeOnError = true; + private boolean cleanup = true; // Options not used by the scrcpy client, but useful to use scrcpy-server directly private boolean sendDeviceMeta = true; // send device name and size @@ -155,6 +156,14 @@ public class Options { this.downsizeOnError = downsizeOnError; } + public boolean getCleanup() { + return cleanup; + } + + public void setCleanup(boolean cleanup) { + this.cleanup = cleanup; + } + public boolean getSendDeviceMeta() { return sendDeviceMeta; } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 8cf289cd..d7e522c7 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -51,11 +51,13 @@ public final class Server { } } - try { - CleanUp.configure(options.getDisplayId(), restoreStayOn, mustDisableShowTouchesOnCleanUp, restoreNormalPowerMode, - options.getPowerOffScreenOnClose()); - } catch (IOException e) { - Ln.e("Could not configure cleanup", e); + if (options.getCleanup()) { + try { + CleanUp.configure(options.getDisplayId(), restoreStayOn, mustDisableShowTouchesOnCleanUp, restoreNormalPowerMode, + options.getPowerOffScreenOnClose()); + } catch (IOException e) { + Ln.e("Could not configure cleanup", e); + } } } @@ -243,6 +245,10 @@ public final class Server { boolean downsizeOnError = Boolean.parseBoolean(value); options.setDownsizeOnError(downsizeOnError); break; + case "cleanup": + boolean cleanup = Boolean.parseBoolean(value); + options.setCleanup(cleanup); + break; case "send_device_meta": boolean sendDeviceMeta = Boolean.parseBoolean(value); options.setSendDeviceMeta(sendDeviceMeta); From b58b566fa5c1d7b5c5386399b592d203d7dd9bbf Mon Sep 17 00:00:00 2001 From: Firq Date: Mon, 14 Feb 2022 23:58:12 +0100 Subject: [PATCH 1122/2244] Add German translation of README.md PR #3023 Signed-off-by: Romain Vimont --- README.de.md | 1016 ++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 1 + 2 files changed, 1017 insertions(+) create mode 100644 README.de.md diff --git a/README.de.md b/README.de.md new file mode 100644 index 00000000..ca080cd3 --- /dev/null +++ b/README.de.md @@ -0,0 +1,1016 @@ +_Only the original [README](README.md) is guaranteed to be up-to-date._ + +# scrcpy (v1.22) + +scrcpy + +_ausgesprochen "**scr**een **c**o**py**"_ + +Diese Anwendung liefert sowohl Anzeige als auch Steuerung eines Android-Gerätes über USB (oder [über TCP/IP](#tcpip-kabellos)). Dabei wird kein _root_ Zugriff benötigt. +Die Anwendung funktioniert unter _GNU/Linux_, _Windows_ und _macOS_. + +![screenshot](assets/screenshot-debian-600.jpg) + +Dabei liegt der Fokus auf: + + - **Leichtigkeit**: native, nur Anzeige des Gerätedisplays + - **Leistung**: 30~120fps, abhängig vom Gerät + - **Qualität**: 1920×1080 oder mehr + - **Geringe Latenz**: [35~70ms][lowlatency] + - **Kurze Startzeit**: ~1 Sekunde um das erste Bild anzuzeigen + - **Keine Aufdringlichkeit**: Es wird keine installierte Software auf dem Gerät zurückgelassen + - **Nutzervorteile**: kein Account, keine Werbung, kein Internetzugriff notwendig + - **Freiheit**: gratis und open-source + +[lowlatency]: https://github.com/Genymobile/scrcpy/pull/646 + +Die Features beinhalten: + - [Aufnahme](#Aufnahme) + - Spiegeln mit [ausgeschaltetem Bildschirm](#bildschirm-ausschalten) + - [Copy&Paste](#copy-paste) in beide Richtungen + - [Einstellbare Qualität](#Aufnahmekonfiguration) + - Gerätebildschirm [als Webcam (V4L2)](#v4l2loopback) (nur Linux) + - [Simulation einer physischen Tastatur (HID)](#simulation-einer-physischen-tastatur-mit-hid) + (nur Linux) + - [Simulation einer physischen Maus (HID)](#simulation-einer-physischen-maus-mit-hid) + (nur Linux) + - [OTG Modus](#otg) (nur Linux) + - und mehr… + +## Voraussetzungen + +Das Android-Gerät benötigt mindestens API 21 (Android 5.0). + +Es muss sichergestellt sein, dass [adb debugging][enable-adb] auf dem Gerät aktiv ist. + +[enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling + +Auf manchen Geräten müssen zudem [weitere Optionen][control] aktiv sein um das Gerät mit Maus und Tastatur steuern zu können. + +[control]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323 + + +## Installation der App + +Packaging status + +### Zusammenfassung + + - Linux: `apt install scrcpy` + - Windows: [download][direct-win64] + - macOS: `brew install scrcpy` + +Direkt von der Source bauen: [BUILD] ([vereinfachter Prozess (englisch)][BUILD_simple]) + +[BUILD]: BUILD.md +[BUILD_simple]: BUILD.md#simple + + +### Linux + +Auf Debian und Ubuntu: + +``` +apt install scrcpy +``` + +Auf Arch Linux: + +``` +pacman -S scrcpy +``` + +Ein [Snap] package ist verfügbar: [`scrcpy`][snap-link]. + +[snap-link]: https://snapstats.org/snaps/scrcpy + +[snap]: https://en.wikipedia.org/wiki/Snappy_(package_manager) + +Für Fedora ist ein [COPR] package verfügbar: [`scrcpy`][copr-link]. + +[COPR]: https://fedoraproject.org/wiki/Category:Copr +[copr-link]: https://copr.fedorainfracloud.org/coprs/zeno/scrcpy/ + + +Für Gentoo ist ein [Ebuild] verfügbar: [`scrcpy/`][ebuild-link]. + +[Ebuild]: https://wiki.gentoo.org/wiki/Ebuild +[ebuild-link]: https://github.com/maggu2810/maggu2810-overlay/tree/master/app-mobilephone/scrcpy + +Die App kann zudem [manuell gebaut werden][BUILD] ([vereinfachter Prozess (englisch)][BUILD_simple]). + + +### Windows + +Für Windows ist der Einfachheit halber ein vorgebautes Archiv mit allen Abhängigkeiten (inklusive `adb`) vorhanden. + + - [README](README.md#windows) + +Es ist zudem in [Chocolatey] vorhanden: + +[Chocolatey]: https://chocolatey.org/ + +```bash +choco install scrcpy +choco install adb # falls noch nicht vorhanden +``` + +Und in [Scoop]: + +```bash +scoop install scrcpy +scoop install adb # falls noch nicht vorhanden +``` + +[Scoop]: https://scoop.sh + +Die App kann zudem [manuell gebaut werden][BUILD]. + + +### macOS + +Die Anwendung ist in [Homebrew] verfügbar. Installation: + +[Homebrew]: https://brew.sh/ + +```bash +brew install scrcpy +``` + +Es wird `adb` benötigt, auf welches über `PATH` zugegriffen werden kann. Falls noch nicht vorhanden: + +```bash +brew install android-platform-tools +``` + +Es ist außerdem in [MacPorts] vorhanden, welches adb bereits aufsetzt: + +```bash +sudo port install scrcpy +``` + +[MacPorts]: https://www.macports.org/ + + +Die Anwendung kann zudem [manuell gebaut werden][BUILD]. + + +## Ausführen + +Ein Android-Gerät anschließen und diese Befehle ausführen: + +```bash +scrcpy +``` + +Dabei werden Kommandozeilenargumente akzeptiert, aufgelistet per: + +```bash +scrcpy --help +``` + +## Funktionalitäten + +### Aufnahmekonfiguration + +#### Größe reduzieren + +Manchmal ist es sinnvoll, das Android-Gerät mit einer geringeren Auflösung zu spiegeln, um die Leistung zu erhöhen. + +Um die Höhe und Breite auf einen Wert zu limitieren (z.B. 1024): + +```bash +scrcpy --max-size 1024 +scrcpy -m 1024 # short version +``` + +Die andere Größe wird dabei so berechnet, dass das Seitenverhältnis des Gerätes erhalten bleibt. +In diesem Fall wird ein Gerät mit einer 1920×1080-Auflösung mit 1024×576 gespiegelt. + + +#### Ändern der Bit-Rate + +Die Standard-Bitrate ist 8 Mbps. Um die Bitrate zu ändern (z.B. zu 2 Mbps): + +```bash +scrcpy --bit-rate 2M +scrcpy -b 2M # Kurzversion +``` + +#### Limitieren der Bildwiederholrate + +Die Aufnahme-Bildwiederholrate kann begrenzt werden: + +```bash +scrcpy --max-fps 15 +``` + +Dies wird offiziell seit Android 10 unterstützt, kann jedoch bereits auf früheren Versionen funktionieren. + +#### Zuschneiden + +Der Geräte-Bildschirm kann zugeschnitten werden, sodass nur ein Teil gespiegelt wird. + +Dies ist beispielsweise nützlich, um nur ein Auge der Oculus Go zu spiegeln: + +```bash +scrcpy --crop 1224:1440:0:0 # 1224x1440 am Versatz (0,0) +``` + +Falls `--max-size` auch festgelegt ist, wird das Ändern der Größe nach dem Zuschneiden angewandt. + + +#### Feststellen der Videoorientierung + + +Um die Orientierung während dem Spiegeln festzustellen: + +```bash +scrcpy --lock-video-orientation # ursprüngliche (momentane) Orientierung +scrcpy --lock-video-orientation=0 # normale Orientierung +scrcpy --lock-video-orientation=1 # 90° gegen den Uhrzeigersinn +scrcpy --lock-video-orientation=2 # 180° +scrcpy --lock-video-orientation=3 # 90° mit dem Uhrzeigersinn +``` + +Dies beeinflusst die Aufnahmeausrichtung. + +Das [Fenster kann auch unabhängig rotiert](#Rotation) werden. + + +#### Encoder + +Manche Geräte besitzen mehr als einen Encoder. Manche dieser Encoder können dabei sogar zu Problemen oder Abstürzen führen. +Die Auswahl eines anderen Encoders ist möglich: + +```bash +scrcpy --encoder OMX.qcom.video.encoder.avc +``` + +Um eine Liste aller verfügbaren Encoder zu erhalten (eine Fehlermeldung gibt alle verfügbaren Encoder aus): + +```bash +scrcpy --encoder _ +``` + +### Aufnahme + +#### Aufnehmen von Videos + +Es ist möglich, das Display während des Spiegelns aufzunehmen: + +```bash +scrcpy --record file.mp4 +scrcpy -r file.mkv +``` + +Um das Spiegeln während des Aufnehmens zu deaktivieren: + +```bash +scrcpy --no-display --record file.mp4 +scrcpy -Nr file.mkv +# Unterbrechen der Aufnahme mit Strg+C +``` + +"Übersprungene Bilder" werden aufgenommen, selbst wenn sie in Echtzeit (aufgrund von Performancegründen) nicht dargestellt werden. Die Einzelbilder sind mit _Zeitstempeln_ des Gerätes versehen are, sodass eine [Paketverzögerungsvariation] nicht die Aufnahmedatei beeinträchtigt. + +[Paketverzögerungs-Variation]: https://www.wikide.wiki/wiki/en/Packet_delay_variation + + +#### v4l2loopback + +Auf Linux ist es möglich, den Video-Stream zu einem v4l2 loopback Gerät zu senden, sodass das Android-Gerät von jedem v4l2-fähigen Tool wie eine Webcam verwendet werden kann. + +Das Modul `v4l2loopback` muss dazu installiert werden: + +```bash +sudo apt install v4l2loopback-dkms +``` + +Um ein v4l2 Gerät zu erzeugen: + +```bash +sudo modprobe v4l2loopback +``` + +Dies erzeugt ein neues Video-Gerät in `/dev/videoN`, wobei `N` ein Integer ist (mehr [Optionen](https://github.com/umlaeute/v4l2loopback#options) sind verfügbar um mehrere Geräte oder Geräte mit spezifischen Nummern zu erzeugen). + +Um die aktivierten Geräte aufzulisten: + +```bash +# benötigt das v4l-utils package +v4l2-ctl --list-devices + +# simpel, kann aber ausreichend +ls /dev/video* +``` + +Um scrcpy mithilfe eines v4l2 sink zu starten: + +```bash +scrcpy --v4l2-sink=/dev/videoN +scrcpy --v4l2-sink=/dev/videoN --no-display # Fenster mit Spiegelung ausschalten +scrcpy --v4l2-sink=/dev/videoN -N # kurze Version +``` + +(`N` muss mit der Geräte-ID ersetzt werden, welche mit `ls /dev/video*` überprüft werden kann) + +Einmal aktiv, kann der Stream mit einem v4l2-fähigen Tool verwendet werden: + +```bash +ffplay -i /dev/videoN +vlc v4l2:///dev/videoN # VLC kann eine gewisse Bufferverzögerung herbeiführen +``` + +Beispielsweise kann das Video mithilfe von [OBS] aufgenommen werden. + +[OBS]: https://obsproject.com/ + + +#### Buffering + +Es ist möglich, Buffering hinzuzufügen. Dies erhöht die Latenz, reduziert aber etwaigen Jitter (see [#2464]). + +[#2464]: https://github.com/Genymobile/scrcpy/issues/2464 + +Diese Option ist sowohl für Video-Buffering: + +```bash +scrcpy --display-buffer=50 # fügt 50ms Buffering zum Display hinzu +``` + +als auch V4L2 sink verfügbar: + +```bash +scrcpy --v4l2-buffer=500 # fügt 500ms Buffering für v4l2 sink hinzu +``` + + +### Verbindung + +#### TCP/IP Kabellos + +_Scrcpy_ verwendet `adb`, um mit dem Gerät zu kommunizieren. `adb` kann sich per TCP/IP mit einem Gerät [verbinden]. Das Gerät muss dabei mit demselben Netzwerk wie der Computer verbunden sein. + +##### Automatisch + +Die Option `--tcpip` erlaubt es, die Verbindung automatisch zu konfigurieren. Dabei gibt es zwei Varianten. + +Falls das Gerät (verfügbar unter 192.168.1.1 in diesem Beispiel) bereit an einem Port (typically 5555) nach einkommenden adb-Verbindungen hört, dann führe diesen Befehl aus: + +```bash +scrcpy --tcpip=192.168.1.1 # Standard-Port ist 5555 +scrcpy --tcpip=192.168.1.1:5555 +``` + +Falls adb TCP/IP auf dem Gerät deaktiviert ist (oder falls die IP-Adresse des Gerätes nicht bekannt ist): Gerät per USB verbinden, anschließend diesen Befehl ausführen: + +```bash +scrcpy --tcpip # ohne weitere Argumente +``` + +Dies finden automatisch das Gerät und aktiviert den TCP/IP-Modus. Anschließend verbindet sich der Befehl mit dem Gerät bevor die Verbindung startet. + +##### Manuell + +Alternativ kann die TCP/IP-Verbindung auch manuell per `adb` aktiviert werden: + +1. Gerät mit demselben Wifi wie den Computer verbinden. +2. IP-Adresse des Gerätes herausfinden, entweder über Einstellungen → Über das Telefon → Status, oder indem dieser Befehl ausgeführt wird: + + ```bash + adb shell ip route | awk '{print $9}' + ``` + +3. Aktivieren von adb über TCP/IP auf dem Gerät: `adb tcpip 5555`. +4. Ausstecken des Geräts. +5. Verbinden zum Gerät: `adb connect DEVICE_IP:5555` _(`DEVICE_IP` ersetzen)_. +6. `scrcpy` wie normal ausführen. + +Es kann sinnvoll sein, die Bit-Rate sowie dei Auflösung zu reduzieren: + +```bash +scrcpy --bit-rate 2M --max-size 800 +scrcpy -b2M -m800 # kurze Version +``` + +[connect]: https://developer.android.com/studio/command-line/adb.html#wireless + + +#### Mehrere Geräte + +Falls mehrere Geräte unter `adb devices` aufgelistet werden, muss die _Seriennummer_ angegeben werden: + +```bash +scrcpy --serial 0123456789abcdef +scrcpy -s 0123456789abcdef # kurze Version +``` + +Falls das Gerät über TCP/IP verbunden ist: + +```bash +scrcpy --serial 192.168.0.1:5555 +scrcpy -s 192.168.0.1:5555 # kurze Version +``` + +Es können mehrere Instanzen von _scrcpy_ für mehrere Geräte gestartet werden. + +#### Autostart beim Verbinden eines Gerätes + +Hierfür kann [AutoAdb] verwendet werden: + +```bash +autoadb scrcpy -s '{}' +``` + +[AutoAdb]: https://github.com/rom1v/autoadb + +#### Tunnel + +Um sich zu einem entfernten Gerät zu verbinden, kann der `adb` Client mit einem remote-`adb`-Server verbunden werden (Voraussetzung: Gleiche Version des `adb`-Protokolls). + +##### Remote ADB Server + +Um sich zu einem Remote-`adb`-Server zu verbinden: Der Server muss auf allen Ports hören + +```bash +adb kill-server +adb -a nodaemon server start +# Diesen Dialog offen halten +``` + +**Warnung: Die gesamte Kommunikation zwischen adb und den Geräten ist unverschlüsselt.** + +Angenommen, der Server ist unter 192.168.1.2 verfügbar. Dann kann von einer anderen Kommandozeile scrcpy aufgeführt werden: + +```bash +export ADB_SERVER_SOCKET=tcp:192.168.1.2:5037 +scrcpy --tunnel-host=192.168.1.2 +``` + +Standardmäßig verwendet scrcpy den lokalen Port für die Einrichtung des `adb forward`-Tunnels (typischerweise `27183`, siehe `--port`). +Es ist zudem möglich, einen anderen Tunnel-Port zuzuweisen (sinnvoll in Situationen, bei welchen viele Weiterleitungen erfolgen): + +``` +scrcpy --tunnel-port=1234 +``` + + +##### SSH Tunnel + +Um mit einem Remote-`adb`-Server sicher zu kommunizieren, wird ein SSH-Tunnel empfohlen. + +Sicherstellen, dass der Remote-`adb`-Server läuft: + +```bash +adb start-server +``` + +Erzeugung eines SSH-Tunnels: + +```bash +# local 5038 --> remote 5037 +# local 27183 <-- remote 27183 +ssh -CN -L5038:localhost:5037 -R27183:localhost:27183 your_remote_computer +# Diesen Dialog geöffnet halten +``` + +Von einer anderen Kommandozeile aus scrcpy ausführen: + +```bash +export ADB_SERVER_SOCKET=tcp:localhost:5038 +scrcpy +``` + +Um das Aktivieren von Remote-Weiterleitung zu verhindern, kann eine Vorwärts-Verbindung verwendet werden (`-L` anstatt von `-R`): + +```bash +# local 5038 --> remote 5037 +# local 27183 --> remote 27183 +ssh -CN -L5038:localhost:5037 -L27183:localhost:27183 your_remote_computer +# Diesen Dialog geöffnet halten +``` + +Von einer anderen Kommandozeile aus scrcpy ausführen: + +```bash +export ADB_SERVER_SOCKET=tcp:localhost:5038 +scrcpy --force-adb-forward +``` + + +Wie für kabellose Verbindungen kann es sinnvoll sein, die Qualität zu reduzieren: + +``` +scrcpy -b2M -m800 --max-fps 15 +``` + +### Fensterkonfiguration + +#### Titel + +Standardmäßig ist der Fenstertitel das Gerätemodell. Der Titel kann jedoch geändert werden: + +```bash +scrcpy --window-title 'Mein Gerät' +``` + +#### Position und Größe + +Die anfängliche Fensterposition und Größe können festgelegt werden: + +```bash +scrcpy --window-x 100 --window-y 100 --window-width 800 --window-height 600 +``` + +#### Rahmenlos + +Um den Rahmen des Fensters zu deaktivieren: + +```bash +scrcpy --window-borderless +``` + +#### Immer im Vordergrund + +Um das Fenster immer im Vordergrund zu halten: + +```bash +scrcpy --always-on-top +``` + +#### Vollbild + +Die Anwendung kann direkt im Vollbildmodus gestartet werden: + +```bash +scrcpy --fullscreen +scrcpy -f # kurze Version +``` + +Das Vollbild kann dynamisch mit MOD+f gewechselt werden. + +#### Rotation + +Das Fenster kann rotiert werden: + +```bash +scrcpy --rotation 1 +``` + +Mögliche Werte sind: + - `0`: keine Rotation + - `1`: 90 grad gegen den Uhrzeigersinn + - `2`: 180 grad + - `3`: 90 grad mit dem Uhrzeigersinn + +die Rotation kann zudem dynamisch mit MOD+ +_(links)_ and MOD+ _(rechts)_ angepasst werden. + +_scrcpy_ schafft 3 verschiedene Rotationen: + - MOD+r erfordert von Gerät den Wechsel zwischen Hochformat und Querformat (die momentane App kann dies verweigern, wenn die geforderte Ausrichtung nicht unterstützt wird). + - [`--lock-video-orientation`](#feststellen-der-videoorientierung) ändert die Ausrichtung der Spiegelung (die Ausrichtung des an den Computer gesendeten Videos). Dies beeinflusst eventuelle Aufnahmen. + - `--rotation` (or MOD+/MOD+) rotiert nur das Fenster, eventuelle Aufnahmen sind hiervon nicht beeinflusst. + + +### Andere Spiegel-Optionen + +#### Lesezugriff + +Um die Steuerung (alles, was mit dem Gerät interagieren kann: Tasten, Mausklicks, Drag-and-drop von Dateien) zu deaktivieren: + +```bash +scrcpy --no-control +scrcpy -n +``` + +#### Anzeige + +Falls mehrere Displays vorhanden sind, kann das zu spiegelnde Display gewählt werden: + +```bash +scrcpy --display 1 +``` + +Die Liste an verfügbaren Displays kann mit diesem Befehl ausgegeben werden: + +```bash +adb shell dumpsys display # Nach "mDisplayId=" in der Ausgabe suchen +``` + +Das zweite Display kann nur gesteuert werden, wenn das Gerät Android 10 oder höher besitzt. Ansonsten wird das Display nur mit Lesezugriff gespiegelt. + + +#### Wach bleiben + +Um zu verhindern, dass das Gerät nach einer Weile in den Ruhezustand übergeht (solange es eingesteckt ist): + +```bash +scrcpy --stay-awake +scrcpy -w +``` + +Der ursprüngliche Zustand wird beim Schließen von scrcpy wiederhergestellt. + + +#### Bildschirm ausschalten + +Es ist möglich, beim Starten des Spiegelns mithilfe eines Kommandozeilenarguments den Bildschirm des Gerätes auszuschalten: + +```bash +scrcpy --turn-screen-off +scrcpy -S +``` + +Oder durch das Drücken von MOD+o jederzeit. + +Um das Display wieder einzuschalten muss MOD+Shift+o gedrückt werden. + +Auf Android aktiviert der `POWER` Knopf das Display immer. +Für den Komfort wird, wenn `POWER` via scrcpy gesendet wird (über Rechtsklick oder MOD+p), wird versucht, das Display nach einer kurzen Zeit wieder auszuschalten (falls es möglich ist). +Der physische `POWER` Button aktiviert das Display jedoch immer. + +Dies kann zudem nützlich sein, um das Gerät vom Ruhezustand abzuhalten: + +```bash +scrcpy --turn-screen-off --stay-awake +scrcpy -Sw +``` + +#### Ausschalten beim Schließen + +Um den Gerätebildschirm abzuschalten, wenn scrcpy geschlossen wird: + +```bash +scrcpy --power-off-on-close +``` + + +#### Anzeigen von Berührungen + +Für Präsentationen kann es sinnvoll sein, die physischen Berührungen anzuzeigen (auf dem physischen Gerät). + +Android stellt dieses Feature in den _Entwickleroptionen_ zur Verfügung. + +_Scrcpy_ stellt die Option zur Verfügung, dies beim Start zu aktivieren und beim Schließen auf den Ursprungszustand zurückzusetzen: + +```bash +scrcpy --show-touches +scrcpy -t +``` + +Anmerkung: Nur _physische Berührungen_ werden angezeigt (mit dem Finger auf dem Gerät). + + +#### Bildschirmschoner deaktivieren + +Standardmäßig unterbindet scrcpy nicht den Bildschirmschoner des Computers. + +Um den Bildschirmschoner zu unterbinden: + +```bash +scrcpy --disable-screensaver +``` + + +### Eingabesteuerung + +#### Geräte-Bildschirm drehen + +MOD+r drücken, um zwischen Hoch- und Querformat zu wechseln. + +Anmerkung: Dis funktioniert nur, wenn die momentan geöffnete App beide Rotationen unterstützt. + +#### Copy-paste + +Immer, wenn sich die Zwischenablage von Android ändert wird dies mit dem Computer synchronisiert. + +Jedes Strg wird an das Gerät weitergegeben. Insbesonders: + - Strg+c kopiert typischerweise + - Strg+x schneidet typischerweise aus + - Strg+v fügt typischerweise ein (nach der Computer-zu-Gerät-Synchronisation) + +Dies funktioniert typischerweise wie erwartet. + +Die wirkliche Funktionsweise hängt jedoch von der jeweiligen Anwendung ab. Beispielhaft sendet _Termux_ SIGINT bei Strg+c, und _K-9 Mail_ erzeugt eine neue Nachricht. + +Um kopieren, ausschneiden und einfügen in diesen Fällen zu verwenden (nur bei Android >= 7 unterstützt): + - MOD+c gibt `COPY` ein + - MOD+x gibt `CUT` ein + - MOD+v gibt `PASTE` ein (nach der Computer-zu-Gerät-Synchronisation) + +Zusätzlich erlaubt es MOD+Shift+v den momentanen Inhalt der Zwischenablage als eine Serie von Tastenevents einzugeben. +Dies ist nützlich, fall die Applikation kein Einfügen unterstützt (z.B. _Termux_). Jedoch kann nicht-ASCII-Inhalt dabei zerstört werden. + +**WARNUNG:** Das Einfügen der Computer-Zwischenablage in das Gerät (entweder mit Strg+v oder MOD+v) kopiert den Inhalt in die Zwischenablage des Gerätes. +Als Konsequenz kann somit jede Android-Applikation diesen Inhalt lesen. Das Einfügen von sensiblen Informationen wie Passwörtern sollte aus diesem Grund vermieden werden. + +Mache Geräte verhalten sich nicht wie erwartet, wenn die Zwischenablage per Programm verändert wird. +Die Option `--legacy-paste` wird bereitgestellt, welche das Verhalten von Strg+v und MOD+v so ändert, dass die Zwischenablage wie bei MOD+Shift+v als eine Serie von Tastenevents ausgeführt wird. + +Um die automatische Synchronisierung der Zwischenablage zu deaktivieren: +`--no-clipboard-autosync`. + +#### Ziehen zum Zoomen + +Um "Ziehen-zum-Zoomen" zu simulieren: Strg+_klicken-und-bewegen_. + +Genauer: Strg halten, während Linksklick gehalten wird. Solange Linksklick gehalten wird, skalieren und rotieren die Mausbewegungen den Inhalt (soweit von der jeweiligen App unterstützt). + +Konkret erzeugt scrcpy einen am Mittelpunkt des Displays gespiegelten, "virtuellen" Finger. + +#### Simulation einer physischen Tastatur mit HID + +Standardmäßig verwendet scrcpy Android-Tasten oder Textinjektion. Dies funktioniert zwar immer, jedoch nur mit ASCII. + +Auf Linux kann scrcpy mithilfe von HID eine physische Tastatur simulieren, um eine bessere Eingabeerfahrung zu gewährleisten (dies nutzt [USB HID over AOAv2][hid-aoav2]): Die virtuelle Tastatur wird deaktiviert, es funktioniert für alle Zeichen und mit IME. + +[hid-aoav2]: https://source.android.com/devices/accessories/aoa2#hid-support + +Dies funktioniert jedoch nur, wenn das Gerät über USB verbunden ist. Zudem wird dies momentan nur unter Linux unterstützt. + +Um diesen Modus zu aktivieren: + +```bash +scrcpy --hid-keyboard +scrcpy -K # kurze Version +``` + +Falls dies auf gewissen Gründen fehlschlägt (z.B. Gerät ist nicht über USB verbunden), so fällt scrcpy auf den Standardmodus zurück (mit einer Ausgabe in der Konsole). +Dies erlaubt es, dieselben Kommandozeilenargumente zu verwenden, egal ob das Gerät per USB oder TCP/IP verbunden ist. + +In diesem Modus werden rohe Tastenevents (scancodes) an das Gerät gesendet. +Aus diesem Grund muss ein nicht passenden Tastaturformat in den Einstellungen des Android-Gerätes unter Einstellungen → System → Sprache und Eingabe → [Physical keyboard] konfiguriert werden. + +Diese Einstellungsseite kann direkt mit diesem Befehl geöffnet werden: + +```bash +adb shell am start -a android.settings.HARD_KEYBOARD_SETTINGS +``` + +Diese Option ist jedoch nur verfügbar, wenn eine HID-Tastatur oder eine physische Tastatur verbunden sind. + +[Physical keyboard]: https://github.com/Genymobile/scrcpy/pull/2632#issuecomment-923756915 + +#### Simulation einer physischen Maus mit HID + +Ähnlich zu einer Tastatur kann auch eine Maus mithilfe von HID simuliert werden. +Wie zuvor funktioniert dies jedoch nur, wenn das Gerät über USB verbunden ist. Zudem wird dies momentan nur unter Linux unterstützt. + +Standardmäßig verwendet scrcpy Android Maus Injektionen mit absoluten Koordinaten. +Durch die Simulation einer physischen Maus erscheint auf dem Display des Geräts ein Mauszeiger, zu welchem die Bewegungen, Klicks und Scrollbewegungen relativ eingegeben werden. + +Um diesen Modus zu aktivieren: + +```bash +scrcpy --hid-mouse +scrcpy -M # kurze Version +``` + +Es kann zudem`--forward-all-clicks` übergeben werden, um [alle Mausklicks an das Gerät weiterzugeben](#rechtsklick-und-mittelklick). + +Wenn dieser Modus aktiv ist, ist der Mauszeiger des Computers auf dem Fenster gefangen (Zeiger verschwindet von Computer und erscheint auf dem Android-Gerät). + +Spezielle Tasteneingaben wie Alt oder Super ändern den Zustand des Mauszeigers (geben diesen wieder frei/fangen ihn wieder ein). +Eine dieser Tasten kann verwendet werden, um die Kontrolle der Maus wieder zurück an den Computer zu geben. + + +#### OTG + +Es ist möglich, _scrcpy_ so auszuführen, dass nur Maus und Tastatur, wie wenn diese direkt über ein OTG-Kabel verbunden wären, simuliert werden. + +In diesem Modus ist _adb_ nicht nötig, ebenso ist das Spiegeln der Anzeige deaktiviert. + +Um den OTG-Modus zu aktivieren: + +```bash +scrcpy --otg +# Seriennummer übergeben, falls mehrere Geräte vorhanden sind +scrcpy --otg -s 0123456789abcdef +``` + +Es ist möglich, nur HID-Tastatur oder HID-Maus zu aktivieren: + +```bash +scrcpy --otg --hid-keyboard # nur Tastatur +scrcpy --otg --hid-mouse # nur Maus +scrcpy --otg --hid-keyboard --hid-mouse # Tastatur und Maus +# Der Einfachheit halber sind standardmäßig beide aktiv +scrcpy --otg # Tastatur und Maus +``` + +Wie `--hid-keyboard` und `--hid-mouse` funktioniert dies nur, wenn das Gerät per USB verbunden ist. +Zudem wird dies momentan nur unter Linux unterstützt. + + +#### Textinjektions-Vorliebe + +Beim Tippen von Text werden zwei verschiedene [Events][textevents] generiert: + - _key events_, welche signalisieren, ob eine Taste gedrückt oder losgelassen wurde; + - _text events_, welche signalisieren, dass Text eingegeben wurde. + +Standardmäßig werden key events verwendet, da sich bei diesen die Tastatur in Spielen wie erwartet verhält (typischerweise für WASD). + +Dies kann jedoch [Probleme verursachen][prefertext]. Trifft man auf ein solches Problem, so kann dies mit diesem Befehl umgangen werden: + +```bash +scrcpy --prefer-text +``` + +Dies kann jedoch das Tastaturverhalten in Spielen beeinträchtigen/zerstören. + +Auf der anderen Seite kann jedoch auch die Nutzung von key events erzwungen werden: + +```bash +scrcpy --raw-key-events +``` + +Diese Optionen haben jedoch keinen Einfluss auf eine etwaige HID-Tastatur, da in diesem modus alle key events als scancodes gesendet werden. + +[textevents]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-text-input +[prefertext]: https://github.com/Genymobile/scrcpy/issues/650#issuecomment-512945343 + + +#### Wiederholen von Tasten + +Standardmäßig löst das gedrückt halten einer Taste das jeweilige Event mehrfach aus. Dies kann jedoch zu Performanceproblemen in manchen Spielen führen. + +Um das Weitergeben von sich wiederholenden Tasteneingaben zu verhindern: + +```bash +scrcpy --no-key-repeat +``` + +This option has no effect on HID keyboard (key repeat is handled by Android +directly in this mode). + + +#### Rechtsklick und Mittelklick + +Standardmäßig löst Rechtsklick BACK (wenn Bildschirm aus: POWER) und Mittelklick BACK aus. Um diese Kürzel abzuschalten und stattdessen die Eingaben direkt an das Gerät weiterzugeben: + +```bash +scrcpy --forward-all-clicks +``` + + +### Dateien ablegen + +#### APK installieren + +Um eine AKP zu installieren, kann diese per Drag-and-drop auf das _scrcpy_-Fenster gezogen werden. + +Dabei erfolgt kein visuelles Feedback, ein Log wird in die Konsole ausgegeben. + + +#### Datei auf Gerät schieben + +Um eine Datei nach `/sdcard/Download/` auf dem Gerät zu schieben, Drag-and-drop die (nicht-APK)-Datei auf das _scrcpy_-Fenster. + +Dabei erfolgt kein visuelles Feedback, ein Log wird in die Konsole ausgegeben. + +Das Zielverzeichnis kann beim Start geändert werden: + +```bash +scrcpy --push-target=/sdcard/Movies/ +``` + + +### Audioweitergabe + +Audio wird von _scrcpy_ nicht übertragen. Hierfür kann [sndcpy] verwendet werden. + +Siehe zudem [issue #14]. + +[sndcpy]: https://github.com/rom1v/sndcpy +[issue #14]: https://github.com/Genymobile/scrcpy/issues/14 + + +## Tastenkürzel + +In der folgenden Liste ist MOD der Kürzel-Auslöser. Standardmäßig ist dies (links) Alt oder (links) Super. + +Dies kann mithilfe von `--shortcut-mod` geändert werden. Mögliche Tasten sind `lstrg`, `rstrg`, +`lalt`, `ralt`, `lsuper` und `rsuper`. Beispielhaft: + +```bash +# Nutze rStrg als Auslöser +scrcpy --shortcut-mod=rctrl + +# Nutze entweder LStrg+LAlt oder LSuper für Tastenkürzel +scrcpy --shortcut-mod=lctrl+lalt,lsuper +``` + +_[Super] ist typischerweise die Windows oder Cmd Taste._ + +[Super]: https://en.wikipedia.org/wiki/Super_key_(keyboard_button) + +| Aktion | Tastenkürzel | | +|--------------------------------------------------------|-----------------------------------------------------------|:-------------------------| +| Vollbild wechseln | MOD+f | | +| Display nach links rotieren | MOD+ _(links)_ | | +| Display nach links rotieren | MOD+ _(rechts)_ | | +| Fenstergröße 1:1 replizieren (pixel-perfect) | MOD+g | | +| Fenstergröße zum entfernen der schwarzen Balken ändern | MOD+w | _Doppel-Linksklick¹_ | +| Klick auf `HOME` | MOD+h | _Mittelklick_ | +| Klick auf `BACK` | MOD+b | _Rechtsklick²_ | +| Klick auf `APP_SWITCH` | MOD+s | _4.-Taste-Klick³_ | +| Klick auf `MENU` (Bildschirm entsperren)⁴ | MOD+m | | +| Klick auf `VOLUME_UP` | MOD+ _(hoch)_ | | +| CKlick auf `VOLUME_DOWN` | MOD+ _(runter)_ | | +| Klick auf `POWER` | MOD+p | | +| Power an | _Rechtsklick²_ | | +| Gerätebildschirm ausschalten (weiterhin spiegeln) | MOD+o | | +| Gerätebildschirm einschalten | MOD+Shift+o | | +| Gerätebildschirm drehen | MOD+r | | +| Benachrichtigungs-Bereich anzeigen | MOD+n | _5.-Taste-Klick³_ | +| Erweitertes Einstellungs-Menü anzeigen | MOD+n+n | _Doppel-5.-Taste-Klick³_ | +| Bedienfelder einklappen | MOD+Shift+n | | +| In die Zwischenablage kopieren⁵ | MOD+c | | +| In die Zwischenablage kopieren⁵ | MOD+x | | +| Zwischenablage synchronisieren und einfügen⁵ | MOD+v | | +| Computer-Zwischenablage einfügen (per Tastenevents) | MOD+Shift+v | | +| FPS-Zähler aktivieren/deaktivieren (ing stdout) | MOD+i | | +| Ziehen zum Zoomen | Strg+_Klicken-und-Bewegen_ | | +| Drag-and-drop mit APK-Datei | APK von Computer installieren | | +| Drag-and-drop mit Nicht-APK Datei | [Datei auf das Gerät schieben](#datei-auf-gerät-schieben) | | + + +_¹Doppelklick auf die schwarzen Balken, um diese zu entfernen._ +_²Rechtsklick aktiviert den Bildschirm, falls dieser aus war, ansonsten ZURÜCK._ +_³4. und 5. Maustasten, wenn diese an der jeweiligen Maus vorhanden sind._ +_⁴Für react-native Applikationen in Entwicklung, `MENU` öffnet das Entwickler-Menü._ +_⁵Nur für Android >= 7._ + +Abkürzungen mit mehreren Tastenanschlägen werden durch das Loslassen und erneute Drücken der Taste erreicht. +Beispielhaft, um "Erweitere das Einstellungs-Menü" auszuführen: + + 1. Drücke und halte MOD. + 2. Doppelklicke n. + 3. Lasse MOD los. + +Alle Strg+_Taste_ Tastenkürzel werden an das Gerät übergeben, sodass sie von der jeweiligen Applikation ausgeführt werden können. + + +## Personalisierte Pfade + +Um eine spezifische _adb_ Binary zu verwenden, muss deren Pfad als Umgebungsvariable `ADB` deklariert werden: + +```bash +ADB=/path/to/adb scrcpy +``` + +Um den Pfad der `scrcpy-server` Datei zu bearbeiten, muss deren Pfad in `SCRCPY_SERVER_PATH` bearbeitet werden. + +Um das Icon von scrcpy zu ändern, muss `SCRCPY_ICON_PATH` geändert werden. + + +## Warum _scrcpy_? + +Ein Kollege hat mich dazu herausgefordert, einen Namen so unaussprechbar wie [gnirehtet] zu finden. + +[`strcpy`] kopiert einen **str**ing; `scrcpy` kopiert einen **scr**een. + +[gnirehtet]: https://github.com/Genymobile/gnirehtet +[`strcpy`]: http://man7.org/linux/man-pages/man3/strcpy.3.html + + +## Selbst bauen? + +Siehe [BUILD]. + + +## Typische Fehler + +Siehe [FAQ](FAQ.md). + + +## Entwickler + +[Entwicklerseite](DEVELOP.md). + + +## Licence + + Copyright (C) 2018 Genymobile + Copyright (C) 2018-2022 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 (auf Englisch) + +- [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 e8ddf907..3d935e73 100644 --- a/README.md +++ b/README.md @@ -1110,6 +1110,7 @@ Read the [developers page]. This README is available in other languages: +- [Deutsch (German, `de`) - v1.22](README.de.md) - [Indonesian (Indonesia, `id`) - v1.16](README.id.md) - [Italiano (Italiano, `it`) - v1.19](README.it.md) - [日本語 (Japanese, `jp`) - v1.19](README.jp.md) From 2a872c3865ad3446cbf64ed72dbfa846110d675d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 16 Feb 2022 18:11:40 +0100 Subject: [PATCH 1123/2244] Fix fps_counter tick type The type uint32_t is not sufficient to store the result of sc_tick_now(). As a consequence, the FPS counter entered a live loop and caused a lock starvation (deadlock in practice). Refs ec871dd3f596a8183e37982821645ac5a5791fe0 Refs 682a6911735cb8f6dccd9653ce30b72f267235c6 --- app/src/fps_counter.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/fps_counter.c b/app/src/fps_counter.c index 25ee00eb..158e60ed 100644 --- a/app/src/fps_counter.c +++ b/app/src/fps_counter.c @@ -57,7 +57,7 @@ display_fps(struct fps_counter *counter) { // must be called with mutex locked static void -check_interval_expired(struct fps_counter *counter, uint32_t now) { +check_interval_expired(struct fps_counter *counter, sc_tick now) { if (now < counter->next_timestamp) { return; } From 85edba20e79cdbde5295696ecc4950f584819899 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 16 Feb 2022 18:29:30 +0100 Subject: [PATCH 1124/2244] Enforce deadline reached on timeout The value of sc_tick_now() has microsecond precision, but sc_cond_timedwait() has only millisecond precision. To guarantee that sc_tick_now() >= deadline when sc_cond_timedwait() returns due to timeout, round up to the next millisecond. This avoids to call a non-blocking sc_cond_timedwait() in a loop for no reason until a target deadline during up to 1 millisecond. Refs 682a6911735cb8f6dccd9653ce30b72f267235c6 --- app/src/util/thread.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/util/thread.c b/app/src/util/thread.c index 0d098b3f..478867b6 100644 --- a/app/src/util/thread.c +++ b/app/src/util/thread.c @@ -136,7 +136,9 @@ sc_cond_timedwait(sc_cond *cond, sc_mutex *mutex, sc_tick deadline) { return false; // timeout } - uint32_t ms = SC_TICK_TO_MS(deadline - now); + // Round up to the next millisecond to guarantee that the deadline is + // reached when returning due to timeout + uint32_t ms = SC_TICK_TO_MS(deadline - now + SC_TICK_FROM_MS(1) - 1); int r = SDL_CondWaitTimeout(cond->cond, mutex->mutex, ms); #ifndef NDEBUG if (r < 0) { @@ -148,6 +150,8 @@ sc_cond_timedwait(sc_cond *cond, sc_mutex *mutex, sc_tick deadline) { memory_order_relaxed); #endif assert(r == 0 || r == SDL_MUTEX_TIMEDOUT); + // The deadline is reached on timeout + assert(r != SDL_MUTEX_TIMEDOUT || sc_tick_now() >= deadline); return r == 0; } From 7a138c6929026b8dee103027ab91182533c6d8d0 Mon Sep 17 00:00:00 2001 From: Firq <75133068+Firq-ow@users.noreply.github.com> Date: Wed, 16 Feb 2022 08:42:46 +0100 Subject: [PATCH 1125/2244] Fix links in German README There were three links that weren't displayed correctly due to incorrect references: - the Windows release link to `README.md#windows` - 2 links that expected a German reference but got an English reference PR #3026 Signed-off-by: Romain Vimont --- README.de.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.de.md b/README.de.md index ca080cd3..92d00df2 100644 --- a/README.de.md +++ b/README.de.md @@ -57,7 +57,7 @@ Auf manchen Geräten müssen zudem [weitere Optionen][control] aktiv sein um das ### Zusammenfassung - Linux: `apt install scrcpy` - - Windows: [download][direct-win64] + - Windows: [download (siehe README)](README.md#windows) - macOS: `brew install scrcpy` Direkt von der Source bauen: [BUILD] ([vereinfachter Prozess (englisch)][BUILD_simple]) @@ -274,7 +274,7 @@ scrcpy -Nr file.mkv "Übersprungene Bilder" werden aufgenommen, selbst wenn sie in Echtzeit (aufgrund von Performancegründen) nicht dargestellt werden. Die Einzelbilder sind mit _Zeitstempeln_ des Gerätes versehen are, sodass eine [Paketverzögerungsvariation] nicht die Aufnahmedatei beeinträchtigt. -[Paketverzögerungs-Variation]: https://www.wikide.wiki/wiki/en/Packet_delay_variation +[Paketverzögerungsvariation]: https://www.wikide.wiki/wiki/en/Packet_delay_variation #### v4l2loopback @@ -394,7 +394,7 @@ scrcpy --bit-rate 2M --max-size 800 scrcpy -b2M -m800 # kurze Version ``` -[connect]: https://developer.android.com/studio/command-line/adb.html#wireless +[verbinden]: https://developer.android.com/studio/command-line/adb.html#wireless #### Mehrere Geräte From 03705b828b0a03ce982ac890bd595d4c43b71f7b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 17 Feb 2022 19:55:14 +0100 Subject: [PATCH 1126/2244] Use sc_prefix for fps counter --- app/src/fps_counter.c | 38 +++++++++++++++++++------------------- app/src/fps_counter.h | 22 +++++++++++----------- app/src/input_manager.c | 8 ++++---- app/src/screen.c | 14 +++++++------- app/src/screen.h | 2 +- 5 files changed, 42 insertions(+), 42 deletions(-) diff --git a/app/src/fps_counter.c b/app/src/fps_counter.c index 158e60ed..41ffeaaf 100644 --- a/app/src/fps_counter.c +++ b/app/src/fps_counter.c @@ -4,10 +4,10 @@ #include "util/log.h" -#define FPS_COUNTER_INTERVAL SC_TICK_FROM_SEC(1) +#define SC_FPS_COUNTER_INTERVAL SC_TICK_FROM_SEC(1) bool -fps_counter_init(struct fps_counter *counter) { +sc_fps_counter_init(struct sc_fps_counter *counter) { bool ok = sc_mutex_init(&counter->mutex); if (!ok) { return false; @@ -27,26 +27,26 @@ fps_counter_init(struct fps_counter *counter) { } void -fps_counter_destroy(struct fps_counter *counter) { +sc_fps_counter_destroy(struct sc_fps_counter *counter) { sc_cond_destroy(&counter->state_cond); sc_mutex_destroy(&counter->mutex); } static inline bool -is_started(struct fps_counter *counter) { +is_started(struct sc_fps_counter *counter) { return atomic_load_explicit(&counter->started, memory_order_acquire); } static inline void -set_started(struct fps_counter *counter, bool started) { +set_started(struct sc_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) { +display_fps(struct sc_fps_counter *counter) { unsigned rendered_per_second = - counter->nr_rendered * SC_TICK_FREQ / FPS_COUNTER_INTERVAL; + counter->nr_rendered * SC_TICK_FREQ / SC_FPS_COUNTER_INTERVAL; if (counter->nr_skipped) { LOGI("%u fps (+%u frames skipped)", rendered_per_second, counter->nr_skipped); @@ -57,7 +57,7 @@ display_fps(struct fps_counter *counter) { // must be called with mutex locked static void -check_interval_expired(struct fps_counter *counter, sc_tick now) { +check_interval_expired(struct sc_fps_counter *counter, sc_tick now) { if (now < counter->next_timestamp) { return; } @@ -67,13 +67,13 @@ check_interval_expired(struct fps_counter *counter, sc_tick now) { counter->nr_skipped = 0; // add a multiple of the interval uint32_t elapsed_slices = - (now - counter->next_timestamp) / FPS_COUNTER_INTERVAL + 1; - counter->next_timestamp += FPS_COUNTER_INTERVAL * elapsed_slices; + (now - counter->next_timestamp) / SC_FPS_COUNTER_INTERVAL + 1; + counter->next_timestamp += SC_FPS_COUNTER_INTERVAL * elapsed_slices; } static int run_fps_counter(void *data) { - struct fps_counter *counter = data; + struct sc_fps_counter *counter = data; sc_mutex_lock(&counter->mutex); while (!counter->interrupted) { @@ -94,9 +94,9 @@ run_fps_counter(void *data) { } bool -fps_counter_start(struct fps_counter *counter) { +sc_fps_counter_start(struct sc_fps_counter *counter) { sc_mutex_lock(&counter->mutex); - counter->next_timestamp = sc_tick_now() + FPS_COUNTER_INTERVAL; + counter->next_timestamp = sc_tick_now() + SC_FPS_COUNTER_INTERVAL; counter->nr_rendered = 0; counter->nr_skipped = 0; sc_mutex_unlock(&counter->mutex); @@ -121,18 +121,18 @@ fps_counter_start(struct fps_counter *counter) { } void -fps_counter_stop(struct fps_counter *counter) { +sc_fps_counter_stop(struct sc_fps_counter *counter) { set_started(counter, false); sc_cond_signal(&counter->state_cond); } bool -fps_counter_is_started(struct fps_counter *counter) { +sc_fps_counter_is_started(struct sc_fps_counter *counter) { return is_started(counter); } void -fps_counter_interrupt(struct fps_counter *counter) { +sc_fps_counter_interrupt(struct sc_fps_counter *counter) { if (!counter->thread_started) { return; } @@ -145,7 +145,7 @@ fps_counter_interrupt(struct fps_counter *counter) { } void -fps_counter_join(struct fps_counter *counter) { +sc_fps_counter_join(struct sc_fps_counter *counter) { if (counter->thread_started) { // interrupted must be set by the thread calling join(), so no need to // lock for the assertion @@ -156,7 +156,7 @@ fps_counter_join(struct fps_counter *counter) { } void -fps_counter_add_rendered_frame(struct fps_counter *counter) { +sc_fps_counter_add_rendered_frame(struct sc_fps_counter *counter) { if (!is_started(counter)) { return; } @@ -169,7 +169,7 @@ fps_counter_add_rendered_frame(struct fps_counter *counter) { } void -fps_counter_add_skipped_frame(struct fps_counter *counter) { +sc_fps_counter_add_skipped_frame(struct sc_fps_counter *counter) { if (!is_started(counter)) { return; } diff --git a/app/src/fps_counter.h b/app/src/fps_counter.h index 9609c814..e21c49c4 100644 --- a/app/src/fps_counter.h +++ b/app/src/fps_counter.h @@ -9,7 +9,7 @@ #include "util/thread.h" -struct fps_counter { +struct sc_fps_counter { sc_thread thread; sc_mutex mutex; sc_cond state_cond; @@ -28,32 +28,32 @@ struct fps_counter { }; bool -fps_counter_init(struct fps_counter *counter); +sc_fps_counter_init(struct sc_fps_counter *counter); void -fps_counter_destroy(struct fps_counter *counter); +sc_fps_counter_destroy(struct sc_fps_counter *counter); bool -fps_counter_start(struct fps_counter *counter); +sc_fps_counter_start(struct sc_fps_counter *counter); void -fps_counter_stop(struct fps_counter *counter); +sc_fps_counter_stop(struct sc_fps_counter *counter); bool -fps_counter_is_started(struct fps_counter *counter); +sc_fps_counter_is_started(struct sc_fps_counter *counter); // request to stop the thread (on quit) -// must be called before fps_counter_join() +// must be called before sc_fps_counter_join() void -fps_counter_interrupt(struct fps_counter *counter); +sc_fps_counter_interrupt(struct sc_fps_counter *counter); void -fps_counter_join(struct fps_counter *counter); +sc_fps_counter_join(struct sc_fps_counter *counter); void -fps_counter_add_rendered_frame(struct fps_counter *counter); +sc_fps_counter_add_rendered_frame(struct sc_fps_counter *counter); void -fps_counter_add_skipped_frame(struct fps_counter *counter); +sc_fps_counter_add_skipped_frame(struct sc_fps_counter *counter); #endif diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 63842d35..89ac450d 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -242,14 +242,14 @@ set_screen_power_mode(struct sc_controller *controller, } static void -switch_fps_counter_state(struct fps_counter *fps_counter) { +switch_fps_counter_state(struct sc_fps_counter *fps_counter) { // the started state can only be written from the current thread, so there // is no ToCToU issue - if (fps_counter_is_started(fps_counter)) { - fps_counter_stop(fps_counter); + if (sc_fps_counter_is_started(fps_counter)) { + sc_fps_counter_stop(fps_counter); LOGI("FPS counter stopped"); } else { - if (fps_counter_start(fps_counter)) { + if (sc_fps_counter_start(fps_counter)) { LOGI("FPS counter started"); } else { LOGE("FPS counter starting failed"); diff --git a/app/src/screen.c b/app/src/screen.c index 9e9ff3ff..2b1c5299 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -347,7 +347,7 @@ sc_video_buffer_on_new_frame(struct sc_video_buffer *vb, bool previous_skipped, bool need_new_event; if (previous_skipped) { - fps_counter_add_skipped_frame(&screen->fps_counter); + sc_fps_counter_add_skipped_frame(&screen->fps_counter); // The EVENT_NEW_FRAME triggered for the previous frame will consume // this new frame instead, unless the previous event failed need_new_event = screen->event_failed; @@ -402,7 +402,7 @@ sc_screen_init(struct sc_screen *screen, goto error_destroy_video_buffer; } - if (!fps_counter_init(&screen->fps_counter)) { + if (!sc_fps_counter_init(&screen->fps_counter)) { goto error_stop_and_join_video_buffer; } @@ -534,7 +534,7 @@ error_destroy_renderer: error_destroy_window: SDL_DestroyWindow(screen->window); error_destroy_fps_counter: - fps_counter_destroy(&screen->fps_counter); + sc_fps_counter_destroy(&screen->fps_counter); error_stop_and_join_video_buffer: sc_video_buffer_stop(&screen->vb); sc_video_buffer_join(&screen->vb); @@ -573,13 +573,13 @@ sc_screen_hide_window(struct sc_screen *screen) { void sc_screen_interrupt(struct sc_screen *screen) { sc_video_buffer_stop(&screen->vb); - fps_counter_interrupt(&screen->fps_counter); + sc_fps_counter_interrupt(&screen->fps_counter); } void sc_screen_join(struct sc_screen *screen) { sc_video_buffer_join(&screen->vb); - fps_counter_join(&screen->fps_counter); + sc_fps_counter_join(&screen->fps_counter); } void @@ -591,7 +591,7 @@ sc_screen_destroy(struct sc_screen *screen) { SDL_DestroyTexture(screen->texture); SDL_DestroyRenderer(screen->renderer); SDL_DestroyWindow(screen->window); - fps_counter_destroy(&screen->fps_counter); + sc_fps_counter_destroy(&screen->fps_counter); sc_video_buffer_destroy(&screen->vb); } @@ -701,7 +701,7 @@ sc_screen_update_frame(struct sc_screen *screen) { sc_video_buffer_consume(&screen->vb, screen->frame); AVFrame *frame = screen->frame; - fps_counter_add_rendered_frame(&screen->fps_counter); + sc_fps_counter_add_rendered_frame(&screen->fps_counter); struct sc_size new_frame_size = {frame->width, frame->height}; if (!prepare_for_frame(screen, new_frame_size)) { diff --git a/app/src/screen.h b/app/src/screen.h index 0ddefc43..12b8816c 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -26,7 +26,7 @@ struct sc_screen { struct sc_input_manager im; struct sc_video_buffer vb; - struct fps_counter fps_counter; + struct sc_fps_counter fps_counter; // The initial requested window properties struct { From 0e22032710728baf3c78d20a53e89cba202138ec Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 17 Feb 2022 21:59:35 +0100 Subject: [PATCH 1127/2244] Update FAQ about Windows scaling behavior Recommend to update to v1.22 before suggesting manual configuration. Fixes #3028 PR #3032 --- FAQ.md | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/FAQ.md b/FAQ.md index 5829136d..49b91e89 100644 --- a/FAQ.md +++ b/FAQ.md @@ -150,22 +150,24 @@ 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. +This problem should be fixed in scrcpy v1.22: **update to the latest version**. -On Windows, you might want to force OpenGL: - -``` -scrcpy --render-driver=opengl -``` - -You may also need to configure the [scaling behavior]: +On older versions, you must 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 +Also, 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 to enable mipmapping: + +``` +scrcpy --render-driver=opengl +``` + ### Issue with Wayland From fa93c8a91b6ea6ce82e61b05c0c3b96da9c589c7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 17 Feb 2022 20:10:09 +0100 Subject: [PATCH 1128/2244] Move FPS counter start/stop logs This will allow to print the same logs for every start/stop call. PR #3030 --- app/src/fps_counter.c | 2 ++ app/src/input_manager.c | 8 ++------ 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/app/src/fps_counter.c b/app/src/fps_counter.c index 41ffeaaf..85312821 100644 --- a/app/src/fps_counter.c +++ b/app/src/fps_counter.c @@ -117,6 +117,7 @@ sc_fps_counter_start(struct sc_fps_counter *counter) { counter->thread_started = true; } + LOGI("FPS counter started"); return true; } @@ -124,6 +125,7 @@ void sc_fps_counter_stop(struct sc_fps_counter *counter) { set_started(counter, false); sc_cond_signal(&counter->state_cond); + LOGI("FPS counter stopped"); } bool diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 89ac450d..bba3665c 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -247,13 +247,9 @@ switch_fps_counter_state(struct sc_fps_counter *fps_counter) { // is no ToCToU issue if (sc_fps_counter_is_started(fps_counter)) { sc_fps_counter_stop(fps_counter); - LOGI("FPS counter stopped"); } else { - if (sc_fps_counter_start(fps_counter)) { - LOGI("FPS counter started"); - } else { - LOGE("FPS counter starting failed"); - } + sc_fps_counter_start(fps_counter); + // Any error is already logged } } From 4b018be78907eec4d85e24e64402e1fbb5860aea Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 17 Feb 2022 20:08:41 +0100 Subject: [PATCH 1129/2244] Add --print-fps to enable FPS counter on start The FPS counter could be enabled/disabled via MOD+i. Add a command line option to enable it on start. This is consistent with other features like --turn-screen-off or --fullscreen. Fixes #468 PR #3030 --- README.md | 9 +++++++++ app/scrcpy.1 | 4 ++++ app/src/cli.c | 10 ++++++++++ app/src/options.c | 1 + app/src/options.h | 1 + app/src/scrcpy.c | 1 + app/src/screen.c | 5 +++++ app/src/screen.h | 2 ++ 8 files changed, 33 insertions(+) diff --git a/README.md b/README.md index dbbc4d71..1139092f 100644 --- a/README.md +++ b/README.md @@ -215,6 +215,15 @@ scrcpy --max-fps 15 This is officially supported since Android 10, but may work on earlier versions. +The actual capture framerate may be printed to the console: + +``` +scrcpy --print-fps +``` + +It may also be enabled or disabled at any time with MOD+i. + + #### 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 13a9a97a..62cfe004 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -211,6 +211,10 @@ Inject alpha characters and space as text events instead of key events. This avoids issues when combining multiple keys to enter special characters, but breaks the expected behavior of alpha keys in games (typically WASD). +.TP +.B "\-\-print\-fps +Start FPS counter, to print framerate logs to the console. It can be started or stopped at any time with MOD+i. + .TP .BI "\-\-push\-target " path Set the target directory for pushing files to the device by drag & drop. It is passed as\-is to "adb push". diff --git a/app/src/cli.c b/app/src/cli.c index eb28b1d7..ab2d7d10 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -55,6 +55,7 @@ #define OPT_NO_DOWNSIZE_ON_ERROR 1035 #define OPT_OTG 1036 #define OPT_NO_CLEANUP 1037 +#define OPT_PRINT_FPS 1038 struct sc_option { char shortopt; @@ -336,6 +337,12 @@ static const struct sc_option options[] = { "special character, but breaks the expected behavior of alpha " "keys in games (typically WASD).", }, + { + .longopt_id = OPT_PRINT_FPS, + .longopt = "print-fps", + .text = "Start FPS counter, to print framerate logs to the console. " + "It can be started or stopped at any time with MOD+i.", + }, { .longopt_id = OPT_PUSH_TARGET, .longopt = "push-target", @@ -1547,6 +1554,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], case OPT_NO_CLEANUP: opts->cleanup = false; break; + case OPT_PRINT_FPS: + opts->start_fps_counter = true; + break; case OPT_OTG: #ifdef HAVE_USB opts->otg = true; diff --git a/app/src/options.c b/app/src/options.c index 5a717df5..6651020f 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -63,4 +63,5 @@ const struct scrcpy_options scrcpy_options_default = { .select_tcpip = false, .select_usb = false, .cleanup = true, + .start_fps_counter = false, }; diff --git a/app/src/options.h b/app/src/options.h index f96edb22..f63e5c42 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -138,6 +138,7 @@ struct scrcpy_options { bool select_usb; bool select_tcpip; bool cleanup; + bool start_fps_counter; }; extern const struct scrcpy_options scrcpy_options_default; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 8c4920d6..3ed1d249 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -588,6 +588,7 @@ aoa_hid_end: .rotation = options->rotation, .mipmaps = options->mipmaps, .fullscreen = options->fullscreen, + .start_fps_counter = options->start_fps_counter, .buffering_time = options->display_buffer, }; diff --git a/app/src/screen.c b/app/src/screen.c index 2b1c5299..c233cf6e 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -386,6 +386,7 @@ sc_screen_init(struct sc_screen *screen, screen->req.width = params->window_width; screen->req.height = params->window_height; screen->req.fullscreen = params->fullscreen; + screen->req.start_fps_counter = params->start_fps_counter; static const struct sc_video_buffer_callbacks cbs = { .on_new_frame = sc_video_buffer_on_new_frame, @@ -562,6 +563,10 @@ sc_screen_show_initial_window(struct sc_screen *screen) { sc_screen_switch_fullscreen(screen); } + if (screen->req.start_fps_counter) { + sc_fps_counter_start(&screen->fps_counter); + } + SDL_ShowWindow(screen->window); } diff --git a/app/src/screen.h b/app/src/screen.h index 12b8816c..222e418f 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -35,6 +35,7 @@ struct sc_screen { uint16_t width; uint16_t height; bool fullscreen; + bool start_fps_counter; } req; SDL_Window *window; @@ -93,6 +94,7 @@ struct sc_screen_params { bool mipmaps; bool fullscreen; + bool start_fps_counter; sc_tick buffering_time; }; From 6edf50d447874f3bb93cbccff52c92f528d7aa9a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 18 Feb 2022 19:10:20 +0100 Subject: [PATCH 1130/2244] Remove fprintf() in tests It was committed by mistake. --- app/tests/test_adb_parser.c | 1 - 1 file changed, 1 deletion(-) diff --git a/app/tests/test_adb_parser.c b/app/tests/test_adb_parser.c index 990418c0..c4b18a8d 100644 --- a/app/tests/test_adb_parser.c +++ b/app/tests/test_adb_parser.c @@ -91,7 +91,6 @@ static void test_adb_devices_daemon_start_mixed() { struct sc_adb_device *device = &devices[0]; assert(!strcmp("0123456789abcdef", device->serial)); assert(!strcmp("unauthorized", device->state)); - fprintf(stderr, "==== [%s]\n", device->model); assert(!device->model); device = &devices[1]; From c4ab65eb790b8b88d561f1cbcbda5afbabd93bfe Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 18 Feb 2022 21:18:36 +0100 Subject: [PATCH 1131/2244] Remove useless '\n' in log --- app/src/adb/adb.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/adb/adb.c b/app/src/adb/adb.c index e415eb4f..8ce9cc6f 100644 --- a/app/src/adb/adb.c +++ b/app/src/adb/adb.c @@ -412,7 +412,7 @@ sc_adb_list_devices(struct sc_intr *intr, unsigned flags, // The implementation assumes that the output of "adb devices -l" fits // in the buffer in a single pass LOGW("Result of \"adb devices -l\" does not fit in 4Kb. " - "Please report an issue.\n"); + "Please report an issue."); return -1; } @@ -676,7 +676,7 @@ sc_adb_get_device_ip(struct sc_intr *intr, const char *serial, unsigned flags) { // The implementation assumes that the output of "ip route" fits in the // buffer in a single pass LOGW("Result of \"ip route\" does not fit in 1Kb. " - "Please report an issue.\n"); + "Please report an issue."); return NULL; } From b4fd882ece2e673f084b7820b7a8925a86c9bfce Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 20 Feb 2022 17:17:37 +0100 Subject: [PATCH 1132/2244] Fix typo --- 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 ab2d7d10..ae88ba40 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -118,7 +118,7 @@ static const struct sc_option options[] = { .text = "Crop the device screen on the server.\n" "The values are expressed in the device natural orientation " "(typically, portrait for a phone, landscape for a tablet). " - "Any --max-size value is cmoputed on the cropped size.", + "Any --max-size value is computed on the cropped size.", }, { .shortopt = 'd', From 33202491e13fa44f894a5f4256c7be1f3eaf7551 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 17 Feb 2022 21:34:48 +0100 Subject: [PATCH 1133/2244] Build on macOS with libusb support Fixes #2774 PR #3031 --- BUILD.md | 2 +- app/meson.build | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/BUILD.md b/BUILD.md index f683be95..e32ab684 100644 --- a/BUILD.md +++ b/BUILD.md @@ -199,7 +199,7 @@ Install the packages with [Homebrew]: ```bash # runtime dependencies -brew install sdl2 ffmpeg +brew install sdl2 ffmpeg libusb # client build dependencies brew install pkg-config meson diff --git a/app/meson.build b/app/meson.build index 9ee38d5f..9cd009fe 100644 --- a/app/meson.build +++ b/app/meson.build @@ -74,7 +74,7 @@ if v4l2_support src += [ 'src/v4l2_sink.c' ] endif -usb_support = get_option('usb') and host_machine.system() == 'linux' +usb_support = get_option('usb') and host_machine.system() != 'windows' if usb_support src += [ 'src/usb/aoa_hid.c', From 82a99f69ec464a0637a16bdccfe5ff806777e942 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 17 Feb 2022 21:40:01 +0100 Subject: [PATCH 1134/2244] Remove "linux-only" mentions for HID/OTG features HID/OTG features are not limited to Linux anymore. PR #3031 --- app/scrcpy.1 | 6 +++--- app/src/cli.c | 9 +++------ 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 62cfe004..f9d4ba24 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -100,7 +100,7 @@ Simulate a physical keyboard by using HID over AOAv2. This provides a better experience for IME users, and allows to generate non-ASCII characters, contrary to the default injection method. -It may only work over USB, and is currently only supported on Linux. +It may only work over USB. The keyboard layout must be configured (once and for all) on the device, via Settings -> System -> Languages and input -> Physical keyboard. This settings page can be started directly: @@ -142,7 +142,7 @@ In this mode, the computer mouse is captured to control the device directly (rel LAlt, LSuper or RSuper toggle the capture mode, to give control of the mouse back to the computer. -It may only work over USB, and is currently only supported on Linux. +It may only work over USB. Also see \fB\-\-hid\-keyboard\fR. @@ -190,7 +190,7 @@ LAlt, LSuper or RSuper toggle the mouse capture mode, to give control of the mou If any of \fB\-\-hid\-keyboard\fR or \fB\-\-hid\-mouse\fR is set, only enable keyboard or mouse respectively, otherwise enable both. -It may only work over USB, and is currently only supported on Linux. +It may only work over USB. See \fB\-\-hid\-keyboard\fR and \fB\-\-hid\-mouse\fR. diff --git a/app/src/cli.c b/app/src/cli.c index ae88ba40..f930142e 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -186,8 +186,7 @@ static const struct sc_option options[] = { "It provides a better experience for IME users, and allows to " "generate non-ASCII characters, contrary to the default " "injection method.\n" - "It may only work over USB, and is currently only supported " - "on Linux.\n" + "It may only work over USB.\n" "The keyboard layout must be configured (once and for all) on " "the device, via Settings -> System -> Languages and input -> " "Physical keyboard. This settings page can be started " @@ -239,8 +238,7 @@ static const struct sc_option options[] = { "device directly (relative mouse mode).\n" "LAlt, LSuper or RSuper toggle the capture mode, to give " "control of the mouse back to the computer.\n" - "It may only work over USB, and is currently only supported " - "on Linux.\n" + "It may only work over USB.\n" "Also see --hid-keyboard.", }, { @@ -311,8 +309,7 @@ static const struct sc_option options[] = { "control of the mouse back to the computer.\n" "If any of --hid-keyboard or --hid-mouse is set, only enable " "keyboard or mouse respectively, otherwise enable both.\n" - "It may only work over USB, and is currently only supported " - "on Linux.\n" + "It may only work over USB.\n" "See --hid-keyboard and --hid-mouse.", }, { From 9db42341e4934989921099c60e307c847eb625b6 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 17 Feb 2022 21:37:14 +0100 Subject: [PATCH 1135/2244] Pass screen instance to mouse capture functions Using the screen instance or not in these functions is an implementation detail. Further changes will require the screen instance. Refs 7848a387c8281cb156f0e0da6bbbb05cda31db22 PR #3031 --- app/src/screen.c | 24 ++++++++++++++---------- app/src/usb/screen_otg.c | 28 ++++++++++++++++------------ 2 files changed, 30 insertions(+), 22 deletions(-) diff --git a/app/src/screen.c b/app/src/screen.c index c233cf6e..7ca9bfb7 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -163,7 +163,8 @@ sc_screen_is_relative_mode(struct sc_screen *screen) { } static void -sc_screen_set_mouse_capture(bool capture) { +sc_screen_set_mouse_capture(struct sc_screen *screen, bool capture) { + (void) screen; if (SDL_SetRelativeMouseMode(capture)) { LOGE("Could not set relative mouse mode to %s: %s", capture ? "true" : "false", SDL_GetError()); @@ -171,13 +172,16 @@ sc_screen_set_mouse_capture(bool capture) { } static inline bool -sc_screen_get_mouse_capture(void) { +sc_screen_get_mouse_capture(struct sc_screen *screen) { + (void) screen; return SDL_GetRelativeMouseMode(); } static inline void -sc_screen_toggle_mouse_capture(void) { - sc_screen_set_mouse_capture(!sc_screen_get_mouse_capture()); +sc_screen_toggle_mouse_capture(struct sc_screen *screen) { + (void) screen; + bool new_value = !sc_screen_get_mouse_capture(screen); + sc_screen_set_mouse_capture(screen, new_value); } static void @@ -721,7 +725,7 @@ sc_screen_update_frame(struct sc_screen *screen) { if (sc_screen_is_relative_mode(screen)) { // Capture mouse on start - sc_screen_set_mouse_capture(true); + sc_screen_set_mouse_capture(screen, true); } } @@ -834,7 +838,7 @@ sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event) { break; case SDL_WINDOWEVENT_FOCUS_LOST: if (relative_mode) { - sc_screen_set_mouse_capture(false); + sc_screen_set_mouse_capture(screen, false); } break; } @@ -864,7 +868,7 @@ sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event) { if (key == cap) { // A mouse capture key has been pressed then released: // toggle the capture mouse mode - sc_screen_toggle_mouse_capture(); + sc_screen_toggle_mouse_capture(screen); } // Mouse capture keys are never forwarded to the device return; @@ -874,7 +878,7 @@ sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event) { case SDL_MOUSEWHEEL: case SDL_MOUSEMOTION: case SDL_MOUSEBUTTONDOWN: - if (relative_mode && !sc_screen_get_mouse_capture()) { + if (relative_mode && !sc_screen_get_mouse_capture(screen)) { // Do not forward to input manager, the mouse will be captured // on SDL_MOUSEBUTTONUP return; @@ -890,8 +894,8 @@ sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event) { } break; case SDL_MOUSEBUTTONUP: - if (relative_mode && !sc_screen_get_mouse_capture()) { - sc_screen_set_mouse_capture(true); + if (relative_mode && !sc_screen_get_mouse_capture(screen)) { + sc_screen_set_mouse_capture(screen, true); return; } break; diff --git a/app/src/usb/screen_otg.c b/app/src/usb/screen_otg.c index f32bc946..96d7eaff 100644 --- a/app/src/usb/screen_otg.c +++ b/app/src/usb/screen_otg.c @@ -5,7 +5,8 @@ #include "util/log.h" static void -sc_screen_otg_set_mouse_capture(bool capture) { +sc_screen_otg_set_mouse_capture(struct sc_screen_otg *screen, bool capture) { + (void) screen; if (SDL_SetRelativeMouseMode(capture)) { LOGE("Could not set relative mouse mode to %s: %s", capture ? "true" : "false", SDL_GetError()); @@ -13,13 +14,16 @@ sc_screen_otg_set_mouse_capture(bool capture) { } static inline bool -sc_screen_otg_get_mouse_capture(void) { +sc_screen_otg_get_mouse_capture(struct sc_screen_otg *screen) { + (void) screen; return SDL_GetRelativeMouseMode(); } static inline void -sc_screen_otg_toggle_mouse_capture(void) { - sc_screen_otg_set_mouse_capture(!sc_screen_otg_get_mouse_capture()); +sc_screen_otg_toggle_mouse_capture(struct sc_screen_otg *screen) { + (void) screen; + bool new_value = !sc_screen_otg_get_mouse_capture(screen); + sc_screen_otg_set_mouse_capture(screen, new_value); } static void @@ -86,7 +90,7 @@ sc_screen_otg_init(struct sc_screen_otg *screen, if (screen->mouse) { // Capture mouse on start - sc_screen_otg_set_mouse_capture(true); + sc_screen_otg_set_mouse_capture(screen, true); } return true; @@ -198,7 +202,7 @@ sc_screen_otg_handle_event(struct sc_screen_otg *screen, SDL_Event *event) { break; case SDL_WINDOWEVENT_FOCUS_LOST: if (screen->mouse) { - sc_screen_otg_set_mouse_capture(false); + sc_screen_otg_set_mouse_capture(screen, false); } break; } @@ -232,7 +236,7 @@ sc_screen_otg_handle_event(struct sc_screen_otg *screen, SDL_Event *event) { if (key == cap) { // A mouse capture key has been pressed then released: // toggle the capture mouse mode - sc_screen_otg_toggle_mouse_capture(); + sc_screen_otg_toggle_mouse_capture(screen); } // Mouse capture keys are never forwarded to the device return; @@ -244,26 +248,26 @@ sc_screen_otg_handle_event(struct sc_screen_otg *screen, SDL_Event *event) { } break; case SDL_MOUSEMOTION: - if (screen->mouse && sc_screen_otg_get_mouse_capture()) { + if (screen->mouse && sc_screen_otg_get_mouse_capture(screen)) { sc_screen_otg_process_mouse_motion(screen, &event->motion); } break; case SDL_MOUSEBUTTONDOWN: - if (screen->mouse && sc_screen_otg_get_mouse_capture()) { + if (screen->mouse && sc_screen_otg_get_mouse_capture(screen)) { sc_screen_otg_process_mouse_button(screen, &event->button); } break; case SDL_MOUSEBUTTONUP: if (screen->mouse) { - if (sc_screen_otg_get_mouse_capture()) { + if (sc_screen_otg_get_mouse_capture(screen)) { sc_screen_otg_process_mouse_button(screen, &event->button); } else { - sc_screen_otg_set_mouse_capture(true); + sc_screen_otg_set_mouse_capture(screen, true); } } break; case SDL_MOUSEWHEEL: - if (screen->mouse && sc_screen_otg_get_mouse_capture()) { + if (screen->mouse && sc_screen_otg_get_mouse_capture(screen)) { sc_screen_otg_process_mouse_wheel(screen, &event->wheel); } break; From 3ee3f8dc027fe4e2d21ef144df4c29acbd709ee7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 17 Feb 2022 21:46:13 +0100 Subject: [PATCH 1136/2244] Work around mouse capture SDL bug on macOS On macOS, SDL relative mouse mode does not work correctly when the cursor is outside the window. As a workaround, move the cursor inside the window before setting the relative mouse mode. Refs SDL/#5340 PR #3031 --- app/src/screen.c | 19 +++++++++++++++++++ app/src/usb/screen_otg.c | 19 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/app/src/screen.c b/app/src/screen.c index 7ca9bfb7..ae28e6e6 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -164,7 +164,26 @@ sc_screen_is_relative_mode(struct sc_screen *screen) { static void sc_screen_set_mouse_capture(struct sc_screen *screen, bool capture) { +#ifdef __APPLE__ + // Workaround for SDL bug on macOS: + // + if (capture) { + int mouse_x, mouse_y; + SDL_GetGlobalMouseState(&mouse_x, &mouse_y); + + int x, y, w, h; + SDL_GetWindowPosition(screen->window, &x, &y); + SDL_GetWindowSize(screen->window, &w, &h); + + bool outside_window = mouse_x < x || mouse_x >= x + w + || mouse_y < y || mouse_y >= y + h; + if (outside_window) { + SDL_WarpMouseInWindow(screen->window, w / 2, h / 2); + } + } +#else (void) screen; +#endif if (SDL_SetRelativeMouseMode(capture)) { LOGE("Could not set relative mouse mode to %s: %s", capture ? "true" : "false", SDL_GetError()); diff --git a/app/src/usb/screen_otg.c b/app/src/usb/screen_otg.c index 96d7eaff..561a84ca 100644 --- a/app/src/usb/screen_otg.c +++ b/app/src/usb/screen_otg.c @@ -6,7 +6,26 @@ static void sc_screen_otg_set_mouse_capture(struct sc_screen_otg *screen, bool capture) { +#ifdef __APPLE__ + // Workaround for SDL bug on macOS: + // + if (capture) { + int mouse_x, mouse_y; + SDL_GetGlobalMouseState(&mouse_x, &mouse_y); + + int x, y, w, h; + SDL_GetWindowPosition(screen->window, &x, &y); + SDL_GetWindowSize(screen->window, &w, &h); + + bool outside_window = mouse_x < x || mouse_x >= x + w + || mouse_y < y || mouse_y >= y + h; + if (outside_window) { + SDL_WarpMouseInWindow(screen->window, w / 2, h / 2); + } + } +#else (void) screen; +#endif if (SDL_SetRelativeMouseMode(capture)) { LOGE("Could not set relative mouse mode to %s: %s", capture ? "true" : "false", SDL_GetError()); From be1936bb85de6a19a805ed320dcabe217e52d656 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 10 Feb 2022 07:57:18 +0100 Subject: [PATCH 1137/2244] Report USB device disconnection when detected USB device disconnection is detected via a hotplug callback when it is supported. In addition, report disconnection on libusb calls returning LIBUSB_ERROR_NO_DEVICE or LIBUSB_ERROR_NOT_FOUND. This allows to detect disconnection after a libusb call when hotplug is not available. PR #3011 --- app/src/usb/aoa_hid.c | 4 ++++ app/src/usb/usb.c | 22 ++++++++++++++++++++-- app/src/usb/usb.h | 6 ++++++ 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/app/src/usb/aoa_hid.c b/app/src/usb/aoa_hid.c index 57296bfc..0007169d 100644 --- a/app/src/usb/aoa_hid.c +++ b/app/src/usb/aoa_hid.c @@ -95,6 +95,7 @@ sc_aoa_register_hid(struct sc_aoa *aoa, uint16_t accessory_id, DEFAULT_TIMEOUT); if (result < 0) { LOGE("REGISTER_HID: libusb error: %s", libusb_strerror(result)); + sc_usb_check_disconnected(aoa->usb, result); return false; } @@ -131,6 +132,7 @@ sc_aoa_set_hid_report_desc(struct sc_aoa *aoa, uint16_t accessory_id, DEFAULT_TIMEOUT); if (result < 0) { LOGE("SET_HID_REPORT_DESC: libusb error: %s", libusb_strerror(result)); + sc_usb_check_disconnected(aoa->usb, result); return false; } @@ -173,6 +175,7 @@ sc_aoa_send_hid_event(struct sc_aoa *aoa, const struct sc_hid_event *event) { DEFAULT_TIMEOUT); if (result < 0) { LOGE("SEND_HID_EVENT: libusb error: %s", libusb_strerror(result)); + sc_usb_check_disconnected(aoa->usb, result); return false; } @@ -195,6 +198,7 @@ sc_aoa_unregister_hid(struct sc_aoa *aoa, const uint16_t accessory_id) { DEFAULT_TIMEOUT); if (result < 0) { LOGE("UNREGISTER_HID: libusb error: %s", libusb_strerror(result)); + sc_usb_check_disconnected(aoa->usb, result); return false; } diff --git a/app/src/usb/usb.c b/app/src/usb/usb.c index 07fb9619..7a0e4cfd 100644 --- a/app/src/usb/usb.c +++ b/app/src/usb/usb.c @@ -216,6 +216,24 @@ sc_usb_destroy(struct sc_usb *usb) { libusb_exit(usb->context); } +static void +sc_usb_report_disconnected(struct sc_usb *usb) { + if (usb->cbs && !atomic_flag_test_and_set(&usb->disconnection_notified)) { + assert(usb->cbs && usb->cbs->on_disconnected); + usb->cbs->on_disconnected(usb, usb->cbs_userdata); + } +} + +bool +sc_usb_check_disconnected(struct sc_usb *usb, int result) { + if (result == LIBUSB_ERROR_NO_DEVICE || result == LIBUSB_ERROR_NOT_FOUND) { + sc_usb_report_disconnected(usb); + return false; + } + + return true; +} + static int sc_usb_libusb_callback(libusb_context *ctx, libusb_device *device, libusb_hotplug_event event, void *userdata) { @@ -232,8 +250,7 @@ sc_usb_libusb_callback(libusb_context *ctx, libusb_device *device, return 0; } - assert(usb->cbs && usb->cbs->on_disconnected); - usb->cbs->on_disconnected(usb, usb->cbs_userdata); + sc_usb_report_disconnected(usb); // Do not automatically deregister the callback by returning 1. Instead, // manually deregister to interrupt libusb_handle_events() from the libusb @@ -307,6 +324,7 @@ sc_usb_connect(struct sc_usb *usb, libusb_device *device, if (cbs) { atomic_init(&usb->stopped, false); + usb->disconnection_notified = (atomic_flag) ATOMIC_FLAG_INIT; if (sc_usb_register_callback(usb)) { // Create a thread to process libusb events, so that device // disconnection could be detected immediately diff --git a/app/src/usb/usb.h b/app/src/usb/usb.h index d264a536..f0ebbd96 100644 --- a/app/src/usb/usb.h +++ b/app/src/usb/usb.h @@ -22,6 +22,7 @@ struct sc_usb { sc_thread libusb_event_thread; atomic_bool stopped; // only used if cbs != NULL + atomic_flag disconnection_notified; }; struct sc_usb_callbacks { @@ -73,6 +74,11 @@ sc_usb_connect(struct sc_usb *usb, libusb_device *device, void sc_usb_disconnect(struct sc_usb *usb); +// A client should call this function with the return value of a libusb call +// to detect disconnection immediately +bool +sc_usb_check_disconnected(struct sc_usb *usb, int result); + void sc_usb_stop(struct sc_usb *usb); From b9b28797893c9449b89b44e1c0ac209a5358cca5 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 10 Feb 2022 09:29:07 +0100 Subject: [PATCH 1138/2244] Remove USB hotplug callback error log If it fails, the error is already logged by sc_usb_register_callback(). PR #3011 --- app/src/usb/usb.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/usb/usb.c b/app/src/usb/usb.c index 7a0e4cfd..799ada94 100644 --- a/app/src/usb/usb.c +++ b/app/src/usb/usb.c @@ -335,8 +335,6 @@ sc_usb_connect(struct sc_usb *usb, libusb_device *device, LOGW("Libusb event thread handler could not be created, USB " "device disconnection might not be detected immediately"); } - } else { - LOGW("Could not register USB device disconnection callback"); } } From 06243e7c3cd313e2d4d4b34fe0d2c3f35d9aee6d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 7 Feb 2022 08:57:42 +0100 Subject: [PATCH 1139/2244] Avoid PRIx16 printf format on Windows Convert uint16_t to unsigned to avoid using PRIx16, which may not exist on Windows. PR #3011 --- app/src/usb/scrcpy_otg.c | 4 ++-- app/src/usb/usb.c | 11 +++++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/app/src/usb/scrcpy_otg.c b/app/src/usb/scrcpy_otg.c index d3a45679..1eaa2537 100644 --- a/app/src/usb/scrcpy_otg.c +++ b/app/src/usb/scrcpy_otg.c @@ -91,8 +91,8 @@ scrcpy_otg(struct scrcpy_options *options) { usb_device_initialized = true; - LOGI("USB device: %s (%04" PRIx16 ":%04" PRIx16 ") %s %s", - usb_device.serial, usb_device.vid, usb_device.pid, + LOGI("USB device: %s (%04x:%04x) %s %s", usb_device.serial, + (unsigned) usb_device.vid, (unsigned) usb_device.pid, usb_device.manufacturer, usb_device.product); ok = sc_usb_connect(&s->usb, usb_device.device, &cbs, NULL); diff --git a/app/src/usb/usb.c b/app/src/usb/usb.c index 799ada94..2d3fc3a6 100644 --- a/app/src/usb/usb.c +++ b/app/src/usb/usb.c @@ -40,8 +40,9 @@ sc_usb_read_device(libusb_device *device, struct sc_usb_device *out) { if (result < 0) { // Log at debug level because it is expected that some non-Android USB // devices present on the computer require special permissions - LOGD("Open USB device %04" PRIx16 ":%04" PRIx16 ": libusb error: %s", - desc.idVendor, desc.idProduct, libusb_strerror(result)); + LOGD("Open USB device %04x:%04x: libusb error: %s", + (unsigned) desc.idVendor, (unsigned) desc.idProduct, + libusb_strerror(result)); return false; } @@ -146,8 +147,10 @@ sc_usb_devices_log(enum sc_log_level level, struct sc_usb_device *devices, for (size_t i = 0; i < count; ++i) { struct sc_usb_device *d = &devices[i]; const char *selection = d->selected ? "-->" : " "; - LOG(level, " %s %-18s (%04" PRIx16 ":%04" PRIx16 ") %s %s", - selection, d->serial, d->vid, d->pid, d->manufacturer, d->product); + // Convert uint16_t to unsigned because PRIx16 may not exist on Windows + LOG(level, " %s %-18s (%04x:%04x) %s %s", + selection, d->serial, (unsigned) d->vid, (unsigned) d->pid, + d->manufacturer, d->product); } } From ff3cb31cb4a5d58354e6a7b59c6e2486091ff691 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 7 Feb 2022 09:08:21 +0100 Subject: [PATCH 1140/2244] Fix libusb callback for Windows Add LIBUSB_CALL so that the callback has the correct signature on Windows (including __attribute__((stdcall))). PR #3011 --- app/src/usb/usb.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/usb/usb.c b/app/src/usb/usb.c index 2d3fc3a6..276b0067 100644 --- a/app/src/usb/usb.c +++ b/app/src/usb/usb.c @@ -237,7 +237,7 @@ sc_usb_check_disconnected(struct sc_usb *usb, int result) { return true; } -static int +static LIBUSB_CALL int sc_usb_libusb_callback(libusb_context *ctx, libusb_device *device, libusb_hotplug_event event, void *userdata) { (void) ctx; From 6b65cd405a56d09b00a609ef43f2846c2cc349f7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 7 Feb 2022 08:52:17 +0100 Subject: [PATCH 1141/2244] Build for Windows with libusb support Fixes #2773 PR #3011 --- BUILD.md | 6 ++++-- app/meson.build | 15 ++++++++++++++- app/prebuilt-deps/prepare-libusb.sh | 28 ++++++++++++++++++++++++++++ cross_win32.txt | 2 ++ cross_win64.txt | 2 ++ release.mk | 4 ++++ 6 files changed, 54 insertions(+), 3 deletions(-) create mode 100755 app/prebuilt-deps/prepare-libusb.sh diff --git a/BUILD.md b/BUILD.md index e32ab684..60be752d 100644 --- a/BUILD.md +++ b/BUILD.md @@ -161,7 +161,8 @@ install the required packages: ```bash # runtime dependencies pacman -S mingw-w64-x86_64-SDL2 \ - mingw-w64-x86_64-ffmpeg + mingw-w64-x86_64-ffmpeg \ + mingw-w64-x86_64-libusb # client build dependencies pacman -S mingw-w64-x86_64-make \ @@ -175,7 +176,8 @@ For a 32 bits version, replace `x86_64` by `i686`: ```bash # runtime dependencies pacman -S mingw-w64-i686-SDL2 \ - mingw-w64-i686-ffmpeg + mingw-w64-i686-ffmpeg \ + mingw-w64-i686-libusb # client build dependencies pacman -S mingw-w64-i686-make \ diff --git a/app/meson.build b/app/meson.build index 9cd009fe..a0086619 100644 --- a/app/meson.build +++ b/app/meson.build @@ -74,7 +74,7 @@ if v4l2_support src += [ 'src/v4l2_sink.c' ] endif -usb_support = get_option('usb') and host_machine.system() != 'windows' +usb_support = get_option('usb') if usb_support src += [ 'src/usb/aoa_hid.c', @@ -141,9 +141,22 @@ else include_directories: include_directories(ffmpeg_include_dir) ) + prebuilt_libusb = meson.get_cross_property('prebuilt_libusb') + prebuilt_libusb_root = meson.get_cross_property('prebuilt_libusb_root') + libusb_bin_dir = meson.current_source_dir() + '/prebuilt-deps/data/' + prebuilt_libusb + '/dll' + libusb_include_dir = 'prebuilt-deps/data/' + prebuilt_libusb_root + '/include' + + libusb = declare_dependency( + dependencies: [ + cc.find_library('libusb-1.0', dirs: libusb_bin_dir), + ], + include_directories: include_directories(libusb_include_dir) + ) + dependencies = [ ffmpeg, sdl2, + libusb, cc.find_library('mingw32') ] diff --git a/app/prebuilt-deps/prepare-libusb.sh b/app/prebuilt-deps/prepare-libusb.sh new file mode 100755 index 00000000..54ead536 --- /dev/null +++ b/app/prebuilt-deps/prepare-libusb.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +set -e +DIR=$(dirname ${BASH_SOURCE[0]}) +cd "$DIR" +. common +mkdir -p "$PREBUILT_DATA_DIR" +cd "$PREBUILT_DATA_DIR" + +DEP_DIR=libusb-1.0.25 + +FILENAME=libusb-1.0.25.7z +SHA256SUM=3d1c98416f454026034b2b5d67f8a294053898cb70a8b489874e75b136c6674d + +if [[ -d "$DEP_DIR" ]] +then + echo "$DEP_DIR" found + exit 0 +fi + +get_file "https://github.com/libusb/libusb/releases/download/v1.0.25/$FILENAME" "$FILENAME" "$SHA256SUM" + +mkdir "$DEP_DIR" +cd "$DEP_DIR" + +7z x "../$FILENAME" \ + MinGW32/dll/libusb-1.0.dll \ + MinGW64/dll/libusb-1.0.dll \ + include / diff --git a/cross_win32.txt b/cross_win32.txt index db448a00..750dbd78 100644 --- a/cross_win32.txt +++ b/cross_win32.txt @@ -21,3 +21,5 @@ ffmpeg_avformat = 'avformat-58' ffmpeg_avutil = 'avutil-56' prebuilt_ffmpeg = 'ffmpeg-win32-4.3.1' prebuilt_sdl2 = 'SDL2-2.0.20/i686-w64-mingw32' +prebuilt_libusb_root = 'libusb-1.0.25' +prebuilt_libusb = prebuilt_libusb_root + '/MinGW32' diff --git a/cross_win64.txt b/cross_win64.txt index 9d169a71..114b0c22 100644 --- a/cross_win64.txt +++ b/cross_win64.txt @@ -21,3 +21,5 @@ ffmpeg_avformat = 'avformat-59' ffmpeg_avutil = 'avutil-57' prebuilt_ffmpeg = 'ffmpeg-win64-5.0' prebuilt_sdl2 = 'SDL2-2.0.20/x86_64-w64-mingw32' +prebuilt_libusb_root = 'libusb-1.0.25' +prebuilt_libusb = prebuilt_libusb_root + '/MinGW64' diff --git a/release.mk b/release.mk index aff9bd89..00d2389f 100644 --- a/release.mk +++ b/release.mk @@ -66,11 +66,13 @@ prepare-deps-win32: @app/prebuilt-deps/prepare-adb.sh @app/prebuilt-deps/prepare-sdl.sh @app/prebuilt-deps/prepare-ffmpeg-win32.sh + @app/prebuilt-deps/prepare-libusb.sh prepare-deps-win64: @app/prebuilt-deps/prepare-adb.sh @app/prebuilt-deps/prepare-sdl.sh @app/prebuilt-deps/prepare-ffmpeg-win64.sh + @app/prebuilt-deps/prepare-libusb.sh build-win32: prepare-deps-win32 [ -d "$(WIN32_BUILD_DIR)" ] || ( mkdir "$(WIN32_BUILD_DIR)" && \ @@ -107,6 +109,7 @@ dist-win32: build-server build-win32 cp app/prebuilt-deps/data/platform-tools-31.0.3/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-31.0.3/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/SDL2-2.0.20/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/libusb-1.0.25/MinGW32/dll/libusb-1.0.dll "$(DIST)/$(WIN32_TARGET_DIR)/" dist-win64: build-server build-win64 mkdir -p "$(DIST)/$(WIN64_TARGET_DIR)" @@ -125,6 +128,7 @@ dist-win64: build-server build-win64 cp app/prebuilt-deps/data/platform-tools-31.0.3/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-31.0.3/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/SDL2-2.0.20/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/libusb-1.0.25/MinGW64/dll/libusb-1.0.dll "$(DIST)/$(WIN64_TARGET_DIR)/" zip-win32: dist-win32 cd "$(DIST)/$(WIN32_TARGET_DIR)"; \ From 6ee75c0cff3eb995486901757e682f960516b6b0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 20 Feb 2022 17:19:11 +0100 Subject: [PATCH 1142/2244] Remove obsolete text in error message The HID/OTG features are now available on all platforms. PR #3011 --- app/src/cli.c | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index f930142e..d8ae4f53 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -1370,8 +1370,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], opts->keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_HID; break; #else - LOGE("HID over AOA (-K/--hid-keyboard) is disabled (or " - "unsupported on this platform)."); + LOGE("HID over AOA (-K/--hid-keyboard) is disabled."); return false; #endif case OPT_MAX_FPS: @@ -1389,8 +1388,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_HID; break; #else - LOGE("HID over AOA (-M/--hid-mouse) is disabled (or " - "unsupported on this platform)."); + LOGE("HID over AOA (-M/--hid-mouse) is disabled."); return false; #endif case OPT_LOCK_VIDEO_ORIENTATION: @@ -1559,8 +1557,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], opts->otg = true; break; #else - LOGE("OTG mode (--otg) is disabled (or unsupported on this " - "platform)."); + LOGE("OTG mode (--otg) is disabled."); return false; #endif case OPT_V4L2_SINK: From 3bb24b3926a2db0fa38393c829b4e703471c0fcc Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 10 Feb 2022 08:05:54 +0100 Subject: [PATCH 1143/2244] Make intr optional for adb commands All adb commands are executed with an "interruptor", so that they can be interrupted on Ctrl+C. Make this interruptor optional, so that we could call "adb kill-server" in OTG mode. This command always returns almost immediately anyway. Ideally, we should make all blocking calls interruptible (including libusb calls, by using the asynchronous API), but it's a lot of work, and in practice it works well enough. PR #3011 --- app/src/adb/adb.c | 6 ++++-- app/src/util/process_intr.c | 14 ++++++++++---- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/app/src/adb/adb.c b/app/src/adb/adb.c index 8ce9cc6f..8742c4d0 100644 --- a/app/src/adb/adb.c +++ b/app/src/adb/adb.c @@ -150,7 +150,7 @@ process_check_success_internal(sc_pid pid, const char *name, bool close, static bool process_check_success_intr(struct sc_intr *intr, sc_pid pid, const char *name, unsigned flags) { - if (!sc_intr_set_process(intr, pid)) { + if (intr && !sc_intr_set_process(intr, pid)) { // Already interrupted return false; } @@ -158,7 +158,9 @@ process_check_success_intr(struct sc_intr *intr, sc_pid pid, const char *name, // Always pass close=false, interrupting would be racy otherwise bool ret = process_check_success_internal(pid, name, false, flags); - sc_intr_set_process(intr, SC_PROCESS_NONE); + if (intr) { + sc_intr_set_process(intr, SC_PROCESS_NONE); + } // Close separately sc_process_close(pid); diff --git a/app/src/util/process_intr.c b/app/src/util/process_intr.c index 940fe89f..d37bd5a5 100644 --- a/app/src/util/process_intr.c +++ b/app/src/util/process_intr.c @@ -3,27 +3,33 @@ ssize_t sc_pipe_read_intr(struct sc_intr *intr, sc_pid pid, sc_pipe pipe, char *data, size_t len) { - if (!sc_intr_set_process(intr, pid)) { + if (intr && !sc_intr_set_process(intr, pid)) { // Already interrupted return false; } ssize_t ret = sc_pipe_read(pipe, data, len); - sc_intr_set_process(intr, SC_PROCESS_NONE); + if (intr) { + sc_intr_set_process(intr, SC_PROCESS_NONE); + } + return ret; } ssize_t sc_pipe_read_all_intr(struct sc_intr *intr, sc_pid pid, sc_pipe pipe, char *data, size_t len) { - if (!sc_intr_set_process(intr, pid)) { + if (intr && !sc_intr_set_process(intr, pid)) { // Already interrupted return false; } ssize_t ret = sc_pipe_read_all(pipe, data, len); - sc_intr_set_process(intr, SC_PROCESS_NONE); + if (intr) { + sc_intr_set_process(intr, SC_PROCESS_NONE); + } + return ret; } From 25296ae16710c0c7bf69e60f4e1aa952e0443075 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 10 Feb 2022 08:42:15 +0100 Subject: [PATCH 1144/2244] Kill adb daemon in OTG mode on Windows On Windows, it is not possible to open a USB device from several process, so HID events may only work if no adb daemon is running. PR #3011 --- app/src/adb/adb.c | 8 ++++++++ app/src/adb/adb.h | 3 +++ app/src/usb/scrcpy_otg.c | 10 ++++++++++ 3 files changed, 21 insertions(+) diff --git a/app/src/adb/adb.c b/app/src/adb/adb.c index 8742c4d0..4ddb93f3 100644 --- a/app/src/adb/adb.c +++ b/app/src/adb/adb.c @@ -204,6 +204,14 @@ sc_adb_start_server(struct sc_intr *intr, unsigned flags) { return process_check_success_intr(intr, pid, "adb start-server", flags); } +bool +sc_adb_kill_server(struct sc_intr *intr, unsigned flags) { + const char *const argv[] = SC_ADB_COMMAND("kill-server"); + + sc_pid pid = sc_adb_execute(argv, flags); + return process_check_success_intr(intr, pid, "adb kill-server", flags); +} + bool sc_adb_forward(struct sc_intr *intr, const char *serial, uint16_t local_port, const char *device_socket_name, unsigned flags) { diff --git a/app/src/adb/adb.h b/app/src/adb/adb.h index 6ea6e897..10e8f293 100644 --- a/app/src/adb/adb.h +++ b/app/src/adb/adb.h @@ -36,6 +36,9 @@ sc_adb_execute(const char *const argv[], unsigned flags); bool sc_adb_start_server(struct sc_intr *intr, unsigned flags); +bool +sc_adb_kill_server(struct sc_intr *intr, unsigned flags); + bool sc_adb_forward(struct sc_intr *intr, const char *serial, uint16_t local_port, const char *device_socket_name, unsigned flags); diff --git a/app/src/usb/scrcpy_otg.c b/app/src/usb/scrcpy_otg.c index 1eaa2537..1c53410e 100644 --- a/app/src/usb/scrcpy_otg.c +++ b/app/src/usb/scrcpy_otg.c @@ -2,6 +2,7 @@ #include +#include "adb/adb.h" #include "events.h" #include "screen_otg.h" #include "util/log.h" @@ -75,6 +76,15 @@ scrcpy_otg(struct scrcpy_options *options) { bool aoa_started = false; bool aoa_initialized = false; +#ifdef _WIN32 + // On Windows, only one process could open a USB device + // + LOGI("Killing adb daemon (if any)..."); + unsigned flags = SC_ADB_NO_STDOUT | SC_ADB_NO_STDERR | SC_ADB_NO_LOGERR; + // uninterruptible (intr == NULL), but in practice it's very quick + sc_adb_kill_server(NULL, flags); +#endif + static const struct sc_usb_callbacks cbs = { .on_disconnected = sc_usb_on_disconnected, }; From 73a5311ac6c6570f1c9d23cd943c8d9aa0b281fe Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 10 Feb 2022 09:01:31 +0100 Subject: [PATCH 1145/2244] Forbid HID input without OTG on Windows On Windows, if the adb daemon is running, opening the USB device will necessarily fail, so HID input is not possible. Refs #2773 PR #3011 --- app/src/cli.c | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/app/src/cli.c b/app/src/cli.c index d8ae4f53..48289678 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -1680,6 +1680,18 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } #ifdef HAVE_USB + +# ifdef _WIN32 + if (!opts->otg && (opts->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_HID + || opts->mouse_input_mode == SC_MOUSE_INPUT_MODE_HID)) { + LOGE("On Windows, it is not possible to open a USB device already open " + "by another process (like adb)."); + LOGE("Therefore, -K/--hid-keyboard and -M/--hid-mouse may only work in " + "OTG mode (--otg)."); + return false; + } +# endif + if (opts->otg) { // OTG mode is compatible with only very few options. // Only report obvious errors. From d9bc5082ab24d214928be499f70c2aaed107b0df Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 20 Feb 2022 17:11:07 +0100 Subject: [PATCH 1146/2244] Disable USB features for win32 Currently, there is an issue with the libusb prebuilt dll. Refs libusb/#1049 PR #3011 --- release.mk | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/release.mk b/release.mk index 00d2389f..98b84a8e 100644 --- a/release.mk +++ b/release.mk @@ -75,10 +75,12 @@ prepare-deps-win64: @app/prebuilt-deps/prepare-libusb.sh build-win32: prepare-deps-win32 + # -Dusb=false because of libusb-win32 build issue, cf #3011 [ -d "$(WIN32_BUILD_DIR)" ] || ( mkdir "$(WIN32_BUILD_DIR)" && \ meson "$(WIN32_BUILD_DIR)" \ --cross-file cross_win32.txt \ --buildtype release --strip -Db_lto=true \ + -Dusb=false \ -Dcompile_server=false \ -Dportable=true ) ninja -C "$(WIN32_BUILD_DIR)" @@ -109,7 +111,7 @@ dist-win32: build-server build-win32 cp app/prebuilt-deps/data/platform-tools-31.0.3/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-31.0.3/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/SDL2-2.0.20/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/libusb-1.0.25/MinGW32/dll/libusb-1.0.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + #cp app/prebuilt-deps/data/libusb-1.0.25/MinGW32/dll/libusb-1.0.dll "$(DIST)/$(WIN32_TARGET_DIR)/" dist-win64: build-server build-win64 mkdir -p "$(DIST)/$(WIN64_TARGET_DIR)" From 36c75e15b8e9eeb01bd287a42fa3f1513a728ebb Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 20 Feb 2022 17:56:50 +0100 Subject: [PATCH 1147/2244] Move data/ to app/ The files in data/ are specific to the client app (not the server). This also avoids to reference the parent directory (../) from app/meson.build. Refs 8d583d36e259ba7f5f21d7a703cca73184200aa9 --- {data => app/data}/icon.ico | Bin {data => app/data}/icon.png | Bin {data => app/data}/icon.svg | 0 {data => app/data}/open_a_terminal_here.bat | 0 {data => app/data}/scrcpy-console.bat | 0 {data => app/data}/scrcpy-noconsole.vbs | 0 app/meson.build | 2 +- app/scrcpy-windows.rc | 2 +- release.mk | 16 ++++++++-------- 9 files changed, 10 insertions(+), 10 deletions(-) rename {data => app/data}/icon.ico (100%) rename {data => app/data}/icon.png (100%) rename {data => app/data}/icon.svg (100%) rename {data => app/data}/open_a_terminal_here.bat (100%) rename {data => app/data}/scrcpy-console.bat (100%) rename {data => app/data}/scrcpy-noconsole.vbs (100%) diff --git a/data/icon.ico b/app/data/icon.ico similarity index 100% rename from data/icon.ico rename to app/data/icon.ico diff --git a/data/icon.png b/app/data/icon.png similarity index 100% rename from data/icon.png rename to app/data/icon.png diff --git a/data/icon.svg b/app/data/icon.svg similarity index 100% rename from data/icon.svg rename to app/data/icon.svg diff --git a/data/open_a_terminal_here.bat b/app/data/open_a_terminal_here.bat similarity index 100% rename from data/open_a_terminal_here.bat rename to app/data/open_a_terminal_here.bat diff --git a/data/scrcpy-console.bat b/app/data/scrcpy-console.bat similarity index 100% rename from data/scrcpy-console.bat rename to app/data/scrcpy-console.bat diff --git a/data/scrcpy-noconsole.vbs b/app/data/scrcpy-noconsole.vbs similarity index 100% rename from data/scrcpy-noconsole.vbs rename to app/data/scrcpy-noconsole.vbs diff --git a/app/meson.build b/app/meson.build index a0086619..6449439a 100644 --- a/app/meson.build +++ b/app/meson.build @@ -224,7 +224,7 @@ executable('scrcpy', src, c_args: []) install_man('scrcpy.1') -install_data('../data/icon.png', +install_data('data/icon.png', rename: 'scrcpy.png', install_dir: 'share/icons/hicolor/256x256/apps') diff --git a/app/scrcpy-windows.rc b/app/scrcpy-windows.rc index b130dc6a..dbd6aedb 100644 --- a/app/scrcpy-windows.rc +++ b/app/scrcpy-windows.rc @@ -1,6 +1,6 @@ #include -0 ICON "../data/icon.ico" +0 ICON "data/icon.ico" 1 RT_MANIFEST "scrcpy-windows.manifest" 2 VERSIONINFO BEGIN diff --git a/release.mk b/release.mk index 98b84a8e..caf6ab1a 100644 --- a/release.mk +++ b/release.mk @@ -98,10 +98,10 @@ dist-win32: build-server build-win32 mkdir -p "$(DIST)/$(WIN32_TARGET_DIR)" cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(DIST)/$(WIN32_TARGET_DIR)/" cp "$(WIN32_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/" - cp data/scrcpy-console.bat "$(DIST)/$(WIN32_TARGET_DIR)" - cp data/scrcpy-noconsole.vbs "$(DIST)/$(WIN32_TARGET_DIR)" - cp data/icon.png "$(DIST)/$(WIN32_TARGET_DIR)" - cp data/open_a_terminal_here.bat "$(DIST)/$(WIN32_TARGET_DIR)" + cp app/data/scrcpy-console.bat "$(DIST)/$(WIN32_TARGET_DIR)" + cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN32_TARGET_DIR)" + cp app/data/icon.png "$(DIST)/$(WIN32_TARGET_DIR)" + cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN32_TARGET_DIR)" cp app/prebuilt-deps/data/ffmpeg-win32-4.3.1/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/ffmpeg-win32-4.3.1/bin/avcodec-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/ffmpeg-win32-4.3.1/bin/avformat-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" @@ -117,10 +117,10 @@ dist-win64: build-server build-win64 mkdir -p "$(DIST)/$(WIN64_TARGET_DIR)" cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(DIST)/$(WIN64_TARGET_DIR)/" cp "$(WIN64_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/" - cp data/scrcpy-console.bat "$(DIST)/$(WIN64_TARGET_DIR)" - cp data/scrcpy-noconsole.vbs "$(DIST)/$(WIN64_TARGET_DIR)" - cp data/icon.png "$(DIST)/$(WIN64_TARGET_DIR)" - cp data/open_a_terminal_here.bat "$(DIST)/$(WIN64_TARGET_DIR)" + cp app/data/scrcpy-console.bat "$(DIST)/$(WIN64_TARGET_DIR)" + cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN64_TARGET_DIR)" + cp app/data/icon.png "$(DIST)/$(WIN64_TARGET_DIR)" + cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN64_TARGET_DIR)" cp app/prebuilt-deps/data/ffmpeg-win64-5.0/bin/avutil-57.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/ffmpeg-win64-5.0/bin/avcodec-59.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/ffmpeg-win64-5.0/bin/avformat-59.dll "$(DIST)/$(WIN64_TARGET_DIR)/" From c070723bc8b9b60255605c409b7a43329d27b4cd Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 18 Feb 2022 08:34:50 +0100 Subject: [PATCH 1148/2244] Add sc_vector Adapt vlc_vector [1], that I initially wrote while implementing the VLC playlist [2]. Change the implementation to use "statement expressions" [3], which are forbidden in VLC because "non-standard", but: - they are supported by gcc and clang; - they are already used in the scrcpy codebase; - they avoid implementation hacks (VLC_VECTOR_FAILFLAG_); - they allow a better API (sc_vector_index_of() may return the result without an output parameter). PR #3035 [1]: https://code.videolan.org/videolan/vlc/-/blob/0857947abaed9c89810cd96353aaa1b7e6ba3b0d/include/vlc_vector.h [2]: https://blog.rom1v.com/2019/05/a-new-core-playlist-for-vlc-4 [3]: https://gcc.gnu.org/onlinedocs/gcc/Statement-Exprs.html --- app/meson.build | 3 + app/src/util/vector.h | 539 ++++++++++++++++++++++++++++++++++++++++ app/tests/test_vector.c | 421 +++++++++++++++++++++++++++++++ 3 files changed, 963 insertions(+) create mode 100644 app/src/util/vector.h create mode 100644 app/tests/test_vector.c diff --git a/app/meson.build b/app/meson.build index 6449439a..6255bcbc 100644 --- a/app/meson.build +++ b/app/meson.build @@ -282,6 +282,9 @@ if get_option('buildtype') == 'debug' 'src/util/str.c', 'src/util/strbuf.c', ]], + ['test_vector', [ + 'tests/test_vector.c', + ]], ] foreach t : tests diff --git a/app/src/util/vector.h b/app/src/util/vector.h new file mode 100644 index 00000000..a08fa9d6 --- /dev/null +++ b/app/src/util/vector.h @@ -0,0 +1,539 @@ +#ifndef SC_VECTOR_H +#define SC_VECTOR_H + +#include "common.h" + +#include +#include + +// Adapted from vlc_vector: +// + +/** + * Vector struct body + * + * A vector is a dynamic array, managed by the sc_vector_* helpers. + * + * It is generic over the type of its items, so it is implemented as macros. + * + * To use a vector, a new type must be defined: + * + * struct vec_int SC_VECTOR(int); + * + * The struct may be anonymous: + * + * struct SC_VECTOR(const char *) names; + * + * Vector size is accessible via `vec.size`, and items are intended to be + * accessed directly, via `vec.data[i]`. + * + * Functions and macros having name ending with '_' are private. + */ +#define SC_VECTOR(type) \ +{ \ + size_t cap; \ + size_t size; \ + type *data; \ +} + +/** + * Static initializer for a vector + */ +#define SC_VECTOR_INITIALIZER { 0, 0, NULL } + +/** + * Initialize an empty vector + */ +#define sc_vector_init(pv) \ +({ \ + (pv)->cap = 0; \ + (pv)->size = 0; \ + (pv)->data = NULL; \ +}) + +/** + * Destroy a vector + * + * The vector may not be used anymore unless sc_vector_init() is called. + */ +#define sc_vector_destroy(pv) \ + free((pv)->data) + +/** + * Clear a vector + * + * Remove all items from the vector. + */ +#define sc_vector_clear(pv) \ +({ \ + sc_vector_destroy(pv); \ + sc_vector_init(pv);\ +}) + +/** + * The minimal allocation size, in number of items + * + * Private. + */ +#define SC_VECTOR_MINCAP_ ((size_t) 10) + +static inline size_t +sc_vector_min_(size_t a, size_t b) +{ + return a < b ? a : b; +} + +static inline size_t +sc_vector_max_(size_t a, size_t b) +{ + return a > b ? a : b; +} + +static inline size_t +sc_vector_clamp_(size_t x, size_t min, size_t max) +{ + return sc_vector_max_(min, sc_vector_min_(max, x)); +} + +/** + * Realloc data and update vector fields + * + * On reallocation success, update the vector capacity (*pcap) and size + * (*psize), and return the reallocated data. + * + * On reallocation failure, return NULL without any change. + * + * Private. + * + * \param ptr the current `data` field of the vector to realloc + * \param count the requested capacity, in number of items + * \param size the size of one item + * \param pcap a pointer to the `cap` field of the vector [IN/OUT] + * \param psize a pointer to the `size` field of the vector [IN/OUT] + * \return the new ptr on success, NULL on error + */ +static inline void * +sc_vector_reallocdata_(void *ptr, size_t count, size_t size, + size_t *restrict pcap, size_t *restrict psize) +{ + void *p = realloc(ptr, count * size); + if (!p) { + return NULL; + } + + *pcap = count; + *psize = sc_vector_min_(*psize, count); + return p; +} + +#define sc_vector_realloc_(pv, newcap) \ +({ \ + void *p = sc_vector_reallocdata_((pv)->data, newcap, sizeof(*(pv)->data), \ + &(pv)->cap, &(pv)->size); \ + if (p) { \ + (pv)->data = p; \ + } \ + (bool) p; \ +}); + +#define sc_vector_resize_(pv, newcap) \ +({ \ + bool ok; \ + if ((pv)->cap == (newcap)) { \ + ok = true; \ + } else if ((newcap) > 0) { \ + ok = sc_vector_realloc_(pv, (newcap)); \ + } else { \ + sc_vector_clear(pv); \ + ok = true; \ + } \ + ok; \ +}) + +static inline size_t +sc_vector_growsize_(size_t value) +{ + /* integer multiplication by 1.5 */ + return value + (value >> 1); +} + +/* SIZE_MAX/2 to fit in ssize_t, and so that cap*1.5 does not overflow. */ +#define sc_vector_max_cap_(pv) (SIZE_MAX / 2 / sizeof(*(pv)->data)) + +/** + * Increase the capacity of the vector to at least `mincap` + * + * \param pv a pointer to the vector + * \param mincap (size_t) the requested capacity + * \retval true if no allocation failed + * \retval false on allocation failure (the vector is left untouched) + */ +#define sc_vector_reserve(pv, mincap) \ +({ \ + bool ok; \ + /* avoid to allocate tiny arrays (< SC_VECTOR_MINCAP_) */ \ + size_t mincap_ = sc_vector_max_(mincap, SC_VECTOR_MINCAP_); \ + if (mincap_ <= (pv)->cap) { \ + /* nothing to do */ \ + ok = true; \ + } else if (mincap_ <= sc_vector_max_cap_(pv)) { \ + /* not too big */ \ + size_t newsize = sc_vector_growsize_((pv)->cap); \ + newsize = sc_vector_clamp_(newsize, mincap_, sc_vector_max_cap_(pv)); \ + ok = sc_vector_realloc_(pv, newsize); \ + } else { \ + ok = false; \ + } \ + ok; \ +}) + +#define sc_vector_shrink_to_fit(pv) \ + /* decreasing the size may not fail */ \ + (void) sc_vector_resize_(pv, (pv)->size) + +/** + * Resize the vector down automatically + * + * Shrink only when necessary (in practice when cap > (size+5)*1.5) + * + * \param pv a pointer to the vector + */ +#define sc_vector_autoshrink(pv) \ +({ \ + bool must_shrink = \ + /* do not shrink to tiny size */ \ + (pv)->cap > SC_VECTOR_MINCAP_ && \ + /* no need to shrink */ \ + (pv)->cap >= sc_vector_growsize_((pv)->size + 5); \ + if (must_shrink) { \ + size_t newsize = sc_vector_max_((pv)->size + 5, SC_VECTOR_MINCAP_); \ + sc_vector_resize_(pv, newsize); \ + } \ +}) + +#define sc_vector_check_same_ptr_type_(a, b) \ + (void) ((a) == (b)) /* warn on type mismatch */ + +/** + * Push an item at the end of the vector + * + * The amortized complexity is O(1). + * + * \param pv a pointer to the vector + * \param item the item to append + * \retval true if no allocation failed + * \retval false on allocation failure (the vector is left untouched) + */ +#define sc_vector_push(pv, item) \ +({ \ + bool ok = sc_vector_reserve(pv, (pv)->size + 1); \ + if (ok) { \ + (pv)->data[(pv)->size++] = (item); \ + } \ + ok; \ +}) + +/** + * Append `count` items at the end of the vector + * + * \param pv a pointer to the vector + * \param items the items array to append + * \param count the number of items in the array + * \retval true if no allocation failed + * \retval false on allocation failure (the vector is left untouched) + */ +#define sc_vector_push_all(pv, items, count) \ + sc_vector_push_all_(pv, items, (size_t) count) + +#define sc_vector_push_all_(pv, items, count) \ +({ \ + sc_vector_check_same_ptr_type_((pv)->data, items); \ + bool ok = sc_vector_reserve(pv, (pv)->size + (count)); \ + if (ok) { \ + memcpy(&(pv)->data[(pv)->size], items, (count) * sizeof(*(pv)->data)); \ + (pv)->size += count; \ + } \ + ok; \ +}) + +/** + * Insert an hole of size `count` to the given index + * + * The items in range [index; size-1] will be moved. The items in the hole are + * left uninitialized. + * + * \param pv a pointer to the vector + * \param index the index where the hole is to be inserted + * \param count the number of items in the hole + * \retval true if no allocation failed + * \retval false on allocation failure (the vector is left untouched) + */ +#define sc_vector_insert_hole(pv, index, count) \ + sc_vector_insert_hole_(pv, (size_t) index, (size_t) count); + +#define sc_vector_insert_hole_(pv, index, count) \ +({ \ + bool ok = sc_vector_reserve(pv, (pv)->size + (count)); \ + if (ok) { \ + if ((index) < (pv)->size) { \ + memmove(&(pv)->data[(index) + (count)], \ + &(pv)->data[(index)], \ + ((pv)->size - (index)) * sizeof(*(pv)->data)); \ + } \ + (pv)->size += count; \ + } \ + ok; \ +}) + +/** + * Insert an item at the given index + * + * The items in range [index; size-1] will be moved. + * + * \param pv a pointer to the vector + * \param index the index where the item is to be inserted + * \param item the item to append + * \retval true if no allocation failed + * \retval false on allocation failure (the vector is left untouched) + */ +#define sc_vector_insert(pv, index, item) \ + sc_vector_insert_(pv, (size_t) index, (size_t) item); + +#define sc_vector_insert_(pv, index, item) \ +({ \ + bool ok = sc_vector_insert_hole_(pv, index, 1); \ + if (ok) { \ + (pv)->data[index] = (item); \ + } \ + ok; \ +}) + +/** + * Insert `count` items at the given index + * + * The items in range [index; size-1] will be moved. + * + * \param pv a pointer to the vector + * \param index the index where the items are to be inserted + * \param items the items array to append + * \param count the number of items in the array + * \retval true if no allocation failed + * \retval false on allocation failure (the vector is left untouched) + */ +#define sc_vector_insert_all(pv, index, items, count) \ + sc_vector_insert_all_(pv, (size_t) index, items, (size_t) count) + +#define sc_vector_insert_all_(pv, index, items, count) \ +({ \ + sc_vector_check_same_ptr_type_((pv)->data, items); \ + bool ok = sc_vector_insert_hole_(pv, index, count); \ + if (ok) { \ + memcpy(&(pv)->data[index], items, count * sizeof(*(pv)->data)); \ + } \ + ok; \ +}) + +/** Reverse a char array in place */ +static inline void +sc_char_array_reverse(char *array, size_t len) +{ + for (size_t i = 0; i < len / 2; ++i) + { + char c = array[i]; + array[i] = array[len - i - 1]; + array[len - i - 1] = c; + } +} + +/** + * Right-rotate a (char) array in place + * + * For example, left-rotating a char array containing {1, 2, 3, 4, 5, 6} with + * distance 4 will result in {5, 6, 1, 2, 3, 4}. + * + * Private. + */ +static inline void +sc_char_array_rotate_left(char *array, size_t len, size_t distance) +{ + sc_char_array_reverse(array, distance); + sc_char_array_reverse(&array[distance], len - distance); + sc_char_array_reverse(array, len); +} + +/** + * Right-rotate a (char) array in place + * + * For example, left-rotating a char array containing {1, 2, 3, 4, 5, 6} with + * distance 2 will result in {5, 6, 1, 2, 3, 4}. + * + * Private. + */ +static inline void +sc_char_array_rotate_right(char *array, size_t len, size_t distance) +{ + sc_char_array_rotate_left(array, len, len - distance); +} + +/** + * Move items in a (char) array in place + * + * Move slice [index, count] to target. + */ +static inline void +sc_char_array_move(char *array, size_t idx, size_t count, size_t target) +{ + if (idx < target) { + sc_char_array_rotate_left(&array[idx], target - idx + count, count); + } else { + sc_char_array_rotate_right(&array[target], idx - target + count, count); + } +} + +/** + * Move a slice of items to a given target index + * + * The items in range [index; count] will be moved so that the *new* position + * of the first item is `target`. + * + * \param pv a pointer to the vector + * \param index the index of the first item to move + * \param count the number of items to move + * \param target the new index of the moved slice + */ +#define sc_vector_move_slice(pv, index, count, target) \ + sc_vector_move_slice_(pv, (size_t) index, count, (size_t) target); + +#define sc_vector_move_slice_(pv, index, count, target) \ +({ \ + sc_char_array_move((char *) (pv)->data, \ + (index) * sizeof(*(pv)->data), \ + (count) * sizeof(*(pv)->data), \ + (target) * sizeof(*(pv)->data)); \ +}) + +/** + * Move an item to a given target index + * + * The items will be moved so that its *new* position is `target`. + * + * \param pv a pointer to the vector + * \param index the index of the item to move + * \param target the new index of the moved item + */ +#define sc_vector_move(pv, index, target) \ + sc_vector_move_slice(pv, index, 1, target) + +/** + * Remove a slice of items, without shrinking the array + * + * If you have no good reason to use the _noshrink() version, use + * sc_vector_remove_slice() instead. + * + * The items in range [index+count; size-1] will be moved. + * + * \param pv a pointer to the vector + * \param index the index of the first item to remove + * \param count the number of items to remove + */ +#define sc_vector_remove_slice_noshrink(pv, index, count) \ + sc_vector_remove_slice_noshrink_(pv, (size_t) index, (size_t) count) + +#define sc_vector_remove_slice_noshrink_(pv, index, count) \ +({ \ + if ((index) + (count) < (pv)->size) { \ + memmove(&(pv)->data[index], \ + &(pv)->data[(index) + (count)], \ + ((pv)->size - (index) - (count)) * sizeof(*(pv)->data)); \ + } \ + (pv)->size -= count; \ +}) + +/** + * Remove a slice of items + * + * The items in range [index+count; size-1] will be moved. + * + * \param pv a pointer to the vector + * \param index the index of the first item to remove + * \param count the number of items to remove + */ +#define sc_vector_remove_slice(pv, index, count) \ +({ \ + sc_vector_remove_slice_noshrink(pv, index, count); \ + sc_vector_autoshrink(pv); \ +}) + +/** + * Remove an item, without shrinking the array + * + * If you have no good reason to use the _noshrink() version, use + * sc_vector_remove() instead. + * + * The items in range [index+1; size-1] will be moved. + * + * \param pv a pointer to the vector + * \param index the index of item to remove + */ +#define sc_vector_remove_noshrink(pv, index) \ + sc_vector_remove_slice_noshrink(pv, index, 1) + +/** + * Remove an item + * + * The items in range [index+1; size-1] will be moved. + * + * \param pv a pointer to the vector + * \param index the index of item to remove + */ +#define sc_vector_remove(pv, index) \ +({ \ + sc_vector_remove_noshrink(pv, index); \ + sc_vector_autoshrink(pv); \ +}) + +/** + * Remove an item + * + * The removed item is replaced by the last item of the vector. + * + * This does not preserve ordering, but is O(1). This is useful when the order + * of items is not meaningful. + * + * \param pv a pointer to the vector + * \param index the index of item to remove + */ +#define sc_vector_swap_remove(pv, index) \ + sc_vector_swap_remove_(pv, (size_t) index); + +#define sc_vector_swap_remove_(pv, index) \ +({ \ + (pv)->data[index] = (pv)->data[(pv)->size-1]; \ + (pv)->size--; \ +}); + +/** + * Return the index of an item + * + * Iterate over all items to find a given item. + * + * Use only for vectors of primitive types or pointers. + * + * Return the index, or -1 if not found. + * + * \param pv a pointer to the vector + * \param item the item to find (compared with ==) + */ +#define sc_vector_index_of(pv, item) \ +({ \ + ssize_t idx = -1; \ + for (size_t i = 0; i < (pv)->size; ++i) { \ + if ((pv)->data[i] == (item)) { \ + idx = (ssize_t) i; \ + break; \ + } \ + } \ + idx; \ +}) + +#endif diff --git a/app/tests/test_vector.c b/app/tests/test_vector.c new file mode 100644 index 00000000..7ca09989 --- /dev/null +++ b/app/tests/test_vector.c @@ -0,0 +1,421 @@ +#include "common.h" + +#include + +#include "util/vector.h" + +static void test_vector_insert_remove(void) { + struct SC_VECTOR(int) vec = SC_VECTOR_INITIALIZER; + + bool ok; + + ok = sc_vector_push(&vec, 42); + assert(ok); + assert(vec.data[0] == 42); + assert(vec.size == 1); + + ok = sc_vector_push(&vec, 37); + assert(ok); + assert(vec.size == 2); + assert(vec.data[0] == 42); + assert(vec.data[1] == 37); + + ok = sc_vector_insert(&vec, 1, 100); + assert(ok); + assert(vec.size == 3); + assert(vec.data[0] == 42); + assert(vec.data[1] == 100); + assert(vec.data[2] == 37); + + ok = sc_vector_push(&vec, 77); + assert(ok); + assert(vec.size == 4); + assert(vec.data[0] == 42); + assert(vec.data[1] == 100); + assert(vec.data[2] == 37); + assert(vec.data[3] == 77); + + sc_vector_remove(&vec, 1); + assert(vec.size == 3); + assert(vec.data[0] == 42); + assert(vec.data[1] == 37); + assert(vec.data[2] == 77); + + sc_vector_clear(&vec); + assert(vec.size == 0); + + sc_vector_destroy(&vec); +} + +static void test_vector_push_array(void) { + struct SC_VECTOR(int) vec = SC_VECTOR_INITIALIZER; + bool ok; + + ok = sc_vector_push(&vec, 3); assert(ok); + ok = sc_vector_push(&vec, 14); assert(ok); + ok = sc_vector_push(&vec, 15); assert(ok); + ok = sc_vector_push(&vec, 92); assert(ok); + ok = sc_vector_push(&vec, 65); assert(ok); + assert(vec.size == 5); + + int items[] = { 1, 2, 3, 4, 5, 6, 7, 8 }; + ok = sc_vector_push_all(&vec, items, 8); + + assert(ok); + assert(vec.size == 13); + assert(vec.data[0] == 3); + assert(vec.data[1] == 14); + assert(vec.data[2] == 15); + assert(vec.data[3] == 92); + assert(vec.data[4] == 65); + assert(vec.data[5] == 1); + assert(vec.data[6] == 2); + assert(vec.data[7] == 3); + assert(vec.data[8] == 4); + assert(vec.data[9] == 5); + assert(vec.data[10] == 6); + assert(vec.data[11] == 7); + assert(vec.data[12] == 8); + + sc_vector_destroy(&vec); +} + +static void test_vector_insert_array(void) { + struct SC_VECTOR(int) vec = SC_VECTOR_INITIALIZER; + bool ok; + + ok = sc_vector_push(&vec, 3); assert(ok); + ok = sc_vector_push(&vec, 14); assert(ok); + ok = sc_vector_push(&vec, 15); assert(ok); + ok = sc_vector_push(&vec, 92); assert(ok); + ok = sc_vector_push(&vec, 65); assert(ok); + assert(vec.size == 5); + + int items[] = { 1, 2, 3, 4, 5, 6, 7, 8 }; + ok = sc_vector_insert_all(&vec, 3, items, 8); + assert(ok); + assert(vec.size == 13); + assert(vec.data[0] == 3); + assert(vec.data[1] == 14); + assert(vec.data[2] == 15); + assert(vec.data[3] == 1); + assert(vec.data[4] == 2); + assert(vec.data[5] == 3); + assert(vec.data[6] == 4); + assert(vec.data[7] == 5); + assert(vec.data[8] == 6); + assert(vec.data[9] == 7); + assert(vec.data[10] == 8); + assert(vec.data[11] == 92); + assert(vec.data[12] == 65); + + sc_vector_destroy(&vec); +} + +static void test_vector_remove_slice(void) { + struct SC_VECTOR(int) vec = SC_VECTOR_INITIALIZER; + + bool ok; + + for (int i = 0; i < 100; ++i) + { + ok = sc_vector_push(&vec, i); + assert(ok); + } + + assert(vec.size == 100); + + sc_vector_remove_slice(&vec, 32, 60); + assert(vec.size == 40); + assert(vec.data[31] == 31); + assert(vec.data[32] == 92); + + sc_vector_destroy(&vec); +} + +static void test_vector_swap_remove(void) { + struct SC_VECTOR(int) vec = SC_VECTOR_INITIALIZER; + + bool ok; + + ok = sc_vector_push(&vec, 3); assert(ok); + ok = sc_vector_push(&vec, 14); assert(ok); + ok = sc_vector_push(&vec, 15); assert(ok); + ok = sc_vector_push(&vec, 92); assert(ok); + ok = sc_vector_push(&vec, 65); assert(ok); + assert(vec.size == 5); + + sc_vector_swap_remove(&vec, 1); + assert(vec.size == 4); + assert(vec.data[0] == 3); + assert(vec.data[1] == 65); + assert(vec.data[2] == 15); + assert(vec.data[3] == 92); + + sc_vector_destroy(&vec); +} + +static void test_vector_index_of(void) { + struct SC_VECTOR(int) vec; + sc_vector_init(&vec); + + bool ok; + + for (int i = 0; i < 10; ++i) + { + ok = sc_vector_push(&vec, i); + assert(ok); + } + + ssize_t idx; + + idx = sc_vector_index_of(&vec, 0); + assert(idx == 0); + + idx = sc_vector_index_of(&vec, 1); + assert(idx == 1); + + idx = sc_vector_index_of(&vec, 4); + assert(idx == 4); + + idx = sc_vector_index_of(&vec, 9); + assert(idx == 9); + + idx = sc_vector_index_of(&vec, 12); + assert(idx == -1); + + sc_vector_destroy(&vec); +} + +static void test_vector_grow() { + struct SC_VECTOR(int) vec = SC_VECTOR_INITIALIZER; + + bool ok; + + for (int i = 0; i < 50; ++i) + { + ok = sc_vector_push(&vec, i); /* append */ + assert(ok); + } + + assert(vec.cap >= 50); + assert(vec.size == 50); + + for (int i = 0; i < 25; ++i) + { + ok = sc_vector_insert(&vec, 20, i); /* insert in the middle */ + assert(ok); + } + + assert(vec.cap >= 75); + assert(vec.size == 75); + + for (int i = 0; i < 25; ++i) + { + ok = sc_vector_insert(&vec, 0, i); /* prepend */ + assert(ok); + } + + assert(vec.cap >= 100); + assert(vec.size == 100); + + for (int i = 0; i < 50; ++i) + sc_vector_remove(&vec, 20); /* remove from the middle */ + + assert(vec.cap >= 50); + assert(vec.size == 50); + + for (int i = 0; i < 25; ++i) + sc_vector_remove(&vec, 0); /* remove from the head */ + + assert(vec.cap >= 25); + assert(vec.size == 25); + + for (int i = 24; i >=0; --i) + sc_vector_remove(&vec, i); /* remove from the tail */ + + assert(vec.size == 0); + + sc_vector_destroy(&vec); +} + +static void test_vector_exp_growth(void) { + struct SC_VECTOR(int) vec = SC_VECTOR_INITIALIZER; + + size_t oldcap = vec.cap; + int realloc_count = 0; + bool ok; + for (int i = 0; i < 10000; ++i) + { + ok = sc_vector_push(&vec, i); + assert(ok); + if (vec.cap != oldcap) + { + realloc_count++; + oldcap = vec.cap; + } + } + + /* Test speciically for an expected growth factor of 1.5. In practice, the + * result is even lower (19) due to the first alloc of size 10 */ + assert(realloc_count <= 23); /* ln(10000) / ln(1.5) ~= 23 */ + + realloc_count = 0; + for (int i = 9999; i >= 0; --i) + { + sc_vector_remove(&vec, i); + if (vec.cap != oldcap) + { + realloc_count++; + oldcap = vec.cap; + } + } + + assert(realloc_count <= 23); /* same expectations for removals */ + assert(realloc_count > 0); /* sc_vector_remove() must autoshrink */ + + sc_vector_destroy(&vec); +} + +static void test_vector_reserve(void) { + struct SC_VECTOR(int) vec = SC_VECTOR_INITIALIZER; + + bool ok; + + ok = sc_vector_reserve(&vec, 800); + assert(ok); + assert(vec.cap >= 800); + assert(vec.size == 0); + + size_t initial_cap = vec.cap; + + for (int i = 0; i < 800; ++i) + { + ok = sc_vector_push(&vec, i); + assert(ok); + assert(vec.cap == initial_cap); /* no realloc */ + } + + sc_vector_destroy(&vec); +} + +static void test_vector_shrink_to_fit(void) { + struct SC_VECTOR(int) vec = SC_VECTOR_INITIALIZER; + + bool ok; + + ok = sc_vector_reserve(&vec, 800); + assert(ok); + for (int i = 0; i < 250; ++i) + { + ok = sc_vector_push(&vec, i); + assert(ok); + } + + assert(vec.cap >= 800); + assert(vec.size == 250); + + sc_vector_shrink_to_fit(&vec); + assert(vec.cap == 250); + assert(vec.size == 250); + + sc_vector_destroy(&vec); +} + +static void test_vector_move(void) { + struct SC_VECTOR(int) vec = SC_VECTOR_INITIALIZER; + + for (int i = 0; i < 7; ++i) + { + bool ok = sc_vector_push(&vec, i); + assert(ok); + } + + /* move item at 1 so that its new position is 4 */ + sc_vector_move(&vec, 1, 4); + + assert(vec.size == 7); + assert(vec.data[0] == 0); + assert(vec.data[1] == 2); + assert(vec.data[2] == 3); + assert(vec.data[3] == 4); + assert(vec.data[4] == 1); + assert(vec.data[5] == 5); + assert(vec.data[6] == 6); + + sc_vector_destroy(&vec); +} + +static void test_vector_move_slice_forward(void) { + struct SC_VECTOR(int) vec = SC_VECTOR_INITIALIZER; + + for (int i = 0; i < 10; ++i) + { + bool ok = sc_vector_push(&vec, i); + assert(ok); + } + + /* move slice {2, 3, 4, 5} so that its new position is 5 */ + sc_vector_move_slice(&vec, 2, 4, 5); + + assert(vec.size == 10); + assert(vec.data[0] == 0); + assert(vec.data[1] == 1); + assert(vec.data[2] == 6); + assert(vec.data[3] == 7); + assert(vec.data[4] == 8); + assert(vec.data[5] == 2); + assert(vec.data[6] == 3); + assert(vec.data[7] == 4); + assert(vec.data[8] == 5); + assert(vec.data[9] == 9); + + sc_vector_destroy(&vec); +} + +static void test_vector_move_slice_backward(void) { + struct SC_VECTOR(int) vec = SC_VECTOR_INITIALIZER; + + for (int i = 0; i < 10; ++i) + { + bool ok = sc_vector_push(&vec, i); + assert(ok); + } + + /* move slice {5, 6, 7} so that its new position is 2 */ + sc_vector_move_slice(&vec, 5, 3, 2); + + assert(vec.size == 10); + assert(vec.data[0] == 0); + assert(vec.data[1] == 1); + assert(vec.data[2] == 5); + assert(vec.data[3] == 6); + assert(vec.data[4] == 7); + assert(vec.data[5] == 2); + assert(vec.data[6] == 3); + assert(vec.data[7] == 4); + assert(vec.data[8] == 8); + assert(vec.data[9] == 9); + + sc_vector_destroy(&vec); +} + +int main(int argc, char *argv[]) { + (void) argc; + (void) argv; + + test_vector_insert_remove(); + test_vector_push_array(); + test_vector_insert_array(); + test_vector_remove_slice(); + test_vector_swap_remove(); + test_vector_move(); + test_vector_move_slice_forward(); + test_vector_move_slice_backward(); + test_vector_index_of(); + test_vector_grow(); + test_vector_exp_growth(); + test_vector_reserve(); + test_vector_shrink_to_fit(); + return 0; +} From 1790e88278dee3779073849287f5084c5a275eb6 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 18 Feb 2022 19:40:05 +0100 Subject: [PATCH 1149/2244] Use vector for listing USB devices This avoids the hardcoded maximum number of USB devices detected (16). Refs #3029 PR #3035 --- app/src/usb/usb.c | 58 +++++++++++++++++++++++++++-------------------- 1 file changed, 33 insertions(+), 25 deletions(-) diff --git a/app/src/usb/usb.c b/app/src/usb/usb.c index 276b0067..32a66f98 100644 --- a/app/src/usb/usb.c +++ b/app/src/usb/usb.c @@ -3,6 +3,9 @@ #include #include "util/log.h" +#include "util/vector.h" + +struct sc_vec_usb_devices SC_VECTOR(struct sc_usb_device); static char * read_string(libusb_device_handle *handle, uint8_t desc_index) { @@ -85,33 +88,39 @@ sc_usb_device_move(struct sc_usb_device *dst, struct sc_usb_device *src) { } void -sc_usb_devices_destroy_all(struct sc_usb_device *usb_devices, size_t count) { - for (size_t i = 0; i < count; ++i) { - sc_usb_device_destroy(&usb_devices[i]); +sc_usb_devices_destroy(struct sc_vec_usb_devices *usb_devices) { + for (size_t i = 0; i < usb_devices->size; ++i) { + sc_usb_device_destroy(&usb_devices->data[i]); } + sc_vector_destroy(usb_devices); } -static ssize_t -sc_usb_list_devices(struct sc_usb *usb, struct sc_usb_device *devices, - size_t len) { +static bool +sc_usb_list_devices(struct sc_usb *usb, struct sc_vec_usb_devices *out_vec) { libusb_device **list; ssize_t count = libusb_get_device_list(usb->context, &list); if (count < 0) { LOGE("List USB devices: libusb error: %s", libusb_strerror(count)); - return -1; + return false; } - size_t idx = 0; - for (size_t i = 0; i < (size_t) count && idx < len; ++i) { + for (size_t i = 0; i < (size_t) count; ++i) { libusb_device *device = list[i]; - if (sc_usb_read_device(device, &devices[idx])) { - ++idx; + struct sc_usb_device usb_device; + if (sc_usb_read_device(device, &usb_device)) { + bool ok = sc_vector_push(out_vec, usb_device); + if (!ok) { + LOG_OOM(); + LOGE("Could not push usb_device to vector"); + sc_usb_device_destroy(&usb_device); + // continue anyway + } } } libusb_free_device_list(list, 1); - return idx; + return true; } static bool @@ -157,29 +166,28 @@ sc_usb_devices_log(enum sc_log_level level, struct sc_usb_device *devices, bool sc_usb_select_device(struct sc_usb *usb, const char *serial, struct sc_usb_device *out_device) { - struct sc_usb_device usb_devices[16]; - ssize_t count = - sc_usb_list_devices(usb, usb_devices, ARRAY_LEN(usb_devices)); - if (count == -1) { + struct sc_vec_usb_devices vec = SC_VECTOR_INITIALIZER; + bool ok = sc_usb_list_devices(usb, &vec); + if (!ok) { LOGE("Could not list USB devices"); return false; } - if (count == 0) { + if (vec.size == 0) { LOGE("Could not find any USB device"); return false; } size_t sel_idx; // index of the single matching device if sel_count == 1 size_t sel_count = - sc_usb_devices_select(usb_devices, count, serial, &sel_idx); + sc_usb_devices_select(vec.data, vec.size, serial, &sel_idx); if (sel_count == 0) { // if count > 0 && sel_count == 0, then necessarily a serial is provided assert(serial); LOGE("Could not find USB device %s", serial); - sc_usb_devices_log(SC_LOG_LEVEL_ERROR, usb_devices, count); - sc_usb_devices_destroy_all(usb_devices, count); + sc_usb_devices_log(SC_LOG_LEVEL_ERROR, vec.data, vec.size); + sc_usb_devices_destroy(&vec); return false; } @@ -190,21 +198,21 @@ sc_usb_select_device(struct sc_usb *usb, const char *serial, } else { LOGE("Multiple (%" SC_PRIsizet ") USB devices:", sel_count); } - sc_usb_devices_log(SC_LOG_LEVEL_ERROR, usb_devices, count); + sc_usb_devices_log(SC_LOG_LEVEL_ERROR, vec.data, vec.size); LOGE("Select a device via -s (--serial)"); - sc_usb_devices_destroy_all(usb_devices, count); + sc_usb_devices_destroy(&vec); return false; } assert(sel_count == 1); // sel_idx is valid only if sel_count == 1 - struct sc_usb_device *device = &usb_devices[sel_idx]; + struct sc_usb_device *device = &vec.data[sel_idx]; LOGD("USB device found:"); - sc_usb_devices_log(SC_LOG_LEVEL_DEBUG, usb_devices, count); + sc_usb_devices_log(SC_LOG_LEVEL_DEBUG, vec.data, vec.size); // Move device into out_device (do not destroy device) sc_usb_device_move(out_device, device); - sc_usb_devices_destroy_all(usb_devices, count); + sc_usb_devices_destroy(&vec); return true; } From 4b8cb042c41e29002d562e80809a6b3a9f01e49f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 18 Feb 2022 21:16:53 +0100 Subject: [PATCH 1150/2244] Use vector for listing ADB devices This avoids the hardcoded maximum number of ADB devices detected (16). Refs #3029 PR #3035 Co-authored-by: Daniel Ansorregui --- app/src/adb/adb.c | 44 +++++++++---------- app/src/adb/adb_device.c | 7 +-- app/src/adb/adb_device.h | 6 ++- app/src/adb/adb_parser.c | 30 ++++++------- app/src/adb/adb_parser.h | 5 +-- app/tests/test_adb_parser.c | 88 +++++++++++++++++++++---------------- 6 files changed, 95 insertions(+), 85 deletions(-) diff --git a/app/src/adb/adb.c b/app/src/adb/adb.c index 4ddb93f3..c14ded92 100644 --- a/app/src/adb/adb.c +++ b/app/src/adb/adb.c @@ -5,6 +5,7 @@ #include #include +#include "adb_device.h" #include "adb_parser.h" #include "util/file.h" #include "util/log.h" @@ -392,16 +393,16 @@ sc_adb_disconnect(struct sc_intr *intr, const char *ip_port, unsigned flags) { return process_check_success_intr(intr, pid, "adb disconnect", flags); } -static ssize_t +static bool sc_adb_list_devices(struct sc_intr *intr, unsigned flags, - struct sc_adb_device *devices, size_t len) { + struct sc_vec_adb_devices *out_vec) { const char *const argv[] = SC_ADB_COMMAND("devices", "-l"); sc_pipe pout; sc_pid pid = sc_adb_execute_p(argv, flags, &pout); if (pid == SC_PROCESS_NONE) { LOGE("Could not execute \"adb devices -l\""); - return -1; + return false; } char buf[4096]; @@ -410,11 +411,11 @@ sc_adb_list_devices(struct sc_intr *intr, unsigned flags, bool ok = process_check_success_intr(intr, pid, "adb devices -l", flags); if (!ok) { - return -1; + return false; } if (r == -1) { - return -1; + return false; } assert((size_t) r < sizeof(buf)); @@ -423,14 +424,14 @@ sc_adb_list_devices(struct sc_intr *intr, unsigned flags, // in the buffer in a single pass LOGW("Result of \"adb devices -l\" does not fit in 4Kb. " "Please report an issue."); - return -1; + return false; } // It is parsed as a NUL-terminated string buf[r] = '\0'; // List all devices to the output list directly - return sc_adb_parse_devices(buf, devices, len); + return sc_adb_parse_devices(buf, out_vec); } static bool @@ -529,22 +530,21 @@ bool sc_adb_select_device(struct sc_intr *intr, const struct sc_adb_device_selector *selector, unsigned flags, struct sc_adb_device *out_device) { - struct sc_adb_device devices[16]; - ssize_t count = - sc_adb_list_devices(intr, flags, devices, ARRAY_LEN(devices)); - if (count == -1) { + struct sc_vec_adb_devices vec = SC_VECTOR_INITIALIZER; + bool ok = sc_adb_list_devices(intr, flags, &vec); + if (!ok) { LOGE("Could not list ADB devices"); return false; } - if (count == 0) { + if (vec.size == 0) { LOGE("Could not find any ADB device"); return false; } size_t sel_idx; // index of the single matching device if sel_count == 1 size_t sel_count = - sc_adb_devices_select(devices, count, selector, &sel_idx); + sc_adb_devices_select(vec.data, vec.size, selector, &sel_idx); if (sel_count == 0) { // if count > 0 && sel_count == 0, then necessarily a selection is @@ -567,8 +567,8 @@ sc_adb_select_device(struct sc_intr *intr, break; } - sc_adb_devices_log(SC_LOG_LEVEL_ERROR, devices, count); - sc_adb_devices_destroy_all(devices, count); + sc_adb_devices_log(SC_LOG_LEVEL_ERROR, vec.data, vec.size); + sc_adb_devices_destroy(&vec); return false; } @@ -594,28 +594,28 @@ sc_adb_select_device(struct sc_intr *intr, assert(!"Unexpected selector type"); break; } - sc_adb_devices_log(SC_LOG_LEVEL_ERROR, devices, count); + sc_adb_devices_log(SC_LOG_LEVEL_ERROR, vec.data, vec.size); LOGE("Select a device via -s (--serial), -d (--select-usb) or -e " "(--select-tcpip)"); - sc_adb_devices_destroy_all(devices, count); + sc_adb_devices_destroy(&vec); return false; } assert(sel_count == 1); // sel_idx is valid only if sel_count == 1 - struct sc_adb_device *device = &devices[sel_idx]; + struct sc_adb_device *device = &vec.data[sel_idx]; - bool ok = sc_adb_device_check_state(device, devices, count); + ok = sc_adb_device_check_state(device, vec.data, vec.size); if (!ok) { - sc_adb_devices_destroy_all(devices, count); + sc_adb_devices_destroy(&vec); return false; } LOGD("ADB device found:"); - sc_adb_devices_log(SC_LOG_LEVEL_DEBUG, devices, count); + sc_adb_devices_log(SC_LOG_LEVEL_DEBUG, vec.data, vec.size); // Move devics into out_device (do not destroy device) sc_adb_device_move(out_device, device); - sc_adb_devices_destroy_all(devices, count); + sc_adb_devices_destroy(&vec); return true; } diff --git a/app/src/adb/adb_device.c b/app/src/adb/adb_device.c index b6ff16a7..cfd5dc30 100644 --- a/app/src/adb/adb_device.c +++ b/app/src/adb/adb_device.c @@ -18,9 +18,10 @@ sc_adb_device_move(struct sc_adb_device *dst, struct sc_adb_device *src) { } void -sc_adb_devices_destroy_all(struct sc_adb_device *devices, size_t count) { - for (size_t i = 0; i < count; ++i) { - sc_adb_device_destroy(&devices[i]); +sc_adb_devices_destroy(struct sc_vec_adb_devices *devices) { + for (size_t i = 0; i < devices->size; ++i) { + sc_adb_device_destroy(&devices->data[i]); } + sc_vector_destroy(devices); } diff --git a/app/src/adb/adb_device.h b/app/src/adb/adb_device.h index ed8362e9..14d0408c 100644 --- a/app/src/adb/adb_device.h +++ b/app/src/adb/adb_device.h @@ -6,6 +6,8 @@ #include #include +#include "util/vector.h" + struct sc_adb_device { char *serial; char *state; @@ -13,6 +15,8 @@ struct sc_adb_device { bool selected; }; +struct sc_vec_adb_devices SC_VECTOR(struct sc_adb_device); + void sc_adb_device_destroy(struct sc_adb_device *device); @@ -29,6 +33,6 @@ void sc_adb_device_move(struct sc_adb_device *dst, struct sc_adb_device *src); void -sc_adb_devices_destroy_all(struct sc_adb_device *devices, size_t count); +sc_adb_devices_destroy(struct sc_vec_adb_devices *devices); #endif diff --git a/app/src/adb/adb_parser.c b/app/src/adb/adb_parser.c index 85e8ffaf..933eafbb 100644 --- a/app/src/adb/adb_parser.c +++ b/app/src/adb/adb_parser.c @@ -109,11 +109,8 @@ sc_adb_parse_device(char *line, struct sc_adb_device *device) { return true; } -ssize_t -sc_adb_parse_devices(char *str, struct sc_adb_device *devices, - size_t devices_len) { - size_t dev_count = 0; - +bool +sc_adb_parse_devices(char *str, struct sc_vec_adb_devices *out_vec) { #define HEADER "List of devices attached" #define HEADER_LEN (sizeof(HEADER) - 1) bool header_found = false; @@ -144,25 +141,24 @@ sc_adb_parse_devices(char *str, struct sc_adb_device *devices, size_t line_len = sc_str_remove_trailing_cr(line, len); line[line_len] = '\0'; - bool ok = sc_adb_parse_device(line, &devices[dev_count]); + struct sc_adb_device device; + bool ok = sc_adb_parse_device(line, &device); if (!ok) { continue; } - ++dev_count; - - assert(dev_count <= devices_len); - if (dev_count == devices_len) { - // Max number of devices reached - break; + ok = sc_vector_push(out_vec, device); + if (!ok) { + LOG_OOM(); + LOGE("Could not push adb_device to vector"); + sc_adb_device_destroy(&device); + // continue anyway + continue; } } - if (!header_found) { - return -1; - } - - return dev_count; + assert(header_found || out_vec->size == 0); + return header_found; } static char * diff --git a/app/src/adb/adb_parser.h b/app/src/adb/adb_parser.h index 65493a2e..e0cc389b 100644 --- a/app/src/adb/adb_parser.h +++ b/app/src/adb/adb_parser.h @@ -14,9 +14,8 @@ * * Warning: this function modifies the buffer for optimization purposes. */ -ssize_t -sc_adb_parse_devices(char *str, struct sc_adb_device *devices, - size_t devices_len); +bool +sc_adb_parse_devices(char *str, struct sc_vec_adb_devices *out_vec); /** * Parse the ip from the output of `adb shell ip route` diff --git a/app/tests/test_adb_parser.c b/app/tests/test_adb_parser.c index c4b18a8d..1a127632 100644 --- a/app/tests/test_adb_parser.c +++ b/app/tests/test_adb_parser.c @@ -13,21 +13,22 @@ static void test_adb_devices() { "192.168.1.1:5555 device product:MyWifiProduct model:MyWifiModel " "device:MyWifiDevice trandport_id:2\n"; - struct sc_adb_device devices[16]; - ssize_t count = sc_adb_parse_devices(output, devices, ARRAY_LEN(devices)); - assert(count == 2); + struct sc_vec_adb_devices vec = SC_VECTOR_INITIALIZER; + bool ok = sc_adb_parse_devices(output, &vec); + assert(ok); + assert(vec.size == 2); - struct sc_adb_device *device = &devices[0]; + struct sc_adb_device *device = &vec.data[0]; assert(!strcmp("0123456789abcdef", device->serial)); assert(!strcmp("device", device->state)); assert(!strcmp("MyModel", device->model)); - device = &devices[1]; + device = &vec.data[1]; assert(!strcmp("192.168.1.1:5555", device->serial)); assert(!strcmp("device", device->state)); assert(!strcmp("MyWifiModel", device->model)); - sc_adb_devices_destroy_all(devices, count); + sc_adb_devices_destroy(&vec); } static void test_adb_devices_cr() { @@ -38,21 +39,22 @@ static void test_adb_devices_cr() { "192.168.1.1:5555 device product:MyWifiProduct model:MyWifiModel " "device:MyWifiDevice trandport_id:2\r\n"; - struct sc_adb_device devices[16]; - ssize_t count = sc_adb_parse_devices(output, devices, ARRAY_LEN(devices)); - assert(count == 2); + struct sc_vec_adb_devices vec = SC_VECTOR_INITIALIZER; + bool ok = sc_adb_parse_devices(output, &vec); + assert(ok); + assert(vec.size == 2); - struct sc_adb_device *device = &devices[0]; + struct sc_adb_device *device = &vec.data[0]; assert(!strcmp("0123456789abcdef", device->serial)); assert(!strcmp("device", device->state)); assert(!strcmp("MyModel", device->model)); - device = &devices[1]; + device = &vec.data[1]; assert(!strcmp("192.168.1.1:5555", device->serial)); assert(!strcmp("device", device->state)); assert(!strcmp("MyWifiModel", device->model)); - sc_adb_devices_destroy_all(devices, count); + sc_adb_devices_destroy(&vec); } static void test_adb_devices_daemon_start() { @@ -63,16 +65,17 @@ static void test_adb_devices_daemon_start() { "0123456789abcdef device usb:2-1 product:MyProduct model:MyModel " "device:MyDevice transport_id:1\n"; - struct sc_adb_device devices[16]; - ssize_t count = sc_adb_parse_devices(output, devices, ARRAY_LEN(devices)); - assert(count == 1); + struct sc_vec_adb_devices vec = SC_VECTOR_INITIALIZER; + bool ok = sc_adb_parse_devices(output, &vec); + assert(ok); + assert(vec.size == 1); - struct sc_adb_device *device = &devices[0]; + struct sc_adb_device *device = &vec.data[0]; assert(!strcmp("0123456789abcdef", device->serial)); assert(!strcmp("device", device->state)); assert(!strcmp("MyModel", device->model)); - sc_adb_device_destroy(device); + sc_adb_devices_destroy(&vec); } static void test_adb_devices_daemon_start_mixed() { @@ -84,21 +87,22 @@ static void test_adb_devices_daemon_start_mixed() { "87654321 device usb:2-1 product:MyProduct model:MyModel " "device:MyDevice\n"; - struct sc_adb_device devices[16]; - ssize_t count = sc_adb_parse_devices(output, devices, ARRAY_LEN(devices)); - assert(count == 2); + struct sc_vec_adb_devices vec = SC_VECTOR_INITIALIZER; + bool ok = sc_adb_parse_devices(output, &vec); + assert(ok); + assert(vec.size == 2); - struct sc_adb_device *device = &devices[0]; + struct sc_adb_device *device = &vec.data[0]; assert(!strcmp("0123456789abcdef", device->serial)); assert(!strcmp("unauthorized", device->state)); assert(!device->model); - device = &devices[1]; + device = &vec.data[1]; assert(!strcmp("87654321", device->serial)); assert(!strcmp("device", device->state)); assert(!strcmp("MyModel", device->model)); - sc_adb_devices_destroy_all(devices, count); + sc_adb_devices_destroy(&vec); } static void test_adb_devices_without_eol() { @@ -106,34 +110,39 @@ static void test_adb_devices_without_eol() { "List of devices attached\n" "0123456789abcdef device usb:2-1 product:MyProduct model:MyModel " "device:MyDevice transport_id:1"; - struct sc_adb_device devices[16]; - ssize_t count = sc_adb_parse_devices(output, devices, ARRAY_LEN(devices)); - assert(count == 1); - struct sc_adb_device *device = &devices[0]; + struct sc_vec_adb_devices vec = SC_VECTOR_INITIALIZER; + bool ok = sc_adb_parse_devices(output, &vec); + assert(ok); + assert(vec.size == 1); + + struct sc_adb_device *device = &vec.data[0]; assert(!strcmp("0123456789abcdef", device->serial)); assert(!strcmp("device", device->state)); assert(!strcmp("MyModel", device->model)); - sc_adb_device_destroy(device); + sc_adb_devices_destroy(&vec); } static void test_adb_devices_without_header() { char output[] = "0123456789abcdef device usb:2-1 product:MyProduct model:MyModel " "device:MyDevice transport_id:1\n"; - struct sc_adb_device devices[16]; - ssize_t count = sc_adb_parse_devices(output, devices, ARRAY_LEN(devices)); - assert(count == -1); + + struct sc_vec_adb_devices vec = SC_VECTOR_INITIALIZER; + bool ok = sc_adb_parse_devices(output, &vec); + assert(!ok); } static void test_adb_devices_corrupted() { char output[] = "List of devices attached\n" "corrupted_garbage\n"; - struct sc_adb_device devices[16]; - ssize_t count = sc_adb_parse_devices(output, devices, ARRAY_LEN(devices)); - assert(count == 0); + + struct sc_vec_adb_devices vec = SC_VECTOR_INITIALIZER; + bool ok = sc_adb_parse_devices(output, &vec); + assert(ok); + assert(vec.size == 0); } static void test_adb_devices_spaces() { @@ -141,16 +150,17 @@ static void test_adb_devices_spaces() { "List of devices attached\n" "0123456789abcdef unauthorized usb:1-4 transport_id:3\n"; - struct sc_adb_device devices[16]; - ssize_t count = sc_adb_parse_devices(output, devices, ARRAY_LEN(devices)); - assert(count == 1); + struct sc_vec_adb_devices vec = SC_VECTOR_INITIALIZER; + bool ok = sc_adb_parse_devices(output, &vec); + assert(ok); + assert(vec.size == 1); - struct sc_adb_device *device = &devices[0]; + struct sc_adb_device *device = &vec.data[0]; assert(!strcmp("0123456789abcdef", device->serial)); assert(!strcmp("unauthorized", device->state)); assert(!device->model); - sc_adb_device_destroy(device); + sc_adb_devices_destroy(&vec); } static void test_get_ip_single_line() { From e2e76c5d4858530db691a4d8c2bb7a97eaebdfb3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 18 Feb 2022 21:33:20 +0100 Subject: [PATCH 1151/2244] Increase `adb devices -l` max output size For simplicity, the parsing of `adb devices -l` output is performed in a single pass on the whole output. This output was limited to 4096 bytes. Since there are about 100 chars per device line, this limited the number of connected devices to ~40. Increase to 65536 bytes to avoid a limitation in practice. PR #3035 --- app/src/adb/adb.c | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/app/src/adb/adb.c b/app/src/adb/adb.c index c14ded92..06090b46 100644 --- a/app/src/adb/adb.c +++ b/app/src/adb/adb.c @@ -398,31 +398,39 @@ sc_adb_list_devices(struct sc_intr *intr, unsigned flags, struct sc_vec_adb_devices *out_vec) { const char *const argv[] = SC_ADB_COMMAND("devices", "-l"); +#define BUFSIZE 65536 + char *buf = malloc(BUFSIZE); + if (!buf) { + return false; + } + sc_pipe pout; sc_pid pid = sc_adb_execute_p(argv, flags, &pout); if (pid == SC_PROCESS_NONE) { LOGE("Could not execute \"adb devices -l\""); + free(buf); return false; } - char buf[4096]; - ssize_t r = sc_pipe_read_all_intr(intr, pid, pout, buf, sizeof(buf) - 1); + ssize_t r = sc_pipe_read_all_intr(intr, pid, pout, buf, BUFSIZE - 1); sc_pipe_close(pout); bool ok = process_check_success_intr(intr, pid, "adb devices -l", flags); if (!ok) { + free(buf); return false; } if (r == -1) { + free(buf); return false; } - assert((size_t) r < sizeof(buf)); - if (r == sizeof(buf) - 1) { + assert((size_t) r < BUFSIZE); + if (r == BUFSIZE - 1) { // The implementation assumes that the output of "adb devices -l" fits // in the buffer in a single pass - LOGW("Result of \"adb devices -l\" does not fit in 4Kb. " + LOGW("Result of \"adb devices -l\" does not fit in 64Kb. " "Please report an issue."); return false; } @@ -431,7 +439,9 @@ sc_adb_list_devices(struct sc_intr *intr, unsigned flags, buf[r] = '\0'; // List all devices to the output list directly - return sc_adb_parse_devices(buf, out_vec); + ok = sc_adb_parse_devices(buf, out_vec); + free(buf); + return ok; } static bool From 71b41d846fdcae475e3e735adea8491a2663bbc4 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 20 Feb 2022 23:37:14 +0100 Subject: [PATCH 1152/2244] Also retry on IllegalArgumentException MediaCodec.configure() may throw an IllegalArgumentException if it does not support the requested size. Also retry on this exception. Fixes #2993 Refs #2947 Refs #2990 PR #3043 --- server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java | 2 +- 1 file changed, 1 insertion(+), 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 f97206ec..e95896d3 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -102,7 +102,7 @@ public class ScreenEncoder implements Device.RotationListener { alive = encode(codec, fd); // do not call stop() on exception, it would trigger an IllegalStateException codec.stop(); - } catch (IllegalStateException e) { + } catch (IllegalStateException | IllegalArgumentException e) { Ln.e("Encoding error: " + e.getClass().getName() + ": " + e.getMessage()); if (!downsizeOnError || firstFrameSent) { // Fail immediately From 79ed83ab687a848efaddbfbfd195d4252158fe8d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 22 Feb 2022 18:10:30 +0100 Subject: [PATCH 1153/2244] Reorder --tcpip option in cli To keep options in alphabetic order. --- app/src/cli.c | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index 48289678..a4f79840 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -422,6 +422,20 @@ static const struct sc_option options[] = { "on exit.\n" "It only shows physical touches (not clicks from scrcpy).", }, + { + .longopt_id = OPT_TCPIP, + .longopt = "tcpip", + .argdesc = "ip[:port]", + .optional_arg = true, + .text = "Configure and reconnect the device over TCP/IP.\n" + "If a destination address is provided, then scrcpy connects to " + "this address before starting. The device must listen on the " + "given TCP port (default is 5555).\n" + "If no destination address is provided, then scrcpy attempts " + "to find the IP address of the current device (typically " + "connected over USB), enables TCP/IP mode, then connects to " + "this address before starting.", + }, { .longopt_id = OPT_TUNNEL_HOST, .longopt = "tunnel-host", @@ -483,20 +497,6 @@ static const struct sc_option options[] = { .text = "Keep the device on while scrcpy is running, when the device " "is plugged in.", }, - { - .longopt_id = OPT_TCPIP, - .longopt = "tcpip", - .argdesc = "ip[:port]", - .optional_arg = true, - .text = "Configure and reconnect the device over TCP/IP.\n" - "If a destination address is provided, then scrcpy connects to " - "this address before starting. The device must listen on the " - "given TCP port (default is 5555).\n" - "If no destination address is provided, then scrcpy attempts " - "to find the IP address of the current device (typically " - "connected over USB), enables TCP/IP mode, then connects to " - "this address before starting.", - }, { .longopt_id = OPT_WINDOW_BORDERLESS, .longopt = "window-borderless", From 3e0df6ad0586bc435fc1571e8d7fa3953c900f04 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 22 Feb 2022 18:23:42 +0100 Subject: [PATCH 1154/2244] Update HID/OTG features in README HID/OTG features are not limited to Linux anymore. Refs 82a99f69ec464a0637a16bdccfe5ff806777e942 --- README.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 1139092f..3bafd53d 100644 --- a/README.md +++ b/README.md @@ -32,10 +32,8 @@ Its features include: - [configurable quality](#capture-configuration) - device screen [as a webcam (V4L2)](#v4l2loopback) (Linux-only) - [physical keyboard simulation (HID)](#physical-keyboard-simulation-hid) - (Linux-only) - [physical mouse simulation (HID)](#physical-mouse-simulation-hid) - (Linux-only) - - [OTG mode](#otg) (Linux-only) + - [OTG mode](#otg) - and more… ## Requirements @@ -807,14 +805,17 @@ a location inverted through the center of the screen. By default, scrcpy uses Android key or text injection: it works everywhere, but is limited to ASCII. -On Linux, scrcpy can simulate a physical USB keyboard on Android to provide a -better input experience (using [USB HID over AOAv2][hid-aoav2]): the virtual +Alternatively, scrcpy can simulate a physical USB keyboard on Android to provide +a better input experience (using [USB HID over AOAv2][hid-aoav2]): the virtual keyboard is disabled and it works for all characters and IME. [hid-aoav2]: https://source.android.com/devices/accessories/aoa2#hid-support -However, it only works if the device is connected by USB, and is currently only -supported on Linux. +However, it only works if the device is connected by USB. + +Note: On Windows, it may only work in [OTG mode](#otg), not while mirroring (it +is not possible to open a USB device if it is already open by another process +like the adb daemon). To enable this mode: @@ -847,8 +848,7 @@ a physical keyboard is connected). #### Physical mouse simulation (HID) Similarly to the physical keyboard simulation, it is possible to simulate a -physical mouse. Likewise, it only works if the device is connected by USB, and -is currently only supported on Linux. +physical mouse. Likewise, it only works if the device is connected by USB. By default, scrcpy uses Android mouse events injection, using absolute coordinates. By simulating a physical mouse, a mouse pointer appears on the @@ -901,7 +901,7 @@ scrcpy --otg # keyboard and mouse ``` Like `--hid-keyboard` and `--hid-mouse`, it only works if the device is -connected by USB, and is currently only supported on Linux. +connected by USB. #### Text injection preference From 90816291d499ea0bc759f7d10e3d65d6d4313d81 Mon Sep 17 00:00:00 2001 From: Flying Press Date: Mon, 20 Apr 2020 04:59:54 -0500 Subject: [PATCH 1155/2244] Add an explicit first step in wireless section Mention that the device must be plugged via USB before configuring TCP/IP connections. It wasn't obvious that the device should be first plugged before running scrcpy wirelessly, especially to those who aren't very familiar with adb. Note from committer: add this new step with index 0 to make the diff readable, the next commit will renumber all the steps. PR #1303 Signed-off-by: Romain Vimont --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 3bafd53d..28d14dfd 100644 --- a/README.md +++ b/README.md @@ -404,6 +404,7 @@ connect to the device before starting. Alternatively, it is possible to enable the TCP/IP connection manually using `adb`: +0. Plug the device into a USB port on your computer. 1. Connect the device to the same Wi-Fi as your computer. 2. Get your device IP address, in Settings → About phone → Status, or by executing this command: From 78b18b7cee8b0f31ce979d3ddf180370132c4703 Mon Sep 17 00:00:00 2001 From: Flying Press Date: Mon, 20 Apr 2020 04:59:54 -0500 Subject: [PATCH 1156/2244] Renumber steps in wireless section The actual numbers are ignored by markdown, but start at 1 for consistency. PR #1303 Signed-off-by: Romain Vimont --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 28d14dfd..b3f78596 100644 --- a/README.md +++ b/README.md @@ -404,19 +404,19 @@ connect to the device before starting. Alternatively, it is possible to enable the TCP/IP connection manually using `adb`: -0. Plug the device into a USB port on your computer. -1. Connect the device to the same Wi-Fi as your computer. -2. Get your device IP address, in Settings → About phone → Status, or by +1. Plug the device into a USB port on your computer. +2. Connect the device to the same Wi-Fi as your computer. +3. Get your device IP address, in Settings → About phone → Status, or by executing this command: ```bash adb shell ip route | awk '{print $9}' ``` -3. Enable adb over TCP/IP on your device: `adb tcpip 5555`. -4. Unplug your device. -5. Connect to your device: `adb connect DEVICE_IP:5555` _(replace `DEVICE_IP`)_. -6. Run `scrcpy` as usual. +4. Enable adb over TCP/IP on your device: `adb tcpip 5555`. +5. Unplug your device. +6. Connect to your device: `adb connect DEVICE_IP:5555` _(replace `DEVICE_IP`)_. +7. Run `scrcpy` as usual. It may be useful to decrease the bit-rate and the definition: From 528275d5011189539af1fe7a493755002c5bbbf0 Mon Sep 17 00:00:00 2001 From: Flying Press Date: Mon, 20 Apr 2020 04:59:54 -0500 Subject: [PATCH 1157/2244] Improve phrasing in wireless section PR #1303 Signed-off-by: Romain Vimont --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b3f78596..740175e0 100644 --- a/README.md +++ b/README.md @@ -405,7 +405,7 @@ Alternatively, it is possible to enable the TCP/IP connection manually using `adb`: 1. Plug the device into a USB port on your computer. -2. Connect the device to the same Wi-Fi as your computer. +2. Connect the device to the same Wi-Fi network as your computer. 3. Get your device IP address, in Settings → About phone → Status, or by executing this command: From 8610e9a454048b710eb84b45ce197d8d8b8e5382 Mon Sep 17 00:00:00 2001 From: Flying Press Date: Mon, 20 Apr 2020 04:59:54 -0500 Subject: [PATCH 1158/2244] Add troubleshooting in wireless section Add a small troubleshooting section since wireless might add some complexity, and to lessen incoming relevant issue posts. PR #1303 Signed-off-by: Romain Vimont --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 740175e0..99cf8996 100644 --- a/README.md +++ b/README.md @@ -418,6 +418,11 @@ Alternatively, it is possible to enable the TCP/IP connection manually using 6. Connect to your device: `adb connect DEVICE_IP:5555` _(replace `DEVICE_IP`)_. 7. Run `scrcpy` as usual. +If the connection randomly drops, run your `scrcpy` command to reconnect. If it +says there are no devices/emulators found, try running `adb connect +DEVICE_IP:5555` again, and then `scrcpy` as usual. If it still says there are +none found, try running `adb disconnect` and then run those two commands again. + It may be useful to decrease the bit-rate and the definition: ```bash From 16937972770df9ae20f8e5ae5579df15d1b00e57 Mon Sep 17 00:00:00 2001 From: Flying Press Date: Mon, 20 Apr 2020 04:59:54 -0500 Subject: [PATCH 1159/2244] Make step more explicit in wireless section PR #1303 Signed-off-by: Romain Vimont --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 99cf8996..b78acadc 100644 --- a/README.md +++ b/README.md @@ -415,7 +415,8 @@ Alternatively, it is possible to enable the TCP/IP connection manually using 4. Enable adb over TCP/IP on your device: `adb tcpip 5555`. 5. Unplug your device. -6. Connect to your device: `adb connect DEVICE_IP:5555` _(replace `DEVICE_IP`)_. +6. Connect to your device: `adb connect DEVICE_IP:5555` _(replace `DEVICE_IP` +with the device IP address you found)_. 7. Run `scrcpy` as usual. If the connection randomly drops, run your `scrcpy` command to reconnect. If it From ff8a69d8eccac57b53dbebf001d1dce2245ab9b7 Mon Sep 17 00:00:00 2001 From: Flying Press Date: Mon, 20 Apr 2020 04:59:54 -0500 Subject: [PATCH 1160/2244] Mention adb wireless option for Android 11+ Add a paragraph about toggling an option to bypass having to physically connect the device to the user's computer. PR #1303 Signed-off-by: Romain Vimont --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index b78acadc..57e2f8c3 100644 --- a/README.md +++ b/README.md @@ -419,6 +419,11 @@ Alternatively, it is possible to enable the TCP/IP connection manually using with the device IP address you found)_. 7. Run `scrcpy` as usual. +Since Android 11, a [Wireless debugging option][adb-wireless] allows to bypass +having to physically connect your device directly to your computer. + +[adb-wireless]: https://developer.android.com/studio/command-line/adb#connect-to-a-device-over-wi-fi-android-11+ + If the connection randomly drops, run your `scrcpy` command to reconnect. If it says there are no devices/emulators found, try running `adb connect DEVICE_IP:5555` again, and then `scrcpy` as usual. If it still says there are From 6a9b2f2c36fe48edd451ca019523bff61914e499 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 22 Feb 2022 18:31:37 +0100 Subject: [PATCH 1161/2244] Remove spurious empty line --- server/src/main/java/com/genymobile/scrcpy/Server.java | 1 - 1 file changed, 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 d7e522c7..60f485d8 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -11,7 +11,6 @@ import java.util.Locale; public final class Server { - private Server() { // not instantiable } From 1f951f1a3a85377fbcecc6c10aba462517fdab9e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 22 Feb 2022 19:08:15 +0100 Subject: [PATCH 1162/2244] Update FAQ to match the latest version --- FAQ.md | 44 ++++++++++++++++++++++++++------------------ 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/FAQ.md b/FAQ.md index 5829136d..9f22b8ba 100644 --- a/FAQ.md +++ b/FAQ.md @@ -4,23 +4,16 @@ Here are the common reported problems and their status. +If you encounter any error, the first step is to upgrade to the latest version. + ## `adb` issues `scrcpy` execute `adb` commands to initialize the connection with the device. If `adb` fails, then scrcpy will not work. -In that case, it will print this error: - -> ERROR: "adb get-serialno" 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 @@ -32,11 +25,9 @@ in the release, so it should work out-of-the-box. ### Device unauthorized - -> error: device unauthorized. -> This adb server's $ADB_VENDOR_KEYS is not set -> Try 'adb kill-server' if that seems wrong. -> Otherwise check for a confirmation dialog on your device. +> ERROR: Device is unauthorized: +> ERROR: --> (usb) 0123456789abcdef unauthorized +> ERROR: A popup should open on the device to request authorization. When connecting, a popup should open on the device. You must authorize USB debugging. @@ -48,10 +39,16 @@ If it does not open, check [stackoverflow][device-unauthorized]. ### Device not detected -> error: no devices/emulators found +> ERROR: Could not find any ADB device Check that you correctly enabled [adb debugging][enable-adb]. +Your device must be detected by `adb`: + +``` +adb devices +``` + If your device is not detected, you may need some [drivers] (on Windows). There is a separate [USB driver for Google devices][google-usb-driver]. [enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling @@ -63,12 +60,23 @@ If your device is not detected, you may need some [drivers] (on Windows). There If several devices are connected, you will encounter this error: -> error: more than one device/emulator +ERROR: Multiple (2) ADB devices: +ERROR: --> (usb) 0123456789abcdef device Nexus_5 +ERROR: --> (tcpip) 192.168.1.5:5555 device GM1913 +ERROR: Select a device via -s (--serial), -d (--select-usb) or -e (--select-tcpip) -the identifier of the device you want to mirror must be provided: +In that case, you can either provide the identifier of the device you want to +mirror: ```bash -scrcpy -s 01234567890abcdef +scrcpy -s 0123456789abcdef +``` + +Or request the single USB (or TCP/IP) device: + +```bash +scrcpy -d # USB device +scrcpy -e # TCP/IP device ``` Note that if your device is connected over TCP/IP, you might get this message: From 2716385887291d205716be85a0f209610ab04d9c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 22 Feb 2022 19:12:02 +0100 Subject: [PATCH 1163/2244] Move "Device unauthorized" in FAQ Put "Device not detected" first. --- FAQ.md | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/FAQ.md b/FAQ.md index 9f22b8ba..4058e8fe 100644 --- a/FAQ.md +++ b/FAQ.md @@ -23,20 +23,6 @@ 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 - -> ERROR: Device is unauthorized: -> ERROR: --> (usb) 0123456789abcdef unauthorized -> ERROR: A popup should open on the device to request authorization. - -When connecting, a popup should open on the device. You must authorize USB -debugging. - -If it does not open, check [stackoverflow][device-unauthorized]. - -[device-unauthorized]: https://stackoverflow.com/questions/23081263/adb-android-device-unauthorized - - ### Device not detected > ERROR: Could not find any ADB device @@ -56,6 +42,20 @@ If your device is not detected, you may need some [drivers] (on Windows). There [google-usb-driver]: https://developer.android.com/studio/run/win-usb +### Device unauthorized + +> ERROR: Device is unauthorized: +> ERROR: --> (usb) 0123456789abcdef unauthorized +> ERROR: A popup should open on the device to request authorization. + +When connecting, a popup should open on the device. You must authorize USB +debugging. + +If it does not open, check [stackoverflow][device-unauthorized]. + +[device-unauthorized]: https://stackoverflow.com/questions/23081263/adb-android-device-unauthorized + + ### Several devices connected If several devices are connected, you will encounter this error: From 26953784d96c14f66e8b55ec2a1e7a13167ce547 Mon Sep 17 00:00:00 2001 From: hltdev8642 Date: Thu, 10 Feb 2022 09:37:02 -0500 Subject: [PATCH 1164/2244] Add ZSH completion script PR #3012 Signed-off-by: Romain Vimont --- app/data/zsh-completion/_scrcpy | 69 +++++++++++++++++++++++++++++++++ app/meson.build | 2 + 2 files changed, 71 insertions(+) create mode 100644 app/data/zsh-completion/_scrcpy diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy new file mode 100644 index 00000000..77e6027a --- /dev/null +++ b/app/data/zsh-completion/_scrcpy @@ -0,0 +1,69 @@ +#compdef -N scrcpy -N scrcpy.exe +# +# name: scrcpy +# auth: hltdev [hltdev8642@gmail.com] +# desc: completion file for scrcpy (all OSes) +# + +local arguments + +arguments=( + '--always-on-top[Make scrcpy window always on top \(above other windows\)]' + {-b,--bit-rate=}'[Encode the video at the given bit-rate]' + '--codec-options=[Set a list of comma-separated key\:type=value options for the device encoder]' + '--crop=[\[width\:height\:x\:y\] Crop the device screen on the server]' + {-d,--select-usb}'[Use USB device]' + '--disable-screensaver[Disable screensaver while scrcpy is running]' + '--display=[Specify the display id to mirror]' + '--display-buffer=[Add a buffering delay \(in milliseconds\) before displaying]' + {-e,--select-tcpip}'[Use TCP/IP device]' + '--encoder=[Use a specific MediaCodec encoder \(must be a H.264 encoder\)]' + '--force-adb-forward[Do not attempt to use \"adb reverse\" to connect to the device]' + '--forward-all-clicks[Forward clicks to device]' + {-f,--fullscreen}'[Start in fullscreen]' + {-K,--hid-keyboard}'[Simulate a physical keyboard by using HID over AOAv2]' + {-h,--help}'[Print the help]' + '--legacy-paste[Inject computer clipboard text as a sequence of key events on Ctrl+v]' + '--lock-video-orientation=[Lock video orientation]:orientation:(unlocked initial 0 1 2 3)' + '--max-fps=[Limit the frame rate of screen capture]' + {-M,--hid-mouse}'[Simulate a physical mouse by using HID over AOAv2]' + {-m,--max-size=}'[Limit both the width and height of the video to value]' + '--no-cleanup[Disable device cleanup actions on exit]' + '--no-clipboard-autosync[Disable automatic clipboard synchronization]' + '--no-downsize-on-error[Disable lowering definition on MediaCodec error]' + {-n,--no-control}'[Disable device control \(mirror the device in read only\)]' + {-N,--no-display}'[Do not display device \(during screen recording or when V4L2 sink is enabled\)]' + '--no-key-repeat[Do not forward repeated key events when a key is held down]' + '--no-mipmaps[Disable the generation of mipmaps]' + '--otg[Run in OTG mode \(simulating physical keyboard and mouse\)]' + {-p,--port=}'[\[port\[\:port\]\] Set the TCP port \(range\) used by the client to listen]' + '--power-off-on-close[Turn the device screen off when closing scrcpy]' + '--prefer-text[Inject alpha characters and space as text events instead of key events]' + '--print-fps[Start FPS counter, to print frame logs to the console]' + '--push-target=[Set the target directory for pushing files to the device by drag and drop]' + '--raw-key-events[Inject key events for all input keys, and ignore text events]' + {-r,--record=}'[Record screen to file]:record file:_files' + '--record-format=[Force recording format]:format:(mp4 mkv)' + '--render-driver=[Request SDL to use the given render driver]:driver name:(direct3d opengl opengles2 opengles metal software)' + '--rotation=[Set the initial display rotation]:rotation values:(0 1 2 3)' + {-s,--serial=}'[The device serial number \(mandatory for multiple devices only\)]' + '--shortcut-mod=[\[key1,key2+key3,...\] Specify the modifiers to use for scrcpy shortcuts]:shortcut mod:(lctrl rctrl lalt ralt lsuper rsuper)' + {-S,--turn-screen-off}'[Turn the device screen off immediately]' + {-t,--show-touches}'[Show physical touches]' + '--tcpip[\(optional \[ip\:port\]\) Configure and connect the device over TCP/IP]' + '--tunnel-host=[Set the IP address of the adb tunnel to reach the scrcpy server]' + '--tunnel-port=[Set the TCP port of the adb tunnel to reach the scrcpy server]' + '--v4l2-buffer=[Add a buffering delay \(in milliseconds\) before pushing frames]' + '--v4l2-sink=[\[\/dev\/videoN\] Output to v4l2loopback device]' + {-V,--verbosity=}'[Set the log level]:verbosity:(verbose debug info warn error)' + {-v,--version}'[Print the version of scrcpy]' + {-w,--stay-awake}'[Keep the device on while scrcpy is running, when the device is plugged in]' + '--window-borderless[Disable window decorations \(display borderless window\)]' + '--window-title=[Set a custom window title]' + '--window-x=[Set the initial window horizontal position]' + '--window-y=[Set the initial window vertical position]' + '--window-width=[Set the initial window width]' + '--window-height=[Set the initial window height]' +) + +_arguments -s $arguments diff --git a/app/meson.build b/app/meson.build index 6255bcbc..95a92da2 100644 --- a/app/meson.build +++ b/app/meson.build @@ -227,6 +227,8 @@ install_man('scrcpy.1') install_data('data/icon.png', rename: 'scrcpy.png', install_dir: 'share/icons/hicolor/256x256/apps') +install_data('data/zsh-completion/_scrcpy', + install_dir: 'share/zsh/site-functions') ### TESTS From 3ce6f8ca91bd28e980943f9346ce6efcbee145b8 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 21 Feb 2022 22:38:53 +0100 Subject: [PATCH 1165/2244] Add Bash completion script Fixes #2930 Refs #3012 --- app/data/bash-completion/scrcpy | 121 ++++++++++++++++++++++++++++++++ app/meson.build | 2 + 2 files changed, 123 insertions(+) create mode 100644 app/data/bash-completion/scrcpy diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy new file mode 100644 index 00000000..464bc532 --- /dev/null +++ b/app/data/bash-completion/scrcpy @@ -0,0 +1,121 @@ +_scrcpy() { + local cur prev words cword + local opts=" + --always-on-top + -b --bit-rate= + --codec-options= + --crop= + -d --select-usb + --disable-screensaver + --display= + --display-buffer= + -e --select-tcpip + --encoder= + --force-adb-forward + --forward-all-clicks + -f --fullscreen + -K --hid-keyboard + -h --help + --legacy-paste + --lock-video-orientation + --lock-video-orientation= + --max-fps= + -M --hid-mouse + -m --max-size= + --no-cleanup + --no-clipboard-on-error + --no-downsize-on-error + -n --no-control + -N --no-display + --no-key-repeat + --no-mipmaps + --otg + -p --port= + --power-off-on-close + --prefer-text + --print-fps + --push-target= + --raw-key-events + -r --record= + --record-format= + --render-driver= + --rotation= + -s --serial= + --shortcut-mod= + -S --turn-screen-off + -t --show-touches + --tcpip + --tcpip= + --tunnel-host= + --tunnel-port= + --v4l2-buffer= + --v4l2-sink= + -V --verbosity= + -v --version + -w --stay-awake + --window-borderless + --window-title= + --window-x= + --window-y= + --window-width= + --window-height=" + + _init_completion -s || return + + case "$prev" in + --lock-video-orientation) + COMPREPLY=($(compgen -W 'unlocked initial 0 1 2 3' -- "$cur")) + return + ;; + -r|--record) + COMPREPLY=($(compgen -f -- "$cur")) + return + ;; + --record-format) + COMPREPLY=($(compgen -W 'mkv mp4' -- "$cur")) + return + ;; + --render-driver) + COMPREPLY=($(compgen -W 'direct3d opengl opengles2 opengles metal software' -- "$cur")) + return + ;; + --rotation) + COMPREPLY=($(compgen -W '0 1 2 3' -- "$cur")) + return + ;; + --shortcut-mod) + # Only auto-complete a single key + COMPREPLY=($(compgen -W 'lctrl rctrl lalt ralt lsuper rsuper' -- "$cur")) + return + ;; + -V|--verbosity) + COMPREPLY=($(compgen -W 'verbose debug info warn error' -- "$cur")) + return + ;; + -b|--bitrate \ + |--codec-options \ + |--crop \ + |--display \ + |--display-buffer \ + |--encoder \ + |--max-fps \ + |-m|--max-size \ + |-p|--port \ + |--push-target \ + |-s|--serial \ + |--tunnel-host \ + |--tunnel-port \ + |--v4l2-buffer \ + |--v4l2-sink \ + |--tcpip \ + |--window-*) + # Option accepting an argument, but nothing to auto-complete + return + ;; + esac + + COMPREPLY=($(compgen -W "$opts" -- "$cur")) + [[ $COMPREPLY == *= ]] && compopt -o nospace +} + +complete -F _scrcpy scrcpy diff --git a/app/meson.build b/app/meson.build index 95a92da2..e34b1893 100644 --- a/app/meson.build +++ b/app/meson.build @@ -229,6 +229,8 @@ install_data('data/icon.png', install_dir: 'share/icons/hicolor/256x256/apps') install_data('data/zsh-completion/_scrcpy', install_dir: 'share/zsh/site-functions') +install_data('data/bash-completion/scrcpy', + install_dir: 'share/bash-completion/completions') ### TESTS From 4ab4548769cb3d27052f366f5c1755895b10c375 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 22 Feb 2022 19:31:35 +0100 Subject: [PATCH 1166/2244] Add contact links to the README Add Reddit and Twitter links (and an additional link to the GitHub issues). --- README.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 57e2f8c3..a3df2314 100644 --- a/README.md +++ b/README.md @@ -1105,7 +1105,9 @@ See [BUILD]. ## Common issues -See the [FAQ](FAQ.md). +See the [FAQ].md). + +[FAQ]: FAQ.md ## Developers @@ -1140,6 +1142,17 @@ Read the [developers page]. [article-intro]: https://blog.rom1v.com/2018/03/introducing-scrcpy/ [article-tcpip]: https://www.genymotion.com/blog/open-source-project-scrcpy-now-works-wirelessly/ +## Contact + +If you encounter a bug, please read the [FAQ] first, then open an [issue]. + +[issue]: https://github.com/Genymobile/scrcpy/issues + +For general questions or discussions, you could also use: + + - Reddit: [`r/scrcpy`](https://www.reddit.com/r/scrcpy) + - Twitter: [`@scrcpy_app`](https://twitter.com/scrcpy_app) + ## Translations This README is available in other languages: From 71ef5cc0a9936f3a1f0f6e1809135953ca74bc98 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 22 Feb 2022 21:00:43 +0100 Subject: [PATCH 1167/2244] Add missing include for vector Include stdlib.h for realloc(). --- app/src/util/vector.h | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/util/vector.h b/app/src/util/vector.h index a08fa9d6..2c03a430 100644 --- a/app/src/util/vector.h +++ b/app/src/util/vector.h @@ -5,6 +5,7 @@ #include #include +#include // Adapted from vlc_vector: // From 7deccef1c2f609870fae00a3a1eb11d0eb2a28a5 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 22 Feb 2022 21:01:55 +0100 Subject: [PATCH 1168/2244] Bump version to 1.23 --- app/scrcpy-windows.rc | 2 +- meson.build | 2 +- server/build.gradle | 4 ++-- server/build_without_gradle.sh | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/scrcpy-windows.rc b/app/scrcpy-windows.rc index dbd6aedb..41101e70 100644 --- a/app/scrcpy-windows.rc +++ b/app/scrcpy-windows.rc @@ -13,7 +13,7 @@ BEGIN VALUE "LegalCopyright", "Romain Vimont, Genymobile" VALUE "OriginalFilename", "scrcpy.exe" VALUE "ProductName", "scrcpy" - VALUE "ProductVersion", "1.22" + VALUE "ProductVersion", "1.23" END END BLOCK "VarFileInfo" diff --git a/meson.build b/meson.build index b4b59353..e7270b5a 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('scrcpy', 'c', - version: '1.22', + version: '1.23', meson_version: '>= 0.48', default_options: [ 'c_std=c11', diff --git a/server/build.gradle b/server/build.gradle index f262b02d..f3eb6d64 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -6,8 +6,8 @@ android { applicationId "com.genymobile.scrcpy" minSdkVersion 21 targetSdkVersion 31 - versionCode 12200 - versionName "1.22" + versionCode 12300 + versionName "1.23" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index ecf7b604..b2bafa3f 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.22 +SCRCPY_VERSION_NAME=1.23 PLATFORM=${ANDROID_PLATFORM:-31} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-31.0.0} From 49434da36ea9bd52907609c08c0b644c2d0d6649 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 22 Feb 2022 23:45:43 +0100 Subject: [PATCH 1169/2244] Update links to v1.23 --- BUILD.md | 6 +++--- README.md | 8 ++++---- install_release.sh | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/BUILD.md b/BUILD.md index 60be752d..90a7d18e 100644 --- a/BUILD.md +++ b/BUILD.md @@ -272,10 +272,10 @@ install` must be run as root)._ #### Option 2: Use prebuilt server - - [`scrcpy-server-v1.22`][direct-scrcpy-server] - _(SHA-256: c05d273eec7533c0e106282e0254cf04e7f5e8f0c2920ca39448865fab2a419b)_ + - [`scrcpy-server-v1.23`][direct-scrcpy-server] + _(SHA-256: 2a913fd47478c0b306fca507cb0beb625e49a19ff9fc7ab904e36ef5b9fe7e68)_ -[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.22/scrcpy-server-v1.22 +[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.23/scrcpy-server-v1.23 Download the prebuilt server somewhere, and specify its path during the Meson configuration: diff --git a/README.md b/README.md index 4bf075d8..23a28d1e 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# scrcpy (v1.22) +# scrcpy (v1.23) scrcpy @@ -106,10 +106,10 @@ process][BUILD_simple]). For Windows, for simplicity, a prebuilt archive with all the dependencies (including `adb`) is available: - - [`scrcpy-win64-v1.22.zip`][direct-win64] - _(SHA-256: ce4d9b8cc761e29862c4a72d8ad6f538bdd1f1831d15fd1f36633cd3b403db82)_ + - [`scrcpy-win64-v1.23.zip`][direct-win64] + _(SHA-256: d2f601b1d0157faf65153d8a093d827fd65aec5d5842d677ac86fb2b5b7704cc)_ -[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.22/scrcpy-win64-v1.22.zip +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.23/scrcpy-win64-v1.23.zip It is also available in [Chocolatey]: diff --git a/install_release.sh b/install_release.sh index 6dd71d25..9b1ed8e7 100755 --- a/install_release.sh +++ b/install_release.sh @@ -2,8 +2,8 @@ set -e BUILDDIR=build-auto -PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v1.22/scrcpy-server-v1.22 -PREBUILT_SERVER_SHA256=c05d273eec7533c0e106282e0254cf04e7f5e8f0c2920ca39448865fab2a419b +PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v1.23/scrcpy-server-v1.23 +PREBUILT_SERVER_SHA256=2a913fd47478c0b306fca507cb0beb625e49a19ff9fc7ab904e36ef5b9fe7e68 echo "[scrcpy] Downloading prebuilt server..." wget "$PREBUILT_SERVER_URL" -O scrcpy-server From adbe7908c6e05899d8c4a03cd5cbc10209e3d221 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 23 Feb 2022 01:21:18 +0100 Subject: [PATCH 1170/2244] Fix icon path in README The data/ directory was moved to app/data/. Refs 36c75e15b8e9eeb01bd287a42fa3f1513a728ebb --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 23a28d1e..c89b0c8d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # scrcpy (v1.23) -scrcpy +scrcpy _pronounced "**scr**een **c**o**py**"_ From e4bb2b8728168aebb942bfdeabbba30a29f6b405 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 24 Feb 2022 23:25:02 +0100 Subject: [PATCH 1171/2244] Add libusb error log Log libusb_get_string_descriptor_ascii() errors. Refs #3050 --- app/src/usb/usb.c | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/usb/usb.c b/app/src/usb/usb.c index 32a66f98..97aa9a33 100644 --- a/app/src/usb/usb.c +++ b/app/src/usb/usb.c @@ -15,6 +15,7 @@ read_string(libusb_device_handle *handle, uint8_t desc_index) { (unsigned char *) buffer, sizeof(buffer)); if (result < 0) { + LOGD("Read string: libusb error: %s", libusb_strerror(result)); return NULL; } From 59656fe649d8fd46062fd130e41921b53d517bca Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 24 Feb 2022 23:26:12 +0100 Subject: [PATCH 1172/2244] Fix typo in error message --- app/src/usb/hid_keyboard.c | 4 ++-- app/src/usb/hid_mouse.c | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/usb/hid_keyboard.c b/app/src/usb/hid_keyboard.c index 4a0e64ba..dc88944c 100644 --- a/app/src/usb/hid_keyboard.c +++ b/app/src/usb/hid_keyboard.c @@ -340,7 +340,7 @@ push_mod_lock_state(struct sc_hid_keyboard *kb, uint16_t mods_state) { if (!sc_aoa_push_hid_event(kb->aoa, &hid_event)) { sc_hid_event_destroy(&hid_event); - LOGW("Could request HID event"); + LOGW("Could not request HID event"); return false; } @@ -382,7 +382,7 @@ sc_key_processor_process_key(struct sc_key_processor *kp, if (!sc_aoa_push_hid_event(kb->aoa, &hid_event)) { sc_hid_event_destroy(&hid_event); - LOGW("Could request HID event"); + LOGW("Could not request HID event"); } } } diff --git a/app/src/usb/hid_mouse.c b/app/src/usb/hid_mouse.c index c24b4a4a..dadab586 100644 --- a/app/src/usb/hid_mouse.c +++ b/app/src/usb/hid_mouse.c @@ -181,7 +181,7 @@ sc_mouse_processor_process_mouse_motion(struct sc_mouse_processor *mp, if (!sc_aoa_push_hid_event(mouse->aoa, &hid_event)) { sc_hid_event_destroy(&hid_event); - LOGW("Could request HID event"); + LOGW("Could not request HID event"); } } @@ -203,7 +203,7 @@ sc_mouse_processor_process_mouse_click(struct sc_mouse_processor *mp, if (!sc_aoa_push_hid_event(mouse->aoa, &hid_event)) { sc_hid_event_destroy(&hid_event); - LOGW("Could request HID event"); + LOGW("Could not request HID event"); } } @@ -228,7 +228,7 @@ sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp, if (!sc_aoa_push_hid_event(mouse->aoa, &hid_event)) { sc_hid_event_destroy(&hid_event); - LOGW("Could request HID event"); + LOGW("Could not request HID event"); } } From 8d91cda4f61b710caee719609ed9a8c043cb41c8 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 24 Feb 2022 23:27:39 +0100 Subject: [PATCH 1173/2244] Improve HID event push error message On HID event push failure, add the event type in the error message. --- app/src/usb/hid_keyboard.c | 4 ++-- app/src/usb/hid_mouse.c | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/usb/hid_keyboard.c b/app/src/usb/hid_keyboard.c index dc88944c..a12fbf3b 100644 --- a/app/src/usb/hid_keyboard.c +++ b/app/src/usb/hid_keyboard.c @@ -340,7 +340,7 @@ push_mod_lock_state(struct sc_hid_keyboard *kb, uint16_t mods_state) { if (!sc_aoa_push_hid_event(kb->aoa, &hid_event)) { sc_hid_event_destroy(&hid_event); - LOGW("Could not request HID event"); + LOGW("Could not request HID event (mod lock state)"); return false; } @@ -382,7 +382,7 @@ sc_key_processor_process_key(struct sc_key_processor *kp, if (!sc_aoa_push_hid_event(kb->aoa, &hid_event)) { sc_hid_event_destroy(&hid_event); - LOGW("Could not request HID event"); + LOGW("Could not request HID event (key)"); } } } diff --git a/app/src/usb/hid_mouse.c b/app/src/usb/hid_mouse.c index dadab586..bab89940 100644 --- a/app/src/usb/hid_mouse.c +++ b/app/src/usb/hid_mouse.c @@ -181,7 +181,7 @@ sc_mouse_processor_process_mouse_motion(struct sc_mouse_processor *mp, if (!sc_aoa_push_hid_event(mouse->aoa, &hid_event)) { sc_hid_event_destroy(&hid_event); - LOGW("Could not request HID event"); + LOGW("Could not request HID event (mouse motion)"); } } @@ -203,7 +203,7 @@ sc_mouse_processor_process_mouse_click(struct sc_mouse_processor *mp, if (!sc_aoa_push_hid_event(mouse->aoa, &hid_event)) { sc_hid_event_destroy(&hid_event); - LOGW("Could not request HID event"); + LOGW("Could not request HID event (mouse click)"); } } @@ -228,7 +228,7 @@ sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp, if (!sc_aoa_push_hid_event(mouse->aoa, &hid_event)) { sc_hid_event_destroy(&hid_event); - LOGW("Could not request HID event"); + LOGW("Could not request HID event (mouse scroll)"); } } From 1f4c801f3c4b2b46d001f705abb921c3f3c8a212 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 6 Mar 2022 22:02:46 +0100 Subject: [PATCH 1174/2244] Report server connection state We must distinguish 3 cases for await_for_server(): - an error occurred - no error occurred, the device is connected - no error occurred, the device is not connected (user requested to quit) For this purpose, use an additional output parameter to indicate if the device is connected (only set when no error occurs). Refs #3085 --- app/src/scrcpy.c | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 3ed1d249..8c549a4e 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -168,19 +168,22 @@ event_loop(struct scrcpy *s) { return false; } +// Return true on success, false on error static bool -await_for_server(void) { +await_for_server(bool *connected) { SDL_Event event; while (SDL_WaitEvent(&event)) { switch (event.type) { case SDL_QUIT: LOGD("User requested to quit"); - return false; + *connected = false; + return true; case EVENT_SERVER_CONNECTION_FAILED: LOGE("Server connection failed"); return false; case EVENT_SERVER_CONNECTED: LOGD("Server connected"); + *connected = true; return true; default: break; @@ -351,7 +354,14 @@ scrcpy(struct scrcpy_options *options) { sdl_configure(options->display, options->disable_screensaver); // Await for server without blocking Ctrl+C handling - if (!await_for_server()) { + bool connected; + if (!await_for_server(&connected)) { + goto end; + } + + if (!connected) { + // This is not an error, user requested to quit + ret = true; goto end; } From b3f5dfe1de03ff8e19225aee08dcbd62d6089d56 Mon Sep 17 00:00:00 2001 From: "martin f. krafft" Date: Sat, 5 Mar 2022 15:47:58 +0100 Subject: [PATCH 1175/2244] Add specific exit code for device disconnection Modify the return logic such that exit code 1 is used when the initial connection fails, but if a session is established, and then the device disconnects, exit code 2 is emitted. Fixes #3083 PR #3085 Signed-off-by: martin f. krafft Signed-off-by: Romain Vimont --- app/scrcpy.1 | 6 ++++++ app/src/main.c | 16 ++++++++-------- app/src/scrcpy.c | 18 +++++++++--------- app/src/scrcpy.h | 13 ++++++++++++- app/src/usb/scrcpy_otg.c | 14 +++++++------- app/src/usb/scrcpy_otg.h | 4 ++-- 6 files changed, 44 insertions(+), 27 deletions(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index f9d4ba24..f1ee732d 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -355,6 +355,12 @@ Set the initial window height. Default is 0 (automatic). +.SH EXIT STATUS +.B scrcpy +will exit with code 0 on normal program termination. If an initial +connection cannot be established, the exit code 1 will be returned. If the +device disconnects while a session is active, exit code 2 will be returned. + .SH SHORTCUTS In the following list, MOD is the shortcut modifier. By default, it's (left) diff --git a/app/src/main.c b/app/src/main.c index 2d1575f8..3334cbf9 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -40,19 +40,19 @@ main(int argc, char *argv[]) { #endif if (!scrcpy_parse_args(&args, argc, argv)) { - return 1; + return SCRCPY_EXIT_FAILURE; } sc_set_log_level(args.opts.log_level); if (args.help) { scrcpy_print_usage(argv[0]); - return 0; + return SCRCPY_EXIT_SUCCESS; } if (args.version) { scrcpy_print_version(); - return 0; + return SCRCPY_EXIT_SUCCESS; } #ifdef SCRCPY_LAVF_REQUIRES_REGISTER_ALL @@ -66,17 +66,17 @@ main(int argc, char *argv[]) { #endif if (avformat_network_init()) { - return 1; + return SCRCPY_EXIT_FAILURE; } #ifdef HAVE_USB - bool ok = args.opts.otg ? scrcpy_otg(&args.opts) - : scrcpy(&args.opts); + enum scrcpy_exit_code ret = args.opts.otg ? scrcpy_otg(&args.opts) + : scrcpy(&args.opts); #else - bool ok = scrcpy(&args.opts); + enum scrcpy_exit_code ret = scrcpy(&args.opts); #endif avformat_network_deinit(); // ignore failure - return ok ? 0 : 1; + return ret; } diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 8c549a4e..8fbfe394 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -149,23 +149,23 @@ sdl_configure(bool display, bool disable_screensaver) { } } -static bool +static enum scrcpy_exit_code event_loop(struct scrcpy *s) { SDL_Event event; while (SDL_WaitEvent(&event)) { switch (event.type) { case EVENT_STREAM_STOPPED: LOGW("Device disconnected"); - return false; + return SCRCPY_EXIT_DISCONNECTED; case SDL_QUIT: LOGD("User requested to quit"); - return true; + return SCRCPY_EXIT_SUCCESS; default: sc_screen_handle_event(&s->screen, &event); break; } } - return false; + return SCRCPY_EXIT_FAILURE; } // Return true on success, false on error @@ -265,7 +265,7 @@ sc_server_on_disconnected(struct sc_server *server, void *userdata) { // event } -bool +enum scrcpy_exit_code scrcpy(struct scrcpy_options *options) { static struct scrcpy scrcpy; struct scrcpy *s = &scrcpy; @@ -273,12 +273,12 @@ scrcpy(struct scrcpy_options *options) { // Minimal SDL initialization if (SDL_Init(SDL_INIT_EVENTS)) { LOGE("Could not initialize SDL: %s", SDL_GetError()); - return false; + return SCRCPY_EXIT_FAILURE; } atexit(SDL_Quit); - bool ret = false; + enum scrcpy_exit_code ret = SCRCPY_EXIT_FAILURE; bool server_started = false; bool file_pusher_initialized = false; @@ -332,7 +332,7 @@ scrcpy(struct scrcpy_options *options) { .on_disconnected = sc_server_on_disconnected, }; if (!sc_server_init(&s->server, ¶ms, &cbs, NULL)) { - return false; + return SCRCPY_EXIT_FAILURE; } if (!sc_server_start(&s->server)) { @@ -361,7 +361,7 @@ scrcpy(struct scrcpy_options *options) { if (!connected) { // This is not an error, user requested to quit - ret = true; + ret = SCRCPY_EXIT_SUCCESS; goto end; } diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index cdcecda7..d4d494a3 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -6,7 +6,18 @@ #include #include "options.h" -bool +enum scrcpy_exit_code { + // Normal program termination + SCRCPY_EXIT_SUCCESS, + + // No connection could be established + SCRCPY_EXIT_FAILURE, + + // Device was disconnected while running + SCRCPY_EXIT_DISCONNECTED, +}; + +enum scrcpy_exit_code scrcpy(struct scrcpy_options *options); #endif diff --git a/app/src/usb/scrcpy_otg.c b/app/src/usb/scrcpy_otg.c index 1c53410e..db5e64d8 100644 --- a/app/src/usb/scrcpy_otg.c +++ b/app/src/usb/scrcpy_otg.c @@ -29,26 +29,26 @@ sc_usb_on_disconnected(struct sc_usb *usb, void *userdata) { } } -static bool +static enum scrcpy_exit_code event_loop(struct scrcpy_otg *s) { SDL_Event event; while (SDL_WaitEvent(&event)) { switch (event.type) { case EVENT_USB_DEVICE_DISCONNECTED: LOGW("Device disconnected"); - return false; + return SCRCPY_EXIT_DISCONNECTED; case SDL_QUIT: LOGD("User requested to quit"); - return true; + return SCRCPY_EXIT_SUCCESS; default: sc_screen_otg_handle_event(&s->screen_otg, &event); break; } } - return false; + return SCRCPY_EXIT_FAILURE; } -bool +enum scrcpy_exit_code scrcpy_otg(struct scrcpy_options *options) { static struct scrcpy_otg scrcpy_otg; struct scrcpy_otg *s = &scrcpy_otg; @@ -67,7 +67,7 @@ scrcpy_otg(struct scrcpy_options *options) { LOGW("Could not enable mouse focus clickthrough"); } - bool ret = false; + enum scrcpy_exit_code ret = SCRCPY_EXIT_FAILURE; struct sc_hid_keyboard *keyboard = NULL; struct sc_hid_mouse *mouse = NULL; @@ -90,7 +90,7 @@ scrcpy_otg(struct scrcpy_options *options) { }; bool ok = sc_usb_init(&s->usb); if (!ok) { - return false; + return SCRCPY_EXIT_FAILURE; } struct sc_usb_device usb_device; diff --git a/app/src/usb/scrcpy_otg.h b/app/src/usb/scrcpy_otg.h index 24b9cde8..e477660b 100644 --- a/app/src/usb/scrcpy_otg.h +++ b/app/src/usb/scrcpy_otg.h @@ -3,10 +3,10 @@ #include "common.h" -#include #include "options.h" +#include "scrcpy.h" -bool +enum scrcpy_exit_code scrcpy_otg(struct scrcpy_options *options); #endif From b1dbc30072bdf4171f7d73532607ca329003094c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 10 Mar 2022 09:11:50 +0100 Subject: [PATCH 1176/2244] Document exit status in --help Refs #3085 --- app/src/cli.c | 46 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/app/src/cli.c b/app/src/cli.c index a4f79840..bde5eb00 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -80,6 +80,11 @@ struct sc_envvar { const char *text; }; +struct sc_exit_status { + unsigned value; + const char *text; +}; + struct sc_getopt_adapter { char *optstring; struct option *longopts; @@ -662,7 +667,22 @@ static const struct sc_envvar envvars[] = { { .name = "SCRCPY_SERVER_PATH", .text = "Path to the server binary", - } + }, +}; + +static const struct sc_exit_status exit_statuses[] = { + { + .value = 0, + .text = "Normal program termination", + }, + { + .value = 1, + .text = "Start failure", + }, + { + .value = 2, + .text = "Device disconnected while running", + }, }; static char * @@ -901,6 +921,25 @@ print_envvar(const struct sc_envvar *envvar, unsigned cols) { free(text); } +static void +print_exit_status(const struct sc_exit_status *status, unsigned cols) { + assert(cols > 8); // sc_str_wrap_lines() requires indent < columns + assert(status->text); + + // The text starts at 9: 4 ident spaces, 3 chars for numeric value, 2 spaces + char *text = sc_str_wrap_lines(status->text, cols, 9); + if (!text) { + printf("\n"); + return; + } + + assert(strlen(text) >= 9); // Contains at least the initial identation + + // text + 9 to remove the initial indentation + printf(" %3d %s\n", status->value, text + 9); + free(text); +} + void scrcpy_print_usage(const char *arg0) { #define SC_TERM_COLS_DEFAULT 80 @@ -939,6 +978,11 @@ scrcpy_print_usage(const char *arg0) { for (size_t i = 0; i < ARRAY_LEN(envvars); ++i) { print_envvar(&envvars[i], cols); } + + printf("\nExit status:\n\n"); + for (size_t i = 0; i < ARRAY_LEN(exit_statuses); ++i) { + print_exit_status(&exit_statuses[i], cols); + } } static bool From 4ce7af42c635a6e9edd7c67902718d3a2c7f45fd Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 14 Mar 2022 12:29:33 +0100 Subject: [PATCH 1177/2244] Use $ANDROID_SERIAL if no selector is specified Like adb, read the ANDROID_SERIAL environment variable to select a device by serial if no explicit selection (-s, -d, -e or --tcpip=) is provided via the command line. Fixes #3111 PR #3113 --- README.md | 3 +++ app/scrcpy.1 | 4 ++++ app/src/cli.c | 5 +++++ app/src/server.c | 10 +++++++++- 4 files changed, 21 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c89b0c8d..614d1ed4 100644 --- a/README.md +++ b/README.md @@ -448,6 +448,9 @@ scrcpy --serial 0123456789abcdef scrcpy -s 0123456789abcdef # short version ``` +The serial may also be provided via the environment variable `ANDROID_SERIAL` +(also used by `adb`). + If the device is connected over TCP/IP: ```bash diff --git a/app/scrcpy.1 b/app/scrcpy.1 index f1ee732d..eb164475 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -477,6 +477,10 @@ Push file to device (see \fB\-\-push\-target\fR) .B ADB Path to adb. +.TP +.B ANDROID_SERIAL +Device serial to use if no selector (-s, -d, -e or --tcpip=) is specified. + .TP .B SCRCPY_ICON_PATH Path to the program icon. diff --git a/app/src/cli.c b/app/src/cli.c index bde5eb00..5dda86e5 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -660,6 +660,11 @@ static const struct sc_envvar envvars[] = { .name = "ADB", .text = "Path to adb executable", }, + { + .name = "ANDROID_SERIAL", + .text = "Device serial to use if no selector (-s, -d, -e or " + "--tcpip=) is specified", + }, { .name = "SCRCPY_ICON_PATH", .text = "Path to the program icon", diff --git a/app/src/server.c b/app/src/server.c index c12b03d4..6b61924b 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -707,7 +707,15 @@ run_server(void *data) { } else if (params->select_tcpip) { selector.type = SC_ADB_DEVICE_SELECT_TCPIP; } else { - selector.type = SC_ADB_DEVICE_SELECT_ALL; + // No explicit selection, check $ANDROID_SERIAL + const char *env_serial = getenv("ANDROID_SERIAL"); + if (env_serial) { + LOGI("Using ANDROID_SERIAL: %s", env_serial); + selector.type = SC_ADB_DEVICE_SELECT_SERIAL; + selector.serial = env_serial; + } else { + selector.type = SC_ADB_DEVICE_SELECT_ALL; + } } struct sc_adb_device device; ok = sc_adb_select_device(&server->intr, &selector, 0, &device); From e56f2ac7a9a3c6bbe8ea3d311e2054f5c1fa1c0d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 20 Mar 2022 14:56:52 +0100 Subject: [PATCH 1178/2244] Log an error on unexpected device state Refs #3129 --- app/src/adb/adb.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/adb/adb.c b/app/src/adb/adb.c index 06090b46..7e1c7bc2 100644 --- a/app/src/adb/adb.c +++ b/app/src/adb/adb.c @@ -531,6 +531,8 @@ sc_adb_device_check_state(struct sc_adb_device *device, LOGE("A popup should open on the device to request authorization."); LOGE("Check the FAQ: " ""); + } else { + LOGE("Device could not be connected (state=%s)", state); } return false; From c3d45c8397b7886d9071ccd69f04e2c9245fb8e7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 22 Mar 2022 21:07:31 +0100 Subject: [PATCH 1179/2244] Consider emulators as TCP/IP devices Emulators were wrongly considered as USB devices, so they were selected using -d instead of -e (which was inconsistent with the adb behavior). Refs #3005 Refs #3137 --- app/src/adb/adb.c | 18 +++++++++--------- app/src/adb/adb.h | 9 --------- app/src/adb/adb_device.c | 15 +++++++++++++++ app/src/adb/adb_device.h | 12 ++++++++++++ app/src/server.c | 3 ++- 5 files changed, 38 insertions(+), 19 deletions(-) diff --git a/app/src/adb/adb.c b/app/src/adb/adb.c index 7e1c7bc2..344f7fcc 100644 --- a/app/src/adb/adb.c +++ b/app/src/adb/adb.c @@ -473,9 +473,12 @@ sc_adb_accept_device(const struct sc_adb_device *device, } return !strcmp(selector->serial, device->serial); case SC_ADB_DEVICE_SELECT_USB: - return !sc_adb_is_serial_tcpip(device->serial); + return sc_adb_device_get_type(device->serial) == + SC_ADB_DEVICE_TYPE_USB; case SC_ADB_DEVICE_SELECT_TCPIP: - return sc_adb_is_serial_tcpip(device->serial); + // Both emulators and TCP/IP devices are selected via -e + return sc_adb_device_get_type(device->serial) != + SC_ADB_DEVICE_TYPE_USB; default: assert(!"Missing SC_ADB_DEVICE_SELECT_* handling"); break; @@ -509,8 +512,10 @@ sc_adb_devices_log(enum sc_log_level level, struct sc_adb_device *devices, for (size_t i = 0; i < count; ++i) { struct sc_adb_device *d = &devices[i]; const char *selection = d->selected ? "-->" : " "; - const char *type = sc_adb_is_serial_tcpip(d->serial) ? "(tcpip)" - : " (usb)"; + bool is_usb = + sc_adb_device_get_type(d->serial) == SC_ADB_DEVICE_TYPE_USB; + const char *type = is_usb ? " (usb)" + : "(tcpip)"; LOG(level, " %s %s %-20s %16s %s", selection, type, d->serial, d->state, d->model ? d->model : ""); } @@ -707,8 +712,3 @@ sc_adb_get_device_ip(struct sc_intr *intr, const char *serial, unsigned flags) { return sc_adb_parse_device_ip_from_output(buf); } - -bool -sc_adb_is_serial_tcpip(const char *serial) { - return strchr(serial, ':'); -} diff --git a/app/src/adb/adb.h b/app/src/adb/adb.h index 10e8f293..ffd532ea 100644 --- a/app/src/adb/adb.h +++ b/app/src/adb/adb.h @@ -114,13 +114,4 @@ sc_adb_getprop(struct sc_intr *intr, const char *serial, const char *prop, char * sc_adb_get_device_ip(struct sc_intr *intr, const char *serial, unsigned flags); -/** - * Indicate if the serial represents an IP address - * - * In practice, it just returns true if and only if it contains a ':', which is - * sufficient to distinguish an ip:port from a real USB serial. - */ -bool -sc_adb_is_serial_tcpip(const char *serial); - #endif diff --git a/app/src/adb/adb_device.c b/app/src/adb/adb_device.c index cfd5dc30..4f500243 100644 --- a/app/src/adb/adb_device.c +++ b/app/src/adb/adb_device.c @@ -25,3 +25,18 @@ sc_adb_devices_destroy(struct sc_vec_adb_devices *devices) { sc_vector_destroy(devices); } +enum sc_adb_device_type +sc_adb_device_get_type(const char *serial) { + // Starts with "emulator-" + if (!strncmp(serial, "emulator-", sizeof("emulator-") - 1)) { + return SC_ADB_DEVICE_TYPE_EMULATOR; + } + + // If the serial contains a ':', then it is a TCP/IP device (it is + // sufficient to distinguish an ip:port from a real USB serial) + if (strchr(serial, ':')) { + return SC_ADB_DEVICE_TYPE_TCPIP; + } + + return SC_ADB_DEVICE_TYPE_USB; +} diff --git a/app/src/adb/adb_device.h b/app/src/adb/adb_device.h index 14d0408c..56393bcf 100644 --- a/app/src/adb/adb_device.h +++ b/app/src/adb/adb_device.h @@ -15,6 +15,12 @@ struct sc_adb_device { bool selected; }; +enum sc_adb_device_type { + SC_ADB_DEVICE_TYPE_USB, + SC_ADB_DEVICE_TYPE_TCPIP, + SC_ADB_DEVICE_TYPE_EMULATOR, +}; + struct sc_vec_adb_devices SC_VECTOR(struct sc_adb_device); void @@ -35,4 +41,10 @@ sc_adb_device_move(struct sc_adb_device *dst, struct sc_adb_device *src); void sc_adb_devices_destroy(struct sc_vec_adb_devices *devices); +/** + * Deduce the device type from the serial + */ +enum sc_adb_device_type +sc_adb_device_get_type(const char *serial); + #endif diff --git a/app/src/server.c b/app/src/server.c index 6b61924b..c02390a4 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -649,7 +649,8 @@ sc_server_configure_tcpip_known_address(struct sc_server *server, static bool sc_server_configure_tcpip_unknown_address(struct sc_server *server, const char *serial) { - bool is_already_tcpip = sc_adb_is_serial_tcpip(serial); + bool is_already_tcpip = + sc_adb_device_get_type(serial) == SC_ADB_DEVICE_TYPE_TCPIP; if (is_already_tcpip) { // Nothing to do LOGI("Device already connected via TCP/IP: %s", serial); From 2edc73e4b80d35acc594adef2d35d99e743befed Mon Sep 17 00:00:00 2001 From: IskandarMa Date: Fri, 18 Mar 2022 11:53:26 +0800 Subject: [PATCH 1180/2244] Improve README.zh-Hans.md Fix misleading zoom instruction, which gave opposite action steps. PR #3126 Signed-off-by: Romain Vimont --- README.zh-Hans.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.zh-Hans.md b/README.zh-Hans.md index caf964b3..ac0713a6 100644 --- a/README.zh-Hans.md +++ b/README.zh-Hans.md @@ -704,11 +704,11 @@ scrcpy --disable-screensaver #### 双指缩放 -模拟“双指缩放”:Ctrl+_按住并移动鼠标_。 +模拟“双指缩放”:Ctrl+_按下并拖动鼠标_。 -更准确的说,在按住鼠标左键时按住 Ctrl。直到松开鼠标左键,所有鼠标移动将以屏幕中心为原点,缩放或旋转内容 (如果应用支持)。 +在按住 Ctrl 时按下鼠标左键,直到松开鼠标左键前,移动鼠标会使屏幕内容相对于屏幕中心进行缩放或旋转 (如果应用支持)。 -实际上,_scrcpy_ 会在关于屏幕中心对称的位置上用“虚拟手指”发出触摸事件。 +具体来说,_scrcpy_ 会在鼠标位置,以及鼠标以屏幕中心镜像的位置分别生成触摸事件。 #### 物理键盘模拟 (HID) From aaf3869a544d6700032f0a834e047737f18fb145 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 30 Mar 2022 11:57:47 +0200 Subject: [PATCH 1181/2244] Fix OpenGL ES prefix skip --- app/src/opengl.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/opengl.c b/app/src/opengl.c index da05c082..376690af 100644 --- a/app/src/opengl.c +++ b/app/src/opengl.c @@ -28,7 +28,7 @@ sc_opengl_init(struct sc_opengl *gl) { sizeof(OPENGL_ES_PREFIX) - 1); if (gl->is_opengles) { /* skip the prefix */ - version += sizeof(PREFIX) - 1; + version += sizeof(OPENGL_ES_PREFIX) - 1; } int r = sscanf(version, "%d.%d", &gl->version_major, &gl->version_minor); From 88543cb5453f74ac611759ff483b6a413f367e1b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 30 Mar 2022 14:00:05 +0200 Subject: [PATCH 1182/2244] Fix icon path in ./run The data/ directory has been moved to app/data/. Refs 36c75e15b8e9eeb01bd287a42fa3f1513a728ebb --- run | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/run b/run index fda3ea57..56f0a4e1 100755 --- a/run +++ b/run @@ -20,6 +20,6 @@ then exit 1 fi -SCRCPY_ICON_PATH="data/icon.png" \ +SCRCPY_ICON_PATH="app/data/icon.png" \ SCRCPY_SERVER_PATH="$BUILDDIR/server/scrcpy-server" \ "$BUILDDIR/app/scrcpy" "$@" From 7b7cfc3a3e463e83067da2669b401338a9826953 Mon Sep 17 00:00:00 2001 From: e1e0 <91560320+e1e0@users.noreply.github.com> Date: Fri, 25 Feb 2022 22:29:38 +0000 Subject: [PATCH 1183/2244] Fix reference to FAQ in README PR #3065 Signed-off-by: Romain Vimont --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c89b0c8d..5bd515fb 100644 --- a/README.md +++ b/README.md @@ -1105,7 +1105,7 @@ See [BUILD]. ## Common issues -See the [FAQ].md). +See the [FAQ]. [FAQ]: FAQ.md From 0c94887075b4c84532b71cbdbad4f0121ab94099 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 12 Apr 2022 23:51:05 +0200 Subject: [PATCH 1184/2244] Add missing include Refs c3d45c8397b7886d9071ccd69f04e2c9245fb8e7 --- app/src/adb/adb_device.c | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/adb/adb_device.c b/app/src/adb/adb_device.c index 4f500243..5ea8eb44 100644 --- a/app/src/adb/adb_device.c +++ b/app/src/adb/adb_device.c @@ -1,6 +1,7 @@ #include "adb_device.h" #include +#include void sc_adb_device_destroy(struct sc_adb_device *device) { From fa5b2a29e983a46b49531def9cf3d80c40c3de37 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 12 Apr 2022 23:59:01 +0200 Subject: [PATCH 1185/2244] Add missing SC_ prefix to header guards --- app/src/common.h | 4 ++-- app/src/compat.h | 4 ++-- app/src/control_msg.h | 4 ++-- app/src/controller.h | 4 ++-- app/src/device_msg.h | 4 ++-- app/src/fps_counter.h | 4 ++-- app/src/icon.h | 4 ++-- app/src/input_manager.h | 4 ++-- app/src/receiver.h | 4 ++-- app/src/util/cbuf.h | 4 ++-- app/src/util/net.h | 4 ++-- 11 files changed, 22 insertions(+), 22 deletions(-) diff --git a/app/src/common.h b/app/src/common.h index ce9ccad4..dccc8316 100644 --- a/app/src/common.h +++ b/app/src/common.h @@ -1,5 +1,5 @@ -#ifndef COMMON_H -#define COMMON_H +#ifndef SC_COMMON_H +#define SC_COMMON_H #include "config.h" #include "compat.h" diff --git a/app/src/compat.h b/app/src/compat.h index 62435718..8265dbc8 100644 --- a/app/src/compat.h +++ b/app/src/compat.h @@ -1,5 +1,5 @@ -#ifndef COMPAT_H -#define COMPAT_H +#ifndef SC_COMPAT_H +#define SC_COMPAT_H #include "config.h" diff --git a/app/src/control_msg.h b/app/src/control_msg.h index fbe203d4..1463fddc 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -1,5 +1,5 @@ -#ifndef CONTROLMSG_H -#define CONTROLMSG_H +#ifndef SC_CONTROLMSG_H +#define SC_CONTROLMSG_H #include "common.h" diff --git a/app/src/controller.h b/app/src/controller.h index f8bc7c02..f8662353 100644 --- a/app/src/controller.h +++ b/app/src/controller.h @@ -1,5 +1,5 @@ -#ifndef CONTROLLER_H -#define CONTROLLER_H +#ifndef SC_CONTROLLER_H +#define SC_CONTROLLER_H #include "common.h" diff --git a/app/src/device_msg.h b/app/src/device_msg.h index a0c989e3..e8d9fed4 100644 --- a/app/src/device_msg.h +++ b/app/src/device_msg.h @@ -1,5 +1,5 @@ -#ifndef DEVICEMSG_H -#define DEVICEMSG_H +#ifndef SC_DEVICEMSG_H +#define SC_DEVICEMSG_H #include "common.h" diff --git a/app/src/fps_counter.h b/app/src/fps_counter.h index e21c49c4..e7619271 100644 --- a/app/src/fps_counter.h +++ b/app/src/fps_counter.h @@ -1,5 +1,5 @@ -#ifndef FPSCOUNTER_H -#define FPSCOUNTER_H +#ifndef SC_FPSCOUNTER_H +#define SC_FPSCOUNTER_H #include "common.h" diff --git a/app/src/icon.h b/app/src/icon.h index 8df53671..3251e48f 100644 --- a/app/src/icon.h +++ b/app/src/icon.h @@ -1,5 +1,5 @@ -#ifndef ICON_H -#define ICON_H +#ifndef SC_ICON_H +#define SC_ICON_H #include "common.h" diff --git a/app/src/input_manager.h b/app/src/input_manager.h index f6c210e3..46b1160e 100644 --- a/app/src/input_manager.h +++ b/app/src/input_manager.h @@ -1,5 +1,5 @@ -#ifndef INPUTMANAGER_H -#define INPUTMANAGER_H +#ifndef SC_INPUTMANAGER_H +#define SC_INPUTMANAGER_H #include "common.h" diff --git a/app/src/receiver.h b/app/src/receiver.h index 3c4e8c64..f5808e4b 100644 --- a/app/src/receiver.h +++ b/app/src/receiver.h @@ -1,5 +1,5 @@ -#ifndef RECEIVER_H -#define RECEIVER_H +#ifndef SC_RECEIVER_H +#define SC_RECEIVER_H #include "common.h" diff --git a/app/src/util/cbuf.h b/app/src/util/cbuf.h index 01e41044..2a756171 100644 --- a/app/src/util/cbuf.h +++ b/app/src/util/cbuf.h @@ -1,6 +1,6 @@ // generic circular buffer (bounded queue) implementation -#ifndef CBUF_H -#define CBUF_H +#ifndef SC_CBUF_H +#define SC_CBUF_H #include "common.h" diff --git a/app/src/util/net.h b/app/src/util/net.h index 15979cf9..d9289981 100644 --- a/app/src/util/net.h +++ b/app/src/util/net.h @@ -1,5 +1,5 @@ -#ifndef NET_H -#define NET_H +#ifndef SC_NET_H +#define SC_NET_H #include "common.h" From fc65c6cc4baced6d9758ee54d9fdab57966d09b8 Mon Sep 17 00:00:00 2001 From: Alessandro Sarretta Date: Sat, 26 Mar 2022 06:17:26 +0100 Subject: [PATCH 1186/2244] Update README.it.md to v1.23 PR #3151 Signed-off-by: Romain Vimont --- README.it.md | 342 ++++++++++++++++++++++++++++++++++++++++++--------- README.md | 2 +- 2 files changed, 286 insertions(+), 58 deletions(-) diff --git a/README.it.md b/README.it.md index 52e68ba3..a35c7bbb 100644 --- a/README.it.md +++ b/README.it.md @@ -1,23 +1,42 @@ -_Apri il [README](README.md) originale e sempre aggiornato._ +_Apri il [README](README.md) originale (in inglese) e sempre aggiornato._ -# scrcpy (v1.19) +scrcpy -Questa applicazione fornisce la visualizzazione e il controllo dei dispositivi Android collegati via USB (o [via TCP/IP][article-tcpip]). Non richiede alcun accesso _root_. +# scrcpy (v1.23) + +_si pronuncia "**scr**een **c**o**py**"_ + +[Leggi in altre lingue](#traduzioni) + +Questa applicazione fornisce la visualizzazione e il controllo di dispositivi Android collegati via USB (o [via TCP/IP](#tcpip-wireless)). Non richiede alcun accesso _root_. Funziona su _GNU/Linux_, _Windows_ e _macOS_. ![screenshot](assets/screenshot-debian-600.jpg) Si concentra su: - - **leggerezza** (nativo, mostra solo lo schermo del dispositivo) - - **prestazioni** (30~60fps) - - **qualità** (1920×1080 o superiore) - - **bassa latenza** ([35~70ms][lowlatency]) - - **tempo di avvio basso** (~ 1secondo per visualizzare la prima immagine) - - **non invadenza** (nulla viene lasciato installato sul dispositivo) + - **leggerezza**: nativo, mostra solo lo schermo del dispositivo + - **prestazioni**: 30~120fps, in funzione del dispositivo + - **qualità**: 1920×1080 o superiore + - **bassa latenza**: [35~70ms][lowlatency] + - **tempo di avvio basso**: ~ 1secondo per visualizzare la prima immagine + - **non invadenza**: nulla rimane installato sul dispositivo + - **vantaggi per l'utente**: nessun account, nessuna pubblicità, non è richiesta alcuna connessione a internet + - **libertà**: software libero e a codice aperto (_free and open source_) [lowlatency]: https://github.com/Genymobile/scrcpy/pull/646 +Le sue caratteristiche includono: + - [registrazione](#registrazione) + - mirroring con [schermo del dispositivo spento](#spegnere-lo-schermo) + - [copia-incolla](#copia-incolla) in entrambe le direzioni + - [qualità configurabile](#configurazione-di-acquisizione) + - schermo del dispositivo [come webcam (V4L2)](#v4l2loopback) (solo per Linux) + - [simulazione della tastiera fisica (HID)](#simulazione-della-tastiera-fisica-HID) + - [simulazione mouse fisico (HID)](#simulazione-del-mouse-fisico-HID) + - [modalità OTG](#otg) + - e altro ancora... + ## Requisiti @@ -49,12 +68,18 @@ Compila dai sorgenti: [BUILD] (in inglese) ([procedimento semplificato][BUILD_si ### Linux -Su Debian (_testing_ e _sid_ per ora) e Ubuntu (20.04): +Su Debian e Ubuntu: ``` apt install scrcpy ``` +Su Arch Linux: + +``` +pacman -S scrcpy +``` + È disponibile anche un pacchetto [Snap]: [`scrcpy`][snap-link]. [snap-link]: https://snapstats.org/snaps/scrcpy @@ -66,10 +91,6 @@ Per Fedora, è disponibile un pacchetto [COPR]: [`scrcpy`][copr-link]. [COPR]: https://fedoraproject.org/wiki/Category:Copr [copr-link]: https://copr.fedorainfracloud.org/coprs/zeno/scrcpy/ -Per Arch Linux, è disponibile un pacchetto [AUR]: [`scrcpy`][aur-link]. - -[AUR]: https://wiki.archlinux.org/index.php/Arch_User_Repository -[aur-link]: https://aur.archlinux.org/packages/scrcpy/ Per Gentoo, è disponibile una [Ebuild]: [`scrcpy/`][ebuild-link]. @@ -142,7 +163,7 @@ Collega un dispositivo Android ed esegui: scrcpy ``` -Scrcpy accetta argomenti da riga di comando, essi sono listati con: +Scrcpy accetta argomenti da riga di comando, elencati con: ```bash scrcpy --help @@ -186,6 +207,14 @@ scrcpy --max-fps 15 Questo è supportato ufficialmente a partire da Android 10, ma potrebbe funzionare in versioni precedenti. +L'attuale frame rate di acquisizione può essere stampato sulla console: + +``` +scrcpy --print-fps +``` + +Può anche essere abilitato o disabilitato in qualsiasi momento con MOD+i. + #### Ritaglio Lo schermo del dispositivo può essere ritagliato per visualizzare solo parte di esso. @@ -258,7 +287,7 @@ I "fotogrammi saltati" sono registrati nonostante non siano mostrati in tempo re #### v4l2loopback -Su Linux è possibile inviare il flusso video ad un dispositivo v4l2 loopback, cosicchè un dispositivo Android possa essere aperto come una webcam da qualsiasi strumento compatibile con v4l2. +Su Linux è possibile inviare il flusso video ad un dispositivo v4l2 loopback, cosicché un dispositivo Android possa essere aperto come una webcam da qualsiasi strumento compatibile con v4l2. Il modulo `v4l2loopback` deve essere installato: @@ -321,42 +350,72 @@ scrcpy --display-buffer=50 # aggiungi 50 ms di buffer per la visualizzazione e per il V4L2 sink: ```bash -scrcpy --v4l2-buffer=500 # aggiungi 50 ms di buffer per il v4l2 sink +scrcpy --v4l2-buffer=500 # aggiungi 500 ms di buffer per il v4l2 sink ``` ### Connessione -#### Wireless +#### TCP/IP (wireless) +_Scrcpy_ usa `adb` per comunicare col dispositivo e `adb` può [connettersi][connect] a un dispositivo mediante TCP/IP. Il dispositivo deve essere collegato alla stessa rete del computer. -_Scrcpy_ usa `adb` per comunicare col dispositivo e `adb` può [connettersi][connect] al dispositivo mediante TCP/IP: +##### Automatico -1. Connetti il dispositivo alla stessa rete Wi-Fi del tuo computer. -2. Trova l'indirizzo IP del tuo dispositivo in Impostazioni → Informazioni sul telefono → Stato, oppure eseguendo questo comando: +Un'opzione `--tcpip` permette di configurare automaticamente la connessione. Ci sono due varianti. + +Se il dispositivo (accessibile a 192.168.1.1 in questo esempio) ascolta già su una porta (tipicamente 5555) per le connessioni adb in entrata, allora esegui: + +```bash +scrcpy --tcpip=192.168.1.1 # la porta predefinita è 5555 +scrcpy --tcpip=192.168.1.1:5555 +``` + +Se la modalità TCP/IP di adb è disabilitata sul dispositivo (o se non si conosce l'indirizzo IP indirizzo), collegare il dispositivo tramite USB, quindi eseguire: + +```bash +scrcpy --tcpip # senza argomenti +``` + +Il comando troverà automaticamente l'indirizzo IP del dispositivo, abiliterà la modalità TCP/IP, quindi connettersi al dispositivo prima di iniziare. + +##### Manuale + +In alternativa, è possibile abilitare la connessione TCP/IP manualmente usando `adb`: + +1. Inserisci il dispositivo in una porta USB del tuo computer. +2. Connetti il dispositivo alla stessa rete Wi-Fi del tuo computer. +3. Ottieni l'indirizzo IP del tuo dispositivo, in Impostazioni → Informazioni sul telefono → Stato, o + eseguendo questo comando: ```bash adb shell ip route | awk '{print $9}' ``` -3. Abilita adb via TCP/IP sul tuo dispositivo: `adb tcpip 5555`. -4. Scollega il tuo dispositivo. -5. Connetti il tuo dispositivo: `adb connect IP_DISPOSITVO:5555` _(rimpiazza `IP_DISPOSITIVO`)_. -6. Esegui `scrcpy` come al solito. +4. Abilita adb via TCP/IP sul tuo dispositivo: `adb tcpip 5555`. +5. Scollega il tuo dispositivo. +6. Connettiti al tuo dispositivo: `adb connect DEVICE_IP:5555` _(sostituisci `DEVICE_IP` +con l'indirizzo IP del dispositivo che hai trovato)_. +7. Esegui `scrcpy` come al solito. -Potrebbe essere utile diminuire il bit-rate e la definizione +Da Android 11, una [opzione di debug wireless][adb-wireless] permette di evitare di dover collegare fisicamente il dispositivo direttamente al computer. + +[adb-wireless]: https://developer.android.com/studio/command-line/adb#connect-to-a-device-over-wi-fi-android-11+ + +Se la connessione cade casualmente, esegui il comando `scrcpy` per riconnetterti. Se il comando dice che non ci sono dispositivi/emulatori trovati, prova ad eseguire `adb connect DEVICE_IP:5555` di nuovo, e poi `scrcpy` come al solito. Se dice ancora che non ne ha trovato nessuno, prova ad eseguire `adb disconnect` e poi esegui di nuovo questi due comandi. + +Potrebbe essere utile diminuire il bit-rate e la definizione: ```bash scrcpy --bit-rate 2M --max-size 800 -scrcpy -b2M -m800 # versione breve +scrcpy -b2M -m800 # versione breve ``` [connect]: https://developer.android.com/studio/command-line/adb.html#wireless - #### Multi dispositivo -Se in `adb devices` sono listati più dispositivi, è necessario specificare il _seriale_: +Se in `adb devices` sono elencati più dispositivi, è necessario specificare il _seriale_: ```bash scrcpy --serial 0123456789abcdef @@ -370,6 +429,18 @@ scrcpy --serial 192.168.0.1:5555 scrcpy -s 192.168.0.1:5555 # versione breve ``` +Se solo un dispositivo è collegato via USB o TCP/IP, è possibile selezionarlo automaticamente: + +```bash +# Select the only device connected via USB +scrcpy -d # like adb -d +scrcpy --select-usb # long version + +# Select the only device connected via TCP/IP +scrcpy -e # like adb -e +scrcpy --select-tcpip # long version +``` + Puoi avviare più istanze di _scrcpy_ per diversi dispositivi. @@ -383,37 +454,77 @@ autoadb scrcpy -s '{}' [AutoAdb]: https://github.com/rom1v/autoadb -#### Tunnel SSH +#### Tunnels -Per connettersi a un dispositivo remoto è possibile collegare un client `adb` locale ad un server `adb` remoto (assunto che entrambi stiano usando la stessa versione del protocollo _adb_): +Per connettersi a un dispositivo remoto, è possibile collegare un client `adb` locale a un server remoto `adb` (purché usino la stessa versione del protocollo _adb_). ). + +##### Server ADB remoto + +Per connettersi a un server ADB remoto, fate ascoltare il server su tutte le interfacce: ```bash -adb kill-server # termina il server adb locale su 5037 -ssh -CN -L5037:localhost:5037 -R27183:localhost:27183 your_remote_computer -# tieni questo aperto +adb kill-server +adb -a nodaemon server start +# tienilo aperto ``` -Da un altro terminale: +**Attenzione: tutte le comunicazioni tra i client e il server ADB non sono criptate.** + +Supponi che questo server sia accessibile a 192.168.1.2. Poi, da un altro terminale, esegui scrcpy: ```bash +export ADB_SERVER_SOCKET=tcp:192.168.1.2:5037 +scrcpy --tunnel-host=192.168.1.2 +``` + +Per impostazione predefinita, scrcpy utilizza la porta locale utilizzata per il tunnel `adb forward` (tipicamente `27183`, vedi `--port`). È anche possibile forzare una diversa porta del tunnel (può essere utile in situazioni più complesse, quando sono coinvolti più reindirizzamenti): + +``` +scrcpy --tunnel-port=1234 +``` + +##### SSH tunnel + +Per comunicare con un server ADB remoto in modo sicuro, è preferibile utilizzare un tunnel SSH. + +Per prima cosa, assicurati che il server ADB sia in esecuzione sul computer remoto: + +```bash +adb start-server +``` + +Poi, crea un tunnel SSH: + +```bash +# local 5038 --> remote 5037 +# local 27183 <-- remote 27183 +ssh -CN -L5038:localhost:5037 -R27183:localhost:27183 your_remote_computer +# keep this open +``` + +Da un altro terminale, esegui scrcpy: + +```bash +export ADB_SERVER_SOCKET=tcp:localhost:5038 scrcpy ``` Per evitare l'abilitazione dell'apertura porte remota potresti invece forzare una "forward connection" (notare il `-L` invece di `-R`) ```bash -adb kill-server # termina il server adb locale su 5037 -ssh -CN -L5037:localhost:5037 -L27183:localhost:27183 your_remote_computer +# local 5038 --> remote 5037 +# local 27183 --> remote 27183 +ssh -CN -L5038:localhost:5037 -L27183:localhost:27183 your_remote_computer # tieni questo aperto ``` -Da un altro terminale: +Da un altro terminale, esegui scrcpy: ```bash +export ADB_SERVER_SOCKET=tcp:localhost:5038 scrcpy --force-adb-forward ``` - Come per le connessioni wireless potrebbe essere utile ridurre la qualità: ``` @@ -551,6 +662,14 @@ scrcpy --turn-screen-off --stay-awake scrcpy -Sw ``` +#### Spegnimento alla chiusura + +Per spegnere lo schermo del dispositivo quando si chiude scrcpy: + +```bash +scrcpy --power-off-on-close +``` + #### Mostrare i tocchi @@ -596,20 +715,22 @@ Qualsiasi scorciatoia Ctrl viene inoltrata al dispositivo. In partico - Ctrl+x taglia - Ctrl+v incolla (dopo la sincronizzazione degli appunti da computer a dispositivo) -Questo solitamente funziona nella maniera più comune. +Questo solitamente funziona come ci si aspetta. Il comportamento reale, però, dipende dall'applicazione attiva. Per esempio _Termux_ invia SIGINT con Ctrl+c, e _K-9 Mail_ compone un nuovo messaggio. Per copiare, tagliare e incollare in questi casi (ma è solo supportato in Android >= 7): - - MOD+c inietta `COPY` - - MOD+x inietta `CUT` - - MOD+v inietta `PASTE` (dopo la sincronizzazione degli appunti da computer a dispositivo) + - MOD+c invia `COPY` + - MOD+x invia `CUT` + - MOD+v invia `PASTE` (dopo la sincronizzazione degli appunti da computer a dispositivo) -In aggiunta, MOD+Shift+v permette l'iniezione del testo degli appunti del computer come una sequenza di eventi pressione dei tasti. Questo è utile quando il componente non accetta l'incollaggio di testo (per esempio in _Termux_), ma questo può rompere il contenuto non ASCII. +In aggiunta, MOD+Shift+v permette l'invio del testo degli appunti del computer come una sequenza di eventi pressione dei tasti. Questo è utile quando il componente non accetta l'incollaggio di testo (per esempio in _Termux_), ma questo può compromettere il contenuto non ASCII. **AVVISO:** Incollare gli appunti del computer nel dispositivo (sia con Ctrl+v che con MOD+v) copia il contenuto negli appunti del dispositivo. Come conseguenza, qualsiasi applicazione Android potrebbe leggere il suo contenuto. Dovresti evitare di incollare contenuti sensibili (come password) in questa maniera. -Alcuni dispositivi non si comportano come aspettato quando si modificano gli appunti del dispositivo a livello di codice. L'opzione `--legacy-paste` è fornita per cambiare il comportamento di Ctrl+v and MOD+v in modo tale che anch'essi iniettino il testo gli appunti del computer come una sequenza di eventi pressione dei tasti (nella stessa maniera di MOD+Shift+v). +Alcuni dispositivi non si comportano come aspettato quando si modificano gli appunti del dispositivo a livello di codice. L'opzione `--legacy-paste` è fornita per cambiare il comportamento di Ctrl+v and MOD+v in modo tale che anch'essi inviino il testo degli appunti del computer come una sequenza di eventi di pressione dei tasti (nella stessa maniera di MOD+Shift+v). + +Per disabilitare la sincronizzazione automatica degli appunti, usa `--no-clipboard-autosync`. #### Pizzica per zoomare (pinch-to-zoom) @@ -617,16 +738,98 @@ Per simulare il "pizzica per zoomare": Ctrl+_click e trascina_. Più precisamente, tieni premuto Ctrl mentre premi il pulsante sinistro. Finchè il pulsante non sarà rilasciato, tutti i movimenti del mouse ridimensioneranno e ruoteranno il contenuto (se supportato dall'applicazione) relativamente al centro dello schermo. -Concretamente scrcpy genera degli eventi di tocco addizionali di un "dito virtuale" nella posizione simmetricamente opposta rispetto al centro dello schermo. +Concretamente, scrcpy genera degli eventi di tocco addizionali di un "dito virtuale" nella posizione simmetricamente opposta rispetto al centro dello schermo. + +#### Simulazione della tastiera fisica (HID) + +Per impostazione predefinita, scrcpy utilizza l'invio dei tasti o del testo di Android: funziona ovunque, ma è limitato all'ASCII. + +In alternativa scrcpy può simulare una tastiera fisica USB su Android per fornire una migliore esperienza di input (utilizzando [USB HID over AOAv2][hid-aoav2]): la tastiera virtuale è disabilitata e funziona per tutti i caratteri e IME. + +[hid-aoav2]: https://source.android.com/devices/accessories/aoa2#hid-support + +Tuttavia, funziona solo se il dispositivo è collegato via USB. + +Nota: su Windows, può funzionare solo in [odalità OTG](#otg), non durante il mirroring (non è possibile aprire un dispositivo USB se è già aperto da un altro processo come il daemon adb). + +Per abilitare questa modalità: + +```bash +scrcpy --hid-keyboard +scrcpy -K # versione breve +``` + +Se fallisce per qualche motivo (per esempio perché il dispositivo non è connesso via USB), ritorna automaticamente alla modalità predefinita (con un log nella console). Questo permette di usare le stesse opzioni della linea di comando quando si è connessi via USB e TCP/IP. + +In questa modalità, gli eventi i pressione originali (scancodes) sono inviati al dispositivo, indipendentemente dalla mappatura dei tasti dell'host. Pertanto, se il layout della tua tastiera non corrisponde, deve essere configurato sul dispositivo Android, in Impostazioni → Sistema → Lingue e input → [Tastiera fisica] (in inglese). + +Questa pagina di impostazioni può essere avviata direttamente: + +```bash +adb shell am start -a android.settings.HARD_KEYBOARD_SETTINGS +``` + +Tuttavia, l'opzione è disponibile solo quando la tastiera HID è abilitata (o quando una tastiera fisica è collegata). + +[Tastiera fisica]: https://github.com/Genymobile/scrcpy/pull/2632#issuecomment-923756915 + +#### Simulazione del mouse fisico (HID) + +In modo simile alla simulazione della tastiera fisica, è possibile simulare un mouse fisico. Allo stesso modo funziona solo se il dispositivo è connesso via USB. + +Per impostazione predefinita, scrcpy utilizza l'invio degli eventi del mouse di Android, utilizzando coordinate assolute. Simulando un mouse fisico, un puntatore del mouse appare sul dispositivo Android e vengono inviati i movimenti relativi del mouse, i click e gli scorrimenti. + +Per abilitare questa modalità: + +```bash +scrcpy --hid-mouse +scrcpy -M # versione breve +``` + +Si potrebbe anche aggiungere `--forward-all-clicks` a [inoltra tutti i pulsanti del mouse][forward_all_clicks]. + +[forward_all_clicks]: #click-destro-e-click-centrale -#### Preferenze di iniezione del testo +Quando questa modalità è attivata, il mouse del computer viene "catturato" (il puntatore del mouse scompare dal computer e appare invece sul dispositivo Android). + +I tasti speciali di cattura, Alt o Super, commutano (disabilitano o abilitano) la cattura del mouse. Usa uno di essi per ridare il controllo del mouse al computer. + + +#### OTG + +È possibile eseguire _scrcpy_ con la sola simulazione della tastiera fisica e del mouse (HID), come se la tastiera e il mouse del computer fossero collegati direttamente al dispositivo tramite un cavo OTG. + +In questa modalità, _adb_ (debug USB) non è necessario e il mirroring è disabilitato. + +Per attivare la modallità OTG: + +```bash +scrcpy --otg +# Passa la seriale se sono disponibili diversi dispositivi USB +scrcpy --otg -s 0123456789abcdef +``` + +È possibile abilitare solo la tastiera HID o il mouse HID: + +```bash +scrcpy --otg --hid-keyboard # solo la tastiera +scrcpy --otg --hid-mouse # solo mouse +scrcpy --otg --hid-keyboard --hid-mouse # tastiera e mouse +# per comodità, abilita entrambi per default +scrcpy --otg # tastiera e mouse +``` + +Come `--hid-keyboard` e `--hid-mouse`, funziona solo se il dispositivo è collegato via USB. + + +#### Preferenze di invio del testo Ci sono due tipi di [eventi][textevents] generati quando si scrive testo: - _eventi di pressione_, segnalano che tasto è stato premuto o rilasciato; - _eventi di testo_, segnalano che del testo è stato inserito. -In maniera predefinita le lettere sono "iniettate" usando gli eventi di pressione, in maniera tale che la tastiera si comporti come aspettato nei giochi (come accade solitamente per i tasti WASD). +In maniera predefinita le lettere sono inviate usando gli eventi di pressione, in maniera tale che la tastiera si comporti come aspettato nei giochi (come accade solitamente per i tasti WASD). Questo, però, può [causare problemi][prefertext]. Se incontri un problema del genere, puoi evitarlo con: @@ -636,13 +839,21 @@ scrcpy --prefer-text (ma questo romperà il normale funzionamento della tastiera nei giochi) +Al contrario, si potrebbe forzare per inviare sempre eventi di pressione grezzi: + +```bash +scrcpy --raw-key-events +``` + +Queste opzioni non hanno effetto sulla tastiera HID (tutti gli eventi di pressione sono inviati come scancodes in questa modalità). + [textevents]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-text-input [prefertext]: https://github.com/Genymobile/scrcpy/issues/650#issuecomment-512945343 #### Ripetizione di tasti -In maniera predefinita tenere premuto un tasto genera una ripetizione degli eventi di pressione di tale tasto. Questo può creare problemi di performance in alcuni giochi, dove questi eventi sono inutilizzati. +In maniera predefinita, tenere premuto un tasto genera una ripetizione degli eventi di pressione di tale tasto. Questo può creare problemi di performance in alcuni giochi, dove questi eventi sono inutilizzati. Per prevenire l'inoltro ripetuto degli eventi di pressione: @@ -650,9 +861,12 @@ Per prevenire l'inoltro ripetuto degli eventi di pressione: scrcpy --no-key-repeat ``` +Questa opzione non ha effetto sulla tastiera HID (la ripetizione dei tasti è gestita da Android direttamente in questa modalità). + + #### Click destro e click centrale -In maniera predefinita, click destro aziona BACK (indietro) e il click centrale aziona HOME. Per disabilitare queste scorciatoie e, invece, inviare i click al dispositivo: +In maniera predefinita, click destro aziona BACK (indietro) o POWER on (accensione) e il click centrale aziona HOME. Per disabilitare queste scorciatoie e, invece, inviare i click al dispositivo: ```bash scrcpy --forward-all-clicks @@ -705,7 +919,7 @@ scrcpy --shortcut-mod=rctrl scrcpy --shortcut-mod=lctrl+lalt,lsuper ``` -_[Super] è il pulsante Windows o Cmd._ +_[Super] è solitamente il pulsante Windows o Cmd._ [Super]: https://it.wikipedia.org/wiki/Tasto_Windows @@ -720,7 +934,7 @@ _[Super] è il pulsante Windows o Cmd._ | Premi il tasto `HOME` | MOD+h \| _Click centrale_ | Premi il tasto `BACK` | MOD+b \| _Click destro²_ | Premi il tasto `APP_SWITCH` | MOD+s \| _4° click³_ - | Premi il tasto `MENU` (sblocca lo schermo) | MOD+m + | Premi il tasto `MENU` (sblocca lo schermo)⁴ | MOD+m | Premi il tasto `VOLUME_UP` | MOD+ _(su)_ | Premi il tasto `VOLUME_DOWN` | MOD+ _(giù)_ | Premi il tasto `POWER` | MOD+p @@ -731,17 +945,20 @@ _[Super] è il pulsante Windows o Cmd._ | Espandi il pannello delle notifiche | MOD+n \| _5° click³_ | Espandi il pannello delle impostazioni | MOD+n+n \| _Doppio 5° click³_ | Chiudi pannelli | MOD+Shift+n - | Copia negli appunti⁴ | MOD+c - | Taglia negli appunti⁴ | MOD+x - | Sincronizza gli appunti e incolla⁴ | MOD+v - | Inietta il testo degli appunti del computer | MOD+Shift+v + | Copia negli appunti⁵ | MOD+c + | Taglia negli appunti⁵ | MOD+x + | Sincronizza gli appunti e incolla⁵ | MOD+v + | Invia il testo degli appunti del computer | MOD+Shift+v | Abilita/Disabilita il contatore FPS (su stdout) | MOD+i | Pizzica per zoomare | Ctrl+_click e trascina_ + | Trascina file APK | Installa APK dal computer + | Trascina file non-APK | [Trasferisci file verso il dispositivo](#push-file-to-device) _¹Doppio click sui bordi neri per rimuoverli._ _²Il tasto destro accende lo schermo se era spento, preme BACK in caso contrario._ _³4° e 5° pulsante del mouse, se il tuo mouse ne dispone._ -_⁴Solo in Android >= 7._ +_⁴Per le app native react in sviluppo, `MENU` attiva il menu di sviluppo._ +_⁵Solo in Android >= 7._ Le scorciatoie con pulsanti ripetuti sono eseguite rilasciando e premendo il pulsante una seconda volta. Per esempio, per eseguire "Espandi il pannello delle impostazioni": @@ -811,3 +1028,14 @@ Leggi la [pagina per sviluppatori]. [article-intro]: https://blog.rom1v.com/2018/03/introducing-scrcpy/ [article-tcpip]: https://www.genymotion.com/blog/open-source-project-scrcpy-now-works-wirelessly/ + +## Contatti + +Se incontri un bug, per favore leggi prima le [FAQ](FAQ.it.md), poi apri una [issue]. + +[issue]: https://github.com/Genymobile/scrcpy/issues + +Per domande generali o discussioni, puoi anche usare: + + - Reddit: [`r/scrcpy`](https://www.reddit.com/r/scrcpy) + - Twitter: [`@scrcpy_app`](https://twitter.com/scrcpy_app) diff --git a/README.md b/README.md index 5bd515fb..c7346974 100644 --- a/README.md +++ b/README.md @@ -1159,7 +1159,7 @@ This README is available in other languages: - [Deutsch (German, `de`) - v1.22](README.de.md) - [Indonesian (Indonesia, `id`) - v1.16](README.id.md) -- [Italiano (Italiano, `it`) - v1.19](README.it.md) +- [Italiano (Italiano, `it`) - v1.23](README.it.md) - [日本語 (Japanese, `jp`) - v1.19](README.jp.md) - [한국어 (Korean, `ko`) - v1.11](README.ko.md) - [Português Brasileiro (Brazilian Portuguese, `pt-BR`) - v1.19](README.pt-br.md) From 53b1e16f420d818071840889da52d6ac63b14b4d Mon Sep 17 00:00:00 2001 From: John Veness Date: Sat, 9 Apr 2022 15:45:28 +0100 Subject: [PATCH 1187/2244] Fix typos/grammar issues in README PR #3174 Signed-off-by: Romain Vimont --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index c7346974..7eb6cea6 100644 --- a/README.md +++ b/README.md @@ -190,7 +190,7 @@ scrcpy --max-size 1024 scrcpy -m 1024 # short version ``` -The other dimension is computed to that the device aspect ratio is preserved. +The other dimension is computed so that the device aspect ratio is preserved. That way, a device in 1920×1080 will be mirrored at 1024×576. @@ -789,7 +789,7 @@ 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 +its content. You should avoid pasting sensitive content (like passwords) that way. Some devices do not behave as expected when setting the device clipboard @@ -838,7 +838,7 @@ scrcpy -K # short version If it fails for some reason (for example because the device is not connected via USB), it automatically fallbacks to the default mode (with a log in the -console). This allows to use the same command line options when connected over +console). This allows using the same command line options when connected over USB and TCP/IP. In this mode, raw key events (scancodes) are sent to the device, independently @@ -1062,7 +1062,7 @@ _³4th and 5th mouse buttons, if your mouse has them._ _⁴For react-native apps in development, `MENU` triggers development menu._ _⁵Only on Android >= 7._ -Shortcuts with repeated keys are executted by releasing and pressing the key a +Shortcuts with repeated keys are executed by releasing and pressing the key a second time. For example, to execute "Expand settings panel": 1. Press and keep pressing MOD. From 3c8ebf9abd4410fd46485db43035e35b3780e8d4 Mon Sep 17 00:00:00 2001 From: Gitoffthelawn Date: Sun, 10 Apr 2022 08:45:34 -0700 Subject: [PATCH 1188/2244] Improve README Improve clarity, grammar, consistency, punctuation, and formatting. PR #3177 Signed-off-by: Romain Vimont --- README.md | 144 +++++++++++++++++++++++++++--------------------------- 1 file changed, 73 insertions(+), 71 deletions(-) diff --git a/README.md b/README.md index 7eb6cea6..66c8924b 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ _pronounced "**scr**een **c**o**py**"_ [Read in another language](#translations) This application provides display and control of Android devices connected via -USB (or [over TCP/IP](#tcpip-wireless)). It does not require any _root_ access. +USB or [over TCP/IP](#tcpip-wireless). It does not require any _root_ access. It works on _GNU/Linux_, _Windows_ and _macOS_. ![screenshot](assets/screenshot-debian-600.jpg) @@ -19,7 +19,7 @@ It focuses on: - **quality**: 1920×1080 or above - **low latency**: [35~70ms][lowlatency] - **low startup time**: ~1 second to display the first image - - **non-intrusiveness**: nothing is left installed on the device + - **non-intrusiveness**: nothing is left installed on the Android device - **user benefits**: no account, no ads, no internet required - **freedom**: free and open source software @@ -27,10 +27,10 @@ It focuses on: Its features include: - [recording](#recording) - - mirroring with [device screen off](#turn-screen-off) + - mirroring with [Android device screen off](#turn-screen-off) - [copy-paste](#copy-paste) in both directions - [configurable quality](#capture-configuration) - - device screen [as a webcam (V4L2)](#v4l2loopback) (Linux-only) + - Android device [as a webcam (V4L2)](#v4l2loopback) (Linux-only) - [physical keyboard simulation (HID)](#physical-keyboard-simulation-hid) - [physical mouse simulation (HID)](#physical-mouse-simulation-hid) - [OTG mode](#otg) @@ -40,12 +40,12 @@ Its features include: The Android device requires at least API 21 (Android 5.0). -Make sure you [enabled adb debugging][enable-adb] on your device(s). +Make sure you [enable adb debugging][enable-adb] on your device(s). [enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling On some devices, you also need to enable [an additional option][control] to -control it using keyboard and mouse. +control it using a keyboard and mouse. [control]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323 @@ -97,14 +97,14 @@ For Gentoo, an [Ebuild] is available: [`scrcpy/`][ebuild-link]. [Ebuild]: https://wiki.gentoo.org/wiki/Ebuild [ebuild-link]: https://github.com/maggu2810/maggu2810-overlay/tree/master/app-mobilephone/scrcpy -You could also [build the app manually][BUILD] ([simplified +You can also [build the app manually][BUILD] ([simplified process][BUILD_simple]). ### Windows -For Windows, for simplicity, a prebuilt archive with all the dependencies -(including `adb`) is available: +For Windows, a prebuilt archive with all the dependencies (including `adb`) is +available: - [`scrcpy-win64-v1.23.zip`][direct-win64] _(SHA-256: d2f601b1d0157faf65153d8a093d827fd65aec5d5842d677ac86fb2b5b7704cc)_ @@ -148,7 +148,7 @@ You need `adb`, accessible from your `PATH`. If you don't have it yet: brew install android-platform-tools ``` -It's also available in [MacPorts], which sets up adb for you: +It's also available in [MacPorts], which sets up `adb` for you: ```bash sudo port install scrcpy @@ -162,7 +162,7 @@ You can also [build the app manually][BUILD]. ## Run -Plug an Android device, and execute: +Plug an Android device into your computer, and execute: ```bash scrcpy @@ -180,7 +180,7 @@ scrcpy --help #### Reduce size -Sometimes, it is useful to mirror an Android device at a lower definition to +Sometimes, it is useful to mirror an Android device at a lower resolution to increase performance. To limit both the width and height to some value (e.g. 1024): @@ -190,8 +190,8 @@ scrcpy --max-size 1024 scrcpy -m 1024 # short version ``` -The other dimension is computed so that the device aspect ratio is preserved. -That way, a device in 1920×1080 will be mirrored at 1024×576. +The other dimension is computed so that the Android device aspect ratio is +preserved. That way, a device in 1920×1080 will be mirrored at 1024×576. #### Change bit-rate @@ -226,7 +226,7 @@ It may also be enabled or disabled at any time with MOD+i. The device screen may be cropped to mirror only part of the screen. -This is useful for example to mirror only one eye of the Oculus Go: +This is useful, for example, to mirror only one eye of the Oculus Go: ```bash scrcpy --crop 1224:1440:0:0 # 1224x1440 at offset (0,0) @@ -237,7 +237,6 @@ If `--max-size` is also specified, resizing is applied after cropping. #### Lock video orientation - To lock the orientation of the mirroring: ```bash @@ -262,7 +261,7 @@ crash. It is possible to select a different encoder: scrcpy --encoder OMX.qcom.video.encoder.avc ``` -To list the available encoders, you could pass an invalid encoder name, the +To list the available encoders, you can pass an invalid encoder name; the error will give the available encoders: ```bash @@ -326,7 +325,7 @@ v4l2-ctl --list-devices ls /dev/video* ``` -To start scrcpy using a v4l2 sink: +To start `scrcpy` using a v4l2 sink: ```bash scrcpy --v4l2-sink=/dev/videoN @@ -334,7 +333,7 @@ scrcpy --v4l2-sink=/dev/videoN --no-display # disable mirroring window scrcpy --v4l2-sink=/dev/videoN -N # short version ``` -(replace `N` by the device ID, check with `ls /dev/video*`) +(replace `N` with the device ID, check with `ls /dev/video*`) Once enabled, you can open your video stream with a v4l2-capable tool: @@ -350,7 +349,7 @@ For example, you could capture the video within [OBS]. #### Buffering -It is possible to add buffering. This increases latency but reduces jitter (see +It is possible to add buffering. This increases latency, but reduces jitter (see [#2464]). [#2464]: https://github.com/Genymobile/scrcpy/issues/2464 @@ -382,14 +381,14 @@ An option `--tcpip` allows to configure the connection automatically. There are two variants. If the device (accessible at 192.168.1.1 in this example) already listens on a -port (typically 5555) for incoming adb connections, then run: +port (typically 5555) for incoming _adb_ connections, then run: ```bash scrcpy --tcpip=192.168.1.1 # default port is 5555 scrcpy --tcpip=192.168.1.1:5555 ``` -If adb TCP/IP mode is disabled on the device (or if you don't know the IP +If _adb_ TCP/IP mode is disabled on the device (or if you don't know the IP address), connect the device over USB, then run: ```bash @@ -413,7 +412,7 @@ Alternatively, it is possible to enable the TCP/IP connection manually using adb shell ip route | awk '{print $9}' ``` -4. Enable adb over TCP/IP on your device: `adb tcpip 5555`. +4. Enable `adb` over TCP/IP on your device: `adb tcpip 5555`. 5. Unplug your device. 6. Connect to your device: `adb connect DEVICE_IP:5555` _(replace `DEVICE_IP` with the device IP address you found)_. @@ -427,9 +426,9 @@ having to physically connect your device directly to your computer. If the connection randomly drops, run your `scrcpy` command to reconnect. If it says there are no devices/emulators found, try running `adb connect DEVICE_IP:5555` again, and then `scrcpy` as usual. If it still says there are -none found, try running `adb disconnect` and then run those two commands again. +none found, try running `adb disconnect`, and then run those two commands again. -It may be useful to decrease the bit-rate and the definition: +It may be useful to decrease the bit-rate and the resolution: ```bash scrcpy --bit-rate 2M --max-size 800 @@ -488,7 +487,7 @@ protocol). ##### Remote ADB server -To connect to a remote ADB server, make the server listen on all interfaces: +To connect to a remote _adb server_, make the server listen on all interfaces: ```bash adb kill-server @@ -496,17 +495,18 @@ adb -a nodaemon server start # keep this open ``` -**Warning: all communications between clients and ADB server are unencrypted.** +**Warning: all communications between clients and the _adb server_ are +unencrypted.** Suppose that this server is accessible at 192.168.1.2. Then, from another -terminal, run scrcpy: +terminal, run `scrcpy`: ```bash export ADB_SERVER_SOCKET=tcp:192.168.1.2:5037 scrcpy --tunnel-host=192.168.1.2 ``` -By default, scrcpy uses the local port used for `adb forward` tunnel +By default, `scrcpy` uses the local port used for `adb forward` tunnel establishment (typically `27183`, see `--port`). It is also possible to force a different tunnel port (it may be useful in more complex situations, when more redirections are involved): @@ -518,16 +518,16 @@ scrcpy --tunnel-port=1234 ##### SSH tunnel -To communicate with a remote ADB server securely, it is preferable to use a SSH -tunnel. +To communicate with a remote _adb server_ securely, it is preferable to use an +SSH tunnel. -First, make sure the ADB server is running on the remote computer: +First, make sure the _adb server_ is running on the remote computer: ```bash adb start-server ``` -Then, establish a SSH tunnel: +Then, establish an SSH tunnel: ```bash # local 5038 --> remote 5037 @@ -536,7 +536,7 @@ ssh -CN -L5038:localhost:5037 -R27183:localhost:27183 your_remote_computer # keep this open ``` -From another terminal, run scrcpy: +From another terminal, run `scrcpy`: ```bash export ADB_SERVER_SOCKET=tcp:localhost:5038 @@ -553,7 +553,7 @@ ssh -CN -L5038:localhost:5037 -L27183:localhost:27183 your_remote_computer # keep this open ``` -From another terminal, run scrcpy: +From another terminal, run `scrcpy`: ```bash export ADB_SERVER_SOCKET=tcp:localhost:5038 @@ -595,7 +595,7 @@ scrcpy --window-borderless #### Always on top -To keep the scrcpy window always on top: +To keep the _scrcpy_ window always on top: ```bash scrcpy --always-on-top @@ -620,7 +620,7 @@ The window may be rotated: scrcpy --rotation 1 ``` -Possibles values are: +Possible values: - `0`: no rotation - `1`: 90 degrees counterclockwise - `2`: 180 degrees @@ -669,19 +669,19 @@ 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). +10 (otherwise it is mirrored as read-only). #### Stay awake -To prevent the device to sleep after some delay when the device is plugged in: +To prevent the device from sleeping after a delay when the device is plugged in: ```bash scrcpy --stay-awake scrcpy -w ``` -The initial state is restored when scrcpy is closed. +The initial state is restored when _scrcpy_ is closed. #### Turn screen off @@ -699,9 +699,10 @@ 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 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. +`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. It can also be useful to prevent the device from sleeping: @@ -712,7 +713,7 @@ scrcpy -Sw #### Power off on close -To turn the device screen off when closing scrcpy: +To turn the device screen off when closing _scrcpy_: ```bash scrcpy --power-off-on-close @@ -734,12 +735,13 @@ scrcpy --show-touches scrcpy -t ``` -Note that it only shows _physical_ touches (with the finger on the device). +Note that it only shows _physical_ touches (by a finger on the device). #### Disable screensaver -By default, scrcpy does not prevent the screensaver to run on the computer. +By default, _scrcpy_ does not prevent the screensaver from running on the +computer. To disable it: @@ -781,18 +783,18 @@ To copy, cut and paste in such cases (but only supported on Android >= 7): - 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 injects 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 +into the Android clipboard. As a consequence, any Android application could read its content. You should avoid pasting sensitive content (like passwords) that way. -Some devices do not behave as expected when setting the device clipboard +Some Android devices do not behave as expected when setting the device clipboard programmatically. An option `--legacy-paste` is provided to change the behavior of Ctrl+v and MOD+v so that they also inject the computer clipboard text as a sequence of key events (the same @@ -805,29 +807,29 @@ To disable automatic clipboard synchronization, use 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. +More precisely, hold down 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. +Technically, _scrcpy_ generates additional touch events from a "virtual finger" +at a location inverted through the center of the screen. #### Physical keyboard simulation (HID) -By default, scrcpy uses Android key or text injection: it works everywhere, but -is limited to ASCII. +By default, _scrcpy_ uses Android key or text injection: it works everywhere, +but is limited to ASCII. -Alternatively, scrcpy can simulate a physical USB keyboard on Android to provide -a better input experience (using [USB HID over AOAv2][hid-aoav2]): the virtual -keyboard is disabled and it works for all characters and IME. +Alternatively, `scrcpy` can simulate a physical USB keyboard on Android to +provide a better input experience (using [USB HID over AOAv2][hid-aoav2]): the +virtual keyboard is disabled and it works for all characters and IME. [hid-aoav2]: https://source.android.com/devices/accessories/aoa2#hid-support -However, it only works if the device is connected by USB. +However, it only works if the device is connected via USB. Note: On Windows, it may only work in [OTG mode](#otg), not while mirroring (it is not possible to open a USB device if it is already open by another process -like the adb daemon). +like the _adb daemon_). To enable this mode: @@ -862,7 +864,7 @@ a physical keyboard is connected). Similarly to the physical keyboard simulation, it is possible to simulate a physical mouse. Likewise, it only works if the device is connected by USB. -By default, scrcpy uses Android mouse events injection, using absolute +By default, _scrcpy_ uses Android mouse events injection with absolute coordinates. By simulating a physical mouse, a mouse pointer appears on the Android device, and relative mouse motion, clicks and scrolls are injected. @@ -873,7 +875,7 @@ scrcpy --hid-mouse scrcpy -M # short version ``` -You could also add `--forward-all-clicks` to [forward all mouse +You can also add `--forward-all-clicks` to [forward all mouse buttons][forward_all_clicks]. [forward_all_clicks]: #right-click-and-middle-click @@ -892,7 +894,7 @@ It is possible to run _scrcpy_ with only physical keyboard and mouse simulation (HID), as if the computer keyboard and mouse were plugged directly to the device via an OTG cable. -In this mode, _adb_ (USB debugging) is not necessary, and mirroring is disabled. +In this mode, `adb` (USB debugging) is not necessary, and mirroring is disabled. To enable OTG mode: @@ -918,7 +920,7 @@ connected by USB. #### Text injection preference -There are two kinds of [events][textevents] generated when typing text: +Two kinds of [events][textevents] are generated when typing text: - _key events_, signaling that a key is pressed or released; - _text events_, signaling that a text has been entered. @@ -1075,7 +1077,7 @@ handled by the active application. ## Custom paths -To use a specific _adb_ binary, configure its path in the environment variable +To use a specific `adb` binary, configure its path in the environment variable `ADB`: ```bash @@ -1088,7 +1090,7 @@ To override the path of the `scrcpy-server` file, configure its path in To override the icon, configure its path in `SCRCPY_ICON_PATH`. -## Why _scrcpy_? +## Why the name _scrcpy_? A colleague challenged me to find a name as unpronounceable as [gnirehtet]. @@ -1148,7 +1150,7 @@ If you encounter a bug, please read the [FAQ] first, then open an [issue]. [issue]: https://github.com/Genymobile/scrcpy/issues -For general questions or discussions, you could also use: +For general questions or discussions, you can also use: - Reddit: [`r/scrcpy`](https://www.reddit.com/r/scrcpy) - Twitter: [`@scrcpy_app`](https://twitter.com/scrcpy_app) From 7d8b72d4a644baf0b8ced72c2313605ad0a32a12 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 15 Apr 2022 15:51:01 +0200 Subject: [PATCH 1189/2244] Adapt event injection to Android 13 Using the "input" service results in a permission error in Android 13. Use the InputManager instance (retrieved by InputManager.getInstance()) instead. Fixes #3186 PR #3190 --- .../com/genymobile/scrcpy/wrappers/InputManager.java | 5 ++--- .../com/genymobile/scrcpy/wrappers/ServiceManager.java | 9 ++++++++- 2 files changed, 10 insertions(+), 4 deletions(-) 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 61168993..c42f9de1 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java @@ -2,7 +2,6 @@ package com.genymobile.scrcpy.wrappers; import com.genymobile.scrcpy.Ln; -import android.os.IInterface; import android.view.InputEvent; import java.lang.reflect.InvocationTargetException; @@ -14,13 +13,13 @@ public final class InputManager { public static final int INJECT_INPUT_EVENT_MODE_WAIT_FOR_RESULT = 1; public static final int INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH = 2; - private final IInterface manager; + private final android.hardware.input.InputManager manager; private Method injectInputEventMethod; private boolean alternativeInjectInputEventMethod; private static Method setDisplayIdMethod; - public InputManager(IInterface manager) { + public InputManager(android.hardware.input.InputManager manager) { this.manager = manager; } 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 6f4b9c04..ea2a0784 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java @@ -4,6 +4,7 @@ import android.annotation.SuppressLint; import android.os.IBinder; import android.os.IInterface; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @SuppressLint("PrivateApi,DiscouragedPrivateApi") @@ -56,7 +57,13 @@ public final class ServiceManager { public InputManager getInputManager() { if (inputManager == null) { - inputManager = new InputManager(getService("input", "android.hardware.input.IInputManager")); + try { + Method getInstanceMethod = android.hardware.input.InputManager.class.getDeclaredMethod("getInstance"); + android.hardware.input.InputManager im = (android.hardware.input.InputManager) getInstanceMethod.invoke(null); + inputManager = new InputManager(im); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + throw new AssertionError(e); + } } return inputManager; } From 6a4a4a283d4b79f41cf46fc6eb314af4b36cc1a5 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 16 Apr 2022 09:28:24 +0200 Subject: [PATCH 1190/2244] Remove obsolete alternative injection method The previous commit replaced the IInterface instance (the "input" service) by the InputManager instance (retrieved by InputManager.getInstance()). Both define an "injectInputEvent" method, but the alternate version (probably) does not concern the InputManager. This reverts commit b7a06278fee0dbfbb18b651ad563d9fb0797c8bc. PR #3190 --- .../com/genymobile/scrcpy/wrappers/InputManager.java | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) 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 c42f9de1..38e96d45 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java @@ -15,7 +15,6 @@ public final class InputManager { private final android.hardware.input.InputManager manager; private Method injectInputEventMethod; - private boolean alternativeInjectInputEventMethod; private static Method setDisplayIdMethod; @@ -25,12 +24,7 @@ public final class InputManager { private Method getInjectInputEventMethod() throws NoSuchMethodException { if (injectInputEventMethod == null) { - try { - injectInputEventMethod = manager.getClass().getMethod("injectInputEvent", InputEvent.class, int.class); - } catch (NoSuchMethodException e) { - injectInputEventMethod = manager.getClass().getMethod("injectInputEvent", InputEvent.class, int.class, int.class); - alternativeInjectInputEventMethod = true; - } + injectInputEventMethod = manager.getClass().getMethod("injectInputEvent", InputEvent.class, int.class); } return injectInputEventMethod; } @@ -38,10 +32,6 @@ public final class InputManager { public boolean injectInputEvent(InputEvent inputEvent, int mode) { try { Method method = getInjectInputEventMethod(); - if (alternativeInjectInputEventMethod) { - // See - return (boolean) method.invoke(manager, inputEvent, mode, 0); - } return (boolean) method.invoke(manager, inputEvent, mode); } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { Ln.e("Could not invoke method", e); From b8d78743f7fbe7aefed957f41e68efc4791573ba Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 22 Apr 2022 13:29:04 +0200 Subject: [PATCH 1191/2244] Upgrade platform-tools (33.0.1) for Windows PR #3206 --- app/prebuilt-deps/prepare-adb.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/prebuilt-deps/prepare-adb.sh b/app/prebuilt-deps/prepare-adb.sh index 2e7997e1..6a1f4896 100755 --- a/app/prebuilt-deps/prepare-adb.sh +++ b/app/prebuilt-deps/prepare-adb.sh @@ -6,10 +6,10 @@ cd "$DIR" mkdir -p "$PREBUILT_DATA_DIR" cd "$PREBUILT_DATA_DIR" -DEP_DIR=platform-tools-31.0.3 +DEP_DIR=platform-tools-33.0.1 -FILENAME=platform-tools_r31.0.3-windows.zip -SHA256SUM=0f4b8fdd26af2c3733539d6eebb3c2ed499ea1d4bb1f4e0ecc2d6016961a6e24 +FILENAME=platform-tools_r33.0.1-windows.zip +SHA256SUM=c1f02d42ea24ef4ff2a405ae7370e764ef4546f9b3e4520f5571a00ed5012c42 if [[ -d "$DEP_DIR" ]] then From 4db97531e887ef3455548ce09dd6cbff54b630ed Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 22 Apr 2022 13:33:50 +0200 Subject: [PATCH 1192/2244] Upgrade libusb (1.0.26) for Windows Upgrade and enable libusb support for Windows 32-bit builds. Refs #3011 Fixes #3204 PR #3206 --- app/meson.build | 4 ++-- app/prebuilt-deps/prepare-libusb.sh | 20 +++++++++++++------- cross_win32.txt | 4 ++-- cross_win64.txt | 4 ++-- release.mk | 6 ++---- 5 files changed, 21 insertions(+), 17 deletions(-) diff --git a/app/meson.build b/app/meson.build index e34b1893..f5d76c61 100644 --- a/app/meson.build +++ b/app/meson.build @@ -143,12 +143,12 @@ else prebuilt_libusb = meson.get_cross_property('prebuilt_libusb') prebuilt_libusb_root = meson.get_cross_property('prebuilt_libusb_root') - libusb_bin_dir = meson.current_source_dir() + '/prebuilt-deps/data/' + prebuilt_libusb + '/dll' + libusb_bin_dir = meson.current_source_dir() + '/prebuilt-deps/data/' + prebuilt_libusb libusb_include_dir = 'prebuilt-deps/data/' + prebuilt_libusb_root + '/include' libusb = declare_dependency( dependencies: [ - cc.find_library('libusb-1.0', dirs: libusb_bin_dir), + cc.find_library('msys-usb-1.0', dirs: libusb_bin_dir), ], include_directories: include_directories(libusb_include_dir) ) diff --git a/app/prebuilt-deps/prepare-libusb.sh b/app/prebuilt-deps/prepare-libusb.sh index 54ead536..a0c3721d 100755 --- a/app/prebuilt-deps/prepare-libusb.sh +++ b/app/prebuilt-deps/prepare-libusb.sh @@ -6,10 +6,10 @@ cd "$DIR" mkdir -p "$PREBUILT_DATA_DIR" cd "$PREBUILT_DATA_DIR" -DEP_DIR=libusb-1.0.25 +DEP_DIR=libusb-1.0.26 -FILENAME=libusb-1.0.25.7z -SHA256SUM=3d1c98416f454026034b2b5d67f8a294053898cb70a8b489874e75b136c6674d +FILENAME=libusb-1.0.26-binaries.7z +SHA256SUM=9c242696342dbde9cdc47239391f71833939bf9f7aa2bbb28cdaabe890465ec5 if [[ -d "$DEP_DIR" ]] then @@ -17,12 +17,18 @@ then exit 0 fi -get_file "https://github.com/libusb/libusb/releases/download/v1.0.25/$FILENAME" "$FILENAME" "$SHA256SUM" +get_file "https://github.com/libusb/libusb/releases/download/v1.0.26/$FILENAME" "$FILENAME" "$SHA256SUM" mkdir "$DEP_DIR" cd "$DEP_DIR" +# include/ is the same in all folders of the archive 7z x "../$FILENAME" \ - MinGW32/dll/libusb-1.0.dll \ - MinGW64/dll/libusb-1.0.dll \ - include / + libusb-1.0.26-binaries/libusb-MinGW-Win32/bin/msys-usb-1.0.dll \ + libusb-1.0.26-binaries/libusb-MinGW-x64/bin/msys-usb-1.0.dll \ + libusb-1.0.26-binaries/libusb-MinGW-x64/include/ + +mv libusb-1.0.26-binaries/libusb-MinGW-Win32/bin MinGW-Win32 +mv libusb-1.0.26-binaries/libusb-MinGW-x64/bin MinGW-x64 +mv libusb-1.0.26-binaries/libusb-MinGW-x64/include . +rm -rf libusb-1.0.26-binaries diff --git a/cross_win32.txt b/cross_win32.txt index 750dbd78..a2341f21 100644 --- a/cross_win32.txt +++ b/cross_win32.txt @@ -21,5 +21,5 @@ ffmpeg_avformat = 'avformat-58' ffmpeg_avutil = 'avutil-56' prebuilt_ffmpeg = 'ffmpeg-win32-4.3.1' prebuilt_sdl2 = 'SDL2-2.0.20/i686-w64-mingw32' -prebuilt_libusb_root = 'libusb-1.0.25' -prebuilt_libusb = prebuilt_libusb_root + '/MinGW32' +prebuilt_libusb_root = 'libusb-1.0.26' +prebuilt_libusb = prebuilt_libusb_root + '/MinGW-Win32' diff --git a/cross_win64.txt b/cross_win64.txt index 114b0c22..0404aedc 100644 --- a/cross_win64.txt +++ b/cross_win64.txt @@ -21,5 +21,5 @@ ffmpeg_avformat = 'avformat-59' ffmpeg_avutil = 'avutil-57' prebuilt_ffmpeg = 'ffmpeg-win64-5.0' prebuilt_sdl2 = 'SDL2-2.0.20/x86_64-w64-mingw32' -prebuilt_libusb_root = 'libusb-1.0.25' -prebuilt_libusb = prebuilt_libusb_root + '/MinGW64' +prebuilt_libusb_root = 'libusb-1.0.26' +prebuilt_libusb = prebuilt_libusb_root + '/MinGW-x64' diff --git a/release.mk b/release.mk index caf6ab1a..1c550afb 100644 --- a/release.mk +++ b/release.mk @@ -75,12 +75,10 @@ prepare-deps-win64: @app/prebuilt-deps/prepare-libusb.sh build-win32: prepare-deps-win32 - # -Dusb=false because of libusb-win32 build issue, cf #3011 [ -d "$(WIN32_BUILD_DIR)" ] || ( mkdir "$(WIN32_BUILD_DIR)" && \ meson "$(WIN32_BUILD_DIR)" \ --cross-file cross_win32.txt \ --buildtype release --strip -Db_lto=true \ - -Dusb=false \ -Dcompile_server=false \ -Dportable=true ) ninja -C "$(WIN32_BUILD_DIR)" @@ -111,7 +109,7 @@ dist-win32: build-server build-win32 cp app/prebuilt-deps/data/platform-tools-31.0.3/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-31.0.3/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/SDL2-2.0.20/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - #cp app/prebuilt-deps/data/libusb-1.0.25/MinGW32/dll/libusb-1.0.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/libusb-1.0.26/MinGW-Win32/msys-usb-1.0.dll "$(DIST)/$(WIN32_TARGET_DIR)/" dist-win64: build-server build-win64 mkdir -p "$(DIST)/$(WIN64_TARGET_DIR)" @@ -130,7 +128,7 @@ dist-win64: build-server build-win64 cp app/prebuilt-deps/data/platform-tools-31.0.3/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-31.0.3/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/SDL2-2.0.20/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/libusb-1.0.25/MinGW64/dll/libusb-1.0.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/libusb-1.0.26/MinGW-x64/msys-usb-1.0.dll "$(DIST)/$(WIN64_TARGET_DIR)/" zip-win32: dist-win32 cd "$(DIST)/$(WIN32_TARGET_DIR)"; \ From c05981587f1a322c9d7c16f5ec6e9392f7e98849 Mon Sep 17 00:00:00 2001 From: Sean Wei Date: Mon, 25 Apr 2022 16:37:03 +0800 Subject: [PATCH 1193/2244] Fix typos in Indonesian README Fix `code` blocks. PR #3216 Signed-off-by: Romain Vimont --- README.id.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.id.md b/README.id.md index 9657b95a..1230f0cc 100644 --- a/README.id.md +++ b/README.id.md @@ -218,7 +218,7 @@ variation] does not impact the recorded file. #### Wireless -_Scrcpy_ menggunakan `adb` untuk berkomunikasi dengan perangkat, dan` adb` dapat [terhubung] ke perangkat melalui TCP / IP: +_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). @@ -281,7 +281,7 @@ Dari terminal lain: scrcpy ``` -Untuk menghindari mengaktifkan penerusan port jarak jauh, Anda dapat memaksa sambungan maju sebagai gantinya (perhatikan `-L`, bukan` -R`): +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 @@ -579,7 +579,7 @@ Lihat juga [Masalah #14]. 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: +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 From 85512b14670db9b41606ec9841b8755a86633536 Mon Sep 17 00:00:00 2001 From: Sean Wei Date: Mon, 25 Apr 2022 16:39:12 +0800 Subject: [PATCH 1194/2244] Fix typo in German README Replace "Wifi" with "Wi-Fi", as in English and other translations. PR #3217 Signed-off-by: Romain Vimont --- README.de.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.de.md b/README.de.md index 92d00df2..1299ae0e 100644 --- a/README.de.md +++ b/README.de.md @@ -375,7 +375,7 @@ Dies finden automatisch das Gerät und aktiviert den TCP/IP-Modus. Anschließend Alternativ kann die TCP/IP-Verbindung auch manuell per `adb` aktiviert werden: -1. Gerät mit demselben Wifi wie den Computer verbinden. +1. Gerät mit demselben Wi-Fi wie den Computer verbinden. 2. IP-Adresse des Gerätes herausfinden, entweder über Einstellungen → Über das Telefon → Status, oder indem dieser Befehl ausgeführt wird: ```bash From a90dfb46bc2266f93eae5585f84b6a9265dd05c2 Mon Sep 17 00:00:00 2001 From: Sean Wei Date: Mon, 25 Apr 2022 16:39:15 +0800 Subject: [PATCH 1195/2244] Fix GitHub case in BUILD Replace "Github" with "GitHub". PR #3218 Signed-off-by: Romain Vimont --- BUILD.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BUILD.md b/BUILD.md index 90a7d18e..bd007155 100644 --- a/BUILD.md +++ b/BUILD.md @@ -46,7 +46,7 @@ sudo ninja -Cbuild-auto uninstall ### `master` The `master` branch concerns the latest release, and is the home page of the -project on Github. +project on GitHub. ### `dev` From 326897a0d4ab3ea0cf741cd87f01d2703db38374 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 25 Apr 2022 18:33:08 +0200 Subject: [PATCH 1196/2244] Add missing mouse shortcuts in --help Document 4th-click and 5th-click shortcuts. Fixes #3122 --- app/src/cli.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index 5dda86e5..2983af73 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -577,7 +577,7 @@ static const struct sc_shortcut shortcuts[] = { .text = "Click on BACK", }, { - .shortcuts = { "MOD+s" }, + .shortcuts = { "MOD+s", "4th-click" }, .text = "Click on APP_SWITCH", }, { @@ -613,7 +613,7 @@ static const struct sc_shortcut shortcuts[] = { .text = "Rotate device screen", }, { - .shortcuts = { "MOD+n" }, + .shortcuts = { "MOD+n", "5th-click" }, .text = "Expand notification panel", }, { From 0fca2ad830dbcc24a9a38b974d922359be40b0d2 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 23 Apr 2022 15:08:30 +0200 Subject: [PATCH 1197/2244] Add option to not power on on start By default, on start, the device is powered on. To prevent this behavior, add a new option --no-power-on. Fixes #3148 PR #3210 --- README.md | 10 ++++++++++ app/data/bash-completion/scrcpy | 1 + app/data/zsh-completion/_scrcpy | 1 + app/scrcpy.1 | 4 ++++ app/src/cli.c | 9 +++++++++ app/src/options.c | 1 + app/src/options.h | 1 + app/src/scrcpy.c | 1 + app/src/server.c | 4 ++++ app/src/server.h | 1 + .../main/java/com/genymobile/scrcpy/Controller.java | 6 ++++-- .../src/main/java/com/genymobile/scrcpy/Options.java | 9 +++++++++ server/src/main/java/com/genymobile/scrcpy/Server.java | 6 +++++- 13 files changed, 51 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 614d1ed4..846b18ac 100644 --- a/README.md +++ b/README.md @@ -721,6 +721,16 @@ To turn the device screen off when closing scrcpy: scrcpy --power-off-on-close ``` +#### Power on on start + +By default, on start, the device is powered on. + +To prevent this behavior: + +```bash +scrcpy --no-power-on +``` + #### Show touches diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index 464bc532..3e75cbb0 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -29,6 +29,7 @@ _scrcpy() { -N --no-display --no-key-repeat --no-mipmaps + --no-power-on --otg -p --port= --power-off-on-close diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 77e6027a..097c80d6 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -35,6 +35,7 @@ arguments=( {-N,--no-display}'[Do not display device \(during screen recording or when V4L2 sink is enabled\)]' '--no-key-repeat[Do not forward repeated key events when a key is held down]' '--no-mipmaps[Disable the generation of mipmaps]' + '--no-power-on[Do not power on the device on start]' '--otg[Run in OTG mode \(simulating physical keyboard and mouse\)]' {-p,--port=}'[\[port\[\:port\]\] Set the TCP port \(range\) used by the client to listen]' '--power-off-on-close[Turn the device screen off when closing scrcpy]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index eb164475..7cb893b7 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -180,6 +180,10 @@ Do not forward repeated key events when a key is held down. .B \-\-no\-mipmaps If the renderer is OpenGL 3.0+ or OpenGL ES 2.0+, then mipmaps are automatically generated to improve downscaling quality. This option disables the generation of mipmaps. +.TP +.B \-\-no\-power\-on +Do not power on the device on start. + .TP .B \-\-otg Run in OTG mode: simulate physical keyboard and mouse, as if the computer keyboard and mouse were plugged directly to the device via an OTG cable. diff --git a/app/src/cli.c b/app/src/cli.c index 2983af73..538dd3e7 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -56,6 +56,7 @@ #define OPT_OTG 1036 #define OPT_NO_CLEANUP 1037 #define OPT_PRINT_FPS 1038 +#define OPT_NO_POWER_ON 1039 struct sc_option { char shortopt; @@ -302,6 +303,11 @@ static const struct sc_option options[] = { "mipmaps are automatically generated to improve downscaling " "quality. This option disables the generation of mipmaps.", }, + { + .longopt_id = OPT_NO_POWER_ON, + .longopt = "no-power-on", + .text = "Do not power on the device on start.", + }, { .longopt_id = OPT_OTG, .longopt = "otg", @@ -1598,6 +1604,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], case OPT_NO_CLEANUP: opts->cleanup = false; break; + case OPT_NO_POWER_ON: + opts->power_on = false; + break; case OPT_PRINT_FPS: opts->start_fps_counter = true; break; diff --git a/app/src/options.c b/app/src/options.c index 6651020f..8b2624d9 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -64,4 +64,5 @@ const struct scrcpy_options scrcpy_options_default = { .select_usb = false, .cleanup = true, .start_fps_counter = false, + .power_on = true, }; diff --git a/app/src/options.h b/app/src/options.h index f63e5c42..7e542c06 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -139,6 +139,7 @@ struct scrcpy_options { bool select_tcpip; bool cleanup; bool start_fps_counter; + bool power_on; }; extern const struct scrcpy_options scrcpy_options_default; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 8fbfe394..3588e9ae 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -324,6 +324,7 @@ scrcpy(struct scrcpy_options *options) { .tcpip = options->tcpip, .tcpip_dst = options->tcpip_dst, .cleanup = options->cleanup, + .power_on = options->power_on, }; static const struct sc_server_callbacks cbs = { diff --git a/app/src/server.c b/app/src/server.c index c02390a4..663ef18b 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -248,6 +248,10 @@ execute_server(struct sc_server *server, // By default, cleanup is true ADD_PARAM("cleanup=false"); } + if (!params->power_on) { + // By default, power_on is true + ADD_PARAM("power_on=false"); + } #undef ADD_PARAM diff --git a/app/src/server.h b/app/src/server.h index 5f630ca8..49ba83c1 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -47,6 +47,7 @@ struct sc_server_params { bool select_usb; bool select_tcpip; bool cleanup; + bool power_on; }; struct sc_server { diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index 481c512f..913371ee 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -22,6 +22,7 @@ public class Controller { private final DesktopConnection connection; private final DeviceMessageSender sender; private final boolean clipboardAutosync; + private final boolean powerOn; private final KeyCharacterMap charMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD); @@ -32,10 +33,11 @@ public class Controller { private boolean keepPowerModeOff; - public Controller(Device device, DesktopConnection connection, boolean clipboardAutosync) { + public Controller(Device device, DesktopConnection connection, boolean clipboardAutosync, boolean powerOn) { this.device = device; this.connection = connection; this.clipboardAutosync = clipboardAutosync; + this.powerOn = powerOn; initPointers(); sender = new DeviceMessageSender(connection); } @@ -56,7 +58,7 @@ public class Controller { public void control() throws IOException { // on start, power on the device - if (!Device.isScreenOn()) { + if (powerOn && !Device.isScreenOn()) { device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER, Device.INJECT_MODE_ASYNC); // dirty hack diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 4842b635..d1607c20 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -22,6 +22,7 @@ public class Options { private boolean clipboardAutosync = true; private boolean downsizeOnError = true; private boolean cleanup = true; + private boolean powerOn = true; // Options not used by the scrcpy client, but useful to use scrcpy-server directly private boolean sendDeviceMeta = true; // send device name and size @@ -164,6 +165,14 @@ public class Options { this.cleanup = cleanup; } + public boolean getPowerOn() { + return powerOn; + } + + public void setPowerOn(boolean powerOn) { + this.powerOn = powerOn; + } + public boolean getSendDeviceMeta() { return sendDeviceMeta; } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 60f485d8..1df91552 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -82,7 +82,7 @@ public final class Server { Thread controllerThread = null; Thread deviceMessageSenderThread = null; if (control) { - final Controller controller = new Controller(device, connection, options.getClipboardAutosync()); + final Controller controller = new Controller(device, connection, options.getClipboardAutosync(), options.getPowerOn()); // asynchronous controllerThread = startController(controller); @@ -248,6 +248,10 @@ public final class Server { boolean cleanup = Boolean.parseBoolean(value); options.setCleanup(cleanup); break; + case "power_on": + boolean powerOn = Boolean.parseBoolean(value); + options.setPowerOn(powerOn); + break; case "send_device_meta": boolean sendDeviceMeta = Boolean.parseBoolean(value); options.setSendDeviceMeta(sendDeviceMeta); From c6d97111097514cdf333689900abafd57ad354ec Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 28 Apr 2022 19:10:45 +0200 Subject: [PATCH 1198/2244] Create OTG window with HIGHDPI flag This will avoid poor quality with HiDPI displays. PR #3219 --- app/src/usb/screen_otg.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/usb/screen_otg.c b/app/src/usb/screen_otg.c index 561a84ca..93b0ba6e 100644 --- a/app/src/usb/screen_otg.c +++ b/app/src/usb/screen_otg.c @@ -72,7 +72,7 @@ sc_screen_otg_init(struct sc_screen_otg *screen, int width = 256; int height = 256; - uint32_t window_flags = 0; + uint32_t window_flags = SDL_WINDOW_ALLOW_HIGHDPI; if (params->always_on_top) { window_flags |= SDL_WINDOW_ALWAYS_ON_TOP; } From fc8942aa03dd18b20d6b2e450b13ba56d2c3489e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 25 Apr 2022 18:44:42 +0200 Subject: [PATCH 1199/2244] Apply requested window size in OTG mode Fixes #3099 PR #3219 --- app/src/usb/scrcpy_otg.c | 2 ++ app/src/usb/screen_otg.c | 9 +++++++-- app/src/usb/screen_otg.h | 2 ++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/app/src/usb/scrcpy_otg.c b/app/src/usb/scrcpy_otg.c index db5e64d8..052facff 100644 --- a/app/src/usb/scrcpy_otg.c +++ b/app/src/usb/scrcpy_otg.c @@ -162,6 +162,8 @@ scrcpy_otg(struct scrcpy_options *options) { .always_on_top = options->always_on_top, .window_x = options->window_x, .window_y = options->window_y, + .window_width = options->window_width, + .window_height = options->window_height, .window_borderless = options->window_borderless, }; diff --git a/app/src/usb/screen_otg.c b/app/src/usb/screen_otg.c index 93b0ba6e..450cbe1e 100644 --- a/app/src/usb/screen_otg.c +++ b/app/src/usb/screen_otg.c @@ -69,8 +69,8 @@ sc_screen_otg_init(struct sc_screen_otg *screen, ? params->window_x : (int) SDL_WINDOWPOS_UNDEFINED; int y = params->window_y != SC_WINDOW_POSITION_UNDEFINED ? params->window_y : (int) SDL_WINDOWPOS_UNDEFINED; - int width = 256; - int height = 256; + int width = params->window_width ? params->window_width : 256; + int height = params->window_height ? params->window_height : 256; uint32_t window_flags = SDL_WINDOW_ALLOW_HIGHDPI; if (params->always_on_top) { @@ -97,6 +97,11 @@ sc_screen_otg_init(struct sc_screen_otg *screen, if (icon) { SDL_SetWindowIcon(screen->window, icon); + if (!SDL_RenderSetLogicalSize(screen->renderer, icon->w, icon->h)) { + LOGW("Could not set renderer logical size: %s", SDL_GetError()); + // don't fail + } + screen->texture = SDL_CreateTextureFromSurface(screen->renderer, icon); scrcpy_icon_destroy(icon); if (!screen->texture) { diff --git a/app/src/usb/screen_otg.h b/app/src/usb/screen_otg.h index 0973ce59..a0acf40b 100644 --- a/app/src/usb/screen_otg.h +++ b/app/src/usb/screen_otg.h @@ -29,6 +29,8 @@ struct sc_screen_otg_params { bool always_on_top; int16_t window_x; // accepts SC_WINDOW_POSITION_UNDEFINED int16_t window_y; // accepts SC_WINDOW_POSITION_UNDEFINED + uint16_t window_width; + uint16_t window_height; bool window_borderless; }; From 436b368f9df4469274502f29626586b48db44a0c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 28 Apr 2022 19:11:42 +0200 Subject: [PATCH 1200/2244] Make OTG window resizable PR #3219 --- app/src/usb/screen_otg.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/usb/screen_otg.c b/app/src/usb/screen_otg.c index 450cbe1e..abdfc9a4 100644 --- a/app/src/usb/screen_otg.c +++ b/app/src/usb/screen_otg.c @@ -72,7 +72,8 @@ sc_screen_otg_init(struct sc_screen_otg *screen, int width = params->window_width ? params->window_width : 256; int height = params->window_height ? params->window_height : 256; - uint32_t window_flags = SDL_WINDOW_ALLOW_HIGHDPI; + uint32_t window_flags = SDL_WINDOW_ALLOW_HIGHDPI + | SDL_WINDOW_RESIZABLE; if (params->always_on_top) { window_flags |= SDL_WINDOW_ALWAYS_ON_TOP; } From 854a56e58893da352410d6e01708704f550fd6e4 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 28 Apr 2022 19:12:37 +0200 Subject: [PATCH 1201/2244] Enable linear filtering in OTG mode This improves the icon quality with non-standard window size. PR #3219 --- app/src/usb/scrcpy_otg.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/usb/scrcpy_otg.c b/app/src/usb/scrcpy_otg.c index 052facff..ebcfa36f 100644 --- a/app/src/usb/scrcpy_otg.c +++ b/app/src/usb/scrcpy_otg.c @@ -55,6 +55,10 @@ scrcpy_otg(struct scrcpy_options *options) { const char *serial = options->serial; + if (!SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1")) { + LOGW("Could not enable linear filtering"); + } + // Minimal SDL initialization if (SDL_Init(SDL_INIT_EVENTS)) { LOGE("Could not initialize SDL: %s", SDL_GetError()); From 91706ae3d079abafa009c574845ddb12377588aa Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 28 Apr 2022 19:23:59 +0200 Subject: [PATCH 1202/2244] Upgrade SDL (2.0.22) for Windows Include the latest version of SDL in Windows releases. --- app/prebuilt-deps/prepare-sdl.sh | 6 +++--- cross_win32.txt | 2 +- cross_win64.txt | 2 +- release.mk | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/prebuilt-deps/prepare-sdl.sh b/app/prebuilt-deps/prepare-sdl.sh index c414c854..0b41fe5c 100755 --- a/app/prebuilt-deps/prepare-sdl.sh +++ b/app/prebuilt-deps/prepare-sdl.sh @@ -6,10 +6,10 @@ cd "$DIR" mkdir -p "$PREBUILT_DATA_DIR" cd "$PREBUILT_DATA_DIR" -DEP_DIR=SDL2-2.0.20 +DEP_DIR=SDL2-2.0.22 -FILENAME=SDL2-devel-2.0.20-mingw.tar.gz -SHA256SUM=38094d82a857d6c62352e5c5cdec74948c5b4d25c59cbd298d6d233568976bd1 +FILENAME=SDL2-devel-2.0.22-mingw.tar.gz +SHA256SUM=0e91e35973366aa1e6f81ee368924d9b4f93f9da4d2f2a89ec80b06eadcf23d1 if [[ -d "$DEP_DIR" ]] then diff --git a/cross_win32.txt b/cross_win32.txt index a2341f21..d1d5d11b 100644 --- a/cross_win32.txt +++ b/cross_win32.txt @@ -20,6 +20,6 @@ ffmpeg_avcodec = 'avcodec-58' ffmpeg_avformat = 'avformat-58' ffmpeg_avutil = 'avutil-56' prebuilt_ffmpeg = 'ffmpeg-win32-4.3.1' -prebuilt_sdl2 = 'SDL2-2.0.20/i686-w64-mingw32' +prebuilt_sdl2 = 'SDL2-2.0.22/i686-w64-mingw32' prebuilt_libusb_root = 'libusb-1.0.26' prebuilt_libusb = prebuilt_libusb_root + '/MinGW-Win32' diff --git a/cross_win64.txt b/cross_win64.txt index 0404aedc..f673c530 100644 --- a/cross_win64.txt +++ b/cross_win64.txt @@ -20,6 +20,6 @@ ffmpeg_avcodec = 'avcodec-59' ffmpeg_avformat = 'avformat-59' ffmpeg_avutil = 'avutil-57' prebuilt_ffmpeg = 'ffmpeg-win64-5.0' -prebuilt_sdl2 = 'SDL2-2.0.20/x86_64-w64-mingw32' +prebuilt_sdl2 = 'SDL2-2.0.22/x86_64-w64-mingw32' prebuilt_libusb_root = 'libusb-1.0.26' prebuilt_libusb = prebuilt_libusb_root + '/MinGW-x64' diff --git a/release.mk b/release.mk index 1c550afb..3e3d14db 100644 --- a/release.mk +++ b/release.mk @@ -108,7 +108,7 @@ dist-win32: build-server build-win32 cp app/prebuilt-deps/data/platform-tools-31.0.3/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-31.0.3/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-31.0.3/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/SDL2-2.0.20/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/SDL2-2.0.22/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/libusb-1.0.26/MinGW-Win32/msys-usb-1.0.dll "$(DIST)/$(WIN32_TARGET_DIR)/" dist-win64: build-server build-win64 @@ -127,7 +127,7 @@ dist-win64: build-server build-win64 cp app/prebuilt-deps/data/platform-tools-31.0.3/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-31.0.3/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-31.0.3/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/SDL2-2.0.20/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/SDL2-2.0.22/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/libusb-1.0.26/MinGW-x64/msys-usb-1.0.dll "$(DIST)/$(WIN64_TARGET_DIR)/" zip-win32: dist-win32 From f9e3275d4e77394e19ac789ea46e16a244ec4dfe Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 28 Apr 2022 19:32:14 +0200 Subject: [PATCH 1203/2244] Upgrade FFmpeg (5.0.1) for Windows 64-bit Use the latest version of FFmpeg in Windows 64-bit releases. --- app/prebuilt-deps/prepare-ffmpeg-win64.sh | 11 ++++++----- cross_win64.txt | 2 +- release.mk | 10 +++++----- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/app/prebuilt-deps/prepare-ffmpeg-win64.sh b/app/prebuilt-deps/prepare-ffmpeg-win64.sh index a62eb261..8e6c2440 100755 --- a/app/prebuilt-deps/prepare-ffmpeg-win64.sh +++ b/app/prebuilt-deps/prepare-ffmpeg-win64.sh @@ -6,10 +6,11 @@ cd "$DIR" mkdir -p "$PREBUILT_DATA_DIR" cd "$PREBUILT_DATA_DIR" -DEP_DIR=ffmpeg-win64-5.0 +VERSION=5.0.1 +DEP_DIR=ffmpeg-win64-$VERSION -FILENAME=ffmpeg-5.0-full_build-shared.7z -SHA256SUM=e5900f6cecd4c438d398bd2fc308736c10b857cd8dd61c11bcfb05bff5d1211a +FILENAME=ffmpeg-$VERSION-full_build-shared.7z +SHA256SUM=ded28435b6f04b74f5ef5a6a13761233bce9e8e9f8ecb0eabe936fd36a778b0c if [[ -d "$DEP_DIR" ]] then @@ -17,13 +18,13 @@ then exit 0 fi -get_file "https://github.com/GyanD/codexffmpeg/releases/download/5.0/$FILENAME" \ +get_file "https://github.com/GyanD/codexffmpeg/releases/download/$VERSION/$FILENAME" \ "$FILENAME" "$SHA256SUM" mkdir "$DEP_DIR" cd "$DEP_DIR" -ZIP_PREFIX=ffmpeg-5.0-full_build-shared +ZIP_PREFIX=ffmpeg-$VERSION-full_build-shared 7z x "../$FILENAME" \ "$ZIP_PREFIX"/bin/avutil-57.dll \ "$ZIP_PREFIX"/bin/avcodec-59.dll \ diff --git a/cross_win64.txt b/cross_win64.txt index f673c530..cc9e4a0b 100644 --- a/cross_win64.txt +++ b/cross_win64.txt @@ -19,7 +19,7 @@ endian = 'little' ffmpeg_avcodec = 'avcodec-59' ffmpeg_avformat = 'avformat-59' ffmpeg_avutil = 'avutil-57' -prebuilt_ffmpeg = 'ffmpeg-win64-5.0' +prebuilt_ffmpeg = 'ffmpeg-win64-5.0.1' prebuilt_sdl2 = 'SDL2-2.0.22/x86_64-w64-mingw32' prebuilt_libusb_root = 'libusb-1.0.26' prebuilt_libusb = prebuilt_libusb_root + '/MinGW-x64' diff --git a/release.mk b/release.mk index 3e3d14db..e8c5f177 100644 --- a/release.mk +++ b/release.mk @@ -119,11 +119,11 @@ dist-win64: build-server build-win64 cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN64_TARGET_DIR)" cp app/data/icon.png "$(DIST)/$(WIN64_TARGET_DIR)" cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN64_TARGET_DIR)" - cp app/prebuilt-deps/data/ffmpeg-win64-5.0/bin/avutil-57.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-win64-5.0/bin/avcodec-59.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-win64-5.0/bin/avformat-59.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-win64-5.0/bin/swresample-4.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-win64-5.0/bin/swscale-6.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-win64-5.0.1/bin/avutil-57.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-win64-5.0.1/bin/avcodec-59.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-win64-5.0.1/bin/avformat-59.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-win64-5.0.1/bin/swresample-4.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-win64-5.0.1/bin/swscale-6.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-31.0.3/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-31.0.3/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-31.0.3/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" From 349dcd8e7b501d39ab2b854378a1974e38f3dac2 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 28 Apr 2022 19:49:34 +0200 Subject: [PATCH 1204/2244] Update installed files list in BUILD documentation --- BUILD.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/BUILD.md b/BUILD.md index 90a7d18e..e69c180b 100644 --- a/BUILD.md +++ b/BUILD.md @@ -305,11 +305,14 @@ After a successful build, you can install _scrcpy_ on the system: sudo ninja -Cx install # without sudo on Windows ``` -This installs three files: +This installs several files: - - `/usr/local/bin/scrcpy` - - `/usr/local/share/scrcpy/scrcpy-server` - - `/usr/local/share/man/man1/scrcpy.1` + - `/usr/local/bin/scrcpy` (main app) + - `/usr/local/share/scrcpy/scrcpy-server` (server to push to the device) + - `/usr/local/share/man/man1/scrcpy.1` (manpage) + - `/usr/local/share/icons/hicolor/256x256/apps/icon.png` (app icon) + - `/usr/local/share/zsh/site-functions/_scrcpy` (zsh completion) + - `/usr/local/share/bash-completion/completions/scrcpy` (bash completion) You can then [run](README.md#run) _scrcpy_. From 471a360099f73d6a3fa3ea5abb7d5b8f59b23415 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 28 Apr 2022 19:50:07 +0200 Subject: [PATCH 1205/2244] Use quotes for commands in documentation --- BUILD.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BUILD.md b/BUILD.md index e69c180b..f079518e 100644 --- a/BUILD.md +++ b/BUILD.md @@ -314,7 +314,7 @@ This installs several files: - `/usr/local/share/zsh/site-functions/_scrcpy` (zsh completion) - `/usr/local/share/bash-completion/completions/scrcpy` (bash completion) -You can then [run](README.md#run) _scrcpy_. +You can then [run](README.md#run) `scrcpy`. ### Uninstall From 05d84084efb07d07673123427c31331ca0de4f2c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 28 Apr 2022 20:18:13 +0200 Subject: [PATCH 1206/2244] Fix release script for platform-tools 33.0.1 These paths were not updated by commit b8d78743f7fbe7aefed957f41e68efc4791573ba. --- release.mk | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/release.mk b/release.mk index e8c5f177..94a9680e 100644 --- a/release.mk +++ b/release.mk @@ -105,9 +105,9 @@ dist-win32: build-server build-win32 cp app/prebuilt-deps/data/ffmpeg-win32-4.3.1/bin/avformat-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/ffmpeg-win32-4.3.1/bin/swresample-3.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/ffmpeg-win32-4.3.1/bin/swscale-5.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/platform-tools-31.0.3/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/platform-tools-31.0.3/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/platform-tools-31.0.3/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/platform-tools-33.0.1/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/platform-tools-33.0.1/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/platform-tools-33.0.1/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/SDL2-2.0.22/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/libusb-1.0.26/MinGW-Win32/msys-usb-1.0.dll "$(DIST)/$(WIN32_TARGET_DIR)/" @@ -124,9 +124,9 @@ dist-win64: build-server build-win64 cp app/prebuilt-deps/data/ffmpeg-win64-5.0.1/bin/avformat-59.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/ffmpeg-win64-5.0.1/bin/swresample-4.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/ffmpeg-win64-5.0.1/bin/swscale-6.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/platform-tools-31.0.3/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/platform-tools-31.0.3/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/platform-tools-31.0.3/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/platform-tools-33.0.1/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/platform-tools-33.0.1/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/platform-tools-33.0.1/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/SDL2-2.0.22/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/libusb-1.0.26/MinGW-x64/msys-usb-1.0.dll "$(DIST)/$(WIN64_TARGET_DIR)/" From 76b3fcf9863ce42de4057b583f9261104c78cda1 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 28 Apr 2022 20:40:20 +0200 Subject: [PATCH 1207/2244] Fix inverted check SDL_RenderSetLogicalSize() returns 0 on success. Refs fc8942aa03dd18b20d6b2e450b13ba56d2c3489e --- app/src/usb/screen_otg.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/usb/screen_otg.c b/app/src/usb/screen_otg.c index abdfc9a4..a8f72296 100644 --- a/app/src/usb/screen_otg.c +++ b/app/src/usb/screen_otg.c @@ -98,7 +98,7 @@ sc_screen_otg_init(struct sc_screen_otg *screen, if (icon) { SDL_SetWindowIcon(screen->window, icon); - if (!SDL_RenderSetLogicalSize(screen->renderer, icon->w, icon->h)) { + if (SDL_RenderSetLogicalSize(screen->renderer, icon->w, icon->h)) { LOGW("Could not set renderer logical size: %s", SDL_GetError()); // don't fail } From 2f038c834a5f891d20095ed1fe7c44a05d6760e0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 28 Apr 2022 20:41:51 +0200 Subject: [PATCH 1208/2244] Revert "Make OTG window resizable" On Windows and macOS, resizing blocks the event loop. Handling it properly would require the same workaround as done in screen.c. This reverts commit 436b368f9df4469274502f29626586b48db44a0c. --- app/src/usb/screen_otg.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/usb/screen_otg.c b/app/src/usb/screen_otg.c index a8f72296..e1d5cb01 100644 --- a/app/src/usb/screen_otg.c +++ b/app/src/usb/screen_otg.c @@ -72,8 +72,7 @@ sc_screen_otg_init(struct sc_screen_otg *screen, int width = params->window_width ? params->window_width : 256; int height = params->window_height ? params->window_height : 256; - uint32_t window_flags = SDL_WINDOW_ALLOW_HIGHDPI - | SDL_WINDOW_RESIZABLE; + uint32_t window_flags = SDL_WINDOW_ALLOW_HIGHDPI; if (params->always_on_top) { window_flags |= SDL_WINDOW_ALWAYS_ON_TOP; } From ef13d394fd83a2c534d86f2dbc188f4324a6ee34 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 28 Apr 2022 20:09:48 +0200 Subject: [PATCH 1209/2244] Bump version to 1.24 --- app/scrcpy-windows.rc | 2 +- meson.build | 2 +- server/build.gradle | 4 ++-- server/build_without_gradle.sh | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/scrcpy-windows.rc b/app/scrcpy-windows.rc index 41101e70..6c731003 100644 --- a/app/scrcpy-windows.rc +++ b/app/scrcpy-windows.rc @@ -13,7 +13,7 @@ BEGIN VALUE "LegalCopyright", "Romain Vimont, Genymobile" VALUE "OriginalFilename", "scrcpy.exe" VALUE "ProductName", "scrcpy" - VALUE "ProductVersion", "1.23" + VALUE "ProductVersion", "1.24" END END BLOCK "VarFileInfo" diff --git a/meson.build b/meson.build index e7270b5a..bfca6134 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('scrcpy', 'c', - version: '1.23', + version: '1.24', meson_version: '>= 0.48', default_options: [ 'c_std=c11', diff --git a/server/build.gradle b/server/build.gradle index f3eb6d64..dbc8261f 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -6,8 +6,8 @@ android { applicationId "com.genymobile.scrcpy" minSdkVersion 21 targetSdkVersion 31 - versionCode 12300 - versionName "1.23" + versionCode 12400 + versionName "1.24" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index b2bafa3f..c881e38a 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.23 +SCRCPY_VERSION_NAME=1.24 PLATFORM=${ANDROID_PLATFORM:-31} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-31.0.0} From 3a99e129e628911dde73f486aece1ca3ebe06ead Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 28 Apr 2022 21:03:03 +0200 Subject: [PATCH 1210/2244] Update links to v1.24 --- BUILD.md | 6 +++--- README.md | 8 ++++---- install_release.sh | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/BUILD.md b/BUILD.md index 84d1acac..e68e8834 100644 --- a/BUILD.md +++ b/BUILD.md @@ -272,10 +272,10 @@ install` must be run as root)._ #### Option 2: Use prebuilt server - - [`scrcpy-server-v1.23`][direct-scrcpy-server] - _(SHA-256: 2a913fd47478c0b306fca507cb0beb625e49a19ff9fc7ab904e36ef5b9fe7e68)_ + - [`scrcpy-server-v1.24`][direct-scrcpy-server] + _(SHA-256: ae74a81ea79c0dc7250e586627c278c0a9a8c5de46c9fb5c38c167fb1a36f056)_ -[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.23/scrcpy-server-v1.23 +[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.24/scrcpy-server-v1.24 Download the prebuilt server somewhere, and specify its path during the Meson configuration: diff --git a/README.md b/README.md index 375c465d..ba5bdefd 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# scrcpy (v1.23) +# scrcpy (v1.24) scrcpy @@ -106,10 +106,10 @@ process][BUILD_simple]). For Windows, a prebuilt archive with all the dependencies (including `adb`) is available: - - [`scrcpy-win64-v1.23.zip`][direct-win64] - _(SHA-256: d2f601b1d0157faf65153d8a093d827fd65aec5d5842d677ac86fb2b5b7704cc)_ + - [`scrcpy-win64-v1.24.zip`][direct-win64] + _(SHA-256: 6ccb64cba0a3e75715e85a188daeb4f306a1985f8ce123eba92ba74fc9b27367)_ -[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.23/scrcpy-win64-v1.23.zip +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.24/scrcpy-win64-v1.24.zip It is also available in [Chocolatey]: diff --git a/install_release.sh b/install_release.sh index 9b1ed8e7..88262f8e 100755 --- a/install_release.sh +++ b/install_release.sh @@ -2,8 +2,8 @@ set -e BUILDDIR=build-auto -PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v1.23/scrcpy-server-v1.23 -PREBUILT_SERVER_SHA256=2a913fd47478c0b306fca507cb0beb625e49a19ff9fc7ab904e36ef5b9fe7e68 +PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v1.24/scrcpy-server-v1.24 +PREBUILT_SERVER_SHA256=ae74a81ea79c0dc7250e586627c278c0a9a8c5de46c9fb5c38c167fb1a36f056 echo "[scrcpy] Downloading prebuilt server..." wget "$PREBUILT_SERVER_URL" -O scrcpy-server From faf4535487926853250ebe842c76a40e7b5f8ceb Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 28 Apr 2022 21:25:40 +0200 Subject: [PATCH 1211/2244] Reduce SHA-256 size in README and BUILD This avoids breaking the page layout on GitHub. --- BUILD.md | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/BUILD.md b/BUILD.md index e68e8834..1d5d970b 100644 --- a/BUILD.md +++ b/BUILD.md @@ -273,7 +273,7 @@ install` must be run as root)._ #### Option 2: Use prebuilt server - [`scrcpy-server-v1.24`][direct-scrcpy-server] - _(SHA-256: ae74a81ea79c0dc7250e586627c278c0a9a8c5de46c9fb5c38c167fb1a36f056)_ + SHA-256: `ae74a81ea79c0dc7250e586627c278c0a9a8c5de46c9fb5c38c167fb1a36f056` [direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.24/scrcpy-server-v1.24 diff --git a/README.md b/README.md index ba5bdefd..20ad0f9c 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ For Windows, a prebuilt archive with all the dependencies (including `adb`) is available: - [`scrcpy-win64-v1.24.zip`][direct-win64] - _(SHA-256: 6ccb64cba0a3e75715e85a188daeb4f306a1985f8ce123eba92ba74fc9b27367)_ + SHA-256: `6ccb64cba0a3e75715e85a188daeb4f306a1985f8ce123eba92ba74fc9b27367` [direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.24/scrcpy-win64-v1.24.zip From 69fb5f6ee118f1e732c58f3b190e5cb4f1edc575 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 24 May 2022 21:03:42 +0200 Subject: [PATCH 1212/2244] Fix function declarations Add missing void in function parameters list. --- app/tests/test_adb_parser.c | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/app/tests/test_adb_parser.c b/app/tests/test_adb_parser.c index 1a127632..43abfe0a 100644 --- a/app/tests/test_adb_parser.c +++ b/app/tests/test_adb_parser.c @@ -5,7 +5,7 @@ #include "adb/adb_device.h" #include "adb/adb_parser.h" -static void test_adb_devices() { +static void test_adb_devices(void) { char output[] = "List of devices attached\n" "0123456789abcdef device usb:2-1 product:MyProduct model:MyModel " @@ -31,7 +31,7 @@ static void test_adb_devices() { sc_adb_devices_destroy(&vec); } -static void test_adb_devices_cr() { +static void test_adb_devices_cr(void) { char output[] = "List of devices attached\r\n" "0123456789abcdef device usb:2-1 product:MyProduct model:MyModel " @@ -57,7 +57,7 @@ static void test_adb_devices_cr() { sc_adb_devices_destroy(&vec); } -static void test_adb_devices_daemon_start() { +static void test_adb_devices_daemon_start(void) { char output[] = "* daemon not running; starting now at tcp:5037\n" "* daemon started successfully\n" @@ -78,7 +78,7 @@ static void test_adb_devices_daemon_start() { sc_adb_devices_destroy(&vec); } -static void test_adb_devices_daemon_start_mixed() { +static void test_adb_devices_daemon_start_mixed(void) { char output[] = "List of devices attached\n" "adb server version (41) doesn't match this client (39); killing...\n" @@ -105,7 +105,7 @@ static void test_adb_devices_daemon_start_mixed() { sc_adb_devices_destroy(&vec); } -static void test_adb_devices_without_eol() { +static void test_adb_devices_without_eol(void) { char output[] = "List of devices attached\n" "0123456789abcdef device usb:2-1 product:MyProduct model:MyModel " @@ -124,7 +124,7 @@ static void test_adb_devices_without_eol() { sc_adb_devices_destroy(&vec); } -static void test_adb_devices_without_header() { +static void test_adb_devices_without_header(void) { char output[] = "0123456789abcdef device usb:2-1 product:MyProduct model:MyModel " "device:MyDevice transport_id:1\n"; @@ -134,7 +134,7 @@ static void test_adb_devices_without_header() { assert(!ok); } -static void test_adb_devices_corrupted() { +static void test_adb_devices_corrupted(void) { char output[] = "List of devices attached\n" "corrupted_garbage\n"; @@ -145,7 +145,7 @@ static void test_adb_devices_corrupted() { assert(vec.size == 0); } -static void test_adb_devices_spaces() { +static void test_adb_devices_spaces(void) { char output[] = "List of devices attached\n" "0123456789abcdef unauthorized usb:1-4 transport_id:3\n"; @@ -163,7 +163,7 @@ static void test_adb_devices_spaces() { sc_adb_devices_destroy(&vec); } -static void test_get_ip_single_line() { +static void test_get_ip_single_line(void) { char ip_route[] = "192.168.1.0/24 dev wlan0 proto kernel scope link src " "192.168.12.34\r\r\n"; @@ -173,7 +173,7 @@ static void test_get_ip_single_line() { free(ip); } -static void test_get_ip_single_line_without_eol() { +static void test_get_ip_single_line_without_eol(void) { char ip_route[] = "192.168.1.0/24 dev wlan0 proto kernel scope link src " "192.168.12.34"; @@ -183,7 +183,7 @@ static void test_get_ip_single_line_without_eol() { free(ip); } -static void test_get_ip_single_line_with_trailing_space() { +static void test_get_ip_single_line_with_trailing_space(void) { char ip_route[] = "192.168.1.0/24 dev wlan0 proto kernel scope link src " "192.168.12.34 \n"; @@ -193,7 +193,7 @@ static void test_get_ip_single_line_with_trailing_space() { free(ip); } -static void test_get_ip_multiline_first_ok() { +static void test_get_ip_multiline_first_ok(void) { char ip_route[] = "192.168.1.0/24 dev wlan0 proto kernel scope link src " "192.168.1.2\r\n" "10.0.0.0/24 dev rmnet proto kernel scope link src " @@ -205,7 +205,7 @@ static void test_get_ip_multiline_first_ok() { free(ip); } -static void test_get_ip_multiline_second_ok() { +static void test_get_ip_multiline_second_ok(void) { char ip_route[] = "10.0.0.0/24 dev rmnet proto kernel scope link src " "10.0.0.3\r\n" "192.168.1.0/24 dev wlan0 proto kernel scope link src " @@ -217,7 +217,7 @@ static void test_get_ip_multiline_second_ok() { free(ip); } -static void test_get_ip_no_wlan() { +static void test_get_ip_no_wlan(void) { char ip_route[] = "192.168.1.0/24 dev rmnet proto kernel scope link src " "192.168.12.34\r\r\n"; @@ -225,7 +225,7 @@ static void test_get_ip_no_wlan() { assert(!ip); } -static void test_get_ip_no_wlan_without_eol() { +static void test_get_ip_no_wlan_without_eol(void) { char ip_route[] = "192.168.1.0/24 dev rmnet proto kernel scope link src " "192.168.12.34"; @@ -233,7 +233,7 @@ static void test_get_ip_no_wlan_without_eol() { assert(!ip); } -static void test_get_ip_truncated() { +static void test_get_ip_truncated(void) { char ip_route[] = "192.168.1.0/24 dev rmnet proto kernel scope link src " "\n"; From 55e65fa270643519d55d12d73d628d02e73de464 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 24 May 2022 21:04:10 +0200 Subject: [PATCH 1213/2244] Add missing return 0 in tests --- app/tests/test_adb_parser.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/tests/test_adb_parser.c b/app/tests/test_adb_parser.c index 43abfe0a..6506122f 100644 --- a/app/tests/test_adb_parser.c +++ b/app/tests/test_adb_parser.c @@ -262,4 +262,6 @@ int main(int argc, char *argv[]) { test_get_ip_no_wlan(); test_get_ip_no_wlan_without_eol(); test_get_ip_truncated(); + + return 0; } From b1d8c7278054f33fffb57b163a1bf07aaba53305 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 24 May 2022 21:20:27 +0200 Subject: [PATCH 1214/2244] Rename function to simplify For consistency with sc_adb_parse_device(), do not include "from_output" in the function name. --- app/src/adb/adb.c | 2 +- app/src/adb/adb_parser.c | 2 +- app/src/adb/adb_parser.h | 2 +- app/tests/test_adb_parser.c | 16 ++++++++-------- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/app/src/adb/adb.c b/app/src/adb/adb.c index 344f7fcc..8b92a2b0 100644 --- a/app/src/adb/adb.c +++ b/app/src/adb/adb.c @@ -710,5 +710,5 @@ sc_adb_get_device_ip(struct sc_intr *intr, const char *serial, unsigned flags) { // It is parsed as a NUL-terminated string buf[r] = '\0'; - return sc_adb_parse_device_ip_from_output(buf); + return sc_adb_parse_device_ip(buf); } diff --git a/app/src/adb/adb_parser.c b/app/src/adb/adb_parser.c index 933eafbb..ab121347 100644 --- a/app/src/adb/adb_parser.c +++ b/app/src/adb/adb_parser.c @@ -199,7 +199,7 @@ sc_adb_parse_device_ip_from_line(char *line) { } char * -sc_adb_parse_device_ip_from_output(char *str) { +sc_adb_parse_device_ip(char *str) { size_t idx_line = 0; while (str[idx_line] != '\0') { char *line = &str[idx_line]; diff --git a/app/src/adb/adb_parser.h b/app/src/adb/adb_parser.h index e0cc389b..f20349f6 100644 --- a/app/src/adb/adb_parser.h +++ b/app/src/adb/adb_parser.h @@ -25,6 +25,6 @@ sc_adb_parse_devices(char *str, struct sc_vec_adb_devices *out_vec); * Warning: this function modifies the buffer for optimization purposes. */ char * -sc_adb_parse_device_ip_from_output(char *str); +sc_adb_parse_device_ip(char *str); #endif diff --git a/app/tests/test_adb_parser.c b/app/tests/test_adb_parser.c index 6506122f..d95e7ef2 100644 --- a/app/tests/test_adb_parser.c +++ b/app/tests/test_adb_parser.c @@ -167,7 +167,7 @@ static void test_get_ip_single_line(void) { char ip_route[] = "192.168.1.0/24 dev wlan0 proto kernel scope link src " "192.168.12.34\r\r\n"; - char *ip = sc_adb_parse_device_ip_from_output(ip_route); + char *ip = sc_adb_parse_device_ip(ip_route); assert(ip); assert(!strcmp(ip, "192.168.12.34")); free(ip); @@ -177,7 +177,7 @@ static void test_get_ip_single_line_without_eol(void) { char ip_route[] = "192.168.1.0/24 dev wlan0 proto kernel scope link src " "192.168.12.34"; - char *ip = sc_adb_parse_device_ip_from_output(ip_route); + char *ip = sc_adb_parse_device_ip(ip_route); assert(ip); assert(!strcmp(ip, "192.168.12.34")); free(ip); @@ -187,7 +187,7 @@ static void test_get_ip_single_line_with_trailing_space(void) { char ip_route[] = "192.168.1.0/24 dev wlan0 proto kernel scope link src " "192.168.12.34 \n"; - char *ip = sc_adb_parse_device_ip_from_output(ip_route); + char *ip = sc_adb_parse_device_ip(ip_route); assert(ip); assert(!strcmp(ip, "192.168.12.34")); free(ip); @@ -199,7 +199,7 @@ static void test_get_ip_multiline_first_ok(void) { "10.0.0.0/24 dev rmnet proto kernel scope link src " "10.0.0.2\r\n"; - char *ip = sc_adb_parse_device_ip_from_output(ip_route); + char *ip = sc_adb_parse_device_ip(ip_route); assert(ip); assert(!strcmp(ip, "192.168.1.2")); free(ip); @@ -211,7 +211,7 @@ static void test_get_ip_multiline_second_ok(void) { "192.168.1.0/24 dev wlan0 proto kernel scope link src " "192.168.1.3\r\n"; - char *ip = sc_adb_parse_device_ip_from_output(ip_route); + char *ip = sc_adb_parse_device_ip(ip_route); assert(ip); assert(!strcmp(ip, "192.168.1.3")); free(ip); @@ -221,7 +221,7 @@ static void test_get_ip_no_wlan(void) { char ip_route[] = "192.168.1.0/24 dev rmnet proto kernel scope link src " "192.168.12.34\r\r\n"; - char *ip = sc_adb_parse_device_ip_from_output(ip_route); + char *ip = sc_adb_parse_device_ip(ip_route); assert(!ip); } @@ -229,7 +229,7 @@ static void test_get_ip_no_wlan_without_eol(void) { char ip_route[] = "192.168.1.0/24 dev rmnet proto kernel scope link src " "192.168.12.34"; - char *ip = sc_adb_parse_device_ip_from_output(ip_route); + char *ip = sc_adb_parse_device_ip(ip_route); assert(!ip); } @@ -237,7 +237,7 @@ static void test_get_ip_truncated(void) { char ip_route[] = "192.168.1.0/24 dev rmnet proto kernel scope link src " "\n"; - char *ip = sc_adb_parse_device_ip_from_output(ip_route); + char *ip = sc_adb_parse_device_ip(ip_route); assert(!ip); } From af4b7855e115878b7b87c7fe2e719d62c07af40d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 9 Jun 2022 15:01:03 +0200 Subject: [PATCH 1215/2244] Remove unused stream.h The file was not removed by 7dec225cebbf208cf1d607bd45fb49e3a9fa1167. --- app/src/stream.h | 51 ------------------------------------------------ 1 file changed, 51 deletions(-) delete mode 100644 app/src/stream.h diff --git a/app/src/stream.h b/app/src/stream.h deleted file mode 100644 index bdcefe39..00000000 --- a/app/src/stream.h +++ /dev/null @@ -1,51 +0,0 @@ -#ifndef STREAM_H -#define STREAM_H - -#include "common.h" - -#include -#include -#include -#include - -#include "trait/packet_sink.h" -#include "util/net.h" -#include "util/thread.h" - -#define STREAM_MAX_SINKS 2 - -struct stream { - sc_socket socket; - sc_thread thread; - - struct sc_packet_sink *sinks[STREAM_MAX_SINKS]; - unsigned sink_count; - - AVCodecContext *codec_ctx; - AVCodecParserContext *parser; - // successive packets may need to be concatenated, until a non-config - // packet is available - AVPacket *pending; - - const struct stream_callbacks *cbs; - void *cbs_userdata; -}; - -struct stream_callbacks { - void (*on_eos)(struct stream *stream, void *userdata); -}; - -void -stream_init(struct stream *stream, sc_socket socket, - const struct stream_callbacks *cbs, void *cbs_userdata); - -void -stream_add_sink(struct stream *stream, struct sc_packet_sink *sink); - -bool -stream_start(struct stream *stream); - -void -stream_join(struct stream *stream); - -#endif From a83c3e30f3afbe1e0008dc50f769bb06fcdf0a87 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 17 Jun 2022 08:36:58 +0200 Subject: [PATCH 1216/2244] Fix environment variable configuration in FAQ In bash, the variable is set using "export". --- FAQ.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FAQ.md b/FAQ.md index 400c4014..0cebeeb7 100644 --- a/FAQ.md +++ b/FAQ.md @@ -103,7 +103,7 @@ 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 +export ADB=/path/to/your/adb scrcpy ``` From ed84e18b1ae3e51d368f8c7bc88ba4db088e6855 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 17 Jun 2022 08:39:40 +0200 Subject: [PATCH 1217/2244] Document envvars for all platforms Document how to set environment variables from the terminal for bash, cmd and PowerShell. --- FAQ.md | 13 +++++++++++++ README.md | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/FAQ.md b/FAQ.md index 0cebeeb7..f6a8f226 100644 --- a/FAQ.md +++ b/FAQ.md @@ -103,10 +103,23 @@ 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 +# in bash export ADB=/path/to/your/adb scrcpy ``` +```cmd +:: in cmd +set ADB=C:\path\to\your\adb.exe +scrcpy +``` + +```powershell +# in PowerShell +$env:ADB = 'C:\path\to\your\adb.exe' +scrcpy +``` + ### Device disconnected diff --git a/README.md b/README.md index 20ad0f9c..db531c61 100644 --- a/README.md +++ b/README.md @@ -505,10 +505,23 @@ Suppose that this server is accessible at 192.168.1.2. Then, from another terminal, run `scrcpy`: ```bash +# in bash export ADB_SERVER_SOCKET=tcp:192.168.1.2:5037 scrcpy --tunnel-host=192.168.1.2 ``` +```cmd +:: in cmd +set ADB_SERVER_SOCKET=tcp:192.168.1.2:5037 +scrcpy --tunnel-host=192.168.1.2 +``` + +```powershell +# in PowerShell +$env:ADB_SERVER_SOCKET = 'tcp:192.168.1.2:5037' +scrcpy --tunnel-host=192.168.1.2 +``` + By default, `scrcpy` uses the local port used for `adb forward` tunnel establishment (typically `27183`, see `--port`). It is also possible to force a different tunnel port (it may be useful in more complex situations, when more @@ -542,10 +555,23 @@ ssh -CN -L5038:localhost:5037 -R27183:localhost:27183 your_remote_computer From another terminal, run `scrcpy`: ```bash +# in bash export ADB_SERVER_SOCKET=tcp:localhost:5038 scrcpy ``` +```cmd +:: in cmd +set ADB_SERVER_SOCKET=tcp:localhost:5038 +scrcpy +``` + +```powershell +# in PowerShell +$env:ADB_SERVER_SOCKET = 'tcp:localhost:5038' +scrcpy +``` + To avoid enabling remote port forwarding, you could force a forward connection instead (notice the `-L` instead of `-R`): @@ -559,10 +585,23 @@ ssh -CN -L5038:localhost:5037 -L27183:localhost:27183 your_remote_computer From another terminal, run `scrcpy`: ```bash +# in bash export ADB_SERVER_SOCKET=tcp:localhost:5038 scrcpy --force-adb-forward ``` +```cmd +:: in cmd +set ADB_SERVER_SOCKET=tcp:localhost:5038 +scrcpy --force-adb-forward +``` + +```powershell +# in PowerShell +$env:ADB_SERVER_SOCKET = 'tcp:localhost:5038' +scrcpy --force-adb-forward +``` + Like for wireless connections, it may be useful to reduce quality: From 7f2f5950f2624c1a3fd847af6248bcbef71a3124 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 20 Jun 2022 21:23:05 +0200 Subject: [PATCH 1218/2244] Remove useless dependencies reference There is no libs/ directory with local jar files. --- server/build.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/server/build.gradle b/server/build.gradle index dbc8261f..9725089e 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -19,7 +19,6 @@ android { } dependencies { - implementation fileTree(dir: 'libs', include: ['*.jar']) testImplementation 'junit:junit:4.13.1' } From 396e4bd9258cba5c841cc40a02e7fea0819f2c7f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 19 Jul 2022 12:15:05 +0200 Subject: [PATCH 1219/2244] Add missing LOG_OOM() on malloc failure --- app/src/adb/adb.c | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/adb/adb.c b/app/src/adb/adb.c index 8b92a2b0..e8775a01 100644 --- a/app/src/adb/adb.c +++ b/app/src/adb/adb.c @@ -401,6 +401,7 @@ sc_adb_list_devices(struct sc_intr *intr, unsigned flags, #define BUFSIZE 65536 char *buf = malloc(BUFSIZE); if (!buf) { + LOG_OOM(); return false; } From 4aeb78ece280d44607eda23f7996bae3d0c9415a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 19 Jul 2022 12:17:02 +0200 Subject: [PATCH 1220/2244] Add missing allocation failure check --- app/src/usb/usb.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/usb/usb.c b/app/src/usb/usb.c index 97aa9a33..190a4108 100644 --- a/app/src/usb/usb.c +++ b/app/src/usb/usb.c @@ -23,6 +23,11 @@ read_string(libusb_device_handle *handle, uint8_t desc_index) { // When non-negative, 'result' contains the number of bytes written char *s = malloc(result + 1); + if (!s) { + LOG_OOM(); + return NULL; + } + memcpy(s, buffer, result); s[result] = '\0'; return s; From db8c1ce8e1e4a7d3f96fad7d36a281798f529453 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 20 Jul 2022 11:39:55 +0200 Subject: [PATCH 1221/2244] Fix protocol documentation in comments Flags were in the correct order in the schema, but their description were reversed. --- app/src/demuxer.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/demuxer.c b/app/src/demuxer.c index 35ea4c06..9412eda7 100644 --- a/app/src/demuxer.c +++ b/app/src/demuxer.c @@ -37,8 +37,8 @@ sc_demuxer_recv_packet(struct sc_demuxer *demuxer, AVPacket *packet) { // CK...... ........ ........ ........ ........ ........ ........ ........ // ^^<-------------------------------------------------------------------> // || PTS - // | `- config packet - // `-- key frame + // | `- key frame + // `-- config packet uint8_t header[SC_PACKET_HEADER_SIZE]; ssize_t r = net_recv_all(demuxer->socket, header, SC_PACKET_HEADER_SIZE); From a47848f304965fe4baf548697ae79ed8db73e8b3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 27 Jul 2022 14:53:15 +0200 Subject: [PATCH 1222/2244] Detect Windows using _WIN32 in network util For consistency, always use _WIN32 instead of a mix of __WINDOWS__ and _WIN32. --- app/src/util/net.c | 15 +++++++-------- app/src/util/net.h | 5 ++--- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/app/src/util/net.c b/app/src/util/net.c index b1aa7445..e5f84676 100644 --- a/app/src/util/net.c +++ b/app/src/util/net.c @@ -3,11 +3,10 @@ #include #include #include -#include #include "log.h" -#ifdef __WINDOWS__ +#ifdef _WIN32 # include typedef int socklen_t; typedef SOCKET sc_raw_socket; @@ -29,7 +28,7 @@ bool net_init(void) { -#ifdef __WINDOWS__ +#ifdef _WIN32 WSADATA wsa; int res = WSAStartup(MAKEWORD(2, 2), &wsa) < 0; if (res < 0) { @@ -42,14 +41,14 @@ net_init(void) { void net_cleanup(void) { -#ifdef __WINDOWS__ +#ifdef _WIN32 WSACleanup(); #endif } static inline sc_socket wrap(sc_raw_socket sock) { -#ifdef __WINDOWS__ +#ifdef _WIN32 if (sock == INVALID_SOCKET) { return SC_SOCKET_NONE; } @@ -72,7 +71,7 @@ wrap(sc_raw_socket sock) { static inline sc_raw_socket unwrap(sc_socket socket) { -#ifdef __WINDOWS__ +#ifdef _WIN32 if (socket == SC_SOCKET_NONE) { return INVALID_SOCKET; } @@ -248,7 +247,7 @@ net_interrupt(sc_socket socket) { sc_raw_socket raw_sock = unwrap(socket); -#ifdef __WINDOWS__ +#ifdef _WIN32 if (!atomic_flag_test_and_set(&socket->closed)) { return !closesocket(raw_sock); } @@ -262,7 +261,7 @@ bool net_close(sc_socket socket) { sc_raw_socket raw_sock = unwrap(socket); -#ifdef __WINDOWS__ +#ifdef _WIN32 bool ret = true; if (!atomic_flag_test_and_set(&socket->closed)) { ret = !closesocket(raw_sock); diff --git a/app/src/util/net.h b/app/src/util/net.h index d9289981..801bdeaf 100644 --- a/app/src/util/net.h +++ b/app/src/util/net.h @@ -5,9 +5,8 @@ #include #include -#include -#ifdef __WINDOWS__ +#ifdef _WIN32 # include # include @@ -17,7 +16,7 @@ atomic_flag closed; } *sc_socket; -#else // not __WINDOWS__ +#else // not _WIN32 # include # define SC_SOCKET_NONE -1 From d23b3e88a4010716a50502da03c8326c2d0a1e81 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 26 Jul 2022 12:31:31 +0200 Subject: [PATCH 1223/2244] Replace '%g' by '%f' as printf format For some reason, '%g' does not work correctly with MinGW. Refs #3369 PR #3399 --- app/src/clock.c | 2 +- app/src/control_msg.c | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/clock.c b/app/src/clock.c index fe072f01..bb2430fd 100644 --- a/app/src/clock.c +++ b/app/src/clock.c @@ -98,7 +98,7 @@ sc_clock_update(struct sc_clock *clock, sc_tick system, sc_tick stream) { sc_clock_estimate(clock, &clock->slope, &clock->offset); #ifndef SC_CLOCK_NDEBUG - LOGD("Clock estimation: %g * pts + %" PRItick, + LOGD("Clock estimation: %f * pts + %" PRItick, clock->slope, clock->offset); #endif } diff --git a/app/src/control_msg.c b/app/src/control_msg.c index a57d8cc2..3c398016 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -170,7 +170,7 @@ sc_control_msg_log(const struct sc_control_msg *msg) { if (id == POINTER_ID_MOUSE || id == POINTER_ID_VIRTUAL_FINGER) { // string pointer id LOG_CMSG("touch [id=%s] %-4s position=%" PRIi32 ",%" PRIi32 - " pressure=%g buttons=%06lx", + " pressure=%f buttons=%06lx", id == POINTER_ID_MOUSE ? "mouse" : "vfinger", MOTIONEVENT_ACTION_LABEL(action), msg->inject_touch_event.position.point.x, @@ -180,7 +180,7 @@ sc_control_msg_log(const struct sc_control_msg *msg) { } else { // numeric pointer id LOG_CMSG("touch [id=%" PRIu64_ "] %-4s position=%" PRIi32 ",%" - PRIi32 " pressure=%g buttons=%06lx", + PRIi32 " pressure=%f buttons=%06lx", id, MOTIONEVENT_ACTION_LABEL(action), msg->inject_touch_event.position.point.x, From d19606eb0cd51b532177f80d3a9bc6619be304de Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 17 Aug 2022 16:38:59 +0200 Subject: [PATCH 1224/2244] Rename net_listen() parameter For consistency with net_accept(), which necessarily uses a server socket, name the net_listen() parameter "server_socket". --- app/src/util/net.c | 4 ++-- app/src/util/net.h | 2 +- app/src/util/net_intr.c | 6 +++--- app/src/util/net_intr.h | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/src/util/net.c b/app/src/util/net.c index e5f84676..ed4882cd 100644 --- a/app/src/util/net.c +++ b/app/src/util/net.c @@ -159,8 +159,8 @@ net_connect(sc_socket socket, uint32_t addr, uint16_t port) { } bool -net_listen(sc_socket socket, uint32_t addr, uint16_t port, int backlog) { - sc_raw_socket raw_sock = unwrap(socket); +net_listen(sc_socket server_socket, uint32_t addr, uint16_t port, int backlog) { + sc_raw_socket raw_sock = unwrap(server_socket); int reuse = 1; if (setsockopt(raw_sock, SOL_SOCKET, SO_REUSEADDR, (const void *) &reuse, diff --git a/app/src/util/net.h b/app/src/util/net.h index 801bdeaf..21396882 100644 --- a/app/src/util/net.h +++ b/app/src/util/net.h @@ -39,7 +39,7 @@ bool net_connect(sc_socket socket, uint32_t addr, uint16_t port); bool -net_listen(sc_socket socket, uint32_t addr, uint16_t port, int backlog); +net_listen(sc_socket server_socket, uint32_t addr, uint16_t port, int backlog); sc_socket net_accept(sc_socket server_socket); diff --git a/app/src/util/net_intr.c b/app/src/util/net_intr.c index bb70010b..55286af6 100644 --- a/app/src/util/net_intr.c +++ b/app/src/util/net_intr.c @@ -15,14 +15,14 @@ net_connect_intr(struct sc_intr *intr, sc_socket socket, uint32_t addr, } bool -net_listen_intr(struct sc_intr *intr, sc_socket socket, uint32_t addr, +net_listen_intr(struct sc_intr *intr, sc_socket server_socket, uint32_t addr, uint16_t port, int backlog) { - if (!sc_intr_set_socket(intr, socket)) { + if (!sc_intr_set_socket(intr, server_socket)) { // Already interrupted return false; } - bool ret = net_listen(socket, addr, port, backlog); + bool ret = net_listen(server_socket, addr, port, backlog); sc_intr_set_socket(intr, SC_SOCKET_NONE); return ret; diff --git a/app/src/util/net_intr.h b/app/src/util/net_intr.h index a83fadda..dbef528d 100644 --- a/app/src/util/net_intr.h +++ b/app/src/util/net_intr.h @@ -11,7 +11,7 @@ net_connect_intr(struct sc_intr *intr, sc_socket socket, uint32_t addr, uint16_t port); bool -net_listen_intr(struct sc_intr *intr, sc_socket socket, uint32_t addr, +net_listen_intr(struct sc_intr *intr, sc_socket server_socket, uint32_t addr, uint16_t port, int backlog); sc_socket From 9c1722f42846859866d26823a8f6cf07495ab803 Mon Sep 17 00:00:00 2001 From: Derek Wu Date: Tue, 16 Aug 2022 23:52:08 +0200 Subject: [PATCH 1225/2244] Use DisplayManagerGlobal instance Use the client instance to communicate with the DisplayManager server. Fixes #3446 Signed-off-by: Romain Vimont --- .../com/genymobile/scrcpy/wrappers/DisplayManager.java | 6 ++---- .../com/genymobile/scrcpy/wrappers/ServiceManager.java | 9 ++++++++- 2 files changed, 10 insertions(+), 5 deletions(-) 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 cedb3f47..3f4f897d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java @@ -3,12 +3,10 @@ package com.genymobile.scrcpy.wrappers; import com.genymobile.scrcpy.DisplayInfo; import com.genymobile.scrcpy.Size; -import android.os.IInterface; - public final class DisplayManager { - private final IInterface manager; + private final Object manager; // instance of hidden class android.hardware.display.DisplayManagerGlobal - public DisplayManager(IInterface manager) { + public DisplayManager(Object manager) { this.manager = manager; } 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 ea2a0784..68f6817d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java @@ -50,7 +50,14 @@ public final class ServiceManager { public DisplayManager getDisplayManager() { if (displayManager == null) { - displayManager = new DisplayManager(getService("display", "android.hardware.display.IDisplayManager")); + try { + Class clazz = Class.forName("android.hardware.display.DisplayManagerGlobal"); + Method getInstanceMethod = clazz.getDeclaredMethod("getInstance"); + Object dmg = getInstanceMethod.invoke(null); + displayManager = new DisplayManager(dmg); + } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + throw new AssertionError(e); + } } return displayManager; } From 77ebe786ea18ed0a049107231668bb48c5f32d01 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 21 Aug 2022 13:51:12 +0200 Subject: [PATCH 1226/2244] Fix FAQ formatting --- FAQ.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/FAQ.md b/FAQ.md index f6a8f226..e74d5873 100644 --- a/FAQ.md +++ b/FAQ.md @@ -44,9 +44,9 @@ If your device is not detected, you may need some [drivers] (on Windows). There ### Device unauthorized -> ERROR: Device is unauthorized: -> ERROR: --> (usb) 0123456789abcdef unauthorized -> ERROR: A popup should open on the device to request authorization. +> ERROR: Device is unauthorized: +> ERROR: --> (usb) 0123456789abcdef unauthorized +> ERROR: A popup should open on the device to request authorization. When connecting, a popup should open on the device. You must authorize USB debugging. @@ -60,10 +60,10 @@ If it does not open, check [stackoverflow][device-unauthorized]. If several devices are connected, you will encounter this error: -ERROR: Multiple (2) ADB devices: -ERROR: --> (usb) 0123456789abcdef device Nexus_5 -ERROR: --> (tcpip) 192.168.1.5:5555 device GM1913 -ERROR: Select a device via -s (--serial), -d (--select-usb) or -e (--select-tcpip) +> ERROR: Multiple (2) ADB devices: +> ERROR: --> (usb) 0123456789abcdef device Nexus_5 +> ERROR: --> (tcpip) 192.168.1.5:5555 device GM1913 +> ERROR: Select a device via -s (--serial), -d (--select-usb) or -e (--select-tcpip) In that case, you can either provide the identifier of the device you want to mirror: From 72ba913324d65cded8672dca7a57415cb82da095 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 23 Jul 2022 16:10:48 +0200 Subject: [PATCH 1227/2244] Move README and FAQ translations to the wiki This lowers the barrier to contribute to translations, and frees up my maintenance time used to review and merge translations and their updates in many languages. --- FAQ.it.md | 235 ---------- FAQ.ko.md | 84 ---- FAQ.md | 8 +- FAQ.zh-Hans.md | 284 ------------- README.de.md | 1016 ------------------------------------------- README.id.md | 696 ------------------------------ README.it.md | 1041 --------------------------------------------- README.jp.md | 799 ---------------------------------- README.ko.md | 498 ---------------------- README.md | 13 +- README.pt-br.md | 880 -------------------------------------- README.sp.md | 974 ------------------------------------------ README.tr.md | 824 ----------------------------------- README.zh-Hans.md | 993 ------------------------------------------ README.zh-Hant.md | 702 ------------------------------ 15 files changed, 6 insertions(+), 9041 deletions(-) delete mode 100644 FAQ.it.md delete mode 100644 FAQ.ko.md delete mode 100644 FAQ.zh-Hans.md delete mode 100644 README.de.md delete mode 100644 README.id.md delete mode 100644 README.it.md delete mode 100644 README.jp.md delete mode 100644 README.ko.md delete mode 100644 README.pt-br.md delete mode 100644 README.sp.md delete mode 100644 README.tr.md delete mode 100644 README.zh-Hans.md delete mode 100644 README.zh-Hant.md diff --git a/FAQ.it.md b/FAQ.it.md deleted file mode 100644 index 0da656c0..00000000 --- a/FAQ.it.md +++ /dev/null @@ -1,235 +0,0 @@ -_Apri le [FAQ](FAQ.md) originali e sempre aggiornate._ - -# Domande Frequenti (FAQ) - -Questi sono i problemi più comuni riportati e i loro stati. - - -## Problemi di `adb` - -`scrcpy` esegue comandi `adb` per inizializzare la connessione con il dispositivo. Se `adb` fallisce, scrcpy non funzionerà. - -In questo caso sarà stampato questo errore: - -> ERROR: "adb push" returned with value 1 - -Questo solitamente non è un bug di _scrcpy_, ma un problema del tuo ambiente. - -Per trovare la causa, esegui: - -```bash -adb devices -``` - -### `adb` not found (`adb` non trovato) - -È necessario che `adb` sia accessibile dal tuo `PATH`. - -In Windows, la cartella corrente è nel tuo `PATH` e `adb.exe` è incluso nella release, perciò dovrebbe già essere pronto all'uso. - - -### Device unauthorized (Dispositivo non autorizzato) - -Controlla [stackoverflow][device-unauthorized] (in inglese). - -[device-unauthorized]: https://stackoverflow.com/questions/23081263/adb-android-device-unauthorized - - -### Device not detected (Dispositivo non rilevato) - -> adb: error: failed to get feature set: no devices/emulators found - -Controlla di aver abilitato correttamente il [debug con adb][enable-adb] (link in inglese). - -Se il tuo dispositivo non è rilevato, potresti avere bisogno dei [driver][drivers] (link in inglese) (in Windows). - -[enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling -[drivers]: https://developer.android.com/studio/run/oem-usb.html - - -### Più dispositivi connessi - -Se più dispositivi sono connessi, riscontrerai questo errore: - -> adb: error: failed to get feature set: more than one device/emulator - -l'identificatore del tuo dispositivo deve essere fornito: - -```bash -scrcpy -s 01234567890abcdef -``` - -Notare che se il tuo dispositivo è connesso mediante TCP/IP, riscontrerai questo messaggio: - -> adb: error: more than one device/emulator -> ERROR: "adb reverse" returned with value 1 -> WARN: 'adb reverse' failed, fallback to 'adb forward' - -Questo è un problema atteso (a causa di un bug di una vecchia versione di Android, vedi [#5] (link in inglese)), ma in quel caso scrcpy ripiega su un metodo differente, il quale dovrebbe funzionare. - -[#5]: https://github.com/Genymobile/scrcpy/issues/5 - - -### Conflitti tra versioni di adb - -> adb server version (41) doesn't match this client (39); killing... - -L'errore compare quando usi più versioni di `adb` simultaneamente. Devi trovare il programma che sta utilizzando una versione differente di `adb` e utilizzare la stessa versione dappertutto. - -Puoi sovrascrivere i binari di `adb` nell'altro programma, oppure chiedere a _scrcpy_ di usare un binario specifico di `adb`, impostando la variabile d'ambiente `ADB`: - -```bash -set ADB=/path/to/your/adb -scrcpy -``` - - -### Device disconnected (Dispositivo disconnesso) - -Se _scrcpy_ si interrompe con l'avviso "Device disconnected", allora la connessione `adb` è stata chiusa. - -Prova con un altro cavo USB o inseriscilo in un'altra porta USB. Vedi [#281] (in inglese) e [#283] (in inglese). - -[#281]: https://github.com/Genymobile/scrcpy/issues/281 -[#283]: https://github.com/Genymobile/scrcpy/issues/283 - - - -## Problemi di controllo - -### Mouse e tastiera non funzionano - -Su alcuni dispositivi potresti dover abilitare un opzione che permette l'[input simulato][simulating input] (link in inglese). Nelle opzioni sviluppatore, abilita: - -> **Debug USB (Impostazioni di sicurezza)** -> _Permetti la concessione dei permessi e la simulazione degli input mediante il debug USB_ - - -[simulating input]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323 - - -### I caratteri speciali non funzionano - -Iniettare del testo in input è [limitato ai caratteri ASCII][text-input] (link in inglese). Un trucco permette di iniettare dei [caratteri accentati][accented-characters] (link in inglese), ma questo è tutto. Vedi [#37] (link in inglese). - -[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 - - -## Problemi del client - -### La qualità è bassa - -Se la definizione della finestra del tuo client è minore di quella del tuo dispositivo, allora potresti avere una bassa qualità di visualizzazione, specialmente individuabile nei testi (vedi [#40] (link in inglese)). - -[#40]: https://github.com/Genymobile/scrcpy/issues/40 - -Per migliorare la qualità di ridimensionamento (downscaling), il filtro trilineare è applicato automaticamente se il renderizzatore è OpenGL e se supporta la creazione di mipmap. - -In Windows, potresti voler forzare OpenGL: - -``` -scrcpy --render-driver=opengl -``` - -Potresti anche dover configurare il [comportamento di ridimensionamento][scaling behavior] (link in inglese): - -> `scrcpy.exe` > Propietà > Compatibilità > Modifica impostazioni DPI elevati > Esegui l'override del comportamento di ridimensionamento DPI elevati > Ridimensionamento eseguito per: _Applicazione_. - -[scaling behavior]: https://github.com/Genymobile/scrcpy/issues/40#issuecomment-424466723 - - -### Problema con Wayland - -Per impostazione predefinita, SDL utilizza x11 su Linux. Il [video driver] può essere cambiato attraversio la variabile d'ambiente `SDL_VIDEODRIVER`: - -[video driver]: https://wiki.libsdl.org/FAQUsingSDL#how_do_i_choose_a_specific_video_driver - -```bash -export SDL_VIDEODRIVER=wayland -scrcpy -``` - -Su alcune distribuzioni (almeno Fedora), il pacchetto `libdecor` deve essere installato manualmente. - -Vedi le issues [#2554] e [#2559]. - -[#2554]: https://github.com/Genymobile/scrcpy/issues/2554 -[#2559]: https://github.com/Genymobile/scrcpy/issues/2559 - - -### Crash del compositore KWin - -In Plasma Desktop, il compositore è disabilitato mentre _scrcpy_ è in esecuzione. - -Come soluzione alternativa, [disattiva la "composizione dei blocchi"][kwin] (link in inglese). - - -[kwin]: https://github.com/Genymobile/scrcpy/issues/114#issuecomment-378778613 - - -## Crash - -### Eccezione - -Ci potrebbero essere molte ragioni. Una causa comune è che il codificatore hardware del tuo dispositivo non riesce a codificare alla definizione selezionata: - -> ``` -> 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 -> ``` - -o - -> ``` -> ERROR: Exception on thread Thread[main,5,main] -> java.lang.IllegalStateException -> at android.media.MediaCodec.native_dequeueOutputBuffer(Native Method) -> ``` - -Prova con una definizione inferiore: - -``` -scrcpy -m 1920 -scrcpy -m 1024 -scrcpy -m 800 -``` - -Potresti anche provare un altro [codificatore](README.it.md#codificatore). - - -## Linea di comando in Windows - -Alcuni utenti Windows non sono familiari con la riga di comando. Qui è descritto come aprire un terminale ed eseguire `scrcpy` con gli argomenti: - - 1. Premi Windows+r, questo apre una finestra di dialogo. - 2. Scrivi `cmd` e premi Enter, questo apre un terminale. - 3. Vai nella tua cartella di _scrcpy_ scrivendo (adatta il percorso): - - ```bat - cd C:\Users\user\Downloads\scrcpy-win64-xxx - ``` - - e premi Enter - 4. Scrivi il tuo comando. Per esempio: - - ```bat - scrcpy --record file.mkv - ``` - -Se pianifichi di utilizzare sempre gli stessi argomenti, crea un file `myscrcpy.bat` (abilita mostra [estensioni nomi file][show file extensions] per evitare di far confusione) contenente il tuo comando nella cartella di `scrcpy`. Per esempio: - -```bat -scrcpy --prefer-text --turn-screen-off --stay-awake -``` - -Poi fai doppio click su quel file. - -Potresti anche modificare (una copia di) `scrcpy-console.bat` o `scrcpy-noconsole.vbs` per aggiungere alcuni argomenti. - -[show file extensions]: https://www.techpedia.it/14-windows/windows-10/171-visualizzare-le-estensioni-nomi-file-con-windows-10 diff --git a/FAQ.ko.md b/FAQ.ko.md deleted file mode 100644 index c9e06e24..00000000 --- a/FAQ.ko.md +++ /dev/null @@ -1,84 +0,0 @@ -# 자주하는 질문 (FAQ) - -다음은 자주 제보되는 문제들과 그들의 현황입니다. - - -### Windows 운영체제에서, 디바이스가 발견되지 않습니다. - -가장 흔한 제보는 `adb`에 발견되지 않는 디바이스 혹은 권한 관련 문제입니다. -다음 명령어를 호출하여 모든 것들에 이상이 없는지 확인하세요: - - adb devices - -Windows는 당신의 디바이스를 감지하기 위해 [드라이버]가 필요할 수도 있습니다. - -[드라이버]: https://developer.android.com/studio/run/oem-usb.html - - -### 내 디바이스의 미러링만 가능하고, 디바이스와 상호작용을 할 수 없습니다. - -일부 디바이스에서는, [simulating input]을 허용하기 위해서 한가지 옵션을 활성화해야 할 수도 있습니다. -개발자 옵션에서 (developer options) 다음을 활성화 하세요: - -> **USB debugging (Security settings)** -> _권한 부여와 USB 디버깅을 통한 simulating input을 허용한다_ - -[simulating input]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323 - - -### 마우스 클릭이 다른 곳에 적용됩니다. - -Mac 운영체제에서, HiDPI support 와 여러 스크린 창이 있는 경우, 입력 위치가 잘못 파악될 수 있습니다. -[issue 15]를 참고하세요. - -[issue 15]: https://github.com/Genymobile/scrcpy/issues/15 - -차선책은 HiDPI support을 비활성화 하고 build하는 방법입니다: - -```bash -meson x --buildtype release -Dhidpi_support=false -``` - -하지만, 동영상은 낮은 해상도로 재생될 것 입니다. - - -### HiDPI display의 화질이 낮습니다. - -Windows에서는, [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 - - -### KWin compositor가 실행되지 않습니다 - -Plasma Desktop에서는,_scrcpy_ 가 실행중에는 compositor가 비활성화 됩니다. - -차석책으로는, ["Block compositing"를 비활성화하세요][kwin]. - -[kwin]: https://github.com/Genymobile/scrcpy/issues/114#issuecomment-378778613 - - -###비디오 스트림을 열 수 없는 에러가 발생합니다.(Could not open video stream). - -여러가지 원인이 있을 수 있습니다. 가장 흔한 원인은 디바이스의 하드웨어 인코더(hardware encoder)가 -주어진 해상도를 인코딩할 수 없는 경우입니다. - -``` -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 -``` - -더 낮은 해상도로 시도 해보세요: - -``` -scrcpy -m 1920 -scrcpy -m 1024 -scrcpy -m 800 -``` diff --git a/FAQ.md b/FAQ.md index e74d5873..e6c3c94d 100644 --- a/FAQ.md +++ b/FAQ.md @@ -324,8 +324,8 @@ to add some arguments. ## Translations -This FAQ is available in other languages: +Translations of this FAQ in other languages are available in the [wiki]. - - [Italiano (Italiano, `it`) - v1.19](FAQ.it.md) - - [한국어 (Korean, `ko`) - v1.11](FAQ.ko.md) - - [简体中文 (Simplified Chinese, `zh-Hans`) - v1.22](FAQ.zh-Hans.md) +[wiki]: https://github.com/Genymobile/scrcpy/wiki + +Only this README file is guaranteed to be up-to-date. diff --git a/FAQ.zh-Hans.md b/FAQ.zh-Hans.md deleted file mode 100644 index 23674eed..00000000 --- a/FAQ.zh-Hans.md +++ /dev/null @@ -1,284 +0,0 @@ -_Only the original [FAQ.md](FAQ.md) is guaranteed to be up-to-date._ - -_只有原版的 [FAQ.md](FAQ.md)是保证最新的。_ - -Current version is based on [28054cd] - -本文根据[28054cd]进行翻译。 - -[28054cd]: https://github.com/Genymobile/scrcpy/blob/28054cd471f848733e11372c9d745cd5d71e6ce7/FAQ.md - -# 常见问题 - -这里是一些常见的问题以及他们的状态。 - -## `adb` 相关问题 - -`scrcpy` 执行 `adb` 命令来初始化和设备之间的连接。如果 `adb` 执行失败了, scrcpy 就无法工作。 - -在这种情况中,将会输出这个错误: - -> ERROR: "adb get-serialno" returned with value 1 - -这通常不是 _scrcpy_ 的bug,而是你的环境的问题。 - -要找出原因,请执行以下操作: - -```bash -adb devices -``` - -### 找不到`adb` - - -你的`PATH`中需要能访问到`adb`。 - -在Windows上,当前目录会包含在`PATH`中,并且`adb.exe`也包含在发行版中,因此它应该是开箱即用(直接解压就可以)的。 - - -### 设备未授权 - - -> error: device unauthorized. -> This adb server's $ADB_VENDOR_KEYS is not set -> Try 'adb kill-server' if that seems wrong. -> Otherwise check for a confirmation dialog on your device. - -连接时,在设备上应该会打开一个弹出窗口。 您必须授权 USB 调试。 - -如果没有打开,参见[stackoverflow][device-unauthorized]. - -[device-unauthorized]: https://stackoverflow.com/questions/23081263/adb-android-device-unauthorized - - -### 未检测到设备 - -> error: no devices/emulators found - -确认已经正确启用 [adb debugging][enable-adb]. - -如果你的设备没有被检测到,你可能需要一些[驱动][drivers] (在 Windows上)。这里有一个单独的 [适用于Google设备的USB驱动][google-usb-driver]. - -[enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling -[drivers]: https://developer.android.com/studio/run/oem-usb.html -[google-usb-driver]: https://developer.android.com/studio/run/win-usb - - -### 已连接多个设备 - -如果连接了多个设备,您将遇到以下错误: - -> error: more than one device/emulator - -必须提供要镜像的设备的标识符: - -```bash -scrcpy -s 01234567890abcdef -``` - -注意,如果你的设备是通过 TCP/IP 连接的, 你将会收到以下消息: - -> adb: error: more than one device/emulator -> ERROR: "adb reverse" returned with value 1 -> WARN: 'adb reverse' failed, fallback to 'adb forward' - -这是意料之中的 (由于旧版安卓的一个bug, 请参见 [#5]),但是在这种情况下,scrcpy会退回到另一种方法,这种方法应该可以起作用。 - -[#5]: https://github.com/Genymobile/scrcpy/issues/5 - - -### adb版本之间冲突 - -> adb server version (41) doesn't match this client (39); killing... - -同时使用多个版本的`adb`时会发生此错误。你必须查找使用不同`adb`版本的程序,并在所有地方使用相同版本的`adb`。 - -你可以覆盖另一个程序中的`adb`二进制文件,或者通过设置`ADB`环境变量来让 _scrcpy_ 使用特定的`adb`二进制文件。 - -```bash -set ADB=/path/to/your/adb -scrcpy -``` - - -### 设备断开连接 - -如果 _scrcpy_ 在警告“设备连接断开”的情况下自动中止,那就意味着`adb`连接已经断开了。 - -请尝试使用另一条USB线或者电脑上的另一个USB接口。请参看 [#281] 和 [#283]。 - -[#281]: https://github.com/Genymobile/scrcpy/issues/281 -[#283]: https://github.com/Genymobile/scrcpy/issues/283 - - -## 控制相关问题 - -### 鼠标和键盘不起作用 - - -在某些设备上,您可能需要启用一个选项以允许 [模拟输入][simulating input]。 -在开发者选项中,打开: - -> **USB调试 (安全设置)** -> _允许通过USB调试修改权限或模拟点击_ - -[simulating input]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323 - - -### 特殊字符不起作用 - -可输入的文本[被限制为ASCII字符][text-input]。也可以用一些小技巧输入一些[带重音符号的字符][accented-characters],但是仅此而已。参见[#37]。 - -自 Linux 上的 scrcpy v1.20 之后,可以模拟[物理键盘][hid] (HID)。 - -[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 -[hid]: README.md#physical-keyboard-simulation-hid - - -## 客户端相关问题 - -### 效果很差 - -如果你的客户端窗口分辨率比你的设备屏幕小,则可能出现效果差的问题,尤其是在文本上(参见 [#40])。 - -[#40]: https://github.com/Genymobile/scrcpy/issues/40 - -为了提升降尺度的质量,如果渲染器是OpenGL并且支持mip映射,就会自动开启三线性过滤。 - -在Windows上,你可能希望强制使用OpenGL: - -``` -scrcpy --render-driver=opengl -``` - -你可能还需要配置[缩放行为][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 - - -### Wayland相关的问题 - -在Linux上,SDL默认使用x11。可以通过`SDL_VIDEODRIVER`环境变量来更改[视频驱动][video driver]: - -[video driver]: https://wiki.libsdl.org/FAQUsingSDL#how_do_i_choose_a_specific_video_driver - -```bash -export SDL_VIDEODRIVER=wayland -scrcpy -``` - -在一些发行版上 (至少包括 Fedora), `libdecor` 包必须手动安装。 - -参见 [#2554] 和 [#2559]。 - -[#2554]: https://github.com/Genymobile/scrcpy/issues/2554 -[#2559]: https://github.com/Genymobile/scrcpy/issues/2559 - - -### KWin compositor 崩溃 - -在Plasma桌面中,当 _scrcpy_ 运行时,会禁用compositor。 - -一种解决方法是, [禁用 "Block compositing"][kwin]. - -[kwin]: https://github.com/Genymobile/scrcpy/issues/114#issuecomment-378778613 - - -## 崩溃 - -### 异常 - -可能有很多原因。一个常见的原因是您的设备无法按给定清晰度进行编码: - -> ``` -> 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] -> java.lang.IllegalStateException -> at android.media.MediaCodec.native_dequeueOutputBuffer(Native Method) -> ``` - -请尝试使用更低的清晰度: - -``` -scrcpy -m 1920 -scrcpy -m 1024 -scrcpy -m 800 -``` - -自 scrcpy v1.22以来,scrcpy 会自动在失败前以更低的分辨率重试。这种行为可以用`--no-downsize-on-error`关闭。 - -你也可以尝试另一种 [编码器](README.md#encoder)。 - - -如果您在 Android 12 上遇到此异常,则只需升级到 scrcpy >= 1.18 (见 [#2129]): - -``` -> ERROR: Exception on thread Thread[main,5,main] -java.lang.AssertionError: java.lang.reflect.InvocationTargetException - at com.genymobile.scrcpy.wrappers.SurfaceControl.setDisplaySurface(SurfaceControl.java:75) - ... -Caused by: java.lang.reflect.InvocationTargetException - at java.lang.reflect.Method.invoke(Native Method) - at com.genymobile.scrcpy.wrappers.SurfaceControl.setDisplaySurface(SurfaceControl.java:73) - ... 7 more -Caused by: java.lang.IllegalArgumentException: displayToken must not be null - at android.view.SurfaceControl$Transaction.setDisplaySurface(SurfaceControl.java:3067) - at android.view.SurfaceControl.setDisplaySurface(SurfaceControl.java:2147) - ... 9 more -``` - -[#2129]: https://github.com/Genymobile/scrcpy/issues/2129 - - -## Windows命令行 - -从 v1.22 开始,增加了一个“快捷方式”,可以直接在 scrcpy 目录打开一个终端。双击`open_a_terminal_here.bat`,然后输入你的命令。 例如: - -``` -scrcpy --record file.mkv -``` - -您也可以打开终端并手动转到 scrcpy 文件夹: - - 1. 按下 Windows+r,打开一个对话框。 - 2. 输入 `cmd` 并按 Enter,这样就打开了一个终端。 - 3. 通过输入以下命令,切换到你的 _scrcpy_ 所在的目录 (根据你的实际位置修改路径): - - ```bat - cd C:\Users\user\Downloads\scrcpy-win64-xxx - ``` - - 然后按 Enter - 4. 输入你的命令。比如: - - ```bat - scrcpy --record file.mkv - ``` - -如果你打算总是使用相同的参数,在`scrcpy`目录创建一个文件 `myscrcpy.bat` -(启用 [显示文件拓展名][show file extensions] 避免混淆),文件中包含你的命令。例如: - -```bat -scrcpy --prefer-text --turn-screen-off --stay-awake -``` - -然后只需双击刚刚创建的文件。 - -你也可以编辑 `scrcpy-console.bat` 或者 `scrcpy-noconsole.vbs`(的副本)来添加参数。 - -[show file extensions]: https://www.howtogeek.com/205086/beginner-how-to-make-windows-show-file-extensions/ diff --git a/README.de.md b/README.de.md deleted file mode 100644 index 1299ae0e..00000000 --- a/README.de.md +++ /dev/null @@ -1,1016 +0,0 @@ -_Only the original [README](README.md) is guaranteed to be up-to-date._ - -# scrcpy (v1.22) - -scrcpy - -_ausgesprochen "**scr**een **c**o**py**"_ - -Diese Anwendung liefert sowohl Anzeige als auch Steuerung eines Android-Gerätes über USB (oder [über TCP/IP](#tcpip-kabellos)). Dabei wird kein _root_ Zugriff benötigt. -Die Anwendung funktioniert unter _GNU/Linux_, _Windows_ und _macOS_. - -![screenshot](assets/screenshot-debian-600.jpg) - -Dabei liegt der Fokus auf: - - - **Leichtigkeit**: native, nur Anzeige des Gerätedisplays - - **Leistung**: 30~120fps, abhängig vom Gerät - - **Qualität**: 1920×1080 oder mehr - - **Geringe Latenz**: [35~70ms][lowlatency] - - **Kurze Startzeit**: ~1 Sekunde um das erste Bild anzuzeigen - - **Keine Aufdringlichkeit**: Es wird keine installierte Software auf dem Gerät zurückgelassen - - **Nutzervorteile**: kein Account, keine Werbung, kein Internetzugriff notwendig - - **Freiheit**: gratis und open-source - -[lowlatency]: https://github.com/Genymobile/scrcpy/pull/646 - -Die Features beinhalten: - - [Aufnahme](#Aufnahme) - - Spiegeln mit [ausgeschaltetem Bildschirm](#bildschirm-ausschalten) - - [Copy&Paste](#copy-paste) in beide Richtungen - - [Einstellbare Qualität](#Aufnahmekonfiguration) - - Gerätebildschirm [als Webcam (V4L2)](#v4l2loopback) (nur Linux) - - [Simulation einer physischen Tastatur (HID)](#simulation-einer-physischen-tastatur-mit-hid) - (nur Linux) - - [Simulation einer physischen Maus (HID)](#simulation-einer-physischen-maus-mit-hid) - (nur Linux) - - [OTG Modus](#otg) (nur Linux) - - und mehr… - -## Voraussetzungen - -Das Android-Gerät benötigt mindestens API 21 (Android 5.0). - -Es muss sichergestellt sein, dass [adb debugging][enable-adb] auf dem Gerät aktiv ist. - -[enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling - -Auf manchen Geräten müssen zudem [weitere Optionen][control] aktiv sein um das Gerät mit Maus und Tastatur steuern zu können. - -[control]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323 - - -## Installation der App - -Packaging status - -### Zusammenfassung - - - Linux: `apt install scrcpy` - - Windows: [download (siehe README)](README.md#windows) - - macOS: `brew install scrcpy` - -Direkt von der Source bauen: [BUILD] ([vereinfachter Prozess (englisch)][BUILD_simple]) - -[BUILD]: BUILD.md -[BUILD_simple]: BUILD.md#simple - - -### Linux - -Auf Debian und Ubuntu: - -``` -apt install scrcpy -``` - -Auf Arch Linux: - -``` -pacman -S scrcpy -``` - -Ein [Snap] package ist verfügbar: [`scrcpy`][snap-link]. - -[snap-link]: https://snapstats.org/snaps/scrcpy - -[snap]: https://en.wikipedia.org/wiki/Snappy_(package_manager) - -Für Fedora ist ein [COPR] package verfügbar: [`scrcpy`][copr-link]. - -[COPR]: https://fedoraproject.org/wiki/Category:Copr -[copr-link]: https://copr.fedorainfracloud.org/coprs/zeno/scrcpy/ - - -Für Gentoo ist ein [Ebuild] verfügbar: [`scrcpy/`][ebuild-link]. - -[Ebuild]: https://wiki.gentoo.org/wiki/Ebuild -[ebuild-link]: https://github.com/maggu2810/maggu2810-overlay/tree/master/app-mobilephone/scrcpy - -Die App kann zudem [manuell gebaut werden][BUILD] ([vereinfachter Prozess (englisch)][BUILD_simple]). - - -### Windows - -Für Windows ist der Einfachheit halber ein vorgebautes Archiv mit allen Abhängigkeiten (inklusive `adb`) vorhanden. - - - [README](README.md#windows) - -Es ist zudem in [Chocolatey] vorhanden: - -[Chocolatey]: https://chocolatey.org/ - -```bash -choco install scrcpy -choco install adb # falls noch nicht vorhanden -``` - -Und in [Scoop]: - -```bash -scoop install scrcpy -scoop install adb # falls noch nicht vorhanden -``` - -[Scoop]: https://scoop.sh - -Die App kann zudem [manuell gebaut werden][BUILD]. - - -### macOS - -Die Anwendung ist in [Homebrew] verfügbar. Installation: - -[Homebrew]: https://brew.sh/ - -```bash -brew install scrcpy -``` - -Es wird `adb` benötigt, auf welches über `PATH` zugegriffen werden kann. Falls noch nicht vorhanden: - -```bash -brew install android-platform-tools -``` - -Es ist außerdem in [MacPorts] vorhanden, welches adb bereits aufsetzt: - -```bash -sudo port install scrcpy -``` - -[MacPorts]: https://www.macports.org/ - - -Die Anwendung kann zudem [manuell gebaut werden][BUILD]. - - -## Ausführen - -Ein Android-Gerät anschließen und diese Befehle ausführen: - -```bash -scrcpy -``` - -Dabei werden Kommandozeilenargumente akzeptiert, aufgelistet per: - -```bash -scrcpy --help -``` - -## Funktionalitäten - -### Aufnahmekonfiguration - -#### Größe reduzieren - -Manchmal ist es sinnvoll, das Android-Gerät mit einer geringeren Auflösung zu spiegeln, um die Leistung zu erhöhen. - -Um die Höhe und Breite auf einen Wert zu limitieren (z.B. 1024): - -```bash -scrcpy --max-size 1024 -scrcpy -m 1024 # short version -``` - -Die andere Größe wird dabei so berechnet, dass das Seitenverhältnis des Gerätes erhalten bleibt. -In diesem Fall wird ein Gerät mit einer 1920×1080-Auflösung mit 1024×576 gespiegelt. - - -#### Ändern der Bit-Rate - -Die Standard-Bitrate ist 8 Mbps. Um die Bitrate zu ändern (z.B. zu 2 Mbps): - -```bash -scrcpy --bit-rate 2M -scrcpy -b 2M # Kurzversion -``` - -#### Limitieren der Bildwiederholrate - -Die Aufnahme-Bildwiederholrate kann begrenzt werden: - -```bash -scrcpy --max-fps 15 -``` - -Dies wird offiziell seit Android 10 unterstützt, kann jedoch bereits auf früheren Versionen funktionieren. - -#### Zuschneiden - -Der Geräte-Bildschirm kann zugeschnitten werden, sodass nur ein Teil gespiegelt wird. - -Dies ist beispielsweise nützlich, um nur ein Auge der Oculus Go zu spiegeln: - -```bash -scrcpy --crop 1224:1440:0:0 # 1224x1440 am Versatz (0,0) -``` - -Falls `--max-size` auch festgelegt ist, wird das Ändern der Größe nach dem Zuschneiden angewandt. - - -#### Feststellen der Videoorientierung - - -Um die Orientierung während dem Spiegeln festzustellen: - -```bash -scrcpy --lock-video-orientation # ursprüngliche (momentane) Orientierung -scrcpy --lock-video-orientation=0 # normale Orientierung -scrcpy --lock-video-orientation=1 # 90° gegen den Uhrzeigersinn -scrcpy --lock-video-orientation=2 # 180° -scrcpy --lock-video-orientation=3 # 90° mit dem Uhrzeigersinn -``` - -Dies beeinflusst die Aufnahmeausrichtung. - -Das [Fenster kann auch unabhängig rotiert](#Rotation) werden. - - -#### Encoder - -Manche Geräte besitzen mehr als einen Encoder. Manche dieser Encoder können dabei sogar zu Problemen oder Abstürzen führen. -Die Auswahl eines anderen Encoders ist möglich: - -```bash -scrcpy --encoder OMX.qcom.video.encoder.avc -``` - -Um eine Liste aller verfügbaren Encoder zu erhalten (eine Fehlermeldung gibt alle verfügbaren Encoder aus): - -```bash -scrcpy --encoder _ -``` - -### Aufnahme - -#### Aufnehmen von Videos - -Es ist möglich, das Display während des Spiegelns aufzunehmen: - -```bash -scrcpy --record file.mp4 -scrcpy -r file.mkv -``` - -Um das Spiegeln während des Aufnehmens zu deaktivieren: - -```bash -scrcpy --no-display --record file.mp4 -scrcpy -Nr file.mkv -# Unterbrechen der Aufnahme mit Strg+C -``` - -"Übersprungene Bilder" werden aufgenommen, selbst wenn sie in Echtzeit (aufgrund von Performancegründen) nicht dargestellt werden. Die Einzelbilder sind mit _Zeitstempeln_ des Gerätes versehen are, sodass eine [Paketverzögerungsvariation] nicht die Aufnahmedatei beeinträchtigt. - -[Paketverzögerungsvariation]: https://www.wikide.wiki/wiki/en/Packet_delay_variation - - -#### v4l2loopback - -Auf Linux ist es möglich, den Video-Stream zu einem v4l2 loopback Gerät zu senden, sodass das Android-Gerät von jedem v4l2-fähigen Tool wie eine Webcam verwendet werden kann. - -Das Modul `v4l2loopback` muss dazu installiert werden: - -```bash -sudo apt install v4l2loopback-dkms -``` - -Um ein v4l2 Gerät zu erzeugen: - -```bash -sudo modprobe v4l2loopback -``` - -Dies erzeugt ein neues Video-Gerät in `/dev/videoN`, wobei `N` ein Integer ist (mehr [Optionen](https://github.com/umlaeute/v4l2loopback#options) sind verfügbar um mehrere Geräte oder Geräte mit spezifischen Nummern zu erzeugen). - -Um die aktivierten Geräte aufzulisten: - -```bash -# benötigt das v4l-utils package -v4l2-ctl --list-devices - -# simpel, kann aber ausreichend -ls /dev/video* -``` - -Um scrcpy mithilfe eines v4l2 sink zu starten: - -```bash -scrcpy --v4l2-sink=/dev/videoN -scrcpy --v4l2-sink=/dev/videoN --no-display # Fenster mit Spiegelung ausschalten -scrcpy --v4l2-sink=/dev/videoN -N # kurze Version -``` - -(`N` muss mit der Geräte-ID ersetzt werden, welche mit `ls /dev/video*` überprüft werden kann) - -Einmal aktiv, kann der Stream mit einem v4l2-fähigen Tool verwendet werden: - -```bash -ffplay -i /dev/videoN -vlc v4l2:///dev/videoN # VLC kann eine gewisse Bufferverzögerung herbeiführen -``` - -Beispielsweise kann das Video mithilfe von [OBS] aufgenommen werden. - -[OBS]: https://obsproject.com/ - - -#### Buffering - -Es ist möglich, Buffering hinzuzufügen. Dies erhöht die Latenz, reduziert aber etwaigen Jitter (see [#2464]). - -[#2464]: https://github.com/Genymobile/scrcpy/issues/2464 - -Diese Option ist sowohl für Video-Buffering: - -```bash -scrcpy --display-buffer=50 # fügt 50ms Buffering zum Display hinzu -``` - -als auch V4L2 sink verfügbar: - -```bash -scrcpy --v4l2-buffer=500 # fügt 500ms Buffering für v4l2 sink hinzu -``` - - -### Verbindung - -#### TCP/IP Kabellos - -_Scrcpy_ verwendet `adb`, um mit dem Gerät zu kommunizieren. `adb` kann sich per TCP/IP mit einem Gerät [verbinden]. Das Gerät muss dabei mit demselben Netzwerk wie der Computer verbunden sein. - -##### Automatisch - -Die Option `--tcpip` erlaubt es, die Verbindung automatisch zu konfigurieren. Dabei gibt es zwei Varianten. - -Falls das Gerät (verfügbar unter 192.168.1.1 in diesem Beispiel) bereit an einem Port (typically 5555) nach einkommenden adb-Verbindungen hört, dann führe diesen Befehl aus: - -```bash -scrcpy --tcpip=192.168.1.1 # Standard-Port ist 5555 -scrcpy --tcpip=192.168.1.1:5555 -``` - -Falls adb TCP/IP auf dem Gerät deaktiviert ist (oder falls die IP-Adresse des Gerätes nicht bekannt ist): Gerät per USB verbinden, anschließend diesen Befehl ausführen: - -```bash -scrcpy --tcpip # ohne weitere Argumente -``` - -Dies finden automatisch das Gerät und aktiviert den TCP/IP-Modus. Anschließend verbindet sich der Befehl mit dem Gerät bevor die Verbindung startet. - -##### Manuell - -Alternativ kann die TCP/IP-Verbindung auch manuell per `adb` aktiviert werden: - -1. Gerät mit demselben Wi-Fi wie den Computer verbinden. -2. IP-Adresse des Gerätes herausfinden, entweder über Einstellungen → Über das Telefon → Status, oder indem dieser Befehl ausgeführt wird: - - ```bash - adb shell ip route | awk '{print $9}' - ``` - -3. Aktivieren von adb über TCP/IP auf dem Gerät: `adb tcpip 5555`. -4. Ausstecken des Geräts. -5. Verbinden zum Gerät: `adb connect DEVICE_IP:5555` _(`DEVICE_IP` ersetzen)_. -6. `scrcpy` wie normal ausführen. - -Es kann sinnvoll sein, die Bit-Rate sowie dei Auflösung zu reduzieren: - -```bash -scrcpy --bit-rate 2M --max-size 800 -scrcpy -b2M -m800 # kurze Version -``` - -[verbinden]: https://developer.android.com/studio/command-line/adb.html#wireless - - -#### Mehrere Geräte - -Falls mehrere Geräte unter `adb devices` aufgelistet werden, muss die _Seriennummer_ angegeben werden: - -```bash -scrcpy --serial 0123456789abcdef -scrcpy -s 0123456789abcdef # kurze Version -``` - -Falls das Gerät über TCP/IP verbunden ist: - -```bash -scrcpy --serial 192.168.0.1:5555 -scrcpy -s 192.168.0.1:5555 # kurze Version -``` - -Es können mehrere Instanzen von _scrcpy_ für mehrere Geräte gestartet werden. - -#### Autostart beim Verbinden eines Gerätes - -Hierfür kann [AutoAdb] verwendet werden: - -```bash -autoadb scrcpy -s '{}' -``` - -[AutoAdb]: https://github.com/rom1v/autoadb - -#### Tunnel - -Um sich zu einem entfernten Gerät zu verbinden, kann der `adb` Client mit einem remote-`adb`-Server verbunden werden (Voraussetzung: Gleiche Version des `adb`-Protokolls). - -##### Remote ADB Server - -Um sich zu einem Remote-`adb`-Server zu verbinden: Der Server muss auf allen Ports hören - -```bash -adb kill-server -adb -a nodaemon server start -# Diesen Dialog offen halten -``` - -**Warnung: Die gesamte Kommunikation zwischen adb und den Geräten ist unverschlüsselt.** - -Angenommen, der Server ist unter 192.168.1.2 verfügbar. Dann kann von einer anderen Kommandozeile scrcpy aufgeführt werden: - -```bash -export ADB_SERVER_SOCKET=tcp:192.168.1.2:5037 -scrcpy --tunnel-host=192.168.1.2 -``` - -Standardmäßig verwendet scrcpy den lokalen Port für die Einrichtung des `adb forward`-Tunnels (typischerweise `27183`, siehe `--port`). -Es ist zudem möglich, einen anderen Tunnel-Port zuzuweisen (sinnvoll in Situationen, bei welchen viele Weiterleitungen erfolgen): - -``` -scrcpy --tunnel-port=1234 -``` - - -##### SSH Tunnel - -Um mit einem Remote-`adb`-Server sicher zu kommunizieren, wird ein SSH-Tunnel empfohlen. - -Sicherstellen, dass der Remote-`adb`-Server läuft: - -```bash -adb start-server -``` - -Erzeugung eines SSH-Tunnels: - -```bash -# local 5038 --> remote 5037 -# local 27183 <-- remote 27183 -ssh -CN -L5038:localhost:5037 -R27183:localhost:27183 your_remote_computer -# Diesen Dialog geöffnet halten -``` - -Von einer anderen Kommandozeile aus scrcpy ausführen: - -```bash -export ADB_SERVER_SOCKET=tcp:localhost:5038 -scrcpy -``` - -Um das Aktivieren von Remote-Weiterleitung zu verhindern, kann eine Vorwärts-Verbindung verwendet werden (`-L` anstatt von `-R`): - -```bash -# local 5038 --> remote 5037 -# local 27183 --> remote 27183 -ssh -CN -L5038:localhost:5037 -L27183:localhost:27183 your_remote_computer -# Diesen Dialog geöffnet halten -``` - -Von einer anderen Kommandozeile aus scrcpy ausführen: - -```bash -export ADB_SERVER_SOCKET=tcp:localhost:5038 -scrcpy --force-adb-forward -``` - - -Wie für kabellose Verbindungen kann es sinnvoll sein, die Qualität zu reduzieren: - -``` -scrcpy -b2M -m800 --max-fps 15 -``` - -### Fensterkonfiguration - -#### Titel - -Standardmäßig ist der Fenstertitel das Gerätemodell. Der Titel kann jedoch geändert werden: - -```bash -scrcpy --window-title 'Mein Gerät' -``` - -#### Position und Größe - -Die anfängliche Fensterposition und Größe können festgelegt werden: - -```bash -scrcpy --window-x 100 --window-y 100 --window-width 800 --window-height 600 -``` - -#### Rahmenlos - -Um den Rahmen des Fensters zu deaktivieren: - -```bash -scrcpy --window-borderless -``` - -#### Immer im Vordergrund - -Um das Fenster immer im Vordergrund zu halten: - -```bash -scrcpy --always-on-top -``` - -#### Vollbild - -Die Anwendung kann direkt im Vollbildmodus gestartet werden: - -```bash -scrcpy --fullscreen -scrcpy -f # kurze Version -``` - -Das Vollbild kann dynamisch mit MOD+f gewechselt werden. - -#### Rotation - -Das Fenster kann rotiert werden: - -```bash -scrcpy --rotation 1 -``` - -Mögliche Werte sind: - - `0`: keine Rotation - - `1`: 90 grad gegen den Uhrzeigersinn - - `2`: 180 grad - - `3`: 90 grad mit dem Uhrzeigersinn - -die Rotation kann zudem dynamisch mit MOD+ -_(links)_ and MOD+ _(rechts)_ angepasst werden. - -_scrcpy_ schafft 3 verschiedene Rotationen: - - MOD+r erfordert von Gerät den Wechsel zwischen Hochformat und Querformat (die momentane App kann dies verweigern, wenn die geforderte Ausrichtung nicht unterstützt wird). - - [`--lock-video-orientation`](#feststellen-der-videoorientierung) ändert die Ausrichtung der Spiegelung (die Ausrichtung des an den Computer gesendeten Videos). Dies beeinflusst eventuelle Aufnahmen. - - `--rotation` (or MOD+/MOD+) rotiert nur das Fenster, eventuelle Aufnahmen sind hiervon nicht beeinflusst. - - -### Andere Spiegel-Optionen - -#### Lesezugriff - -Um die Steuerung (alles, was mit dem Gerät interagieren kann: Tasten, Mausklicks, Drag-and-drop von Dateien) zu deaktivieren: - -```bash -scrcpy --no-control -scrcpy -n -``` - -#### Anzeige - -Falls mehrere Displays vorhanden sind, kann das zu spiegelnde Display gewählt werden: - -```bash -scrcpy --display 1 -``` - -Die Liste an verfügbaren Displays kann mit diesem Befehl ausgegeben werden: - -```bash -adb shell dumpsys display # Nach "mDisplayId=" in der Ausgabe suchen -``` - -Das zweite Display kann nur gesteuert werden, wenn das Gerät Android 10 oder höher besitzt. Ansonsten wird das Display nur mit Lesezugriff gespiegelt. - - -#### Wach bleiben - -Um zu verhindern, dass das Gerät nach einer Weile in den Ruhezustand übergeht (solange es eingesteckt ist): - -```bash -scrcpy --stay-awake -scrcpy -w -``` - -Der ursprüngliche Zustand wird beim Schließen von scrcpy wiederhergestellt. - - -#### Bildschirm ausschalten - -Es ist möglich, beim Starten des Spiegelns mithilfe eines Kommandozeilenarguments den Bildschirm des Gerätes auszuschalten: - -```bash -scrcpy --turn-screen-off -scrcpy -S -``` - -Oder durch das Drücken von MOD+o jederzeit. - -Um das Display wieder einzuschalten muss MOD+Shift+o gedrückt werden. - -Auf Android aktiviert der `POWER` Knopf das Display immer. -Für den Komfort wird, wenn `POWER` via scrcpy gesendet wird (über Rechtsklick oder MOD+p), wird versucht, das Display nach einer kurzen Zeit wieder auszuschalten (falls es möglich ist). -Der physische `POWER` Button aktiviert das Display jedoch immer. - -Dies kann zudem nützlich sein, um das Gerät vom Ruhezustand abzuhalten: - -```bash -scrcpy --turn-screen-off --stay-awake -scrcpy -Sw -``` - -#### Ausschalten beim Schließen - -Um den Gerätebildschirm abzuschalten, wenn scrcpy geschlossen wird: - -```bash -scrcpy --power-off-on-close -``` - - -#### Anzeigen von Berührungen - -Für Präsentationen kann es sinnvoll sein, die physischen Berührungen anzuzeigen (auf dem physischen Gerät). - -Android stellt dieses Feature in den _Entwickleroptionen_ zur Verfügung. - -_Scrcpy_ stellt die Option zur Verfügung, dies beim Start zu aktivieren und beim Schließen auf den Ursprungszustand zurückzusetzen: - -```bash -scrcpy --show-touches -scrcpy -t -``` - -Anmerkung: Nur _physische Berührungen_ werden angezeigt (mit dem Finger auf dem Gerät). - - -#### Bildschirmschoner deaktivieren - -Standardmäßig unterbindet scrcpy nicht den Bildschirmschoner des Computers. - -Um den Bildschirmschoner zu unterbinden: - -```bash -scrcpy --disable-screensaver -``` - - -### Eingabesteuerung - -#### Geräte-Bildschirm drehen - -MOD+r drücken, um zwischen Hoch- und Querformat zu wechseln. - -Anmerkung: Dis funktioniert nur, wenn die momentan geöffnete App beide Rotationen unterstützt. - -#### Copy-paste - -Immer, wenn sich die Zwischenablage von Android ändert wird dies mit dem Computer synchronisiert. - -Jedes Strg wird an das Gerät weitergegeben. Insbesonders: - - Strg+c kopiert typischerweise - - Strg+x schneidet typischerweise aus - - Strg+v fügt typischerweise ein (nach der Computer-zu-Gerät-Synchronisation) - -Dies funktioniert typischerweise wie erwartet. - -Die wirkliche Funktionsweise hängt jedoch von der jeweiligen Anwendung ab. Beispielhaft sendet _Termux_ SIGINT bei Strg+c, und _K-9 Mail_ erzeugt eine neue Nachricht. - -Um kopieren, ausschneiden und einfügen in diesen Fällen zu verwenden (nur bei Android >= 7 unterstützt): - - MOD+c gibt `COPY` ein - - MOD+x gibt `CUT` ein - - MOD+v gibt `PASTE` ein (nach der Computer-zu-Gerät-Synchronisation) - -Zusätzlich erlaubt es MOD+Shift+v den momentanen Inhalt der Zwischenablage als eine Serie von Tastenevents einzugeben. -Dies ist nützlich, fall die Applikation kein Einfügen unterstützt (z.B. _Termux_). Jedoch kann nicht-ASCII-Inhalt dabei zerstört werden. - -**WARNUNG:** Das Einfügen der Computer-Zwischenablage in das Gerät (entweder mit Strg+v oder MOD+v) kopiert den Inhalt in die Zwischenablage des Gerätes. -Als Konsequenz kann somit jede Android-Applikation diesen Inhalt lesen. Das Einfügen von sensiblen Informationen wie Passwörtern sollte aus diesem Grund vermieden werden. - -Mache Geräte verhalten sich nicht wie erwartet, wenn die Zwischenablage per Programm verändert wird. -Die Option `--legacy-paste` wird bereitgestellt, welche das Verhalten von Strg+v und MOD+v so ändert, dass die Zwischenablage wie bei MOD+Shift+v als eine Serie von Tastenevents ausgeführt wird. - -Um die automatische Synchronisierung der Zwischenablage zu deaktivieren: -`--no-clipboard-autosync`. - -#### Ziehen zum Zoomen - -Um "Ziehen-zum-Zoomen" zu simulieren: Strg+_klicken-und-bewegen_. - -Genauer: Strg halten, während Linksklick gehalten wird. Solange Linksklick gehalten wird, skalieren und rotieren die Mausbewegungen den Inhalt (soweit von der jeweiligen App unterstützt). - -Konkret erzeugt scrcpy einen am Mittelpunkt des Displays gespiegelten, "virtuellen" Finger. - -#### Simulation einer physischen Tastatur mit HID - -Standardmäßig verwendet scrcpy Android-Tasten oder Textinjektion. Dies funktioniert zwar immer, jedoch nur mit ASCII. - -Auf Linux kann scrcpy mithilfe von HID eine physische Tastatur simulieren, um eine bessere Eingabeerfahrung zu gewährleisten (dies nutzt [USB HID over AOAv2][hid-aoav2]): Die virtuelle Tastatur wird deaktiviert, es funktioniert für alle Zeichen und mit IME. - -[hid-aoav2]: https://source.android.com/devices/accessories/aoa2#hid-support - -Dies funktioniert jedoch nur, wenn das Gerät über USB verbunden ist. Zudem wird dies momentan nur unter Linux unterstützt. - -Um diesen Modus zu aktivieren: - -```bash -scrcpy --hid-keyboard -scrcpy -K # kurze Version -``` - -Falls dies auf gewissen Gründen fehlschlägt (z.B. Gerät ist nicht über USB verbunden), so fällt scrcpy auf den Standardmodus zurück (mit einer Ausgabe in der Konsole). -Dies erlaubt es, dieselben Kommandozeilenargumente zu verwenden, egal ob das Gerät per USB oder TCP/IP verbunden ist. - -In diesem Modus werden rohe Tastenevents (scancodes) an das Gerät gesendet. -Aus diesem Grund muss ein nicht passenden Tastaturformat in den Einstellungen des Android-Gerätes unter Einstellungen → System → Sprache und Eingabe → [Physical keyboard] konfiguriert werden. - -Diese Einstellungsseite kann direkt mit diesem Befehl geöffnet werden: - -```bash -adb shell am start -a android.settings.HARD_KEYBOARD_SETTINGS -``` - -Diese Option ist jedoch nur verfügbar, wenn eine HID-Tastatur oder eine physische Tastatur verbunden sind. - -[Physical keyboard]: https://github.com/Genymobile/scrcpy/pull/2632#issuecomment-923756915 - -#### Simulation einer physischen Maus mit HID - -Ähnlich zu einer Tastatur kann auch eine Maus mithilfe von HID simuliert werden. -Wie zuvor funktioniert dies jedoch nur, wenn das Gerät über USB verbunden ist. Zudem wird dies momentan nur unter Linux unterstützt. - -Standardmäßig verwendet scrcpy Android Maus Injektionen mit absoluten Koordinaten. -Durch die Simulation einer physischen Maus erscheint auf dem Display des Geräts ein Mauszeiger, zu welchem die Bewegungen, Klicks und Scrollbewegungen relativ eingegeben werden. - -Um diesen Modus zu aktivieren: - -```bash -scrcpy --hid-mouse -scrcpy -M # kurze Version -``` - -Es kann zudem`--forward-all-clicks` übergeben werden, um [alle Mausklicks an das Gerät weiterzugeben](#rechtsklick-und-mittelklick). - -Wenn dieser Modus aktiv ist, ist der Mauszeiger des Computers auf dem Fenster gefangen (Zeiger verschwindet von Computer und erscheint auf dem Android-Gerät). - -Spezielle Tasteneingaben wie Alt oder Super ändern den Zustand des Mauszeigers (geben diesen wieder frei/fangen ihn wieder ein). -Eine dieser Tasten kann verwendet werden, um die Kontrolle der Maus wieder zurück an den Computer zu geben. - - -#### OTG - -Es ist möglich, _scrcpy_ so auszuführen, dass nur Maus und Tastatur, wie wenn diese direkt über ein OTG-Kabel verbunden wären, simuliert werden. - -In diesem Modus ist _adb_ nicht nötig, ebenso ist das Spiegeln der Anzeige deaktiviert. - -Um den OTG-Modus zu aktivieren: - -```bash -scrcpy --otg -# Seriennummer übergeben, falls mehrere Geräte vorhanden sind -scrcpy --otg -s 0123456789abcdef -``` - -Es ist möglich, nur HID-Tastatur oder HID-Maus zu aktivieren: - -```bash -scrcpy --otg --hid-keyboard # nur Tastatur -scrcpy --otg --hid-mouse # nur Maus -scrcpy --otg --hid-keyboard --hid-mouse # Tastatur und Maus -# Der Einfachheit halber sind standardmäßig beide aktiv -scrcpy --otg # Tastatur und Maus -``` - -Wie `--hid-keyboard` und `--hid-mouse` funktioniert dies nur, wenn das Gerät per USB verbunden ist. -Zudem wird dies momentan nur unter Linux unterstützt. - - -#### Textinjektions-Vorliebe - -Beim Tippen von Text werden zwei verschiedene [Events][textevents] generiert: - - _key events_, welche signalisieren, ob eine Taste gedrückt oder losgelassen wurde; - - _text events_, welche signalisieren, dass Text eingegeben wurde. - -Standardmäßig werden key events verwendet, da sich bei diesen die Tastatur in Spielen wie erwartet verhält (typischerweise für WASD). - -Dies kann jedoch [Probleme verursachen][prefertext]. Trifft man auf ein solches Problem, so kann dies mit diesem Befehl umgangen werden: - -```bash -scrcpy --prefer-text -``` - -Dies kann jedoch das Tastaturverhalten in Spielen beeinträchtigen/zerstören. - -Auf der anderen Seite kann jedoch auch die Nutzung von key events erzwungen werden: - -```bash -scrcpy --raw-key-events -``` - -Diese Optionen haben jedoch keinen Einfluss auf eine etwaige HID-Tastatur, da in diesem modus alle key events als scancodes gesendet werden. - -[textevents]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-text-input -[prefertext]: https://github.com/Genymobile/scrcpy/issues/650#issuecomment-512945343 - - -#### Wiederholen von Tasten - -Standardmäßig löst das gedrückt halten einer Taste das jeweilige Event mehrfach aus. Dies kann jedoch zu Performanceproblemen in manchen Spielen führen. - -Um das Weitergeben von sich wiederholenden Tasteneingaben zu verhindern: - -```bash -scrcpy --no-key-repeat -``` - -This option has no effect on HID keyboard (key repeat is handled by Android -directly in this mode). - - -#### Rechtsklick und Mittelklick - -Standardmäßig löst Rechtsklick BACK (wenn Bildschirm aus: POWER) und Mittelklick BACK aus. Um diese Kürzel abzuschalten und stattdessen die Eingaben direkt an das Gerät weiterzugeben: - -```bash -scrcpy --forward-all-clicks -``` - - -### Dateien ablegen - -#### APK installieren - -Um eine AKP zu installieren, kann diese per Drag-and-drop auf das _scrcpy_-Fenster gezogen werden. - -Dabei erfolgt kein visuelles Feedback, ein Log wird in die Konsole ausgegeben. - - -#### Datei auf Gerät schieben - -Um eine Datei nach `/sdcard/Download/` auf dem Gerät zu schieben, Drag-and-drop die (nicht-APK)-Datei auf das _scrcpy_-Fenster. - -Dabei erfolgt kein visuelles Feedback, ein Log wird in die Konsole ausgegeben. - -Das Zielverzeichnis kann beim Start geändert werden: - -```bash -scrcpy --push-target=/sdcard/Movies/ -``` - - -### Audioweitergabe - -Audio wird von _scrcpy_ nicht übertragen. Hierfür kann [sndcpy] verwendet werden. - -Siehe zudem [issue #14]. - -[sndcpy]: https://github.com/rom1v/sndcpy -[issue #14]: https://github.com/Genymobile/scrcpy/issues/14 - - -## Tastenkürzel - -In der folgenden Liste ist MOD der Kürzel-Auslöser. Standardmäßig ist dies (links) Alt oder (links) Super. - -Dies kann mithilfe von `--shortcut-mod` geändert werden. Mögliche Tasten sind `lstrg`, `rstrg`, -`lalt`, `ralt`, `lsuper` und `rsuper`. Beispielhaft: - -```bash -# Nutze rStrg als Auslöser -scrcpy --shortcut-mod=rctrl - -# Nutze entweder LStrg+LAlt oder LSuper für Tastenkürzel -scrcpy --shortcut-mod=lctrl+lalt,lsuper -``` - -_[Super] ist typischerweise die Windows oder Cmd Taste._ - -[Super]: https://en.wikipedia.org/wiki/Super_key_(keyboard_button) - -| Aktion | Tastenkürzel | | -|--------------------------------------------------------|-----------------------------------------------------------|:-------------------------| -| Vollbild wechseln | MOD+f | | -| Display nach links rotieren | MOD+ _(links)_ | | -| Display nach links rotieren | MOD+ _(rechts)_ | | -| Fenstergröße 1:1 replizieren (pixel-perfect) | MOD+g | | -| Fenstergröße zum entfernen der schwarzen Balken ändern | MOD+w | _Doppel-Linksklick¹_ | -| Klick auf `HOME` | MOD+h | _Mittelklick_ | -| Klick auf `BACK` | MOD+b | _Rechtsklick²_ | -| Klick auf `APP_SWITCH` | MOD+s | _4.-Taste-Klick³_ | -| Klick auf `MENU` (Bildschirm entsperren)⁴ | MOD+m | | -| Klick auf `VOLUME_UP` | MOD+ _(hoch)_ | | -| CKlick auf `VOLUME_DOWN` | MOD+ _(runter)_ | | -| Klick auf `POWER` | MOD+p | | -| Power an | _Rechtsklick²_ | | -| Gerätebildschirm ausschalten (weiterhin spiegeln) | MOD+o | | -| Gerätebildschirm einschalten | MOD+Shift+o | | -| Gerätebildschirm drehen | MOD+r | | -| Benachrichtigungs-Bereich anzeigen | MOD+n | _5.-Taste-Klick³_ | -| Erweitertes Einstellungs-Menü anzeigen | MOD+n+n | _Doppel-5.-Taste-Klick³_ | -| Bedienfelder einklappen | MOD+Shift+n | | -| In die Zwischenablage kopieren⁵ | MOD+c | | -| In die Zwischenablage kopieren⁵ | MOD+x | | -| Zwischenablage synchronisieren und einfügen⁵ | MOD+v | | -| Computer-Zwischenablage einfügen (per Tastenevents) | MOD+Shift+v | | -| FPS-Zähler aktivieren/deaktivieren (ing stdout) | MOD+i | | -| Ziehen zum Zoomen | Strg+_Klicken-und-Bewegen_ | | -| Drag-and-drop mit APK-Datei | APK von Computer installieren | | -| Drag-and-drop mit Nicht-APK Datei | [Datei auf das Gerät schieben](#datei-auf-gerät-schieben) | | - - -_¹Doppelklick auf die schwarzen Balken, um diese zu entfernen._ -_²Rechtsklick aktiviert den Bildschirm, falls dieser aus war, ansonsten ZURÜCK._ -_³4. und 5. Maustasten, wenn diese an der jeweiligen Maus vorhanden sind._ -_⁴Für react-native Applikationen in Entwicklung, `MENU` öffnet das Entwickler-Menü._ -_⁵Nur für Android >= 7._ - -Abkürzungen mit mehreren Tastenanschlägen werden durch das Loslassen und erneute Drücken der Taste erreicht. -Beispielhaft, um "Erweitere das Einstellungs-Menü" auszuführen: - - 1. Drücke und halte MOD. - 2. Doppelklicke n. - 3. Lasse MOD los. - -Alle Strg+_Taste_ Tastenkürzel werden an das Gerät übergeben, sodass sie von der jeweiligen Applikation ausgeführt werden können. - - -## Personalisierte Pfade - -Um eine spezifische _adb_ Binary zu verwenden, muss deren Pfad als Umgebungsvariable `ADB` deklariert werden: - -```bash -ADB=/path/to/adb scrcpy -``` - -Um den Pfad der `scrcpy-server` Datei zu bearbeiten, muss deren Pfad in `SCRCPY_SERVER_PATH` bearbeitet werden. - -Um das Icon von scrcpy zu ändern, muss `SCRCPY_ICON_PATH` geändert werden. - - -## Warum _scrcpy_? - -Ein Kollege hat mich dazu herausgefordert, einen Namen so unaussprechbar wie [gnirehtet] zu finden. - -[`strcpy`] kopiert einen **str**ing; `scrcpy` kopiert einen **scr**een. - -[gnirehtet]: https://github.com/Genymobile/gnirehtet -[`strcpy`]: http://man7.org/linux/man-pages/man3/strcpy.3.html - - -## Selbst bauen? - -Siehe [BUILD]. - - -## Typische Fehler - -Siehe [FAQ](FAQ.md). - - -## Entwickler - -[Entwicklerseite](DEVELOP.md). - - -## Licence - - Copyright (C) 2018 Genymobile - Copyright (C) 2018-2022 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 (auf Englisch) - -- [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.id.md b/README.id.md deleted file mode 100644 index 1230f0cc..00000000 --- a/README.id.md +++ /dev/null @@ -1,696 +0,0 @@ -_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 : - - - [README](README.md#windows) - -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-2022 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.it.md b/README.it.md deleted file mode 100644 index a35c7bbb..00000000 --- a/README.it.md +++ /dev/null @@ -1,1041 +0,0 @@ -_Apri il [README](README.md) originale (in inglese) e sempre aggiornato._ - -scrcpy - -# scrcpy (v1.23) - -_si pronuncia "**scr**een **c**o**py**"_ - -[Leggi in altre lingue](#traduzioni) - -Questa applicazione fornisce la visualizzazione e il controllo di dispositivi Android collegati via USB (o [via TCP/IP](#tcpip-wireless)). Non richiede alcun accesso _root_. -Funziona su _GNU/Linux_, _Windows_ e _macOS_. - -![screenshot](assets/screenshot-debian-600.jpg) - -Si concentra su: - - - **leggerezza**: nativo, mostra solo lo schermo del dispositivo - - **prestazioni**: 30~120fps, in funzione del dispositivo - - **qualità**: 1920×1080 o superiore - - **bassa latenza**: [35~70ms][lowlatency] - - **tempo di avvio basso**: ~ 1secondo per visualizzare la prima immagine - - **non invadenza**: nulla rimane installato sul dispositivo - - **vantaggi per l'utente**: nessun account, nessuna pubblicità, non è richiesta alcuna connessione a internet - - **libertà**: software libero e a codice aperto (_free and open source_) - -[lowlatency]: https://github.com/Genymobile/scrcpy/pull/646 - -Le sue caratteristiche includono: - - [registrazione](#registrazione) - - mirroring con [schermo del dispositivo spento](#spegnere-lo-schermo) - - [copia-incolla](#copia-incolla) in entrambe le direzioni - - [qualità configurabile](#configurazione-di-acquisizione) - - schermo del dispositivo [come webcam (V4L2)](#v4l2loopback) (solo per Linux) - - [simulazione della tastiera fisica (HID)](#simulazione-della-tastiera-fisica-HID) - - [simulazione mouse fisico (HID)](#simulazione-del-mouse-fisico-HID) - - [modalità OTG](#otg) - - e altro ancora... - - -## Requisiti - -Il dispositivo Android richiede almeno le API 21 (Android 5.0). - -Assiucurati di aver [attivato il debug usb][enable-adb] sul(/i) tuo(i) dispositivo(/i). - -[enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling - -In alcuni dispositivi, devi anche abilitare [un'opzione aggiuntiva][control] per controllarli con tastiera e mouse. - -[control]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323 - -## Ottieni l'app - -Packaging status - -### Sommario - - - Linux: `apt install scrcpy` - - Windows: [download](README.md#windows) - - macOS: `brew install scrcpy` - -Compila dai sorgenti: [BUILD] (in inglese) ([procedimento semplificato][BUILD_simple] (in inglese)) - -[BUILD]: BUILD.md -[BUILD_simple]: BUILD.md#simple - - -### Linux - -Su Debian e Ubuntu: - -``` -apt install scrcpy -``` - -Su Arch Linux: - -``` -pacman -S scrcpy -``` - -È disponibile anche un pacchetto [Snap]: [`scrcpy`][snap-link]. - -[snap-link]: https://snapstats.org/snaps/scrcpy - -[snap]: https://it.wikipedia.org/wiki/Snappy_(gestore_pacchetti) - -Per Fedora, è disponibile un pacchetto [COPR]: [`scrcpy`][copr-link]. - -[COPR]: https://fedoraproject.org/wiki/Category:Copr -[copr-link]: https://copr.fedorainfracloud.org/coprs/zeno/scrcpy/ - - -Per Gentoo, è disponibile una [Ebuild]: [`scrcpy/`][ebuild-link]. - -[Ebuild]: https://wiki.gentoo.org/wiki/Ebuild -[ebuild-link]: https://github.com/maggu2810/maggu2810-overlay/tree/master/app-mobilephone/scrcpy - -Puoi anche [compilare l'app manualmente][BUILD] (in inglese) ([procedimento semplificato][BUILD_simple] (in inglese)). - - -### Windows - -Per Windows, per semplicità è disponibile un archivio precompilato con tutte le dipendenze (incluso `adb`): - - - [README](README.md#windows) (Link al README originale per l'ultima versione) - -È anche disponibile in [Chocolatey]: - -[Chocolatey]: https://chocolatey.org/ - -```bash -choco install scrcpy -choco install adb # se non lo hai già -``` - -E in [Scoop]: - -```bash -scoop install scrcpy -scoop install adb # se non lo hai già -``` - -[Scoop]: https://scoop.sh - -Puoi anche [compilare l'app manualmente][BUILD] (in inglese). - - -### macOS - -L'applicazione è disponibile in [Homebrew]. Basta installarlo: - -[Homebrew]: https://brew.sh/ - -```bash -brew install scrcpy -``` - -Serve che `adb` sia accessibile dal tuo `PATH`. Se non lo hai già: - -```bash -brew install android-platform-tools -``` - -È anche disponibile in [MacPorts], che imposta adb per te: - -```bash -sudo port install scrcpy -``` - -[MacPorts]: https://www.macports.org/ - - -Puoi anche [compilare l'app manualmente][BUILD] (in inglese). - - -## Esecuzione - -Collega un dispositivo Android ed esegui: - -```bash -scrcpy -``` - -Scrcpy accetta argomenti da riga di comando, elencati con: - -```bash -scrcpy --help -``` - -## Funzionalità - -### Configurazione di acquisizione - -#### Riduci dimensione - -Qualche volta è utile trasmettere un dispositvo Android ad una definizione inferiore per aumentare le prestazioni. - -Per limitare sia larghezza che altezza ad un certo valore (ad es. 1024): - -```bash -scrcpy --max-size 1024 -scrcpy -m 1024 # versione breve -``` - -L'altra dimensione è calcolata in modo tale che il rapporto di forma del dispositivo sia preservato. -In questo esempio un dispositivo in 1920x1080 viene trasmesso a 1024x576. - - -#### Cambia bit-rate (velocità di trasmissione) - -Il bit-rate predefinito è 8 Mbps. Per cambiare il bitrate video (ad es. a 2 Mbps): - -```bash -scrcpy --bit-rate 2M -scrcpy -b 2M # versione breve -``` - -#### Limitare il frame rate (frequenza di fotogrammi) - -Il frame rate di acquisizione può essere limitato: - -```bash -scrcpy --max-fps 15 -``` - -Questo è supportato ufficialmente a partire da Android 10, ma potrebbe funzionare in versioni precedenti. - -L'attuale frame rate di acquisizione può essere stampato sulla console: - -``` -scrcpy --print-fps -``` - -Può anche essere abilitato o disabilitato in qualsiasi momento con MOD+i. - -#### Ritaglio - -Lo schermo del dispositivo può essere ritagliato per visualizzare solo parte di esso. - -Questo può essere utile, per esempio, per trasmettere solo un occhio dell'Oculus Go: - -```bash -scrcpy --crop 1224:1440:0:0 # 1224x1440 at offset (0,0) -``` - -Se anche `--max-size` è specificata, il ridimensionamento è applicato dopo il ritaglio. - - -#### Blocca orientamento del video - - -Per bloccare l'orientamento della trasmissione: - -```bash -scrcpy --lock-video-orientation # orientamento iniziale (corrente) -scrcpy --lock-video-orientation=0 # orientamento naturale -scrcpy --lock-video-orientation=1 # 90° antiorario -scrcpy --lock-video-orientation=2 # 180° -scrcpy --lock-video-orientation=3 # 90° orario -``` - -Questo influisce sull'orientamento della registrazione. - - -La [finestra può anche essere ruotata](#rotazione) indipendentemente. - - -#### Codificatore - -Alcuni dispositivi hanno più di un codificatore e alcuni di questi possono provocare problemi o crash. È possibile selezionare un encoder diverso: - -```bash -scrcpy --encoder OMX.qcom.video.encoder.avc -``` - -Per elencare i codificatori disponibili puoi immettere un nome di codificatore non valido e l'errore mostrerà i codificatori disponibili: - -```bash -scrcpy --encoder _ -``` - -### Cattura - -#### Registrazione - -È possibile registrare lo schermo durante la trasmissione: - -```bash -scrcpy --record file.mp4 -scrcpy -r file.mkv -``` - -Per disabilitare la trasmissione durante la registrazione: - -```bash -scrcpy --no-display --record file.mp4 -scrcpy -Nr file.mkv -# interrompere la registrazione con Ctrl+C -``` - -I "fotogrammi saltati" sono registrati nonostante non siano mostrati in tempo reale (per motivi di prestazioni). I fotogrammi sono _datati_ sul dispositivo, così una [variazione di latenza dei pacchetti][packet delay variation] non impatta il file registrato. - -[packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation - - -#### v4l2loopback - -Su Linux è possibile inviare il flusso video ad un dispositivo v4l2 loopback, cosicché un dispositivo Android possa essere aperto come una webcam da qualsiasi strumento compatibile con v4l2. - -Il modulo `v4l2loopback` deve essere installato: - -```bash -sudo apt install v4l2loopback-dkms -``` - -Per creare un dispositvo v4l2: - -```bash -sudo modprobe v4l2loopback -``` - -Questo creerà un nuovo dispositivo video in `/dev/videoN` dove `N` è un intero (più [opzioni](https://github.com/umlaeute/v4l2loopback#options) sono disponibili per crere più dispositivi o dispositivi con ID specifici). - -Per elencare i dispositvi attivati: - -```bash -# necessita del pacchetto v4l-utils -v4l2-ctl --list-devices - -# semplice ma potrebbe essere sufficiente -ls /dev/video* -``` - -Per avviare scrcpy utilizzando un v4l2 sink: - -```bash -scrcpy --v4l2-sink=/dev/videoN -scrcpy --v4l2-sink=/dev/videoN --no-display # disabilita la finestra di trasmissione -scrcpy --v4l2-sink=/dev/videoN -N # versione corta -``` - -(sostituisci `N` con l'ID del dispositivo, controlla con `ls /dev/video*`) - -Una volta abilitato, puoi aprire il tuo flusso video con uno strumento compatibile con v4l2: - -```bash -ffplay -i /dev/videoN -vlc v4l2:///dev/videoN # VLC potrebbe aggiungere del ritardo per il buffer -``` - -Per esempio potresti catturare il video in [OBS]. - -[OBS]: https://obsproject.com/ - - -#### Buffering - -È possibile aggiungere del buffer. Questo aumenta la latenza ma riduce il jitter (vedi [#2464]). - -[#2464]: https://github.com/Genymobile/scrcpy/issues/2464 - -L'opzione è disponibile per il buffer della visualizzazione: - -```bash -scrcpy --display-buffer=50 # aggiungi 50 ms di buffer per la visualizzazione -``` - -e per il V4L2 sink: - -```bash -scrcpy --v4l2-buffer=500 # aggiungi 500 ms di buffer per il v4l2 sink -``` - - -### Connessione - -#### TCP/IP (wireless) - -_Scrcpy_ usa `adb` per comunicare col dispositivo e `adb` può [connettersi][connect] a un dispositivo mediante TCP/IP. Il dispositivo deve essere collegato alla stessa rete del computer. - -##### Automatico - -Un'opzione `--tcpip` permette di configurare automaticamente la connessione. Ci sono due varianti. - -Se il dispositivo (accessibile a 192.168.1.1 in questo esempio) ascolta già su una porta (tipicamente 5555) per le connessioni adb in entrata, allora esegui: - -```bash -scrcpy --tcpip=192.168.1.1 # la porta predefinita è 5555 -scrcpy --tcpip=192.168.1.1:5555 -``` - -Se la modalità TCP/IP di adb è disabilitata sul dispositivo (o se non si conosce l'indirizzo IP indirizzo), collegare il dispositivo tramite USB, quindi eseguire: - -```bash -scrcpy --tcpip # senza argomenti -``` - -Il comando troverà automaticamente l'indirizzo IP del dispositivo, abiliterà la modalità TCP/IP, quindi connettersi al dispositivo prima di iniziare. - -##### Manuale - -In alternativa, è possibile abilitare la connessione TCP/IP manualmente usando `adb`: - -1. Inserisci il dispositivo in una porta USB del tuo computer. -2. Connetti il dispositivo alla stessa rete Wi-Fi del tuo computer. -3. Ottieni l'indirizzo IP del tuo dispositivo, in Impostazioni → Informazioni sul telefono → Stato, o - eseguendo questo comando: - - ```bash - adb shell ip route | awk '{print $9}' - ``` - -4. Abilita adb via TCP/IP sul tuo dispositivo: `adb tcpip 5555`. -5. Scollega il tuo dispositivo. -6. Connettiti al tuo dispositivo: `adb connect DEVICE_IP:5555` _(sostituisci `DEVICE_IP` -con l'indirizzo IP del dispositivo che hai trovato)_. -7. Esegui `scrcpy` come al solito. - -Da Android 11, una [opzione di debug wireless][adb-wireless] permette di evitare di dover collegare fisicamente il dispositivo direttamente al computer. - -[adb-wireless]: https://developer.android.com/studio/command-line/adb#connect-to-a-device-over-wi-fi-android-11+ - -Se la connessione cade casualmente, esegui il comando `scrcpy` per riconnetterti. Se il comando dice che non ci sono dispositivi/emulatori trovati, prova ad eseguire `adb connect DEVICE_IP:5555` di nuovo, e poi `scrcpy` come al solito. Se dice ancora che non ne ha trovato nessuno, prova ad eseguire `adb disconnect` e poi esegui di nuovo questi due comandi. - -Potrebbe essere utile diminuire il bit-rate e la definizione: - -```bash -scrcpy --bit-rate 2M --max-size 800 -scrcpy -b2M -m800 # versione breve -``` - -[connect]: https://developer.android.com/studio/command-line/adb.html#wireless - -#### Multi dispositivo - -Se in `adb devices` sono elencati più dispositivi, è necessario specificare il _seriale_: - -```bash -scrcpy --serial 0123456789abcdef -scrcpy -s 0123456789abcdef # versione breve -``` - -Se il dispositivo è collegato mediante TCP/IP: - -```bash -scrcpy --serial 192.168.0.1:5555 -scrcpy -s 192.168.0.1:5555 # versione breve -``` - -Se solo un dispositivo è collegato via USB o TCP/IP, è possibile selezionarlo automaticamente: - -```bash -# Select the only device connected via USB -scrcpy -d # like adb -d -scrcpy --select-usb # long version - -# Select the only device connected via TCP/IP -scrcpy -e # like adb -e -scrcpy --select-tcpip # long version -``` - -Puoi avviare più istanze di _scrcpy_ per diversi dispositivi. - - -#### Avvio automativo alla connessione del dispositivo - -Potresti usare [AutoAdb]: - -```bash -autoadb scrcpy -s '{}' -``` - -[AutoAdb]: https://github.com/rom1v/autoadb - -#### Tunnels - -Per connettersi a un dispositivo remoto, è possibile collegare un client `adb` locale a un server remoto `adb` (purché usino la stessa versione del protocollo _adb_). ). - -##### Server ADB remoto - -Per connettersi a un server ADB remoto, fate ascoltare il server su tutte le interfacce: - -```bash -adb kill-server -adb -a nodaemon server start -# tienilo aperto -``` - -**Attenzione: tutte le comunicazioni tra i client e il server ADB non sono criptate.** - -Supponi che questo server sia accessibile a 192.168.1.2. Poi, da un altro terminale, esegui scrcpy: - -```bash -export ADB_SERVER_SOCKET=tcp:192.168.1.2:5037 -scrcpy --tunnel-host=192.168.1.2 -``` - -Per impostazione predefinita, scrcpy utilizza la porta locale utilizzata per il tunnel `adb forward` (tipicamente `27183`, vedi `--port`). È anche possibile forzare una diversa porta del tunnel (può essere utile in situazioni più complesse, quando sono coinvolti più reindirizzamenti): - -``` -scrcpy --tunnel-port=1234 -``` - -##### SSH tunnel - -Per comunicare con un server ADB remoto in modo sicuro, è preferibile utilizzare un tunnel SSH. - -Per prima cosa, assicurati che il server ADB sia in esecuzione sul computer remoto: - -```bash -adb start-server -``` - -Poi, crea un tunnel SSH: - -```bash -# local 5038 --> remote 5037 -# local 27183 <-- remote 27183 -ssh -CN -L5038:localhost:5037 -R27183:localhost:27183 your_remote_computer -# keep this open -``` - -Da un altro terminale, esegui scrcpy: - -```bash -export ADB_SERVER_SOCKET=tcp:localhost:5038 -scrcpy -``` - -Per evitare l'abilitazione dell'apertura porte remota potresti invece forzare una "forward connection" (notare il `-L` invece di `-R`) - -```bash -# local 5038 --> remote 5037 -# local 27183 --> remote 27183 -ssh -CN -L5038:localhost:5037 -L27183:localhost:27183 your_remote_computer -# tieni questo aperto -``` - -Da un altro terminale, esegui scrcpy: - -```bash -export ADB_SERVER_SOCKET=tcp:localhost:5038 -scrcpy --force-adb-forward -``` - -Come per le connessioni wireless potrebbe essere utile ridurre la qualità: - -``` -scrcpy -b2M -m800 --max-fps 15 -``` - -### Configurazione della finestra - -#### Titolo - -Il titolo della finestra è il modello del dispositivo per impostazione predefinita. Esso può essere cambiato: - -```bash -scrcpy --window-title 'My device' -``` - -#### Posizione e dimensione - -La posizione e la dimensione iniziale della finestra può essere specificata: - -```bash -scrcpy --window-x 100 --window-y 100 --window-width 800 --window-height 600 -``` - -#### Senza bordi - -Per disabilitare le decorazioni della finestra: - -```bash -scrcpy --window-borderless -``` - -#### Sempre in primo piano - -Per tenere scrcpy sempre in primo piano: - -```bash -scrcpy --always-on-top -``` - -#### Schermo intero - -L'app può essere avviata direttamente a schermo intero: - -```bash -scrcpy --fullscreen -scrcpy -f # versione breve -``` - -Lo schermo intero può anche essere attivato/disattivato con MOD+f. - -#### Rotazione - -La finestra può essere ruotata: - -```bash -scrcpy --rotation 1 -``` - -I valori possibili sono: - - `0`: nessuna rotazione - - `1`: 90 gradi antiorari - - `2`: 180 gradi - - `3`: 90 gradi orari - -La rotazione può anche essere cambiata dinamicamente con MOD+ -_(sinistra)_ e MOD+ _(destra)_. - -Notare che _scrcpy_ gestisce 3 diversi tipi di rotazione: - - MOD+r richiede al dispositvo di cambiare tra orientamento verticale (portrait) e orizzontale (landscape) (l'app in uso potrebbe rifiutarsi se non supporta l'orientamento richiesto). - - [`--lock-video-orientation`](#blocca-orientamento-del-video) cambia l'orientamento della trasmissione (l'orientamento del video inviato dal dispositivo al computer). Questo influenza la registrazione. - - `--rotation` (o MOD+/MOD+) ruota solo il contenuto della finestra. Questo influenza solo la visualizzazione, non la registrazione. - - -### Altre opzioni di trasmissione - -#### "Sola lettura" - -Per disabilitare i controlli (tutto ciò che può interagire col dispositivo: tasti di input, eventi del mouse, trascina e rilascia (drag&drop) file): - -```bash -scrcpy --no-control -scrcpy -n -``` - -#### Schermo - -Se sono disponibili più schermi, è possibile selezionare lo schermo da trasmettere: - -```bash -scrcpy --display 1 -``` - -La lista degli id schermo può essere ricavata da: - -```bash -adb shell dumpsys display # cerca "mDisplayId=" nell'output -``` - -Lo schermo secondario potrebbe essere possibile controllarlo solo se il dispositivo esegue almeno Android 10 (in caso contrario è trasmesso in modalità sola lettura). - - -#### Mantenere sbloccato - -Per evitare che il dispositivo si blocchi dopo un po' che il dispositivo è collegato: - -```bash -scrcpy --stay-awake -scrcpy -w -``` - -Lo stato iniziale è ripristinato quando scrcpy viene chiuso. - - -#### Spegnere lo schermo - -È possibile spegnere lo schermo del dispositivo durante la trasmissione con un'opzione da riga di comando: - -```bash -scrcpy --turn-screen-off -scrcpy -S -``` - -Oppure premendo MOD+o in qualsiasi momento. - -Per riaccenderlo premere MOD+Shift+o. - -In Android il pulsante `POWER` (tasto di accensione) accende sempre lo schermo. Per comodità, se `POWER` è inviato via scrcpy (con click destro o con MOD+p), si forza il dispositivo a spegnere lo schermo dopo un piccolo ritardo (appena possibile). -Il pulsante fisico `POWER` continuerà ad accendere lo schermo normalmente. - -Può anche essere utile evitare il blocco del dispositivo: - -```bash -scrcpy --turn-screen-off --stay-awake -scrcpy -Sw -``` - -#### Spegnimento alla chiusura - -Per spegnere lo schermo del dispositivo quando si chiude scrcpy: - -```bash -scrcpy --power-off-on-close -``` - - -#### Mostrare i tocchi - -Per le presentazioni può essere utile mostrare i tocchi fisici (sul dispositivo fisico). - -Android fornisce questa funzionalità nelle _Opzioni sviluppatore_. - -_Scrcpy_ fornisce un'opzione per abilitare questa funzionalità all'avvio e ripristinare il valore iniziale alla chiusura: - -```bash -scrcpy --show-touches -scrcpy -t -``` - -Notare che mostra solo i tocchi _fisici_ (con le dita sul dispositivo). - - -#### Disabilitare il salvaschermo - -In maniera predefinita scrcpy non previene l'attivazione del salvaschermo del computer. - -Per disabilitarlo: - -```bash -scrcpy --disable-screensaver -``` - - -### Input di controlli - -#### Rotazione dello schermo del dispostivo - -Premere MOD+r per cambiare tra le modalità verticale (portrait) e orizzontale (landscape). - -Notare che la rotazione avviene solo se l'applicazione in primo piano supporta l'orientamento richiesto. - -#### Copia-incolla - -Quando gli appunti di Android cambiano, essi vengono automaticamente sincronizzati con gli appunti del computer. - -Qualsiasi scorciatoia Ctrl viene inoltrata al dispositivo. In particolare: - - Ctrl+c copia - - Ctrl+x taglia - - Ctrl+v incolla (dopo la sincronizzazione degli appunti da computer a dispositivo) - -Questo solitamente funziona come ci si aspetta. - -Il comportamento reale, però, dipende dall'applicazione attiva. Per esempio _Termux_ invia SIGINT con Ctrl+c, e _K-9 Mail_ compone un nuovo messaggio. - -Per copiare, tagliare e incollare in questi casi (ma è solo supportato in Android >= 7): - - MOD+c invia `COPY` - - MOD+x invia `CUT` - - MOD+v invia `PASTE` (dopo la sincronizzazione degli appunti da computer a dispositivo) - -In aggiunta, MOD+Shift+v permette l'invio del testo degli appunti del computer come una sequenza di eventi pressione dei tasti. Questo è utile quando il componente non accetta l'incollaggio di testo (per esempio in _Termux_), ma questo può compromettere il contenuto non ASCII. - -**AVVISO:** Incollare gli appunti del computer nel dispositivo (sia con Ctrl+v che con MOD+v) copia il contenuto negli appunti del dispositivo. Come conseguenza, qualsiasi applicazione Android potrebbe leggere il suo contenuto. Dovresti evitare di incollare contenuti sensibili (come password) in questa maniera. - -Alcuni dispositivi non si comportano come aspettato quando si modificano gli appunti del dispositivo a livello di codice. L'opzione `--legacy-paste` è fornita per cambiare il comportamento di Ctrl+v and MOD+v in modo tale che anch'essi inviino il testo degli appunti del computer come una sequenza di eventi di pressione dei tasti (nella stessa maniera di MOD+Shift+v). - -Per disabilitare la sincronizzazione automatica degli appunti, usa `--no-clipboard-autosync`. - -#### Pizzica per zoomare (pinch-to-zoom) - -Per simulare il "pizzica per zoomare": Ctrl+_click e trascina_. - -Più precisamente, tieni premuto Ctrl mentre premi il pulsante sinistro. Finchè il pulsante non sarà rilasciato, tutti i movimenti del mouse ridimensioneranno e ruoteranno il contenuto (se supportato dall'applicazione) relativamente al centro dello schermo. - -Concretamente, scrcpy genera degli eventi di tocco addizionali di un "dito virtuale" nella posizione simmetricamente opposta rispetto al centro dello schermo. - -#### Simulazione della tastiera fisica (HID) - -Per impostazione predefinita, scrcpy utilizza l'invio dei tasti o del testo di Android: funziona ovunque, ma è limitato all'ASCII. - -In alternativa scrcpy può simulare una tastiera fisica USB su Android per fornire una migliore esperienza di input (utilizzando [USB HID over AOAv2][hid-aoav2]): la tastiera virtuale è disabilitata e funziona per tutti i caratteri e IME. - -[hid-aoav2]: https://source.android.com/devices/accessories/aoa2#hid-support - -Tuttavia, funziona solo se il dispositivo è collegato via USB. - -Nota: su Windows, può funzionare solo in [odalità OTG](#otg), non durante il mirroring (non è possibile aprire un dispositivo USB se è già aperto da un altro processo come il daemon adb). - -Per abilitare questa modalità: - -```bash -scrcpy --hid-keyboard -scrcpy -K # versione breve -``` - -Se fallisce per qualche motivo (per esempio perché il dispositivo non è connesso via USB), ritorna automaticamente alla modalità predefinita (con un log nella console). Questo permette di usare le stesse opzioni della linea di comando quando si è connessi via USB e TCP/IP. - -In questa modalità, gli eventi i pressione originali (scancodes) sono inviati al dispositivo, indipendentemente dalla mappatura dei tasti dell'host. Pertanto, se il layout della tua tastiera non corrisponde, deve essere configurato sul dispositivo Android, in Impostazioni → Sistema → Lingue e input → [Tastiera fisica] (in inglese). - -Questa pagina di impostazioni può essere avviata direttamente: - -```bash -adb shell am start -a android.settings.HARD_KEYBOARD_SETTINGS -``` - -Tuttavia, l'opzione è disponibile solo quando la tastiera HID è abilitata (o quando una tastiera fisica è collegata). - -[Tastiera fisica]: https://github.com/Genymobile/scrcpy/pull/2632#issuecomment-923756915 - -#### Simulazione del mouse fisico (HID) - -In modo simile alla simulazione della tastiera fisica, è possibile simulare un mouse fisico. Allo stesso modo funziona solo se il dispositivo è connesso via USB. - -Per impostazione predefinita, scrcpy utilizza l'invio degli eventi del mouse di Android, utilizzando coordinate assolute. Simulando un mouse fisico, un puntatore del mouse appare sul dispositivo Android e vengono inviati i movimenti relativi del mouse, i click e gli scorrimenti. - -Per abilitare questa modalità: - -```bash -scrcpy --hid-mouse -scrcpy -M # versione breve -``` - -Si potrebbe anche aggiungere `--forward-all-clicks` a [inoltra tutti i pulsanti del mouse][forward_all_clicks]. - -[forward_all_clicks]: #click-destro-e-click-centrale - - -Quando questa modalità è attivata, il mouse del computer viene "catturato" (il puntatore del mouse scompare dal computer e appare invece sul dispositivo Android). - -I tasti speciali di cattura, Alt o Super, commutano (disabilitano o abilitano) la cattura del mouse. Usa uno di essi per ridare il controllo del mouse al computer. - - -#### OTG - -È possibile eseguire _scrcpy_ con la sola simulazione della tastiera fisica e del mouse (HID), come se la tastiera e il mouse del computer fossero collegati direttamente al dispositivo tramite un cavo OTG. - -In questa modalità, _adb_ (debug USB) non è necessario e il mirroring è disabilitato. - -Per attivare la modallità OTG: - -```bash -scrcpy --otg -# Passa la seriale se sono disponibili diversi dispositivi USB -scrcpy --otg -s 0123456789abcdef -``` - -È possibile abilitare solo la tastiera HID o il mouse HID: - -```bash -scrcpy --otg --hid-keyboard # solo la tastiera -scrcpy --otg --hid-mouse # solo mouse -scrcpy --otg --hid-keyboard --hid-mouse # tastiera e mouse -# per comodità, abilita entrambi per default -scrcpy --otg # tastiera e mouse -``` - -Come `--hid-keyboard` e `--hid-mouse`, funziona solo se il dispositivo è collegato via USB. - - -#### Preferenze di invio del testo - -Ci sono due tipi di [eventi][textevents] generati quando si scrive testo: - - _eventi di pressione_, segnalano che tasto è stato premuto o rilasciato; - - _eventi di testo_, segnalano che del testo è stato inserito. - -In maniera predefinita le lettere sono inviate usando gli eventi di pressione, in maniera tale che la tastiera si comporti come aspettato nei giochi (come accade solitamente per i tasti WASD). - -Questo, però, può [causare problemi][prefertext]. Se incontri un problema del genere, puoi evitarlo con: - -```bash -scrcpy --prefer-text -``` - -(ma questo romperà il normale funzionamento della tastiera nei giochi) - -Al contrario, si potrebbe forzare per inviare sempre eventi di pressione grezzi: - -```bash -scrcpy --raw-key-events -``` - -Queste opzioni non hanno effetto sulla tastiera HID (tutti gli eventi di pressione sono inviati come scancodes in questa modalità). - -[textevents]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-text-input -[prefertext]: https://github.com/Genymobile/scrcpy/issues/650#issuecomment-512945343 - - -#### Ripetizione di tasti - -In maniera predefinita, tenere premuto un tasto genera una ripetizione degli eventi di pressione di tale tasto. Questo può creare problemi di performance in alcuni giochi, dove questi eventi sono inutilizzati. - -Per prevenire l'inoltro ripetuto degli eventi di pressione: - -```bash -scrcpy --no-key-repeat -``` - -Questa opzione non ha effetto sulla tastiera HID (la ripetizione dei tasti è gestita da Android direttamente in questa modalità). - - -#### Click destro e click centrale - -In maniera predefinita, click destro aziona BACK (indietro) o POWER on (accensione) e il click centrale aziona HOME. Per disabilitare queste scorciatoie e, invece, inviare i click al dispositivo: - -```bash -scrcpy --forward-all-clicks -``` - - -### Rilascio di file - -#### Installare APK - -Per installare un APK, trascina e rilascia un file APK (finisce con `.apk`) nella finestra di _scrcpy_. - -Non c'è alcuna risposta visiva, un log è stampato nella console. - - -#### Trasferimento di file verso il dispositivo - -Per trasferire un file in `/sdcard/Download` del dispositivo trascina e rilascia un file (non APK) nella finestra di _scrcpy_. - -Non c'è alcuna risposta visiva, un log è stampato nella console. - -La cartella di destinazione può essere cambiata all'avvio: - -```bash -scrcpy --push-target=/sdcard/Movies/ -``` - - -### Inoltro dell'audio - -L'audio non è inoltrato da _scrcpy_. Usa [sndcpy]. - -Vedi anche la [issue #14]. - -[sndcpy]: https://github.com/rom1v/sndcpy -[issue #14]: https://github.com/Genymobile/scrcpy/issues/14 - - -## Scociatoie - -Nella lista seguente, MOD è il modificatore delle scorciatoie. In maniera predefinita è Alt (sinistro) o Super (sinistro). - -Può essere cambiato usando `--shortcut-mod`. I tasti possibili sono `lctrl`, `rctrl`, `lalt`, `ralt`, `lsuper` and `rsuper` (`l` significa sinistro e `r` significa destro). Per esempio: - -```bash -# usa ctrl destro per le scorciatoie -scrcpy --shortcut-mod=rctrl - -# use sia "ctrl sinistro"+"alt sinistro" che "super sinistro" per le scorciatoie -scrcpy --shortcut-mod=lctrl+lalt,lsuper -``` - -_[Super] è solitamente il pulsante Windows o Cmd._ - -[Super]: https://it.wikipedia.org/wiki/Tasto_Windows - - - | Azione | Scorciatoia - | ------------------------------------------- |:----------------------------- - | Schermo intero | MOD+f - | Rotazione schermo a sinistra | MOD+ _(sinistra)_ - | Rotazione schermo a destra | MOD+ _(destra)_ - | Ridimensiona finestra a 1:1 (pixel-perfect) | MOD+g - | Ridimensiona la finestra per rimuovere i bordi neri | MOD+w \| _Doppio click sinistro¹_ - | Premi il tasto `HOME` | MOD+h \| _Click centrale_ - | Premi il tasto `BACK` | MOD+b \| _Click destro²_ - | Premi il tasto `APP_SWITCH` | MOD+s \| _4° click³_ - | Premi il tasto `MENU` (sblocca lo schermo)⁴ | MOD+m - | Premi il tasto `VOLUME_UP` | MOD+ _(su)_ - | Premi il tasto `VOLUME_DOWN` | MOD+ _(giù)_ - | Premi il tasto `POWER` | MOD+p - | Accendi | _Click destro²_ - | Spegni lo schermo del dispositivo (continua a trasmettere) | MOD+o - | Accendi lo schermo del dispositivo | MOD+Shift+o - | Ruota lo schermo del dispositivo | MOD+r - | Espandi il pannello delle notifiche | MOD+n \| _5° click³_ - | Espandi il pannello delle impostazioni | MOD+n+n \| _Doppio 5° click³_ - | Chiudi pannelli | MOD+Shift+n - | Copia negli appunti⁵ | MOD+c - | Taglia negli appunti⁵ | MOD+x - | Sincronizza gli appunti e incolla⁵ | MOD+v - | Invia il testo degli appunti del computer | MOD+Shift+v - | Abilita/Disabilita il contatore FPS (su stdout) | MOD+i - | Pizzica per zoomare | Ctrl+_click e trascina_ - | Trascina file APK | Installa APK dal computer - | Trascina file non-APK | [Trasferisci file verso il dispositivo](#push-file-to-device) - -_¹Doppio click sui bordi neri per rimuoverli._ -_²Il tasto destro accende lo schermo se era spento, preme BACK in caso contrario._ -_³4° e 5° pulsante del mouse, se il tuo mouse ne dispone._ -_⁴Per le app native react in sviluppo, `MENU` attiva il menu di sviluppo._ -_⁵Solo in Android >= 7._ - -Le scorciatoie con pulsanti ripetuti sono eseguite rilasciando e premendo il pulsante una seconda volta. Per esempio, per eseguire "Espandi il pannello delle impostazioni": - -1. Premi e tieni premuto MOD. -2. Poi premi due volte n. -3. Infine rilascia MOD. - -Tutte le scorciatoie Ctrl+_tasto_ sono inoltrate al dispositivo, così sono gestite dall'applicazione attiva. - -## Path personalizzati - -Per utilizzare dei binari _adb_ specifici, configura il suo path nella variabile d'ambente `ADB`: - -```bash -ADB=/percorso/per/adb scrcpy -``` - -Per sovrascrivere il percorso del file `scrcpy-server`, configura il percorso in `SCRCPY_SERVER_PATH`. - -## Perchè _scrcpy_? - -Un collega mi ha sfidato a trovare un nome tanto impronunciabile quanto [gnirehtet]. - -[`strcpy`] copia una **str**ing (stringa); `scrcpy` copia uno **scr**een (schermo). - -[gnirehtet]: https://github.com/Genymobile/gnirehtet -[`strcpy`]: http://man7.org/linux/man-pages/man3/strcpy.3.html - -## Come compilare? - -Vedi [BUILD] (in inglese). - - -## Problemi comuni - -Vedi le [FAQ](FAQ.it.md). - - -## Sviluppatori - -Leggi la [pagina per sviluppatori]. - -[pagina per sviluppatori]: DEVELOP.md - - -## Licenza (in inglese) - - Copyright (C) 2018 Genymobile - Copyright (C) 2018-2022 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. - -## Articoli (in inglese) - -- [Introducendo scrcpy][article-intro] -- [Scrcpy ora funziona wireless][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/ - -## Contatti - -Se incontri un bug, per favore leggi prima le [FAQ](FAQ.it.md), poi apri una [issue]. - -[issue]: https://github.com/Genymobile/scrcpy/issues - -Per domande generali o discussioni, puoi anche usare: - - - Reddit: [`r/scrcpy`](https://www.reddit.com/r/scrcpy) - - Twitter: [`@scrcpy_app`](https://twitter.com/scrcpy_app) diff --git a/README.jp.md b/README.jp.md deleted file mode 100644 index 583582fd..00000000 --- a/README.jp.md +++ /dev/null @@ -1,799 +0,0 @@ -_Only the original [README](README.md) is guaranteed to be up-to-date._ - -# scrcpy (v1.19) - -このアプリケーションはUSB(もしくは[TCP/IP経由][article-tcpip])で接続されたAndroidデバイスの表示と制御を提供します。このアプリケーションは _root_ でのアクセスを必要としません。このアプリケーションは _GNU/Linux_ 、 _Windows_ そして _macOS_ 上で動作します。 - -![screenshot](assets/screenshot-debian-600.jpg) - -以下に焦点を当てています: - - - **軽量** (ネイティブ、デバイス画面表示のみ) - - **パフォーマンス** (30~60fps) - - **クオリティ** (1920x1080以上) - - **低遅延** ([35~70ms][lowlatency]) - - **短い起動時間** (初回画像を1秒以内に表示) - - **非侵入型** (デバイスに何もインストールされていない状態になる) - -[lowlatency]: https://github.com/Genymobile/scrcpy/pull/646 - - -## 必要要件 - -AndroidデバイスはAPI21(Android 5.0)以上。 - -Androidデバイスで[adbデバッグが有効][enable-adb]であること。 - -[enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling - -一部のAndroidデバイスでは、キーボードとマウスを使用して制御する[追加オプション][control]を有効にする必要がある。 - -[control]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323 - - -## アプリの取得 - -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 - -[自分でビルド][BUILD]も可能(心配しないでください、それほど難しくはありません。) - - -### Windows - -Windowsでは簡単に、(`adb`を含む)すべての依存関係を構築済みのアーカイブを利用可能です。 - - - [README](README.md#windows) - -[Chocolatey]でも利用可能です: - -[Chocolatey]: https://chocolatey.org/ - -```bash -choco install scrcpy -choco install adb # まだ入手していない場合 -``` - -[Scoop]でも利用可能です: - -```bash -scoop install scrcpy -scoop install adb # まだ入手していない場合 -``` - -[Scoop]: https://scoop.sh - -また、[アプリケーションをビルド][BUILD]することも可能です。 - -### macOS - -アプリケーションは[Homebrew]で利用可能です。ただインストールするだけです。 - -[Homebrew]: https://brew.sh/ - -```bash -brew install scrcpy -``` - -`PATH`からアクセス可能な`adb`が必要です。もし持っていない場合はインストールしてください。 - -```bash -brew install android-platform-tools -``` - -`adb`は[MacPorts]からでもインストールできます。 - -```bash -sudo port install scrcpy -``` - -[MacPorts]: https://www.macports.org/ - -また、[アプリケーションをビルド][BUILD]することも可能です。 - -## 実行 - -Androidデバイスを接続し、実行: - -```bash -scrcpy -``` - -次のコマンドでリストされるコマンドライン引数も受け付けます: - -```bash -scrcpy --help -``` - -## 機能 - -### キャプチャ構成 - -#### サイズ削減 - -Androidデバイスを低解像度でミラーリングする場合、パフォーマンス向上に便利な場合があります。 - -幅と高さをある値(例:1024)に制限するには: - -```bash -scrcpy --max-size 1024 -scrcpy -m 1024 # 短縮版 -``` - -一方のサイズはデバイスのアスペクト比が維持されるように計算されます。この方法では、1920x1080のデバイスでは1024x576にミラーリングされます。 - - -#### ビットレート変更 - -ビットレートの初期値は8Mbpsです。ビットレートを変更するには(例:2Mbpsに変更): - -```bash -scrcpy --bit-rate 2M -scrcpy -b 2M # 短縮版 -``` - -#### フレームレート制限 - -キャプチャするフレームレートを制限できます: - -```bash -scrcpy --max-fps 15 -``` - -この機能はAndroid 10からオフィシャルサポートとなっていますが、以前のバージョンでも動作する可能性があります。 - -#### トリミング - -デバイスの画面は、画面の一部のみをミラーリングするようにトリミングできます。 - -これは、例えばOculus Goの片方の目をミラーリングする場合に便利です。: - -```bash -scrcpy --crop 1224:1440:0:0 # オフセット位置(0,0)で1224x1440 -``` - -もし`--max-size`も指定されている場合、トリミング後にサイズ変更が適用されます。 - -#### ビデオの向きをロックする - -ミラーリングの向きをロックするには: - -```bash -scrcpy --lock-video-orientation # 現在の向き -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 --encoder OMX.qcom.video.encoder.avc -``` - -利用可能なエンコーダをリストするために、無効なエンコーダ名を渡すことができます。エラー表示で利用可能なエンコーダを提供します。 - -```bash -scrcpy --encoder _ -``` - -### キャプチャ - -#### 録画 - -ミラーリング中に画面の録画をすることが可能です: - -```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 - -#### v4l2loopback - -Linuxでは、ビデオストリームをv4l2ループバックデバイスに送信することができます。 -v4l2loopbackのデバイスにビデオストリームを送信することで、Androidデバイスをウェブカメラのようにv4l2対応ツールで開くこともできます。 - -`v4l2loopback` モジュールのインストールが必要です。 - -```bash -sudo apt install v4l2loopback-dkms -``` - -v4l2デバイスを作成する。 - -```bash -sudo modprobe v4l2loopback -``` - -これにより、新しいビデオデバイスが `/dev/videoN` に作成されます。(`N` は整数) -(複数のデバイスや特定のIDのデバイスを作成するために、より多くの[オプション](https://github.com/umlaeute/v4l2loopback#options)が利用可能です。 -多くの[オプション]()が利用可能で複数のデバイスや特定のIDのデバイスを作成できます。 - - -有効なデバイスを一覧表示する: - -```bash -# v4l-utilsパッケージが必要 -v4l2-ctl --list-devices - -# シンプルですが十分これで確認できます -ls /dev/video* -``` - -v4l2シンクを使用してscrcpyを起動する。 - -```bash -scrcpy --v4l2-sink=/dev/videoN -scrcpy --v4l2-sink=/dev/videoN --no-display # ミラーリングウィンドウを無効化する -scrcpy --v4l2-sink=/dev/videoN -N # 短縮版 -``` - -(`N` をデバイス ID に置き換えて、`ls /dev/video*` で確認してください) -有効にすると、v4l2対応のツールでビデオストリームを開けます。 - -```bash -ffplay -i /dev/videoN -vlc v4l2:///dev/videoN # VLCではバッファリングの遅延が発生する場合があります -``` - -例えばですが [OBS]の中にこの映像を取り込めことができます。 - -[OBS]: https://obsproject.com/ - - -#### Buffering - -バッファリングを追加することも可能です。これによりレイテンシーは増加しますが、ジッターは減少します。(参照 -[#2464]) - -[#2464]: https://github.com/Genymobile/scrcpy/issues/2464 - -このオプションでディスプレイバッファリングを設定できます。 - -```bash -scrcpy --display-buffer=50 # ディスプレイに50msのバッファリングを追加する -``` - -V4L2の場合はこちらのオプションで設定できます。 - -```bash -scrcpy --v4l2-buffer=500 # add 500 ms buffering for v4l2 sink -``` - -### 接続 - -#### ワイヤレス - -_Scrcpy_ はデバイスとの通信に`adb`を使用します。そして`adb`はTCP/IPを介しデバイスに[接続]することができます: - -1. あなたのコンピュータと同じWi-Fiに接続します。 -2. あなたのIPアドレスを取得します。設定 → 端末情報 → ステータス情報、もしくは、このコマンドを実行します: - - ```bash - adb shell ip route | awk '{print $9}' - ``` - -3. あなたのデバイスでTCP/IPを介したadbを有効にします: `adb tcpip 5555` -4. あなたのデバイスの接続を外します。 -5. あなたのデバイスに接続します: - `adb connect DEVICE_IP:5555` _(`DEVICE_IP`は置き換える)_ -6. 通常通り`scrcpy`を実行します。 - -この方法はビットレートと解像度を減らすのにおそらく有用です: - -```bash -scrcpy --bit-rate 2M --max-size 800 -scrcpy -b2M -m800 # 短縮版 -``` - -[接続]: https://developer.android.com/studio/command-line/adb.html#wireless - - -#### マルチデバイス - -もし`adb devices`でいくつかのデバイスがリストされる場合、 _シリアルナンバー_ を指定する必要があります: - -```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トンネル - -リモートデバイスに接続するため、ローカル`adb`クライアントからリモート`adb`サーバーへ接続することが可能です(同じバージョンの _adb_ プロトコルを使用している場合): - -```bash -adb kill-server # 5037ポートのローカルadbサーバーを終了する -ssh -CN -L5037:localhost:5037 -R27183:localhost:27183 your_remote_computer -# オープンしたままにする -``` - -他の端末から: - -```bash -scrcpy -``` - -リモートポート転送の有効化を回避するためには、代わりに転送接続を強制することができます(`-R`の代わりに`-L`を使用することに注意): - -```bash -adb kill-server # 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はデバイスに縦向きと横向きの切り替えを要求する(現在実行中のアプリで要求している向きをサポートしていない場合、拒否することがある) - - [`--lock-video-orientation`](#ビデオの向きをロックする)は、ミラーリングする向きを変更する(デバイスからPCへ送信される向き)。録画に影響します。 - - `--rotation` (もしくはMOD+/MOD+)は、ウィンドウのコンテンツのみを回転します。これは表示にのみに影響し、録画には影響しません。 - -### 他のミラーリングオプション - -#### Read-only リードオンリー - -制御を無効にするには(デバイスと対話する全てのもの:入力キー、マウスイベント、ファイルのドラッグ&ドロップ): - -```bash -scrcpy --no-control -scrcpy -n -``` - -#### ディスプレイ - -いくつか利用可能なディスプレイがある場合、ミラーリングするディスプレイを選択できます: - -```bash -scrcpy --display 1 -``` - -ディスプレイIDのリストは次の方法で取得できます: - -``` -adb shell dumpsys display # search "mDisplayId=" in the output -``` - -セカンダリディスプレイは、デバイスが少なくともAndroid 10の場合にコントロール可能です。(それ以外ではリードオンリーでミラーリングされます) - - -#### 起動状態にする - -デバイス接続時、少し遅れてからデバイスのスリープを防ぐには: - -```bash -scrcpy --stay-awake -scrcpy -w -``` - -scrcpyが閉じられた時、初期状態に復元されます。 - -#### 画面OFF - -コマンドラインオプションを使用することで、ミラーリングの開始時にデバイスの画面をOFFにすることができます: - -```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 -``` - - -#### タッチを表示 - -プレゼンテーションの場合(物理デバイス上で)物理的なタッチを表示すると便利な場合があります。 - -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を介して)ペーストすることは、デバイスのクリップボードにコンテンツをコピーします。結果としてどのAndoridアプリケーションもそのコンテンツを読み取ることができます。機密性の高いコンテンツ(例えばパスワードなど)をこの方法でペーストすることは避けてください。 - -プログラムでデバイスのクリップボードを設定した場合、一部のデバイスは期待どおりに動作しません。`--legacy-paste`オプションは、コンピュータのクリップボードテキストをキーイベントのシーケンスとして挿入するため(MOD+Shift+vと同じ方法)、Ctrl+vMOD+vの動作の変更を提供します。 - -#### ピンチしてズームする - -"ピンチしてズームする"をシミュレートするには: Ctrl+_クリック&移動_ - -より正確にするには、左クリックボタンを押している間、Ctrlを押したままにします。左クリックボタンを離すまで、全てのマウスの動きは、(アプリでサポートされている場合)画面の中心を基準として、コンテンツを拡大縮小および回転します。 - -具体的には、scrcpyは画面の中央を反転した位置にある"バーチャルフィンガー"から追加のタッチイベントを生成します。 - - -#### テキストインジェクション環境設定 - -テキストをタイプした時に生成される2種類の[イベント][textevents]があります: - - _key events_ はキーを押したときと離したことを通知します。 - - _text 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 -``` - - -#### 右クリックと真ん中クリック - -初期状態では、右クリックはバックの動作(もしくはパワーオン)を起こし、真ん中クリックではホーム画面へ戻ります。このショートカットを無効にし、代わりにデバイスへクリックを転送するには: - -```bash -scrcpy --forward-all-clicks -``` - - -### ファイルのドロップ - -#### APKのインストール - -APKをインストールするには、(`.apk`で終わる)APKファイルを _scrcpy_ の画面にドラッグ&ドロップします。 - -見た目のフィードバックはありません。コンソールにログが出力されます。 - - -#### デバイスにファイルを送る - -デバイスの`/sdcard/Download`ディレクトリにファイルを送るには、(APKではない)ファイルを _scrcpy_ の画面にドラッグ&ドロップします。 - -見た目のフィードバックはありません。コンソールにログが出力されます。 - -転送先ディレクトリを起動時に変更することができます: - -```bash -scrcpy --push-target=/sdcard/Movies/ -``` - - -### 音声転送 - -音声は _scrcpy_ では転送されません。[sndcpy]を使用します。 - -[issue #14]も参照ください。 - -[sndcpy]: https://github.com/rom1v/sndcpy -[issue #14]: https://github.com/Genymobile/scrcpy/issues/14 - - -## ショートカット - -次のリストでは、MODでショートカット変更します。初期状態では、(left)Altまたは(left)Superです。 - -これは`--shortcut-mod`で変更することができます。可能なキーは`lctrl`、`rctrl`、`lalt`、 `ralt`、 `lsuper`そして`rsuper`です。例えば: - -```bash -# RCtrlをショートカットとして使用します -scrcpy --shortcut-mod=rctrl - -# ショートカットにLCtrl+LAltまたはLSuperのいずれかを使用します -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 \| _ダブルクリック¹_ - | `HOME`をクリック | MOD+h \| _真ん中クリック_ - | `BACK`をクリック | MOD+b \| _右クリック²_ - | `APP_SWITCH`をクリック | MOD+s \| _4クリック³_ - | `MENU` (画面のアンロック)をクリック | MOD+m - | `VOLUME_UP`をクリック | MOD+ _(上)_ - | `VOLUME_DOWN`をクリック | MOD+ _(下)_ - | `POWER`をクリック | MOD+p - | 電源オン | _右クリック²_ - | デバイス画面をオフにする(ミラーリングしたまま) | MOD+o - | デバイス画面をオンにする | MOD+Shift+o - | デバイス画面を回転する | MOD+r - | 通知パネルを展開する | MOD+n \| _5ボタンクリック³_ - | 設定パネルを展開する | MOD+n+n \| _5ダブルクリック³_ - | 通知パネルを折りたたむ | MOD+Shift+n - | クリップボードへのコピー³ | MOD+c - | クリップボードへのカット³ | MOD+x - | クリップボードの同期とペースト³ | MOD+v - | コンピュータのクリップボードテキストの挿入 | MOD+Shift+v - | FPSカウンタ有効/無効(標準入出力上) | MOD+i - | ピンチしてズームする | Ctrl+_クリック&移動_ - -_¹黒い境界線を削除するため、境界線上でダブルクリック_ -_²もしスクリーンがオフの場合、右クリックでスクリーンをオンする。それ以外の場合はBackを押します._ -_³4と5はマウスのボタンです、もしあなたのマウスにボタンがあれば使えます._ -_⁴Android 7以上のみ._ - -キーを繰り返すショートカットはキーを離して2回目を押したら実行されます。例えば「設定パネルを展開する」を実行する場合は以下のように操作する。 - - 1. MOD キーを押し、押したままにする. - 2. その後に nキーを2回押す. - 3. 最後に MODキーを離す. - -全ての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]を参照してください。 - -[BUILD]: BUILD.md - - -## よくある質問 - -[FAQ](FAQ.md)を参照してください。 - - -## 開発者 - -[開発者のページ]を読んでください。 - -[開発者のページ]: DEVELOP.md - - -## ライセンス - - Copyright (C) 2018 Genymobile - Copyright (C) 2018-2022 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/ diff --git a/README.ko.md b/README.ko.md deleted file mode 100644 index a77cde48..00000000 --- a/README.ko.md +++ /dev/null @@ -1,498 +0,0 @@ -_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 -이 문서는 원어 리드미 파일의 업데이트에 따라 종종 업데이트 될 것입니다 - - 이 어플리케이션은 UBS ( 혹은 [TCP/IP][article-tcpip] ) 로 연결된 Android 디바이스를 화면에 보여주고 관리하는 것을 제공합니다. - _GNU/Linux_, _Windows_ 와 _macOS_ 상에서 작동합니다. - (아래 설명에서 디바이스는 안드로이드 핸드폰을 의미합니다.) - -[article-tcpip]:https://www.genymotion.com/blog/open-source-project-scrcpy-now-works-wirelessly/ - -![screenshot](https://github.com/Genymobile/scrcpy/blob/master/assets/screenshot-debian-600.jpg?raw=true) - -주요 기능은 다음과 같습니다. - - - **가벼움** (기본적이며 디바이스의 화면만을 보여줌) - - **뛰어난 성능** (30~60fps) - - **높은 품질** (1920×1080 이상의 해상도) - - **빠른 반응 속도** ([35~70ms][lowlatency]) - - **짧은 부팅 시간** (첫 사진을 보여주는데 최대 1초 소요됨) - - **장치 설치와는 무관함** (디바이스에 설치하지 않아도 됨) - -[lowlatency]: https://github.com/Genymobile/scrcpy/pull/646 - - -## 요구사항 - -안드로이드 장치는 최소 API 21 (Android 5.0) 을 필요로 합니다. - -디바이스에 [adb debugging][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 (리눅스) - -리눅스 상에서는 보통 [어플을 직접 설치][BUILD] 해야합니다. 어렵지 않으므로 걱정하지 않아도 됩니다. - -[BUILD]:https://github.com/Genymobile/scrcpy/blob/master/BUILD.md - -[Snap] 패키지가 가능합니다 : [`scrcpy`][snap-link]. - -[snap-link]: https://snapstats.org/snaps/scrcpy - -[snap]: https://en.wikipedia.org/wiki/Snappy_(package_manager) - -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 (윈도우) - -윈도우 상에서, 간단하게 설치하기 위해 종속성이 있는 사전 구축된 아카이브가 제공됩니다 (`adb` 포함) : -해당 파일은 Readme원본 링크를 통해서 다운로드가 가능합니다. - - [README](README.md#windows) - - -[어플을 직접 설치][BUILD] 할 수도 있습니다. - - -### macOS (맥 OS) - -이 어플리케이션은 아래 사항을 따라 설치한다면 [Homebrew] 에서도 사용 가능합니다 : - -[Homebrew]: https://brew.sh/ - -```bash -brew install scrcpy -``` - -`PATH` 로부터 접근 가능한 `adb` 가 필요합니다. 아직 설치하지 않았다면 다음을 따라 설치해야 합니다 : - -```bash -brew cask install android-platform-tools -``` - -[어플을 직접 설치][BUILD] 할 수도 있습니다. - - -## 실행 - -안드로이드 디바이스를 연결하고 실행하십시오: - -```bash -scrcpy -``` - -다음과 같이 명령창 옵션 기능도 제공합니다. - -```bash -scrcpy --help -``` - -## 기능 - -### 캡쳐 환경 설정 - - -### 사이즈 재정의 - -가끔씩 성능을 향상시키기위해 안드로이드 디바이스를 낮은 해상도에서 미러링하는 것이 유용할 때도 있습니다. - -너비와 높이를 제한하기 위해 특정 값으로 지정할 수 있습니다 (e.g. 1024) : - -```bash -scrcpy --max-size 1024 -scrcpy -m 1024 # 축약 버전 -``` - -이 외의 크기도 디바이스의 가로 세로 비율이 유지된 상태에서 계산됩니다. -이러한 방식으로 디바이스 상에서 1920×1080 는 모니터 상에서1024×576로 미러링될 것 입니다. - - -### bit-rate 변경 - -기본 bit-rate 는 8 Mbps입니다. 비디오 bit-rate 를 변경하기 위해선 다음과 같이 입력하십시오 (e.g. 2 Mbps로 변경): - -```bash -scrcpy --bit-rate 2M -scrcpy -b 2M # 축약 버전 -``` - -### 프레임 비율 제한 - -안드로이드 버전 10이상의 디바이스에서는, 다음의 명령어로 캡쳐 화면의 프레임 비율을 제한할 수 있습니다: - -```bash -scrcpy --max-fps 15 -``` - - -### Crop (잘라내기) - -디바이스 화면은 화면의 일부만 미러링하기 위해 잘라질 것입니다. - -예를 들어, *Oculus Go* 의 한 쪽 눈만 미러링할 때 유용합니다 : - -```bash -scrcpy --crop 1224:1440:0:0 # 1224x1440 at offset (0,0) -scrcpy -c 1224:1440:0:0 # 축약 버전 -``` - -만약 `--max-size` 도 지정하는 경우, 잘라낸 다음에 재정의된 크기가 적용될 것입니다. - - -### 화면 녹화 - -미러링하는 동안 화면 녹화를 할 수 있습니다 : - -```bash -scrcpy --record file.mp4 -scrcpy -r file.mkv -``` - -녹화하는 동안 미러링을 멈출 수 있습니다 : - -```bash -scrcpy --no-display --record file.mp4 -scrcpy -Nr file.mkv -# Ctrl+C 로 녹화를 중단할 수 있습니다. -# 윈도우 상에서 Ctrl+C 는 정상정으로 종료되지 않을 수 있으므로, 디바이스 연결을 해제하십시오. -``` - -"skipped frames" 은 모니터 화면에 보여지지 않았지만 녹화되었습니다 ( 성능 문제로 인해 ). 프레임은 디바이스 상에서 _타임 스탬프 ( 어느 시점에 데이터가 존재했다는 사실을 증명하기 위해 특정 위치에 시각을 표시 )_ 되었으므로, [packet delay -variation] 은 녹화된 파일에 영향을 끼치지 않습니다. - -[packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation - -## 연결 - -### 무선연결 - -_Scrcpy_ 장치와 정보를 주고받기 위해 `adb` 를 사용합니다. `adb` 는 TCIP/IP 를 통해 디바이스와 [연결][connect] 할 수 있습니다 : - -1. 컴퓨터와 디바이스를 동일한 Wi-Fi 에 연결합니다. -2. 디바이스의 IP address 를 확인합니다 (설정 → 내 기기 → 상태 / 혹은 인터넷에 '내 IP'검색 시 확인 가능합니다. ). -3. TCP/IP 를 통해 디바이스에서 adb 를 사용할 수 있게 합니다: `adb tcpip 5555`. -4. 디바이스 연결을 해제합니다. -5. adb 를 통해 디바이스에 연결을 합니다\: `adb connect DEVICE_IP:5555` _(`DEVICE_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 # 축약 버전 -``` - -_scrcpy_ 로 여러 디바이스를 연결해 사용할 수 있습니다. - - -#### SSH tunnel - -떨어져 있는 디바이스와 연결하기 위해서는, 로컬 `adb` client와 떨어져 있는 `adb` 서버를 연결해야 합니다. (디바이스와 클라이언트가 동일한 버전의 _adb_ protocol을 사용할 경우에 제공됩니다.): - -```bash -adb kill-server # 5037의 로컬 local adb server를 중단 -ssh -CN -L5037:localhost:5037 -R27183:localhost:27183 your_remote_computer -# 실행 유지 -``` - -다른 터미널에서는 : - -```bash -scrcpy -``` - -무선 연결과 동일하게, 화질을 줄이는 것이 나을 수 있습니다: - -``` -scrcpy -b2M -m800 --max-fps 15 -``` - -## Window에서의 배치 - -### 맞춤형 window 제목 - -기본적으로, window의 이름은 디바이스의 모델명 입니다. -다음의 명령어를 통해 변경하세요. - -```bash -scrcpy --window-title 'My device' -``` - - -### 배치와 크기 - -초기 window창의 배치와 크기는 다음과 같이 설정할 수 있습니다: - -```bash -scrcpy --window-x 100 --window-y 100 --window-width 800 --window-height 600 -``` - - -### 경계 없애기 - -윈도우 장식(경계선 등)을 다음과 같이 제거할 수 있습니다: - -```bash -scrcpy --window-borderless -``` - -### 항상 모든 윈도우 위에 실행창 고정 - -이 어플리케이션의 윈도우 창은 다음의 명령어로 다른 window 위에 디스플레이 할 수 있습니다: - -```bash -scrcpy --always-on-top -scrcpy -T # 축약 버전 -``` - -### 전체 화면 - -이 어플리케이션은 전체화면으로 바로 시작할 수 있습니다. - -```bash -scrcpy --fullscreen -scrcpy -f # short version -``` - -전체 화면은 `Ctrl`+`f`키로 끄거나 켤 수 있습니다. - - -## 다른 미러링 옵션 - -### 읽기 전용(Read-only) - -권한을 제한하기 위해서는 (디바이스와 관련된 모든 것: 입력 키, 마우스 이벤트 , 파일의 드래그 앤 드랍(drag&drop)): - -```bash -scrcpy --no-control -scrcpy -n -``` - -### 화면 끄기 - -미러링을 실행하는 와중에 디바이스의 화면을 끌 수 있게 하기 위해서는 -다음의 커맨드 라인 옵션을(command line option) 입력하세요: - -```bash -scrcpy --turn-screen-off -scrcpy -S -``` - -혹은 `Ctrl`+`o`을 눌러 언제든지 디바이스의 화면을 끌 수 있습니다. - -다시 화면을 켜기 위해서는`POWER` (혹은 `Ctrl`+`p`)를 누르세요. - - -### 유효기간이 지난 프레임 제공 (Render expired frames) - -디폴트로, 대기시간을 최소화하기 위해 _scrcpy_ 는 항상 마지막으로 디코딩된 프레임을 제공합니다 -과거의 프레임은 하나씩 삭제합니다. - -모든 프레임을 강제로 렌더링하기 위해서는 (대기 시간이 증가될 수 있습니다) -다음의 명령어를 사용하세요: - -```bash -scrcpy --render-expired-frames -``` - - -### 화면에 터치 나타내기 - -발표를 할 때, 물리적인 기기에 한 물리적 터치를 나타내는 것이 유용할 수 있습니다. - -안드로이드 운영체제는 이런 기능을 _Developers options_에서 제공합니다. - -_Scrcpy_ 는 이런 기능을 시작할 때와 종료할 때 옵션으로 제공합니다. - -```bash -scrcpy --show-touches -scrcpy -t -``` - -화면에 _물리적인 터치만_ 나타나는 것에 유의하세요 (손가락을 디바이스에 대는 행위). - - -### 입력 제어 - -#### 복사-붙여넣기 - -컴퓨터와 디바이스 양방향으로 클립보드를 복사하는 것이 가능합니다: - - - `Ctrl`+`c` 디바이스의 클립보드를 컴퓨터로 복사합니다; - - `Ctrl`+`Shift`+`v` 컴퓨터의 클립보드를 디바이스로 복사합니다; - - `Ctrl`+`v` 컴퓨터의 클립보드를 text event 로써 _붙여넣습니다_ ( 그러나, ASCII 코드가 아닌 경우 실행되지 않습니다 ) - -#### 텍스트 삽입 우선 순위 - -텍스트를 입력할 때 생성되는 두 가지의 [events][textevents] 가 있습니다: - - _key events_, 키가 눌려있는 지에 대한 신호; - - _text events_, 텍스트가 입력되었는지에 대한 신호. - -기본적으로, 글자들은 key event 를 이용해 입력되기 때문에, 키보드는 게임에서처럼 처리합니다 ( 보통 WASD 키에 대해서 ). - -그러나 이는 [issues 를 발생][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 - - -### 파일 드랍 - -### APK 실행하기 - -APK를 실행하기 위해서는, APK file(파일명이`.apk`로 끝나는 파일)을 드래그하고 _scrcpy_ window에 드랍하세요 (drag and drop) - -시각적인 피드백은 없고,log 하나가 콘솔에 출력될 것입니다. - -### 디바이스에 파일 push하기 - -디바이스의`/sdcard/`에 파일을 push하기 위해서는, -APK파일이 아닌 파일을_scrcpy_ window에 드래그하고 드랍하세요.(drag and drop). - -시각적인 피드백은 없고,log 하나가 콘솔에 출력될 것입니다. - -해당 디렉토리는 시작할 때 변경이 가능합니다: - -```bash -scrcpy --push-target /sdcard/foo/bar/ -``` - -### 오디오의 전달 - -_scrcpy_는 오디오를 직접 전달해주지 않습니다. [USBaudio] (Linux-only)를 사용하세요. - -추가적으로 [issue #14]를 참고하세요. - -[USBaudio]: https://github.com/rom1v/usbaudio -[issue #14]: https://github.com/Genymobile/scrcpy/issues/14 - -## 단축키 - - | 실행내용 | 단축키 | 단축키 (macOS) - | -------------------------------------- |:----------------------------- |:----------------------------- - | 전체화면 모드로 전환 | `Ctrl`+`f` | `Cmd`+`f` - | window를 1:1비율로 전환하기(픽셀 맞춤) | `Ctrl`+`g` | `Cmd`+`g` - | 검은 공백 제거 위한 window 크기 조정 | `Ctrl`+`x` \| _Double-click¹_ | `Cmd`+`x` \| _Double-click¹_ - |`HOME` 클릭 | `Ctrl`+`h` \| _Middle-click_ | `Ctrl`+`h` \| _Middle-click_ - | `BACK` 클릭 | `Ctrl`+`b` \| _Right-click²_ | `Cmd`+`b` \| _Right-click²_ - | `APP_SWITCH` 클릭 | `Ctrl`+`s` | `Cmd`+`s` - | `MENU` 클릭 | `Ctrl`+`m` | `Ctrl`+`m` - | `VOLUME_UP` 클릭 | `Ctrl`+`↑` _(up)_ | `Cmd`+`↑` _(up)_ - | `VOLUME_DOWN` 클릭 | `Ctrl`+`↓` _(down)_ | `Cmd`+`↓` _(down)_ - | `POWER` 클릭 | `Ctrl`+`p` | `Cmd`+`p` - | 전원 켜기 | _Right-click²_ | _Right-click²_ - | 미러링 중 디바이스 화면 끄기 | `Ctrl`+`o` | `Cmd`+`o` - | 알림 패널 늘리기 | `Ctrl`+`n` | `Cmd`+`n` - | 알림 패널 닫기 | `Ctrl`+`Shift`+`n` | `Cmd`+`Shift`+`n` - | 디바이스의 clipboard 컴퓨터로 복사하기 | `Ctrl`+`c` | `Cmd`+`c` - | 컴퓨터의 clipboard 디바이스에 붙여넣기 | `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` - -_¹검은 공백을 제거하기 위해서는 그 부분을 더블 클릭하세요_ -_²화면이 꺼진 상태에서 우클릭 시 다시 켜지며, 그 외의 상태에서는 뒤로 돌아갑니다. - -## 맞춤 경로 (custom path) - -특정한 _adb_ binary를 사용하기 위해서는, 그것의 경로를 환경변수로 설정하세요. -`ADB`: - - ADB=/path/to/adb scrcpy - -`scrcpy-server.jar`파일의 경로에 오버라이드 하기 위해서는, 그것의 경로를 `SCRCPY_SERVER_PATH`에 저장하세요. - -[useful]: https://github.com/Genymobile/scrcpy/issues/278#issuecomment-429330345 - - -## _scrcpy_ 인 이유? - -한 동료가 [gnirehtet]와 같이 발음하기 어려운 이름을 찾을 수 있는지 도발했습니다. - -[`strcpy`] 는 **str**ing을 copy하고; `scrcpy`는 **scr**een을 copy합니다. - -[gnirehtet]: https://github.com/Genymobile/gnirehtet -[`strcpy`]: http://man7.org/linux/man-pages/man3/strcpy.3.html - - - -## 빌드하는 방법? - -[BUILD]을 참고하세요. - -[BUILD]: BUILD.md - -## 흔한 issue - -[FAQ](FAQ.md)을 참고하세요. - - -## 개발자들 - -[developers page]를 참고하세요. - -[developers page]: DEVELOP.md - - -## 라이선스 - - Copyright (C) 2018 Genymobile - Copyright (C) 2018-2022 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. - -## 관련 글 (articles) - -- [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/ diff --git a/README.md b/README.md index db531c61..2ae50c4b 100644 --- a/README.md +++ b/README.md @@ -1209,17 +1209,8 @@ For general questions or discussions, you can also use: ## Translations -This README is available in other languages: +Translations of this README in other languages are available in the [wiki]. -- [Deutsch (German, `de`) - v1.22](README.de.md) -- [Indonesian (Indonesia, `id`) - v1.16](README.id.md) -- [Italiano (Italiano, `it`) - v1.23](README.it.md) -- [日本語 (Japanese, `jp`) - v1.19](README.jp.md) -- [한국어 (Korean, `ko`) - v1.11](README.ko.md) -- [Português Brasileiro (Brazilian Portuguese, `pt-BR`) - v1.19](README.pt-br.md) -- [Español (Spanish, `sp`) - v1.21](README.sp.md) -- [简体中文 (Simplified Chinese, `zh-Hans`) - v1.22](README.zh-Hans.md) -- [繁體中文 (Traditional Chinese, `zh-Hant`) - v1.15](README.zh-Hant.md) -- [Turkish (Turkish, `tr`) - v1.18](README.tr.md) +[wiki]: https://github.com/Genymobile/scrcpy/wiki Only this README file is guaranteed to be up-to-date. diff --git a/README.pt-br.md b/README.pt-br.md deleted file mode 100644 index cc7e5f0b..00000000 --- a/README.pt-br.md +++ /dev/null @@ -1,880 +0,0 @@ -_Apenas o [README](README.md) original é garantido estar atualizado._ - -# scrcpy (v1.19) - -Esta aplicação fornece exibiçã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 adb][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 precisa ativar [uma opção adicional][control] para -controlá-lo usando teclado e mouse. - -[control]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323 - - -## Obter o app - -Packaging status - -### Sumário - - - Linux: `apt install scrcpy` - - Windows: [baixar][direct-win64] - - macOS: `brew install scrcpy` - - Compilar pelos arquivos fontes: [BUILD] ([processo simplificado][BUILD_simple]) - -[BUILD]: BUILD.md -[BUILD_simple]: BUILD.md#simple - - -### Linux - -No Debian (_testing_ e _sid_ por enquanto) e Ubuntu (20.04): - -``` -apt install scrcpy -``` - -Um 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 Fedora, um pacote [COPR] está disponível: [`scrcpy`][copr-link]. - -[COPR]: https://fedoraproject.org/wiki/Category:Copr -[copr-link]: https://copr.fedorainfracloud.org/coprs/zeno/scrcpy/ - -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 o app manualmente][BUILD] ([processo simplificado][BUILD_simple]). - - -### Windows - -Para Windows, por simplicidade, um arquivo pré-compilado com todas as dependências -(incluindo `adb`) está disponível: - - - [README](README.md#windows) - -Também está 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 o app manualmente][BUILD]. - - -### macOS - -A aplicação está disponível em [Homebrew]. Apenas instale-a: - -[Homebrew]: https://brew.sh/ - -```bash -brew install scrcpy -``` - -Você precisa do `adb`, acessível pelo seu `PATH`. Se você ainda não o tem: - -```bash -brew install android-platform-tools -``` - -Está também disponivel em [MacPorts], que prepara o adb para você: - -```bash -sudo port install scrcpy -``` - -[MacPorts]: https://www.macports.org/ - - -Você também pode [compilar o app manualmente][BUILD]. - - -## Executar - -Conecte 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 - -#### Reduzir tamanho - -Algumas vezes, é útil espelhar um dispositivo Android em uma resolução menor para -aumentar a performance. - -Para limitar ambos (largura e altura) para algum valor (ex: 1024): - -```bash -scrcpy --max-size 1024 -scrcpy -m 1024 # versão curta -``` - -A outra dimensão é calculada para que a proporção do dispositivo seja preservada. -Dessa forma, um dispositivo de 1920x1080 será espelhado em 1024x576. - - -#### Mudar bit-rate - -O bit-rate padrão é 8 Mbps. Para mudar o bit-rate do vídeo (ex: para 2 Mbps): - -```bash -scrcpy --bit-rate 2M -scrcpy -b 2M # versão curta -``` - -#### Limitar frame rate - -O frame rate de captura pode ser limitado: - -```bash -scrcpy --max-fps 15 -``` - -Isso é oficialmente suportado desde o Android 10, mas pode funcionar em versões anteriores. - -#### Cortar - -A tela do dispositivo pode ser cortada para espelhar apenas uma parte da tela. - -Isso é útil por exemplo, para 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, o redimensionamento é aplicado após o corte. - - -#### Travar orientação do vídeo - - -Para travar a orientação do espelhamento: - -```bash -scrcpy --lock-video-orientation # orientação inicial (Atual) -scrcpy --lock-video-orientation=0 # orientação natural -scrcpy --lock-video-orientation=1 # 90° sentido anti-horário -scrcpy --lock-video-orientation=2 # 180° -scrcpy --lock-video-orientation=3 # 90° sentido horário -``` - -Isso afeta a orientação de gravação. - -A [janela também pode ser rotacionada](#rotação) independentemente. - - -#### Encoder - -Alguns dispositivos têm mais de um encoder, e alguns deles podem causar problemas ou -travar. É possível selecionar um encoder diferente: - -```bash -scrcpy --encoder OMX.qcom.video.encoder.avc -``` - -Para listar os encoders disponíveis, você pode passar um nome de encoder inválido, o -erro dará os encoders disponíveis: - -```bash -scrcpy --encoder _ -``` - -### Captura - -#### 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 -# interrompa a gravação com Ctrl+C -``` - -"Frames pulados" são gravados, mesmo que não sejam exibidos em tempo real (por -motivos de performance). Frames têm seu _horário carimbado_ no dispositivo, então [variação de atraso nos -pacotes][packet delay variation] não impacta o arquivo gravado. - -[packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation - - -#### v4l2loopback - -Em Linux, é possível enviar a transmissão do video para um disposiivo v4l2 loopback, assim -o dispositivo Android pode ser aberto como uma webcam por qualquer ferramneta capaz de v4l2 - -The module `v4l2loopback` must be installed: - -```bash -sudo apt install v4l2loopback-dkms -``` - -Para criar um dispositivo v4l2: - -```bash -sudo modprobe v4l2loopback -``` - -Isso criara um novo dispositivo de vídeo em `/dev/videoN`, onde `N` é uma integer -(mais [opções](https://github.com/umlaeute/v4l2loopback#options) estão disponiveis -para criar varios dispositivos ou dispositivos com IDs específicas). - -Para listar os dispositivos disponíveis: - -```bash -# requer o pacote v4l-utils -v4l2-ctl --list-devices - -# simples, mas pode ser suficiente -ls /dev/video* -``` - -Para iniciar o scrcpy usando o coletor v4l2 (sink): - -```bash -scrcpy --v4l2-sink=/dev/videoN -scrcpy --v4l2-sink=/dev/videoN --no-display # desativa a janela espelhada -scrcpy --v4l2-sink=/dev/videoN -N # versão curta -``` - -(troque `N` pelo ID do dipositivo, verifique com `ls /dev/video*`) - -Uma vez ativado, você pode abrir suas trasmissões de videos com uma ferramenta capaz de v4l2: - -```bash -ffplay -i /dev/videoN -vlc v4l2:///dev/videoN # VLC pode adicionar um pouco de atraso de buffering -``` - -Por exemplo, você pode capturar o video dentro do [OBS]. - -[OBS]: https://obsproject.com/ - - -#### Buffering - -É possivel adicionar buffering. Isso aumenta a latência, mas reduz a tenção (jitter) (veja -[#2464]). - -[#2464]: https://github.com/Genymobile/scrcpy/issues/2464 - -A opção éta disponivel para buffering de exibição: - -```bash -scrcpy --display-buffer=50 # adiciona 50 ms de buffering para a exibição -``` - -e coletor V4L2: - -```bash -scrcpy --v4l2-buffer=500 # adiciona 500 ms de buffering para coletor V4L2 -``` - -, -### Conexão - -#### Sem fio - -_Scrcpy_ usa `adb` para se comunicar com o dispositivo, e `adb` pode [conectar-se][connect] a um -dispositivo via TCP/IP: - -1. Conecte o dispositivo no mesmo Wi-Fi do seu computador. -2. Pegue o endereço IP do seu dispositivo, em Configurações → Sobre o telefone → Status, ou - executando este comando: - - ```bash - adb shell ip route | awk '{print $9}' - ``` - -3. Ative o adb via TCP/IP no seu dispositivo: `adb tcpip 5555`. -4. Desconecte seu dispositivo. -5. Conecte-se ao seu dispositivo: `adb connect DEVICE_IP:5555` _(substitua `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 curta -``` - -[connect]: https://developer.android.com/studio/command-line/adb.html#wireless - - -#### Múltiplos dispositivos - -Se vários dispositivos são listados em `adb devices`, você deve especificar o _serial_: - -```bash -scrcpy --serial 0123456789abcdef -scrcpy -s 0123456789abcdef # versão curta -``` - -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 curta -``` - -Você pode iniciar várias instâncias do _scrcpy_ para vários dispositivos. - -#### Iniciar automaticamente quando dispositivo é conectado - -Você pode usar [AutoAdb]: - -```bash -autoadb scrcpy -s '{}' -``` - -[AutoAdb]: https://github.com/rom1v/autoadb - -#### Túnel SSH - -Para conectar-se a um dispositivo remoto, é possível conectar um cliente `adb` local a -um servidor `adb` remoto (contanto que eles usem a mesma versão do protocolo -_adb_): - -```bash -adb kill-server # encerra o servidor adb local em 5037 -ssh -CN -L5037:localhost:5037 -R27183:localhost:27183 your_remote_computer -# mantenha isso aberto -``` - -De outro terminal: - -```bash -scrcpy -``` - -Para evitar ativar o encaminhamento de porta remota, você pode forçar uma conexão -de encaminhamento (note o `-L` em vez de `-R`): - -```bash -adb kill-server # encerra o servidor adb local em 5037 -ssh -CN -L5037:localhost:5037 -L27183:localhost:27183 your_remote_computer -# mantenha isso aberto -``` - -De outro terminal: - -```bash -scrcpy --force-adb-forward -``` - - -Igual a conexões sem fio, pode ser útil reduzir a qualidade: - -``` -scrcpy -b2M -m800 --max-fps 15 -``` - -### Configuração de janela - -#### Título - -Por padrão, o título da janela é o modelo do dispositivo. Isso 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 de janela: - -```bash -scrcpy --window-borderless -``` - -#### Sempre no topo - -Para manter a janela do scrcpy sempre no topo: - -```bash -scrcpy --always-on-top -``` - -#### Tela cheia - -A aplicação pode ser iniciada diretamente em tela cheia: - -```bash -scrcpy --fullscreen -scrcpy -f # versão curta -``` - -Tela cheia pode ser alternada dinamicamente com MOD+f. - -#### Rotação - -A janela pode ser rotacionada: - -```bash -scrcpy --rotation 1 -``` - -Valores possíveis são: - - `0`: sem rotação - - `1`: 90 graus sentido anti-horário - - `2`: 180 graus - - `3`: 90 graus sentido horário - -A rotação também pode ser mudada dinamicamente com MOD+ -_(esquerda)_ e MOD+ _(direita)_. - -Note que _scrcpy_ controla 3 rotações diferentes: - - MOD+r requisita ao dispositivo para mudar entre retrato - e paisagem (a aplicação em execução pode se recusar, se ela não suporta a - orientação requisitada). - - [`--lock-video-orientation`](#travar-orientação-do-vídeo) muda a orientação de - espelhamento (a orientação do vídeo enviado pelo dispositivo para o - computador). Isso afeta a gravação. - - `--rotation` (ou MOD+/MOD+) - rotaciona apenas o conteúdo da janela. Isso afeta apenas a exibição, não a - gravação. - - -### 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 -``` - -#### Display - -Se vários displays estão disponíveis, é possível selecionar o display para -espelhar: - -```bash -scrcpy --display 1 -``` - -A lista de IDs dos displays pode ser obtida por: - -``` -adb shell dumpsys display # busca "mDisplayId=" na saída -``` - -O display secundário pode apenas ser controlado se o dispositivo roda pelo menos Android -10 (caso contrário é espelhado como apenas leitura). - - -#### Permanecer ativo - -Para evitar que o dispositivo seja suspenso após um delay quando o dispositivo é conectado: - -```bash -scrcpy --stay-awake -scrcpy -w -``` - -O estado inicial é restaurado quando o scrcpy é fechado. - - -#### Desligar 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 MOD+o a qualquer momento. - -Para ligar novamente, pressione MOD+Shift+o. - -No Android, o botão de `POWER` sempre liga a tela. Por conveniência, se -`POWER` é enviado via scrcpy (via clique-direito ou MOD+p), ele -forçará a desligar a tela após um delay pequeno (numa base de melhor esforço). -O botão `POWER` físico ainda causará a tela ser ligada. - -Também pode ser útil evitar que o dispositivo seja suspenso: - -```bash -scrcpy --turn-screen-off --stay-awake -scrcpy -Sw -``` - - -#### Mostrar toques - -Para apresentações, pode ser útil mostrar toques físicos (no 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 restaurar o -valor inicial no encerramento: - -```bash -scrcpy --show-touches -scrcpy -t -``` - -Note que isto mostra apenas toques _físicos_ (com o dedo no dispositivo). - - -#### Desativar descanso de tela - -Por padrão, scrcpy não evita que o descanso de tela rode no computador. - -Para desativá-lo: - -```bash -scrcpy --disable-screensaver -``` - - -### Controle de entrada - -#### Rotacionar a tela do dispositivo - -Pressione MOD+r para mudar entre os modos retrato e -paisagem. - -Note que só será rotacionado se a aplicação em primeiro plano suportar a -orientação requisitada. - -#### Copiar-colar - -Sempre que a área de transferência do Android muda, é automaticamente sincronizada com a -área de transferência do computador. - -Qualquer atalho com Ctrl é encaminhado para o dispositivo. Em particular: - - Ctrl+c tipicamente copia - - Ctrl+x tipicamente recorta - - Ctrl+v tipicamente cola (após a sincronização de área de transferência - computador-para-dispositivo) - -Isso tipicamente funciona como esperado. - -O comportamento de fato depende da aplicação ativa, no entanto. Por exemplo, -_Termux_ envia SIGINT com Ctrl+c, e _K-9 Mail_ -compõe uma nova mensagem. - -Para copiar, recortar e colar em tais casos (mas apenas suportado no Android >= 7): - - MOD+c injeta `COPY` - - MOD+x injeta `CUT` - - MOD+v injeta `PASTE` (após a sincronização de área de transferência - computador-para-dispositivo) - -Em adição, MOD+Shift+v permite injetar o -texto da área de transferência do computador como uma sequência de eventos de tecla. Isso é útil quando o -componente não aceita colar texto (por exemplo no _Termux_), mas pode -quebrar conteúdo não-ASCII. - -**ADVERTÊNCIA:** Colar a área de transferência do computador para o dispositivo (tanto via -Ctrl+v quanto MOD+v) copia o conteúdo -para a área de transferência do dispositivo. Como consequência, qualquer aplicação Android pode ler -o seu conteúdo. Você deve evitar colar conteúdo sensível (como senhas) dessa -forma. - -Alguns dispositivos não se comportam como esperado quando a área de transferência é definida -programaticamente. Uma opção `--legacy-paste` é fornecida para mudar o comportamento -de Ctrl+v e MOD+v para que eles -também injetem o texto da área de transferência do computador como uma sequência de eventos de tecla (da mesma -forma que MOD+Shift+v). - -#### Pinçar para dar zoom - -Para simular "pinçar para dar zoom": Ctrl+_clicar-e-mover_. - -Mais precisamente, segure Ctrl enquanto pressiona o botão de clique-esquerdo. Até que -o botão de clique-esquerdo seja liberado, todos os movimentos do mouse ampliar e rotacionam o -conteúdo (se suportado pelo app) relativo ao centro da tela. - -Concretamente, scrcpy gera eventos adicionais de toque de um "dedo virtual" em -uma posição invertida em relação ao centro da tela. - - -#### Preferência de injeção de texto - -Existem dois tipos de [eventos][textevents] gerados ao digitar um texto: - - _eventos de tecla_, 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 tecla, assim o teclado comporta-se -como esperado em jogos (normalmente para teclas WASD). - -Mas isso pode [causar problemas][prefertext]. Se você encontrar tal problema, você -pode evitá-lo com: - -```bash -scrcpy --prefer-text -``` - -(mas isso 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 - - -#### Repetir tecla - -Por padrão, segurar uma tecla gera eventos de tecla repetidos. Isso pode causar -problemas de performance em alguns jogos, onde esses eventos são inúteis de qualquer forma. - -Para evitar o encaminhamento eventos de tecla repetidos: - -```bash -scrcpy --no-key-repeat -``` - - -#### Clique-direito e clique-do-meio - -Por padrão, clique-direito dispara BACK (ou POWER) e clique-do-meio dispara -HOME. Para desabilitar esses atalhos e encaminhar os cliques para o dispositivo: - -```bash -scrcpy --forward-all-clicks -``` - - -### Soltar 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 dispositivo - -Para enviar um arquivo para `/sdcard/Download/` 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 é encaminhado pelo _scrcpy_. Use [sndcpy]. - -Também veja [issue #14]. - -[sndcpy]: https://github.com/rom1v/sndcpy -[issue #14]: https://github.com/Genymobile/scrcpy/issues/14 - - -## Atalhos - -Na lista a seguir, MOD é o modificador de atalho. Por padrão, é -Alt (esquerdo) ou Super (esquerdo). - -Ele pode ser mudado usando `--shortcut-mod`. Possíveis teclas são `lctrl`, `rctrl`, -`lalt`, `ralt`, `lsuper` e `rsuper`. Por exemplo: - -```bash -# usar RCtrl para atalhos -scrcpy --shortcut-mod=rctrl - -# usar tanto LCtrl+LAlt quanto LSuper para atalhos -scrcpy --shortcut-mod=lctrl+lalt,lsuper -``` - -_[Super] é tipicamente a tecla Windows ou Cmd._ - -[Super]: https://en.wikipedia.org/wiki/Super_key_(keyboard_button) - - | Ação | Atalho - | ------------------------------------------- |:----------------------------- - | Mudar modo de tela cheia | MOD+f - | Rotacionar display para esquerda | MOD+ _(esquerda)_ - | Rotacionar display para direita | MOD+ _(direita)_ - | Redimensionar janela para 1:1 (pixel-perfeito) | MOD+g - | Redimensionar janela para remover bordas pretas | MOD+w \| _Clique-duplo-esquerdo¹_ - | Clicar em `HOME` | MOD+h \| _Clique-do-meio_ - | Clicar em `BACK` | MOD+b \| _Clique-direito²_ - | Clicar em `APP_SWITCH` | MOD+s \| _Clique-do-4.°³_ - | Clicar em `MENU` (desbloquear tela) | MOD+m - | Clicar em `VOLUME_UP` | MOD+ _(cima)_ - | Clicar em `VOLUME_DOWN` | MOD+ _(baixo)_ - | Clicar em `POWER` | MOD+p - | Ligar | _Clique-direito²_ - | Desligar tela do dispositivo (continuar espelhando) | MOD+o - | Ligar tela do dispositivo | MOD+Shift+o - | Rotacionar tela do dispositivo | MOD+r - | Expandir painel de notificação | MOD+n \| _Clique-do-5.°³_ - | Expandir painel de configurção | MOD+n+n \| _Clique-duplo-do-5.°³_ - | Colapsar paineis | MOD+Shift+n - | Copiar para área de transferência⁴ | MOD+c - | Recortar para área de transferência⁴ | MOD+x - | Sincronizar áreas de transferência e colar⁴ | MOD+v - | Injetar texto da área de transferência do computador | MOD+Shift+v - | Ativar/desativar contador de FPS (em stdout) | MOD+i - | Pinçar para dar zoom | Ctrl+_Clicar-e-mover_ - -_¹Clique-duplo-esquerdo na borda preta para remove-la._ -_²Clique-direito liga a tela caso esteja desligada, pressione BACK caso contrário._ -_³4.° and 5.° botões do mouse, caso o mouse possua._ -_⁴Apenas em Android >= 7._ - -Atalhos com teclas reptidas são executados soltando e precionando a tecla -uma segunda vez. Por exemplo, para executar "Expandir painel de Configurção": - - 1. Mantenha pressionado MOD. - 2. Depois click duas vezes n. - 3. Finalmente, solte MOD. - -Todos os atalhos Ctrl+_tecla_ são encaminhados para o dispositivo, para que eles sejam -tratados pela aplicação ativa. - - -## Caminhos personalizados - -Para usar um binário _adb_ específico, configure seu caminho na variável de ambiente -`ADB`: - -```bash -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 tão impronunciável quanto [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]. - - -## Problemas comuns - -Veja o [FAQ](FAQ.md). - - -## Desenvolvedores - -Leia a [página dos desenvolvedores][developers page]. - -[developers page]: DEVELOP.md - - -## Licença - - Copyright (C) 2018 Genymobile - Copyright (C) 2018-2022 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/ diff --git a/README.sp.md b/README.sp.md deleted file mode 100644 index 2fc3eb53..00000000 --- a/README.sp.md +++ /dev/null @@ -1,974 +0,0 @@ -Solo se garantiza que el archivo [README](README.md) original esté actualizado. - -# scrcpy (v1.21) - -scrcpy - -Esta aplicación proporciona control e imagen de un dispositivo Android conectado -por USB (o [por TCP/IP](#conexión)). No requiere acceso _root_. -Compatible con _GNU/Linux_, _Windows_ y _macOS_. - -![screenshot](assets/screenshot-debian-600.jpg) - -Se enfoca en: - - **ser ligera**: aplicación nativa, solo muestra la imagen del dispositivo - - **rendimiento**: 30~120fps, dependiendo del dispositivo - - **calidad**: 1920×1080 o superior - - **baja latencia**: [35~70ms][lowlatency] - - **inicio rápido**: ~1 segundo para mostrar la primera imagen - - **no intrusivo**: no deja nada instalado en el dispositivo - - **beneficios**: sin cuentas, sin anuncios, no requiere acceso a internet - - **libertad**: software gratis y de código abierto - -[lowlatency]: https://github.com/Genymobile/scrcpy/pull/646 - -Con la aplicación puede: - - [grabar la pantalla](#capturas-y-grabaciones) - - duplicar la imagen con [la pantalla apagada](#apagar-la-pantalla) - - [copiar y pegar](#copiar-y-pegar) en ambos sentidos - - [configurar la calidad](#configuración-de-captura) - - usar la pantalla del dispositivo [como webcam (V4L2)](#v4l2loopback) (solo en Linux) - - [emular un teclado físico (HID)](#emular-teclado-físico-hid) - (solo en Linux) - - y mucho más… - -## Requisitos - -El dispositivo Android requiere como mínimo API 21 (Android 5.0). - -Asegurate de [habilitar el adb debugging][enable-adb] en tu(s) dispositivo(s). - -[enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling - -En algunos dispositivos, también necesitas habilitar [una opción adicional][control] para controlarlo con el teclado y ratón. - -[control]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323 - - -## Consigue la app - -Packaging status - -### Resumen - - - Linux: `apt install scrcpy` - - Windows: [download](README.md#windows) - - macOS: `brew install scrcpy` - -Construir desde la fuente: [BUILD] ([proceso simplificado][BUILD_simple]) - -[BUILD]: BUILD.md -[BUILD_simple]: BUILD.md#simple - - -### Linux - -En Debian y Ubuntu: - -``` -apt install scrcpy -``` - -Hay un paquete [Snap]: [`scrcpy`][snap-link]. - -[snap-link]: https://snapstats.org/snaps/scrcpy - -[snap]: https://en.wikipedia.org/wiki/Snappy_(package_manager) - -Para Fedora, hay un paquete [COPR]: [`scrcpy`][copr-link]. - -[COPR]: https://fedoraproject.org/wiki/Category:Copr -[copr-link]: https://copr.fedorainfracloud.org/coprs/zeno/scrcpy/ - -Para Arch Linux, hay un paquete [AUR]: [`scrcpy`][aur-link]. - -[AUR]: https://wiki.archlinux.org/index.php/Arch_User_Repository -[aur-link]: https://aur.archlinux.org/packages/scrcpy/ - -Para Gentoo, hay un paquete [Ebuild]: [`scrcpy/`][ebuild-link]. - -[Ebuild]: https://wiki.gentoo.org/wiki/Ebuild -[ebuild-link]: https://github.com/maggu2810/maggu2810-overlay/tree/master/app-mobilephone/scrcpy - -También puedes [construir la aplicación manualmente][BUILD] ([proceso simplificado][BUILD_simple]). - - -### Windows - -Para Windows, por simplicidad, hay un pre-compilado con todas las dependencias -(incluyendo `adb`): - - - [README](README.md#windows) - -También está disponible en [Chocolatey]: - -[Chocolatey]: https://chocolatey.org/ - -```bash -choco install scrcpy -choco install adb # si aún no está instalado -``` - -Y en [Scoop]: - -```bash -scoop install scrcpy -scoop install adb # si aún no está instalado -``` - -[Scoop]: https://scoop.sh - -También puedes [construir la aplicación manualmente][BUILD]. - - -### macOS - -La aplicación está disponible en [Homebrew]. Solo instalala: - -[Homebrew]: https://brew.sh/ - -```bash -brew install scrcpy -``` - -Necesitarás `adb`, accesible desde `PATH`. Si aún no lo tienes: - -```bash -brew install android-platform-tools -``` - -También está disponible en [MacPorts], que configura el adb automáticamente: - -```bash -sudo port install scrcpy -``` - -[MacPorts]: https://www.macports.org/ - - -También puedes [construir la aplicación manualmente][BUILD]. - - -## Ejecutar - -Enchufa el dispositivo Android, y ejecuta: - -```bash -scrcpy -``` - -Acepta argumentos desde la línea de comandos, listados en: - -```bash -scrcpy --help -``` - -## Características - -### Configuración de captura - -#### Reducir la definición - -A veces es útil reducir la definición de la imagen del dispositivo Android para aumentar el desempeño. - -Para limitar el ancho y la altura a un valor específico (ej. 1024): - -```bash -scrcpy --max-size 1024 -scrcpy -m 1024 # versión breve -``` - -La otra dimensión es calculada para conservar el aspect ratio del dispositivo. -De esta forma, un dispositivo en 1920×1080 será transmitido a 1024×576. - - -#### Cambiar el bit-rate - -El bit-rate por defecto es 8 Mbps. Para cambiar el bit-rate del video (ej. a 2 Mbps): - -```bash -scrcpy --bit-rate 2M -scrcpy -b 2M # versión breve -``` - -#### Limitar los fps - -El fps puede ser limitado: - -```bash -scrcpy --max-fps 15 -``` - -Es oficialmente soportado desde Android 10, pero puede funcionar en versiones anteriores. - -#### Recortar - -La imagen del dispositivo puede ser recortada para transmitir solo una parte de la pantalla. - -Por ejemplo, puede ser útil para transmitir la imagen de un solo ojo del Oculus Go: - -```bash -scrcpy --crop 1224:1440:0:0 # 1224x1440 con coordenadas de origen en (0,0) -``` - -Si `--max-size` también está especificado, el cambio de tamaño es aplicado después de cortar. - - -#### Fijar la rotación del video - - -Para fijar la rotación de la transmisión: - -```bash -scrcpy --lock-video-orientation # orientación inicial -scrcpy --lock-video-orientation=0 # orientación normal -scrcpy --lock-video-orientation=1 # 90° contrarreloj -scrcpy --lock-video-orientation=2 # 180° -scrcpy --lock-video-orientation=3 # 90° sentido de las agujas del reloj -``` - -Esto afecta la rotación de la grabación. - -La [ventana también puede ser rotada](#rotación) independientemente. - - -#### Codificador - -Algunos dispositivos pueden tener más de una rotación, y algunos pueden causar problemas o errores. Es posible seleccionar un codificador diferente: - -```bash -scrcpy --encoder OMX.qcom.video.encoder.avc -``` - -Para listar los codificadores disponibles, puedes pasar un nombre de codificador inválido, el error te dará los codificadores disponibles: - -```bash -scrcpy --encoder _ -``` - -### Capturas y grabaciones - - -#### Grabación - -Es posible grabar la pantalla mientras se transmite: - -```bash -scrcpy --record file.mp4 -scrcpy -r file.mkv -``` - -Para grabar sin transmitir la pantalla: - -```bash -scrcpy --no-display --record file.mp4 -scrcpy -Nr file.mkv -# interrumpe la grabación con Ctrl+C -``` - -Los "skipped frames" son grabados, incluso si no se mostrados en tiempo real (por razones de desempeño). Los frames tienen _marcas de tiempo_ en el dispositivo, por lo que el "[packet delay -variation]" no impacta el archivo grabado. - -[packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation - - -#### v4l2loopback - -En Linux se puede mandar el stream del video a un dispositivo loopback v4l2, por -lo que se puede abrir el dispositivo Android como una webcam con cualquier -programa compatible con v4l2. - -Se debe instalar el modulo `v4l2loopback`: - -```bash -sudo apt install v4l2loopback-dkms -``` - -Para crear un dispositivo v4l2: - -```bash -sudo modprobe v4l2loopback -``` - -Esto va a crear un nuevo dispositivo de video en `/dev/videoN`, donde `N` es un número -(hay más [opciones](https://github.com/umlaeute/v4l2loopback#options) disponibles -para crear múltiples dispositivos o usar un ID en específico). - -Para ver los dispositivos disponibles: - -```bash -# requiere el paquete v4l-utils -v4l2-ctl --list-devices -# simple pero generalmente suficiente -ls /dev/video* -``` - -Para iniciar scrcpy usando una fuente v4l2: - -```bash -scrcpy --v4l2-sink=/dev/videoN -scrcpy --v4l2-sink=/dev/videoN --no-display # deshabilita la transmisión de imagen -scrcpy --v4l2-sink=/dev/videoN -N # más corto -``` - -(reemplace `N` con el ID del dispositivo, compruebe con `ls /dev/video*`) - -Una vez habilitado, podés abrir el stream del video con una herramienta compatible con v4l2: - -```bash -ffplay -i /dev/videoN -vlc v4l2:///dev/videoN # VLC puede agregar un delay por buffering -``` - -Por ejemplo, podrías capturar el video usando [OBS]. - -[OBS]: https://obsproject.com/ - - -#### Buffering - -Es posible agregar buffering al video. Esto reduce el ruido en la imagen ("jitter") -pero aumenta la latencia (vea [#2464]). - -[#2464]: https://github.com/Genymobile/scrcpy/issues/2464 - -La opción de buffering está disponible para la transmisión de imagen: - -```bash -scrcpy --display-buffer=50 # agrega 50 ms de buffering a la imagen -``` - -y las fuentes V4L2: - -```bash -scrcpy --v4l2-buffer=500 # agrega 500 ms de buffering a la fuente v4l2 -``` - - -### Conexión - -#### TCP/IP (Inalámbrica) - -_Scrcpy_ usa `adb` para comunicarse con el dispositivo, y `adb` puede [conectarse] vía TCP/IP. -El dispositivo debe estar conectado a la misma red que la computadora: - -##### Automático - -La opción `--tcpip` permite configurar la conexión automáticamente. Hay 2 variables. - -Si el dispositivo (accesible en 192.168.1.1 para este ejemplo) ya está escuchando -en un puerto (generalmente 5555) esperando una conexión adb entrante, entonces corré: - -```bash -scrcpy --tcpip=192.168.1.1 # el puerto default es 5555 -scrcpy --tcpip=192.168.1.1:5555 -``` - -Si el dispositivo no tiene habilitado el modo adb TCP/IP (o si no sabés la dirección IP), -entonces conectá el dispositivo por USB y corré: - -```bash -scrcpy --tcpip # sin argumentos -``` - -El programa buscará automáticamente la IP del dispositivo, habilitará el modo TCP/IP, y -se conectará al dispositivo antes de comenzar a transmitir la imagen. - -##### Manual - -Como alternativa, se puede habilitar la conexión TCP/IP manualmente usando `adb`: - -1. Conecta el dispositivo al mismo Wi-Fi que tu computadora. -2. Obtén la dirección IP del dispositivo, en Ajustes → Acerca del dispositivo → Estado, o ejecutando este comando: - - ```bash - adb shell ip route | awk '{print $9}' - ``` - -3. Habilita adb vía TCP/IP en el dispositivo: `adb tcpip 5555`. -4. Desenchufa el dispositivo. -5. Conéctate a tu dispositivo: `adb connect IP_DEL_DISPOSITIVO:5555` _(reemplaza `IP_DEL_DISPOSITIVO`)_. -6. Ejecuta `scrcpy` con normalidad. - -Podría resultar útil reducir el bit-rate y la definición: - -```bash -scrcpy --bit-rate 2M --max-size 800 -scrcpy -b2M -m800 # versión breve -``` - -[conectarse]: https://developer.android.com/studio/command-line/adb.html#wireless - - -#### Múltiples dispositivos - -Si hay muchos dispositivos listados en `adb devices`, será necesario especificar el _número de serie_: - -```bash -scrcpy --serial 0123456789abcdef -scrcpy -s 0123456789abcdef # versión breve -``` - -Si el dispositivo está conectado por TCP/IP: - -```bash -scrcpy --serial 192.168.0.1:5555 -scrcpy -s 192.168.0.1:5555 # versión breve -``` - -Puedes iniciar múltiples instancias de _scrcpy_ para múltiples dispositivos. - -#### Iniciar automáticamente al detectar dispositivo - -Puedes utilizar [AutoAdb]: - -```bash -autoadb scrcpy -s '{}' -``` - -[AutoAdb]: https://github.com/rom1v/autoadb - -#### Túneles - -Para conectarse a un dispositivo remoto, es posible conectar un cliente local `adb` a un servidor remoto `adb` (siempre y cuando utilicen la misma versión de protocolos _adb_). - -##### Servidor ADB remoto - -Para conectarse a un servidor ADB remoto, haz que el servidor escuche en todas las interfaces: - -```bash -adb kill-server -adb -a nodaemon server start -# conserva este servidor abierto -``` - -**Advertencia: todas las comunicaciones entre los clientes y el servidor ADB están desencriptadas.** - -Supondremos que este servidor se puede acceder desde 192.168.1.2. Entonces, desde otra -terminal, corré scrcpy: - -```bash -export ADB_SERVER_SOCKET=tcp:192.168.1.2:5037 -scrcpy --tunnel-host=192.168.1.2 -``` - -Por default, scrcpy usa el puerto local que se usó para establecer el tunel -`adb forward` (típicamente `27183`, vea `--port`). También es posible forzar un -puerto diferente (puede resultar útil en situaciones más complejas, donde haya -múltiples redirecciones): - -``` -scrcpy --tunnel-port=1234 -``` - - -##### Túnel SSH - -Para comunicarse con un servidor ADB remoto de forma segura, es preferible usar un túnel SSH. - -Primero, asegurate que el servidor ADB está corriendo en la computadora remota: - -```bash -adb start-server -``` - -Después, establecé el túnel SSH: - -```bash -# local 5038 --> remoto 5037 -# local 27183 <-- remoto 27183 -ssh -CN -L5038:localhost:5037 -R27183:localhost:27183 your_remote_computer -# conserva este servidor abierto -``` - -Desde otra terminal, corré scrcpy: - -```bash -export ADB_SERVER_SOCKET=tcp:localhost:5038 -scrcpy -``` - -Para evitar habilitar "remote port forwarding", puedes forzar una "forward connection" (nótese el argumento `-L` en vez de `-R`): - -```bash -# local 5038 --> remoto 5037 -# local 27183 --> remoto 27183 -ssh -CN -L5038:localhost:5037 -L27183:localhost:27183 your_remote_computer -# conserva este servidor abierto -``` - -Desde otra terminal, corré scrcpy: - -```bash -export ADB_SERVER_SOCKET=tcp:localhost:5038 -scrcpy --force-adb-forward -``` - -Al igual que las conexiones inalámbricas, puede resultar útil reducir la calidad: - -``` -scrcpy -b2M -m800 --max-fps 15 -``` - -### Configuración de la ventana - -#### Título - -Por defecto, el título de la ventana es el modelo del dispositivo. Puede ser modificado: - -```bash -scrcpy --window-title 'My device' -``` - -#### Posición y tamaño - -La posición y tamaño inicial de la ventana puede ser especificado: - -```bash -scrcpy --window-x 100 --window-y 100 --window-width 800 --window-height 600 -``` - -#### Sin bordes - -Para deshabilitar el diseño de la ventana: - -```bash -scrcpy --window-borderless -``` - -#### Siempre adelante - -Para mantener la ventana de scrcpy siempre adelante: - -```bash -scrcpy --always-on-top -``` - -#### Pantalla completa - -La aplicación puede ser iniciada en pantalla completa: - -```bash -scrcpy --fullscreen -scrcpy -f # versión breve -``` - -Puede entrar y salir de la pantalla completa con la combinación MOD+f. - -#### Rotación - -Se puede rotar la ventana: - -```bash -scrcpy --rotation 1 -``` - -Los posibles valores son: - - `0`: sin rotación - - `1`: 90 grados contrarreloj - - `2`: 180 grados - - `3`: 90 grados en sentido de las agujas del reloj - -La rotación también puede ser modificada con la combinación de teclas MOD+ _(izquierda)_ y MOD+ _(derecha)_. - -Nótese que _scrcpy_ maneja 3 diferentes rotaciones: - - MOD+r solicita al dispositivo cambiar entre vertical y horizontal (la aplicación en uso puede rechazarlo si no soporta la orientación solicitada). - - [`--lock-video-orientation`](#fijar-la-rotación-del-video) cambia la rotación de la transmisión (la orientación del video enviado a la PC). Esto afecta a la grabación. - - `--rotation` (o MOD+/MOD+) rota solo el contenido de la imagen. Esto solo afecta a la imagen mostrada, no a la grabación. - - -### Otras opciones - -#### Solo lectura ("Read-only") - -Para deshabilitar los controles (todo lo que interactúe con el dispositivo: eventos del teclado, eventos del mouse, arrastrar y soltar archivos): - -```bash -scrcpy --no-control -scrcpy -n # versión breve -``` - -#### Pantalla - -Si múltiples pantallas están disponibles, es posible elegir cual transmitir: - -```bash -scrcpy --display 1 -``` - -Los ids de las pantallas se pueden obtener con el siguiente comando: - -```bash -adb shell dumpsys display # busque "mDisplayId=" en la respuesta -``` - -La segunda pantalla solo puede ser manejada si el dispositivo cuenta con Android 10 (en caso contrario será transmitida en el modo solo lectura). - - -#### Permanecer activo - -Para evitar que el dispositivo descanse después de un tiempo mientras está conectado: - -```bash -scrcpy --stay-awake -scrcpy -w # versión breve -``` - -La configuración original se restaura al cerrar scrcpy. - - -#### Apagar la pantalla - -Es posible apagar la pantalla mientras se transmite al iniciar con el siguiente comando: - -```bash -scrcpy --turn-screen-off -scrcpy -S # versión breve -``` - -O presionando MOD+o en cualquier momento. - -Para volver a prenderla, presione MOD+Shift+o. - -En Android, el botón de `POWER` siempre prende la pantalla. Por conveniencia, si `POWER` es enviado vía scrcpy (con click-derecho o MOD+p), esto forzará a apagar la pantalla con un poco de atraso (en la mejor de las situaciones). El botón físico `POWER` seguirá prendiendo la pantalla. - -También puede resultar útil para evitar que el dispositivo entre en inactividad: - -```bash -scrcpy --turn-screen-off --stay-awake -scrcpy -Sw # versión breve -``` - - -#### Apagar al cerrar la aplicación - -Para apagar la pantalla del dispositivo al cerrar scrcpy: - -```bash -scrcpy --power-off-on-close -``` - -#### Mostrar clicks - -Para presentaciones, puede resultar útil mostrar los clicks físicos (en el dispositivo físicamente). - -Android provee esta opción en _Opciones para desarrolladores_. - -_Scrcpy_ provee una opción para habilitar esta función al iniciar la aplicación y restaurar el valor original al salir: - -```bash -scrcpy --show-touches -scrcpy -t # versión breve -``` - -Nótese que solo muestra los clicks _físicos_ (con el dedo en el dispositivo). - - -#### Desactivar protector de pantalla - -Por defecto, scrcpy no evita que el protector de pantalla se active en la computadora. - -Para deshabilitarlo: - -```bash -scrcpy --disable-screensaver -``` - - -### Control - -#### Rotar pantalla del dispositivo - -Presione MOD+r para cambiar entre posición vertical y horizontal. - -Nótese que solo rotará si la aplicación activa soporta la orientación solicitada. - -#### Copiar y pegar - -Cuando que el portapapeles de Android cambia, automáticamente se sincroniza al portapapeles de la computadora. - -Cualquier shortcut con Ctrl es enviado al dispositivo. En particular: - - Ctrl+c normalmente copia - - Ctrl+x normalmente corta - - Ctrl+v normalmente pega (después de la sincronización de portapapeles entre la computadora y el dispositivo) - -Esto normalmente funciona como es esperado. - -Sin embargo, este comportamiento depende de la aplicación en uso. Por ejemplo, _Termux_ envía SIGINT con Ctrl+c, y _K-9 Mail_ crea un nuevo mensaje. - -Para copiar, cortar y pegar, en tales casos (solo soportado en Android >= 7): - - MOD+c inyecta `COPY` - - MOD+x inyecta `CUT` - - MOD+v inyecta `PASTE` (después de la sincronización de portapapeles entre la computadora y el dispositivo) - -Además, MOD+Shift+v permite inyectar el texto en el portapapeles de la computadora como una secuencia de teclas. Esto es útil cuando el componente no acepta pegado de texto (por ejemplo en _Termux_), pero puede romper caracteres no pertenecientes a ASCII. - -**AVISO:** Pegar de la computadora al dispositivo (tanto con Ctrl+v o MOD+v) copia el contenido al portapapeles del dispositivo. Como consecuencia, cualquier aplicación de Android puede leer su contenido. Debería evitar pegar contenido sensible (como contraseñas) de esta forma. - -Algunos dispositivos no se comportan como es esperado al establecer el portapapeles programáticamente. La opción `--legacy-paste` está disponible para cambiar el comportamiento de Ctrl+v y MOD+v para que también inyecten el texto del portapapeles de la computadora como una secuencia de teclas (de la misma forma que MOD+Shift+v). - -Para deshabilitar la auto-sincronización del portapapeles, use `--no-clipboard-autosync`. - -#### Pellizcar para zoom - -Para simular "pinch-to-zoom": Ctrl+_click-y-mover_. - -Más precisamente, mantén Ctrl mientras presionas botón izquierdo. Hasta que no se suelte el botón, todos los movimientos del mouse cambiarán el tamaño y rotación del contenido (si es soportado por la app en uso) respecto al centro de la pantalla. - -Concretamente, scrcpy genera clicks adicionales con un "dedo virtual" en la posición invertida respecto al centro de la pantalla. - -#### Emular teclado físico (HID) - -Por default, scrcpy usa el sistema de Android para la injección de teclas o texto: -funciona en todas partes, pero está limitado a ASCII. - -En Linux, scrcpy puede emular un teclado USB físico en Android para proveer -una mejor experiencia al enviar _inputs_ (usando [USB HID vía AOAv2][hid-aoav2]): -deshabilita el teclado virtual y funciona para todos los caracteres y IME. - -[hid-aoav2]: https://source.android.com/devices/accessories/aoa2#hid-support - -Sin embargo, solo funciona si el dispositivo está conectado por USB, y por ahora -solo funciona en Linux. - -Para habilitar este modo: - -```bash -scrcpy --hid-keyboard -scrcpy -K # más corto -``` - -Si por alguna razón falla (por ejemplo si el dispositivo no está conectado vía -USB), automáticamente vuelve al modo default (un mensaje se escribirá en la consola). -Se puede usar los mismos argumentos en la línea de comandos tanto si se conecta con -USB o vía TCP/IP. - -En este modo, los _raw key events_ (_scancodes_) se envían al dispositivo, independientemente -del mapeo del teclado en el host. Por eso, si el diseño de tu teclado no concuerda, debe ser -configurado en el dispositivo Android, en Ajustes → Sistema → Idioma y Entrada de Texto -→ [Teclado Físico]. - -Se puede iniciar automáticamente en esta página de ajustes: - -```bash -adb shell am start -a android.settings.HARD_KEYBOARD_SETTINGS -``` - -Sin embargo, la opción solo está disponible cuando el teclado HID está activo -(o cuando se conecta un teclado físico). - -[Teclado Físico]: https://github.com/Genymobile/scrcpy/pull/2632#issuecomment-923756915 - - -#### Preferencias de inyección de texto - -Existen dos tipos de [eventos][textevents] generados al escribir texto: - - _key events_, marcando si la tecla es presionada o soltada; - - _text events_, marcando si un texto fue introducido. - -Por defecto, las letras son inyectadas usando _key events_, para que el teclado funcione como es esperado en juegos (típicamente las teclas WASD). - -Pero esto puede [causar problemas][prefertext]. Si encuentras tales problemas, los puedes evitar con: - -```bash -scrcpy --prefer-text -``` - -(Pero esto romperá el comportamiento del teclado en los juegos) - -Por el contrario, se puede forzar scrcpy para siempre injectar _raw key events_: - -```bash -scrcpy --raw-key-events -``` - -Estas opciones no tienen efecto en los teclados HID (todos los _key events_ son enviados como -_scancodes_ en este modo). - -[textevents]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-text-input -[prefertext]: https://github.com/Genymobile/scrcpy/issues/650#issuecomment-512945343 - - -#### Repetir tecla - -Por defecto, mantener una tecla presionada genera múltiples _key events_. Esto puede -causar problemas de desempeño en algunos juegos, donde estos eventos no tienen sentido de todos modos. - -Para evitar enviar _key events_ repetidos: - -```bash -scrcpy --no-key-repeat -``` - -Estas opciones no tienen efecto en los teclados HID (Android maneja directamente -las repeticiones de teclas en este modo) - - -#### Botón derecho y botón del medio - -Por defecto, botón derecho ejecuta RETROCEDER (o ENCENDIDO) y botón del medio INICIO. Para inhabilitar estos atajos y enviar los clicks al dispositivo: - -```bash -scrcpy --forward-all-clicks -``` - - -### Arrastrar y soltar archivos - -#### Instalar APKs - -Para instalar un APK, arrastre y suelte el archivo APK (terminado en `.apk`) a la ventana de _scrcpy_. - -No hay respuesta visual, un mensaje se escribirá en la consola. - - -#### Enviar archivos al dispositivo - -Para enviar un archivo a `/sdcard/Download/` en el dispositivo, arrastre y suelte -un archivo (no APK) a la ventana de _scrcpy_. - -No hay ninguna respuesta visual, un mensaje se escribirá en la consola. - -El directorio de destino puede ser modificado al iniciar: - -```bash -scrcpy --push-target=/sdcard/Movies/ -``` - - -### Envío de Audio - -_Scrcpy_ no envía el audio. Use [sndcpy]. - -También lea [issue #14]. - -[sndcpy]: https://github.com/rom1v/sndcpy -[issue #14]: https://github.com/Genymobile/scrcpy/issues/14 - - -## Atajos - -En la siguiente lista, MOD es el atajo modificador. Por defecto es Alt (izquierdo) o Super (izquierdo). - -Se puede modificar usando `--shortcut-mod`. Las posibles teclas son `lctrl` (izquierdo), `rctrl` (derecho), `lalt` (izquierdo), `ralt` (derecho), `lsuper` (izquierdo) y `rsuper` (derecho). Por ejemplo: - -```bash -# use RCtrl para los atajos -scrcpy --shortcut-mod=rctrl - -# use tanto LCtrl+LAlt o LSuper para los atajos -scrcpy --shortcut-mod=lctrl+lalt,lsuper -``` - -_[Super] es generalmente la tecla Windows o Cmd._ - -[Super]: https://en.wikipedia.org/wiki/Super_key_(keyboard_button) - - | Acción | Atajo - | ------------------------------------------- |:----------------------------- - | Alterne entre pantalla compelta | MOD+f - | Rotar pantalla hacia la izquierda | MOD+ _(izquierda)_ - | Rotar pantalla hacia la derecha | MOD+ _(derecha)_ - | Ajustar ventana a 1:1 ("pixel-perfect") | MOD+g - | Ajustar ventana para quitar los bordes negros| MOD+w \| _Doble click izquierdo¹_ - | Click en `INICIO` | MOD+h \| _Click medio_ - | Click en `RETROCEDER` | MOD+b \| _Click derecho²_ - | Click en `CAMBIAR APLICACIÓN` | MOD+s \| _Cuarto botón³_ - | Click en `MENÚ` (desbloquear pantalla)⁴ | MOD+m - | Click en `SUBIR VOLUMEN` | MOD+ _(arriba)_ - | Click en `BAJAR VOLUME` | MOD+ _(abajo)_ - | Click en `ENCENDIDO` | MOD+p - | Encendido | _Botón derecho²_ - | Apagar pantalla (manteniendo la transmisión) | MOD+o - | Encender pantalla | MOD+Shift+o - | Rotar pantalla del dispositivo | MOD+r - | Abrir panel de notificaciones | MOD+n \| _Quinto botón³_ - | Abrir panel de configuración | MOD+n+n \| _Doble quinto botón³_ - | Cerrar paneles | MOD+Shift+n - | Copiar al portapapeles⁵ | MOD+c - | Cortar al portapapeles⁵ | MOD+x - | Synchronizar portapapeles y pegar⁵ | MOD+v - | Inyectar texto del portapapeles de la PC | MOD+Shift+v - | Habilitar/Deshabilitar contador de FPS (en stdout) | MOD+i - | Pellizcar para zoom | Ctrl+_click-y-mover_ - | Arrastrar y soltar un archivo (APK) | Instalar APK desde la computadora - | Arrastrar y soltar un archivo (no APK) | [Mover archivo al dispositivo](#enviar-archivos-al-dispositivo) - -_¹Doble click en los bordes negros para eliminarlos._ -_²Botón derecho enciende la pantalla si estaba apagada, sino ejecuta RETROCEDER._ -_³Cuarto y quinto botón del mouse, si tu mouse los tiene._ -_⁴Para las apps react-native en desarrollo, `MENU` activa el menú de desarrollo._ -_⁵Solo en Android >= 7._ - -Los shortcuts con teclas repetidas se ejecutan soltando y volviendo a apretar la tecla -por segunda vez. Por ejemplo, para ejecutar "Abrir panel de configuración": - - 1. Apretá y mantené apretado MOD. - 2. Después apretá dos veces la tecla n. - 3. Por último, soltá la tecla MOD. - -Todos los atajos Ctrl+_tecla_ son enviados al dispositivo para que sean manejados por la aplicación activa. - - -## Path personalizado - -Para usar un binario de _adb_ en particular, configure el path `ADB` en las variables de entorno: - -```bash -ADB=/path/to/adb scrcpy -``` - -Para sobreescribir el path del archivo `scrcpy-server`, configure el path en `SCRCPY_SERVER_PATH`. - -Para sobreescribir el ícono, configure el path en `SCRCPY_ICON_PATH`. - - -## ¿Por qué _scrcpy_? - -Un colega me retó a encontrar un nombre tan impronunciable como [gnirehtet]. - -[`strcpy`] copia un **str**ing; `scrcpy` copia un **scr**een. - -[gnirehtet]: https://github.com/Genymobile/gnirehtet -[`strcpy`]: http://man7.org/linux/man-pages/man3/strcpy.3.html - - -## ¿Cómo construir (BUILD)? - -Véase [BUILD] (en inglés). - - -## Problemas generales - -Vea las [preguntas frecuentes (en inglés)](FAQ.md). - - -## Desarrolladores - -Lea la [hoja de desarrolladores (en inglés)](DEVELOP.md). - - -## Licencia - - Copyright (C) 2018 Genymobile - Copyright (C) 2018-2022 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. - -## Artículos - -- [Introducing scrcpy][article-intro] (en inglés) -- [Scrcpy now works wirelessly][article-tcpip] (en inglés) - -[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.tr.md b/README.tr.md deleted file mode 100644 index 9501a889..00000000 --- a/README.tr.md +++ /dev/null @@ -1,824 +0,0 @@ -# scrcpy (v1.18) - -Bu uygulama Android cihazların USB (ya da [TCP/IP][article-tcpip]) üzerinden -görüntülenmesini ve kontrol edilmesini sağlar. _root_ erişimine ihtiyaç duymaz. -_GNU/Linux_, _Windows_ ve _macOS_ sistemlerinde çalışabilir. - -![screenshot](assets/screenshot-debian-600.jpg) - -Öne çıkan özellikler: - -- **hafiflik** (doğal, sadece cihazın ekranını gösterir) -- **performans** (30~60fps) -- **kalite** (1920×1080 ya da üzeri) -- **düşük gecikme süresi** ([35~70ms][lowlatency]) -- **düşük başlangıç süresi** (~1 saniye ilk kareyi gösterme süresi) -- **müdaheleci olmama** (cihazda kurulu yazılım kalmaz) - -[lowlatency]: https://github.com/Genymobile/scrcpy/pull/646 - -## Gereksinimler - -Android cihaz en düşük API 21 (Android 5.0) olmalıdır. - -[Adb hata ayıklamasının][enable-adb] cihazınızda aktif olduğundan emin olun. - -[enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling - -Bazı cihazlarda klavye ve fare ile kontrol için [ilave bir seçenek][control] daha -etkinleştirmeniz gerekebilir. - -[control]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323 - -## Uygulamayı indirin - -Packaging status - -### Özet - -- Linux: `apt install scrcpy` -- Windows: [indir][direct-win64] -- macOS: `brew install scrcpy` - -Kaynak kodu derle: [BUILD] ([basitleştirilmiş süreç][build_simple]) - -[build]: BUILD.md -[build_simple]: BUILD.md#simple - -### Linux - -Debian (şimdilik _testing_ ve _sid_) ve Ubuntu (20.04) için: - -``` -apt install scrcpy -``` - -[Snap] paketi: [`scrcpy`][snap-link]. - -[snap-link]: https://snapstats.org/snaps/scrcpy -[snap]: https://en.wikipedia.org/wiki/Snappy_(package_manager) - -Fedora için, [COPR] paketi: [`scrcpy`][copr-link]. - -[copr]: https://fedoraproject.org/wiki/Category:Copr -[copr-link]: https://copr.fedorainfracloud.org/coprs/zeno/scrcpy/ - -Arch Linux için, [AUR] paketi: [`scrcpy`][aur-link]. - -[aur]: https://wiki.archlinux.org/index.php/Arch_User_Repository -[aur-link]: https://aur.archlinux.org/packages/scrcpy/ - -Gentoo için, [Ebuild] mevcut: [`scrcpy/`][ebuild-link]. - -[ebuild]: https://wiki.gentoo.org/wiki/Ebuild -[ebuild-link]: https://github.com/maggu2810/maggu2810-overlay/tree/master/app-mobilephone/scrcpy - -Ayrıca [uygulamayı el ile de derleyebilirsiniz][build] ([basitleştirilmiş süreç][build_simple]). - -### Windows - -Windows için (`adb` dahil) tüm gereksinimleri ile derlenmiş bir arşiv mevcut: - - - [README](README.md#windows) - -[Chocolatey] ile kurulum: - -[chocolatey]: https://chocolatey.org/ - -```bash -choco install scrcpy -choco install adb # if you don't have it yet -``` - -[Scoop] ile kurulum: - -```bash -scoop install scrcpy -scoop install adb # if you don't have it yet -``` - -[scoop]: https://scoop.sh - -Ayrıca [uygulamayı el ile de derleyebilirsiniz][build]. - -### macOS - -Uygulama [Homebrew] içerisinde mevcut. Sadece kurun: - -[homebrew]: https://brew.sh/ - -```bash -brew install scrcpy -``` - -`adb`, `PATH` içerisinden erişilebilir olmalıdır. Eğer değilse: - -```bash -brew install android-platform-tools -``` - -[MacPorts] kullanılarak adb ve uygulamanın birlikte kurulumu yapılabilir: - -```bash -sudo port install scrcpy -``` - -[macports]: https://www.macports.org/ - -Ayrıca [uygulamayı el ile de derleyebilirsiniz][build]. - -## Çalıştırma - -Android cihazınızı bağlayın ve aşağıdaki komutu çalıştırın: - -```bash -scrcpy -``` - -Komut satırı argümanları aşağıdaki komut ile listelenebilir: - -```bash -scrcpy --help -``` - -## Özellikler - -### Ekran yakalama ayarları - -#### Boyut azaltma - -Bazen, Android cihaz ekranını daha düşük seviyede göstermek performansı artırabilir. - -Hem genişliği hem de yüksekliği bir değere sabitlemek için (ör. 1024): - -```bash -scrcpy --max-size 1024 -scrcpy -m 1024 # kısa versiyon -``` - -Diğer boyut en-boy oranı korunacak şekilde hesaplanır. -Bu şekilde ekran boyutu 1920x1080 olan bir cihaz 1024x576 olarak görünür. - -#### Bit-oranı değiştirme - -Varsayılan bit-oranı 8 Mbps'dir. Değiştirmek için (ör. 2 Mbps): - -```bash -scrcpy --bit-rate 2M -scrcpy -b 2M # kısa versiyon -``` - -#### Çerçeve oranı sınırlama - -Ekran yakalama için maksimum çerçeve oranı için sınır koyulabilir: - -```bash -scrcpy --max-fps 15 -``` - -Bu özellik Android 10 ve sonrası sürümlerde resmi olarak desteklenmektedir, -ancak daha önceki sürümlerde çalışmayabilir. - -#### Kesme - -Cihaz ekranının sadece bir kısmı görünecek şekilde kesilebilir. - -Bu özellik Oculus Go'nun bir gözünü yakalamak gibi durumlarda kullanışlı olur: - -```bash -scrcpy --crop 1224:1440:0:0 # (0,0) noktasından 1224x1440 -``` - -Eğer `--max-size` belirtilmişse yeniden boyutlandırma kesme işleminden sonra yapılır. - -#### Video yönünü kilitleme - -Videonun yönünü kilitlemek için: - -```bash -scrcpy --lock-video-orientation # başlangıç yönü -scrcpy --lock-video-orientation=0 # doğal yön -scrcpy --lock-video-orientation=1 # 90° saatin tersi yönü -scrcpy --lock-video-orientation=2 # 180° -scrcpy --lock-video-orientation=3 # 90° saat yönü -``` - -Bu özellik kaydetme yönünü de etkiler. - -[Pencere ayrı olarak döndürülmüş](#rotation) olabilir. - -#### Kodlayıcı - -Bazı cihazlar birden fazla kodlayıcıya sahiptir, ve bunların bazıları programın -kapanmasına sebep olabilir. Bu durumda farklı bir kodlayıcı seçilebilir: - -```bash -scrcpy --encoder OMX.qcom.video.encoder.avc -``` - -Mevcut kodlayıcıları listelemek için geçerli olmayan bir kodlayıcı ismi girebilirsiniz, -hata mesajı mevcut kodlayıcıları listeleyecektir: - -```bash -scrcpy --encoder _ -``` - -### Yakalama - -#### Kaydetme - -Ekran yakalama sırasında kaydedilebilir: - -```bash -scrcpy --record file.mp4 -scrcpy -r file.mkv -``` - -Yakalama olmadan kayıt için: - -```bash -scrcpy --no-display --record file.mp4 -scrcpy -Nr file.mkv -# Ctrl+C ile kayıt kesilebilir -``` - -"Atlanan kareler" gerçek zamanlı olarak gösterilmese (performans sebeplerinden ötürü) dahi kaydedilir. -Kareler cihazda _zamandamgası_ ile saklanır, bu sayede [paket gecikme varyasyonu] -kayıt edilen dosyayı etkilemez. - -[paket gecikme varyasyonu]: https://en.wikipedia.org/wiki/Packet_delay_variation - -#### v4l2loopback - -Linux'ta video akışı bir v4l2 loopback cihazına gönderilebilir. Bu sayede Android -cihaz bir web kamerası gibi davranabilir. - -Bu işlem için `v4l2loopback` modülü kurulu olmalıdır: - -```bash -sudo apt install v4l2loopback-dkms -``` - -v4l2 cihazı oluşturmak için: - -```bash -sudo modprobe v4l2loopback -``` - -Bu komut `/dev/videoN` adresinde `N` yerine bir tamsayı koyarak yeni bir video -cihazı oluşturacaktır. -(birden fazla cihaz oluşturmak veya spesifik ID'ye sahip cihazlar için -diğer [seçenekleri](https://github.com/umlaeute/v4l2loopback#options) inceleyebilirsiniz.) - -Aktif cihazları listelemek için: - -```bash -# v4l-utils paketi ile -v4l2-ctl --list-devices - -# daha basit ama yeterli olabilecek şekilde -ls /dev/video* -``` - -v4l2 kullanarak scrpy kullanmaya başlamak için: - -```bash -scrcpy --v4l2-sink=/dev/videoN -scrcpy --v4l2-sink=/dev/videoN --no-display # ayna penceresini kapatarak -scrcpy --v4l2-sink=/dev/videoN -N # kısa versiyon -``` - -(`N` harfini oluşturulan cihaz ID numarası ile değiştirin. `ls /dev/video*` cihaz ID'lerini görebilirsiniz.) - -Aktifleştirildikten sonra video akışını herhangi bir v4l2 özellikli araçla açabilirsiniz: - -```bash -ffplay -i /dev/videoN -vlc v4l2:///dev/videoN # VLC kullanırken yükleme gecikmesi olabilir -``` - -Örneğin, [OBS] ile video akışını kullanabilirsiniz. - -[obs]: https://obsproject.com/ - -### Bağlantı - -#### Kablosuz - -_Scrcpy_ cihazla iletişim kurmak için `adb`'yi kullanır, Ve `adb` -bir cihaza TCP/IP kullanarak [bağlanabilir]. - -1. Cihazınızı bilgisayarınızla aynı Wi-Fi ağına bağlayın. -2. Cihazınızın IP adresini bulun. Ayarlar → Telefon Hakkında → Durum sekmesinden veya - aşağıdaki komutu çalıştırarak öğrenebilirsiniz: - - ```bash - adb shell ip route | awk '{print $9}' - ``` - -3. Cihazınızda TCP/IP üzerinden adb kullanımını etkinleştirin: `adb tcpip 5555`. -4. Cihazınızı bilgisayarınızdan sökün. -5. Cihazınıza bağlanın: `adb connect DEVICE_IP:5555` _(`DEVICE_IP` değerini değiştirin)_. -6. `scrcpy` komutunu normal olarak çalıştırın. - -Bit-oranını ve büyüklüğü azaltmak yararlı olabilir: - -```bash -scrcpy --bit-rate 2M --max-size 800 -scrcpy -b2M -m800 # kısa version -``` - -[bağlanabilir]: https://developer.android.com/studio/command-line/adb.html#wireless - -#### Birden fazla cihaz - -Eğer `adb devices` komutu birden fazla cihaz listeliyorsa _serial_ değerini belirtmeniz gerekir: - -```bash -scrcpy --serial 0123456789abcdef -scrcpy -s 0123456789abcdef # kısa versiyon -``` - -Eğer cihaz TCP/IP üzerinden bağlanmışsa: - -```bash -scrcpy --serial 192.168.0.1:5555 -scrcpy -s 192.168.0.1:5555 # kısa version -``` - -Birden fazla cihaz için birden fazla _scrcpy_ uygulaması çalıştırabilirsiniz. - -#### Cihaz bağlantısı ile otomatik başlatma - -[AutoAdb] ile yapılabilir: - -```bash -autoadb scrcpy -s '{}' -``` - -[autoadb]: https://github.com/rom1v/autoadb - -#### SSH Tünel - -Uzaktaki bir cihaza erişmek için lokal `adb` istemcisi, uzaktaki bir `adb` sunucusuna -(aynı _adb_ sürümünü kullanmak şartı ile) bağlanabilir : - -```bash -adb kill-server # 5037 portunda çalışan lokal adb sunucusunu kapat -ssh -CN -L5037:localhost:5037 -R27183:localhost:27183 your_remote_computer -# bunu açık tutun -``` - -Başka bir terminalde: - -```bash -scrcpy -``` - -Uzaktan port yönlendirme ileri yönlü bağlantı kullanabilirsiniz -(`-R` yerine `-L` olduğuna dikkat edin): - -```bash -adb kill-server # 5037 portunda çalışan lokal adb sunucusunu kapat -ssh -CN -L5037:localhost:5037 -L27183:localhost:27183 your_remote_computer -# bunu açık tutun -``` - -Başka bir terminalde: - -```bash -scrcpy --force-adb-forward -``` - -Kablosuz bağlantı gibi burada da kalite düşürmek faydalı olabilir: - -``` -scrcpy -b2M -m800 --max-fps 15 -``` - -### Pencere ayarları - -#### İsim - -Cihaz modeli varsayılan pencere ismidir. Değiştirmek için: - -```bash -scrcpy --window-title 'Benim cihazım' -``` - -#### Konum ve - -Pencerenin başlangıç konumu ve boyutu belirtilebilir: - -```bash -scrcpy --window-x 100 --window-y 100 --window-width 800 --window-height 600 -``` - -#### Kenarlıklar - -Pencere dekorasyonunu kapatmak için: - -```bash -scrcpy --window-borderless -``` - -#### Her zaman üstte - -Scrcpy penceresini her zaman üstte tutmak için: - -```bash -scrcpy --always-on-top -``` - -#### Tam ekran - -Uygulamayı tam ekran başlatmak için: - -```bash -scrcpy --fullscreen -scrcpy -f # kısa versiyon -``` - -Tam ekran MOD+f ile dinamik olarak değiştirilebilir. - -#### Döndürme - -Pencere döndürülebilir: - -```bash -scrcpy --rotation 1 -``` - -Seçilebilecek değerler: - -- `0`: döndürme yok -- `1`: 90 derece saat yönünün tersi -- `2`: 180 derece -- `3`: 90 derece saat yönü - -Döndürme MOD+_(sol)_ ve -MOD+ _(sağ)_ ile dinamik olarak değiştirilebilir. - -_scrcpy_'de 3 farklı döndürme olduğuna dikkat edin: - -- MOD+r cihazın yatay veya dikey modda çalışmasını sağlar. - (çalışan uygulama istenilen oryantasyonda çalışmayı desteklemiyorsa döndürme - işlemini reddedebilir.) -- [`--lock-video-orientation`](#lock-video-orientation) görüntü yakalama oryantasyonunu - (cihazdan bilgisayara gelen video akışının oryantasyonu) değiştirir. Bu kayıt işlemini - etkiler. -- `--rotation` (or MOD+/MOD+) - pencere içeriğini dönderir. Bu sadece canlı görüntüyü etkiler, kayıt işlemini etkilemez. - -### Diğer ekran yakalama seçenekleri - -#### Yazma korumalı - -Kontrolleri devre dışı bırakmak için (cihazla etkileşime geçebilecek her şey: klavye ve -fare girdileri, dosya sürükleyip bırakma): - -```bash -scrcpy --no-control -scrcpy -n -``` - -#### Ekran - -Eğer cihazın birden fazla ekranı varsa hangi ekranın kullanılacağını seçebilirsiniz: - -```bash -scrcpy --display 1 -``` - -Kullanılabilecek ekranları listelemek için: - -```bash -adb shell dumpsys display # çıktı içerisinde "mDisplayId=" terimini arayın -``` - -İkinci ekran ancak cihaz Android sürümü 10 veya üzeri olmalıdır (değilse yazma korumalı -olarak görüntülenir). - -#### Uyanık kalma - -Cihazın uyku moduna girmesini engellemek için: - -```bash -scrcpy --stay-awake -scrcpy -w -``` - -scrcpy kapandığında cihaz başlangıç durumuna geri döner. - -#### Ekranı kapatma - -Ekran yakalama sırasında cihazın ekranı kapatılabilir: - -```bash -scrcpy --turn-screen-off -scrcpy -S -``` - -Ya da MOD+o kısayolunu kullanabilirsiniz. - -Tekrar açmak için ise MOD+Shift+o tuşlarına basın. - -Android'de, `GÜÇ` tuşu her zaman ekranı açar. Eğer `GÜÇ` sinyali scrcpy ile -gönderilsiyse (sağ tık veya MOD+p), ekran kısa bir gecikme -ile kapanacaktır. Fiziksel `GÜÇ` tuşuna basmak hala ekranın açılmasına sebep olacaktır. - -Bu cihazın uykuya geçmesini engellemek için kullanılabilir: - -```bash -scrcpy --turn-screen-off --stay-awake -scrcpy -Sw -``` - -#### Dokunuşları gösterme - -Sunumlar sırasında fiziksel dokunuşları (fiziksel cihazdaki) göstermek -faydalı olabilir. - -Android'de bu özellik _Geliştici seçenekleri_ içerisinde bulunur. - -_Scrcpy_ bu özelliği çalışırken etkinleştirebilir ve kapanırken eski -haline geri getirebilir: - -```bash -scrcpy --show-touches -scrcpy -t -``` - -Bu opsiyon sadece _fiziksel_ dokunuşları (cihaz ekranındaki) gösterir. - -#### Ekran koruyucuyu devre dışı bırakma - -Scrcpy varsayılan ayarlarında ekran koruyucuyu devre dışı bırakmaz. - -Bırakmak için: - -```bash -scrcpy --disable-screensaver -``` - -### Girdi kontrolü - -#### Cihaz ekranını dönderme - -MOD+r tuşları ile yatay ve dikey modlar arasında -geçiş yapabilirsiniz. - -Bu kısayol ancak çalışan uygulama desteklediği takdirde ekranı döndürecektir. - -#### Kopyala yapıştır - -Ne zaman Android cihazdaki pano değişse bilgisayardaki pano otomatik olarak -senkronize edilir. - -Tüm Ctrl kısayolları cihaza iletilir: - -- Ctrl+c genelde kopyalar -- Ctrl+x genelde keser -- Ctrl+v genelde yapıştırır (bilgisayar ve cihaz arasındaki - pano senkronizasyonundan sonra) - -Bu kısayollar genelde beklediğiniz gibi çalışır. - -Ancak kısayolun gerçekten yaptığı eylemi açık olan uygulama belirler. -Örneğin, _Termux_ Ctrl+c ile kopyalama yerine -SIGINT sinyali gönderir, _K-9 Mail_ ise yeni mesaj oluşturur. - -Bu tip durumlarda kopyalama, kesme ve yapıştırma için (Android versiyon 7 ve -üstü): - -- MOD+c `KOPYALA` -- MOD+x `KES` -- MOD+v `YAPIŞTIR` (bilgisayar ve cihaz arasındaki - pano senkronizasyonundan sonra) - -Bunlara ek olarak, MOD+Shift+v tuşları -bilgisayar pano içeriğini tuş basma eylemleri şeklinde gönderir. Bu metin -yapıştırmayı desteklemeyen (_Termux_ gibi) uygulamar için kullanışlıdır, -ancak ASCII olmayan içerikleri bozabilir. - -**UYARI:** Bilgisayar pano içeriğini cihaza yapıştırmak -(Ctrl+v ya da MOD+v tuşları ile) -içeriği cihaz panosuna kopyalar. Sonuç olarak, herhangi bir Android uygulaması -içeriğe erişebilir. Hassas içerikler (parolalar gibi) için bu özelliği kullanmaktan -kaçının. - -Bazı cihazlar pano değişikleri konusunda beklenilen şekilde çalışmayabilir. -Bu durumlarda `--legacy-paste` argümanı kullanılabilir. Bu sayede -Ctrl+v ve MOD+v tuşları da -pano içeriğini tuş basma eylemleri şeklinde gönderir -(MOD+Shift+v ile aynı şekilde). - -#### İki parmak ile yakınlaştırma - -"İki parmak ile yakınlaştırma" için: Ctrl+_tıkla-ve-sürükle_. - -Daha açıklayıcı şekilde, Ctrl tuşuna sol-tık ile birlikte basılı -tutun. Sol-tık serbest bırakılıncaya kadar yapılan tüm fare hareketleri -ekran içeriğini ekranın merkezini baz alarak dönderir, büyütür veya küçültür -(eğer uygulama destekliyorsa). - -Scrcpy ekranın merkezinde bir "sanal parmak" varmış gibi davranır. - -#### Metin gönderme tercihi - -Metin girilirken ili çeşit [eylem][textevents] gerçekleştirilir: - -- _tuş eylemleri_, bir tuşa basıldığı sinyalini verir; -- _metin eylemleri_, bir metin girildiği sinyalini verir. - -Varsayılan olarak, harfler tuş eylemleri kullanılarak gönderilir. Bu sayede -klavye oyunlarda beklenilene uygun olarak çalışır (Genelde WASD tuşları). - -Ancak bu [bazı problemlere][prefertext] yol açabilir. Eğer bu problemler ile -karşılaşırsanız metin eylemlerini tercih edebilirsiniz: - -```bash -scrcpy --prefer-text -``` - -(Ama bu oyunlardaki klavye davranışlarını bozacaktır) - -[textevents]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-text-input -[prefertext]: https://github.com/Genymobile/scrcpy/issues/650#issuecomment-512945343 - -#### Tuş tekrarı - -Varsayılan olarak, bir tuşa basılı tutmak tuş eylemini tekrarlar. Bu durum -bazı oyunlarda problemlere yol açabilir. - -Tuş eylemlerinin tekrarını kapatmak için: - -```bash -scrcpy --no-key-repeat -``` - -#### Sağ-tık ve Orta-tık - -Varsayılan olarak, sağ-tık GERİ (ya da GÜÇ açma) eylemlerini, orta-tık ise -ANA EKRAN eylemini tetikler. Bu kısayolları devre dışı bırakmak için: - -```bash -scrcpy --forward-all-clicks -``` - -### Dosya bırakma - -#### APK kurulumu - -APK kurmak için, bilgisayarınızdaki APK dosyasını (`.apk` ile biten) _scrcpy_ -penceresine sürükleyip bırakın. - -Bu eylem görsel bir geri dönüt oluşturmaz, konsola log yazılır. - -#### Dosyayı cihaza gönderme - -Bir dosyayı cihazdaki `/sdcard/Download/` dizinine atmak için, (APK olmayan) -bir dosyayı _scrcpy_ penceresine sürükleyip bırakın. - -Bu eylem görsel bir geri dönüt oluşturmaz, konsola log yazılır. - -Hedef dizin uygulama başlatılırken değiştirilebilir: - -```bash -scrcpy --push-target=/sdcard/Movies/ -``` - -### Ses iletimi - -_Scrcpy_ ses iletimi yapmaz. Yerine [sndcpy] kullanabilirsiniz. - -Ayrıca bakınız [issue #14]. - -[sndcpy]: https://github.com/rom1v/sndcpy -[issue #14]: https://github.com/Genymobile/scrcpy/issues/14 - -## Kısayollar - -Aşağıdaki listede, MOD kısayol tamamlayıcısıdır. Varsayılan olarak -(sol) Alt veya (sol) Super tuşudur. - -Bu tuş `--shortcut-mod` argümanı kullanılarak `lctrl`, `rctrl`, -`lalt`, `ralt`, `lsuper` ve `rsuper` tuşlarından biri ile değiştirilebilir. -Örneğin: - -```bash -# Sağ Ctrl kullanmak için -scrcpy --shortcut-mod=rctrl - -# Sol Ctrl, Sol Alt veya Sol Super tuşlarından birini kullanmak için -scrcpy --shortcut-mod=lctrl+lalt,lsuper -``` - -_[Super] tuşu genelde Windows veya Cmd tuşudur._ - -[super]: https://en.wikipedia.org/wiki/Super_key_(keyboard_button) - -| Action | Shortcut | -| ------------------------------------------------ | :-------------------------------------------------------- | -| Tam ekran modunu değiştirme | MOD+f | -| Ekranı sola çevirme | MOD+ _(sol)_ | -| Ekranı sağa çevirme | MOD+ _(sağ)_ | -| Pencereyi 1:1 oranına çevirme (pixel-perfect) | MOD+g | -| Penceredeki siyah kenarlıkları kaldırma | MOD+w \| _Çift-sol-tık¹_ | -| `ANA EKRAN` tuşu | MOD+h \| _Orta-tık_ | -| `GERİ` tuşu | MOD+b \| _Sağ-tık²_ | -| `UYGULAMA_DEĞİŞTİR` tuşu | MOD+s \| _4.tık³_ | -| `MENÜ` tuşu (ekran kilidini açma) | MOD+m | -| `SES_AÇ` tuşu | MOD+ _(yukarı)_ | -| `SES_KIS` tuşu | MOD+ _(aşağı)_ | -| `GÜÇ` tuşu | MOD+p | -| Gücü açma | _Sağ-tık²_ | -| Cihaz ekranını kapatma (ekran yakalama durmadan) | MOD+o | -| Cihaz ekranını açma | MOD+Shift+o | -| Cihaz ekranını dönderme | MOD+r | -| Bildirim panelini genişletme | MOD+n \| _5.tık³_ | -| Ayarlar panelini genişletme | MOD+n+n \| _Çift-5.tık³_ | -| Panelleri kapatma | MOD+Shift+n | -| Panoya kopyalama⁴ | MOD+c | -| Panoya kesme⁴ | MOD+x | -| Panoları senkronize ederek yapıştırma⁴ | MOD+v | -| Bilgisayar panosundaki metini girme | MOD+Shift+v | -| FPS sayacını açma/kapatma (terminalde) | MOD+i | -| İki parmakla yakınlaştırma | Ctrl+_tıkla-ve-sürükle_ | - -_¹Siyah kenarlıkları silmek için üzerine çift tıklayın._ -_²Sağ-tık ekran kapalıysa açar, değilse GERİ sinyali gönderir._ -_³4. ve 5. fare tuşları (eğer varsa)._ -_⁴Sadece Android 7 ve üzeri versiyonlarda._ - -Tekrarlı tuşu olan kısayollar tuş bırakılıp tekrar basılarak tekrar çalıştırılır. -Örneğin, "Ayarlar panelini genişletmek" için: - -1. MOD tuşuna basın ve basılı tutun. -2. n tuşuna iki defa basın. -3. MOD tuşuna basmayı bırakın. - -Tüm Ctrl+_tuş_ kısayolları cihaza gönderilir. Bu sayede istenilen komut -uygulama tarafından çalıştırılır. - -## Özel dizinler - -Varsayılandan farklı bir _adb_ programı çalıştırmak için `ADB` ortam değişkenini -ayarlayın: - -```bash -ADB=/path/to/adb scrcpy -``` - -`scrcpy-server` programının dizinini değiştirmek için `SCRCPY_SERVER_PATH` -değişkenini ayarlayın. - -[useful]: https://github.com/Genymobile/scrcpy/issues/278#issuecomment-429330345 - -## Neden _scrcpy_? - -Bir meslektaşım [gnirehtet] gibi söylenmesi zor bir isim bulmam için bana meydan okudu. - -[`strcpy`] **str**ing kopyalıyor; `scrcpy` **scr**een kopyalıyor. - -[gnirehtet]: https://github.com/Genymobile/gnirehtet -[`strcpy`]: http://man7.org/linux/man-pages/man3/strcpy.3.html - -## Nasıl derlenir? - -Bakınız [BUILD]. - -## Yaygın problemler - -Bakınız [FAQ](FAQ.md). - -## Geliştiriciler - -[Geliştiriciler sayfası]nı okuyun. - -[geliştiriciler sayfası]: DEVELOP.md - -## Lisans - - Copyright (C) 2018 Genymobile - Copyright (C) 2018-2022 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. - -## Makaleler - -- [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.zh-Hans.md b/README.zh-Hans.md deleted file mode 100644 index ac0713a6..00000000 --- a/README.zh-Hans.md +++ /dev/null @@ -1,993 +0,0 @@ -_Only the original [README](README.md) is guaranteed to be up-to-date._ - -_只有原版的 [README](README.md)是保证最新的。_ - -Current version is based on [f4c7044] - -本文根据[f4c7044]进行翻译。 - -[f4c7044]: https://github.com/Genymobile/scrcpy/blob/f4c7044b46ae28eb64cb5e1a15c9649a44023c70/README.md - -# scrcpy (v1.22) - -scrcpy - -_发音为 "**scr**een **c**o**py**"_ - -本应用程序可以显示并控制通过 USB (或 [TCP/IP][article-tcpip]) 连接的安卓设备,且不需要任何 _root_ 权限。本程序支持 _GNU/Linux_, _Windows_ 和 _macOS_。 - -![screenshot](assets/screenshot-debian-600.jpg) - -本应用专注于: - - - **轻量**: 原生,仅显示设备屏幕 - - **性能**: 30~120fps,取决于设备 - - **质量**: 分辨率可达 1920×1080 或更高 - - **低延迟**: [35~70ms][lowlatency] - - **快速启动**: 最快 1 秒内即可显示第一帧 - - **无侵入性**: 不会在设备上遗留任何程序 - - **用户利益**: 无需帐号,无广告,无需联网 - - **自由**: 自由和开源软件 - -[lowlatency]: https://github.com/Genymobile/scrcpy/pull/646 - -功能: - - [屏幕录制](#屏幕录制) - - 镜像时[关闭设备屏幕](#关闭设备屏幕) - - 双向[复制粘贴](#复制粘贴) - - [可配置显示质量](#采集设置) - - 以设备屏幕[作为摄像头(V4L2)](#v4l2loopback) (仅限 Linux) - - [模拟物理键盘 (HID)](#物理键盘模拟-hid) (仅限 Linux) - - [物理鼠标模拟 (HID)](#物理鼠标模拟-hid) (仅限 Linux) - - [OTG模式](#otg) (仅限 Linux) - - 更多 …… - -## 系统要求 - -安卓设备最低需要支持 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 - - -## 获取本程序 - -Packaging status - -### 概要 - - - Linux: `apt install scrcpy` - - Windows: [下载][direct-win64] - - macOS: `brew install scrcpy` - -从源代码编译: [构建][BUILD] ([简化过程][BUILD_simple]) - -[BUILD]: BUILD.md -[BUILD_simple]: BUILD.md#simple - -### Linux - -在 Debian 和 Ubuntu 上: - -``` -apt install scrcpy -``` - -在 Arch Linux 上: - -``` -pacman -S 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/ - -对 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 - -您也可以[自行构建][BUILD] ([简化过程][BUILD_simple])。 - - -### Windows - -在 Windows 上,为简便起见,我们提供包含了所有依赖 (包括 `adb`) 的预编译包。 - - - [README](README.md#windows) - -也可以使用 [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 - -您也可以[自行构建][BUILD]。 - - -### macOS - -本程序已发布到 [Homebrew]。直接安装即可: - -[Homebrew]: https://brew.sh/ - -```bash -brew install scrcpy -``` - -你还需要在 `PATH` 内有 `adb`。如果还没有: - -```bash -brew install android-platform-tools -``` - -或者通过 [MacPorts],该方法同时设置好 adb: - -```bash -sudo port install scrcpy -``` - -[MacPorts]: https://www.macports.org/ - -您也可以[自行构建][BUILD]。 - - -## 运行 - -连接安卓设备,然后执行: - -```bash -scrcpy -``` - -本程序支持命令行参数,查看参数列表: - -```bash -scrcpy --help -``` - -## 功能介绍 - -### 采集设置 - -#### 降低分辨率 - -有时候,可以通过降低镜像的分辨率来提高性能。 - -要同时限制宽度和高度到某个值 (例如 1024): - -```bash -scrcpy --max-size 1024 -scrcpy -m 1024 # 简写 -``` - -另一边会被按比例缩小以保持设备的显示比例。这样,1920×1080 分辨率的设备会以 1024×576 的分辨率进行镜像。 - - -#### 修改码率 - -默认码率是 8 Mbps。改变视频码率 (例如改为 2 Mbps): - -```bash -scrcpy --bit-rate 2M -scrcpy -b 2M # 简写 -``` - -#### 限制帧率 - -要限制采集的帧率: - -```bash -scrcpy --max-fps 15 -``` - -本功能从 Android 10 开始才被官方支持,但在一些旧版本中也能生效。 - -#### 画面裁剪 - -可以对设备屏幕进行裁剪,只镜像屏幕的一部分。 - -例如可以只镜像 Oculus Go 的一只眼睛。 - -```bash -scrcpy --crop 1224:1440:0:0 # 以 (0,0) 为原点的 1224x1440 像素 -``` - -如果同时指定了 `--max-size`,会先进行裁剪,再进行缩放。 - - -#### 锁定屏幕方向 - - -要锁定镜像画面的方向: - -```bash -scrcpy --lock-video-orientation # 初始(目前)方向 -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 --encoder OMX.qcom.video.encoder.avc -``` - -要列出可用的编码器,可以指定一个不存在的编码器名称,错误信息中会包含所有的编码器: - -```bash -scrcpy --encoder _ -``` - -### 采集 - -#### 屏幕录制 - -可以在镜像的同时录制视频: - -```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] 不会影响录制的文件。 - -[packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation - - -#### v4l2loopback - -在 Linux 上,可以将视频流发送至 v4l2 回环 (loopback) 设备,因此可以使用任何 v4l2 工具像摄像头一样打开安卓设备。 - -需安装 `v4l2loopback` 模块: - -```bash -sudo apt install v4l2loopback-dkms -``` - -创建一个 v4l2 设备: - -```bash -sudo modprobe v4l2loopback -``` - -这样会在 `/dev/videoN` 创建一个新的视频设备,其中 `N` 是整数。 ([更多选项](https://github.com/umlaeute/v4l2loopback#options) 可以用来创建多个设备或者特定 ID 的设备)。 - -列出已启用的设备: - -```bash -# 需要 v4l-utils 包 -v4l2-ctl --list-devices - -# 简单但或许足够 -ls /dev/video* -``` - -使用一个 v4l2 漏开启 scrcpy: - -```bash -scrcpy --v4l2-sink=/dev/videoN -scrcpy --v4l2-sink=/dev/videoN --no-display # 禁用窗口镜像 -scrcpy --v4l2-sink=/dev/videoN -N # 简写 -``` - -(将 `N` 替换为设备 ID,使用 `ls /dev/video*` 命令查看) - -启用之后,可以使用 v4l2 工具打开视频流: - -```bash -ffplay -i /dev/videoN -vlc v4l2:///dev/videoN # VLC 可能存在一些缓冲延迟 -``` - -例如,可以在 [OBS] 中采集视频。 - -[OBS]: https://obsproject.com/ - - -#### 缓冲 - -可以加入缓冲,会增加延迟,但可以减少抖动 (见 [#2464])。 - -[#2464]: https://github.com/Genymobile/scrcpy/issues/2464 - -对于显示缓冲: - -```bash -scrcpy --display-buffer=50 # 为显示增加 50 毫秒的缓冲 -``` - -对于 V4L2 漏: - -```bash -scrcpy --v4l2-buffer=500 # 为 v4l2 漏增加 500 毫秒的缓冲 -``` - - -### 连接 - -#### TCP/IP (无线) - -_Scrcpy_ 使用 `adb` 与设备通信,并且 `adb` 支持通过 TCP/IP [连接]到设备(设备必须连接与电脑相同的网络)。 - -##### 自动配置 - -参数 `--tcpip` 允许自动配置连接。这里有两种方式。 - -对于传入的 adb 连接,如果设备(在这个例子中以192.168.1.1为可用地址)已经监听了一个端口(通常是5555),运行: - -```bash -scrcpy --tcpip=192.168.1.1 # 默认端口是5555 -scrcpy --tcpip=192.168.1.1:5555 -``` - -如果adb TCP/IP(无线) 模式在某些设备上不被启用(或者你不知道IP地址),用USB连接设备,然后运行: - -```bash -scrcpy --tcpip # 无需其他参数 -``` - -这将会自动寻找设备IP地址,启用TCP/IP模式,然后在启动之前连接到设备。 - -##### 手动配置 - -或者,可以通过 `adb` 使用手动启用 TCP/IP 连接: - -1. 将设备和电脑连接至同一 Wi-Fi。 -2. 打开 设置 → 关于手机 → 状态信息,获取设备的 IP 地址,也可以执行以下的命令: - - ```bash - adb shell ip route | awk '{print $9}' - ``` - -3. 启用设备的网络 adb 功能:`adb tcpip 5555`。 -4. 断开设备的 USB 连接。 -5. 连接到您的设备:`adb connect DEVICE_IP:5555` _(将 `DEVICE_IP` 替换为设备 IP)_。 -6. 正常运行 `scrcpy`。 - -降低比特率和分辨率可能很有用: - -```bash -scrcpy --bit-rate 2M --max-size 800 -scrcpy -b2M -m800 # 简写 -``` - -[连接]: https://developer.android.com/studio/command-line/adb.html#wireless - - -#### 多设备 - -如果 `adb devices` 列出了多个设备,您必须指定设备的 _序列号_ : - -```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 - -#### 隧道 - -要远程连接到设备,可以将本地的 adb 客户端连接到远程的 adb 服务端 (需要两端的 _adb_ 协议版本相同)。 - -##### 远程ADB服务器 - -要连接到一个远程ADB服务器,让服务器在所有接口上监听: - -```bash -adb kill-server -adb -a nodaemon server start -# 保持该窗口开启 -``` - -**警告:所有客户端与ADB服务器的交流都是未加密的。** - -假设此服务器可在 192.168.1.2 访问。 然后,从另一个终端,运行 scrcpy: - -```bash -export ADB_SERVER_SOCKET=tcp:192.168.1.2:5037 -scrcpy --tunnel-host=192.168.1.2 -``` - -默认情况下,scrcpy使用用于 `adb forward` 隧道建立的本地端口(通常是 `27183`,见 `--port` )。它也可以强制使用一个不同的隧道端口(当涉及更多的重定向时,这在更复杂的情况下可能很有用): - -``` -scrcpy --tunnel-port=1234 -``` - - -##### SSH 隧道 - -为了安全地与远程ADB服务器通信,最好使用SSH隧道。 - -首先,确保ADB服务器正在远程计算机上运行: - -```bash -adb start-server -``` - -然后,建立一个SSH隧道: - -```bash -# 本地 5038 --> 远程 5037 -# 本地 27183 <-- 远程 27183 -ssh -CN -L5038:localhost:5037 -R27183:localhost:27183 your_remote_computer -# 保持该窗口开启 -``` - -在另一个终端上,运行scrcpy: - -```bash -export ADB_SERVER_SOCKET=tcp:localhost:5038 -scrcpy -``` - -若要不使用远程端口转发,可以强制使用正向连接(注意是 `-L` 而不是 `-R` ): - -```bash -# 本地 5038 --> 远程 5037 -# 本地 27183 <-- 远程 27183 -ssh -CN -L5038:localhost:5037 -L27183:localhost:27183 your_remote_computer -# 保持该窗口开启 -``` - -在另一个终端上,运行scrcpy: - -```bash -export ADB_SERVER_SOCKET=tcp:localhost:5038 -scrcpy --force-adb-forward -``` - - -类似地,对于无线连接,可能需要降低画面质量: - -``` -scrcpy -b2M -m800 --max-fps 15 -``` - -### 窗口设置 - -#### 标题 - -窗口的标题默认为设备型号。可以通过如下命令修改: - -```bash -scrcpy --window-title "我的设备" -``` - -#### 位置和大小 - -您可以指定初始的窗口位置和大小: - -```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 # 简写 -``` - -全屏状态可以通过 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 -``` - -scrcpy 关闭时会恢复设备原来的设置。 - - -#### 关闭设备屏幕 - -可以通过以下的命令行参数在关闭设备屏幕的状态下进行镜像: - -```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 --power-off-on-close -``` - -#### 显示触摸 - -在演示时,可能会需要显示 (在物理设备上的) 物理触摸点。 - -Android 在 _开发者选项_ 中提供了这项功能。 - -_Scrcpy_ 提供一个选项可以在启动时开启这项功能并在退出时恢复初始设置: - -```bash -scrcpy --show-touches -scrcpy -t -``` - -请注意这项功能只能显示 _物理_ 触摸 (用手指在屏幕上的触摸)。 - - -#### 关闭屏保 - -_Scrcpy_ 默认不会阻止电脑上开启的屏幕保护。 - -关闭屏幕保护: - -```bash -scrcpy --disable-screensaver -``` - - -### 输入控制 - -#### 旋转设备屏幕 - -使用 MOD+r 在竖屏和横屏模式之间切换。 - -需要注意的是,只有在前台应用程序支持所要求的模式时,才会进行切换。 - -#### 复制粘贴 - -每次安卓的剪贴板变化时,其内容都会被自动同步到电脑的剪贴板上。 - -所有的 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) 都会将内容复制到设备的剪贴板。如此,任何安卓应用程序都能读取到。您应避免将敏感内容 (如密码) 通过这种方式粘贴。 - -一些设备不支持通过程序设置剪贴板。通过 `--legacy-paste` 选项可以修改 Ctrl+vMOD+v 的工作方式,使它们通过按键事件 (同 MOD+Shift+v) 来注入电脑剪贴板内容。 - -要禁用自动剪贴板同步功能,使用`--no-clipboard-autosync`。 - -#### 双指缩放 - -模拟“双指缩放”:Ctrl+_按下并拖动鼠标_。 - -在按住 Ctrl 时按下鼠标左键,直到松开鼠标左键前,移动鼠标会使屏幕内容相对于屏幕中心进行缩放或旋转 (如果应用支持)。 - -具体来说,_scrcpy_ 会在鼠标位置,以及鼠标以屏幕中心镜像的位置分别生成触摸事件。 - -#### 物理键盘模拟 (HID) - -默认情况下,scrcpy 使用安卓按键或文本注入,这在任何情况都可以使用,但仅限于ASCII字符。 - -在 Linux 上,scrcpy 可以模拟为 Android 上的物理 USB 键盘,以提供更好地输入体验 (使用 [USB HID over AOAv2][hid-aoav2]):禁用虚拟键盘,并适用于任何字符和输入法。 - -[hid-aoav2]: https://source.android.com/devices/accessories/aoa2#hid-support - -不过,这种方法仅支持 USB 连接以及 Linux平台。 - -启用 HID 模式: - -```bash -scrcpy --hid-keyboard -scrcpy -K # 简写 -``` - -如果失败了 (如设备未通过 USB 连接),则自动回退至默认模式 (终端中会输出日志)。这即允许通过 USB 和 TCP/IP 连接时使用相同的命令行参数。 - -在这种模式下,原始按键事件 (扫描码) 被发送给设备,而与宿主机按键映射无关。因此,若键盘布局不匹配,需要在 Android 设备上进行配置,具体为 设置 → 系统 → 语言和输入法 → [实体键盘]。 - -[实体键盘]: https://github.com/Genymobile/scrcpy/pull/2632#issuecomment-923756915 - -#### 物理鼠标模拟 (HID) - -与物理键盘模拟类似,可以模拟一个物理鼠标。 同样,它仅在设备通过 USB 连接时才有效,并且目前仅在 Linux 上受支持。 - -默认情况下,scrcpy 使用 Android 鼠标事件注入,使用绝对坐标。 通过模拟物理鼠标,在Android设备上出现鼠标指针,并注入鼠标相对运动、点击和滚动。 - -启用此模式: - -```bash -scrcpy --hid-mouse -scrcpy -M # 简写 -``` - -您还可以将 `--forward-all-clicks` 添加到 [转发所有点击][forward_all_clicks]. - -[forward_all_clicks]: #右键和中键 - -启用此模式后,计算机鼠标将被“捕获”(鼠标指针从计算机上消失并出现在 Android 设备上)。 - -特殊的捕获键,AltSuper,切换(禁用或启用)鼠标捕获。 使用其中之一将鼠标的控制权交还给计算机。 - - -#### OTG - -可以仅使用物理键盘和鼠标模拟 (HID) 运行 _scrcpy_,就好像计算机键盘和鼠标通过 OTG 线直接插入设备一样。 - -在这个模式下,_adb_ (USB 调试)是不必要的,且镜像被禁用。 - -启用 OTG 模式: - -```bash -scrcpy --otg -# 如果有多个 USB 设备可用,则通过序列号选择 -scrcpy --otg -s 0123456789abcdef -``` - -只开启 HID 键盘 或 HID 鼠标 是可行的: - -```bash -scrcpy --otg --hid-keyboard # 只开启 HID 键盘 -scrcpy --otg --hid-mouse # 只开启 HID 鼠标 -scrcpy --otg --hid-keyboard --hid-mouse # 开启 HID 键盘 和 HID 鼠标 -# 为了方便,默认两者都开启 -scrcpy --otg # 开启 HID 键盘 和 HID 鼠标 -``` - -像 `--hid-keyboard` 和 `--hid-mouse` 一样,它只在设备通过 USB 连接时才有效,且目前仅在 Linux 上支持。 - - -#### 文本注入偏好 - -输入文字的时候,系统会产生两种[事件][textevents]: - - _按键事件_ ,代表一个按键被按下或松开。 - - _文本事件_ ,代表一个字符被输入。 - -程序默认使用按键事件来输入字母。只有这样,键盘才会在游戏中正常运作 (例如 WASD 键)。 - -但这也有可能[造成一些问题][prefertext]。如果您遇到了问题,可以通过以下方式避免: - -```bash -scrcpy --prefer-text -``` - -(但这会导致键盘在游戏中工作不正常) - -相反,您可以强制始终注入原始按键事件: - -```bash -scrcpy --raw-key-events -``` - -该选项不影响 HID 键盘 (该模式下,所有按键都发送为扫描码)。 - -[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 -``` - -该选项不影响 HID 键盘 (该模式下,按键重复由 Android 直接管理)。 - -#### 右键和中键 - -默认状态下,右键会触发返回键 (或电源键开启),中键会触发 HOME 键。要禁用这些快捷键并把所有点击转发到设备: - -```bash -scrcpy --forward-all-clicks -``` - - -### 文件拖放 - -#### 安装APK - -将 APK 文件 (文件名以 `.apk` 结尾) 拖放到 _scrcpy_ 窗口来安装。 - -不会有视觉反馈,终端会输出一条日志。 - - -#### 将文件推送至设备 - -要推送文件到设备的 `/sdcard/Download/`,将 (非 APK) 文件拖放至 _scrcpy_ 窗口。 - -不会有视觉反馈,终端会输出一条日志。 - -在启动时可以修改目标目录: - -```bash -scrcpy --push-target=/sdcard/Movies/ -``` - - -### 音频转发 - -_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] 键通常是指 WindowsCmd 键。_ - -[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 \| _第4键³_ - | 点按 `菜单` (解锁屏幕)⁴ | MOD+m - | 点按 `音量+` | MOD+ _(上箭头)_ - | 点按 `音量-` | MOD+ _(下箭头)_ - | 点按 `电源` | MOD+p - | 打开屏幕 | _鼠标右键²_ - | 关闭设备屏幕 (但继续在电脑上显示) | MOD+o - | 打开设备屏幕 | MOD+Shift+o - | 旋转设备屏幕 | MOD+r - | 展开通知面板 | MOD+n \| _第5键³_ - | 展开设置面板 | MOD+n+n \| _双击第5键³_ - | 收起通知面板 | MOD+Shift+n - | 复制到剪贴板⁵ | MOD+c - | 剪切到剪贴板⁵ | MOD+x - | 同步剪贴板并粘贴⁵ | MOD+v - | 注入电脑剪贴板文本 | MOD+Shift+v - | 打开/关闭FPS显示 (至标准输出) | MOD+i - | 捏拉缩放 | Ctrl+_按住并移动鼠标_ - | 拖放 APK 文件 | 从电脑安装 APK 文件 - | 拖放非 APK 文件 | [将文件推送至设备](#push-file-to-device) - -_¹双击黑边可以去除黑边。_ -_²点击鼠标右键将在屏幕熄灭时点亮屏幕,其余情况则视为按下返回键 。_ -_³鼠标的第4键和第5键。_ -_⁴对于开发中的 react-native 应用程序,`MENU` 触发开发菜单。_ -_⁵需要安卓版本 Android >= 7。_ - -有重复按键的快捷键通过松开再按下一个按键来进行,如“展开设置面板”: - - 1. 按下 MOD 不放。 - 2. 双击 n。 - 3. 松开 MOD。 - -所有的 Ctrl+_按键_ 的快捷键都会被转发到设备,所以会由当前应用程序进行处理。 - - -## 自定义路径 - -要使用指定的 _adb_ 二进制文件,可以设置环境变量 `ADB`: - -```bash -ADB=/path/to/adb scrcpy -``` - -要覆盖 `scrcpy-server` 的路径,可以设置 `SCRCPY_SERVER_PATH`。 - -要覆盖图标,可以设置其路径至 `SCRCPY_ICON_PATH`。 - - -## 为什么叫 _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]。 - - -## 常见问题 - -请查看 [FAQ](FAQ.md)。 - - -## 开发者 - -请查看[开发者页面]。 - -[开发者页面]: DEVELOP.md - - -## 许可协议 - - Copyright (C) 2018 Genymobile - Copyright (C) 2018-2022 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/ diff --git a/README.zh-Hant.md b/README.zh-Hant.md deleted file mode 100644 index 87c0a8dd..00000000 --- a/README.zh-Hant.md +++ /dev/null @@ -1,702 +0,0 @@ -_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`) 的壓縮包: - - - [README](README.md#windows) - -[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-2022 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 3a66b5fd01f23ea24c1c540267fdd86a68a958a8 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 17 Aug 2022 00:15:25 +0200 Subject: [PATCH 1228/2244] Remove deprecated meson.source_root() This method is deprecated since Meson 0.56.0: We could replace it with meson.project_source_root(), but this would make Meson 0.56 or above mandatory. Since the path in always computed from the server/ directory, just add '..' to reference the root project directory. Refs c456e3826470753bdaefcffc93e9ae1bf25b12f7 --- server/meson.build | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/meson.build b/server/meson.build index 984daf3b..42b97981 100644 --- a/server/meson.build +++ b/server/meson.build @@ -13,8 +13,8 @@ if prebuilt_server == '' install_dir: 'share/scrcpy') else if not prebuilt_server.startswith('/') - # relative path needs some trick - prebuilt_server = meson.source_root() + '/' + prebuilt_server + # prebuilt server path is relative to the root scrcpy directory + prebuilt_server = '../' + prebuilt_server endif custom_target('scrcpy-server-prebuilt', input: prebuilt_server, From 5b8e9aa0e95dd5e4aa234dfcbd0445005d197bf0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 3 Aug 2022 14:51:07 +0200 Subject: [PATCH 1229/2244] Move toUnsigned() to a Binary util class PR #3369 --- .../java/com/genymobile/scrcpy/Binary.java | 15 +++++++++++++ .../scrcpy/ControlMessageReader.java | 22 ++++++------------- 2 files changed, 22 insertions(+), 15 deletions(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/Binary.java diff --git a/server/src/main/java/com/genymobile/scrcpy/Binary.java b/server/src/main/java/com/genymobile/scrcpy/Binary.java new file mode 100644 index 00000000..1a096de9 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/Binary.java @@ -0,0 +1,15 @@ +package com.genymobile.scrcpy; + +public final class Binary { + private Binary() { + // not instantiable + } + + public static int toUnsigned(short value) { + return value & 0xffff; + } + + public static int toUnsigned(byte value) { + return value & 0xff; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java index 24dc5e50..07ef1f7c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java @@ -103,7 +103,7 @@ public class ControlMessageReader { if (buffer.remaining() < INJECT_KEYCODE_PAYLOAD_LENGTH) { return null; } - int action = toUnsigned(buffer.get()); + int action = Binary.toUnsigned(buffer.get()); int keycode = buffer.getInt(); int repeat = buffer.getInt(); int metaState = buffer.getInt(); @@ -136,11 +136,11 @@ public class ControlMessageReader { if (buffer.remaining() < INJECT_TOUCH_EVENT_PAYLOAD_LENGTH) { return null; } - int action = toUnsigned(buffer.get()); + int action = Binary.toUnsigned(buffer.get()); long pointerId = buffer.getLong(); Position position = readPosition(buffer); // 16 bits fixed-point - int pressureInt = toUnsigned(buffer.getShort()); + int pressureInt = Binary.toUnsigned(buffer.getShort()); // convert it to a float between 0 and 1 (0x1p16f is 2^16 as float) float pressure = pressureInt == 0xffff ? 1f : (pressureInt / 0x1p16f); int buttons = buffer.getInt(); @@ -162,7 +162,7 @@ public class ControlMessageReader { if (buffer.remaining() < BACK_OR_SCREEN_ON_LENGTH) { return null; } - int action = toUnsigned(buffer.get()); + int action = Binary.toUnsigned(buffer.get()); return ControlMessage.createBackOrScreenOn(action); } @@ -170,7 +170,7 @@ public class ControlMessageReader { if (buffer.remaining() < GET_CLIPBOARD_LENGTH) { return null; } - int copyKey = toUnsigned(buffer.get()); + int copyKey = Binary.toUnsigned(buffer.get()); return ControlMessage.createGetClipboard(copyKey); } @@ -198,16 +198,8 @@ public class ControlMessageReader { private static Position readPosition(ByteBuffer buffer) { int x = buffer.getInt(); int y = buffer.getInt(); - int screenWidth = toUnsigned(buffer.getShort()); - int screenHeight = toUnsigned(buffer.getShort()); + int screenWidth = Binary.toUnsigned(buffer.getShort()); + int screenHeight = Binary.toUnsigned(buffer.getShort()); return new Position(x, y, screenWidth, screenHeight); } - - private static int toUnsigned(short value) { - return value & 0xffff; - } - - private static int toUnsigned(byte value) { - return value & 0xff; - } } From 3848ce86f118496467ebd91b40dbf571e2a29e24 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 24 Jul 2022 16:00:26 +0200 Subject: [PATCH 1230/2244] Extract conversion from u16 fixed-point to float PR #3369 --- .../src/main/java/com/genymobile/scrcpy/Binary.java | 12 ++++++++++++ .../com/genymobile/scrcpy/ControlMessageReader.java | 5 +---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Binary.java b/server/src/main/java/com/genymobile/scrcpy/Binary.java index 1a096de9..db946664 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Binary.java +++ b/server/src/main/java/com/genymobile/scrcpy/Binary.java @@ -12,4 +12,16 @@ public final class Binary { public static int toUnsigned(byte value) { return value & 0xff; } + + /** + * Convert unsigned 16-bit fixed-point to a float between 0 and 1 + * + * @param value encoded value + * @return Float value between 0 and 1 + */ + public static float u16FixedPointToFloat(short value) { + int unsignedShort = Binary.toUnsigned(value); + // 0x1p16f is 2^16 as float + return unsignedShort == 0xffff ? 1f : (unsignedShort / 0x1p16f); + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java index 07ef1f7c..b2d500c2 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java @@ -139,10 +139,7 @@ public class ControlMessageReader { int action = Binary.toUnsigned(buffer.get()); long pointerId = buffer.getLong(); Position position = readPosition(buffer); - // 16 bits fixed-point - int pressureInt = Binary.toUnsigned(buffer.getShort()); - // convert it to a float between 0 and 1 (0x1p16f is 2^16 as float) - float pressure = pressureInt == 0xffff ? 1f : (pressureInt / 0x1p16f); + float pressure = Binary.u16FixedPointToFloat(buffer.getShort()); int buttons = buffer.getInt(); return ControlMessage.createInjectTouchEvent(action, pointerId, position, pressure, buttons); } From 136ab8c19902e1b41915dabb23e36945e5b6e394 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 3 Aug 2022 14:55:35 +0200 Subject: [PATCH 1231/2244] Add unit test for float decoding PR #3369 --- .../com/genymobile/scrcpy/BinaryTest.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 server/src/test/java/com/genymobile/scrcpy/BinaryTest.java diff --git a/server/src/test/java/com/genymobile/scrcpy/BinaryTest.java b/server/src/test/java/com/genymobile/scrcpy/BinaryTest.java new file mode 100644 index 00000000..b2d3d72d --- /dev/null +++ b/server/src/test/java/com/genymobile/scrcpy/BinaryTest.java @@ -0,0 +1,20 @@ +package com.genymobile.scrcpy; + +import org.junit.Assert; +import org.junit.Test; + +public class BinaryTest { + + @Test + public void testU16FixedPointToFloat() { + final float delta = 0.0f; // on these values, there MUST be no rounding error + Assert.assertEquals(0.0f, Binary.u16FixedPointToFloat((short) 0), delta); + Assert.assertEquals(0.03125f, Binary.u16FixedPointToFloat((short) 0x800), delta); + Assert.assertEquals(0.0625f, Binary.u16FixedPointToFloat((short) 0x1000), delta); + Assert.assertEquals(0.125f, Binary.u16FixedPointToFloat((short) 0x2000), delta); + Assert.assertEquals(0.25f, Binary.u16FixedPointToFloat((short) 0x4000), delta); + Assert.assertEquals(0.5f, Binary.u16FixedPointToFloat((short) 0x8000), delta); + Assert.assertEquals(0.75f, Binary.u16FixedPointToFloat((short) 0xc000), delta); + Assert.assertEquals(1.0f, Binary.u16FixedPointToFloat((short) 0xffff), delta); + } +} From 041cdf6cf5a5afa0d7794c89c5e477c67b31af24 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 3 Aug 2022 15:13:16 +0200 Subject: [PATCH 1232/2244] Rename buffer_util.h to binary.h It will allow to expose more binary util functions not related to buffers. PR #3369 --- app/meson.build | 4 +-- app/src/control_msg.c | 2 +- app/src/demuxer.c | 2 +- app/src/device_msg.c | 2 +- app/src/util/{buffer_util.h => binary.h} | 4 +-- .../{test_buffer_util.c => test_binary.c} | 26 +++++++++---------- 6 files changed, 20 insertions(+), 20 deletions(-) rename app/src/util/{buffer_util.h => binary.h} (94%) rename app/tests/{test_buffer_util.c => test_binary.c} (72%) diff --git a/app/meson.build b/app/meson.build index f5d76c61..e41b02ba 100644 --- a/app/meson.build +++ b/app/meson.build @@ -245,8 +245,8 @@ if get_option('buildtype') == 'debug' 'src/util/str.c', 'src/util/strbuf.c', ]], - ['test_buffer_util', [ - 'tests/test_buffer_util.c', + ['test_binary', [ + 'tests/test_binary.c', ]], ['test_cbuf', [ 'tests/test_cbuf.c', diff --git a/app/src/control_msg.c b/app/src/control_msg.c index 3c398016..0d68c45c 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -5,7 +5,7 @@ #include #include -#include "util/buffer_util.h" +#include "util/binary.h" #include "util/log.h" #include "util/str.h" diff --git a/app/src/demuxer.c b/app/src/demuxer.c index 9412eda7..2c0c64a8 100644 --- a/app/src/demuxer.c +++ b/app/src/demuxer.c @@ -7,7 +7,7 @@ #include "decoder.h" #include "events.h" #include "recorder.h" -#include "util/buffer_util.h" +#include "util/binary.h" #include "util/log.h" #define SC_PACKET_HEADER_SIZE 12 diff --git a/app/src/device_msg.c b/app/src/device_msg.c index 4b4a4974..265c7505 100644 --- a/app/src/device_msg.c +++ b/app/src/device_msg.c @@ -4,7 +4,7 @@ #include #include -#include "util/buffer_util.h" +#include "util/binary.h" #include "util/log.h" ssize_t diff --git a/app/src/util/buffer_util.h b/app/src/util/binary.h similarity index 94% rename from app/src/util/buffer_util.h rename to app/src/util/binary.h index a5456abf..e77f5e82 100644 --- a/app/src/util/buffer_util.h +++ b/app/src/util/binary.h @@ -1,5 +1,5 @@ -#ifndef SC_BUFFER_UTIL_H -#define SC_BUFFER_UTIL_H +#ifndef SC_BINARY_H +#define SC_BINARY_H #include "common.h" diff --git a/app/tests/test_buffer_util.c b/app/tests/test_binary.c similarity index 72% rename from app/tests/test_buffer_util.c rename to app/tests/test_binary.c index a9fec896..93aeccc8 100644 --- a/app/tests/test_buffer_util.c +++ b/app/tests/test_binary.c @@ -2,9 +2,9 @@ #include -#include "util/buffer_util.h" +#include "util/binary.h" -static void test_buffer_write16be(void) { +static void test_write16be(void) { uint16_t val = 0xABCD; uint8_t buf[2]; @@ -14,7 +14,7 @@ static void test_buffer_write16be(void) { assert(buf[1] == 0xCD); } -static void test_buffer_write32be(void) { +static void test_write32be(void) { uint32_t val = 0xABCD1234; uint8_t buf[4]; @@ -26,7 +26,7 @@ static void test_buffer_write32be(void) { assert(buf[3] == 0x34); } -static void test_buffer_write64be(void) { +static void test_write64be(void) { uint64_t val = 0xABCD1234567890EF; uint8_t buf[8]; @@ -42,7 +42,7 @@ static void test_buffer_write64be(void) { assert(buf[7] == 0xEF); } -static void test_buffer_read16be(void) { +static void test_read16be(void) { uint8_t buf[2] = {0xAB, 0xCD}; uint16_t val = sc_read16be(buf); @@ -50,7 +50,7 @@ static void test_buffer_read16be(void) { assert(val == 0xABCD); } -static void test_buffer_read32be(void) { +static void test_read32be(void) { uint8_t buf[4] = {0xAB, 0xCD, 0x12, 0x34}; uint32_t val = sc_read32be(buf); @@ -58,7 +58,7 @@ static void test_buffer_read32be(void) { assert(val == 0xABCD1234); } -static void test_buffer_read64be(void) { +static void test_read64be(void) { uint8_t buf[8] = {0xAB, 0xCD, 0x12, 0x34, 0x56, 0x78, 0x90, 0xEF}; @@ -71,11 +71,11 @@ int main(int argc, char *argv[]) { (void) argc; (void) argv; - test_buffer_write16be(); - test_buffer_write32be(); - test_buffer_write64be(); - test_buffer_read16be(); - test_buffer_read32be(); - test_buffer_read64be(); + test_write16be(); + test_write32be(); + test_write64be(); + test_read16be(); + test_read32be(); + test_read64be(); return 0; } From fd3483c83730a0a59f7b731e9cceabacb759ab20 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 3 Aug 2022 15:17:44 +0200 Subject: [PATCH 1233/2244] Extract conversion from float to u16 fixed-point PR #3369 --- app/src/control_msg.c | 12 +----------- app/src/util/binary.h | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/app/src/control_msg.c b/app/src/control_msg.c index 0d68c45c..4aa0944c 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -78,16 +78,6 @@ write_string(const char *utf8, size_t max_len, unsigned char *buf) { return 4 + len; } -static uint16_t -to_fixed_point_16(float f) { - assert(f >= 0.0f && f <= 1.0f); - uint32_t u = f * 0x1p16f; // 2^16 - if (u >= 0xffff) { - u = 0xffff; - } - return (uint16_t) u; -} - size_t sc_control_msg_serialize(const struct sc_control_msg *msg, unsigned char *buf) { buf[0] = msg->type; @@ -109,7 +99,7 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, unsigned char *buf) { sc_write64be(&buf[2], msg->inject_touch_event.pointer_id); write_position(&buf[10], &msg->inject_touch_event.position); uint16_t pressure = - to_fixed_point_16(msg->inject_touch_event.pressure); + sc_float_to_u16fp(msg->inject_touch_event.pressure); sc_write16be(&buf[22], pressure); sc_write32be(&buf[24], msg->inject_touch_event.buttons); return 28; diff --git a/app/src/util/binary.h b/app/src/util/binary.h index e77f5e82..734ab1d0 100644 --- a/app/src/util/binary.h +++ b/app/src/util/binary.h @@ -3,6 +3,7 @@ #include "common.h" +#include #include #include @@ -43,4 +44,18 @@ sc_read64be(const uint8_t *buf) { return ((uint64_t) msb << 32) | lsb; } +/** + * Convert a float between 0 and 1 to an unsigned 16-bit fixed-point value + */ +static inline uint16_t +sc_float_to_u16fp(float f) { + assert(f >= 0.0f && f <= 1.0f); + uint32_t u = f * 0x1p16f; // 2^16 + if (u >= 0xffff) { + assert(u == 0x10000); // for f == 1.0f + u = 0xffff; + } + return (uint16_t) u; +} + #endif From 1ab6c19486b43c55a14c0a309f34cd081bfab85d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 3 Aug 2022 15:23:39 +0200 Subject: [PATCH 1234/2244] Add unit test for float encoding PR #3369 --- app/tests/test_binary.c | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/app/tests/test_binary.c b/app/tests/test_binary.c index 93aeccc8..6e52da72 100644 --- a/app/tests/test_binary.c +++ b/app/tests/test_binary.c @@ -67,6 +67,17 @@ static void test_read64be(void) { assert(val == 0xABCD1234567890EF); } +static void test_float_to_u16fp(void) { + assert(sc_float_to_u16fp(0.0f) == 0); + assert(sc_float_to_u16fp(0.03125f) == 0x800); + assert(sc_float_to_u16fp(0.0625f) == 0x1000); + assert(sc_float_to_u16fp(0.125f) == 0x2000); + assert(sc_float_to_u16fp(0.25f) == 0x4000); + assert(sc_float_to_u16fp(0.5f) == 0x8000); + assert(sc_float_to_u16fp(0.75f) == 0xc000); + assert(sc_float_to_u16fp(1.0f) == 0xffff); +} + int main(int argc, char *argv[]) { (void) argc; (void) argv; @@ -77,5 +88,7 @@ int main(int argc, char *argv[]) { test_read16be(); test_read32be(); test_read64be(); + + test_float_to_u16fp(); return 0; } From 1f138aef41de651668043b32c4effc2d4adbfc44 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 3 Aug 2022 13:04:15 +0200 Subject: [PATCH 1235/2244] Add conversion from float to fixed-point i16 To encode float values between -1 and 1. PR #3369 --- app/src/util/binary.h | 15 +++++++++++++ app/tests/test_binary.c | 20 +++++++++++++++++ .../java/com/genymobile/scrcpy/Binary.java | 11 ++++++++++ .../com/genymobile/scrcpy/BinaryTest.java | 22 +++++++++++++++++++ 4 files changed, 68 insertions(+) diff --git a/app/src/util/binary.h b/app/src/util/binary.h index 734ab1d0..6dc1b58e 100644 --- a/app/src/util/binary.h +++ b/app/src/util/binary.h @@ -58,4 +58,19 @@ sc_float_to_u16fp(float f) { return (uint16_t) u; } +/** + * Convert a float between -1 and 1 to a signed 16-bit fixed-point value + */ +static inline int16_t +sc_float_to_i16fp(float f) { + assert(f >= -1.0f && f <= 1.0f); + int32_t i = f * 0x1p15f; // 2^15 + assert(i >= -0x8000); + if (i >= 0x7fff) { + assert(i == 0x8000); // for f == 1.0f + i = 0x7fff; + } + return (int16_t) i; +} + #endif diff --git a/app/tests/test_binary.c b/app/tests/test_binary.c index 6e52da72..82a9c1e0 100644 --- a/app/tests/test_binary.c +++ b/app/tests/test_binary.c @@ -78,6 +78,25 @@ static void test_float_to_u16fp(void) { assert(sc_float_to_u16fp(1.0f) == 0xffff); } +static void test_float_to_i16fp(void) { + assert(sc_float_to_i16fp(0.0f) == 0); + assert(sc_float_to_i16fp(0.03125f) == 0x400); + assert(sc_float_to_i16fp(0.0625f) == 0x800); + assert(sc_float_to_i16fp(0.125f) == 0x1000); + assert(sc_float_to_i16fp(0.25f) == 0x2000); + assert(sc_float_to_i16fp(0.5f) == 0x4000); + assert(sc_float_to_i16fp(0.75f) == 0x6000); + assert(sc_float_to_i16fp(1.0f) == 0x7fff); + + assert(sc_float_to_i16fp(-0.03125f) == -0x400); + assert(sc_float_to_i16fp(-0.0625f) == -0x800); + assert(sc_float_to_i16fp(-0.125f) == -0x1000); + assert(sc_float_to_i16fp(-0.25f) == -0x2000); + assert(sc_float_to_i16fp(-0.5f) == -0x4000); + assert(sc_float_to_i16fp(-0.75f) == -0x6000); + assert(sc_float_to_i16fp(-1.0f) == -0x8000); +} + int main(int argc, char *argv[]) { (void) argc; (void) argv; @@ -90,5 +109,6 @@ int main(int argc, char *argv[]) { test_read64be(); test_float_to_u16fp(); + test_float_to_i16fp(); return 0; } diff --git a/server/src/main/java/com/genymobile/scrcpy/Binary.java b/server/src/main/java/com/genymobile/scrcpy/Binary.java index db946664..29534f59 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Binary.java +++ b/server/src/main/java/com/genymobile/scrcpy/Binary.java @@ -24,4 +24,15 @@ public final class Binary { // 0x1p16f is 2^16 as float return unsignedShort == 0xffff ? 1f : (unsignedShort / 0x1p16f); } + + /** + * Convert signed 16-bit fixed-point to a float between -1 and 1 + * + * @param value encoded value + * @return Float value between -1 and 1 + */ + public static float i16FixedPointToFloat(short value) { + // 0x1p15f is 2^15 as float + return value == 0x7fff ? 1f : (value / 0x1p15f); + } } diff --git a/server/src/test/java/com/genymobile/scrcpy/BinaryTest.java b/server/src/test/java/com/genymobile/scrcpy/BinaryTest.java index b2d3d72d..569a2f2c 100644 --- a/server/src/test/java/com/genymobile/scrcpy/BinaryTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/BinaryTest.java @@ -17,4 +17,26 @@ public class BinaryTest { Assert.assertEquals(0.75f, Binary.u16FixedPointToFloat((short) 0xc000), delta); Assert.assertEquals(1.0f, Binary.u16FixedPointToFloat((short) 0xffff), delta); } + + @Test + public void testI16FixedPointToFloat() { + final float delta = 0.0f; // on these values, there MUST be no rounding error + + Assert.assertEquals(0.0f, Binary.i16FixedPointToFloat((short) 0), delta); + Assert.assertEquals(0.03125f, Binary.i16FixedPointToFloat((short) 0x400), delta); + Assert.assertEquals(0.0625f, Binary.i16FixedPointToFloat((short) 0x800), delta); + Assert.assertEquals(0.125f, Binary.i16FixedPointToFloat((short) 0x1000), delta); + Assert.assertEquals(0.25f, Binary.i16FixedPointToFloat((short) 0x2000), delta); + Assert.assertEquals(0.5f, Binary.i16FixedPointToFloat((short) 0x4000), delta); + Assert.assertEquals(0.75f, Binary.i16FixedPointToFloat((short) 0x6000), delta); + Assert.assertEquals(1.0f, Binary.i16FixedPointToFloat((short) 0x7fff), delta); + + Assert.assertEquals(-0.03125f, Binary.i16FixedPointToFloat((short) -0x400), delta); + Assert.assertEquals(-0.0625f, Binary.i16FixedPointToFloat((short) -0x800), delta); + Assert.assertEquals(-0.125f, Binary.i16FixedPointToFloat((short) -0x1000), delta); + Assert.assertEquals(-0.25f, Binary.i16FixedPointToFloat((short) -0x2000), delta); + Assert.assertEquals(-0.5f, Binary.i16FixedPointToFloat((short) -0x4000), delta); + Assert.assertEquals(-0.75f, Binary.i16FixedPointToFloat((short) -0x6000), delta); + Assert.assertEquals(-1.0f, Binary.i16FixedPointToFloat((short) -0x8000), delta); + } } From 57056d078ddcac73fe10fcb41395ed21f3d486b6 Mon Sep 17 00:00:00 2001 From: Simon Chan <1330321+yume-chan@users.noreply.github.com> Date: Sun, 3 Jul 2022 07:02:17 +0000 Subject: [PATCH 1236/2244] Use precise scrolling values Since SDL 2.0.18, the amount scrolled horizontally or vertically is exposed as a float (between 0 and 1). Forward a precise value to the Android device when possible. Refs Fixes #3363 PR #3369 Signed-off-by: Romain Vimont --- app/src/control_msg.c | 18 ++++++++++-------- app/src/control_msg.h | 4 ++-- app/src/input_events.h | 4 ++-- app/src/input_manager.c | 9 +++++++-- app/tests/test_control_msg_serialize.c | 6 +++--- .../com/genymobile/scrcpy/ControlMessage.java | 10 +++++----- .../scrcpy/ControlMessageReader.java | 6 +++--- .../java/com/genymobile/scrcpy/Controller.java | 2 +- .../scrcpy/ControlMessageReaderTest.java | 8 ++++---- 9 files changed, 37 insertions(+), 30 deletions(-) diff --git a/app/src/control_msg.c b/app/src/control_msg.c index 4aa0944c..3513abc7 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -105,12 +105,14 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, unsigned char *buf) { return 28; case SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT: write_position(&buf[1], &msg->inject_scroll_event.position); - sc_write32be(&buf[13], - (uint32_t) msg->inject_scroll_event.hscroll); - sc_write32be(&buf[17], - (uint32_t) msg->inject_scroll_event.vscroll); - sc_write32be(&buf[21], msg->inject_scroll_event.buttons); - return 25; + int16_t hscroll = + sc_float_to_i16fp(msg->inject_scroll_event.hscroll); + int16_t vscroll = + sc_float_to_i16fp(msg->inject_scroll_event.vscroll); + sc_write16be(&buf[13], (uint16_t) hscroll); + sc_write16be(&buf[15], (uint16_t) vscroll); + sc_write32be(&buf[17], msg->inject_scroll_event.buttons); + return 21; case SC_CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON: buf[1] = msg->inject_keycode.action; return 2; @@ -181,8 +183,8 @@ sc_control_msg_log(const struct sc_control_msg *msg) { break; } case SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT: - LOG_CMSG("scroll position=%" PRIi32 ",%" PRIi32 " hscroll=%" PRIi32 - " vscroll=%" PRIi32 " buttons=%06lx", + LOG_CMSG("scroll position=%" PRIi32 ",%" PRIi32 " hscroll=%f" + " vscroll=%f buttons=%06lx", msg->inject_scroll_event.position.point.x, msg->inject_scroll_event.position.point.y, msg->inject_scroll_event.hscroll, diff --git a/app/src/control_msg.h b/app/src/control_msg.h index 1463fddc..f51bdecd 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -68,8 +68,8 @@ struct sc_control_msg { } inject_touch_event; struct { struct sc_position position; - int32_t hscroll; - int32_t vscroll; + float hscroll; + float vscroll; enum android_motionevent_buttons buttons; } inject_scroll_event; struct { diff --git a/app/src/input_events.h b/app/src/input_events.h index 9bf3c421..15d22910 100644 --- a/app/src/input_events.h +++ b/app/src/input_events.h @@ -358,8 +358,8 @@ struct sc_mouse_click_event { struct sc_mouse_scroll_event { struct sc_position position; - int32_t hscroll; - int32_t vscroll; + float hscroll; + float vscroll; uint8_t buttons_state; // bitwise-OR of sc_mouse_button values }; diff --git a/app/src/input_manager.c b/app/src/input_manager.c index bba3665c..42b49a13 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -747,8 +747,13 @@ sc_input_manager_process_mouse_wheel(struct sc_input_manager *im, .point = sc_screen_convert_window_to_frame_coords(im->screen, mouse_x, mouse_y), }, - .hscroll = event->x, - .vscroll = event->y, +#if SDL_VERSION_ATLEAST(2, 0, 18) + .hscroll = CLAMP(event->preciseX, -1.0f, 1.0f), + .vscroll = CLAMP(event->preciseY, -1.0f, 1.0f), +#else + .hscroll = CLAMP(event->x, -1, 1), + .vscroll = CLAMP(event->y, -1, 1), +#endif .buttons_state = sc_mouse_buttons_state_from_sdl(buttons, im->forward_all_clicks), }; diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c index 72e3d87a..87117e3a 100644 --- a/app/tests/test_control_msg_serialize.c +++ b/app/tests/test_control_msg_serialize.c @@ -132,14 +132,14 @@ static void test_serialize_inject_scroll_event(void) { unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; size_t size = sc_control_msg_serialize(&msg, buf); - assert(size == 25); + assert(size == 21); const unsigned char expected[] = { SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT, 0x00, 0x00, 0x01, 0x04, 0x00, 0x00, 0x04, 0x02, // 260 1026 0x04, 0x38, 0x07, 0x80, // 1080 1920 - 0x00, 0x00, 0x00, 0x01, // 1 - 0xFF, 0xFF, 0xFF, 0xFF, // -1 + 0x7F, 0xFF, // 1 (float encoded as i16) + 0x80, 0x00, // -1 (float encoded as i16) 0x00, 0x00, 0x00, 0x01, // 1 }; 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 99eb805f..0b05b22a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java @@ -33,8 +33,8 @@ public final class ControlMessage { private long pointerId; private float pressure; private Position position; - private int hScroll; - private int vScroll; + private float hScroll; + private float vScroll; private int copyKey; private boolean paste; private int repeat; @@ -71,7 +71,7 @@ public final class ControlMessage { return msg; } - public static ControlMessage createInjectScrollEvent(Position position, int hScroll, int vScroll, int buttons) { + public static ControlMessage createInjectScrollEvent(Position position, float hScroll, float vScroll, int buttons) { ControlMessage msg = new ControlMessage(); msg.type = TYPE_INJECT_SCROLL_EVENT; msg.position = position; @@ -156,11 +156,11 @@ public final class ControlMessage { return position; } - public int getHScroll() { + public float getHScroll() { return hScroll; } - public int getVScroll() { + public float getVScroll() { return vScroll; } diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java index b2d500c2..a52fd134 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java @@ -10,7 +10,7 @@ public class ControlMessageReader { 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 = 24; + static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20; static final int BACK_OR_SCREEN_ON_LENGTH = 1; static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1; static final int GET_CLIPBOARD_LENGTH = 1; @@ -149,8 +149,8 @@ public class ControlMessageReader { return null; } Position position = readPosition(buffer); - int hScroll = buffer.getInt(); - int vScroll = buffer.getInt(); + float hScroll = Binary.i16FixedPointToFloat(buffer.getShort()); + float vScroll = Binary.i16FixedPointToFloat(buffer.getShort()); int buttons = buffer.getInt(); return ControlMessage.createInjectScrollEvent(position, hScroll, vScroll, buttons); } diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index 913371ee..95b64711 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -223,7 +223,7 @@ public class Controller { return device.injectEvent(event, Device.INJECT_MODE_ASYNC); } - private boolean injectScroll(Position position, int hScroll, int vScroll, int buttons) { + private boolean injectScroll(Position position, float hScroll, float vScroll, int buttons) { long now = SystemClock.uptimeMillis(); Point point = device.getPhysicalPoint(position); if (point == null) { diff --git a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java index 2a4ffe75..1fc009ce 100644 --- a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java @@ -126,8 +126,8 @@ public class ControlMessageReaderTest { dos.writeInt(1026); dos.writeShort(1080); dos.writeShort(1920); - dos.writeInt(1); - dos.writeInt(-1); + dos.writeShort(0); // 0.0f encoded as i16 + dos.writeShort(0x8000); // -1.0f encoded as i16 dos.writeInt(1); byte[] packet = bos.toByteArray(); @@ -143,8 +143,8 @@ public class ControlMessageReaderTest { Assert.assertEquals(1026, event.getPosition().getPoint().getY()); Assert.assertEquals(1080, event.getPosition().getScreenSize().getWidth()); Assert.assertEquals(1920, event.getPosition().getScreenSize().getHeight()); - Assert.assertEquals(1, event.getHScroll()); - Assert.assertEquals(-1, event.getVScroll()); + Assert.assertEquals(0f, event.getHScroll(), 0f); + Assert.assertEquals(-1f, event.getVScroll(), 0f); Assert.assertEquals(1, event.getButtons()); } From 121bb71dfe15bbb8797acf47f02f60a043817762 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 2 Sep 2022 14:31:15 +0200 Subject: [PATCH 1237/2244] Move from jcenter() to mavenCentral() Refs Refs --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index c7d31ef7..2f76a75a 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ buildscript { repositories { google() - jcenter() + mavenCentral() } dependencies { classpath 'com.android.tools.build:gradle:7.0.3' @@ -17,7 +17,7 @@ buildscript { allprojects { repositories { google() - jcenter() + mavenCentral() } tasks.withType(JavaCompile) { options.compilerArgs << "-Xlint:deprecation" From fccfc43b9ed702480dcf89cc446b35f90b673b18 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 2 Sep 2022 14:35:05 +0200 Subject: [PATCH 1238/2244] Upgrade gradle build tools to 7.2.2 Plugin version 7.2.2. Gradle version 7.3.3. Refs: --- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 2f76a75a..ecc7972e 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.0.3' + classpath 'com.android.tools.build:gradle:7.2.2' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 29e41345..669386b8 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-7.0.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 0a0a446ea6d790420c3d52eb1e6d4afebd086b35 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 2 Sep 2022 14:41:23 +0200 Subject: [PATCH 1239/2244] Upgrade Android SDK to 33 --- server/build.gradle | 4 ++-- server/build_without_gradle.sh | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/server/build.gradle b/server/build.gradle index 9725089e..00590381 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -1,11 +1,11 @@ apply plugin: 'com.android.application' android { - compileSdkVersion 31 + compileSdkVersion 33 defaultConfig { applicationId "com.genymobile.scrcpy" minSdkVersion 21 - targetSdkVersion 31 + targetSdkVersion 33 versionCode 12400 versionName "1.24" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index c881e38a..1444e72c 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -14,8 +14,8 @@ set -e SCRCPY_DEBUG=false SCRCPY_VERSION_NAME=1.24 -PLATFORM=${ANDROID_PLATFORM:-31} -BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-31.0.0} +PLATFORM=${ANDROID_PLATFORM:-33} +BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-33.0.0} BUILD_DIR="$(realpath ${BUILD_DIR:-build_manual})" CLASSES_DIR="$BUILD_DIR/classes" From c1ec1d1023e9056195a386200fedf5f038c3ea6f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 27 Jun 2022 12:15:37 +0200 Subject: [PATCH 1240/2244] Replace hardcoded 'share/' by datadir variable Meson defines a variable for the data directory. PR #3351 --- app/meson.build | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/meson.build b/app/meson.build index e41b02ba..e57a8428 100644 --- a/app/meson.build +++ b/app/meson.build @@ -223,14 +223,17 @@ executable('scrcpy', src, install: true, c_args: []) +# +datadir = get_option('datadir') # by default 'share' + install_man('scrcpy.1') install_data('data/icon.png', rename: 'scrcpy.png', - install_dir: 'share/icons/hicolor/256x256/apps') + install_dir: join_paths(datadir, 'icons/hicolor/256x256/apps')) install_data('data/zsh-completion/_scrcpy', - install_dir: 'share/zsh/site-functions') + install_dir: join_paths(datadir, 'zsh/site-functions')) install_data('data/bash-completion/scrcpy', - install_dir: 'share/bash-completion/completions') + install_dir: join_paths(datadir, 'bash-completion/completions')) ### TESTS From 51a1762cbd3fc522bdb81afe58962277b8af8a04 Mon Sep 17 00:00:00 2001 From: Addison Snelling Date: Sun, 14 Oct 2018 03:50:35 -0500 Subject: [PATCH 1241/2244] Add desktop entry file for Linux app launchers Refs PR #3351 Replaces PR #296 Fixes #295 Fixes #748 Fixes #1636 Co-authored-by: Chih-Hsuan Yen Signed-off-by: Romain Vimont --- app/data/scrcpy.desktop | 10 ++++++++++ app/meson.build | 7 +++++++ 2 files changed, 17 insertions(+) create mode 100644 app/data/scrcpy.desktop diff --git a/app/data/scrcpy.desktop b/app/data/scrcpy.desktop new file mode 100644 index 00000000..5933272d --- /dev/null +++ b/app/data/scrcpy.desktop @@ -0,0 +1,10 @@ +[Desktop Entry] +Name=scrcpy +GenericName=Android Remote Control +Comment=Display and control your Android device +Exec=scrcpy +Icon=scrcpy +Terminal=false +Type=Application +Categories=Utility;RemoteAccess; +StartupNotify=false diff --git a/app/meson.build b/app/meson.build index e57a8428..0551011e 100644 --- a/app/meson.build +++ b/app/meson.build @@ -235,6 +235,13 @@ install_data('data/zsh-completion/_scrcpy', install_data('data/bash-completion/scrcpy', install_dir: join_paths(datadir, 'bash-completion/completions')) +# Desktop entry file for application launchers +if host_machine.system() == 'linux' + # Install a launcher (ex: /usr/local/share/applications/scrcpy.desktop) + install_data('data/scrcpy.desktop', + install_dir: join_paths(datadir, 'applications')) +endif + ### TESTS From a2a22f497f38daa35b5dc42b5e93f06f3b37a3cc Mon Sep 17 00:00:00 2001 From: Chih-Hsuan Yen Date: Sat, 11 Dec 2021 11:47:57 +0800 Subject: [PATCH 1242/2244] Use shell environment to execute launcher MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make Exec= compatible with $PATH configured in .bashrc or .zshrc… PR #3351 Refs #296 Signed-off-by: Romain Vimont --- app/data/scrcpy.desktop | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/data/scrcpy.desktop b/app/data/scrcpy.desktop index 5933272d..082b75e0 100644 --- a/app/data/scrcpy.desktop +++ b/app/data/scrcpy.desktop @@ -2,7 +2,10 @@ Name=scrcpy GenericName=Android Remote Control Comment=Display and control your Android device -Exec=scrcpy +# For some users, the PATH or ADB environment variables are set from the shell +# startup file, like .bashrc or .zshrc… Run an interactive shell to get +# environment correctly initialized. +Exec=/bin/sh -c '"$SHELL" -i -c scrcpy' Icon=scrcpy Terminal=false Type=Application From e5e210506f62ded9417b252d059ed79505e3b4eb Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 27 Jun 2022 13:32:40 +0200 Subject: [PATCH 1243/2244] Add scrcpy-console.desktop Add a launcher which opens a terminal, and keep it open in case of errors (so that the user has time to read error messages). The behavior is the same as scrcpy-console.bat on Windows. PR #3351 --- app/data/scrcpy-console.desktop | 13 +++++++++++++ app/meson.build | 2 ++ 2 files changed, 15 insertions(+) create mode 100644 app/data/scrcpy-console.desktop diff --git a/app/data/scrcpy-console.desktop b/app/data/scrcpy-console.desktop new file mode 100644 index 00000000..47a63ec9 --- /dev/null +++ b/app/data/scrcpy-console.desktop @@ -0,0 +1,13 @@ +[Desktop Entry] +Name=scrcpy (console) +GenericName=Android Remote Control +Comment=Display and control your Android device +# For some users, the PATH or ADB environment variables are set from the shell +# startup file, like .bashrc or .zshrc… Run an interactive shell to get +# environment correctly initialized. +Exec=/bin/bash --norc --noprofile -i -c '"$SHELL" -i -c scrcpy || read -p "Press any key to quit..."' +Icon=scrcpy +Terminal=true +Type=Application +Categories=Utility;RemoteAccess; +StartupNotify=false diff --git a/app/meson.build b/app/meson.build index 0551011e..fd5418e3 100644 --- a/app/meson.build +++ b/app/meson.build @@ -240,6 +240,8 @@ if host_machine.system() == 'linux' # Install a launcher (ex: /usr/local/share/applications/scrcpy.desktop) install_data('data/scrcpy.desktop', install_dir: join_paths(datadir, 'applications')) + install_data('data/scrcpy-console.desktop', + install_dir: join_paths(datadir, 'applications')) endif From 4a5cdcd3908b5bdce7e8528d9045553a6019faa6 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 10 Jun 2022 15:19:53 +0200 Subject: [PATCH 1244/2244] Extract $BUILD_TOOLS_DIR In the script to build without gradle, the build-tools full path is used at several places. Use a separate variable for readability. --- server/build_without_gradle.sh | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index 1444e72c..f4fcba10 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -16,6 +16,7 @@ SCRCPY_VERSION_NAME=1.24 PLATFORM=${ANDROID_PLATFORM:-33} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-33.0.0} +BUILD_TOOLS_DIR="$ANDROID_HOME/build-tools/$BUILD_TOOLS" BUILD_DIR="$(realpath ${BUILD_DIR:-build_manual})" CLASSES_DIR="$BUILD_DIR/classes" @@ -41,9 +42,8 @@ EOF 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" \ +"$BUILD_TOOLS_DIR/aidl" -o"$CLASSES_DIR" android/view/IRotationWatcher.aidl +"$BUILD_TOOLS_DIR/aidl" -o"$CLASSES_DIR" \ android/content/IOnPrimaryClipChangedListener.aidl echo "Compiling java sources..." @@ -59,8 +59,7 @@ cd "$CLASSES_DIR" if [[ $PLATFORM -lt 31 ]] then # use dx - "$ANDROID_HOME/build-tools/$BUILD_TOOLS/dx" --dex \ - --output "$BUILD_DIR/classes.dex" \ + "$BUILD_TOOLS_DIR/dx" --dex --output "$BUILD_DIR/classes.dex" \ android/view/*.class \ android/content/*.class \ com/genymobile/scrcpy/*.class \ @@ -72,7 +71,7 @@ then rm -rf classes.dex classes else # use d8 - "$ANDROID_HOME/build-tools/$BUILD_TOOLS/d8" --classpath "$ANDROID_JAR" \ + "$BUILD_TOOLS_DIR/d8" --classpath "$ANDROID_JAR" \ --output "$BUILD_DIR/classes.zip" \ android/view/*.class \ android/content/*.class \ From 00e9e69c2a198974d992d8bcbb97866f10da4e9f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 25 Sep 2022 15:39:55 +0200 Subject: [PATCH 1245/2244] Use /dev/null instead of closing fds Some adb commands do not like when stdin, stdout or stderr are closed (they hang forever). Open /dev/null for each. --- app/src/sys/unix/process.c | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/app/src/sys/unix/process.c b/app/src/sys/unix/process.c index cef227ed..8c4a53c3 100644 --- a/app/src/sys/unix/process.c +++ b/app/src/sys/unix/process.c @@ -92,8 +92,14 @@ sc_process_execute_p(const char *const argv[], sc_pid *pid, unsigned flags, close(in[0]); } close(in[1]); + } else { + int devnull = open("/dev/null", O_RDONLY | O_CREAT, 0666); + if (devnull != -1) { + dup2(devnull, STDIN_FILENO); + } else { + LOGE("Could not open /dev/null for stdin"); + } } - // Do not close stdin in the child process, this makes adb fail on Linux if (pout) { if (out[1] != STDOUT_FILENO) { @@ -102,8 +108,12 @@ sc_process_execute_p(const char *const argv[], sc_pid *pid, unsigned flags, } close(out[0]); } else if (!inherit_stdout) { - // Close stdout in the child process - close(STDOUT_FILENO); + int devnull = open("/dev/null", O_WRONLY | O_CREAT, 0666); + if (devnull != -1) { + dup2(devnull, STDOUT_FILENO); + } else { + LOGE("Could not open /dev/null for stdout"); + } } if (perr) { @@ -113,8 +123,12 @@ sc_process_execute_p(const char *const argv[], sc_pid *pid, unsigned flags, } close(err[0]); } else if (!inherit_stderr) { - // Close stderr in the child process - close(STDERR_FILENO); + int devnull = open("/dev/null", O_WRONLY | O_CREAT, 0666); + if (devnull != -1) { + dup2(devnull, STDERR_FILENO); + } else { + LOGE("Could not open /dev/null for stderr"); + } } close(internal[0]); From 949b64dff2efe23039fa4801fbc95ef46e208477 Mon Sep 17 00:00:00 2001 From: SeungHoon Han Date: Tue, 2 Aug 2022 19:39:50 +0900 Subject: [PATCH 1246/2244] Add fallback to get DisplayInfo PR #3416 Signed-off-by: Romain Vimont --- .../java/com/genymobile/scrcpy/Command.java | 10 + .../main/java/com/genymobile/scrcpy/IO.java | 11 ++ .../scrcpy/wrappers/DisplayManager.java | 60 +++++- .../genymobile/scrcpy/CommandParserTest.java | 186 ++++++++++++++++++ 4 files changed, 266 insertions(+), 1 deletion(-) create mode 100644 server/src/test/java/com/genymobile/scrcpy/CommandParserTest.java diff --git a/server/src/main/java/com/genymobile/scrcpy/Command.java b/server/src/main/java/com/genymobile/scrcpy/Command.java index 0ef976a6..362504ff 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Command.java +++ b/server/src/main/java/com/genymobile/scrcpy/Command.java @@ -30,4 +30,14 @@ public final class Command { } return result; } + + public static String execReadOutput(String... cmd) throws IOException, InterruptedException { + Process process = Runtime.getRuntime().exec(cmd); + String output = IO.toString(process.getInputStream()); + int exitCode = process.waitFor(); + if (exitCode != 0) { + throw new IOException("Command " + Arrays.toString(cmd) + " returned with value " + exitCode); + } + return output; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/IO.java b/server/src/main/java/com/genymobile/scrcpy/IO.java index 57c017db..6eaf0d6a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/IO.java +++ b/server/src/main/java/com/genymobile/scrcpy/IO.java @@ -6,7 +6,9 @@ import android.system.OsConstants; import java.io.FileDescriptor; import java.io.IOException; +import java.io.InputStream; import java.nio.ByteBuffer; +import java.util.Scanner; public final class IO { private IO() { @@ -37,4 +39,13 @@ public final class IO { public static void writeFully(FileDescriptor fd, byte[] buffer, int offset, int len) throws IOException { writeFully(fd, ByteBuffer.wrap(buffer, offset, len)); } + + public static String toString(InputStream inputStream) { + StringBuilder builder = new StringBuilder(); + Scanner scanner = new Scanner(inputStream); + while (scanner.hasNextLine()) { + builder.append(scanner.nextLine()).append('\n'); + } + return builder.toString(); + } } 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 3f4f897d..bf172126 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java @@ -1,8 +1,16 @@ package com.genymobile.scrcpy.wrappers; +import com.genymobile.scrcpy.Command; import com.genymobile.scrcpy.DisplayInfo; +import com.genymobile.scrcpy.Ln; import com.genymobile.scrcpy.Size; +import android.view.Display; + +import java.lang.reflect.Field; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + public final class DisplayManager { private final Object manager; // instance of hidden class android.hardware.display.DisplayManagerGlobal @@ -10,11 +18,61 @@ public final class DisplayManager { this.manager = manager; } + // public to call it from unit tests + public static DisplayInfo parseDisplayInfo(String dumpsysDisplayOutput, int displayId) { + Pattern regex = Pattern.compile( + "^ mOverrideDisplayInfo=DisplayInfo\\{\".*?\", displayId " + displayId + ".*?(, FLAG_.*)?, real ([0-9]+) x ([0-9]+).*?, " + + "rotation ([0-9]+).*?, layerStack ([0-9]+)", + Pattern.MULTILINE); + Matcher m = regex.matcher(dumpsysDisplayOutput); + if (!m.find()) { + return null; + } + int flags = parseDisplayFlags(m.group(1)); + int width = Integer.parseInt(m.group(2)); + int height = Integer.parseInt(m.group(3)); + int rotation = Integer.parseInt(m.group(4)); + int layerStack = Integer.parseInt(m.group(5)); + + return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags); + } + + private static DisplayInfo getDisplayInfoFromDumpsysDisplay(int displayId) { + try { + String dumpsysDisplayOutput = Command.execReadOutput("dumpsys", "display"); + return parseDisplayInfo(dumpsysDisplayOutput, displayId); + } catch (Exception e) { + Ln.e("Could not get display info from \"dumpsys display\" output", e); + return null; + } + } + + private static int parseDisplayFlags(String text) { + Pattern regex = Pattern.compile("FLAG_[A-Z_]+"); + if (text == null) { + return 0; + } + + int flags = 0; + Matcher m = regex.matcher(text); + while (m.find()) { + String flagString = m.group(); + try { + Field filed = Display.class.getDeclaredField(flagString); + flags |= filed.getInt(null); + } catch (NoSuchFieldException | IllegalAccessException e) { + // Silently ignore, some flags reported by "dumpsys display" are @TestApi + } + } + return flags; + } + public DisplayInfo getDisplayInfo(int displayId) { try { Object displayInfo = manager.getClass().getMethod("getDisplayInfo", int.class).invoke(manager, displayId); if (displayInfo == null) { - return null; + // fallback when displayInfo is null + return getDisplayInfoFromDumpsysDisplay(displayId); } Class cls = displayInfo.getClass(); // width and height already take the rotation into account diff --git a/server/src/test/java/com/genymobile/scrcpy/CommandParserTest.java b/server/src/test/java/com/genymobile/scrcpy/CommandParserTest.java new file mode 100644 index 00000000..e6f57bcb --- /dev/null +++ b/server/src/test/java/com/genymobile/scrcpy/CommandParserTest.java @@ -0,0 +1,186 @@ +package com.genymobile.scrcpy; + +import com.genymobile.scrcpy.wrappers.DisplayManager; + +import android.view.Display; +import org.junit.Assert; +import org.junit.Test; + +public class CommandParserTest { + @Test + public void testParseDisplayInfoFromDumpsysDisplay() { + /* @formatter:off */ + String partialOutput = "Logical Displays: size=1\n" + + " Display 0:\n" + + "mDisplayId=0\n" + + " mLayerStack=0\n" + + " mHasContent=true\n" + + " mDesiredDisplayModeSpecs={baseModeId=2 primaryRefreshRateRange=[90 90] appRequestRefreshRateRange=[90 90]}\n" + + " mRequestedColorMode=0\n" + + " mDisplayOffset=(0, 0)\n" + + " mDisplayScalingDisabled=false\n" + + " mPrimaryDisplayDevice=Built-in Screen\n" + + " mBaseDisplayInfo=DisplayInfo{\"Built-in Screen\", displayId 0, FLAG_SECURE, FLAG_SUPPORTS_PROTECTED_BUFFERS, FLAG_TRUSTED, " + + "real 1440 x 3120, largest app 1440 x 3120, smallest app 1440 x 3120, appVsyncOff 2000000, presDeadline 11111111, mode 2, " + + "defaultMode 1, modes [{id=1, width=1440, height=3120, fps=60.0}, {id=2, width=1440, height=3120, fps=90.0}, {id=3, width=1080, " + + "height=2340, fps=90.0}, {id=4, width=1080, height=2340, fps=60.0}], hdrCapabilities HdrCapabilities{mSupportedHdrTypes=[2, 3, 4], " + + "mMaxLuminance=540.0, mMaxAverageLuminance=270.1, mMinLuminance=0.2}, minimalPostProcessingSupported false, rotation 0, state OFF, " + + "type INTERNAL, uniqueId \"local:0\", app 1440 x 3120, density 600 (515.154 x 514.597) dpi, layerStack 0, colorMode 0, " + + "supportedColorModes [0, 7, 9], address {port=129, model=0}, deviceProductInfo DeviceProductInfo{name=, manufacturerPnpId=QCM, " + + "productId=1, modelYear=null, manufactureDate=ManufactureDate{week=27, year=2006}, relativeAddress=null}, removeMode 0}\n" + + " mOverrideDisplayInfo=DisplayInfo{\"Built-in Screen\", displayId 0, FLAG_SECURE, FLAG_SUPPORTS_PROTECTED_BUFFERS, " + + "FLAG_TRUSTED, real 1440 x 3120, largest app 3120 x 2983, smallest app 1440 x 1303, appVsyncOff 2000000, presDeadline 11111111, " + + "mode 2, defaultMode 1, modes [{id=1, width=1440, height=3120, fps=60.0}, {id=2, width=1440, height=3120, fps=90.0}, {id=3, " + + "width=1080, height=2340, fps=90.0}, {id=4, width=1080, height=2340, fps=60.0}], hdrCapabilities " + + "HdrCapabilities{mSupportedHdrTypes=[2, 3, 4], mMaxLuminance=540.0, mMaxAverageLuminance=270.1, mMinLuminance=0.2}, " + + "minimalPostProcessingSupported false, rotation 0, state ON, type INTERNAL, uniqueId \"local:0\", app 1440 x 3120, density 600 " + + "(515.154 x 514.597) dpi, layerStack 0, colorMode 0, supportedColorModes [0, 7, 9], address {port=129, model=0}, deviceProductInfo " + + "DeviceProductInfo{name=, manufacturerPnpId=QCM, productId=1, modelYear=null, manufactureDate=ManufactureDate{week=27, year=2006}, " + + "relativeAddress=null}, removeMode 0}\n" + + " mRequestedMinimalPostProcessing=false\n"; + DisplayInfo displayInfo = DisplayManager.parseDisplayInfo(partialOutput, 0); + Assert.assertNotNull(displayInfo); + Assert.assertEquals(0, displayInfo.getDisplayId()); + Assert.assertEquals(0, displayInfo.getRotation()); + Assert.assertEquals(0, displayInfo.getLayerStack()); + // FLAG_TRUSTED does not exist in Display (@TestApi), so it won't be reported + Assert.assertEquals(Display.FLAG_SECURE | Display.FLAG_SUPPORTS_PROTECTED_BUFFERS, displayInfo.getFlags()); + Assert.assertEquals(1440, displayInfo.getSize().getWidth()); + Assert.assertEquals(3120, displayInfo.getSize().getHeight()); + } + + @Test + public void testParseDisplayInfoFromDumpsysDisplayWithRotation() { + /* @formatter:off */ + String partialOutput = "Logical Displays: size=1\n" + + " Display 0:\n" + + "mDisplayId=0\n" + + " mLayerStack=0\n" + + " mHasContent=true\n" + + " mDesiredDisplayModeSpecs={baseModeId=2 primaryRefreshRateRange=[90 90] appRequestRefreshRateRange=[90 90]}\n" + + " mRequestedColorMode=0\n" + + " mDisplayOffset=(0, 0)\n" + + " mDisplayScalingDisabled=false\n" + + " mPrimaryDisplayDevice=Built-in Screen\n" + + " mBaseDisplayInfo=DisplayInfo{\"Built-in Screen\", displayId 0, FLAG_SECURE, FLAG_SUPPORTS_PROTECTED_BUFFERS, FLAG_TRUSTED, " + + "real 1440 x 3120, largest app 1440 x 3120, smallest app 1440 x 3120, appVsyncOff 2000000, presDeadline 11111111, mode 2, " + + "defaultMode 1, modes [{id=1, width=1440, height=3120, fps=60.0}, {id=2, width=1440, height=3120, fps=90.0}, {id=3, width=1080, " + + "height=2340, fps=90.0}, {id=4, width=1080, height=2340, fps=60.0}], hdrCapabilities HdrCapabilities{mSupportedHdrTypes=[2, 3, 4], " + + "mMaxLuminance=540.0, mMaxAverageLuminance=270.1, mMinLuminance=0.2}, minimalPostProcessingSupported false, rotation 0, state ON, " + + "type INTERNAL, uniqueId \"local:0\", app 1440 x 3120, density 600 (515.154 x 514.597) dpi, layerStack 0, colorMode 0, " + + "supportedColorModes [0, 7, 9], address {port=129, model=0}, deviceProductInfo DeviceProductInfo{name=, manufacturerPnpId=QCM, " + + "productId=1, modelYear=null, manufactureDate=ManufactureDate{week=27, year=2006}, relativeAddress=null}, removeMode 0}\n" + + " mOverrideDisplayInfo=DisplayInfo{\"Built-in Screen\", displayId 0, FLAG_SECURE, FLAG_SUPPORTS_PROTECTED_BUFFERS, " + + "FLAG_TRUSTED, real 3120 x 1440, largest app 3120 x 2983, smallest app 1440 x 1303, appVsyncOff 2000000, presDeadline 11111111, " + + "mode 2, defaultMode 1, modes [{id=1, width=1440, height=3120, fps=60.0}, {id=2, width=1440, height=3120, fps=90.0}, {id=3, " + + "width=1080, height=2340, fps=90.0}, {id=4, width=1080, height=2340, fps=60.0}], hdrCapabilities " + + "HdrCapabilities{mSupportedHdrTypes=[2, 3, 4], mMaxLuminance=540.0, mMaxAverageLuminance=270.1, mMinLuminance=0.2}, " + + "minimalPostProcessingSupported false, rotation 3, state ON, type INTERNAL, uniqueId \"local:0\", app 3120 x 1440, density 600 " + + "(515.154 x 514.597) dpi, layerStack 0, colorMode 0, supportedColorModes [0, 7, 9], address {port=129, model=0}, deviceProductInfo " + + "DeviceProductInfo{name=, manufacturerPnpId=QCM, productId=1, modelYear=null, manufactureDate=ManufactureDate{week=27, year=2006}, " + + "relativeAddress=null}, removeMode 0}\n" + + " mRequestedMinimalPostProcessing=false"; + DisplayInfo displayInfo = DisplayManager.parseDisplayInfo(partialOutput, 0); + Assert.assertNotNull(displayInfo); + Assert.assertEquals(0, displayInfo.getDisplayId()); + Assert.assertEquals(3, displayInfo.getRotation()); + Assert.assertEquals(0, displayInfo.getLayerStack()); + // FLAG_TRUSTED does not exist in Display (@TestApi), so it won't be reported + Assert.assertEquals(Display.FLAG_SECURE | Display.FLAG_SUPPORTS_PROTECTED_BUFFERS, displayInfo.getFlags()); + Assert.assertEquals(3120, displayInfo.getSize().getWidth()); + Assert.assertEquals(1440, displayInfo.getSize().getHeight()); + } + + @Test + public void testParseDisplayInfoFromDumpsysDisplayAPI31() { + /* @formatter:off */ + String partialOutput = "Logical Displays: size=1\n" + + " Display 0:\n" + + " mDisplayId=0\n" + + " mPhase=1\n" + + " mLayerStack=0\n" + + " mHasContent=true\n" + + " mDesiredDisplayModeSpecs={baseModeId=1 allowGroupSwitching=false primaryRefreshRateRange=[0 60] appRequestRefreshRateRange=[0 " + + "Infinity]}\n" + + " mRequestedColorMode=0\n" + + " mDisplayOffset=(0, 0)\n" + + " mDisplayScalingDisabled=false\n" + + " mPrimaryDisplayDevice=Built-in Screen\n" + + " mBaseDisplayInfo=DisplayInfo{\"Built-in Screen\", displayId 0\", displayGroupId 0, FLAG_SECURE, " + + "FLAG_SUPPORTS_PROTECTED_BUFFERS, FLAG_TRUSTED, real 1080 x 2280, largest app 1080 x 2280, smallest app 1080 x 2280, appVsyncOff " + + "1000000, presDeadline 16666666, mode 1, defaultMode 1, modes [{id=1, width=1080, height=2280, fps=60.000004, " + + "alternativeRefreshRates=[]}], hdrCapabilities HdrCapabilities{mSupportedHdrTypes=[], mMaxLuminance=500.0, " + + "mMaxAverageLuminance=500.0, mMinLuminance=0.0}, userDisabledHdrTypes [], minimalPostProcessingSupported false, rotation 0, state " + + "ON, type INTERNAL, uniqueId \"local:0\", app 1080 x 2280, density 440 (440.0 x 440.0) dpi, layerStack 0, colorMode 0, " + + "supportedColorModes [0], address {port=0, model=0}, deviceProductInfo DeviceProductInfo{name=EMU_display_0, " + + "manufacturerPnpId=GGL, productId=1, modelYear=null, manufactureDate=ManufactureDate{week=27, year=2006}, connectionToSinkType=0}, " + + "removeMode 0, refreshRateOverride 0.0, brightnessMinimum 0.0, brightnessMaximum 1.0, brightnessDefault 0.39763778}\n" + + " mOverrideDisplayInfo=DisplayInfo{\"Built-in Screen\", displayId 0\", displayGroupId 0, FLAG_SECURE, " + + "FLAG_SUPPORTS_PROTECTED_BUFFERS, FLAG_TRUSTED, real 1080 x 2280, largest app 2148 x 2065, smallest app 1080 x 997, appVsyncOff " + + "1000000, presDeadline 16666666, mode 1, defaultMode 1, modes [{id=1, width=1080, height=2280, fps=60.000004, " + + "alternativeRefreshRates=[]}], hdrCapabilities HdrCapabilities{mSupportedHdrTypes=[], mMaxLuminance=500.0, " + + "mMaxAverageLuminance=500.0, mMinLuminance=0.0}, userDisabledHdrTypes [], minimalPostProcessingSupported false, rotation 0, state " + + "ON, type INTERNAL, uniqueId \"local:0\", app 1080 x 2148, density 440 (440.0 x 440.0) dpi, layerStack 0, colorMode 0, " + + "supportedColorModes [0], address {port=0, model=0}, deviceProductInfo DeviceProductInfo{name=EMU_display_0, " + + "manufacturerPnpId=GGL, productId=1, modelYear=null, manufactureDate=ManufactureDate{week=27, year=2006}, connectionToSinkType=0}, " + + "removeMode 0, refreshRateOverride 0.0, brightnessMinimum 0.0, brightnessMaximum 1.0, brightnessDefault 0.39763778}\n" + + " mRequestedMinimalPostProcessing=false\n" + + " mFrameRateOverrides=[]\n" + + " mPendingFrameRateOverrideUids={}\n"; + DisplayInfo displayInfo = DisplayManager.parseDisplayInfo(partialOutput, 0); + Assert.assertNotNull(displayInfo); + Assert.assertEquals(0, displayInfo.getDisplayId()); + Assert.assertEquals(0, displayInfo.getRotation()); + Assert.assertEquals(0, displayInfo.getLayerStack()); + // FLAG_TRUSTED does not exist in Display (@TestApi), so it won't be reported + Assert.assertEquals(Display.FLAG_SECURE | Display.FLAG_SUPPORTS_PROTECTED_BUFFERS, displayInfo.getFlags()); + Assert.assertEquals(1080, displayInfo.getSize().getWidth()); + Assert.assertEquals(2280, displayInfo.getSize().getHeight()); + } + + @Test + public void testParseDisplayInfoFromDumpsysDisplayAPI31NoFlags() { + /* @formatter:off */ + String partialOutput = "Logical Displays: size=1\n" + + " Display 0:\n" + + " mDisplayId=0\n" + + " mPhase=1\n" + + " mLayerStack=0\n" + + " mHasContent=true\n" + + " mDesiredDisplayModeSpecs={baseModeId=1 allowGroupSwitching=false primaryRefreshRateRange=[0 60] appRequestRefreshRateRange=[0 " + + "Infinity]}\n" + + " mRequestedColorMode=0\n" + + " mDisplayOffset=(0, 0)\n" + + " mDisplayScalingDisabled=false\n" + + " mPrimaryDisplayDevice=Built-in Screen\n" + + " mBaseDisplayInfo=DisplayInfo{\"Built-in Screen\", displayId 0\", displayGroupId 0, " + + "real 1080 x 2280, largest app 1080 x 2280, smallest app 1080 x 2280, appVsyncOff " + + "1000000, presDeadline 16666666, mode 1, defaultMode 1, modes [{id=1, width=1080, height=2280, fps=60.000004, " + + "alternativeRefreshRates=[]}], hdrCapabilities HdrCapabilities{mSupportedHdrTypes=[], mMaxLuminance=500.0, " + + "mMaxAverageLuminance=500.0, mMinLuminance=0.0}, userDisabledHdrTypes [], minimalPostProcessingSupported false, rotation 0, state " + + "ON, type INTERNAL, uniqueId \"local:0\", app 1080 x 2280, density 440 (440.0 x 440.0) dpi, layerStack 0, colorMode 0, " + + "supportedColorModes [0], address {port=0, model=0}, deviceProductInfo DeviceProductInfo{name=EMU_display_0, " + + "manufacturerPnpId=GGL, productId=1, modelYear=null, manufactureDate=ManufactureDate{week=27, year=2006}, connectionToSinkType=0}, " + + "removeMode 0, refreshRateOverride 0.0, brightnessMinimum 0.0, brightnessMaximum 1.0, brightnessDefault 0.39763778}\n" + + " mOverrideDisplayInfo=DisplayInfo{\"Built-in Screen\", displayId 0\", displayGroupId 0, " + + "real 1080 x 2280, largest app 2148 x 2065, smallest app 1080 x 997, appVsyncOff " + + "1000000, presDeadline 16666666, mode 1, defaultMode 1, modes [{id=1, width=1080, height=2280, fps=60.000004, " + + "alternativeRefreshRates=[]}], hdrCapabilities HdrCapabilities{mSupportedHdrTypes=[], mMaxLuminance=500.0, " + + "mMaxAverageLuminance=500.0, mMinLuminance=0.0}, userDisabledHdrTypes [], minimalPostProcessingSupported false, rotation 0, state " + + "ON, type INTERNAL, uniqueId \"local:0\", app 1080 x 2148, density 440 (440.0 x 440.0) dpi, layerStack 0, colorMode 0, " + + "supportedColorModes [0], address {port=0, model=0}, deviceProductInfo DeviceProductInfo{name=EMU_display_0, " + + "manufacturerPnpId=GGL, productId=1, modelYear=null, manufactureDate=ManufactureDate{week=27, year=2006}, connectionToSinkType=0}, " + + "removeMode 0, refreshRateOverride 0.0, brightnessMinimum 0.0, brightnessMaximum 1.0, brightnessDefault 0.39763778}\n" + + " mRequestedMinimalPostProcessing=false\n" + + " mFrameRateOverrides=[]\n" + + " mPendingFrameRateOverrideUids={}\n"; + DisplayInfo displayInfo = DisplayManager.parseDisplayInfo(partialOutput, 0); + Assert.assertNotNull(displayInfo); + Assert.assertEquals(0, displayInfo.getDisplayId()); + Assert.assertEquals(0, displayInfo.getRotation()); + Assert.assertEquals(0, displayInfo.getLayerStack()); + Assert.assertEquals(0, displayInfo.getFlags()); + Assert.assertEquals(1080, displayInfo.getSize().getWidth()); + Assert.assertEquals(2280, displayInfo.getSize().getHeight()); + } +} From 7505f7117e19de18e405a3662a38523bf7d20209 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 27 Sep 2022 14:12:27 +0200 Subject: [PATCH 1247/2244] Fix typo in logs --- app/src/control_msg.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/control_msg.c b/app/src/control_msg.c index 3513abc7..fce846ed 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -37,7 +37,7 @@ static const char *const android_motionevent_action_labels[] = { "move", "cancel", "outside", - "ponter-down", + "pointer-down", "pointer-up", "hover-move", "scroll", From 40644994e8fb400dfd298810cd05227a7d5915eb Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 2 Oct 2022 17:39:23 +0200 Subject: [PATCH 1248/2244] Make ServiceManager and Settings methods static There were exactly one instance of ServiceManager and Settings, stored in Device. Since a Device instance is not created by the CleanUp executable, it was not straightforward to call wrapper methods on cleanup. Remove this artificial restriction and expose them publicly via static methods (this is equivalent to expose a singleton, but less verbose). --- .../java/com/genymobile/scrcpy/CleanUp.java | 8 +--- .../java/com/genymobile/scrcpy/Device.java | 31 +++++-------- .../java/com/genymobile/scrcpy/Server.java | 5 +-- .../java/com/genymobile/scrcpy/Settings.java | 20 ++++----- .../scrcpy/wrappers/ServiceManager.java | 45 ++++++++++--------- 5 files changed, 49 insertions(+), 60 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java index 319a957d..831dc994 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java +++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java @@ -1,7 +1,5 @@ package com.genymobile.scrcpy; -import com.genymobile.scrcpy.wrappers.ServiceManager; - import android.os.Parcel; import android.os.Parcelable; import android.util.Base64; @@ -164,12 +162,10 @@ public final class CleanUp { Config config = Config.fromBase64(args[0]); if (config.disableShowTouches || config.restoreStayOn != -1) { - ServiceManager serviceManager = new ServiceManager(); - Settings settings = new Settings(serviceManager); if (config.disableShowTouches) { Ln.i("Disabling \"show touches\""); try { - settings.putValue(Settings.TABLE_SYSTEM, "show_touches", "0"); + Settings.putValue(Settings.TABLE_SYSTEM, "show_touches", "0"); } catch (SettingsException e) { Ln.e("Could not restore \"show_touches\"", e); } @@ -177,7 +173,7 @@ public final class CleanUp { if (config.restoreStayOn != -1) { Ln.i("Restoring \"stay awake\""); try { - settings.putValue(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(config.restoreStayOn)); + Settings.putValue(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(config.restoreStayOn)); } catch (SettingsException e) { Ln.e("Could not restore \"stay_on_while_plugged_in\"", e); } diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index 763a7fad..97b25b22 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -31,9 +31,6 @@ public final class Device { public static final int LOCK_VIDEO_ORIENTATION_UNLOCKED = -1; public static final int LOCK_VIDEO_ORIENTATION_INITIAL = -2; - private static final ServiceManager SERVICE_MANAGER = new ServiceManager(); - private static final Settings SETTINGS = new Settings(SERVICE_MANAGER); - public interface RotationListener { void onRotationChanged(int rotation); } @@ -66,9 +63,9 @@ public final class Device { public Device(Options options) { displayId = options.getDisplayId(); - DisplayInfo displayInfo = SERVICE_MANAGER.getDisplayManager().getDisplayInfo(displayId); + DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(displayId); if (displayInfo == null) { - int[] displayIds = SERVICE_MANAGER.getDisplayManager().getDisplayIds(); + int[] displayIds = ServiceManager.getDisplayManager().getDisplayIds(); throw new InvalidDisplayIdException(displayId, displayIds); } @@ -82,7 +79,7 @@ public final class Device { screenInfo = ScreenInfo.computeScreenInfo(displayInfo.getRotation(), deviceSize, crop, maxSize, lockVideoOrientation); layerStack = displayInfo.getLayerStack(); - SERVICE_MANAGER.getWindowManager().registerRotationWatcher(new IRotationWatcher.Stub() { + ServiceManager.getWindowManager().registerRotationWatcher(new IRotationWatcher.Stub() { @Override public void onRotationChanged(int rotation) { synchronized (Device.this) { @@ -98,7 +95,7 @@ public final class Device { if (options.getControl() && options.getClipboardAutosync()) { // If control and autosync are enabled, synchronize Android clipboard to the computer automatically - ClipboardManager clipboardManager = SERVICE_MANAGER.getClipboardManager(); + ClipboardManager clipboardManager = ServiceManager.getClipboardManager(); if (clipboardManager != null) { clipboardManager.addPrimaryClipChangedListener(new IOnPrimaryClipChangedListener.Stub() { @Override @@ -192,7 +189,7 @@ public final class Device { return false; } - return SERVICE_MANAGER.getInputManager().injectInputEvent(inputEvent, injectMode); + return ServiceManager.getInputManager().injectInputEvent(inputEvent, injectMode); } public boolean injectEvent(InputEvent event, int injectMode) { @@ -220,7 +217,7 @@ public final class Device { } public static boolean isScreenOn() { - return SERVICE_MANAGER.getPowerManager().isScreenOn(); + return ServiceManager.getPowerManager().isScreenOn(); } public synchronized void setRotationListener(RotationListener rotationListener) { @@ -232,19 +229,19 @@ public final class Device { } public static void expandNotificationPanel() { - SERVICE_MANAGER.getStatusBarManager().expandNotificationsPanel(); + ServiceManager.getStatusBarManager().expandNotificationsPanel(); } public static void expandSettingsPanel() { - SERVICE_MANAGER.getStatusBarManager().expandSettingsPanel(); + ServiceManager.getStatusBarManager().expandSettingsPanel(); } public static void collapsePanels() { - SERVICE_MANAGER.getStatusBarManager().collapsePanels(); + ServiceManager.getStatusBarManager().collapsePanels(); } public static String getClipboardText() { - ClipboardManager clipboardManager = SERVICE_MANAGER.getClipboardManager(); + ClipboardManager clipboardManager = ServiceManager.getClipboardManager(); if (clipboardManager == null) { return null; } @@ -256,7 +253,7 @@ public final class Device { } public boolean setClipboardText(String text) { - ClipboardManager clipboardManager = SERVICE_MANAGER.getClipboardManager(); + ClipboardManager clipboardManager = ServiceManager.getClipboardManager(); if (clipboardManager == null) { return false; } @@ -299,7 +296,7 @@ public final class Device { * Disable auto-rotation (if enabled), set the screen rotation and re-enable auto-rotation (if it was enabled). */ public static void rotateDevice() { - WindowManager wm = SERVICE_MANAGER.getWindowManager(); + WindowManager wm = ServiceManager.getWindowManager(); boolean accelerometerRotation = !wm.isRotationFrozen(); @@ -315,8 +312,4 @@ public final class Device { wm.thawRotation(); } } - - public static Settings getSettings() { - return SETTINGS; - } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 1df91552..ec03515e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -20,10 +20,9 @@ public final class Server { int restoreStayOn = -1; boolean restoreNormalPowerMode = options.getControl(); // only restore power mode if control is enabled if (options.getShowTouches() || options.getStayAwake()) { - Settings settings = Device.getSettings(); if (options.getShowTouches()) { try { - String oldValue = settings.getAndPutValue(Settings.TABLE_SYSTEM, "show_touches", "1"); + String oldValue = Settings.getAndPutValue(Settings.TABLE_SYSTEM, "show_touches", "1"); // If "show touches" was disabled, it must be disabled back on clean up mustDisableShowTouchesOnCleanUp = !"1".equals(oldValue); } catch (SettingsException e) { @@ -34,7 +33,7 @@ public final class Server { if (options.getStayAwake()) { int stayOn = BatteryManager.BATTERY_PLUGGED_AC | BatteryManager.BATTERY_PLUGGED_USB | BatteryManager.BATTERY_PLUGGED_WIRELESS; try { - String oldValue = settings.getAndPutValue(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(stayOn)); + String oldValue = Settings.getAndPutValue(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(stayOn)); try { restoreStayOn = Integer.parseInt(oldValue); if (restoreStayOn == stayOn) { diff --git a/server/src/main/java/com/genymobile/scrcpy/Settings.java b/server/src/main/java/com/genymobile/scrcpy/Settings.java index cb15ebb4..1d38814d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Settings.java +++ b/server/src/main/java/com/genymobile/scrcpy/Settings.java @@ -7,16 +7,14 @@ import android.os.Build; import java.io.IOException; -public class Settings { +public final class Settings { public static final String TABLE_SYSTEM = ContentProvider.TABLE_SYSTEM; public static final String TABLE_SECURE = ContentProvider.TABLE_SECURE; public static final String TABLE_GLOBAL = ContentProvider.TABLE_GLOBAL; - private final ServiceManager serviceManager; - - public Settings(ServiceManager serviceManager) { - this.serviceManager = serviceManager; + private Settings() { + /* not instantiable */ } private static void execSettingsPut(String table, String key, String value) throws SettingsException { @@ -35,10 +33,10 @@ public class Settings { } } - public String getValue(String table, String key) throws SettingsException { + public static String getValue(String table, String key) throws SettingsException { if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { // on Android >= 12, it always fails: - try (ContentProvider provider = serviceManager.getActivityManager().createSettingsProvider()) { + try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) { return provider.getValue(table, key); } catch (SettingsException e) { Ln.w("Could not get settings value via ContentProvider, fallback to settings process", e); @@ -48,10 +46,10 @@ public class Settings { return execSettingsGet(table, key); } - public void putValue(String table, String key, String value) throws SettingsException { + public static void putValue(String table, String key, String value) throws SettingsException { if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { // on Android >= 12, it always fails: - try (ContentProvider provider = serviceManager.getActivityManager().createSettingsProvider()) { + try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) { provider.putValue(table, key, value); } catch (SettingsException e) { Ln.w("Could not put settings value via ContentProvider, fallback to settings process", e); @@ -61,10 +59,10 @@ public class Settings { execSettingsPut(table, key, value); } - public String getAndPutValue(String table, String key, String value) throws SettingsException { + public static String getAndPutValue(String table, String key, String value) throws SettingsException { if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { // on Android >= 12, it always fails: - try (ContentProvider provider = serviceManager.getActivityManager().createSettingsProvider()) { + try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) { String oldValue = provider.getValue(table, key); if (!value.equals(oldValue)) { provider.putValue(table, key, value); 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 68f6817d..cb6863b6 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java @@ -13,27 +13,30 @@ 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; - private DisplayManager displayManager; - private InputManager inputManager; - private PowerManager powerManager; - private StatusBarManager statusBarManager; - private ClipboardManager clipboardManager; - private ActivityManager activityManager; - - public ServiceManager() { + private static final Method GET_SERVICE_METHOD; + static { try { - getServiceMethod = Class.forName("android.os.ServiceManager").getDeclaredMethod("getService", String.class); + GET_SERVICE_METHOD = Class.forName("android.os.ServiceManager").getDeclaredMethod("getService", String.class); } catch (Exception e) { throw new AssertionError(e); } } - private IInterface getService(String service, String type) { + private static WindowManager windowManager; + private static DisplayManager displayManager; + private static InputManager inputManager; + private static PowerManager powerManager; + private static StatusBarManager statusBarManager; + private static ClipboardManager clipboardManager; + private static ActivityManager activityManager; + + private ServiceManager() { + /* not instantiable */ + } + + private static IInterface getService(String service, String type) { try { - IBinder binder = (IBinder) getServiceMethod.invoke(null, service); + IBinder binder = (IBinder) GET_SERVICE_METHOD.invoke(null, service); Method asInterfaceMethod = Class.forName(type + "$Stub").getMethod("asInterface", IBinder.class); return (IInterface) asInterfaceMethod.invoke(null, binder); } catch (Exception e) { @@ -41,14 +44,14 @@ public final class ServiceManager { } } - public WindowManager getWindowManager() { + public static WindowManager getWindowManager() { if (windowManager == null) { windowManager = new WindowManager(getService("window", "android.view.IWindowManager")); } return windowManager; } - public DisplayManager getDisplayManager() { + public static DisplayManager getDisplayManager() { if (displayManager == null) { try { Class clazz = Class.forName("android.hardware.display.DisplayManagerGlobal"); @@ -62,7 +65,7 @@ public final class ServiceManager { return displayManager; } - public InputManager getInputManager() { + public static InputManager getInputManager() { if (inputManager == null) { try { Method getInstanceMethod = android.hardware.input.InputManager.class.getDeclaredMethod("getInstance"); @@ -75,21 +78,21 @@ public final class ServiceManager { return inputManager; } - public PowerManager getPowerManager() { + public static PowerManager getPowerManager() { if (powerManager == null) { powerManager = new PowerManager(getService("power", "android.os.IPowerManager")); } return powerManager; } - public StatusBarManager getStatusBarManager() { + public static StatusBarManager getStatusBarManager() { if (statusBarManager == null) { statusBarManager = new StatusBarManager(getService("statusbar", "com.android.internal.statusbar.IStatusBarService")); } return statusBarManager; } - public ClipboardManager getClipboardManager() { + public static ClipboardManager getClipboardManager() { if (clipboardManager == null) { IInterface clipboard = getService("clipboard", "android.content.IClipboard"); if (clipboard == null) { @@ -103,7 +106,7 @@ public final class ServiceManager { return clipboardManager; } - public ActivityManager getActivityManager() { + public static ActivityManager getActivityManager() { if (activityManager == null) { try { // On old Android versions, the ActivityManager is not exposed via AIDL, From 1bfbadef9658c50d8d6206a2114785cdfa176bdf Mon Sep 17 00:00:00 2001 From: Anima C13 <31348553+animaone@users.noreply.github.com> Date: Fri, 7 Oct 2022 11:23:59 -0300 Subject: [PATCH 1249/2244] Add -s auto-completion for bash Fixes #3522 PR #3523 Signed-off-by: Romain Vimont --- app/data/bash-completion/scrcpy | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index 3e75cbb0..0d3a2559 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -93,6 +93,11 @@ _scrcpy() { COMPREPLY=($(compgen -W 'verbose debug info warn error' -- "$cur")) return ;; + -s|--serial) + # Use 'adb devices' to list serial numbers + COMPREPLY=($(compgen -W "$("${ADB:-adb}" devices | awk '$2 == "device" {print $1}')" -- ${cur})) + return + ;; -b|--bitrate \ |--codec-options \ |--crop \ @@ -103,7 +108,6 @@ _scrcpy() { |-m|--max-size \ |-p|--port \ |--push-target \ - |-s|--serial \ |--tunnel-host \ |--tunnel-port \ |--v4l2-buffer \ From 16e2c1ce26d0bd73b914530263a46e3ea6c7ba6c Mon Sep 17 00:00:00 2001 From: Anima C13 <31348553+animaone@users.noreply.github.com> Date: Sat, 8 Oct 2022 13:27:29 -0300 Subject: [PATCH 1250/2244] Add -s auto-completion for zsh Fixes #3522 PR #3523 Signed-off-by: Romain Vimont --- app/data/zsh-completion/_scrcpy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 097c80d6..56c13fd0 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -47,7 +47,7 @@ arguments=( '--record-format=[Force recording format]:format:(mp4 mkv)' '--render-driver=[Request SDL to use the given render driver]:driver name:(direct3d opengl opengles2 opengles metal software)' '--rotation=[Set the initial display rotation]:rotation values:(0 1 2 3)' - {-s,--serial=}'[The device serial number \(mandatory for multiple devices only\)]' + {-s,--serial=}'[The device serial number \(mandatory for multiple devices only\)]:serial:($("${ADB-adb}" devices | awk '\''$2 == "device" {print $1}'\''))' '--shortcut-mod=[\[key1,key2+key3,...\] Specify the modifiers to use for scrcpy shortcuts]:shortcut mod:(lctrl rctrl lalt ralt lsuper rsuper)' {-S,--turn-screen-off}'[Turn the device screen off immediately]' {-t,--show-touches}'[Show physical touches]' From cb46e4a64ae33054a019c70479e2cf71e2a1970d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 19 Oct 2022 15:13:55 +0200 Subject: [PATCH 1251/2244] Add missing include for memmove() --- app/src/util/vector.h | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/util/vector.h b/app/src/util/vector.h index 2c03a430..0c6cab98 100644 --- a/app/src/util/vector.h +++ b/app/src/util/vector.h @@ -6,6 +6,7 @@ #include #include #include +#include // Adapted from vlc_vector: // From ffc7b9169303374f3bf3cae3ea3251111f9ab825 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 19 Oct 2022 15:14:56 +0200 Subject: [PATCH 1252/2244] Add missing include for strlen() --- app/src/util/thread.c | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/util/thread.c b/app/src/util/thread.c index 478867b6..f9687add 100644 --- a/app/src/util/thread.c +++ b/app/src/util/thread.c @@ -1,6 +1,7 @@ #include "thread.h" #include +#include #include #include "log.h" From b62424a98abe301a23525befe7d97cb2dd141ea5 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 19 Oct 2022 15:17:43 +0200 Subject: [PATCH 1253/2244] Build log.c for test_cli On Windows, sc_log_windows_error() is called from net.c, so log.c must also be compiled. Fixes #3542 --- app/meson.build | 1 + 1 file changed, 1 insertion(+) diff --git a/app/meson.build b/app/meson.build index fd5418e3..626e89f8 100644 --- a/app/meson.build +++ b/app/meson.build @@ -267,6 +267,7 @@ if get_option('buildtype') == 'debug' 'tests/test_cli.c', 'src/cli.c', 'src/options.c', + 'src/util/log.c', 'src/util/net.c', 'src/util/str.c', 'src/util/strbuf.c', From d71587e39ba0d0c1541629a2cc8dce443de6581d Mon Sep 17 00:00:00 2001 From: Yu-Chen Lin Date: Sat, 22 Oct 2022 21:32:12 +0800 Subject: [PATCH 1254/2244] Avoid string concatenation in crossfiles This feature is not supported on older meson versions: ERROR: Malformed value in cross file variable prebuilt_libusb. Refs PR #3546 Signed-off-by: Yu-Chen Lin Signed-off-by: Romain Vimont --- cross_win32.txt | 2 +- cross_win64.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cross_win32.txt b/cross_win32.txt index d1d5d11b..1e1b5242 100644 --- a/cross_win32.txt +++ b/cross_win32.txt @@ -22,4 +22,4 @@ ffmpeg_avutil = 'avutil-56' prebuilt_ffmpeg = 'ffmpeg-win32-4.3.1' prebuilt_sdl2 = 'SDL2-2.0.22/i686-w64-mingw32' prebuilt_libusb_root = 'libusb-1.0.26' -prebuilt_libusb = prebuilt_libusb_root + '/MinGW-Win32' +prebuilt_libusb = 'libusb-1.0.26/MinGW-Win32' diff --git a/cross_win64.txt b/cross_win64.txt index cc9e4a0b..f952dec2 100644 --- a/cross_win64.txt +++ b/cross_win64.txt @@ -22,4 +22,4 @@ ffmpeg_avutil = 'avutil-57' prebuilt_ffmpeg = 'ffmpeg-win64-5.0.1' prebuilt_sdl2 = 'SDL2-2.0.22/x86_64-w64-mingw32' prebuilt_libusb_root = 'libusb-1.0.26' -prebuilt_libusb = prebuilt_libusb_root + '/MinGW-x64' +prebuilt_libusb = 'libusb-1.0.26/MinGW-x64' From 48bb6f2ea858a02e05900d9828c0089166de17df Mon Sep 17 00:00:00 2001 From: Yu-Chen Lin Date: Sun, 23 Oct 2022 14:15:25 +0800 Subject: [PATCH 1255/2244] Support wchar_t in argv for Windows PR #3547 Fixes #2932 Signed-off-by: Yu-Chen Lin Signed-off-by: Romain Vimont --- app/src/main.c | 57 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 55 insertions(+), 2 deletions(-) diff --git a/app/src/main.c b/app/src/main.c index 3334cbf9..b3a468cc 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -4,6 +4,10 @@ #include #include #include +#ifdef _WIN32 +#include +#include "util/str.h" +#endif #ifdef HAVE_V4L2 # include #endif @@ -18,8 +22,8 @@ #include "version.h" int -main(int argc, char *argv[]) { -#ifdef __WINDOWS__ +main_scrcpy(int argc, char *argv[]) { +#ifdef _WIN32 // disable buffering, we want logs immediately // even line buffering (setvbuf() with mode _IOLBF) is not sufficient setbuf(stdout, NULL); @@ -80,3 +84,52 @@ main(int argc, char *argv[]) { return ret; } + +int +main(int argc, char *argv[]) { +#ifndef _WIN32 + return main_scrcpy(argc, argv); +#else + (void) argc; + (void) argv; + int wargc; + wchar_t **wargv = CommandLineToArgvW(GetCommandLineW(), &wargc); + if (!wargv) { + LOG_OOM(); + return SCRCPY_EXIT_FAILURE; + } + + char **argv_utf8 = malloc((wargc + 1) * sizeof(*argv_utf8)); + if (!argv_utf8) { + LOG_OOM(); + LocalFree(wargv); + return SCRCPY_EXIT_FAILURE; + } + + argv_utf8[wargc] = NULL; + + for (int i = 0; i < wargc; ++i) { + argv_utf8[i] = sc_str_from_wchars(wargv[i]); + if (!argv_utf8[i]) { + LOG_OOM(); + for (int j = 0; j < i; ++j) { + free(argv_utf8[j]); + } + LocalFree(wargv); + free(argv_utf8); + return SCRCPY_EXIT_FAILURE; + } + } + + LocalFree(wargv); + + int ret = main_scrcpy(wargc, argv_utf8); + + for (int i = 0; i < wargc; ++i) { + free(argv_utf8[i]); + } + free(argv_utf8); + + return ret; +#endif +} From 597703b62e05faea3c556565e0add4cff6bb54e6 Mon Sep 17 00:00:00 2001 From: SeungHoon Han Date: Wed, 9 Nov 2022 13:42:56 +0900 Subject: [PATCH 1256/2244] Fix DisplayInfo parsing for Android Q The DisplayInfo dump format has slightly changed in AOSP: PR #3573 Ref #3416 Signed-off-by: Romain Vimont --- .../scrcpy/wrappers/DisplayManager.java | 2 +- .../genymobile/scrcpy/CommandParserTest.java | 57 +++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) 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 bf172126..17b9ae4d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java @@ -21,7 +21,7 @@ public final class DisplayManager { // public to call it from unit tests public static DisplayInfo parseDisplayInfo(String dumpsysDisplayOutput, int displayId) { Pattern regex = Pattern.compile( - "^ mOverrideDisplayInfo=DisplayInfo\\{\".*?\", displayId " + displayId + ".*?(, FLAG_.*)?, real ([0-9]+) x ([0-9]+).*?, " + "^ mOverrideDisplayInfo=DisplayInfo\\{\".*?, displayId " + displayId + ".*?(, FLAG_.*)?, real ([0-9]+) x ([0-9]+).*?, " + "rotation ([0-9]+).*?, layerStack ([0-9]+)", Pattern.MULTILINE); Matcher m = regex.matcher(dumpsysDisplayOutput); diff --git a/server/src/test/java/com/genymobile/scrcpy/CommandParserTest.java b/server/src/test/java/com/genymobile/scrcpy/CommandParserTest.java index e6f57bcb..d74c5d77 100644 --- a/server/src/test/java/com/genymobile/scrcpy/CommandParserTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/CommandParserTest.java @@ -3,6 +3,7 @@ package com.genymobile.scrcpy; import com.genymobile.scrcpy.wrappers.DisplayManager; import android.view.Display; + import org.junit.Assert; import org.junit.Test; @@ -183,4 +184,60 @@ public class CommandParserTest { Assert.assertEquals(1080, displayInfo.getSize().getWidth()); Assert.assertEquals(2280, displayInfo.getSize().getHeight()); } + + @Test + public void testParseDisplayInfoFromDumpsysDisplayAPI29WithNoFlags() { + /* @formatter:off */ + String partialOutput = "Logical Displays: size=2\n" + + " Display 0:\n" + + " mDisplayId=0\n" + + " mLayerStack=0\n" + + " mHasContent=true\n" + + " mAllowedDisplayModes=[1]\n" + + " mRequestedColorMode=0\n" + + " mDisplayOffset=(0, 0)\n" + + " mDisplayScalingDisabled=false\n" + + " mPrimaryDisplayDevice=Built-in Screen\n" + + " mBaseDisplayInfo=DisplayInfo{\"Built-in Screen, displayId 0\", uniqueId \"local:0\", app 3664 x 1920, " + + "real 3664 x 1920, largest app 3664 x 1920, smallest app 3664 x 1920, mode 61, defaultMode 61, modes [" + + "{id=1, width=3664, height=1920, fps=60.000004}, {id=2, width=3664, height=1920, fps=61.000004}, " + + "{id=61, width=3664, height=1920, fps=120.00001}], colorMode 0, supportedColorModes [0], " + + "hdrCapabilities android.view.Display$HdrCapabilities@4a41fe79, rotation 0, density 290 (320.842 x 319.813) dpi, " + + "layerStack 0, appVsyncOff 1000000, presDeadline 8333333, type BUILT_IN, address {port=129, model=0}, " + + "state ON, FLAG_SECURE, FLAG_SUPPORTS_PROTECTED_BUFFERS, removeMode 0}\n" + + " mOverrideDisplayInfo=DisplayInfo{\"Built-in Screen, displayId 0\", uniqueId \"local:0\", app 3664 x 1920, " + + "real 3664 x 1920, largest app 3664 x 3620, smallest app 1920 x 1876, mode 61, defaultMode 61, modes [" + + "{id=1, width=3664, height=1920, fps=60.000004}, {id=2, width=3664, height=1920, fps=61.000004}, " + + "{id=61, width=3664, height=1920, fps=120.00001}], colorMode 0, supportedColorModes [0], " + + "hdrCapabilities android.view.Display$HdrCapabilities@4a41fe79, rotation 0, density 290 (320.842 x 319.813) dpi, " + + "layerStack 0, appVsyncOff 1000000, presDeadline 8333333, type BUILT_IN, address {port=129, model=0}, " + + "state ON, FLAG_SECURE, FLAG_SUPPORTS_PROTECTED_BUFFERS, removeMode 0}\n" + + " Display 31:\n" + + " mDisplayId=31\n" + + " mLayerStack=31\n" + + " mHasContent=true\n" + + " mAllowedDisplayModes=[92]\n" + + " mRequestedColorMode=0\n" + + " mDisplayOffset=(0, 0)\n" + + " mDisplayScalingDisabled=false\n" + + " mPrimaryDisplayDevice=PanelLayer-#main\n" + + " mBaseDisplayInfo=DisplayInfo{\"PanelLayer-#main, displayId 31\", uniqueId \"virtual:com.test.system,10040,PanelLayer-#main,0\", " + + "app 800 x 110, real 800 x 110, largest app 800 x 110, smallest app 800 x 110, mode 92, defaultMode 92, modes [" + + "{id=92, width=800, height=110, fps=60.0}], colorMode 0, supportedColorModes [0], " + + "hdrCapabilities null, rotation 0, density 200 (200.0 x 200.0) dpi, layerStack 31, appVsyncOff 0, presDeadline 16666666, " + + "type VIRTUAL, state ON, owner com.test.system (uid 10040), FLAG_PRIVATE, removeMode 1}\n" + + " mOverrideDisplayInfo=DisplayInfo{\"PanelLayer-#main, displayId 31\", uniqueId \"virtual:com.test.system,10040,PanelLayer-#main,0\", " + + "app 800 x 110, real 800 x 110, largest app 800 x 800, smallest app 110 x 110, mode 92, defaultMode 92, modes [" + + "{id=92, width=800, height=110, fps=60.0}], colorMode 0, supportedColorModes [0], " + + "hdrCapabilities null, rotation 0, density 200 (200.0 x 200.0) dpi, layerStack 31, appVsyncOff 0, presDeadline 16666666, " + + "type VIRTUAL, state OFF, owner com.test.system (uid 10040), FLAG_PRIVATE, removeMode 1}\n"; + DisplayInfo displayInfo = DisplayManager.parseDisplayInfo(partialOutput, 31); + Assert.assertNotNull(displayInfo); + Assert.assertEquals(31, displayInfo.getDisplayId()); + Assert.assertEquals(0, displayInfo.getRotation()); + Assert.assertEquals(31, displayInfo.getLayerStack()); + Assert.assertEquals(0, displayInfo.getFlags()); + Assert.assertEquals(800, displayInfo.getSize().getWidth()); + Assert.assertEquals(110, displayInfo.getSize().getHeight()); + } } From c00a9ead5e383b00d6b36464c2b234909720f095 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 17 Nov 2022 09:21:16 +0100 Subject: [PATCH 1257/2244] Always use --key=value in README Mandatory arguments may be passed in either of these two forms: 1. --key value 2. --key=value Optional argument may only be passed in the second form. For consistency, always document using --key=value. Refs f76fe2c0d4847d0e40d8708f97591abf3fa22ea5 --- README.md | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 2ae50c4b..1fedde12 100644 --- a/README.md +++ b/README.md @@ -186,7 +186,7 @@ increase performance. To limit both the width and height to some value (e.g. 1024): ```bash -scrcpy --max-size 1024 +scrcpy --max-size=1024 scrcpy -m 1024 # short version ``` @@ -199,7 +199,7 @@ preserved. That way, a device in 1920×1080 will be mirrored at 1024×576. The default bit-rate is 8 Mbps. To change the video bitrate (e.g. to 2 Mbps): ```bash -scrcpy --bit-rate 2M +scrcpy --bit-rate=2M scrcpy -b 2M # short version ``` @@ -208,7 +208,7 @@ scrcpy -b 2M # short version The capture frame rate can be limited: ```bash -scrcpy --max-fps 15 +scrcpy --max-fps=15 ``` This is officially supported since Android 10, but may work on earlier versions. @@ -229,7 +229,7 @@ The device screen may be cropped to mirror only part of the screen. This is useful, for example, to mirror only one eye of the Oculus Go: ```bash -scrcpy --crop 1224:1440:0:0 # 1224x1440 at offset (0,0) +scrcpy --crop=1224:1440:0:0 # 1224x1440 at offset (0,0) ``` If `--max-size` is also specified, resizing is applied after cropping. @@ -258,14 +258,14 @@ Some devices have more than one encoder, and some of them may cause issues or crash. It is possible to select a different encoder: ```bash -scrcpy --encoder OMX.qcom.video.encoder.avc +scrcpy --encoder=OMX.qcom.video.encoder.avc ``` To list the available encoders, you can pass an invalid encoder name; the error will give the available encoders: ```bash -scrcpy --encoder _ +scrcpy --encoder=_ ``` ### Capture @@ -275,14 +275,14 @@ scrcpy --encoder _ It is possible to record the screen while mirroring: ```bash -scrcpy --record file.mp4 +scrcpy --record=file.mp4 scrcpy -r file.mkv ``` To disable mirroring while recording: ```bash -scrcpy --no-display --record file.mp4 +scrcpy --no-display --record=file.mp4 scrcpy -Nr file.mkv # interrupt recording with Ctrl+C ``` @@ -431,7 +431,7 @@ none found, try running `adb disconnect`, and then run those two commands again. It may be useful to decrease the bit-rate and the resolution: ```bash -scrcpy --bit-rate 2M --max-size 800 +scrcpy --bit-rate=2M --max-size=800 scrcpy -b2M -m800 # short version ``` @@ -443,7 +443,7 @@ scrcpy -b2M -m800 # short version If several devices are listed in `adb devices`, you can specify the _serial_: ```bash -scrcpy --serial 0123456789abcdef +scrcpy --serial=0123456789abcdef scrcpy -s 0123456789abcdef # short version ``` @@ -453,7 +453,7 @@ The serial may also be provided via the environment variable `ANDROID_SERIAL` If the device is connected over TCP/IP: ```bash -scrcpy --serial 192.168.0.1:5555 +scrcpy --serial=192.168.0.1:5555 scrcpy -s 192.168.0.1:5555 # short version ``` @@ -606,7 +606,7 @@ scrcpy --force-adb-forward Like for wireless connections, it may be useful to reduce quality: ``` -scrcpy -b2M -m800 --max-fps 15 +scrcpy -b2M -m800 --max-fps=15 ``` ### Window configuration @@ -616,7 +616,7 @@ scrcpy -b2M -m800 --max-fps 15 By default, the window title is the device model. It can be changed: ```bash -scrcpy --window-title 'My device' +scrcpy --window-title='My device' ``` #### Position and size @@ -624,7 +624,7 @@ scrcpy --window-title 'My device' The initial window position and size may be specified: ```bash -scrcpy --window-x 100 --window-y 100 --window-width 800 --window-height 600 +scrcpy --window-x=100 --window-y=100 --window-width=800 --window-height=600 ``` #### Borderless @@ -659,7 +659,7 @@ Fullscreen can then be toggled dynamically with MOD+f. The window may be rotated: ```bash -scrcpy --rotation 1 +scrcpy --rotation=1 ``` Possible values: @@ -701,7 +701,7 @@ If several displays are available, it is possible to select the display to mirror: ```bash -scrcpy --display 1 +scrcpy --display=1 ``` The list of display ids can be retrieved by: From 6469b55861a820e7b7d897376acd3f1f4988e689 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 24 Nov 2022 09:27:10 +0100 Subject: [PATCH 1258/2244] Fix CommandParserTest code style Make checkstyle happy. --- .../genymobile/scrcpy/CommandParserTest.java | 87 +++++++++---------- 1 file changed, 43 insertions(+), 44 deletions(-) diff --git a/server/src/test/java/com/genymobile/scrcpy/CommandParserTest.java b/server/src/test/java/com/genymobile/scrcpy/CommandParserTest.java index d74c5d77..de996a07 100644 --- a/server/src/test/java/com/genymobile/scrcpy/CommandParserTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/CommandParserTest.java @@ -3,7 +3,6 @@ package com.genymobile.scrcpy; import com.genymobile.scrcpy.wrappers.DisplayManager; import android.view.Display; - import org.junit.Assert; import org.junit.Test; @@ -188,49 +187,49 @@ public class CommandParserTest { @Test public void testParseDisplayInfoFromDumpsysDisplayAPI29WithNoFlags() { /* @formatter:off */ - String partialOutput = "Logical Displays: size=2\n" + - " Display 0:\n" + - " mDisplayId=0\n" + - " mLayerStack=0\n" + - " mHasContent=true\n" + - " mAllowedDisplayModes=[1]\n" + - " mRequestedColorMode=0\n" + - " mDisplayOffset=(0, 0)\n" + - " mDisplayScalingDisabled=false\n" + - " mPrimaryDisplayDevice=Built-in Screen\n" + - " mBaseDisplayInfo=DisplayInfo{\"Built-in Screen, displayId 0\", uniqueId \"local:0\", app 3664 x 1920, " + - "real 3664 x 1920, largest app 3664 x 1920, smallest app 3664 x 1920, mode 61, defaultMode 61, modes [" + - "{id=1, width=3664, height=1920, fps=60.000004}, {id=2, width=3664, height=1920, fps=61.000004}, " + - "{id=61, width=3664, height=1920, fps=120.00001}], colorMode 0, supportedColorModes [0], " + - "hdrCapabilities android.view.Display$HdrCapabilities@4a41fe79, rotation 0, density 290 (320.842 x 319.813) dpi, " + - "layerStack 0, appVsyncOff 1000000, presDeadline 8333333, type BUILT_IN, address {port=129, model=0}, " + - "state ON, FLAG_SECURE, FLAG_SUPPORTS_PROTECTED_BUFFERS, removeMode 0}\n" + - " mOverrideDisplayInfo=DisplayInfo{\"Built-in Screen, displayId 0\", uniqueId \"local:0\", app 3664 x 1920, " + - "real 3664 x 1920, largest app 3664 x 3620, smallest app 1920 x 1876, mode 61, defaultMode 61, modes [" + - "{id=1, width=3664, height=1920, fps=60.000004}, {id=2, width=3664, height=1920, fps=61.000004}, " + - "{id=61, width=3664, height=1920, fps=120.00001}], colorMode 0, supportedColorModes [0], " + - "hdrCapabilities android.view.Display$HdrCapabilities@4a41fe79, rotation 0, density 290 (320.842 x 319.813) dpi, " + - "layerStack 0, appVsyncOff 1000000, presDeadline 8333333, type BUILT_IN, address {port=129, model=0}, " + - "state ON, FLAG_SECURE, FLAG_SUPPORTS_PROTECTED_BUFFERS, removeMode 0}\n" + - " Display 31:\n" + - " mDisplayId=31\n" + - " mLayerStack=31\n" + - " mHasContent=true\n" + - " mAllowedDisplayModes=[92]\n" + - " mRequestedColorMode=0\n" + - " mDisplayOffset=(0, 0)\n" + - " mDisplayScalingDisabled=false\n" + - " mPrimaryDisplayDevice=PanelLayer-#main\n" + - " mBaseDisplayInfo=DisplayInfo{\"PanelLayer-#main, displayId 31\", uniqueId \"virtual:com.test.system,10040,PanelLayer-#main,0\", " + - "app 800 x 110, real 800 x 110, largest app 800 x 110, smallest app 800 x 110, mode 92, defaultMode 92, modes [" + - "{id=92, width=800, height=110, fps=60.0}], colorMode 0, supportedColorModes [0], " + - "hdrCapabilities null, rotation 0, density 200 (200.0 x 200.0) dpi, layerStack 31, appVsyncOff 0, presDeadline 16666666, " + - "type VIRTUAL, state ON, owner com.test.system (uid 10040), FLAG_PRIVATE, removeMode 1}\n" + - " mOverrideDisplayInfo=DisplayInfo{\"PanelLayer-#main, displayId 31\", uniqueId \"virtual:com.test.system,10040,PanelLayer-#main,0\", " + - "app 800 x 110, real 800 x 110, largest app 800 x 800, smallest app 110 x 110, mode 92, defaultMode 92, modes [" + - "{id=92, width=800, height=110, fps=60.0}], colorMode 0, supportedColorModes [0], " + - "hdrCapabilities null, rotation 0, density 200 (200.0 x 200.0) dpi, layerStack 31, appVsyncOff 0, presDeadline 16666666, " + - "type VIRTUAL, state OFF, owner com.test.system (uid 10040), FLAG_PRIVATE, removeMode 1}\n"; + String partialOutput = "Logical Displays: size=2\n" + + " Display 0:\n" + + " mDisplayId=0\n" + + " mLayerStack=0\n" + + " mHasContent=true\n" + + " mAllowedDisplayModes=[1]\n" + + " mRequestedColorMode=0\n" + + " mDisplayOffset=(0, 0)\n" + + " mDisplayScalingDisabled=false\n" + + " mPrimaryDisplayDevice=Built-in Screen\n" + + " mBaseDisplayInfo=DisplayInfo{\"Built-in Screen, displayId 0\", uniqueId \"local:0\", app 3664 x 1920, " + + "real 3664 x 1920, largest app 3664 x 1920, smallest app 3664 x 1920, mode 61, defaultMode 61, modes [" + + "{id=1, width=3664, height=1920, fps=60.000004}, {id=2, width=3664, height=1920, fps=61.000004}, " + + "{id=61, width=3664, height=1920, fps=120.00001}], colorMode 0, supportedColorModes [0], " + + "hdrCapabilities android.view.Display$HdrCapabilities@4a41fe79, rotation 0, density 290 (320.842 x 319.813) dpi, " + + "layerStack 0, appVsyncOff 1000000, presDeadline 8333333, type BUILT_IN, address {port=129, model=0}, " + + "state ON, FLAG_SECURE, FLAG_SUPPORTS_PROTECTED_BUFFERS, removeMode 0}\n" + + " mOverrideDisplayInfo=DisplayInfo{\"Built-in Screen, displayId 0\", uniqueId \"local:0\", app 3664 x 1920, " + + "real 3664 x 1920, largest app 3664 x 3620, smallest app 1920 x 1876, mode 61, defaultMode 61, modes [" + + "{id=1, width=3664, height=1920, fps=60.000004}, {id=2, width=3664, height=1920, fps=61.000004}, " + + "{id=61, width=3664, height=1920, fps=120.00001}], colorMode 0, supportedColorModes [0], " + + "hdrCapabilities android.view.Display$HdrCapabilities@4a41fe79, rotation 0, density 290 (320.842 x 319.813) dpi, " + + "layerStack 0, appVsyncOff 1000000, presDeadline 8333333, type BUILT_IN, address {port=129, model=0}, " + + "state ON, FLAG_SECURE, FLAG_SUPPORTS_PROTECTED_BUFFERS, removeMode 0}\n" + + " Display 31:\n" + + " mDisplayId=31\n" + + " mLayerStack=31\n" + + " mHasContent=true\n" + + " mAllowedDisplayModes=[92]\n" + + " mRequestedColorMode=0\n" + + " mDisplayOffset=(0, 0)\n" + + " mDisplayScalingDisabled=false\n" + + " mPrimaryDisplayDevice=PanelLayer-#main\n" + + " mBaseDisplayInfo=DisplayInfo{\"PanelLayer-#main, displayId 31\", uniqueId " + + "\"virtual:com.test.system,10040,PanelLayer-#main,0\", app 800 x 110, real 800 x 110, largest app 800 x 110, smallest app 800 x " + + "110, mode 92, defaultMode 92, modes [{id=92, width=800, height=110, fps=60.0}], colorMode 0, supportedColorModes [0], " + + "hdrCapabilities null, rotation 0, density 200 (200.0 x 200.0) dpi, layerStack 31, appVsyncOff 0, presDeadline 16666666, " + + "type VIRTUAL, state ON, owner com.test.system (uid 10040), FLAG_PRIVATE, removeMode 1}\n" + + " mOverrideDisplayInfo=DisplayInfo{\"PanelLayer-#main, displayId 31\", uniqueId " + + "\"virtual:com.test.system,10040,PanelLayer-#main,0\", app 800 x 110, real 800 x 110, largest app 800 x 800, smallest app 110 x " + + "110, mode 92, defaultMode 92, modes [{id=92, width=800, height=110, fps=60.0}], colorMode 0, supportedColorModes [0], " + + "hdrCapabilities null, rotation 0, density 200 (200.0 x 200.0) dpi, layerStack 31, appVsyncOff 0, presDeadline 16666666, " + + "type VIRTUAL, state OFF, owner com.test.system (uid 10040), FLAG_PRIVATE, removeMode 1}\n"; DisplayInfo displayInfo = DisplayManager.parseDisplayInfo(partialOutput, 31); Assert.assertNotNull(displayInfo); Assert.assertEquals(31, displayInfo.getDisplayId()); From bd1deffa70832a2ab30b8d060992ca70e3136ba8 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 24 Nov 2022 09:04:42 +0100 Subject: [PATCH 1259/2244] Use current adb port (if any) for --tcpip If the current adb port is not 5555 (typically 0 because it is not in TCP/IP mode), --tcpip automatically executes (among other commands): adb tcpip 5555 In case adb was already listening on another port, this command forced to listen on 5555, and the connection should still succeed. But this reconfiguration might be inconvenient for the user. If adb is already in TCP/IP mode, use the current enabled port without reconfiguration. Fixes #3591 --- README.md | 4 +-- app/scrcpy.1 | 2 +- app/src/server.c | 85 +++++++++++++++++++++++++--------------------- app/src/util/str.h | 4 +++ 4 files changed, 54 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 20ad0f9c..87091696 100644 --- a/README.md +++ b/README.md @@ -395,8 +395,8 @@ address), connect the device over USB, then run: scrcpy --tcpip # without arguments ``` -It will automatically find the device IP address, enable TCP/IP mode, then -connect to the device before starting. +It will automatically find the device IP address and adb port, enable TCP/IP +mode if necessary, then connect to the device before starting. ##### Manual diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 7cb893b7..852d9d03 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -275,7 +275,7 @@ Configure and reconnect the device over TCP/IP. If a destination address is provided, then scrcpy connects to this address before starting. The device must listen on the given TCP port (default is 5555). -If no destination address is provided, then scrcpy attempts to find the IP address of the current device (typically connected over USB), enables TCP/IP mode, then connects to this address before starting. +If no destination address is provided, then scrcpy attempts to find the IP address and adb port of the current device (typically connected over USB), enables TCP/IP mode if necessary, then connects to this address before starting. .TP .B \-S, \-\-turn\-screen\-off diff --git a/app/src/server.c b/app/src/server.c index 663ef18b..e5970487 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -19,6 +19,8 @@ #define SC_SERVER_PATH_DEFAULT PREFIX "/share/scrcpy/" SC_SERVER_FILENAME #define SC_DEVICE_SERVER_PATH "/data/local/tmp/scrcpy-server.jar" +#define SC_ADB_PORT_DEFAULT 5555 + static char * get_server_path(void) { #ifdef __WINDOWS__ @@ -513,27 +515,36 @@ sc_server_on_terminated(void *userdata) { LOGD("Server terminated"); } -static bool -is_tcpip_mode_enabled(struct sc_server *server, const char *serial) { +static uint16_t +get_adb_tcp_port(struct sc_server *server, const char *serial) { struct sc_intr *intr = &server->intr; char *current_port = sc_adb_getprop(intr, serial, "service.adb.tcp.port", SC_ADB_SILENT); if (!current_port) { - return false; + return 0; } - // Is the device is listening on TCP on port 5555? - bool enabled = !strcmp("5555", current_port); + long value; + bool ok = sc_str_parse_integer(current_port, &value); free(current_port); - return enabled; + if (!ok) { + return 0; + } + + if (value < 0 || value > 0xFFFF) { + return 0; + } + + return value; } static bool wait_tcpip_mode_enabled(struct sc_server *server, const char *serial, - unsigned attempts, sc_tick delay) { - if (is_tcpip_mode_enabled(server, serial)) { - LOGI("TCP/IP mode enabled"); + uint16_t expected_port, unsigned attempts, + sc_tick delay) { + uint16_t adb_port = get_adb_tcp_port(server, serial); + if (adb_port == expected_port) { return true; } @@ -547,28 +558,23 @@ wait_tcpip_mode_enabled(struct sc_server *server, const char *serial, return false; } - if (is_tcpip_mode_enabled(server, serial)) { - LOGI("TCP/IP mode enabled"); + adb_port = get_adb_tcp_port(server, serial); + if (adb_port == expected_port) { return true; } } while (--attempts); return false; } -char * -append_port_5555(const char *ip) { - size_t len = strlen(ip); - - // sizeof counts the final '\0' - char *ip_port = malloc(len + sizeof(":5555")); - if (!ip_port) { +static char * +append_port(const char *ip, uint16_t port) { + char *ip_port; + int ret = asprintf(&ip_port, "%s:%" PRIu16, ip, port); + if (ret == -1) { LOG_OOM(); return NULL; } - memcpy(ip_port, ip, len); - memcpy(ip_port + len, ":5555", sizeof(":5555")); - return ip_port; } @@ -586,34 +592,36 @@ sc_server_switch_to_tcpip(struct sc_server *server, const char *serial) { return NULL; } - char *ip_port = append_port_5555(ip); - free(ip); - if (!ip_port) { - return NULL; - } + uint16_t adb_port = get_adb_tcp_port(server, serial); + if (adb_port) { + LOGI("TCP/IP mode already enabled on port %" PRIu16, adb_port); + } else { + LOGI("Enabling TCP/IP mode on port " SC_STR(SC_ADB_PORT_DEFAULT) "..."); - bool tcp_mode = is_tcpip_mode_enabled(server, serial); - - if (!tcp_mode) { - bool ok = sc_adb_tcpip(intr, serial, 5555, SC_ADB_NO_STDOUT); + bool ok = sc_adb_tcpip(intr, serial, SC_ADB_PORT_DEFAULT, + SC_ADB_NO_STDOUT); if (!ok) { LOGE("Could not restart adbd in TCP/IP mode"); - goto error; + free(ip); + return NULL; } unsigned attempts = 40; sc_tick delay = SC_TICK_FROM_MS(250); - ok = wait_tcpip_mode_enabled(server, serial, attempts, delay); + ok = wait_tcpip_mode_enabled(server, serial, SC_ADB_PORT_DEFAULT, + attempts, delay); if (!ok) { - goto error; + free(ip); + return NULL; } + + adb_port = SC_ADB_PORT_DEFAULT; + LOGI("TCP/IP mode enabled on port " SC_STR(SC_ADB_PORT_DEFAULT)); } + char *ip_port = append_port(ip, adb_port); + free(ip); return ip_port; - -error: - free(ip_port); - return NULL; } static bool @@ -640,7 +648,8 @@ sc_server_configure_tcpip_known_address(struct sc_server *server, const char *addr) { // Append ":5555" if no port is present bool contains_port = strchr(addr, ':'); - char *ip_port = contains_port ? strdup(addr) : append_port_5555(addr); + char *ip_port = contains_port ? strdup(addr) + : append_port(addr, SC_ADB_PORT_DEFAULT); if (!ip_port) { LOG_OOM(); return false; diff --git a/app/src/util/str.h b/app/src/util/str.h index 1736bd95..4f7eeeda 100644 --- a/app/src/util/str.h +++ b/app/src/util/str.h @@ -6,6 +6,10 @@ #include #include +/* Stringify a numeric value */ +#define SC_STR(s) SC_XSTR(s) +#define SC_XSTR(s) #s + /** * Like strncpy(), except: * - it copies at most n-1 chars From b51841e85d22284eb5dda42096dc46197c710daf Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 21 Dec 2022 13:28:08 +0100 Subject: [PATCH 1260/2244] Upgrade junit to 4.13.2 --- server/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/build.gradle b/server/build.gradle index 00590381..c239e6e5 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -19,7 +19,7 @@ android { } dependencies { - testImplementation 'junit:junit:4.13.1' + testImplementation 'junit:junit:4.13.2' } apply from: "$project.rootDir/config/android-checkstyle.gradle" From 82cb8ab870140403f244fba80ddf2bc5b26b2d78 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 20 Dec 2022 10:38:39 +0100 Subject: [PATCH 1261/2244] Adapt ClipboardManager for Android 13 A new "attributionTag" parameter has been added to the methods getPrimaryClip(), setPrimaryClip() and addPrimaryClipChangedListener() of IClipboard.aidl. Refs Fixes #3497 --- .../scrcpy/wrappers/ClipboardManager.java | 49 ++++++++++++++----- 1 file changed, 38 insertions(+), 11 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 e25b6e99..f43a76bc 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java @@ -15,6 +15,9 @@ public class ClipboardManager { private Method getPrimaryClipMethod; private Method setPrimaryClipMethod; private Method addPrimaryClipChangedListener; + private boolean alternativeGetMethod; + private boolean alternativeSetMethod; + private boolean alternativeAddListenerMethod; public ClipboardManager(IInterface manager) { this.manager = manager; @@ -25,7 +28,12 @@ public class ClipboardManager { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class); } else { - getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class); + try { + getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class); + } catch (NoSuchMethodException e) { + getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class); + alternativeGetMethod = true; + } } } return getPrimaryClipMethod; @@ -36,23 +44,34 @@ public class ClipboardManager { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class); } else { - setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, int.class); + try { + setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, int.class); + } catch (NoSuchMethodException e) { + setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class); + alternativeSetMethod = true; + } } } return setPrimaryClipMethod; } - private static ClipData getPrimaryClip(Method method, IInterface manager) throws InvocationTargetException, IllegalAccessException { + private static ClipData getPrimaryClip(Method method, boolean alternativeMethod, IInterface manager) + throws InvocationTargetException, IllegalAccessException { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { return (ClipData) method.invoke(manager, ServiceManager.PACKAGE_NAME); } + if (alternativeMethod) { + return (ClipData) method.invoke(manager, ServiceManager.PACKAGE_NAME, null, ServiceManager.USER_ID); + } return (ClipData) method.invoke(manager, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID); } - private static void setPrimaryClip(Method method, IInterface manager, ClipData clipData) + private static void setPrimaryClip(Method method, boolean alternativeMethod, IInterface manager, ClipData clipData) throws InvocationTargetException, IllegalAccessException { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { method.invoke(manager, clipData, ServiceManager.PACKAGE_NAME); + } else if (alternativeMethod) { + method.invoke(manager, clipData, ServiceManager.PACKAGE_NAME, null, ServiceManager.USER_ID); } else { method.invoke(manager, clipData, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID); } @@ -61,7 +80,7 @@ public class ClipboardManager { public CharSequence getText() { try { Method method = getGetPrimaryClipMethod(); - ClipData clipData = getPrimaryClip(method, manager); + ClipData clipData = getPrimaryClip(method, alternativeGetMethod, manager); if (clipData == null || clipData.getItemCount() == 0) { return null; } @@ -76,7 +95,7 @@ public class ClipboardManager { try { Method method = getSetPrimaryClipMethod(); ClipData clipData = ClipData.newPlainText(null, text); - setPrimaryClip(method, manager, clipData); + setPrimaryClip(method, alternativeSetMethod, manager, clipData); return true; } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { Ln.e("Could not invoke method", e); @@ -84,10 +103,12 @@ public class ClipboardManager { } } - private static void addPrimaryClipChangedListener(Method method, IInterface manager, IOnPrimaryClipChangedListener listener) - throws InvocationTargetException, IllegalAccessException { + private static void addPrimaryClipChangedListener(Method method, boolean alternativeMethod, IInterface manager, + IOnPrimaryClipChangedListener listener) throws InvocationTargetException, IllegalAccessException { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { method.invoke(manager, listener, ServiceManager.PACKAGE_NAME); + } else if (alternativeMethod) { + method.invoke(manager, listener, ServiceManager.PACKAGE_NAME, null, ServiceManager.USER_ID); } else { method.invoke(manager, listener, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID); } @@ -99,8 +120,14 @@ public class ClipboardManager { addPrimaryClipChangedListener = manager.getClass() .getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class); } else { - addPrimaryClipChangedListener = manager.getClass() - .getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, int.class); + try { + addPrimaryClipChangedListener = manager.getClass() + .getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, int.class); + } catch (NoSuchMethodException e) { + addPrimaryClipChangedListener = manager.getClass() + .getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, String.class, int.class); + alternativeAddListenerMethod = true; + } } } return addPrimaryClipChangedListener; @@ -109,7 +136,7 @@ public class ClipboardManager { public boolean addPrimaryClipChangedListener(IOnPrimaryClipChangedListener listener) { try { Method method = getAddPrimaryClipChangedListener(); - addPrimaryClipChangedListener(method, manager, listener); + addPrimaryClipChangedListener(method, alternativeAddListenerMethod, manager, listener); return true; } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { Ln.e("Could not invoke method", e); From 64821466a1ddcdf7f0a50fa39067f066284ef9ca Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 21 Dec 2022 13:29:27 +0100 Subject: [PATCH 1262/2244] Use "meson setup" This fixes the following warning: > WARNING: Running the setup command as `meson [options]` instead of > `meson setup [options]` is ambiguous and deprecated. --- BUILD.md | 4 ++-- DEVELOP.md | 4 ++-- release.mk | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/BUILD.md b/BUILD.md index 1d5d970b..e3a2a56b 100644 --- a/BUILD.md +++ b/BUILD.md @@ -260,7 +260,7 @@ set ANDROID_SDK_ROOT=%LOCALAPPDATA%\Android\sdk Then, build: ```bash -meson x --buildtype=release --strip -Db_lto=true +meson setup x --buildtype=release --strip -Db_lto=true ninja -Cx # DO NOT RUN AS ROOT ``` @@ -281,7 +281,7 @@ Download the prebuilt server somewhere, and specify its path during the Meson configuration: ```bash -meson x --buildtype=release --strip -Db_lto=true \ +meson setup x --buildtype=release --strip -Db_lto=true \ -Dprebuilt_server=/path/to/scrcpy-server ninja -Cx # DO NOT RUN AS ROOT ``` diff --git a/DEVELOP.md b/DEVELOP.md index d200c3fd..bd409fff 100644 --- a/DEVELOP.md +++ b/DEVELOP.md @@ -277,7 +277,7 @@ The server is pushed to the device by the client on startup. To debug it, enable the server debugger during configuration: ```bash -meson x -Dserver_debugger=true +meson setup x -Dserver_debugger=true # or, if x is already configured meson configure x -Dserver_debugger=true ``` @@ -286,7 +286,7 @@ 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 +meson setup x -Dserver_debugger=true -Dserver_debugger_method=old # or, if x is already configured meson configure x -Dserver_debugger=true -Dserver_debugger_method=old ``` diff --git a/release.mk b/release.mk index 94a9680e..339c42cc 100644 --- a/release.mk +++ b/release.mk @@ -53,13 +53,13 @@ clean: test: [ -d "$(TEST_BUILD_DIR)" ] || ( mkdir "$(TEST_BUILD_DIR)" && \ - meson "$(TEST_BUILD_DIR)" -Db_sanitize=address ) + meson setup "$(TEST_BUILD_DIR)" -Db_sanitize=address ) ninja -C "$(TEST_BUILD_DIR)" $(GRADLE) -p server check build-server: [ -d "$(SERVER_BUILD_DIR)" ] || ( mkdir "$(SERVER_BUILD_DIR)" && \ - meson "$(SERVER_BUILD_DIR)" --buildtype release -Dcompile_app=false ) + meson setup "$(SERVER_BUILD_DIR)" --buildtype release -Dcompile_app=false ) ninja -C "$(SERVER_BUILD_DIR)" prepare-deps-win32: @@ -76,7 +76,7 @@ prepare-deps-win64: build-win32: prepare-deps-win32 [ -d "$(WIN32_BUILD_DIR)" ] || ( mkdir "$(WIN32_BUILD_DIR)" && \ - meson "$(WIN32_BUILD_DIR)" \ + meson setup "$(WIN32_BUILD_DIR)" \ --cross-file cross_win32.txt \ --buildtype release --strip -Db_lto=true \ -Dcompile_server=false \ @@ -85,7 +85,7 @@ build-win32: prepare-deps-win32 build-win64: prepare-deps-win64 [ -d "$(WIN64_BUILD_DIR)" ] || ( mkdir "$(WIN64_BUILD_DIR)" && \ - meson "$(WIN64_BUILD_DIR)" \ + meson setup "$(WIN64_BUILD_DIR)" \ --cross-file cross_win64.txt \ --buildtype release --strip -Db_lto=true \ -Dcompile_server=false \ From 8b38b11875eee81186adebe709ef0fcfd68d88ab Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 21 Dec 2022 13:31:18 +0100 Subject: [PATCH 1263/2244] Add parent directory in release zipfile This avoids to mistakenly extract all the files in the current directory. --- release.mk | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/release.mk b/release.mk index 339c42cc..20561d61 100644 --- a/release.mk +++ b/release.mk @@ -24,13 +24,13 @@ SERVER_BUILD_DIR := build-server WIN32_BUILD_DIR := build-win32 WIN64_BUILD_DIR := build-win64 -DIST := dist -WIN32_TARGET_DIR := scrcpy-win32 -WIN64_TARGET_DIR := scrcpy-win64 - VERSION := $(shell git describe --tags --always) -WIN32_TARGET := $(WIN32_TARGET_DIR)-$(VERSION).zip -WIN64_TARGET := $(WIN64_TARGET_DIR)-$(VERSION).zip + +DIST := dist +WIN32_TARGET_DIR := scrcpy-win32-$(VERSION) +WIN64_TARGET_DIR := scrcpy-win64-$(VERSION) +WIN32_TARGET := $(WIN32_TARGET_DIR).zip +WIN64_TARGET := $(WIN64_TARGET_DIR).zip RELEASE_DIR := release-$(VERSION) @@ -131,9 +131,9 @@ dist-win64: build-server build-win64 cp app/prebuilt-deps/data/libusb-1.0.26/MinGW-x64/msys-usb-1.0.dll "$(DIST)/$(WIN64_TARGET_DIR)/" zip-win32: dist-win32 - cd "$(DIST)/$(WIN32_TARGET_DIR)"; \ - zip -r "../$(WIN32_TARGET)" . + cd "$(DIST)"; \ + zip -r "$(WIN32_TARGET)" "$(WIN32_TARGET_DIR)" zip-win64: dist-win64 - cd "$(DIST)/$(WIN64_TARGET_DIR)"; \ - zip -r "../$(WIN64_TARGET)" . + cd "$(DIST)"; \ + zip -r "$(WIN64_TARGET)" "$(WIN64_TARGET_DIR)" From 18082f60697ae8edad2db950637638c8bb6bf0d2 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 21 Dec 2022 22:06:43 +0100 Subject: [PATCH 1264/2244] Remove continuous resizing workaround for Windows It turns out that the workaround only worked for MacOS. Refs #3458 Refs SDL/#1059 --- app/src/screen.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/screen.c b/app/src/screen.c index ae28e6e6..b2c17575 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -306,13 +306,14 @@ sc_screen_render(struct sc_screen *screen, bool update_content_rect) { } -#if defined(__APPLE__) || defined(__WINDOWS__) +#if defined(__APPLE__) # define CONTINUOUS_RESIZING_WORKAROUND #endif #ifdef CONTINUOUS_RESIZING_WORKAROUND // On Windows and MacOS, resizing blocks the event loop, so resizing events are -// not triggered. As a workaround, handle them in an event handler. +// not triggered. On MacOS, as a workaround, handle them in an event handler +// (it does not work for Windows unfortunately). // // // From c7b1d0ea9af8bb9603ec8f713f1a5fbf3f628b6a Mon Sep 17 00:00:00 2001 From: Pawel Jasinski Date: Tue, 8 Nov 2022 17:15:08 +0100 Subject: [PATCH 1265/2244] Force mouse source when --forward-all-clicks Right click and middle click require the source device to be a mouse, not a touchscreen. Therefore, the source device was changed only when a button other than the primary button was pressed (see adc547fa6e8e6167cd9633a97d98de6665b8c23a). However, this led to inconsistencies between the ACTION_DOWN when a secondary button is pressed (with a mouse as source device) and the matching ACTION_UP when the secondary button is released (with a touchscreen as source device, because then there is no button pressed). To avoid the problem in all cases, force a mouse as source device when --forward-all-clicks is set. Concretely, for mouse events in --forward-all-clicks mode: - device source is set to InputDevice.SOURCE_MOUSE; - motion event toolType is set to MotionEvent.TOOL_TYPE_MOUSE; Otherwise (when --forward-all-clicks is unset, or for real touch events), finger events are injected: - device source is set to InputDevice.SOURCE_TOUCHSCREEN; - motion event toolType is set to MotionEvent.TOOL_TYPE_FINGER. Fixes #3568 PR #3579 Co-authored-by: Romain Vimont Signed-off-by: Romain Vimont --- app/src/control_msg.c | 21 ++++++++++++++-- app/src/control_msg.h | 6 ++++- app/src/input_events.h | 2 ++ app/src/input_manager.c | 8 ++++++- app/src/mouse_inject.c | 4 ++-- .../com/genymobile/scrcpy/Controller.java | 24 ++++++++++++------- 6 files changed, 51 insertions(+), 14 deletions(-) diff --git a/app/src/control_msg.c b/app/src/control_msg.c index fce846ed..60bbd826 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -61,6 +61,22 @@ static const char *const copy_key_labels[] = { "cut", }; +static inline const char * +get_well_known_pointer_id_name(uint64_t pointer_id) { + switch (pointer_id) { + case POINTER_ID_MOUSE: + return "mouse"; + case POINTER_ID_GENERIC_FINGER: + return "finger"; + case POINTER_ID_VIRTUAL_MOUSE: + return "vmouse"; + case POINTER_ID_VIRTUAL_FINGER: + return "vfinger"; + default: + return NULL; + } +} + static void write_position(uint8_t *buf, const struct sc_position *position) { sc_write32be(&buf[0], position->point.x); @@ -159,11 +175,12 @@ sc_control_msg_log(const struct sc_control_msg *msg) { int action = msg->inject_touch_event.action & AMOTION_EVENT_ACTION_MASK; uint64_t id = msg->inject_touch_event.pointer_id; - if (id == POINTER_ID_MOUSE || id == POINTER_ID_VIRTUAL_FINGER) { + const char *pointer_name = get_well_known_pointer_id_name(id); + if (pointer_name) { // string pointer id LOG_CMSG("touch [id=%s] %-4s position=%" PRIi32 ",%" PRIi32 " pressure=%f buttons=%06lx", - id == POINTER_ID_MOUSE ? "mouse" : "vfinger", + pointer_name, MOTIONEVENT_ACTION_LABEL(action), msg->inject_touch_event.position.point.x, msg->inject_touch_event.position.point.y, diff --git a/app/src/control_msg.h b/app/src/control_msg.h index f51bdecd..eb7e25b3 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -18,7 +18,11 @@ #define SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH (SC_CONTROL_MSG_MAX_SIZE - 14) #define POINTER_ID_MOUSE UINT64_C(-1) -#define POINTER_ID_VIRTUAL_FINGER UINT64_C(-2) +#define POINTER_ID_GENERIC_FINGER UINT64_C(-2) + +// Used for injecting an additional virtual pointer for pinch-to-zoom +#define POINTER_ID_VIRTUAL_MOUSE UINT64_C(-3) +#define POINTER_ID_VIRTUAL_FINGER UINT64_C(-4) enum sc_control_msg_type { SC_CONTROL_MSG_TYPE_INJECT_KEYCODE, diff --git a/app/src/input_events.h b/app/src/input_events.h index 15d22910..5831ba0f 100644 --- a/app/src/input_events.h +++ b/app/src/input_events.h @@ -353,6 +353,7 @@ struct sc_mouse_click_event { struct sc_position position; enum sc_action action; enum sc_mouse_button button; + uint64_t pointer_id; uint8_t buttons_state; // bitwise-OR of sc_mouse_button values }; @@ -365,6 +366,7 @@ struct sc_mouse_scroll_event { struct sc_mouse_motion_event { struct sc_position position; + uint64_t pointer_id; int32_t xrel; int32_t yrel; uint8_t buttons_state; // bitwise-OR of sc_mouse_button values diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 42b49a13..ee95d00a 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -335,7 +335,9 @@ simulate_virtual_finger(struct sc_input_manager *im, 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.pointer_id = + im->forward_all_clicks ? POINTER_ID_VIRTUAL_MOUSE + : POINTER_ID_VIRTUAL_FINGER; msg.inject_touch_event.pressure = up ? 0.0f : 1.0f; msg.inject_touch_event.buttons = 0; @@ -564,6 +566,8 @@ sc_input_manager_process_mouse_motion(struct sc_input_manager *im, event->x, event->y), }, + .pointer_id = im->forward_all_clicks ? POINTER_ID_MOUSE + : POINTER_ID_GENERIC_FINGER, .xrel = event->xrel, .yrel = event->yrel, .buttons_state = @@ -687,6 +691,8 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im, }, .action = sc_action_from_sdl_mousebutton_type(event->type), .button = sc_mouse_button_from_sdl(event->button), + .pointer_id = im->forward_all_clicks ? POINTER_ID_MOUSE + : POINTER_ID_GENERIC_FINGER, .buttons_state = sc_mouse_buttons_state_from_sdl(sdl_buttons_state, im->forward_all_clicks), diff --git a/app/src/mouse_inject.c b/app/src/mouse_inject.c index 2e89de9a..bca94637 100644 --- a/app/src/mouse_inject.c +++ b/app/src/mouse_inject.c @@ -69,7 +69,7 @@ sc_mouse_processor_process_mouse_motion(struct sc_mouse_processor *mp, .type = SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, .inject_touch_event = { .action = AMOTION_EVENT_ACTION_MOVE, - .pointer_id = POINTER_ID_MOUSE, + .pointer_id = event->pointer_id, .position = event->position, .pressure = 1.f, .buttons = convert_mouse_buttons(event->buttons_state), @@ -90,7 +90,7 @@ sc_mouse_processor_process_mouse_click(struct sc_mouse_processor *mp, .type = SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, .inject_touch_event = { .action = convert_mouse_action(event->action), - .pointer_id = POINTER_ID_MOUSE, + .pointer_id = event->pointer_id, .position = event->position, .pressure = event->action == SC_ACTION_DOWN ? 1.f : 0.f, .buttons = convert_mouse_buttons(event->buttons_state), diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index 95b64711..a8219edd 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -16,6 +16,10 @@ public class Controller { private static final int DEFAULT_DEVICE_ID = 0; + // control_msg.h values of the pointerId field in inject_touch_event message + private static final int POINTER_ID_MOUSE = -1; + private static final int POINTER_ID_VIRTUAL_MOUSE = -3; + private static final ScheduledExecutorService EXECUTOR = Executors.newSingleThreadScheduledExecutor(); private final Device device; @@ -194,7 +198,19 @@ public class Controller { pointer.setPressure(pressure); pointer.setUp(action == MotionEvent.ACTION_UP); + int source; int pointerCount = pointersState.update(pointerProperties, pointerCoords); + if (pointerId == POINTER_ID_MOUSE || pointerId == POINTER_ID_VIRTUAL_MOUSE) { + // real mouse event (forced by the client when --forward-on-click) + pointerProperties[pointerIndex].toolType = MotionEvent.TOOL_TYPE_MOUSE; + source = InputDevice.SOURCE_MOUSE; + } else { + // POINTER_ID_GENERIC_FINGER, POINTER_ID_VIRTUAL_FINGER or real touch from device + pointerProperties[pointerIndex].toolType = MotionEvent.TOOL_TYPE_FINGER; + source = InputDevice.SOURCE_TOUCHSCREEN; + // Buttons must not be set for touch events + buttons = 0; + } if (pointerCount == 1) { if (action == MotionEvent.ACTION_DOWN) { @@ -209,14 +225,6 @@ public class Controller { } } - // Right-click and middle-click only work if the source is a mouse - boolean nonPrimaryButtonPressed = (buttons & ~MotionEvent.BUTTON_PRIMARY) != 0; - int source = nonPrimaryButtonPressed ? InputDevice.SOURCE_MOUSE : InputDevice.SOURCE_TOUCHSCREEN; - if (source != InputDevice.SOURCE_MOUSE) { - // Buttons must not be set for touch events - buttons = 0; - } - MotionEvent event = MotionEvent .obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0); From b5773a6fe8f6b6cc84594418fc64bcd40eba9b2f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 22 Dec 2022 12:24:58 +0100 Subject: [PATCH 1266/2244] Upgrade platform-tools (33.0.3) for Windows Include the latest version of adb in Windows releases. --- app/prebuilt-deps/prepare-adb.sh | 6 +++--- release.mk | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/prebuilt-deps/prepare-adb.sh b/app/prebuilt-deps/prepare-adb.sh index 6a1f4896..0e4e498c 100755 --- a/app/prebuilt-deps/prepare-adb.sh +++ b/app/prebuilt-deps/prepare-adb.sh @@ -6,10 +6,10 @@ cd "$DIR" mkdir -p "$PREBUILT_DATA_DIR" cd "$PREBUILT_DATA_DIR" -DEP_DIR=platform-tools-33.0.1 +DEP_DIR=platform-tools-33.0.3 -FILENAME=platform-tools_r33.0.1-windows.zip -SHA256SUM=c1f02d42ea24ef4ff2a405ae7370e764ef4546f9b3e4520f5571a00ed5012c42 +FILENAME=platform-tools_r33.0.3-windows.zip +SHA256SUM=1e59afd40a74c5c0eab0a9fad3f0faf8a674267106e0b19921be9f67081808c2 if [[ -d "$DEP_DIR" ]] then diff --git a/release.mk b/release.mk index 20561d61..c55706ea 100644 --- a/release.mk +++ b/release.mk @@ -105,9 +105,9 @@ dist-win32: build-server build-win32 cp app/prebuilt-deps/data/ffmpeg-win32-4.3.1/bin/avformat-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/ffmpeg-win32-4.3.1/bin/swresample-3.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/ffmpeg-win32-4.3.1/bin/swscale-5.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/platform-tools-33.0.1/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/platform-tools-33.0.1/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/platform-tools-33.0.1/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/platform-tools-33.0.3/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/platform-tools-33.0.3/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/platform-tools-33.0.3/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/SDL2-2.0.22/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/libusb-1.0.26/MinGW-Win32/msys-usb-1.0.dll "$(DIST)/$(WIN32_TARGET_DIR)/" @@ -124,9 +124,9 @@ dist-win64: build-server build-win64 cp app/prebuilt-deps/data/ffmpeg-win64-5.0.1/bin/avformat-59.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/ffmpeg-win64-5.0.1/bin/swresample-4.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/ffmpeg-win64-5.0.1/bin/swscale-6.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/platform-tools-33.0.1/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/platform-tools-33.0.1/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/platform-tools-33.0.1/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/platform-tools-33.0.3/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/platform-tools-33.0.3/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/platform-tools-33.0.3/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/SDL2-2.0.22/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/libusb-1.0.26/MinGW-x64/msys-usb-1.0.dll "$(DIST)/$(WIN64_TARGET_DIR)/" From 725a922271170706ef29c8883f179f71a89e21ab Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 22 Dec 2022 12:28:21 +0100 Subject: [PATCH 1267/2244] Upgrade SDL (2.26.1) for Windows Include the latest version of SDL in Windows releases. --- app/prebuilt-deps/prepare-sdl.sh | 6 +++--- cross_win32.txt | 2 +- cross_win64.txt | 2 +- release.mk | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/prebuilt-deps/prepare-sdl.sh b/app/prebuilt-deps/prepare-sdl.sh index 0b41fe5c..644ed72d 100755 --- a/app/prebuilt-deps/prepare-sdl.sh +++ b/app/prebuilt-deps/prepare-sdl.sh @@ -6,10 +6,10 @@ cd "$DIR" mkdir -p "$PREBUILT_DATA_DIR" cd "$PREBUILT_DATA_DIR" -DEP_DIR=SDL2-2.0.22 +DEP_DIR=SDL2-2.26.1 -FILENAME=SDL2-devel-2.0.22-mingw.tar.gz -SHA256SUM=0e91e35973366aa1e6f81ee368924d9b4f93f9da4d2f2a89ec80b06eadcf23d1 +FILENAME=SDL2-devel-2.26.1-mingw.tar.gz +SHA256SUM=aa43e1531a89551f9f9e14b27953a81d4ac946a9e574b5813cd0f2b36e83cc1c if [[ -d "$DEP_DIR" ]] then diff --git a/cross_win32.txt b/cross_win32.txt index 1e1b5242..32226949 100644 --- a/cross_win32.txt +++ b/cross_win32.txt @@ -20,6 +20,6 @@ ffmpeg_avcodec = 'avcodec-58' ffmpeg_avformat = 'avformat-58' ffmpeg_avutil = 'avutil-56' prebuilt_ffmpeg = 'ffmpeg-win32-4.3.1' -prebuilt_sdl2 = 'SDL2-2.0.22/i686-w64-mingw32' +prebuilt_sdl2 = 'SDL2-2.26.1/i686-w64-mingw32' prebuilt_libusb_root = 'libusb-1.0.26' prebuilt_libusb = 'libusb-1.0.26/MinGW-Win32' diff --git a/cross_win64.txt b/cross_win64.txt index f952dec2..4a020f51 100644 --- a/cross_win64.txt +++ b/cross_win64.txt @@ -20,6 +20,6 @@ ffmpeg_avcodec = 'avcodec-59' ffmpeg_avformat = 'avformat-59' ffmpeg_avutil = 'avutil-57' prebuilt_ffmpeg = 'ffmpeg-win64-5.0.1' -prebuilt_sdl2 = 'SDL2-2.0.22/x86_64-w64-mingw32' +prebuilt_sdl2 = 'SDL2-2.26.1/x86_64-w64-mingw32' prebuilt_libusb_root = 'libusb-1.0.26' prebuilt_libusb = 'libusb-1.0.26/MinGW-x64' diff --git a/release.mk b/release.mk index c55706ea..ab2cf301 100644 --- a/release.mk +++ b/release.mk @@ -108,7 +108,7 @@ dist-win32: build-server build-win32 cp app/prebuilt-deps/data/platform-tools-33.0.3/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-33.0.3/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-33.0.3/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/SDL2-2.0.22/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/SDL2-2.26.1/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/libusb-1.0.26/MinGW-Win32/msys-usb-1.0.dll "$(DIST)/$(WIN32_TARGET_DIR)/" dist-win64: build-server build-win64 @@ -127,7 +127,7 @@ dist-win64: build-server build-win64 cp app/prebuilt-deps/data/platform-tools-33.0.3/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-33.0.3/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-33.0.3/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/SDL2-2.0.22/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/SDL2-2.26.1/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/libusb-1.0.26/MinGW-x64/msys-usb-1.0.dll "$(DIST)/$(WIN64_TARGET_DIR)/" zip-win32: dist-win32 From 8e0c89921856c495a102e30db27d52060aa176dd Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 22 Dec 2022 12:33:08 +0100 Subject: [PATCH 1268/2244] Upgrade FFmpeg (5.1.2) for Windows 64-bit Use the latest version of FFmpeg in Windows 64-bit releases. --- app/prebuilt-deps/prepare-ffmpeg-win64.sh | 4 ++-- cross_win64.txt | 2 +- release.mk | 10 +++++----- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/prebuilt-deps/prepare-ffmpeg-win64.sh b/app/prebuilt-deps/prepare-ffmpeg-win64.sh index 8e6c2440..f5d56e6f 100755 --- a/app/prebuilt-deps/prepare-ffmpeg-win64.sh +++ b/app/prebuilt-deps/prepare-ffmpeg-win64.sh @@ -6,11 +6,11 @@ cd "$DIR" mkdir -p "$PREBUILT_DATA_DIR" cd "$PREBUILT_DATA_DIR" -VERSION=5.0.1 +VERSION=5.1.2 DEP_DIR=ffmpeg-win64-$VERSION FILENAME=ffmpeg-$VERSION-full_build-shared.7z -SHA256SUM=ded28435b6f04b74f5ef5a6a13761233bce9e8e9f8ecb0eabe936fd36a778b0c +SHA256SUM=d9eb97b72d7cfdae4d0f7eaea59ccffb8c364d67d88018ea715d5e2e193f00e9 if [[ -d "$DEP_DIR" ]] then diff --git a/cross_win64.txt b/cross_win64.txt index 4a020f51..4dde4ab1 100644 --- a/cross_win64.txt +++ b/cross_win64.txt @@ -19,7 +19,7 @@ endian = 'little' ffmpeg_avcodec = 'avcodec-59' ffmpeg_avformat = 'avformat-59' ffmpeg_avutil = 'avutil-57' -prebuilt_ffmpeg = 'ffmpeg-win64-5.0.1' +prebuilt_ffmpeg = 'ffmpeg-win64-5.1.2' prebuilt_sdl2 = 'SDL2-2.26.1/x86_64-w64-mingw32' prebuilt_libusb_root = 'libusb-1.0.26' prebuilt_libusb = 'libusb-1.0.26/MinGW-x64' diff --git a/release.mk b/release.mk index ab2cf301..06443e1a 100644 --- a/release.mk +++ b/release.mk @@ -119,11 +119,11 @@ dist-win64: build-server build-win64 cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN64_TARGET_DIR)" cp app/data/icon.png "$(DIST)/$(WIN64_TARGET_DIR)" cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN64_TARGET_DIR)" - cp app/prebuilt-deps/data/ffmpeg-win64-5.0.1/bin/avutil-57.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-win64-5.0.1/bin/avcodec-59.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-win64-5.0.1/bin/avformat-59.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-win64-5.0.1/bin/swresample-4.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-win64-5.0.1/bin/swscale-6.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-win64-5.1.2/bin/avutil-57.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-win64-5.1.2/bin/avcodec-59.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-win64-5.1.2/bin/avformat-59.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-win64-5.1.2/bin/swresample-4.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-win64-5.1.2/bin/swscale-6.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-33.0.3/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-33.0.3/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-33.0.3/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" From fe21158c2023017df39ee7ecc6462579a1f3fe45 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 22 Dec 2022 12:33:29 +0100 Subject: [PATCH 1269/2244] Bump version to 1.25 --- app/scrcpy-windows.rc | 2 +- meson.build | 2 +- server/build.gradle | 4 ++-- server/build_without_gradle.sh | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/scrcpy-windows.rc b/app/scrcpy-windows.rc index 6c731003..e20453c1 100644 --- a/app/scrcpy-windows.rc +++ b/app/scrcpy-windows.rc @@ -13,7 +13,7 @@ BEGIN VALUE "LegalCopyright", "Romain Vimont, Genymobile" VALUE "OriginalFilename", "scrcpy.exe" VALUE "ProductName", "scrcpy" - VALUE "ProductVersion", "1.24" + VALUE "ProductVersion", "1.25" END END BLOCK "VarFileInfo" diff --git a/meson.build b/meson.build index bfca6134..0d25085a 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('scrcpy', 'c', - version: '1.24', + version: '1.25', meson_version: '>= 0.48', default_options: [ 'c_std=c11', diff --git a/server/build.gradle b/server/build.gradle index c239e6e5..44bd78e8 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -6,8 +6,8 @@ android { applicationId "com.genymobile.scrcpy" minSdkVersion 21 targetSdkVersion 33 - versionCode 12400 - versionName "1.24" + versionCode 12500 + versionName "1.25" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index f4fcba10..d2757d37 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.24 +SCRCPY_VERSION_NAME=1.25 PLATFORM=${ANDROID_PLATFORM:-33} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-33.0.0} From 4c43784fd1e4e0fad8e3816b240bda5694ea0717 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 22 Dec 2022 12:44:01 +0100 Subject: [PATCH 1270/2244] Update links to v1.25 --- BUILD.md | 6 +++--- README.md | 8 ++++---- install_release.sh | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/BUILD.md b/BUILD.md index e3a2a56b..0c708bde 100644 --- a/BUILD.md +++ b/BUILD.md @@ -272,10 +272,10 @@ install` must be run as root)._ #### Option 2: Use prebuilt server - - [`scrcpy-server-v1.24`][direct-scrcpy-server] - SHA-256: `ae74a81ea79c0dc7250e586627c278c0a9a8c5de46c9fb5c38c167fb1a36f056` + - [`scrcpy-server-v1.25`][direct-scrcpy-server] + SHA-256: `ce0306c7bbd06ae72f6d06f7ec0ee33774995a65de71e0a83813ecb67aec9bdb` -[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.24/scrcpy-server-v1.24 +[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.25/scrcpy-server-v1.25 Download the prebuilt server somewhere, and specify its path during the Meson configuration: diff --git a/README.md b/README.md index 87424360..b2a767fd 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# scrcpy (v1.24) +# scrcpy (v1.25) scrcpy @@ -106,10 +106,10 @@ process][BUILD_simple]). For Windows, a prebuilt archive with all the dependencies (including `adb`) is available: - - [`scrcpy-win64-v1.24.zip`][direct-win64] - SHA-256: `6ccb64cba0a3e75715e85a188daeb4f306a1985f8ce123eba92ba74fc9b27367` + - [`scrcpy-win64-v1.25.zip`][direct-win64] + SHA-256: `db65125e9c65acd00359efb7cea9c05f63cc7ccd5833000cd243cc92f5053028` -[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.24/scrcpy-win64-v1.24.zip +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.25/scrcpy-win64-v1.25.zip It is also available in [Chocolatey]: diff --git a/install_release.sh b/install_release.sh index 88262f8e..0ee80a72 100755 --- a/install_release.sh +++ b/install_release.sh @@ -2,8 +2,8 @@ set -e BUILDDIR=build-auto -PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v1.24/scrcpy-server-v1.24 -PREBUILT_SERVER_SHA256=ae74a81ea79c0dc7250e586627c278c0a9a8c5de46c9fb5c38c167fb1a36f056 +PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v1.25/scrcpy-server-v1.25 +PREBUILT_SERVER_SHA256=ce0306c7bbd06ae72f6d06f7ec0ee33774995a65de71e0a83813ecb67aec9bdb echo "[scrcpy] Downloading prebuilt server..." wget "$PREBUILT_SERVER_URL" -O scrcpy-server From 54c7baceac11626618392f4312d592fd97238dff Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 22 Dec 2022 13:07:07 +0100 Subject: [PATCH 1271/2244] Use "meson setup" from install_release.sh Refs 64821466a1ddcdf7f0a50fa39067f066284ef9ca --- install_release.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install_release.sh b/install_release.sh index 0ee80a72..319aaa4f 100755 --- a/install_release.sh +++ b/install_release.sh @@ -12,7 +12,7 @@ echo "$PREBUILT_SERVER_SHA256 scrcpy-server" | sha256sum --check echo "[scrcpy] Building client..." rm -rf "$BUILDDIR" -meson "$BUILDDIR" --buildtype=release --strip -Db_lto=true \ +meson setup "$BUILDDIR" --buildtype=release --strip -Db_lto=true \ -Dprebuilt_server=scrcpy-server cd "$BUILDDIR" ninja From d8c2fe6ef2279769089fb617e93735217a8668b4 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 26 Dec 2022 12:42:59 +0100 Subject: [PATCH 1272/2244] Revert "Remove continuous resizing workaround for Windows" This reverts commit 18082f60697ae8edad2db950637638c8bb6bf0d2. I can't reproduce, but it seems the workaround improves the behavior on some Windows versions. Fixes #3640 Refs #3458 --- app/src/screen.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/src/screen.c b/app/src/screen.c index b2c17575..ae28e6e6 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -306,14 +306,13 @@ sc_screen_render(struct sc_screen *screen, bool update_content_rect) { } -#if defined(__APPLE__) +#if defined(__APPLE__) || defined(__WINDOWS__) # define CONTINUOUS_RESIZING_WORKAROUND #endif #ifdef CONTINUOUS_RESIZING_WORKAROUND // On Windows and MacOS, resizing blocks the event loop, so resizing events are -// not triggered. On MacOS, as a workaround, handle them in an event handler -// (it does not work for Windows unfortunately). +// not triggered. As a workaround, handle them in an event handler. // // // From bf8696d02e2fdc81854909b43513981b20228f13 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 2 Jan 2023 15:55:46 +0100 Subject: [PATCH 1273/2244] Avoid unnecessary copy on config packets demuxing Use av_packet_ref() to reference the packet without copy. This also simplifies the logic, by making the "offset" variable and the memcpy() call local to the if-block. --- app/src/demuxer.c | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/app/src/demuxer.c b/app/src/demuxer.c index 2c0c64a8..c88af220 100644 --- a/app/src/demuxer.c +++ b/app/src/demuxer.c @@ -95,29 +95,27 @@ sc_demuxer_push_packet(struct sc_demuxer *demuxer, AVPacket *packet) { // A config packet must not be decoded immediately (it contains no // frame); instead, it must be concatenated with the future data packet. if (demuxer->pending || is_config) { - size_t offset; if (demuxer->pending) { - offset = demuxer->pending->size; + size_t offset = demuxer->pending->size; if (av_grow_packet(demuxer->pending, packet->size)) { LOG_OOM(); return false; } + + memcpy(demuxer->pending->data + offset, packet->data, packet->size); } else { - offset = 0; demuxer->pending = av_packet_alloc(); if (!demuxer->pending) { LOG_OOM(); return false; } - if (av_new_packet(demuxer->pending, packet->size)) { + if (av_packet_ref(demuxer->pending, packet)) { LOG_OOM(); av_packet_free(&demuxer->pending); return false; } } - memcpy(demuxer->pending->data + offset, packet->data, packet->size); - if (!is_config) { // prepare the concat packet to send to the decoder demuxer->pending->pts = packet->pts; From b3f626feee7efd34e1d7417a4b6fcfcdfd735d89 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 3 Jan 2023 08:45:29 +0100 Subject: [PATCH 1274/2244] Add FAQ section about HID/OTG on Windows Refs #3654 --- FAQ.md | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/FAQ.md b/FAQ.md index e6c3c94d..76e418dc 100644 --- a/FAQ.md +++ b/FAQ.md @@ -7,7 +7,7 @@ Here are the common reported problems and their status. If you encounter any error, the first step is to upgrade to the latest version. -## `adb` issues +## `adb` and USB issues `scrcpy` execute `adb` commands to initialize the connection with the device. If `adb` fails, then scrcpy will not work. @@ -133,6 +133,21 @@ Try with another USB cable or plug it into another USB port. See [#281] and [#283]: https://github.com/Genymobile/scrcpy/issues/283 +## HID/OTG issues on Windows + +On Windows, if `scrcpy --otg` (or `--hid-keyboard`/`--hid-mouse`) results in: + +> ERROR: Could not find any USB device + +(or if only unrelated USB devices are detected), there might be drivers issues. + +Please read [#3654], in particular [this comment][#3654-comment1] and [the next +one][#3654-comment2]. + +[#3654]: https://github.com/Genymobile/scrcpy/issues/3654 +[#3654-comment1]: https://github.com/Genymobile/scrcpy/issues/3654#issuecomment-1369278232 +[#3654-comment2]: https://github.com/Genymobile/scrcpy/issues/3654#issuecomment-1369295011 + ## Control issues From 87da1372380ebddb60e4d89cff9a251c866e21c5 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 18 Jan 2023 14:37:16 +0100 Subject: [PATCH 1275/2244] Remove "on Linux" in FAQ HID now works on all platforms. --- FAQ.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/FAQ.md b/FAQ.md index 76e418dc..a6b106cc 100644 --- a/FAQ.md +++ b/FAQ.md @@ -168,8 +168,7 @@ The default text injection method is [limited to ASCII characters][text-input]. A trick allows to also inject some [accented characters][accented-characters], but that's all. See [#37]. -Since scrcpy v1.20 on Linux, it is possible to simulate a [physical -keyboard][hid] (HID). +Since scrcpy v1.20, it is possible to simulate a [physical keyboard][hid] (HID). [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 From e6cd42355b773bd0e9613ca9fb5f8364352ac38b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 26 Jan 2023 10:40:48 +0100 Subject: [PATCH 1276/2244] Use separate gen dir to build without gradle The generated source files were written to the classes dir. Use a separate gen dir instead. --- server/build_without_gradle.sh | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index d2757d37..47e65b6b 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -20,6 +20,7 @@ BUILD_TOOLS_DIR="$ANDROID_HOME/build-tools/$BUILD_TOOLS" BUILD_DIR="$(realpath ${BUILD_DIR:-build_manual})" CLASSES_DIR="$BUILD_DIR/classes" +GEN_DIR="$BUILD_DIR/gen" SERVER_DIR=$(dirname "$0") SERVER_BINARY=scrcpy-server ANDROID_JAR="$ANDROID_HOME/platforms/android-$PLATFORM/android.jar" @@ -28,10 +29,11 @@ echo "Platform: android-$PLATFORM" echo "Build-tools: $BUILD_TOOLS" echo "Build dir: $BUILD_DIR" -rm -rf "$CLASSES_DIR" "$BUILD_DIR/$SERVER_BINARY" classes.dex -mkdir -p "$CLASSES_DIR/com/genymobile/scrcpy" +rm -rf "$CLASSES_DIR" "$GEN_DIR" "$BUILD_DIR/$SERVER_BINARY" classes.dex +mkdir -p "$CLASSES_DIR" +mkdir -p "$GEN_DIR/com/genymobile/scrcpy" -<< EOF cat > "$CLASSES_DIR/com/genymobile/scrcpy/BuildConfig.java" +<< EOF cat > "$GEN_DIR/com/genymobile/scrcpy/BuildConfig.java" package com.genymobile.scrcpy; public final class BuildConfig { @@ -42,13 +44,13 @@ EOF echo "Generating java from aidl..." cd "$SERVER_DIR/src/main/aidl" -"$BUILD_TOOLS_DIR/aidl" -o"$CLASSES_DIR" android/view/IRotationWatcher.aidl -"$BUILD_TOOLS_DIR/aidl" -o"$CLASSES_DIR" \ +"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" android/view/IRotationWatcher.aidl +"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" \ android/content/IOnPrimaryClipChangedListener.aidl echo "Compiling java sources..." cd ../java -javac -bootclasspath "$ANDROID_JAR" -cp "$CLASSES_DIR" -d "$CLASSES_DIR" \ +javac -bootclasspath "$ANDROID_JAR" -cp "$GEN_DIR" -d "$CLASSES_DIR" \ -source 1.8 -target 1.8 \ com/genymobile/scrcpy/*.java \ com/genymobile/scrcpy/wrappers/*.java @@ -68,7 +70,7 @@ then echo "Archiving..." cd "$BUILD_DIR" jar cvf "$SERVER_BINARY" classes.dex - rm -rf classes.dex classes + rm -rf classes.dex else # use d8 "$BUILD_TOOLS_DIR/d8" --classpath "$ANDROID_JAR" \ @@ -80,7 +82,8 @@ else cd "$BUILD_DIR" mv classes.zip "$SERVER_BINARY" - rm -rf classes fi +rm -rf "$GEN_DIR" "$CLASSES_DIR" + echo "Server generated in $BUILD_DIR/$SERVER_BINARY" From 059ec45f8244ac3b01cf024c5182c77cb40c76d9 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 21 Jan 2023 19:35:04 +0100 Subject: [PATCH 1277/2244] Add jrand48()/nrand48() compat functions These functions are not available on all platforms. --- app/meson.build | 2 ++ app/src/compat.c | 44 ++++++++++++++++++++++++++++++++++++++++++++ app/src/compat.h | 8 ++++++++ 3 files changed, 54 insertions(+) diff --git a/app/meson.build b/app/meson.build index 626e89f8..cc465564 100644 --- a/app/meson.build +++ b/app/meson.build @@ -170,6 +170,8 @@ check_functions = [ 'strdup', 'asprintf', 'vasprintf', + 'nrand48', + 'jrand48', ] foreach f : check_functions diff --git a/app/src/compat.c b/app/src/compat.c index 11ddd3cb..bb0152aa 100644 --- a/app/src/compat.c +++ b/app/src/compat.c @@ -51,3 +51,47 @@ int vasprintf(char **strp, const char *fmt, va_list ap) { return len; } #endif + +#if !defined(HAVE_NRAND48) || !defined(HAVE_JRAND48) +#define SC_RAND48_MASK UINT64_C(0xFFFFFFFFFFFF) // 48 bits +#define SC_RAND48_A UINT64_C(0x5DEECE66D) +#define SC_RAND48_C 0xB +static inline uint64_t rand_iter48(uint64_t x) { + assert((x & ~SC_RAND48_MASK) == 0); + return (x * SC_RAND48_A + SC_RAND48_C) & SC_RAND48_MASK; +} + +static uint64_t rand_iter48_xsubi(unsigned short xsubi[3]) { + uint64_t x = ((uint64_t) xsubi[0] << 32) + | ((uint64_t) xsubi[1] << 16) + | xsubi[2]; + + x = rand_iter48(x); + + xsubi[0] = (x >> 32) & 0XFFFF; + xsubi[1] = (x >> 16) & 0XFFFF; + xsubi[2] = x & 0XFFFF; + + return x; +} + +#ifndef HAVE_NRAND48 +long nrand48(unsigned short xsubi[3]) { + // range [0, 2^31) + return rand_iter48_xsubi(xsubi) >> 17; +} +#endif + +#ifndef HAVE_JRAND48 +long jrand48(unsigned short xsubi[3]) { + // range [-2^31, 2^31) + union { + uint32_t u; + int32_t i; + } v; + v.u = rand_iter48_xsubi(xsubi) >> 16; + return v.i; +} +#endif + +#endif diff --git a/app/src/compat.h b/app/src/compat.h index 8265dbc8..857623e6 100644 --- a/app/src/compat.h +++ b/app/src/compat.h @@ -59,4 +59,12 @@ int asprintf(char **strp, const char *fmt, ...); int vasprintf(char **strp, const char *fmt, va_list ap); #endif +#ifndef HAVE_NRAND48 +long nrand48(unsigned short xsubi[3]); +#endif + +#ifndef HAVE_JRAND48 +long jrand48(unsigned short xsubi[3]); +#endif + #endif From 74e3f8b2531ee873c3555060d5b02791a325553a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 27 Jan 2023 19:26:19 +0100 Subject: [PATCH 1278/2244] Add random util Add a user-friendly tool to generate random numbers. --- app/meson.build | 1 + app/src/util/rand.c | 24 ++++++++++++++++++++++++ app/src/util/rand.h | 16 ++++++++++++++++ 3 files changed, 41 insertions(+) create mode 100644 app/src/util/rand.c create mode 100644 app/src/util/rand.h diff --git a/app/meson.build b/app/meson.build index cc465564..3ddd5a5d 100644 --- a/app/meson.build +++ b/app/meson.build @@ -37,6 +37,7 @@ src = [ 'src/util/net_intr.c', 'src/util/process.c', 'src/util/process_intr.c', + 'src/util/rand.c', 'src/util/strbuf.c', 'src/util/str.c', 'src/util/term.c', diff --git a/app/src/util/rand.c b/app/src/util/rand.c new file mode 100644 index 00000000..590e4ca4 --- /dev/null +++ b/app/src/util/rand.c @@ -0,0 +1,24 @@ +#include "rand.h" + +#include + +#include "tick.h" + +void sc_rand_init(struct sc_rand *rand) { + sc_tick seed = sc_tick_now(); // microsecond precision + rand->xsubi[0] = (seed >> 32) & 0xFFFF; + rand->xsubi[1] = (seed >> 16) & 0xFFFF; + rand->xsubi[2] = seed & 0xFFFF; +} + +uint32_t sc_rand_u32(struct sc_rand *rand) { + // jrand returns a value in range [-2^31, 2^31] + // conversion from signed to unsigned is well-defined to wrap-around + return jrand48(rand->xsubi); +} + +uint64_t sc_rand_u64(struct sc_rand *rand) { + uint32_t msb = sc_rand_u32(rand); + uint32_t lsb = sc_rand_u32(rand); + return ((uint64_t) msb << 32) | lsb; +} diff --git a/app/src/util/rand.h b/app/src/util/rand.h new file mode 100644 index 00000000..262b0b9b --- /dev/null +++ b/app/src/util/rand.h @@ -0,0 +1,16 @@ +#ifndef SC_RAND_H +#define SC_RAND_H + +#include "common.h" + +#include + +struct sc_rand { + unsigned short xsubi[3]; +}; + +void sc_rand_init(struct sc_rand *rand); +uint32_t sc_rand_u32(struct sc_rand *rand); +uint64_t sc_rand_u64(struct sc_rand *rand); + +#endif From 4315be164823d2c8fc44b475b52af79bfee98ff1 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 27 Jan 2023 21:48:54 +0100 Subject: [PATCH 1279/2244] Use random name for device socket For the initial connection between the device and the computer, an adb tunnel is established (with "adb reverse" or "adb forward"). The device-side of the tunnel is a local socket having the hard-coded name "scrcpy". This may cause issues when several scrcpy instances are started in a few seconds for the same device, since they will try to bind the same name. To avoid conflicts, make the client generate a random UID, and append this UID to the local socket name ("scrcpy_01234567"). --- app/src/adb/adb_tunnel.c | 24 ++++++++-------- app/src/adb/adb_tunnel.h | 6 ++-- app/src/scrcpy.c | 12 ++++++++ app/src/server.c | 28 +++++++++++++++---- app/src/server.h | 2 ++ .../genymobile/scrcpy/DesktopConnection.java | 21 ++++++++++---- .../java/com/genymobile/scrcpy/Options.java | 9 ++++++ .../java/com/genymobile/scrcpy/Server.java | 10 ++++++- 8 files changed, 87 insertions(+), 25 deletions(-) diff --git a/app/src/adb/adb_tunnel.c b/app/src/adb/adb_tunnel.c index c613bc2b..fa936e4b 100644 --- a/app/src/adb/adb_tunnel.c +++ b/app/src/adb/adb_tunnel.c @@ -7,8 +7,6 @@ #include "util/net_intr.h" #include "util/process_intr.h" -#define SC_SOCKET_NAME "scrcpy" - static bool listen_on_port(struct sc_intr *intr, sc_socket socket, uint16_t port) { return net_listen_intr(intr, socket, IPV4_LOCALHOST, port, 1); @@ -17,10 +15,11 @@ listen_on_port(struct sc_intr *intr, sc_socket socket, uint16_t port) { static bool enable_tunnel_reverse_any_port(struct sc_adb_tunnel *tunnel, struct sc_intr *intr, const char *serial, + const char *device_socket_name, struct sc_port_range port_range) { uint16_t port = port_range.first; for (;;) { - if (!sc_adb_reverse(intr, serial, SC_SOCKET_NAME, port, + if (!sc_adb_reverse(intr, serial, device_socket_name, port, SC_ADB_NO_STDOUT)) { // the command itself failed, it will fail on any port return false; @@ -52,7 +51,7 @@ enable_tunnel_reverse_any_port(struct sc_adb_tunnel *tunnel, } // failure, disable tunnel and try another port - if (!sc_adb_reverse_remove(intr, serial, SC_SOCKET_NAME, + if (!sc_adb_reverse_remove(intr, serial, device_socket_name, SC_ADB_NO_STDOUT)) { LOGW("Could not remove reverse tunnel on port %" PRIu16, port); } @@ -78,12 +77,13 @@ enable_tunnel_reverse_any_port(struct sc_adb_tunnel *tunnel, static bool enable_tunnel_forward_any_port(struct sc_adb_tunnel *tunnel, struct sc_intr *intr, const char *serial, + const char *device_socket_name, struct sc_port_range port_range) { tunnel->forward = true; uint16_t port = port_range.first; for (;;) { - if (sc_adb_forward(intr, serial, port, SC_SOCKET_NAME, + if (sc_adb_forward(intr, serial, port, device_socket_name, SC_ADB_NO_STDOUT)) { // success tunnel->local_port = port; @@ -123,13 +123,14 @@ sc_adb_tunnel_init(struct sc_adb_tunnel *tunnel) { bool sc_adb_tunnel_open(struct sc_adb_tunnel *tunnel, struct sc_intr *intr, - const char *serial, struct sc_port_range port_range, - bool force_adb_forward) { + const char *serial, const char *device_socket_name, + struct sc_port_range port_range, bool force_adb_forward) { assert(!tunnel->enabled); if (!force_adb_forward) { // Attempt to use "adb reverse" - if (enable_tunnel_reverse_any_port(tunnel, intr, serial, port_range)) { + if (enable_tunnel_reverse_any_port(tunnel, intr, serial, + device_socket_name, port_range)) { return true; } @@ -139,12 +140,13 @@ sc_adb_tunnel_open(struct sc_adb_tunnel *tunnel, struct sc_intr *intr, LOGW("'adb reverse' failed, fallback to 'adb forward'"); } - return enable_tunnel_forward_any_port(tunnel, intr, serial, port_range); + return enable_tunnel_forward_any_port(tunnel, intr, serial, + device_socket_name, port_range); } bool sc_adb_tunnel_close(struct sc_adb_tunnel *tunnel, struct sc_intr *intr, - const char *serial) { + const char *serial, const char *device_socket_name) { assert(tunnel->enabled); bool ret; @@ -152,7 +154,7 @@ sc_adb_tunnel_close(struct sc_adb_tunnel *tunnel, struct sc_intr *intr, ret = sc_adb_forward_remove(intr, serial, tunnel->local_port, SC_ADB_NO_STDOUT); } else { - ret = sc_adb_reverse_remove(intr, serial, SC_SOCKET_NAME, + ret = sc_adb_reverse_remove(intr, serial, device_socket_name, SC_ADB_NO_STDOUT); assert(tunnel->server_socket != SC_SOCKET_NONE); diff --git a/app/src/adb/adb_tunnel.h b/app/src/adb/adb_tunnel.h index 12e3cf17..7ed5bf54 100644 --- a/app/src/adb/adb_tunnel.h +++ b/app/src/adb/adb_tunnel.h @@ -34,14 +34,14 @@ sc_adb_tunnel_init(struct sc_adb_tunnel *tunnel); */ bool sc_adb_tunnel_open(struct sc_adb_tunnel *tunnel, struct sc_intr *intr, - const char *serial, struct sc_port_range port_range, - bool force_adb_forward); + const char *serial, const char *device_socket_name, + struct sc_port_range port_range, bool force_adb_forward); /** * Close the tunnel */ bool sc_adb_tunnel_close(struct sc_adb_tunnel *tunnel, struct sc_intr *intr, - const char *serial); + const char *serial, const char *device_socket_name); #endif diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 3588e9ae..14688471 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -32,6 +32,7 @@ #include "util/acksync.h" #include "util/log.h" #include "util/net.h" +#include "util/rand.h" #ifdef HAVE_V4L2 # include "v4l2_sink.h" #endif @@ -265,6 +266,14 @@ sc_server_on_disconnected(struct sc_server *server, void *userdata) { // event } +static uint32_t +scrcpy_generate_uid() { + struct sc_rand rand; + sc_rand_init(&rand); + // Only use 31 bits to avoid issues with signed values on the Java-side + return sc_rand_u32(&rand) & 0x7FFFFFFF; +} + enum scrcpy_exit_code scrcpy(struct scrcpy_options *options) { static struct scrcpy scrcpy; @@ -298,7 +307,10 @@ scrcpy(struct scrcpy_options *options) { struct sc_acksync *acksync = NULL; + uint32_t uid = scrcpy_generate_uid(); + struct sc_server_params params = { + .uid = uid, .req_serial = options->serial, .select_usb = options->select_usb, .select_tcpip = options->select_tcpip, diff --git a/app/src/server.c b/app/src/server.c index e5970487..9384ce64 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -20,6 +20,7 @@ #define SC_DEVICE_SERVER_PATH "/data/local/tmp/scrcpy-server.jar" #define SC_ADB_PORT_DEFAULT 5555 +#define SC_SOCKET_NAME_PREFIX "scrcpy_" static char * get_server_path(void) { @@ -197,6 +198,7 @@ execute_server(struct sc_server *server, cmd[count++] = p; \ } + ADD_PARAM("uid=%08x", params->uid); ADD_PARAM("log_level=%s", log_level_to_server_string(params->log_level)); ADD_PARAM("bit_rate=%" PRIu32, params->bit_rate); @@ -364,6 +366,7 @@ sc_server_init(struct sc_server *server, const struct sc_server_params *params, } server->serial = NULL; + server->device_socket_name = NULL; server->stopped = false; server->video_socket = SC_SOCKET_NONE; @@ -463,7 +466,8 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) { } // we don't need the adb tunnel anymore - sc_adb_tunnel_close(tunnel, &server->intr, serial); + sc_adb_tunnel_close(tunnel, &server->intr, serial, + server->device_socket_name); // The sockets will be closed on stop if device_read_info() fails bool ok = device_read_info(&server->intr, video_socket, info); @@ -494,7 +498,8 @@ fail: if (tunnel->enabled) { // Always leave this function with tunnel disabled - sc_adb_tunnel_close(tunnel, &server->intr, serial); + sc_adb_tunnel_close(tunnel, &server->intr, serial, + server->device_socket_name); } return false; @@ -764,13 +769,23 @@ run_server(void *data) { assert(serial); LOGD("Device serial: %s", serial); + int r = asprintf(&server->device_socket_name, SC_SOCKET_NAME_PREFIX "%08x", + params->uid); + if (r == -1) { + LOG_OOM(); + goto error_connection_failed; + } + assert(r == sizeof(SC_SOCKET_NAME_PREFIX) - 1 + 8); + assert(server->device_socket_name); + ok = push_server(&server->intr, serial); if (!ok) { goto error_connection_failed; } ok = sc_adb_tunnel_open(&server->tunnel, &server->intr, serial, - params->port_range, params->force_adb_forward); + server->device_socket_name, params->port_range, + params->force_adb_forward); if (!ok) { goto error_connection_failed; } @@ -778,7 +793,8 @@ run_server(void *data) { // server will connect to our server socket sc_pid pid = execute_server(server, params); if (pid == SC_PROCESS_NONE) { - sc_adb_tunnel_close(&server->tunnel, &server->intr, serial); + sc_adb_tunnel_close(&server->tunnel, &server->intr, serial, + server->device_socket_name); goto error_connection_failed; } @@ -790,7 +806,8 @@ run_server(void *data) { if (!ok) { sc_process_terminate(pid); sc_process_wait(pid, true); // ignore exit code - sc_adb_tunnel_close(&server->tunnel, &server->intr, serial); + sc_adb_tunnel_close(&server->tunnel, &server->intr, serial, + server->device_socket_name); goto error_connection_failed; } @@ -884,6 +901,7 @@ sc_server_destroy(struct sc_server *server) { } free(server->serial); + free(server->device_socket_name); sc_server_params_destroy(&server->params); sc_intr_destroy(&server->intr); sc_cond_destroy(&server->cond_stopped); diff --git a/app/src/server.h b/app/src/server.h index 49ba83c1..e0f2c225 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -22,6 +22,7 @@ struct sc_server_info { }; struct sc_server_params { + uint32_t uid; const char *req_serial; enum sc_log_level log_level; const char *crop; @@ -54,6 +55,7 @@ struct sc_server { // The internal allocated strings are copies owned by the server struct sc_server_params params; char *serial; + char *device_socket_name; sc_thread thread; struct sc_server_info info; // initialized once connected diff --git a/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java b/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java index 78728d81..54a40d22 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java +++ b/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java @@ -15,7 +15,7 @@ public final class DesktopConnection implements Closeable { private static final int DEVICE_NAME_FIELD_LENGTH = 64; - private static final String SOCKET_NAME = "scrcpy"; + private static final String SOCKET_NAME_PREFIX = "scrcpy"; private final LocalSocket videoSocket; private final FileDescriptor videoFd; @@ -46,11 +46,22 @@ public final class DesktopConnection implements Closeable { return localSocket; } - public static DesktopConnection open(boolean tunnelForward, boolean control, boolean sendDummyByte) throws IOException { + private static String getSocketName(int uid) { + if (uid == -1) { + // If no UID is set, use "scrcpy" to simplify using scrcpy-server alone + return SOCKET_NAME_PREFIX; + } + + return SOCKET_NAME_PREFIX + String.format("_%08x", uid); + } + + public static DesktopConnection open(int uid, boolean tunnelForward, boolean control, boolean sendDummyByte) throws IOException { + String socketName = getSocketName(uid); + LocalSocket videoSocket; LocalSocket controlSocket = null; if (tunnelForward) { - LocalServerSocket localServerSocket = new LocalServerSocket(SOCKET_NAME); + LocalServerSocket localServerSocket = new LocalServerSocket(socketName); try { videoSocket = localServerSocket.accept(); if (sendDummyByte) { @@ -69,10 +80,10 @@ public final class DesktopConnection implements Closeable { localServerSocket.close(); } } else { - videoSocket = connect(SOCKET_NAME); + videoSocket = connect(socketName); if (control) { try { - controlSocket = connect(SOCKET_NAME); + controlSocket = connect(socketName); } catch (IOException | RuntimeException e) { videoSocket.close(); throw e; diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index d1607c20..171d6661 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 @@ import java.util.List; public class Options { private Ln.Level logLevel = Ln.Level.DEBUG; + private int uid = -1; // 31-bit non-negative value, or -1 private int maxSize; private int bitRate = 8000000; private int maxFps; @@ -37,6 +38,14 @@ public class Options { this.logLevel = logLevel; } + public int getUid() { + return uid; + } + + public void setUid(int uid) { + this.uid = uid; + } + 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 ec03515e..a8b948c6 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -66,11 +66,12 @@ public final class Server { Thread initThread = startInitThread(options); + int uid = options.getUid(); boolean tunnelForward = options.isTunnelForward(); boolean control = options.getControl(); boolean sendDummyByte = options.getSendDummyByte(); - try (DesktopConnection connection = DesktopConnection.open(tunnelForward, control, sendDummyByte)) { + try (DesktopConnection connection = DesktopConnection.open(uid, tunnelForward, control, sendDummyByte)) { if (options.getSendDeviceMeta()) { Size videoSize = device.getScreenInfo().getVideoSize(); connection.sendDeviceMeta(Device.getDeviceName(), videoSize.getWidth(), videoSize.getHeight()); @@ -178,6 +179,13 @@ public final class Server { String key = arg.substring(0, equalIndex); String value = arg.substring(equalIndex + 1); switch (key) { + case "uid": + int uid = Integer.parseInt(value, 0x10); + if (uid < -1) { + throw new IllegalArgumentException("uid may not be negative (except -1 for 'none'): " + uid); + } + options.setUid(uid); + break; case "log_level": Ln.Level level = Ln.Level.valueOf(value.toUpperCase(Locale.ENGLISH)); options.setLogLevel(level); From b22810b17c1319f02919af493aa174db5d2e174e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 27 Jan 2023 21:59:26 +0100 Subject: [PATCH 1280/2244] Use try-with-resources Replace an explicit try-finally by a try-with-resources block. --- .../main/java/com/genymobile/scrcpy/DesktopConnection.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java b/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java index 54a40d22..7f287a6a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java +++ b/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java @@ -61,8 +61,7 @@ public final class DesktopConnection implements Closeable { LocalSocket videoSocket; LocalSocket controlSocket = null; if (tunnelForward) { - LocalServerSocket localServerSocket = new LocalServerSocket(socketName); - try { + try (LocalServerSocket localServerSocket = new LocalServerSocket(socketName)) { videoSocket = localServerSocket.accept(); if (sendDummyByte) { // send one byte so the client may read() to detect a connection error @@ -76,8 +75,6 @@ public final class DesktopConnection implements Closeable { throw e; } } - } finally { - localServerSocket.close(); } } else { videoSocket = connect(socketName); From 8cbbcc939f95f4f5660503233b73f520b1fffdce Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 27 Jan 2023 22:07:57 +0100 Subject: [PATCH 1281/2244] Add missing final modifiers --- server/src/main/java/com/genymobile/scrcpy/CodecOption.java | 4 ++-- server/src/main/java/com/genymobile/scrcpy/Position.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/CodecOption.java b/server/src/main/java/com/genymobile/scrcpy/CodecOption.java index 12f2a889..22c45a90 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CodecOption.java +++ b/server/src/main/java/com/genymobile/scrcpy/CodecOption.java @@ -4,8 +4,8 @@ import java.util.ArrayList; import java.util.List; public class CodecOption { - private String key; - private Object value; + private final String key; + private final Object value; public CodecOption(String key, Object value) { this.key = key; diff --git a/server/src/main/java/com/genymobile/scrcpy/Position.java b/server/src/main/java/com/genymobile/scrcpy/Position.java index e9b6d8a2..2d298645 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Position.java +++ b/server/src/main/java/com/genymobile/scrcpy/Position.java @@ -3,8 +3,8 @@ package com.genymobile.scrcpy; import java.util.Objects; public class Position { - private Point point; - private Size screenSize; + private final Point point; + private final Size screenSize; public Position(Point point, Size screenSize) { this.point = point; From 234ad7ee78e6b466757da4b57a5899d0f081affe Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 27 Jan 2023 22:08:21 +0100 Subject: [PATCH 1282/2244] Support Java lambdas in build_without_gradle.sh Building Java source code using lambdas requires core-lambda-stubs.jar. Refs #3657 --- server/build_without_gradle.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index 47e65b6b..6677844c 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -24,6 +24,7 @@ GEN_DIR="$BUILD_DIR/gen" SERVER_DIR=$(dirname "$0") SERVER_BINARY=scrcpy-server ANDROID_JAR="$ANDROID_HOME/platforms/android-$PLATFORM/android.jar" +LAMBDA_JAR="$BUILD_TOOLS_DIR/core-lambda-stubs.jar" echo "Platform: android-$PLATFORM" echo "Build-tools: $BUILD_TOOLS" @@ -50,7 +51,9 @@ cd "$SERVER_DIR/src/main/aidl" echo "Compiling java sources..." cd ../java -javac -bootclasspath "$ANDROID_JAR" -cp "$GEN_DIR" -d "$CLASSES_DIR" \ +javac -bootclasspath "$ANDROID_JAR" \ + -cp "$LAMBDA_JAR:$GEN_DIR" \ + -d "$CLASSES_DIR" \ -source 1.8 -target 1.8 \ com/genymobile/scrcpy/*.java \ com/genymobile/scrcpy/wrappers/*.java From bdba55411829ab84112f6b62f2a72907639afc98 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 27 Jan 2023 22:16:36 +0100 Subject: [PATCH 1283/2244] Use Java lambdas where possible --- .../com/genymobile/scrcpy/Controller.java | 9 ++-- .../java/com/genymobile/scrcpy/Server.java | 53 ++++++------------- 2 files changed, 20 insertions(+), 42 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index a8219edd..7663b1cf 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -258,12 +258,9 @@ public class Controller { * 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); - } + EXECUTOR.schedule(() -> { + Ln.i("Forcing screen off"); + Device.setScreenPowerMode(Device.POWER_MODE_OFF); }, 200, TimeUnit.MILLISECONDS); } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index a8b948c6..4a371e5b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -88,12 +88,7 @@ public final class Server { controllerThread = startController(controller); deviceMessageSenderThread = startDeviceMessageSender(controller.getSender()); - device.setClipboardListener(new Device.ClipboardListener() { - @Override - public void onClipboardTextChanged(String text) { - controller.getSender().pushClipboardText(text); - } - }); + device.setClipboardListener(text -> controller.getSender().pushClipboardText(text)); } try { @@ -115,26 +110,18 @@ public final class Server { } private static Thread startInitThread(final Options options) { - Thread thread = new Thread(new Runnable() { - @Override - public void run() { - initAndCleanUp(options); - } - }); + Thread thread = new Thread(() -> initAndCleanUp(options)); thread.start(); return thread; } private static Thread startController(final Controller controller) { - Thread thread = new Thread(new Runnable() { - @Override - public void run() { - try { - controller.control(); - } catch (IOException e) { - // this is expected on close - Ln.d("Controller stopped"); - } + Thread thread = new Thread(() -> { + try { + controller.control(); + } catch (IOException e) { + // this is expected on close + Ln.d("Controller stopped"); } }); thread.start(); @@ -142,15 +129,12 @@ public final class Server { } private static Thread startDeviceMessageSender(final DeviceMessageSender sender) { - Thread thread = new Thread(new Runnable() { - @Override - public void run() { - try { - sender.loop(); - } catch (IOException | InterruptedException e) { - // this is expected on close - Ln.d("Device message sender stopped"); - } + Thread thread = new Thread(() -> { + try { + sender.loop(); + } catch (IOException | InterruptedException e) { + // this is expected on close + Ln.d("Device message sender stopped"); } }); thread.start(); @@ -327,12 +311,9 @@ public final class Server { } public static void main(String... args) throws Exception { - Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { - @Override - public void uncaughtException(Thread t, Throwable e) { - Ln.e("Exception on thread " + t, e); - suggestFix(e); - } + Thread.setDefaultUncaughtExceptionHandler((t, e) -> { + Ln.e("Exception on thread " + t, e); + suggestFix(e); }); Options options = createOptions(args); From 74d32e612db1dfb26900d0a11a98f3a20a27ecfd Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 27 Jan 2023 22:18:54 +0100 Subject: [PATCH 1284/2244] Terminate loop explicitly on interrupted Make explicit that the loop terminates when the current thread is interrupted. --- server/src/main/java/com/genymobile/scrcpy/Controller.java | 2 +- .../main/java/com/genymobile/scrcpy/DeviceMessageSender.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 7663b1cf..3dc609f1 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -75,7 +75,7 @@ public class Controller { SystemClock.sleep(500); } - while (true) { + while (!Thread.currentThread().isInterrupted()) { handleEvent(); } } diff --git a/server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java b/server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java index 4ebccacc..2630652a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java +++ b/server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java @@ -25,7 +25,7 @@ public final class DeviceMessageSender { } public void loop() throws IOException, InterruptedException { - while (true) { + while (!Thread.currentThread().isInterrupted()) { String text; long sequence; synchronized (this) { From 75d7c01a0c6be9495edf05eb84d081c892bab414 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 27 Jan 2023 22:26:01 +0100 Subject: [PATCH 1285/2244] Keep the same display binder across sessions Do not destroy/recreate the display when starting a new encoding session (on device rotation for example). --- 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 e95896d3..78a08da8 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -76,12 +76,12 @@ public class ScreenEncoder implements Device.RotationListener { private void internalStreamScreen(Device device, FileDescriptor fd) throws IOException { MediaFormat format = createFormat(bitRate, maxFps, codecOptions); + IBinder display = createDisplay(); device.setRotationListener(this); boolean alive; try { do { MediaCodec codec = createCodec(encoderName); - IBinder display = createDisplay(); ScreenInfo screenInfo = device.getScreenInfo(); Rect contentRect = screenInfo.getContentRect(); // include the locked video orientation @@ -120,7 +120,6 @@ public class ScreenEncoder implements Device.RotationListener { device.setMaxSize(newMaxSize); alive = true; } finally { - destroyDisplay(display); codec.release(); if (surface != null) { surface.release(); @@ -129,6 +128,7 @@ public class ScreenEncoder implements Device.RotationListener { } while (alive); } finally { device.setRotationListener(null); + destroyDisplay(display); } } From 91c69ad95c639f8c6b84ed71165323227d92e676 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 27 Jan 2023 22:27:34 +0100 Subject: [PATCH 1286/2244] Remove useless destroyDisplay() method The method made exactly one simple call. Just make the call directly. --- .../src/main/java/com/genymobile/scrcpy/ScreenEncoder.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index 78a08da8..a63886e9 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -128,7 +128,7 @@ public class ScreenEncoder implements Device.RotationListener { } while (alive); } finally { device.setRotationListener(null); - destroyDisplay(display); + SurfaceControl.destroyDisplay(display); } } @@ -297,8 +297,4 @@ public class ScreenEncoder implements Device.RotationListener { SurfaceControl.closeTransaction(); } } - - private static void destroyDisplay(IBinder display) { - SurfaceControl.destroyDisplay(display); - } } From 52f85fd6f150907818ccd1213719c1d21aa4684d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 27 Jan 2023 22:31:09 +0100 Subject: [PATCH 1287/2244] Keep the same MediaCodec instance across sessions Calling codec.reset() is sufficient. --- .../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 a63886e9..270751f3 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -75,13 +75,13 @@ public class ScreenEncoder implements Device.RotationListener { } private void internalStreamScreen(Device device, FileDescriptor fd) throws IOException { + MediaCodec codec = createCodec(encoderName); MediaFormat format = createFormat(bitRate, maxFps, codecOptions); IBinder display = createDisplay(); device.setRotationListener(this); boolean alive; try { do { - MediaCodec codec = createCodec(encoderName); ScreenInfo screenInfo = device.getScreenInfo(); Rect contentRect = screenInfo.getContentRect(); // include the locked video orientation @@ -120,13 +120,14 @@ public class ScreenEncoder implements Device.RotationListener { device.setMaxSize(newMaxSize); alive = true; } finally { - codec.release(); + codec.reset(); if (surface != null) { surface.release(); } } } while (alive); } finally { + codec.release(); device.setRotationListener(null); SurfaceControl.destroyDisplay(display); } From 6cccf3ab2a67e0353d746dc00e8adf0893c56e55 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 27 Jan 2023 22:38:37 +0100 Subject: [PATCH 1288/2244] Remove useless configure() method Inline its single call. --- .../src/main/java/com/genymobile/scrcpy/ScreenEncoder.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index 270751f3..5688c385 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -94,7 +94,7 @@ public class ScreenEncoder implements Device.RotationListener { Surface surface = null; try { - configure(codec, format); + codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); surface = codec.createInputSurface(); setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack); codec.start(); @@ -279,10 +279,6 @@ public class ScreenEncoder implements Device.RotationListener { return SurfaceControl.createDisplay("scrcpy", secure); } - private static void configure(MediaCodec codec, MediaFormat format) { - 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); From b53d2c66e0549d434b187f39f3a7d71e27e64a79 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 27 Jan 2023 22:39:28 +0100 Subject: [PATCH 1289/2244] Remove useless setSize() method Inline its content. It makes the logic of internalStreamScreen() more readable (it reduces the number of indirections). --- .../main/java/com/genymobile/scrcpy/ScreenEncoder.java | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index 5688c385..023b676f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -84,13 +84,16 @@ public class ScreenEncoder implements Device.RotationListener { do { ScreenInfo screenInfo = device.getScreenInfo(); Rect contentRect = screenInfo.getContentRect(); + // include the locked video orientation Rect videoRect = screenInfo.getVideoSize().toRect(); + format.setInteger(MediaFormat.KEY_WIDTH, videoRect.width()); + format.setInteger(MediaFormat.KEY_HEIGHT, videoRect.height()); + // 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()); Surface surface = null; try { @@ -279,11 +282,6 @@ public class ScreenEncoder implements Device.RotationListener { return SurfaceControl.createDisplay("scrcpy", secure); } - 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, int layerStack) { SurfaceControl.openTransaction(); try { From a9b2697f3e3d43f329d43ee091516fbd4270b136 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 27 Jan 2023 22:42:37 +0100 Subject: [PATCH 1290/2244] Move local variables declarations This makes it clear that these local variables are only passed to setDisplaySurface(). --- .../java/com/genymobile/scrcpy/ScreenEncoder.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index 023b676f..37467937 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -90,16 +90,17 @@ public class ScreenEncoder implements Device.RotationListener { format.setInteger(MediaFormat.KEY_WIDTH, videoRect.width()); format.setInteger(MediaFormat.KEY_HEIGHT, videoRect.height()); - // does not include the locked video orientation - Rect unlockedVideoRect = screenInfo.getUnlockedVideoSize().toRect(); - int videoRotation = screenInfo.getVideoRotation(); - int layerStack = device.getLayerStack(); - Surface surface = null; try { codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); surface = codec.createInputSurface(); + + // does not include the locked video orientation + Rect unlockedVideoRect = screenInfo.getUnlockedVideoSize().toRect(); + int videoRotation = screenInfo.getVideoRotation(); + int layerStack = device.getLayerStack(); setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack); + codec.start(); alive = encode(codec, fd); From a52053421abee7c96b1071a06cb68bbeac2fd464 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 27 Jan 2023 23:03:41 +0100 Subject: [PATCH 1291/2244] Extract retry handling Move the code to downscale and retry on error out of the catch-block. Refs 26b4104844fb9516be13ff1f2be34e2945090e79 --- .../com/genymobile/scrcpy/ScreenEncoder.java | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index 37467937..a695f0db 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -108,20 +108,10 @@ public class ScreenEncoder implements Device.RotationListener { codec.stop(); } catch (IllegalStateException | IllegalArgumentException e) { Ln.e("Encoding error: " + e.getClass().getName() + ": " + e.getMessage()); - if (!downsizeOnError || firstFrameSent) { - // Fail immediately + if (!prepareRetry(device, screenInfo)) { throw e; } - - int newMaxSize = chooseMaxSizeFallback(screenInfo.getVideoSize()); - if (newMaxSize == 0) { - // Definitively fail - throw e; - } - - // Retry with a smaller device size - Ln.i("Retrying with -m" + newMaxSize + "..."); - device.setMaxSize(newMaxSize); + Ln.i("Retrying..."); alive = true; } finally { codec.reset(); @@ -137,6 +127,25 @@ public class ScreenEncoder implements Device.RotationListener { } } + private boolean prepareRetry(Device device, ScreenInfo screenInfo) { + if (!downsizeOnError || firstFrameSent) { + // Must fail immediately + return false; + } + + int newMaxSize = chooseMaxSizeFallback(screenInfo.getVideoSize()); + Ln.i("newMaxSize = " + newMaxSize); + if (newMaxSize == 0) { + // Must definitively fail + return false; + } + + // Retry with a smaller device size + Ln.i("Retrying with -m" + newMaxSize + "..."); + device.setMaxSize(newMaxSize); + return true; + } + private static int chooseMaxSizeFallback(Size failedSize) { int currentMaxSize = Math.max(failedSize.getWidth(), failedSize.getHeight()); for (int value : MAX_SIZE_FALLBACK) { From 07806ba9159b1f4dbd2ede77b110e254af1fd7f6 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 29 Jan 2023 14:48:07 +0100 Subject: [PATCH 1292/2244] Retry on spurious error MediaCodec may fail spuriously, typically when stopping an encoding and starting a new one immediately (for example on device rotation). In that case, retry a few times, in many cases it should work. Refs #3693 --- .../com/genymobile/scrcpy/ScreenEncoder.java | 20 ++++++++++++++++++- 1 file changed, 19 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 a695f0db..f0384e2c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -9,6 +9,7 @@ import android.media.MediaCodecList; import android.media.MediaFormat; import android.os.Build; import android.os.IBinder; +import android.os.SystemClock; import android.view.Surface; import java.io.FileDescriptor; @@ -27,6 +28,7 @@ public class ScreenEncoder implements Device.RotationListener { // Keep the values in descending order private static final int[] MAX_SIZE_FALLBACK = {2560, 1920, 1600, 1280, 1024, 800}; + private static final int MAX_CONSECUTIVE_ERRORS = 3; private static final long PACKET_FLAG_CONFIG = 1L << 63; private static final long PACKET_FLAG_KEY_FRAME = 1L << 62; @@ -43,6 +45,7 @@ public class ScreenEncoder implements Device.RotationListener { private long ptsOrigin; private boolean firstFrameSent; + private int consecutiveErrors; public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, List codecOptions, String encoderName, boolean downsizeOnError) { @@ -128,11 +131,25 @@ public class ScreenEncoder implements Device.RotationListener { } private boolean prepareRetry(Device device, ScreenInfo screenInfo) { - if (!downsizeOnError || firstFrameSent) { + if (firstFrameSent) { + ++consecutiveErrors; + if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) { + // Definitively fail + return false; + } + + // Wait a bit to increase the probability that retrying will fix the problem + SystemClock.sleep(50); + return true; + } + + if (!downsizeOnError) { // Must fail immediately return false; } + // Downsizing on error is only enabled if an encoding failure occurs before the first frame (downsizing later could be surprising) + int newMaxSize = chooseMaxSizeFallback(screenInfo.getVideoSize()); Ln.i("newMaxSize = " + newMaxSize); if (newMaxSize == 0) { @@ -181,6 +198,7 @@ public class ScreenEncoder implements Device.RotationListener { if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) == 0) { // If this is not a config packet, then it contains a frame firstFrameSent = true; + consecutiveErrors = 0; } } } finally { From 0afef0c6349e3c79ebcdc24d955f61fdc27c42f4 Mon Sep 17 00:00:00 2001 From: Simon Chan <1330321+yume-chan@users.noreply.github.com> Date: Sun, 29 Jan 2023 22:14:05 +0100 Subject: [PATCH 1293/2244] Forward action button to device On click event, only the whole buttons state was passed to the device. In addition, on ACTION_DOWN and ACTION_UP, pass the button associated to the action. Refs #3635 Co-authored-by: Romain Vimont Signed-off-by: Romain Vimont --- app/src/control_msg.c | 12 ++++++++---- app/src/control_msg.h | 1 + app/src/input_manager.c | 1 + app/src/mouse_inject.c | 1 + app/tests/test_control_msg_serialize.c | 6 ++++-- .../java/com/genymobile/scrcpy/ControlMessage.java | 9 ++++++++- .../com/genymobile/scrcpy/ControlMessageReader.java | 5 +++-- .../genymobile/scrcpy/ControlMessageReaderTest.java | 4 +++- 8 files changed, 29 insertions(+), 10 deletions(-) diff --git a/app/src/control_msg.c b/app/src/control_msg.c index 60bbd826..d4d6c62a 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -117,8 +117,9 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, unsigned char *buf) { uint16_t pressure = sc_float_to_u16fp(msg->inject_touch_event.pressure); sc_write16be(&buf[22], pressure); - sc_write32be(&buf[24], msg->inject_touch_event.buttons); - return 28; + sc_write32be(&buf[24], msg->inject_touch_event.action_button); + sc_write32be(&buf[28], msg->inject_touch_event.buttons); + return 32; case SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT: write_position(&buf[1], &msg->inject_scroll_event.position); int16_t hscroll = @@ -179,22 +180,25 @@ sc_control_msg_log(const struct sc_control_msg *msg) { if (pointer_name) { // string pointer id LOG_CMSG("touch [id=%s] %-4s position=%" PRIi32 ",%" PRIi32 - " pressure=%f buttons=%06lx", + " pressure=%f action_button=%06lx buttons=%06lx", pointer_name, MOTIONEVENT_ACTION_LABEL(action), msg->inject_touch_event.position.point.x, msg->inject_touch_event.position.point.y, msg->inject_touch_event.pressure, + (long) msg->inject_touch_event.action_button, (long) msg->inject_touch_event.buttons); } else { // numeric pointer id LOG_CMSG("touch [id=%" PRIu64_ "] %-4s position=%" PRIi32 ",%" - PRIi32 " pressure=%f buttons=%06lx", + PRIi32 " pressure=%f action_button=%06lx" + " buttons=%06lx", id, MOTIONEVENT_ACTION_LABEL(action), msg->inject_touch_event.position.point.x, msg->inject_touch_event.position.point.y, msg->inject_touch_event.pressure, + (long) msg->inject_touch_event.action_button, (long) msg->inject_touch_event.buttons); } break; diff --git a/app/src/control_msg.h b/app/src/control_msg.h index eb7e25b3..b90a00b3 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -65,6 +65,7 @@ struct sc_control_msg { } inject_text; struct { enum android_motionevent_action action; + enum android_motionevent_buttons action_button; enum android_motionevent_buttons buttons; uint64_t pointer_id; struct sc_position position; diff --git a/app/src/input_manager.c b/app/src/input_manager.c index ee95d00a..c8098ee7 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -339,6 +339,7 @@ simulate_virtual_finger(struct sc_input_manager *im, im->forward_all_clicks ? POINTER_ID_VIRTUAL_MOUSE : POINTER_ID_VIRTUAL_FINGER; msg.inject_touch_event.pressure = up ? 0.0f : 1.0f; + msg.inject_touch_event.action_button = 0; msg.inject_touch_event.buttons = 0; if (!sc_controller_push_msg(im->controller, &msg)) { diff --git a/app/src/mouse_inject.c b/app/src/mouse_inject.c index bca94637..71b7a64d 100644 --- a/app/src/mouse_inject.c +++ b/app/src/mouse_inject.c @@ -93,6 +93,7 @@ sc_mouse_processor_process_mouse_click(struct sc_mouse_processor *mp, .pointer_id = event->pointer_id, .position = event->position, .pressure = event->action == SC_ACTION_DOWN ? 1.f : 0.f, + .action_button = convert_mouse_buttons(event->button), .buttons = convert_mouse_buttons(event->buttons_state), }, }; diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c index 87117e3a..b2eef49c 100644 --- a/app/tests/test_control_msg_serialize.c +++ b/app/tests/test_control_msg_serialize.c @@ -90,13 +90,14 @@ static void test_serialize_inject_touch_event(void) { }, }, .pressure = 1.0f, + .action_button = AMOTION_EVENT_BUTTON_PRIMARY, .buttons = AMOTION_EVENT_BUTTON_PRIMARY, }, }; unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; size_t size = sc_control_msg_serialize(&msg, buf); - assert(size == 28); + assert(size == 32); const unsigned char expected[] = { SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, @@ -105,7 +106,8 @@ static void test_serialize_inject_touch_event(void) { 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0xc8, // 100 200 0x04, 0x38, 0x07, 0x80, // 1080 1920 0xff, 0xff, // pressure - 0x00, 0x00, 0x00, 0x01 // AMOTION_EVENT_BUTTON_PRIMARY + 0x00, 0x00, 0x00, 0x01, // AMOTION_EVENT_BUTTON_PRIMARY (action button) + 0x00, 0x00, 0x00, 0x01, // AMOTION_EVENT_BUTTON_PRIMARY (buttons) }; 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 0b05b22a..e1800374 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java @@ -29,6 +29,7 @@ public final class ControlMessage { private int metaState; // KeyEvent.META_* private int action; // KeyEvent.ACTION_* or MotionEvent.ACTION_* or POWER_MODE_* private int keycode; // KeyEvent.KEYCODE_* + private int actionButton; // MotionEvent.BUTTON_* private int buttons; // MotionEvent.BUTTON_* private long pointerId; private float pressure; @@ -60,13 +61,15 @@ public final class ControlMessage { return msg; } - public static ControlMessage createInjectTouchEvent(int action, long pointerId, Position position, float pressure, int buttons) { + public static ControlMessage createInjectTouchEvent(int action, long pointerId, Position position, float pressure, int actionButton, + int buttons) { ControlMessage msg = new ControlMessage(); msg.type = TYPE_INJECT_TOUCH_EVENT; msg.action = action; msg.pointerId = pointerId; msg.pressure = pressure; msg.position = position; + msg.actionButton = actionButton; msg.buttons = buttons; return msg; } @@ -140,6 +143,10 @@ public final class ControlMessage { return keycode; } + public int getActionButton() { + return actionButton; + } + public int getButtons() { return buttons; } diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java index a52fd134..d95c36d8 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 { static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 13; - static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 27; + static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 31; static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20; static final int BACK_OR_SCREEN_ON_LENGTH = 1; static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1; @@ -140,8 +140,9 @@ public class ControlMessageReader { long pointerId = buffer.getLong(); Position position = readPosition(buffer); float pressure = Binary.u16FixedPointToFloat(buffer.getShort()); + int actionButton = buffer.getInt(); int buttons = buffer.getInt(); - return ControlMessage.createInjectTouchEvent(action, pointerId, position, pressure, buttons); + return ControlMessage.createInjectTouchEvent(action, pointerId, position, pressure, actionButton, buttons); } private ControlMessage parseInjectScrollEvent() { diff --git a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java index 1fc009ce..8405905a 100644 --- a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java @@ -94,7 +94,8 @@ public class ControlMessageReaderTest { dos.writeShort(1080); dos.writeShort(1920); dos.writeShort(0xffff); // pressure - dos.writeInt(MotionEvent.BUTTON_PRIMARY); + dos.writeInt(MotionEvent.BUTTON_PRIMARY); // action button + dos.writeInt(MotionEvent.BUTTON_PRIMARY); // buttons byte[] packet = bos.toByteArray(); @@ -112,6 +113,7 @@ public class ControlMessageReaderTest { Assert.assertEquals(1080, event.getPosition().getScreenSize().getWidth()); Assert.assertEquals(1920, event.getPosition().getScreenSize().getHeight()); Assert.assertEquals(1f, event.getPressure(), 0f); // must be exact + Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getActionButton()); Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getButtons()); } From 8c5c55f9e193be8a195b885799ba84377c413cc5 Mon Sep 17 00:00:00 2001 From: Simon Chan <1330321+yume-chan@users.noreply.github.com> Date: Sun, 29 Jan 2023 22:23:11 +0100 Subject: [PATCH 1294/2244] Fix mouse pointer state update If the pointer is a mouse, the pointer is UP only when no buttons are pressed (not when a button is released, because there might be other buttons still pressed). Refs #3635 Signed-off-by: Romain Vimont --- server/src/main/java/com/genymobile/scrcpy/Controller.java | 5 +++-- 1 file changed, 3 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 3dc609f1..bff01d9f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -196,22 +196,23 @@ public class Controller { Pointer pointer = pointersState.get(pointerIndex); pointer.setPoint(point); pointer.setPressure(pressure); - pointer.setUp(action == MotionEvent.ACTION_UP); int source; - int pointerCount = pointersState.update(pointerProperties, pointerCoords); if (pointerId == POINTER_ID_MOUSE || pointerId == POINTER_ID_VIRTUAL_MOUSE) { // real mouse event (forced by the client when --forward-on-click) pointerProperties[pointerIndex].toolType = MotionEvent.TOOL_TYPE_MOUSE; source = InputDevice.SOURCE_MOUSE; + pointer.setUp(buttons == 0); } else { // POINTER_ID_GENERIC_FINGER, POINTER_ID_VIRTUAL_FINGER or real touch from device pointerProperties[pointerIndex].toolType = MotionEvent.TOOL_TYPE_FINGER; source = InputDevice.SOURCE_TOUCHSCREEN; // Buttons must not be set for touch events buttons = 0; + pointer.setUp(action == MotionEvent.ACTION_UP); } + int pointerCount = pointersState.update(pointerProperties, pointerCoords); if (pointerCount == 1) { if (action == MotionEvent.ACTION_DOWN) { lastTouchDown = now; From 9b286ec8a7d014c2a91bbc6d24343f9496724521 Mon Sep 17 00:00:00 2001 From: Simon Chan <1330321+yume-chan@users.noreply.github.com> Date: Sun, 29 Jan 2023 23:08:22 +0100 Subject: [PATCH 1295/2244] Inject additional ACTION_BUTTON_* events for mouse On mouse click events: - the first button pressed must first generate ACTION_DOWN; - all button pressed (including the first one) must generate ACTION_BUTTON_PRESS; - all button released (including the last one) must generate ACTION_BUTTON_RELEASE; - the last button released must in addition generate ACTION_UP. Otherwise, Chrome does not work properly. Fixes #3635 Signed-off-by: Romain Vimont --- .../com/genymobile/scrcpy/Controller.java | 62 ++++++++++++++++++- .../scrcpy/wrappers/InputManager.java | 20 ++++++ 2 files changed, 80 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 bff01d9f..b0b0c787 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -1,5 +1,7 @@ package com.genymobile.scrcpy; +import com.genymobile.scrcpy.wrappers.InputManager; + import android.os.Build; import android.os.SystemClock; import android.view.InputDevice; @@ -99,7 +101,7 @@ public class Controller { break; case ControlMessage.TYPE_INJECT_TOUCH_EVENT: if (device.supportsInputEvents()) { - injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getButtons()); + injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getActionButton(), msg.getButtons()); } break; case ControlMessage.TYPE_INJECT_SCROLL_EVENT: @@ -179,7 +181,7 @@ public class Controller { return successCount; } - private boolean injectTouch(int action, long pointerId, Position position, float pressure, int buttons) { + private boolean injectTouch(int action, long pointerId, Position position, float pressure, int actionButton, int buttons) { long now = SystemClock.uptimeMillis(); Point point = device.getPhysicalPoint(position); @@ -226,6 +228,62 @@ public class Controller { } } + /* If the input device is a mouse (on API >= 23): + * - the first button pressed must first generate ACTION_DOWN; + * - all button pressed (including the first one) must generate ACTION_BUTTON_PRESS; + * - all button released (including the last one) must generate ACTION_BUTTON_RELEASE; + * - the last button released must in addition generate ACTION_UP. + * + * Otherwise, Chrome does not work properly: + */ + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && source == InputDevice.SOURCE_MOUSE) { + if (action == MotionEvent.ACTION_DOWN) { + if (actionButton == buttons) { + // First button pressed: ACTION_DOWN + MotionEvent downEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_DOWN, pointerCount, pointerProperties, + pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0); + if (!device.injectEvent(downEvent, Device.INJECT_MODE_ASYNC)) { + return false; + } + } + + // Any button pressed: ACTION_BUTTON_PRESS + MotionEvent pressEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_BUTTON_PRESS, pointerCount, pointerProperties, + pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0); + if (!InputManager.setActionButton(pressEvent, actionButton)) { + return false; + } + if (!device.injectEvent(pressEvent, Device.INJECT_MODE_ASYNC)) { + return false; + } + + return true; + } + + if (action == MotionEvent.ACTION_UP) { + // Any button released: ACTION_BUTTON_RELEASE + MotionEvent releaseEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_BUTTON_RELEASE, pointerCount, pointerProperties, + pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0); + if (!InputManager.setActionButton(releaseEvent, actionButton)) { + return false; + } + if (!device.injectEvent(releaseEvent, Device.INJECT_MODE_ASYNC)) { + return false; + } + + if (buttons == 0) { + // Last button released: ACTION_UP + MotionEvent upEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_UP, pointerCount, pointerProperties, + pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0); + if (!device.injectEvent(upEvent, Device.INJECT_MODE_ASYNC)) { + return false; + } + } + + return true; + } + } + MotionEvent event = MotionEvent .obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0); 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 38e96d45..32bf4252 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java @@ -3,6 +3,7 @@ package com.genymobile.scrcpy.wrappers; import com.genymobile.scrcpy.Ln; import android.view.InputEvent; +import android.view.MotionEvent; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -17,6 +18,7 @@ public final class InputManager { private Method injectInputEventMethod; private static Method setDisplayIdMethod; + private static Method setActionButtonMethod; public InputManager(android.hardware.input.InputManager manager) { this.manager = manager; @@ -56,4 +58,22 @@ public final class InputManager { return false; } } + + private static Method getSetActionButtonMethod() throws NoSuchMethodException { + if (setActionButtonMethod == null) { + setActionButtonMethod = MotionEvent.class.getMethod("setActionButton", int.class); + } + return setActionButtonMethod; + } + + public static boolean setActionButton(MotionEvent motionEvent, int actionButton) { + try { + Method method = getSetActionButtonMethod(); + method.invoke(motionEvent, actionButton); + return true; + } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + Ln.e("Cannot set action button on MotionEvent", e); + return false; + } + } } From 6a07e3d470d9b849676a54406fceb44554b68557 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 3 Feb 2023 12:30:00 +0100 Subject: [PATCH 1296/2244] Fix manpage formatting Only the option arguments must be underlined. --- app/scrcpy.1 | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 852d9d03..a828a239 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -26,7 +26,7 @@ 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[,...] +.BI "\-\-codec\-options " key\fR[:\fItype\fR]=\fIvalue\fR[,...] 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'. @@ -117,7 +117,7 @@ Inject computer clipboard text as a sequence of key events on Ctrl+v (like MOD+S This is a workaround for some devices not behaving as expected when setting the device clipboard programmatically. .TP -.BI "\-\-lock\-video\-orientation[=value] +\fB\-\-lock\-video\-orientation\fR[=\fIvalue\fR] Lock video orientation to \fIvalue\fR. Possible values are "unlocked", "initial" (locked to the initial orientation), 0, 1, 2 and 3. Natural device orientation is 0, and each increment adds a 90 degrees rotation counterclockwise. Default is "unlocked". @@ -199,7 +199,7 @@ It may only work over USB. See \fB\-\-hid\-keyboard\fR and \fB\-\-hid\-mouse\fR. .TP -.BI "\-p, \-\-port " port[:port] +.BI "\-p, \-\-port " port\fR[:\fIport\fR] Set the TCP port (range) used by the client to listen. Default is 27183:27199. @@ -260,7 +260,7 @@ Set the initial display rotation. Possibles values are 0, 1, 2 and 3. Each incre The device serial number. Mandatory only if several devices are connected to adb. .TP -.BI "\-\-shortcut\-mod " key[+...]][,...] +.BI "\-\-shortcut\-mod " key\fR[+...]][,...] Specify the modifiers to use for scrcpy shortcuts. Possible keys are "lctrl", "rctrl", "lalt", "ralt", "lsuper" and "rsuper". A shortcut can consist in several keys, separated by '+'. Several shortcuts can be specified, separated by ','. @@ -270,7 +270,7 @@ For example, to use either LCtrl+LAlt or LSuper for scrcpy shortcuts, pass "lctr Default is "lalt,lsuper" (left-Alt or left-Super). .TP -.BI "\-\-tcpip[=ip[:port]] +.BI "\-\-tcpip\fR[=\fIip\fR[:\fIport\fR]] Configure and reconnect the device over TCP/IP. If a destination address is provided, then scrcpy connects to this address before starting. The device must listen on the given TCP port (default is 5555). From 4177de5880b4f93c03ce30b754b22c57d245ec3a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 1 Feb 2023 22:46:22 +0100 Subject: [PATCH 1297/2244] Do not expose controller threads The way the controller executes its events asynchronously is an implementation detail. --- .../com/genymobile/scrcpy/Controller.java | 25 +++++++++- .../scrcpy/DeviceMessageSender.java | 22 ++++++++- .../java/com/genymobile/scrcpy/Server.java | 46 +++---------------- 3 files changed, 52 insertions(+), 41 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index b0b0c787..6147e36a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -24,6 +24,8 @@ public class Controller { private static final ScheduledExecutorService EXECUTOR = Executors.newSingleThreadScheduledExecutor(); + private Thread thread; + private final Device device; private final DesktopConnection connection; private final DeviceMessageSender sender; @@ -62,7 +64,7 @@ public class Controller { } } - public void control() throws IOException { + private void control() throws IOException { // on start, power on the device if (powerOn && !Device.isScreenOn()) { device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER, Device.INJECT_MODE_ASYNC); @@ -82,6 +84,27 @@ public class Controller { } } + public void start() { + thread = new Thread(() -> { + try { + control(); + } catch (IOException e) { + // this is expected on close + Ln.d("Controller stopped"); + } + }); + thread.start(); + sender.start(); + } + + public void stop() { + if (thread != null) { + thread.interrupt(); + thread = null; + } + sender.stop(); + } + public DeviceMessageSender getSender() { return sender; } diff --git a/server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java b/server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java index 2630652a..7ec4ab41 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java +++ b/server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java @@ -6,6 +6,8 @@ public final class DeviceMessageSender { private final DesktopConnection connection; + private Thread thread; + private String clipboardText; private long ack; @@ -24,7 +26,7 @@ public final class DeviceMessageSender { notify(); } - public void loop() throws IOException, InterruptedException { + private void loop() throws IOException, InterruptedException { while (!Thread.currentThread().isInterrupted()) { String text; long sequence; @@ -49,4 +51,22 @@ public final class DeviceMessageSender { } } } + public void start() { + thread = new Thread(() -> { + try { + loop(); + } catch (IOException | InterruptedException e) { + // this is expected on close + Ln.d("Device message sender stopped"); + } + }); + thread.start(); + } + + public void stop() { + if (thread != null) { + thread.interrupt(); + thread = null; + } + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 4a371e5b..dc14d7c9 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -79,16 +79,13 @@ public final class Server { ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps(), codecOptions, options.getEncoderName(), options.getDownsizeOnError()); - Thread controllerThread = null; - Thread deviceMessageSenderThread = null; + Controller controller = null; if (control) { - final Controller controller = new Controller(device, connection, options.getClipboardAutosync(), options.getPowerOn()); + controller = new Controller(device, connection, options.getClipboardAutosync(), options.getPowerOn()); + controller.start(); - // asynchronous - controllerThread = startController(controller); - deviceMessageSenderThread = startDeviceMessageSender(controller.getSender()); - - device.setClipboardListener(text -> controller.getSender().pushClipboardText(text)); + final Controller controllerRef = controller; + device.setClipboardListener(text -> controllerRef.getSender().pushClipboardText(text)); } try { @@ -99,11 +96,8 @@ public final class Server { Ln.d("Screen streaming stopped"); } finally { initThread.interrupt(); - if (controllerThread != null) { - controllerThread.interrupt(); - } - if (deviceMessageSenderThread != null) { - deviceMessageSenderThread.interrupt(); + if (controller != null) { + controller.stop(); } } } @@ -115,32 +109,6 @@ public final class Server { return thread; } - private static Thread startController(final Controller controller) { - Thread thread = new Thread(() -> { - try { - controller.control(); - } catch (IOException e) { - // this is expected on close - Ln.d("Controller stopped"); - } - }); - thread.start(); - return thread; - } - - private static Thread startDeviceMessageSender(final DeviceMessageSender sender) { - Thread thread = new Thread(() -> { - try { - sender.loop(); - } catch (IOException | InterruptedException e) { - // this is expected on close - Ln.d("Device message sender stopped"); - } - }); - thread.start(); - return thread; - } - private static Options createOptions(String... args) { if (args.length < 1) { throw new IllegalArgumentException("Missing client version"); From bdbf1f4eb7dac0f00c5419a99f23cd9f05e39871 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 31 Jan 2023 22:54:34 +0100 Subject: [PATCH 1298/2244] Move Workarounds call Workarounds are not specific to the screen encoder. Co-authored-by: Simon Chan <1330321+yume-chan@users.noreply.github.com> --- .../java/com/genymobile/scrcpy/ScreenEncoder.java | 11 ----------- .../src/main/java/com/genymobile/scrcpy/Server.java | 7 +++++++ 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index f0384e2c..c7f886b4 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -67,17 +67,6 @@ public class ScreenEncoder implements Device.RotationListener { } public void streamScreen(Device device, FileDescriptor fd) throws IOException { - Workarounds.prepareMainLooper(); - if (Build.BRAND.equalsIgnoreCase("meizu")) { - // - // - Workarounds.fillAppInfo(); - } - - internalStreamScreen(device, fd); - } - - private void internalStreamScreen(Device device, FileDescriptor fd) throws IOException { MediaCodec codec = createCodec(encoderName); MediaFormat format = createFormat(bitRate, maxFps, codecOptions); IBinder display = createDisplay(); diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index dc14d7c9..06281223 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -71,6 +71,13 @@ public final class Server { boolean control = options.getControl(); boolean sendDummyByte = options.getSendDummyByte(); + Workarounds.prepareMainLooper(); + if (Build.BRAND.equalsIgnoreCase("meizu")) { + // + // + Workarounds.fillAppInfo(); + } + try (DesktopConnection connection = DesktopConnection.open(uid, tunnelForward, control, sendDummyByte)) { if (options.getSendDeviceMeta()) { Size videoSize = device.getScreenInfo().getVideoSize(); From 36d656e91f11718dfe43aceaa27f6a7439e93b7e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 2 Feb 2023 19:08:43 +0100 Subject: [PATCH 1299/2244] Improve workarounds call comments --- server/src/main/java/com/genymobile/scrcpy/Server.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 06281223..a4f17262 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -73,8 +73,14 @@ public final class Server { Workarounds.prepareMainLooper(); if (Build.BRAND.equalsIgnoreCase("meizu")) { - // - // + // Workarounds must be applied for Meizu phones: + // - + // - + // - + // + // But only apply when strictly necessary, since workarounds can cause other issues: + // - + // - Workarounds.fillAppInfo(); } From 1c82c3923d63985655686dd5884a7a9e9407619e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 1 Feb 2023 21:56:43 +0100 Subject: [PATCH 1300/2244] Compute relative PTS on the client-side The PTS received from MediaCodec are expressed relative to an arbitrary clock origin. We consider the PTS of the first frame to be 0, and the PTS of every other frame is relative to this first PTS (note that the PTS is only used for recording, it is ignored for mirroring). For simplicity, this relative PTS was computed on the server-side. To prepare support for multiple stream (video and audio), send the packet with its original PTS, and handle the PTS offset on the client-side (by the recorder). Since we can't know in advance which stream will produce the first packet with the lowest PTS (a packet received later on one stream may have a PTS lower than a packet received earlier on another stream), computing the PTS on the server-side would require unnecessary waiting. --- app/src/recorder.c | 15 +++++++++++++++ app/src/recorder.h | 2 ++ .../java/com/genymobile/scrcpy/ScreenEncoder.java | 6 +----- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/app/src/recorder.c b/app/src/recorder.c index b14b6050..455e1db1 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -11,6 +11,8 @@ /** Downcast packet_sink to recorder */ #define DOWNCAST(SINK) container_of(SINK, struct sc_recorder, packet_sink) +#define SC_PTS_ORIGIN_NONE UINT64_C(-1) + static const AVRational SCRCPY_TIME_BASE = {1, 1000000}; // timestamps in us static const AVOutputFormat * @@ -169,6 +171,18 @@ run_recorder(void *data) { sc_mutex_unlock(&recorder->mutex); + if (recorder->pts_origin == SC_PTS_ORIGIN_NONE + && rec->packet->pts != AV_NOPTS_VALUE) { + // First PTS received + recorder->pts_origin = rec->packet->pts; + } + + if (rec->packet->pts != AV_NOPTS_VALUE) { + // Set PTS relatve to the origin + rec->packet->pts -= recorder->pts_origin; + rec->packet->dts = rec->packet->pts; + } + // recorder->previous is only written from this thread, no need to lock struct sc_record_packet *previous = recorder->previous; recorder->previous = rec; @@ -243,6 +257,7 @@ sc_recorder_open(struct sc_recorder *recorder, const AVCodec *input_codec) { recorder->failed = false; recorder->header_written = false; recorder->previous = NULL; + recorder->pts_origin = SC_PTS_ORIGIN_NONE; const char *format_name = sc_recorder_get_format_name(recorder->format); assert(format_name); diff --git a/app/src/recorder.h b/app/src/recorder.h index 373278e6..a03c91d7 100644 --- a/app/src/recorder.h +++ b/app/src/recorder.h @@ -28,6 +28,8 @@ struct sc_recorder { struct sc_size declared_frame_size; bool header_written; + uint64_t pts_origin; + sc_thread thread; sc_mutex mutex; sc_cond queue_cond; diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index c7f886b4..07ea2d80 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -42,7 +42,6 @@ public class ScreenEncoder implements Device.RotationListener { private final int maxFps; private final boolean sendFrameMeta; private final boolean downsizeOnError; - private long ptsOrigin; private boolean firstFrameSent; private int consecutiveErrors; @@ -207,10 +206,7 @@ public class ScreenEncoder implements Device.RotationListener { if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) { pts = PACKET_FLAG_CONFIG; // non-media data packet } else { - if (ptsOrigin == 0) { - ptsOrigin = bufferInfo.presentationTimeUs; - } - pts = bufferInfo.presentationTimeUs - ptsOrigin; + pts = bufferInfo.presentationTimeUs; if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0) { pts |= PACKET_FLAG_KEY_FRAME; } From 3aac74e9e945c7fc71c5e54b3c53afe666707af7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 2 Feb 2023 19:16:23 +0100 Subject: [PATCH 1301/2244] Move variable assignment Computing eof flag is not necessary if rotation changed. --- server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java | 3 ++- 1 file changed, 2 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 07ea2d80..95f89ed5 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -169,12 +169,13 @@ public class ScreenEncoder implements Device.RotationListener { while (!consumeRotationChange() && !eof) { int outputBufferId = codec.dequeueOutputBuffer(bufferInfo, -1); - eof = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0; try { if (consumeRotationChange()) { // must restart encoding with new size break; } + + eof = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0; if (outputBufferId >= 0) { ByteBuffer codecBuffer = codec.getOutputBuffer(outputBufferId); From 87972e2022686b1176bfaf0c678e703856c2b027 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 2 Feb 2023 21:08:43 +0100 Subject: [PATCH 1302/2244] Extract video streaming to a separate class ScreenEncoder handled both capture/encoding and sending over the network. Move the streaming part to a separate VideoStreamer. --- .../com/genymobile/scrcpy/ScreenEncoder.java | 49 +++++------------- .../java/com/genymobile/scrcpy/Server.java | 7 +-- .../com/genymobile/scrcpy/VideoStreamer.java | 51 +++++++++++++++++++ 3 files changed, 67 insertions(+), 40 deletions(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/VideoStreamer.java diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index 95f89ed5..1d66c0d1 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -12,7 +12,6 @@ import android.os.IBinder; import android.os.SystemClock; import android.view.Surface; -import java.io.FileDescriptor; import java.io.IOException; import java.nio.ByteBuffer; import java.util.ArrayList; @@ -22,6 +21,10 @@ import java.util.concurrent.atomic.AtomicBoolean; public class ScreenEncoder implements Device.RotationListener { + public interface Callbacks { + void onPacket(ByteBuffer codecBuffer, MediaCodec.BufferInfo bufferInfo) throws IOException; + } + 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"; @@ -30,25 +33,18 @@ public class ScreenEncoder implements Device.RotationListener { private static final int[] MAX_SIZE_FALLBACK = {2560, 1920, 1600, 1280, 1024, 800}; private static final int MAX_CONSECUTIVE_ERRORS = 3; - private static final long PACKET_FLAG_CONFIG = 1L << 63; - private static final long PACKET_FLAG_KEY_FRAME = 1L << 62; - private final AtomicBoolean rotationChanged = new AtomicBoolean(); - private final ByteBuffer headerBuffer = ByteBuffer.allocate(12); private final String encoderName; private final List codecOptions; private final int bitRate; private final int maxFps; - private final boolean sendFrameMeta; private final boolean downsizeOnError; private boolean firstFrameSent; private int consecutiveErrors; - public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, List codecOptions, String encoderName, - boolean downsizeOnError) { - this.sendFrameMeta = sendFrameMeta; + public ScreenEncoder(int bitRate, int maxFps, List codecOptions, String encoderName, boolean downsizeOnError) { this.bitRate = bitRate; this.maxFps = maxFps; this.codecOptions = codecOptions; @@ -65,7 +61,7 @@ public class ScreenEncoder implements Device.RotationListener { return rotationChanged.getAndSet(false); } - public void streamScreen(Device device, FileDescriptor fd) throws IOException { + public void streamScreen(Device device, Callbacks callbacks) throws IOException { MediaCodec codec = createCodec(encoderName); MediaFormat format = createFormat(bitRate, maxFps, codecOptions); IBinder display = createDisplay(); @@ -94,7 +90,7 @@ public class ScreenEncoder implements Device.RotationListener { codec.start(); - alive = encode(codec, fd); + alive = encode(codec, callbacks); // do not call stop() on exception, it would trigger an IllegalStateException codec.stop(); } catch (IllegalStateException | IllegalArgumentException e) { @@ -163,7 +159,7 @@ public class ScreenEncoder implements Device.RotationListener { return 0; } - private boolean encode(MediaCodec codec, FileDescriptor fd) throws IOException { + private boolean encode(MediaCodec codec, Callbacks callbacks) throws IOException { boolean eof = false; MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); @@ -179,16 +175,14 @@ public class ScreenEncoder implements Device.RotationListener { if (outputBufferId >= 0) { ByteBuffer codecBuffer = codec.getOutputBuffer(outputBufferId); - if (sendFrameMeta) { - writeFrameMeta(fd, bufferInfo, codecBuffer.remaining()); - } - - IO.writeFully(fd, codecBuffer); - if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) == 0) { + boolean isConfig = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0; + if (!isConfig) { // If this is not a config packet, then it contains a frame firstFrameSent = true; consecutiveErrors = 0; } + + callbacks.onPacket(codecBuffer, bufferInfo); } } finally { if (outputBufferId >= 0) { @@ -200,25 +194,6 @@ public class ScreenEncoder implements Device.RotationListener { return !eof; } - private void writeFrameMeta(FileDescriptor fd, MediaCodec.BufferInfo bufferInfo, int packetSize) throws IOException { - headerBuffer.clear(); - - long pts; - if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) { - pts = PACKET_FLAG_CONFIG; // non-media data packet - } else { - pts = bufferInfo.presentationTimeUs; - if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0) { - pts |= PACKET_FLAG_KEY_FRAME; - } - } - - headerBuffer.putLong(pts); - headerBuffer.putInt(packetSize); - headerBuffer.flip(); - IO.writeFully(fd, headerBuffer); - } - private static MediaCodecInfo[] listEncoders() { List result = new ArrayList<>(); MediaCodecList list = new MediaCodecList(MediaCodecList.REGULAR_CODECS); diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index a4f17262..46612069 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -89,8 +89,8 @@ public final class Server { Size videoSize = device.getScreenInfo().getVideoSize(); connection.sendDeviceMeta(Device.getDeviceName(), videoSize.getWidth(), videoSize.getHeight()); } - ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps(), codecOptions, - options.getEncoderName(), options.getDownsizeOnError()); + ScreenEncoder screenEncoder = new ScreenEncoder(options.getBitRate(), options.getMaxFps(), codecOptions, options.getEncoderName(), + options.getDownsizeOnError()); Controller controller = null; if (control) { @@ -103,7 +103,8 @@ public final class Server { try { // synchronous - screenEncoder.streamScreen(device, connection.getVideoFd()); + VideoStreamer videoStreamer = new VideoStreamer(connection.getVideoFd(), options.getSendFrameMeta()); + screenEncoder.streamScreen(device, videoStreamer); } catch (IOException e) { // this is expected on close Ln.d("Screen streaming stopped"); diff --git a/server/src/main/java/com/genymobile/scrcpy/VideoStreamer.java b/server/src/main/java/com/genymobile/scrcpy/VideoStreamer.java new file mode 100644 index 00000000..77e51499 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/VideoStreamer.java @@ -0,0 +1,51 @@ +package com.genymobile.scrcpy; + +import android.media.MediaCodec; + +import java.io.FileDescriptor; +import java.io.IOException; +import java.nio.ByteBuffer; + +public final class VideoStreamer implements ScreenEncoder.Callbacks { + + private static final long PACKET_FLAG_CONFIG = 1L << 63; + private static final long PACKET_FLAG_KEY_FRAME = 1L << 62; + + private final FileDescriptor fd; + private final boolean sendFrameMeta; + + private final ByteBuffer headerBuffer = ByteBuffer.allocate(12); + + public VideoStreamer(FileDescriptor fd, boolean sendFrameMeta) { + this.fd = fd; + this.sendFrameMeta = sendFrameMeta; + } + + @Override + public void onPacket(ByteBuffer codecBuffer, MediaCodec.BufferInfo bufferInfo) throws IOException { + if (sendFrameMeta) { + writeFrameMeta(fd, bufferInfo, codecBuffer.remaining()); + } + + IO.writeFully(fd, codecBuffer); + } + + private void writeFrameMeta(FileDescriptor fd, MediaCodec.BufferInfo bufferInfo, int packetSize) throws IOException { + headerBuffer.clear(); + + long ptsAndFlags; + if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) { + ptsAndFlags = PACKET_FLAG_CONFIG; // non-media data packet + } else { + ptsAndFlags = bufferInfo.presentationTimeUs; + if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0) { + ptsAndFlags |= PACKET_FLAG_KEY_FRAME; + } + } + + headerBuffer.putLong(ptsAndFlags); + headerBuffer.putInt(packetSize); + headerBuffer.flip(); + IO.writeFully(fd, headerBuffer); + } +} From f70f6cdd3e5ffee2a687f47e8227544788932701 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 2 Feb 2023 21:32:17 +0100 Subject: [PATCH 1303/2244] Simplify server info initialization Use sc_read16be() to read 16-bit integer fields. --- app/src/server.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/server.c b/app/src/server.c index 9384ce64..05e040cd 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -8,6 +8,7 @@ #include #include "adb/adb.h" +#include "util/binary.h" #include "util/file.h" #include "util/log.h" #include "util/net_intr.h" @@ -398,10 +399,9 @@ device_read_info(struct sc_intr *intr, sc_socket device_socket, buf[SC_DEVICE_NAME_FIELD_LENGTH - 1] = '\0'; memcpy(info->device_name, (char *) buf, sizeof(info->device_name)); - info->frame_size.width = (buf[SC_DEVICE_NAME_FIELD_LENGTH] << 8) - | buf[SC_DEVICE_NAME_FIELD_LENGTH + 1]; - info->frame_size.height = (buf[SC_DEVICE_NAME_FIELD_LENGTH + 2] << 8) - | buf[SC_DEVICE_NAME_FIELD_LENGTH + 3]; + unsigned char *fields = &buf[SC_DEVICE_NAME_FIELD_LENGTH]; + info->frame_size.width = sc_read16be(fields); + info->frame_size.height = sc_read16be(&fields[2]); return true; } From 3e517cd40eb50f8afb5137f39b0676fead58902a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 3 Feb 2023 12:35:37 +0100 Subject: [PATCH 1304/2244] Add option to select video codec Introduce the selection mechanism. Alternative codecs will be added in further commits. PR #3713 --- README.md | 18 +++++++--- app/scrcpy.1 | 4 +++ app/src/cli.c | 22 ++++++++++++ app/src/demuxer.c | 29 ++++++++++++++-- app/src/options.c | 1 + app/src/options.h | 5 +++ app/src/scrcpy.c | 1 + app/src/server.c | 13 +++++++ app/src/server.h | 1 + .../java/com/genymobile/scrcpy/Options.java | 20 +++++++++++ .../com/genymobile/scrcpy/ScreenEncoder.java | 22 ++++++------ .../java/com/genymobile/scrcpy/Server.java | 20 +++++++++-- .../com/genymobile/scrcpy/VideoCodec.java | 34 +++++++++++++++++++ .../com/genymobile/scrcpy/VideoStreamer.java | 7 ++++ 14 files changed, 179 insertions(+), 18 deletions(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/VideoCodec.java diff --git a/README.md b/README.md index b2a767fd..facdfc81 100644 --- a/README.md +++ b/README.md @@ -252,10 +252,19 @@ This affects recording orientation. The [window may also be rotated](#rotation) independently. -#### Encoder +#### Codec -Some devices have more than one encoder, and some of them may cause issues or -crash. It is possible to select a different encoder: +The video codec can be selected: + +```bash +scrcpy --codec=h264 # default +``` + + +##### Encoder + +Some devices have more than one encoder for a specific codec, and some of them +may cause issues or crash. It is possible to select a different encoder: ```bash scrcpy --encoder=OMX.qcom.video.encoder.avc @@ -265,7 +274,8 @@ To list the available encoders, you can pass an invalid encoder name; the error will give the available encoders: ```bash -scrcpy --encoder=_ +scrcpy --encoder=_ # for the default codec +scrcpy --codec=h264 --encoder=_ # for a specific codec ``` ### Capture diff --git a/app/scrcpy.1 b/app/scrcpy.1 index a828a239..c7ddeefb 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -25,6 +25,10 @@ Encode the video at the given bit\-rate, expressed in bits/s. Unit suffixes are Default is 8000000. +.TP +.BI "\-\-codec " name +Select a video codec (h264). + .TP .BI "\-\-codec\-options " key\fR[:\fItype\fR]=\fIvalue\fR[,...] Set a list of comma-separated key:type=value options for the device encoder. diff --git a/app/src/cli.c b/app/src/cli.c index 538dd3e7..ee65e4c0 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -57,6 +57,7 @@ #define OPT_NO_CLEANUP 1037 #define OPT_PRINT_FPS 1038 #define OPT_NO_POWER_ON 1039 +#define OPT_CODEC 1040 struct sc_option { char shortopt; @@ -105,6 +106,12 @@ static const struct sc_option options[] = { "Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n" "Default is " STR(DEFAULT_BIT_RATE) ".", }, + { + .longopt_id = OPT_CODEC, + .longopt = "codec", + .argdesc = "name", + .text = "Select a video codec (h264).", + }, { .longopt_id = OPT_CODEC_OPTIONS, .longopt = "codec-options", @@ -1377,6 +1384,16 @@ guess_record_format(const char *filename) { return 0; } +static bool +parse_codec(const char *optarg, enum sc_codec *codec) { + if (!strcmp(optarg, "h264")) { + *codec = SC_CODEC_H264; + return true; + } + LOGE("Unsupported codec: %s (expected h264)", optarg); + return false; +} + static bool parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], const char *optstring, const struct option *longopts) { @@ -1610,6 +1627,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], case OPT_PRINT_FPS: opts->start_fps_counter = true; break; + case OPT_CODEC: + if (!parse_codec(optarg, &opts->codec)) { + return false; + } + break; case OPT_OTG: #ifdef HAVE_USB opts->otg = true; diff --git a/app/src/demuxer.c b/app/src/demuxer.c index c88af220..ad0b40a2 100644 --- a/app/src/demuxer.c +++ b/app/src/demuxer.c @@ -17,6 +17,25 @@ #define SC_PACKET_PTS_MASK (SC_PACKET_FLAG_KEY_FRAME - 1) +static enum AVCodecID +sc_demuxer_recv_codec_id(struct sc_demuxer *demuxer) { + uint8_t data[4]; + ssize_t r = net_recv_all(demuxer->socket, data, 4); + if (r < 4) { + return false; + } + +#define SC_CODEC_ID_H264 UINT32_C(0x68323634) // "h264" in ASCII + uint32_t codec_id = sc_read32be(data); + switch (codec_id) { + case SC_CODEC_ID_H264: + return AV_CODEC_ID_H264; + default: + LOGE("Unknown codec id 0x%08" PRIx32, codec_id); + return AV_CODEC_ID_NONE; + } +} + static bool sc_demuxer_recv_packet(struct sc_demuxer *demuxer, AVPacket *packet) { // The video stream contains raw packets, without time information. When we @@ -171,7 +190,13 @@ static int run_demuxer(void *data) { struct sc_demuxer *demuxer = data; - const AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264); + enum AVCodecID codec_id = sc_demuxer_recv_codec_id(demuxer); + if (codec_id == AV_CODEC_ID_NONE) { + // Error already logged + goto end; + } + + const AVCodec *codec = avcodec_find_decoder(codec_id); if (!codec) { LOGE("H.264 decoder not found"); goto end; @@ -188,7 +213,7 @@ run_demuxer(void *data) { goto finally_free_codec_ctx; } - demuxer->parser = av_parser_init(AV_CODEC_ID_H264); + demuxer->parser = av_parser_init(codec_id); if (!demuxer->parser) { LOGE("Could not initialize parser"); goto finally_close_sinks; diff --git a/app/src/options.c b/app/src/options.c index 8b2624d9..525795ae 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -13,6 +13,7 @@ const struct scrcpy_options scrcpy_options_default = { .v4l2_device = NULL, #endif .log_level = SC_LOG_LEVEL_INFO, + .codec = SC_CODEC_H264, .record_format = SC_RECORD_FORMAT_AUTO, .keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_INJECT, .port_range = { diff --git a/app/src/options.h b/app/src/options.h index 7e542c06..18006e42 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -23,6 +23,10 @@ enum sc_record_format { SC_RECORD_FORMAT_MKV, }; +enum sc_codec { + SC_CODEC_H264, +}; + enum sc_lock_video_orientation { SC_LOCK_VIDEO_ORIENTATION_UNLOCKED = -1, // lock the current orientation when scrcpy starts @@ -93,6 +97,7 @@ struct scrcpy_options { const char *v4l2_device; #endif enum sc_log_level log_level; + enum sc_codec codec; enum sc_record_format record_format; enum sc_keyboard_input_mode keyboard_input_mode; enum sc_mouse_input_mode mouse_input_mode; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 14688471..e85536e6 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -315,6 +315,7 @@ scrcpy(struct scrcpy_options *options) { .select_usb = options->select_usb, .select_tcpip = options->select_tcpip, .log_level = options->log_level, + .codec = options->codec, .crop = options->crop, .port_range = options->port_range, .tunnel_host = options->tunnel_host, diff --git a/app/src/server.c b/app/src/server.c index 05e040cd..aaa0ffb1 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -156,6 +156,16 @@ sc_server_sleep(struct sc_server *server, sc_tick deadline) { return !stopped; } +static const char * +sc_server_get_codec_name(enum sc_codec codec) { + switch (codec) { + case SC_CODEC_H264: + return "h264"; + default: + return NULL; + } +} + static sc_pid execute_server(struct sc_server *server, const struct sc_server_params *params) { @@ -203,6 +213,9 @@ execute_server(struct sc_server *server, ADD_PARAM("log_level=%s", log_level_to_server_string(params->log_level)); ADD_PARAM("bit_rate=%" PRIu32, params->bit_rate); + if (params->codec != SC_CODEC_H264) { + ADD_PARAM("codec=%s", sc_server_get_codec_name(params->codec)); + } if (params->max_size) { ADD_PARAM("max_size=%" PRIu16, params->max_size); } diff --git a/app/src/server.h b/app/src/server.h index e0f2c225..950ad532 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -25,6 +25,7 @@ struct sc_server_params { uint32_t uid; const char *req_serial; enum sc_log_level log_level; + enum sc_codec codec; const char *crop; const char *codec_options; const char *encoder_name; diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 171d6661..d43a60a2 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -5,9 +5,12 @@ import android.graphics.Rect; import java.util.List; public class Options { + private static final String VIDEO_CODEC_H264 = "h264"; + private Ln.Level logLevel = Ln.Level.DEBUG; private int uid = -1; // 31-bit non-negative value, or -1 private int maxSize; + private VideoCodec codec = VideoCodec.H264; private int bitRate = 8000000; private int maxFps; private int lockVideoOrientation = -1; @@ -29,6 +32,7 @@ public class Options { private boolean sendDeviceMeta = true; // send device name and size private boolean sendFrameMeta = true; // send PTS so that the client may record properly private boolean sendDummyByte = true; // write a byte on start to detect connection issues + private boolean sendCodecId = true; // write the codec ID (4 bytes) before the stream public Ln.Level getLogLevel() { return logLevel; @@ -54,6 +58,14 @@ public class Options { this.maxSize = maxSize; } + public VideoCodec getCodec() { + return codec; + } + + public void setCodec(VideoCodec codec) { + this.codec = codec; + } + public int getBitRate() { return bitRate; } @@ -205,4 +217,12 @@ public class Options { public void setSendDummyByte(boolean sendDummyByte) { this.sendDummyByte = sendDummyByte; } + + public boolean getSendCodecId() { + return sendCodecId; + } + + public void setSendCodecId(boolean sendCodecId) { + this.sendCodecId = sendCodecId; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index 1d66c0d1..fed6f6c3 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -35,6 +35,7 @@ public class ScreenEncoder implements Device.RotationListener { private final AtomicBoolean rotationChanged = new AtomicBoolean(); + private final String videoMimeType; private final String encoderName; private final List codecOptions; private final int bitRate; @@ -44,7 +45,8 @@ public class ScreenEncoder implements Device.RotationListener { private boolean firstFrameSent; private int consecutiveErrors; - public ScreenEncoder(int bitRate, int maxFps, List codecOptions, String encoderName, boolean downsizeOnError) { + public ScreenEncoder(String videoMimeType, int bitRate, int maxFps, List codecOptions, String encoderName, boolean downsizeOnError) { + this.videoMimeType = videoMimeType; this.bitRate = bitRate; this.maxFps = maxFps; this.codecOptions = codecOptions; @@ -62,8 +64,8 @@ public class ScreenEncoder implements Device.RotationListener { } public void streamScreen(Device device, Callbacks callbacks) throws IOException { - MediaCodec codec = createCodec(encoderName); - MediaFormat format = createFormat(bitRate, maxFps, codecOptions); + MediaCodec codec = createCodec(videoMimeType, encoderName); + MediaFormat format = createFormat(videoMimeType, bitRate, maxFps, codecOptions); IBinder display = createDisplay(); device.setRotationListener(this); boolean alive; @@ -194,28 +196,28 @@ public class ScreenEncoder implements Device.RotationListener { return !eof; } - private static MediaCodecInfo[] listEncoders() { + private static MediaCodecInfo[] listEncoders(String videoMimeType) { List result = new ArrayList<>(); MediaCodecList list = new MediaCodecList(MediaCodecList.REGULAR_CODECS); for (MediaCodecInfo codecInfo : list.getCodecInfos()) { - if (codecInfo.isEncoder() && Arrays.asList(codecInfo.getSupportedTypes()).contains(MediaFormat.MIMETYPE_VIDEO_AVC)) { + if (codecInfo.isEncoder() && Arrays.asList(codecInfo.getSupportedTypes()).contains(videoMimeType)) { result.add(codecInfo); } } return result.toArray(new MediaCodecInfo[result.size()]); } - private static MediaCodec createCodec(String encoderName) throws IOException { + private static MediaCodec createCodec(String videoMimeType, String encoderName) throws IOException { if (encoderName != null) { Ln.d("Creating encoder by name: '" + encoderName + "'"); try { return MediaCodec.createByCodecName(encoderName); } catch (IllegalArgumentException e) { - MediaCodecInfo[] encoders = listEncoders(); + MediaCodecInfo[] encoders = listEncoders(videoMimeType); throw new InvalidEncoderException(encoderName, encoders); } } - MediaCodec codec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC); + MediaCodec codec = MediaCodec.createEncoderByType(videoMimeType); Ln.d("Using encoder: '" + codec.getName() + "'"); return codec; } @@ -237,9 +239,9 @@ public class ScreenEncoder implements Device.RotationListener { Ln.d("Codec option set: " + key + " (" + value.getClass().getSimpleName() + ") = " + value); } - private static MediaFormat createFormat(int bitRate, int maxFps, List codecOptions) { + private static MediaFormat createFormat(String videoMimeType, int bitRate, int maxFps, List codecOptions) { MediaFormat format = new MediaFormat(); - format.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_VIDEO_AVC); + format.setString(MediaFormat.KEY_MIME, videoMimeType); 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); diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 46612069..81c3e813 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -85,12 +85,13 @@ public final class Server { } try (DesktopConnection connection = DesktopConnection.open(uid, tunnelForward, control, sendDummyByte)) { + VideoCodec codec = options.getCodec(); if (options.getSendDeviceMeta()) { Size videoSize = device.getScreenInfo().getVideoSize(); connection.sendDeviceMeta(Device.getDeviceName(), videoSize.getWidth(), videoSize.getHeight()); } - ScreenEncoder screenEncoder = new ScreenEncoder(options.getBitRate(), options.getMaxFps(), codecOptions, options.getEncoderName(), - options.getDownsizeOnError()); + ScreenEncoder screenEncoder = new ScreenEncoder(codec.getMimeType(), options.getBitRate(), options.getMaxFps(), codecOptions, + options.getEncoderName(), options.getDownsizeOnError()); Controller controller = null; if (control) { @@ -104,6 +105,9 @@ public final class Server { try { // synchronous VideoStreamer videoStreamer = new VideoStreamer(connection.getVideoFd(), options.getSendFrameMeta()); + if (options.getSendCodecId()) { + videoStreamer.writeHeader(codec.getId()); + } screenEncoder.streamScreen(device, videoStreamer); } catch (IOException e) { // this is expected on close @@ -156,6 +160,13 @@ public final class Server { Ln.Level level = Ln.Level.valueOf(value.toUpperCase(Locale.ENGLISH)); options.setLogLevel(level); break; + case "codec": + VideoCodec codec = VideoCodec.findByName(value); + if (codec == null) { + throw new IllegalArgumentException("Video codec " + value + " not supported"); + } + options.setCodec(codec); + break; case "max_size": int maxSize = Integer.parseInt(value) & ~7; // multiple of 8 options.setMaxSize(maxSize); @@ -237,12 +248,17 @@ public final class Server { boolean sendDummyByte = Boolean.parseBoolean(value); options.setSendDummyByte(sendDummyByte); break; + case "send_codec_id": + boolean sendCodecId = Boolean.parseBoolean(value); + options.setSendCodecId(sendCodecId); + break; case "raw_video_stream": boolean rawVideoStream = Boolean.parseBoolean(value); if (rawVideoStream) { options.setSendDeviceMeta(false); options.setSendFrameMeta(false); options.setSendDummyByte(false); + options.setSendCodecId(false); } break; default: diff --git a/server/src/main/java/com/genymobile/scrcpy/VideoCodec.java b/server/src/main/java/com/genymobile/scrcpy/VideoCodec.java new file mode 100644 index 00000000..e5edfcf2 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/VideoCodec.java @@ -0,0 +1,34 @@ +package com.genymobile.scrcpy; + +import android.media.MediaFormat; + +public enum VideoCodec { + H264(0x68_32_36_34, "h264", MediaFormat.MIMETYPE_VIDEO_AVC); + + private final int id; // 4-byte ASCII representation of the name + private final String name; + private final String mimeType; + + VideoCodec(int id, String name, String mimeType) { + this.id = id; + this.name = name; + this.mimeType = mimeType; + } + + public int getId() { + return id; + } + + public String getMimeType() { + return mimeType; + } + + public static VideoCodec findByName(String name) { + for (VideoCodec codec : values()) { + if (codec.name.equals(name)) { + return codec; + } + } + return null; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/VideoStreamer.java b/server/src/main/java/com/genymobile/scrcpy/VideoStreamer.java index 77e51499..943c641d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/VideoStreamer.java +++ b/server/src/main/java/com/genymobile/scrcpy/VideoStreamer.java @@ -21,6 +21,13 @@ public final class VideoStreamer implements ScreenEncoder.Callbacks { this.sendFrameMeta = sendFrameMeta; } + public void writeHeader(int codecId) throws IOException { + ByteBuffer buffer = ByteBuffer.allocate(4); + buffer.putInt(codecId); + buffer.flip(); + IO.writeFully(fd, buffer); + } + @Override public void onPacket(ByteBuffer codecBuffer, MediaCodec.BufferInfo bufferInfo) throws IOException { if (sendFrameMeta) { From 4342c5637d74de2a6af5a6c83b81060bdf198af4 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 3 Feb 2023 12:40:55 +0100 Subject: [PATCH 1305/2244] Add support for H265 Add option --codec=h265. PR #3713 Fixes #3092 --- README.md | 6 ++++-- app/scrcpy.1 | 2 +- app/src/cli.c | 8 ++++++-- app/src/demuxer.c | 3 +++ app/src/options.h | 1 + app/src/server.c | 2 ++ .../src/main/java/com/genymobile/scrcpy/VideoCodec.java | 3 ++- 7 files changed, 19 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index facdfc81..6218bee4 100644 --- a/README.md +++ b/README.md @@ -254,10 +254,12 @@ The [window may also be rotated](#rotation) independently. #### Codec -The video codec can be selected: +The video codec can be selected. The possible values are `h264` (default) and +`h265`: ```bash scrcpy --codec=h264 # default +scrcpy --codec=h265 ``` @@ -275,7 +277,7 @@ error will give the available encoders: ```bash scrcpy --encoder=_ # for the default codec -scrcpy --codec=h264 --encoder=_ # for a specific codec +scrcpy --codec=h265 --encoder=_ # for a specific codec ``` ### Capture diff --git a/app/scrcpy.1 b/app/scrcpy.1 index c7ddeefb..be607173 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -27,7 +27,7 @@ Default is 8000000. .TP .BI "\-\-codec " name -Select a video codec (h264). +Select a video codec (h264 or h265). .TP .BI "\-\-codec\-options " key\fR[:\fItype\fR]=\fIvalue\fR[,...] diff --git a/app/src/cli.c b/app/src/cli.c index ee65e4c0..76d59f38 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -110,7 +110,7 @@ static const struct sc_option options[] = { .longopt_id = OPT_CODEC, .longopt = "codec", .argdesc = "name", - .text = "Select a video codec (h264).", + .text = "Select a video codec (h264 or h265).", }, { .longopt_id = OPT_CODEC_OPTIONS, @@ -1390,7 +1390,11 @@ parse_codec(const char *optarg, enum sc_codec *codec) { *codec = SC_CODEC_H264; return true; } - LOGE("Unsupported codec: %s (expected h264)", optarg); + if (!strcmp(optarg, "h265")) { + *codec = SC_CODEC_H265; + return true; + } + LOGE("Unsupported codec: %s (expected h264 or h265)", optarg); return false; } diff --git a/app/src/demuxer.c b/app/src/demuxer.c index ad0b40a2..119d4065 100644 --- a/app/src/demuxer.c +++ b/app/src/demuxer.c @@ -26,10 +26,13 @@ sc_demuxer_recv_codec_id(struct sc_demuxer *demuxer) { } #define SC_CODEC_ID_H264 UINT32_C(0x68323634) // "h264" in ASCII +#define SC_CODEC_ID_H265 UINT32_C(0x68323635) // "h265" in ASCII uint32_t codec_id = sc_read32be(data); switch (codec_id) { case SC_CODEC_ID_H264: return AV_CODEC_ID_H264; + case SC_CODEC_ID_H265: + return AV_CODEC_ID_HEVC; default: LOGE("Unknown codec id 0x%08" PRIx32, codec_id); return AV_CODEC_ID_NONE; diff --git a/app/src/options.h b/app/src/options.h index 18006e42..b3c39594 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -25,6 +25,7 @@ enum sc_record_format { enum sc_codec { SC_CODEC_H264, + SC_CODEC_H265, }; enum sc_lock_video_orientation { diff --git a/app/src/server.c b/app/src/server.c index aaa0ffb1..a63504bb 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -161,6 +161,8 @@ sc_server_get_codec_name(enum sc_codec codec) { switch (codec) { case SC_CODEC_H264: return "h264"; + case SC_CODEC_H265: + return "h265"; default: return NULL; } diff --git a/server/src/main/java/com/genymobile/scrcpy/VideoCodec.java b/server/src/main/java/com/genymobile/scrcpy/VideoCodec.java index e5edfcf2..048765f7 100644 --- a/server/src/main/java/com/genymobile/scrcpy/VideoCodec.java +++ b/server/src/main/java/com/genymobile/scrcpy/VideoCodec.java @@ -3,7 +3,8 @@ package com.genymobile.scrcpy; import android.media.MediaFormat; public enum VideoCodec { - H264(0x68_32_36_34, "h264", MediaFormat.MIMETYPE_VIDEO_AVC); + H264(0x68_32_36_34, "h264", MediaFormat.MIMETYPE_VIDEO_AVC), + H265(0x68_32_36_35, "h265", MediaFormat.MIMETYPE_VIDEO_HEVC); private final int id; // 4-byte ASCII representation of the name private final String name; From d2dce5103832d6a9fc29f56a9795c22e44ceafa1 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 3 Feb 2023 12:42:22 +0100 Subject: [PATCH 1306/2244] Add support for AV1 Add option --codec=av1. PR #3713 --- README.md | 5 +++-- app/scrcpy.1 | 2 +- app/src/cli.c | 15 +++++++++++++-- app/src/demuxer.c | 3 +++ app/src/options.h | 1 + app/src/server.c | 2 ++ .../java/com/genymobile/scrcpy/VideoCodec.java | 3 ++- 7 files changed, 25 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 6218bee4..a19aec0f 100644 --- a/README.md +++ b/README.md @@ -254,12 +254,13 @@ The [window may also be rotated](#rotation) independently. #### Codec -The video codec can be selected. The possible values are `h264` (default) and -`h265`: +The video codec can be selected. The possible values are `h264` (default), +`h265` and `av1`: ```bash scrcpy --codec=h264 # default scrcpy --codec=h265 +scrcpy --codec=av1 ``` diff --git a/app/scrcpy.1 b/app/scrcpy.1 index be607173..18c70d65 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -27,7 +27,7 @@ Default is 8000000. .TP .BI "\-\-codec " name -Select a video codec (h264 or h265). +Select a video codec (h264, h265 or av1). .TP .BI "\-\-codec\-options " key\fR[:\fItype\fR]=\fIvalue\fR[,...] diff --git a/app/src/cli.c b/app/src/cli.c index 76d59f38..27b0ddef 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -110,7 +110,7 @@ static const struct sc_option options[] = { .longopt_id = OPT_CODEC, .longopt = "codec", .argdesc = "name", - .text = "Select a video codec (h264 or h265).", + .text = "Select a video codec (h264, h265 or av1).", }, { .longopt_id = OPT_CODEC_OPTIONS, @@ -1394,7 +1394,11 @@ parse_codec(const char *optarg, enum sc_codec *codec) { *codec = SC_CODEC_H265; return true; } - LOGE("Unsupported codec: %s (expected h264 or h265)", optarg); + if (!strcmp(optarg, "av1")) { + *codec = SC_CODEC_AV1; + return true; + } + LOGE("Unsupported codec: %s (expected h264, h265 or av1)", optarg); return false; } @@ -1744,6 +1748,13 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } } + if (opts->record_format == SC_RECORD_FORMAT_MP4 + && opts->codec == SC_CODEC_AV1) { + LOGE("Could not mux AV1 stream into MP4 container " + "(record to mkv or select another video codec)"); + return false; + } + if (!opts->control) { if (opts->turn_screen_off) { LOGE("Could not request to turn screen off if control is disabled"); diff --git a/app/src/demuxer.c b/app/src/demuxer.c index 119d4065..6c44a558 100644 --- a/app/src/demuxer.c +++ b/app/src/demuxer.c @@ -27,12 +27,15 @@ sc_demuxer_recv_codec_id(struct sc_demuxer *demuxer) { #define SC_CODEC_ID_H264 UINT32_C(0x68323634) // "h264" in ASCII #define SC_CODEC_ID_H265 UINT32_C(0x68323635) // "h265" in ASCII +#define SC_CODEC_ID_AV1 UINT32_C(0x00617631) // "av1" in ASCII uint32_t codec_id = sc_read32be(data); switch (codec_id) { case SC_CODEC_ID_H264: return AV_CODEC_ID_H264; case SC_CODEC_ID_H265: return AV_CODEC_ID_HEVC; + case SC_CODEC_ID_AV1: + return AV_CODEC_ID_AV1; default: LOGE("Unknown codec id 0x%08" PRIx32, codec_id); return AV_CODEC_ID_NONE; diff --git a/app/src/options.h b/app/src/options.h index b3c39594..b9d237e0 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -26,6 +26,7 @@ enum sc_record_format { enum sc_codec { SC_CODEC_H264, SC_CODEC_H265, + SC_CODEC_AV1, }; enum sc_lock_video_orientation { diff --git a/app/src/server.c b/app/src/server.c index a63504bb..74d318c8 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -163,6 +163,8 @@ sc_server_get_codec_name(enum sc_codec codec) { return "h264"; case SC_CODEC_H265: return "h265"; + case SC_CODEC_AV1: + return "av1"; default: return NULL; } diff --git a/server/src/main/java/com/genymobile/scrcpy/VideoCodec.java b/server/src/main/java/com/genymobile/scrcpy/VideoCodec.java index 048765f7..809d7dbc 100644 --- a/server/src/main/java/com/genymobile/scrcpy/VideoCodec.java +++ b/server/src/main/java/com/genymobile/scrcpy/VideoCodec.java @@ -4,7 +4,8 @@ import android.media.MediaFormat; public enum VideoCodec { H264(0x68_32_36_34, "h264", MediaFormat.MIMETYPE_VIDEO_AVC), - H265(0x68_32_36_35, "h265", MediaFormat.MIMETYPE_VIDEO_HEVC); + H265(0x68_32_36_35, "h265", MediaFormat.MIMETYPE_VIDEO_HEVC), + AV1(0x00_61_76_31, "av1", MediaFormat.MIMETYPE_VIDEO_AV1); private final int id; // 4-byte ASCII representation of the name private final String name; From f2dee20a207b9906574d9b3d62a0ab2585dade20 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 5 Feb 2023 09:50:51 +0100 Subject: [PATCH 1307/2244] Set power mode on all physical displays Android 10 and above support multiple physical displays. Apply power mode to all of them. Fixes #3716 --- .../java/com/genymobile/scrcpy/Device.java | 20 ++++++++++ .../scrcpy/wrappers/SurfaceControl.java | 37 ++++++++++++++++++- 2 files changed, 56 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 97b25b22..30e64fd7 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -277,6 +277,26 @@ public final class Device { * @param mode one of the {@code POWER_MODE_*} constants */ public static boolean setScreenPowerMode(int mode) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + // Change the power mode for all physical displays + long[] physicalDisplayIds = SurfaceControl.getPhysicalDisplayIds(); + if (physicalDisplayIds == null) { + Ln.e("Could not get physical display ids"); + return false; + } + + boolean allOk = true; + for (long physicalDisplayId : physicalDisplayIds) { + IBinder binder = SurfaceControl.getPhysicalDisplayToken(physicalDisplayId); + boolean ok = SurfaceControl.setDisplayPowerMode(binder, mode); + if (!ok) { + allOk = false; + } + } + return allOk; + } + + // Older Android versions, only 1 display IBinder d = SurfaceControl.getBuiltInDisplay(); if (d == null) { Ln.e("Could not get built-in display"); 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 8fbb860b..595ee6d4 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java @@ -30,6 +30,8 @@ public final class SurfaceControl { private static Method getBuiltInDisplayMethod; private static Method setDisplayPowerModeMethod; + private static Method getPhysicalDisplayTokenMethod; + private static Method getPhysicalDisplayIdsMethod; private SurfaceControl() { // only static methods @@ -98,7 +100,6 @@ public final class SurfaceControl { } public static IBinder getBuiltInDisplay() { - try { Method method = getGetBuiltInDisplayMethod(); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { @@ -114,6 +115,40 @@ public final class SurfaceControl { } } + private static Method getGetPhysicalDisplayTokenMethod() throws NoSuchMethodException { + if (getPhysicalDisplayTokenMethod == null) { + getPhysicalDisplayTokenMethod = CLASS.getMethod("getPhysicalDisplayToken", long.class); + } + return getPhysicalDisplayTokenMethod; + } + + public static IBinder getPhysicalDisplayToken(long physicalDisplayId) { + try { + Method method = getGetPhysicalDisplayTokenMethod(); + return (IBinder) method.invoke(null, physicalDisplayId); + } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + Ln.e("Could not invoke method", e); + return null; + } + } + + private static Method getGetPhysicalDisplayIdsMethod() throws NoSuchMethodException { + if (getPhysicalDisplayIdsMethod == null) { + getPhysicalDisplayIdsMethod = CLASS.getMethod("getPhysicalDisplayIds"); + } + return getPhysicalDisplayIdsMethod; + } + + public static long[] getPhysicalDisplayIds() { + try { + Method method = getGetPhysicalDisplayIdsMethod(); + return (long[]) method.invoke(null); + } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + Ln.e("Could not invoke method", e); + return null; + } + } + private static Method getSetDisplayPowerModeMethod() throws NoSuchMethodException { if (setDisplayPowerModeMethod == null) { setDisplayPowerModeMethod = CLASS.getMethod("setDisplayPowerMode", IBinder.class, int.class); From b4caa483dd6682279170b5ec61963e6239f15072 Mon Sep 17 00:00:00 2001 From: Kartik Kushwaha <75395993+Corrupter-rot@users.noreply.github.com> Date: Sat, 4 Feb 2023 23:09:01 +0530 Subject: [PATCH 1308/2244] Add Fedora instructions in README Add the command to install the scrcpy package for Fedora directly. PR #3715 Signed-off-by: Romain Vimont --- README.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index b2a767fd..7f4fe704 100644 --- a/README.md +++ b/README.md @@ -80,16 +80,22 @@ On Arch Linux: pacman -S scrcpy ``` +On Fedora, a [COPR] package is available: [`scrcpy`][copr-link]: + +``` +dnf copr enable zeno/scrcpy +dnf install scrcpy +``` + +[COPR]: https://fedoraproject.org/wiki/Category:Copr +[copr-link]: https://copr.fedorainfracloud.org/coprs/zeno/scrcpy/ + A [Snap] package is available: [`scrcpy`][snap-link]. [snap-link]: https://snapstats.org/snaps/scrcpy [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 Gentoo, an [Ebuild] is available: [`scrcpy/`][ebuild-link]. From 6524e90c68d5e85ab2acc6177abed3603b4e9f78 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 7 Feb 2023 23:11:03 +0100 Subject: [PATCH 1309/2244] Remove unused constant This line was committed by error. Refs 3e517cd40eb50f8afb5137f39b0676fead58902a --- server/src/main/java/com/genymobile/scrcpy/Options.java | 1 - 1 file changed, 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index d43a60a2..474e5cf2 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -5,7 +5,6 @@ import android.graphics.Rect; import java.util.List; public class Options { - private static final String VIDEO_CODEC_H264 = "h264"; private Ln.Level logLevel = Ln.Level.DEBUG; private int uid = -1; // 31-bit non-negative value, or -1 From bd56c0abf7287b985702f1d0c24dab56d817969e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 10 Feb 2023 18:45:31 +0100 Subject: [PATCH 1310/2244] Remove unused codec context The demuxer does not need any codec context. --- app/src/demuxer.c | 10 +--------- app/src/demuxer.h | 1 - 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/app/src/demuxer.c b/app/src/demuxer.c index 6c44a558..945d3c78 100644 --- a/app/src/demuxer.c +++ b/app/src/demuxer.c @@ -208,15 +208,9 @@ run_demuxer(void *data) { goto end; } - demuxer->codec_ctx = avcodec_alloc_context3(codec); - if (!demuxer->codec_ctx) { - LOG_OOM(); - goto end; - } - if (!sc_demuxer_open_sinks(demuxer, codec)) { LOGE("Could not open demuxer sinks"); - goto finally_free_codec_ctx; + goto end; } demuxer->parser = av_parser_init(codec_id); @@ -261,8 +255,6 @@ finally_close_parser: av_parser_close(demuxer->parser); finally_close_sinks: sc_demuxer_close_sinks(demuxer); -finally_free_codec_ctx: - avcodec_free_context(&demuxer->codec_ctx); end: demuxer->cbs->on_eos(demuxer, demuxer->cbs_userdata); diff --git a/app/src/demuxer.h b/app/src/demuxer.h index 11e20ad6..4e660fbf 100644 --- a/app/src/demuxer.h +++ b/app/src/demuxer.h @@ -21,7 +21,6 @@ struct sc_demuxer { struct sc_packet_sink *sinks[SC_DEMUXER_MAX_SINKS]; unsigned sink_count; - AVCodecContext *codec_ctx; AVCodecParserContext *parser; // successive packets may need to be concatenated, until a non-config // packet is available From 40866ddc10f35368d25ec05eb2a6cd3d0ebc803e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 10 Feb 2023 18:55:39 +0100 Subject: [PATCH 1311/2244] Fix demuxer error message The message applies to all packets, not only config packets. --- app/src/demuxer.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/demuxer.c b/app/src/demuxer.c index 945d3c78..a913139a 100644 --- a/app/src/demuxer.c +++ b/app/src/demuxer.c @@ -105,7 +105,7 @@ push_packet_to_sinks(struct sc_demuxer *demuxer, const AVPacket *packet) { for (unsigned i = 0; i < demuxer->sink_count; ++i) { struct sc_packet_sink *sink = demuxer->sinks[i]; if (!sink->ops->push(sink, packet)) { - LOGE("Could not send config packet to sink %d", i); + LOGE("Could not send packet to sink %d", i); return false; } } From 230b8274b90cd035b94f849165e8a807346c93d3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 10 Feb 2023 18:47:13 +0100 Subject: [PATCH 1312/2244] Fix error return value The function returns an enum AVCodecID, not a bool. --- app/src/demuxer.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/demuxer.c b/app/src/demuxer.c index a913139a..68c999c0 100644 --- a/app/src/demuxer.c +++ b/app/src/demuxer.c @@ -22,7 +22,7 @@ sc_demuxer_recv_codec_id(struct sc_demuxer *demuxer) { uint8_t data[4]; ssize_t r = net_recv_all(demuxer->socket, data, 4); if (r < 4) { - return false; + return AV_CODEC_ID_NONE; } #define SC_CODEC_ID_H264 UINT32_C(0x68323634) // "h264" in ASCII From 953edfd1df95aadb794167274e2be2f1037848fa Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 10 Feb 2023 18:48:20 +0100 Subject: [PATCH 1313/2244] Split codec_id reading Receive codec id and convert it to AVCodecID separately. This will allow the caller to distinguish between EOS and unknown codec id. --- app/src/demuxer.c | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/app/src/demuxer.c b/app/src/demuxer.c index 68c999c0..e6a35f4f 100644 --- a/app/src/demuxer.c +++ b/app/src/demuxer.c @@ -18,17 +18,10 @@ #define SC_PACKET_PTS_MASK (SC_PACKET_FLAG_KEY_FRAME - 1) static enum AVCodecID -sc_demuxer_recv_codec_id(struct sc_demuxer *demuxer) { - uint8_t data[4]; - ssize_t r = net_recv_all(demuxer->socket, data, 4); - if (r < 4) { - return AV_CODEC_ID_NONE; - } - +sc_demuxer_to_avcodec_id(uint32_t codec_id) { #define SC_CODEC_ID_H264 UINT32_C(0x68323634) // "h264" in ASCII #define SC_CODEC_ID_H265 UINT32_C(0x68323635) // "h265" in ASCII #define SC_CODEC_ID_AV1 UINT32_C(0x00617631) // "av1" in ASCII - uint32_t codec_id = sc_read32be(data); switch (codec_id) { case SC_CODEC_ID_H264: return AV_CODEC_ID_H264; @@ -42,6 +35,18 @@ sc_demuxer_recv_codec_id(struct sc_demuxer *demuxer) { } } +static bool +sc_demuxer_recv_codec_id(struct sc_demuxer *demuxer, uint32_t *codec_id) { + uint8_t data[4]; + ssize_t r = net_recv_all(demuxer->socket, data, 4); + if (r < 4) { + return false; + } + + *codec_id = sc_read32be(data); + return true; +} + static bool sc_demuxer_recv_packet(struct sc_demuxer *demuxer, AVPacket *packet) { // The video stream contains raw packets, without time information. When we @@ -196,7 +201,13 @@ static int run_demuxer(void *data) { struct sc_demuxer *demuxer = data; - enum AVCodecID codec_id = sc_demuxer_recv_codec_id(demuxer); + uint32_t raw_codec_id; + bool ok = sc_demuxer_recv_codec_id(demuxer, &raw_codec_id); + if (!ok) { + goto end; + } + + enum AVCodecID codec_id = sc_demuxer_to_avcodec_id(raw_codec_id); if (codec_id == AV_CODEC_ID_NONE) { // Error already logged goto end; From 4f9e9c6619cdf847e3ee78f480fc4ca642ae7c2c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 10 Feb 2023 18:46:12 +0100 Subject: [PATCH 1314/2244] Prefix UI events constants by SC_ --- app/src/events.h | 10 +++++----- app/src/scrcpy.c | 12 ++++++------ app/src/screen.c | 6 +++--- app/src/usb/scrcpy_otg.c | 4 ++-- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/app/src/events.h b/app/src/events.h index 3c14f96e..477328de 100644 --- a/app/src/events.h +++ b/app/src/events.h @@ -1,5 +1,5 @@ -#define EVENT_NEW_FRAME SDL_USEREVENT -#define EVENT_STREAM_STOPPED (SDL_USEREVENT + 1) -#define EVENT_SERVER_CONNECTION_FAILED (SDL_USEREVENT + 2) -#define EVENT_SERVER_CONNECTED (SDL_USEREVENT + 3) -#define EVENT_USB_DEVICE_DISCONNECTED (SDL_USEREVENT + 4) +#define SC_EVENT_NEW_FRAME SDL_USEREVENT +#define SC_EVENT_STREAM_STOPPED (SDL_USEREVENT + 1) +#define SC_EVENT_SERVER_CONNECTION_FAILED (SDL_USEREVENT + 2) +#define SC_EVENT_SERVER_CONNECTED (SDL_USEREVENT + 3) +#define SC_EVENT_USB_DEVICE_DISCONNECTED (SDL_USEREVENT + 4) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index e85536e6..b0324f7a 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -155,7 +155,7 @@ event_loop(struct scrcpy *s) { SDL_Event event; while (SDL_WaitEvent(&event)) { switch (event.type) { - case EVENT_STREAM_STOPPED: + case SC_EVENT_STREAM_STOPPED: LOGW("Device disconnected"); return SCRCPY_EXIT_DISCONNECTED; case SDL_QUIT: @@ -179,10 +179,10 @@ await_for_server(bool *connected) { LOGD("User requested to quit"); *connected = false; return true; - case EVENT_SERVER_CONNECTION_FAILED: + case SC_EVENT_SERVER_CONNECTION_FAILED: LOGE("Server connection failed"); return false; - case EVENT_SERVER_CONNECTED: + case SC_EVENT_SERVER_CONNECTED: LOGD("Server connected"); *connected = true; return true; @@ -237,7 +237,7 @@ sc_demuxer_on_eos(struct sc_demuxer *demuxer, void *userdata) { (void) demuxer; (void) userdata; - PUSH_EVENT(EVENT_STREAM_STOPPED); + PUSH_EVENT(SC_EVENT_STREAM_STOPPED); } static void @@ -245,7 +245,7 @@ sc_server_on_connection_failed(struct sc_server *server, void *userdata) { (void) server; (void) userdata; - PUSH_EVENT(EVENT_SERVER_CONNECTION_FAILED); + PUSH_EVENT(SC_EVENT_SERVER_CONNECTION_FAILED); } static void @@ -253,7 +253,7 @@ sc_server_on_connected(struct sc_server *server, void *userdata) { (void) server; (void) userdata; - PUSH_EVENT(EVENT_SERVER_CONNECTED); + PUSH_EVENT(SC_EVENT_SERVER_CONNECTED); } static void diff --git a/app/src/screen.c b/app/src/screen.c index ae28e6e6..425ba2c3 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -371,7 +371,7 @@ sc_video_buffer_on_new_frame(struct sc_video_buffer *vb, bool previous_skipped, bool need_new_event; if (previous_skipped) { sc_fps_counter_add_skipped_frame(&screen->fps_counter); - // The EVENT_NEW_FRAME triggered for the previous frame will consume + // The SC_EVENT_NEW_FRAME triggered for the previous frame will consume // this new frame instead, unless the previous event failed need_new_event = screen->event_failed; } else { @@ -380,7 +380,7 @@ sc_video_buffer_on_new_frame(struct sc_video_buffer *vb, bool previous_skipped, if (need_new_event) { static SDL_Event new_frame_event = { - .type = EVENT_NEW_FRAME, + .type = SC_EVENT_NEW_FRAME, }; // Post the event on the UI thread @@ -820,7 +820,7 @@ sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event) { bool relative_mode = sc_screen_is_relative_mode(screen); switch (event->type) { - case EVENT_NEW_FRAME: { + case SC_EVENT_NEW_FRAME: { bool ok = sc_screen_update_frame(screen); if (!ok) { LOGW("Frame update failed\n"); diff --git a/app/src/usb/scrcpy_otg.c b/app/src/usb/scrcpy_otg.c index ebcfa36f..f469de1a 100644 --- a/app/src/usb/scrcpy_otg.c +++ b/app/src/usb/scrcpy_otg.c @@ -22,7 +22,7 @@ sc_usb_on_disconnected(struct sc_usb *usb, void *userdata) { (void) userdata; SDL_Event event; - event.type = EVENT_USB_DEVICE_DISCONNECTED; + event.type = SC_EVENT_USB_DEVICE_DISCONNECTED; int ret = SDL_PushEvent(&event); if (ret < 0) { LOGE("Could not post USB disconnection event: %s", SDL_GetError()); @@ -34,7 +34,7 @@ event_loop(struct scrcpy_otg *s) { SDL_Event event; while (SDL_WaitEvent(&event)) { switch (event.type) { - case EVENT_USB_DEVICE_DISCONNECTED: + case SC_EVENT_USB_DEVICE_DISCONNECTED: LOGW("Device disconnected"); return SCRCPY_EXIT_DISCONNECTED; case SDL_QUIT: From 730eb1086ad32fbd368a6ded61ef8b79741e5f66 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 10 Feb 2023 18:54:46 +0100 Subject: [PATCH 1315/2244] Properly report demuxer errors All demuxer errors were reported as "device disconnected", even if the failure was not related to device socket read. --- app/src/demuxer.c | 9 +++++++-- app/src/demuxer.h | 2 +- app/src/events.h | 3 ++- app/src/scrcpy.c | 15 +++++++++++---- 4 files changed, 21 insertions(+), 8 deletions(-) diff --git a/app/src/demuxer.c b/app/src/demuxer.c index e6a35f4f..97a17594 100644 --- a/app/src/demuxer.c +++ b/app/src/demuxer.c @@ -201,9 +201,13 @@ static int run_demuxer(void *data) { struct sc_demuxer *demuxer = data; + // Flag to report end-of-stream (i.e. device disconnected) + bool eos = false; + uint32_t raw_codec_id; bool ok = sc_demuxer_recv_codec_id(demuxer, &raw_codec_id); if (!ok) { + eos = true; goto end; } @@ -244,6 +248,7 @@ run_demuxer(void *data) { bool ok = sc_demuxer_recv_packet(demuxer, packet); if (!ok) { // end of stream + eos = true; break; } @@ -267,7 +272,7 @@ finally_close_parser: finally_close_sinks: sc_demuxer_close_sinks(demuxer); end: - demuxer->cbs->on_eos(demuxer, demuxer->cbs_userdata); + demuxer->cbs->on_ended(demuxer, eos, demuxer->cbs_userdata); return 0; } @@ -279,7 +284,7 @@ sc_demuxer_init(struct sc_demuxer *demuxer, sc_socket socket, demuxer->pending = NULL; demuxer->sink_count = 0; - assert(cbs && cbs->on_eos); + assert(cbs && cbs->on_ended); demuxer->cbs = cbs; demuxer->cbs_userdata = cbs_userdata; diff --git a/app/src/demuxer.h b/app/src/demuxer.h index 4e660fbf..dfb06675 100644 --- a/app/src/demuxer.h +++ b/app/src/demuxer.h @@ -31,7 +31,7 @@ struct sc_demuxer { }; struct sc_demuxer_callbacks { - void (*on_eos)(struct sc_demuxer *demuxer, void *userdata); + void (*on_ended)(struct sc_demuxer *demuxer, bool eos, void *userdata); }; void diff --git a/app/src/events.h b/app/src/events.h index 477328de..7fa10761 100644 --- a/app/src/events.h +++ b/app/src/events.h @@ -1,5 +1,6 @@ #define SC_EVENT_NEW_FRAME SDL_USEREVENT -#define SC_EVENT_STREAM_STOPPED (SDL_USEREVENT + 1) +#define SC_EVENT_DEVICE_DISCONNECTED (SDL_USEREVENT + 1) #define SC_EVENT_SERVER_CONNECTION_FAILED (SDL_USEREVENT + 2) #define SC_EVENT_SERVER_CONNECTED (SDL_USEREVENT + 3) #define SC_EVENT_USB_DEVICE_DISCONNECTED (SDL_USEREVENT + 4) +#define SC_EVENT_DEMUXER_ERROR (SDL_USEREVENT + 5) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index b0324f7a..bf4bb199 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -155,9 +155,12 @@ event_loop(struct scrcpy *s) { SDL_Event event; while (SDL_WaitEvent(&event)) { switch (event.type) { - case SC_EVENT_STREAM_STOPPED: + case SC_EVENT_DEVICE_DISCONNECTED: LOGW("Device disconnected"); return SCRCPY_EXIT_DISCONNECTED; + case SC_EVENT_DEMUXER_ERROR: + LOGE("Demuxer error"); + return SCRCPY_EXIT_FAILURE; case SDL_QUIT: LOGD("User requested to quit"); return SCRCPY_EXIT_SUCCESS; @@ -233,11 +236,15 @@ av_log_callback(void *avcl, int level, const char *fmt, va_list vl) { } static void -sc_demuxer_on_eos(struct sc_demuxer *demuxer, void *userdata) { +sc_demuxer_on_ended(struct sc_demuxer *demuxer, bool eos, void *userdata) { (void) demuxer; (void) userdata; - PUSH_EVENT(SC_EVENT_STREAM_STOPPED); + if (eos) { + PUSH_EVENT(SC_EVENT_DEVICE_DISCONNECTED); + } else { + PUSH_EVENT(SC_EVENT_DEMUXER_ERROR); + } } static void @@ -421,7 +428,7 @@ scrcpy(struct scrcpy_options *options) { av_log_set_callback(av_log_callback); static const struct sc_demuxer_callbacks demuxer_cbs = { - .on_eos = sc_demuxer_on_eos, + .on_ended = sc_demuxer_on_ended, }; sc_demuxer_init(&s->demuxer, s->server.video_socket, &demuxer_cbs, NULL); From 400a1c69b198103feff9682895529529890c1acc Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 7 Feb 2023 22:58:46 +0100 Subject: [PATCH 1316/2244] Join all threads before end of main Some calls from separate threads may throw exceptions once the main() method has returned. --- .../src/main/java/com/genymobile/scrcpy/Controller.java | 8 +++++++- .../java/com/genymobile/scrcpy/DeviceMessageSender.java | 7 ++++++- server/src/main/java/com/genymobile/scrcpy/Server.java | 9 +++++++++ 3 files changed, 22 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 6147e36a..02684a1d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -100,11 +100,17 @@ public class Controller { public void stop() { if (thread != null) { thread.interrupt(); - thread = null; } sender.stop(); } + public void join() throws InterruptedException { + if (thread != null) { + thread.join(); + } + sender.join(); + } + public DeviceMessageSender getSender() { return sender; } diff --git a/server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java b/server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java index 7ec4ab41..b0e2a388 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java +++ b/server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java @@ -66,7 +66,12 @@ public final class DeviceMessageSender { public void stop() { if (thread != null) { thread.interrupt(); - thread = null; + } + } + + public void join() throws InterruptedException { + if (thread != null) { + thread.join(); } } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 81c3e813..ac5f0293 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -117,6 +117,15 @@ public final class Server { if (controller != null) { controller.stop(); } + + try { + initThread.join(); + if (controller != null) { + controller.join(); + } + } catch (InterruptedException e) { + // ignore + } } } } From 45b2e6db5c8df04b85cf646d1744646d1e679c6d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 10 Feb 2023 19:06:17 +0100 Subject: [PATCH 1317/2244] Log component stopped in finally clause The message must be logged even when no exception occurs. --- server/src/main/java/com/genymobile/scrcpy/Controller.java | 1 + .../main/java/com/genymobile/scrcpy/DeviceMessageSender.java | 1 + server/src/main/java/com/genymobile/scrcpy/Server.java | 2 +- 3 files 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 02684a1d..02d77cb1 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -90,6 +90,7 @@ public class Controller { control(); } catch (IOException e) { // this is expected on close + } finally { Ln.d("Controller stopped"); } }); diff --git a/server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java b/server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java index b0e2a388..0ef2a9ee 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java +++ b/server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java @@ -57,6 +57,7 @@ public final class DeviceMessageSender { loop(); } catch (IOException | InterruptedException e) { // this is expected on close + } finally { Ln.d("Device message sender stopped"); } }); diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index ac5f0293..cfb45e33 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -111,8 +111,8 @@ public final class Server { screenEncoder.streamScreen(device, videoStreamer); } catch (IOException e) { // this is expected on close - Ln.d("Screen streaming stopped"); } finally { + Ln.d("Screen streaming stopped"); initThread.interrupt(); if (controller != null) { controller.stop(); From f03f32267e8c40301a435b5ec9db9655c852b8f9 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 10 Feb 2023 23:18:09 +0100 Subject: [PATCH 1318/2244] Remove unused parser Since 1c02b58412db2646ba6e57a7e305d26482b9bc0d, the parser is not used anymore. --- app/src/demuxer.c | 14 +------------- app/src/demuxer.h | 1 - 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/app/src/demuxer.c b/app/src/demuxer.c index 97a17594..5cc07f6c 100644 --- a/app/src/demuxer.c +++ b/app/src/demuxer.c @@ -228,20 +228,10 @@ run_demuxer(void *data) { goto end; } - demuxer->parser = av_parser_init(codec_id); - if (!demuxer->parser) { - LOGE("Could not initialize parser"); - goto finally_close_sinks; - } - - // We must only pass complete frames to av_parser_parse2()! - // It's more complicated, but this allows to reduce the latency by 1 frame! - demuxer->parser->flags |= PARSER_FLAG_COMPLETE_FRAMES; - AVPacket *packet = av_packet_alloc(); if (!packet) { LOG_OOM(); - goto finally_close_parser; + goto finally_close_sinks; } for (;;) { @@ -267,8 +257,6 @@ run_demuxer(void *data) { } av_packet_free(&packet); -finally_close_parser: - av_parser_close(demuxer->parser); finally_close_sinks: sc_demuxer_close_sinks(demuxer); end: diff --git a/app/src/demuxer.h b/app/src/demuxer.h index dfb06675..86e0536d 100644 --- a/app/src/demuxer.h +++ b/app/src/demuxer.h @@ -21,7 +21,6 @@ struct sc_demuxer { struct sc_packet_sink *sinks[SC_DEMUXER_MAX_SINKS]; unsigned sink_count; - AVCodecParserContext *parser; // successive packets may need to be concatenated, until a non-config // packet is available AVPacket *pending; From 49eb326ce9beb0a993dce0f2a7beb7dcfc3b1c51 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 10 Feb 2023 22:52:40 +0100 Subject: [PATCH 1319/2244] Extract packet merging Config packets must be prepended to the next media packet. Extract the logic to a new sc_packet_merger helper to simplify the demuxer code. --- app/meson.build | 1 + app/src/demuxer.c | 57 +++++++++-------------------------------- app/src/demuxer.h | 4 --- app/src/packet_merger.c | 48 ++++++++++++++++++++++++++++++++++ app/src/packet_merger.h | 43 +++++++++++++++++++++++++++++++ 5 files changed, 104 insertions(+), 49 deletions(-) create mode 100644 app/src/packet_merger.c create mode 100644 app/src/packet_merger.h diff --git a/app/meson.build b/app/meson.build index 3ddd5a5d..5d779756 100644 --- a/app/meson.build +++ b/app/meson.build @@ -21,6 +21,7 @@ src = [ 'src/mouse_inject.c', 'src/opengl.c', 'src/options.c', + 'src/packet_merger.c', 'src/receiver.c', 'src/recorder.c', 'src/scrcpy.c', diff --git a/app/src/demuxer.c b/app/src/demuxer.c index 5cc07f6c..282c07de 100644 --- a/app/src/demuxer.c +++ b/app/src/demuxer.c @@ -6,6 +6,7 @@ #include "decoder.h" #include "events.h" +#include "packet_merger.h" #include "recorder.h" #include "util/binary.h" #include "util/log.h" @@ -120,48 +121,7 @@ push_packet_to_sinks(struct sc_demuxer *demuxer, const AVPacket *packet) { static bool sc_demuxer_push_packet(struct sc_demuxer *demuxer, AVPacket *packet) { - bool is_config = packet->pts == AV_NOPTS_VALUE; - - // A config packet must not be decoded immediately (it contains no - // frame); instead, it must be concatenated with the future data packet. - if (demuxer->pending || is_config) { - if (demuxer->pending) { - size_t offset = demuxer->pending->size; - if (av_grow_packet(demuxer->pending, packet->size)) { - LOG_OOM(); - return false; - } - - memcpy(demuxer->pending->data + offset, packet->data, packet->size); - } else { - demuxer->pending = av_packet_alloc(); - if (!demuxer->pending) { - LOG_OOM(); - return false; - } - if (av_packet_ref(demuxer->pending, packet)) { - LOG_OOM(); - av_packet_free(&demuxer->pending); - return false; - } - } - - if (!is_config) { - // prepare the concat packet to send to the decoder - demuxer->pending->pts = packet->pts; - demuxer->pending->dts = packet->dts; - demuxer->pending->flags = packet->flags; - packet = demuxer->pending; - } - } - bool ok = push_packet_to_sinks(demuxer, packet); - - if (!is_config && demuxer->pending) { - // the pending packet must be discarded (consumed or error) - av_packet_free(&demuxer->pending); - } - if (!ok) { LOGE("Could not process packet"); return false; @@ -228,6 +188,9 @@ run_demuxer(void *data) { goto end; } + struct sc_packet_merger merger; + sc_packet_merger_init(&merger); + AVPacket *packet = av_packet_alloc(); if (!packet) { LOG_OOM(); @@ -242,6 +205,13 @@ run_demuxer(void *data) { break; } + // Prepend any config packet to the next media packet + ok = sc_packet_merger_merge(&merger, packet); + if (!ok) { + av_packet_unref(packet); + break; + } + ok = sc_demuxer_push_packet(demuxer, packet); av_packet_unref(packet); if (!ok) { @@ -252,9 +222,7 @@ run_demuxer(void *data) { LOGD("End of frames"); - if (demuxer->pending) { - av_packet_free(&demuxer->pending); - } + sc_packet_merger_destroy(&merger); av_packet_free(&packet); finally_close_sinks: @@ -269,7 +237,6 @@ void sc_demuxer_init(struct sc_demuxer *demuxer, sc_socket socket, const struct sc_demuxer_callbacks *cbs, void *cbs_userdata) { demuxer->socket = socket; - demuxer->pending = NULL; demuxer->sink_count = 0; assert(cbs && cbs->on_ended); diff --git a/app/src/demuxer.h b/app/src/demuxer.h index 86e0536d..e403fe35 100644 --- a/app/src/demuxer.h +++ b/app/src/demuxer.h @@ -21,10 +21,6 @@ struct sc_demuxer { struct sc_packet_sink *sinks[SC_DEMUXER_MAX_SINKS]; unsigned sink_count; - // successive packets may need to be concatenated, until a non-config - // packet is available - AVPacket *pending; - const struct sc_demuxer_callbacks *cbs; void *cbs_userdata; }; diff --git a/app/src/packet_merger.c b/app/src/packet_merger.c new file mode 100644 index 00000000..81b02d2c --- /dev/null +++ b/app/src/packet_merger.c @@ -0,0 +1,48 @@ +#include "packet_merger.h" + +#include "util/log.h" + +void +sc_packet_merger_init(struct sc_packet_merger *merger) { + merger->config = NULL; +} + +void +sc_packet_merger_destroy(struct sc_packet_merger *merger) { + free(merger->config); +} + +bool +sc_packet_merger_merge(struct sc_packet_merger *merger, AVPacket *packet) { + bool is_config = packet->pts == AV_NOPTS_VALUE; + + if (is_config) { + free(merger->config); + + merger->config = malloc(packet->size); + if (!merger->config) { + LOG_OOM(); + return false; + } + + memcpy(merger->config, packet->data, packet->size); + merger->config_size = packet->size; + } else if (merger->config) { + size_t config_size = merger->config_size; + size_t media_size = packet->size; + + if (av_grow_packet(packet, config_size)) { + LOG_OOM(); + return false; + } + + memmove(packet->data + config_size, packet->data, media_size); + memcpy(packet->data, merger->config, config_size); + + free(merger->config); + merger->config = NULL; + // merger->size is meaningless when merger->config is NULL + } + + return true; +} diff --git a/app/src/packet_merger.h b/app/src/packet_merger.h new file mode 100644 index 00000000..e1824c2c --- /dev/null +++ b/app/src/packet_merger.h @@ -0,0 +1,43 @@ +#ifndef SC_PACKET_MERGER_H +#define SC_PACKET_MERGER_H + +#include "common.h" + +#include +#include +#include + +/** + * Config packets (containing the SPS/PPS) are sent in-band. A new config + * packet is sent whenever a new encoding session is started (on start and on + * device orientation change). + * + * Every time a config packet is received, it must be sent alone (for recorder + * extradata), then concatenated to the next media packet (for correct decoding + * and recording). + * + * This helper reads every input packet and modifies each media packet which + * immediately follows a config packet to prepend the config packet payload. + */ + +struct sc_packet_merger { + uint8_t *config; + size_t config_size; +}; + +void +sc_packet_merger_init(struct sc_packet_merger *merger); + +void +sc_packet_merger_destroy(struct sc_packet_merger *merger); + +/** + * If the packet is a config packet, then keep its data for later. + * Otherwise (if the packet is a media packet), then if a config packet is + * pending, prepend the config packet to this packet (so the packet is + * modified!). + */ +bool +sc_packet_merger_merge(struct sc_packet_merger *merger, AVPacket *packet); + +#endif From 439a1fd4ed3d7ada4c5098a6d52893dd4e87082a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 11 Feb 2023 09:52:31 +0100 Subject: [PATCH 1320/2244] Rename 'uid' to 'scid' A random identifier is generated to differentiate multiple running scrcpy instances. Rename it from 'uid' to 'scid' (scrcpy id) not to confuse it with Linux UID. Fixes #3729 Refs 4315be164823d2c8fc44b475b52af79bfee98ff1 --- app/src/scrcpy.c | 7 ++++--- app/src/server.c | 4 ++-- app/src/server.h | 2 +- .../com/genymobile/scrcpy/DesktopConnection.java | 12 ++++++------ .../main/java/com/genymobile/scrcpy/Options.java | 10 +++++----- .../main/java/com/genymobile/scrcpy/Server.java | 14 +++++++------- 6 files changed, 25 insertions(+), 24 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index bf4bb199..8932dd1d 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -273,8 +273,9 @@ sc_server_on_disconnected(struct sc_server *server, void *userdata) { // event } +// Generate a scrcpy id to differentiate multiple running scrcpy instances static uint32_t -scrcpy_generate_uid() { +scrcpy_generate_scid() { struct sc_rand rand; sc_rand_init(&rand); // Only use 31 bits to avoid issues with signed values on the Java-side @@ -314,10 +315,10 @@ scrcpy(struct scrcpy_options *options) { struct sc_acksync *acksync = NULL; - uint32_t uid = scrcpy_generate_uid(); + uint32_t scid = scrcpy_generate_scid(); struct sc_server_params params = { - .uid = uid, + .scid = scid, .req_serial = options->serial, .select_usb = options->select_usb, .select_tcpip = options->select_tcpip, diff --git a/app/src/server.c b/app/src/server.c index 74d318c8..96bff77e 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -213,7 +213,7 @@ execute_server(struct sc_server *server, cmd[count++] = p; \ } - ADD_PARAM("uid=%08x", params->uid); + ADD_PARAM("scid=%08x", params->scid); ADD_PARAM("log_level=%s", log_level_to_server_string(params->log_level)); ADD_PARAM("bit_rate=%" PRIu32, params->bit_rate); @@ -787,7 +787,7 @@ run_server(void *data) { LOGD("Device serial: %s", serial); int r = asprintf(&server->device_socket_name, SC_SOCKET_NAME_PREFIX "%08x", - params->uid); + params->scid); if (r == -1) { LOG_OOM(); goto error_connection_failed; diff --git a/app/src/server.h b/app/src/server.h index 950ad532..d6b1401e 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -22,7 +22,7 @@ struct sc_server_info { }; struct sc_server_params { - uint32_t uid; + uint32_t scid; const char *req_serial; enum sc_log_level log_level; enum sc_codec codec; diff --git a/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java b/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java index 7f287a6a..1f8f46e4 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java +++ b/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java @@ -46,17 +46,17 @@ public final class DesktopConnection implements Closeable { return localSocket; } - private static String getSocketName(int uid) { - if (uid == -1) { - // If no UID is set, use "scrcpy" to simplify using scrcpy-server alone + private static String getSocketName(int scid) { + if (scid == -1) { + // If no SCID is set, use "scrcpy" to simplify using scrcpy-server alone return SOCKET_NAME_PREFIX; } - return SOCKET_NAME_PREFIX + String.format("_%08x", uid); + return SOCKET_NAME_PREFIX + String.format("_%08x", scid); } - public static DesktopConnection open(int uid, boolean tunnelForward, boolean control, boolean sendDummyByte) throws IOException { - String socketName = getSocketName(uid); + public static DesktopConnection open(int scid, boolean tunnelForward, boolean control, boolean sendDummyByte) throws IOException { + String socketName = getSocketName(scid); LocalSocket videoSocket; LocalSocket controlSocket = null; diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 474e5cf2..5c59ec8e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -7,7 +7,7 @@ import java.util.List; public class Options { private Ln.Level logLevel = Ln.Level.DEBUG; - private int uid = -1; // 31-bit non-negative value, or -1 + private int scid = -1; // 31-bit non-negative value, or -1 private int maxSize; private VideoCodec codec = VideoCodec.H264; private int bitRate = 8000000; @@ -41,12 +41,12 @@ public class Options { this.logLevel = logLevel; } - public int getUid() { - return uid; + public int getScid() { + return scid; } - public void setUid(int uid) { - this.uid = uid; + public void setScid(int scid) { + this.scid = scid; } public int getMaxSize() { diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index cfb45e33..d7a99576 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -66,7 +66,7 @@ public final class Server { Thread initThread = startInitThread(options); - int uid = options.getUid(); + int scid = options.getScid(); boolean tunnelForward = options.isTunnelForward(); boolean control = options.getControl(); boolean sendDummyByte = options.getSendDummyByte(); @@ -84,7 +84,7 @@ public final class Server { Workarounds.fillAppInfo(); } - try (DesktopConnection connection = DesktopConnection.open(uid, tunnelForward, control, sendDummyByte)) { + try (DesktopConnection connection = DesktopConnection.open(scid, tunnelForward, control, sendDummyByte)) { VideoCodec codec = options.getCodec(); if (options.getSendDeviceMeta()) { Size videoSize = device.getScreenInfo().getVideoSize(); @@ -158,12 +158,12 @@ public final class Server { String key = arg.substring(0, equalIndex); String value = arg.substring(equalIndex + 1); switch (key) { - case "uid": - int uid = Integer.parseInt(value, 0x10); - if (uid < -1) { - throw new IllegalArgumentException("uid may not be negative (except -1 for 'none'): " + uid); + case "scid": + int scid = Integer.parseInt(value, 0x10); + if (scid < -1) { + throw new IllegalArgumentException("scid may not be negative (except -1 for 'none'): " + scid); } - options.setUid(uid); + options.setScid(scid); break; case "log_level": Ln.Level level = Ln.Level.valueOf(value.toUpperCase(Locale.ENGLISH)); From f4e7085c349fe79fa5744957e26d18a4b035e0c1 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 17 Feb 2023 08:32:59 +0100 Subject: [PATCH 1321/2244] Log non-EPIPE I/O exceptions On close, the client closes the socket. This wakes up socket blocking calls on the server-side, by throwing an exception. Since this exception is expected, it was not logged. However, other IOExceptions might occur, which must not be ignored. For that purpose, log only IOException when they are not caused by an EPIPE error. --- server/src/main/java/com/genymobile/scrcpy/IO.java | 5 +++++ server/src/main/java/com/genymobile/scrcpy/Server.java | 5 ++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/IO.java b/server/src/main/java/com/genymobile/scrcpy/IO.java index 6eaf0d6a..4a55c152 100644 --- a/server/src/main/java/com/genymobile/scrcpy/IO.java +++ b/server/src/main/java/com/genymobile/scrcpy/IO.java @@ -48,4 +48,9 @@ public final class IO { } return builder.toString(); } + + public static boolean isBrokenPipe(IOException e) { + Throwable cause = e.getCause(); + return cause instanceof ErrnoException && ((ErrnoException) cause).errno == OsConstants.EPIPE; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index d7a99576..0aff79bc 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -110,7 +110,10 @@ public final class Server { } screenEncoder.streamScreen(device, videoStreamer); } catch (IOException e) { - // this is expected on close + // Broken pipe is expected on close, because the socket is closed by the client + if (!IO.isBrokenPipe(e)) { + Ln.e("Video encoding error", e); + } } finally { Ln.d("Screen streaming stopped"); initThread.interrupt(); From 680ddf64bed1d5f225176d3805ab766741d6299e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 18 Feb 2023 09:30:57 +0100 Subject: [PATCH 1322/2244] Fix demuxer error message Now that there are several possible codecs, do not hardcode H.264 in the error message. Refs 3e517cd40eb50f8afb5137f39b0676fead58902a --- app/src/demuxer.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/demuxer.c b/app/src/demuxer.c index 282c07de..3b26415b 100644 --- a/app/src/demuxer.c +++ b/app/src/demuxer.c @@ -179,7 +179,7 @@ run_demuxer(void *data) { const AVCodec *codec = avcodec_find_decoder(codec_id); if (!codec) { - LOGE("H.264 decoder not found"); + LOGE("Decoder not found"); goto end; } From e91618586ca822e02de3a72fafccc962478ca79b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 18 Feb 2023 09:37:13 +0100 Subject: [PATCH 1323/2244] Prefix receiver by sc_ Like all other components in scrcpy. --- app/src/controller.c | 12 ++++++------ app/src/controller.h | 2 +- app/src/receiver.c | 14 +++++++------- app/src/receiver.h | 14 +++++++------- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/app/src/controller.c b/app/src/controller.c index cdf53edb..4a1d2b1d 100644 --- a/app/src/controller.c +++ b/app/src/controller.c @@ -9,20 +9,20 @@ sc_controller_init(struct sc_controller *controller, sc_socket control_socket, struct sc_acksync *acksync) { cbuf_init(&controller->queue); - bool ok = receiver_init(&controller->receiver, control_socket, acksync); + bool ok = sc_receiver_init(&controller->receiver, control_socket, acksync); if (!ok) { return false; } ok = sc_mutex_init(&controller->mutex); if (!ok) { - receiver_destroy(&controller->receiver); + sc_receiver_destroy(&controller->receiver); return false; } ok = sc_cond_init(&controller->msg_cond); if (!ok) { - receiver_destroy(&controller->receiver); + sc_receiver_destroy(&controller->receiver); sc_mutex_destroy(&controller->mutex); return false; } @@ -43,7 +43,7 @@ sc_controller_destroy(struct sc_controller *controller) { sc_control_msg_destroy(&msg); } - receiver_destroy(&controller->receiver); + sc_receiver_destroy(&controller->receiver); } bool @@ -117,7 +117,7 @@ sc_controller_start(struct sc_controller *controller) { return false; } - if (!receiver_start(&controller->receiver)) { + if (!sc_receiver_start(&controller->receiver)) { sc_controller_stop(controller); sc_thread_join(&controller->thread, NULL); return false; @@ -137,5 +137,5 @@ sc_controller_stop(struct sc_controller *controller) { void sc_controller_join(struct sc_controller *controller) { sc_thread_join(&controller->thread, NULL); - receiver_join(&controller->receiver); + sc_receiver_join(&controller->receiver); } diff --git a/app/src/controller.h b/app/src/controller.h index f8662353..67c3c58d 100644 --- a/app/src/controller.h +++ b/app/src/controller.h @@ -21,7 +21,7 @@ struct sc_controller { sc_cond msg_cond; bool stopped; struct sc_control_msg_queue queue; - struct receiver receiver; + struct sc_receiver receiver; }; bool diff --git a/app/src/receiver.c b/app/src/receiver.c index 0376948d..e715a8e6 100644 --- a/app/src/receiver.c +++ b/app/src/receiver.c @@ -7,7 +7,7 @@ #include "util/log.h" bool -receiver_init(struct receiver *receiver, sc_socket control_socket, +sc_receiver_init(struct sc_receiver *receiver, sc_socket control_socket, struct sc_acksync *acksync) { bool ok = sc_mutex_init(&receiver->mutex); if (!ok) { @@ -21,12 +21,12 @@ receiver_init(struct receiver *receiver, sc_socket control_socket, } void -receiver_destroy(struct receiver *receiver) { +sc_receiver_destroy(struct sc_receiver *receiver) { sc_mutex_destroy(&receiver->mutex); } static void -process_msg(struct receiver *receiver, struct device_msg *msg) { +process_msg(struct sc_receiver *receiver, struct device_msg *msg) { switch (msg->type) { case DEVICE_MSG_TYPE_CLIPBOARD: { char *current = SDL_GetClipboardText(); @@ -51,7 +51,7 @@ process_msg(struct receiver *receiver, struct device_msg *msg) { } static ssize_t -process_msgs(struct receiver *receiver, const unsigned char *buf, size_t len) { +process_msgs(struct sc_receiver *receiver, const unsigned char *buf, size_t len) { size_t head = 0; for (;;) { struct device_msg msg; @@ -76,7 +76,7 @@ process_msgs(struct receiver *receiver, const unsigned char *buf, size_t len) { static int run_receiver(void *data) { - struct receiver *receiver = data; + struct sc_receiver *receiver = data; static unsigned char buf[DEVICE_MSG_MAX_SIZE]; size_t head = 0; @@ -108,7 +108,7 @@ run_receiver(void *data) { } bool -receiver_start(struct receiver *receiver) { +sc_receiver_start(struct sc_receiver *receiver) { LOGD("Starting receiver thread"); bool ok = sc_thread_create(&receiver->thread, run_receiver, @@ -122,6 +122,6 @@ receiver_start(struct receiver *receiver) { } void -receiver_join(struct receiver *receiver) { +sc_receiver_join(struct sc_receiver *receiver) { sc_thread_join(&receiver->thread, NULL); } diff --git a/app/src/receiver.h b/app/src/receiver.h index f5808e4b..eb959fb8 100644 --- a/app/src/receiver.h +++ b/app/src/receiver.h @@ -11,7 +11,7 @@ // receive events from the device // managed by the controller -struct receiver { +struct sc_receiver { sc_socket control_socket; sc_thread thread; sc_mutex mutex; @@ -20,18 +20,18 @@ struct receiver { }; bool -receiver_init(struct receiver *receiver, sc_socket control_socket, - struct sc_acksync *acksync); +sc_receiver_init(struct sc_receiver *receiver, sc_socket control_socket, + struct sc_acksync *acksync); void -receiver_destroy(struct receiver *receiver); +sc_receiver_destroy(struct sc_receiver *receiver); bool -receiver_start(struct receiver *receiver); +sc_receiver_start(struct sc_receiver *receiver); -// no receiver_stop(), it will automatically stop on control_socket shutdown +// no sc_receiver_stop(), it will automatically stop on control_socket shutdown void -receiver_join(struct receiver *receiver); +sc_receiver_join(struct sc_receiver *receiver); #endif From 280a9afda8d44cf2450a5365fbb1072f45a4ad6f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 18 Feb 2023 18:11:13 +0100 Subject: [PATCH 1324/2244] Fix command-line help typo --- 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 27b0ddef..94fccb67 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -102,7 +102,7 @@ static const struct sc_option options[] = { .shortopt = 'b', .longopt = "bit-rate", .argdesc = "value", - .text = "Encode the video at the gitven bit-rate, expressed in bits/s. " + .text = "Encode the video at the given bit-rate, expressed in bits/s. " "Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n" "Default is " STR(DEFAULT_BIT_RATE) ".", }, From 25e2eb7d7c21b65ad0c206f63bbadb0a19a890c6 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 18 Feb 2023 19:09:26 +0100 Subject: [PATCH 1325/2244] Document default video codec Mention the default option value, like for other commands. --- app/scrcpy.1 | 2 ++ app/src/cli.c | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 18c70d65..8f028d7c 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -29,6 +29,8 @@ Default is 8000000. .BI "\-\-codec " name Select a video codec (h264, h265 or av1). +Default is h264. + .TP .BI "\-\-codec\-options " key\fR[:\fItype\fR]=\fIvalue\fR[,...] Set a list of comma-separated key:type=value options for the device encoder. diff --git a/app/src/cli.c b/app/src/cli.c index 94fccb67..93712113 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -110,7 +110,8 @@ static const struct sc_option options[] = { .longopt_id = OPT_CODEC, .longopt = "codec", .argdesc = "name", - .text = "Select a video codec (h264, h265 or av1).", + .text = "Select a video codec (h264, h265 or av1).\n" + "Default is h264.", }, { .longopt_id = OPT_CODEC_OPTIONS, From e02f30f8957adf09987c5bf2c7449f8241de7c8e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 19 Feb 2023 02:10:14 +0100 Subject: [PATCH 1326/2244] Remove unnecessary error logs When a call to a packet or frame sink fails, do not log the error on the caller side: either the "failure" is expected (explicitly stopped) or it must be logged by the packet or frame sink implementation. --- app/src/decoder.c | 3 --- app/src/demuxer.c | 3 --- 2 files changed, 6 deletions(-) diff --git a/app/src/decoder.c b/app/src/decoder.c index 9424ec1d..337aa329 100644 --- a/app/src/decoder.c +++ b/app/src/decoder.c @@ -29,7 +29,6 @@ sc_decoder_open_sinks(struct sc_decoder *decoder) { for (unsigned i = 0; i < decoder->sink_count; ++i) { struct sc_frame_sink *sink = decoder->sinks[i]; if (!sink->ops->open(sink)) { - LOGE("Could not open frame sink %d", i); sc_decoder_close_first_sinks(decoder, i); return false; } @@ -63,7 +62,6 @@ sc_decoder_open(struct sc_decoder *decoder, const AVCodec *codec) { } if (!sc_decoder_open_sinks(decoder)) { - LOGE("Could not open decoder sinks"); av_frame_free(&decoder->frame); avcodec_close(decoder->codec_ctx); avcodec_free_context(&decoder->codec_ctx); @@ -86,7 +84,6 @@ push_frame_to_sinks(struct sc_decoder *decoder, const AVFrame *frame) { for (unsigned i = 0; i < decoder->sink_count; ++i) { struct sc_frame_sink *sink = decoder->sinks[i]; if (!sink->ops->push(sink, frame)) { - LOGE("Could not send frame to sink %d", i); return false; } } diff --git a/app/src/demuxer.c b/app/src/demuxer.c index 3b26415b..c83d6bfa 100644 --- a/app/src/demuxer.c +++ b/app/src/demuxer.c @@ -111,7 +111,6 @@ push_packet_to_sinks(struct sc_demuxer *demuxer, const AVPacket *packet) { for (unsigned i = 0; i < demuxer->sink_count; ++i) { struct sc_packet_sink *sink = demuxer->sinks[i]; if (!sink->ops->push(sink, packet)) { - LOGE("Could not send packet to sink %d", i); return false; } } @@ -148,7 +147,6 @@ sc_demuxer_open_sinks(struct sc_demuxer *demuxer, const AVCodec *codec) { for (unsigned i = 0; i < demuxer->sink_count; ++i) { struct sc_packet_sink *sink = demuxer->sinks[i]; if (!sink->ops->open(sink, codec)) { - LOGE("Could not open packet sink %d", i); sc_demuxer_close_first_sinks(demuxer, i); return false; } @@ -184,7 +182,6 @@ run_demuxer(void *data) { } if (!sc_demuxer_open_sinks(demuxer, codec)) { - LOGE("Could not open demuxer sinks"); goto end; } From 5cf86ef7ffb374a8a1d0426b801c97f759375f5d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 19 Feb 2023 15:59:05 +0100 Subject: [PATCH 1327/2244] Move finally-block to fix deadlock on stop DesktopConnection implements Closeable, so it is implicitly closed after its try-with-resources block. Closing the DesktopConnection shutdowns the sockets, so it is necessary in particular to wake up blocking read() calls from the controller. But the controller thread was joined before the DesktopConnection was closed, causing a deadlock. To fix the problem, join the controller thread only after the DesktopConnection is closed. Refs 400a1c69b198103feff9682895529529890c1acc --- .../java/com/genymobile/scrcpy/Server.java | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 0aff79bc..4f9fd55f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -84,6 +84,8 @@ public final class Server { Workarounds.fillAppInfo(); } + Controller controller = null; + try (DesktopConnection connection = DesktopConnection.open(scid, tunnelForward, control, sendDummyByte)) { VideoCodec codec = options.getCodec(); if (options.getSendDeviceMeta()) { @@ -93,7 +95,6 @@ public final class Server { ScreenEncoder screenEncoder = new ScreenEncoder(codec.getMimeType(), options.getBitRate(), options.getMaxFps(), codecOptions, options.getEncoderName(), options.getDownsizeOnError()); - Controller controller = null; if (control) { controller = new Controller(device, connection, options.getClipboardAutosync(), options.getPowerOn()); controller.start(); @@ -114,21 +115,21 @@ public final class Server { if (!IO.isBrokenPipe(e)) { Ln.e("Video encoding error", e); } - } finally { - Ln.d("Screen streaming stopped"); - initThread.interrupt(); - if (controller != null) { - controller.stop(); - } + } + } finally { + Ln.d("Screen streaming stopped"); + initThread.interrupt(); + if (controller != null) { + controller.stop(); + } - try { - initThread.join(); - if (controller != null) { - controller.join(); - } - } catch (InterruptedException e) { - // ignore + try { + initThread.join(); + if (controller != null) { + controller.join(); } + } catch (InterruptedException e) { + // ignore } } } From d5dff239c858d22e8f8aad7dc51524e149561299 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 19 Feb 2023 15:30:46 +0100 Subject: [PATCH 1328/2244] Suggest commands with an explicit '=' --- server/src/main/java/com/genymobile/scrcpy/Server.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 4f9fd55f..5a092061 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -306,7 +306,7 @@ public final class Server { 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); + Ln.e(" scrcpy --display=" + id); } } } else if (e instanceof InvalidEncoderException) { @@ -315,7 +315,7 @@ public final class Server { if (encoders != null && encoders.length > 0) { Ln.e("Try to use one of the available encoders:"); for (MediaCodecInfo encoder : encoders) { - Ln.e(" scrcpy --encoder '" + encoder.getName() + "'"); + Ln.e(" scrcpy --encoder='" + encoder.getName() + "'"); } } } From ebecbe6bc66f6d2c5d7fb18fe37641fdacb0a8df Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 19 Feb 2023 16:08:08 +0100 Subject: [PATCH 1329/2244] Fix inconsistent quotes The encoder name started with a simple quote but ended with a double quote. Use a single quote for both. --- .../java/com/genymobile/scrcpy/InvalidEncoderException.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/InvalidEncoderException.java b/server/src/main/java/com/genymobile/scrcpy/InvalidEncoderException.java index 1efd2989..b38e29b1 100644 --- a/server/src/main/java/com/genymobile/scrcpy/InvalidEncoderException.java +++ b/server/src/main/java/com/genymobile/scrcpy/InvalidEncoderException.java @@ -8,7 +8,7 @@ public class InvalidEncoderException extends RuntimeException { private final MediaCodecInfo[] availableEncoders; public InvalidEncoderException(String name, MediaCodecInfo[] availableEncoders) { - super("There is no encoder having name '" + name + '"'); + super("There is no encoder having name '" + name + "'"); this.name = name; this.availableEncoders = availableEncoders; } From 0a151b96fe7f2331a75306341369acc3dc638ebf Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 20 Feb 2023 20:48:59 +0100 Subject: [PATCH 1330/2244] Accept muxing AV1 into MP4 container MP4 supports AV1. Refs d2dce5103832d6a9fc29f56a9795c22e44ceafa1 --- app/src/cli.c | 7 ------- 1 file changed, 7 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index 93712113..12c4b701 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -1749,13 +1749,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } } - if (opts->record_format == SC_RECORD_FORMAT_MP4 - && opts->codec == SC_CODEC_AV1) { - LOGE("Could not mux AV1 stream into MP4 container " - "(record to mkv or select another video codec)"); - return false; - } - if (!opts->control) { if (opts->turn_screen_off) { LOGE("Could not request to turn screen off if control is disabled"); From 5973d4cdd7a355b6e73997e0a7d95bc41bc1e107 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 21 Feb 2023 12:09:42 +0100 Subject: [PATCH 1331/2244] Initialize mouse_input_mode explicitly The explicit initialization was missing. It had no consequences because SC_MOUSE_INPUT_MODE_INJECT == 0. Fixes #3749 --- app/src/options.c | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/options.c b/app/src/options.c index 525795ae..a75e584e 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -16,6 +16,7 @@ const struct scrcpy_options scrcpy_options_default = { .codec = SC_CODEC_H264, .record_format = SC_RECORD_FORMAT_AUTO, .keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_INJECT, + .mouse_input_mode = SC_MOUSE_INPUT_MODE_INJECT, .port_range = { .first = DEFAULT_LOCAL_PORT_RANGE_FIRST, .last = DEFAULT_LOCAL_PORT_RANGE_LAST, From 5d6bcc59662e520b48362163d65e4dabaaada965 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 21 Feb 2023 19:47:27 +0100 Subject: [PATCH 1332/2244] Use enum for long options constants This avoids to manually assign values. --- app/src/cli.c | 84 ++++++++++++++++++++++++++------------------------- 1 file changed, 43 insertions(+), 41 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index 12c4b701..1851bad6 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -17,47 +17,49 @@ #define STR_IMPL_(x) #x #define STR(x) STR_IMPL_(x) -#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 -#define OPT_DISPLAY_ID 1014 -#define OPT_ROTATION 1015 -#define OPT_RENDER_DRIVER 1016 -#define OPT_NO_MIPMAPS 1017 -#define OPT_CODEC_OPTIONS 1018 -#define OPT_FORCE_ADB_FORWARD 1019 -#define OPT_DISABLE_SCREENSAVER 1020 -#define OPT_SHORTCUT_MOD 1021 -#define OPT_NO_KEY_REPEAT 1022 -#define OPT_FORWARD_ALL_CLICKS 1023 -#define OPT_LEGACY_PASTE 1024 -#define OPT_ENCODER_NAME 1025 -#define OPT_POWER_OFF_ON_CLOSE 1026 -#define OPT_V4L2_SINK 1027 -#define OPT_DISPLAY_BUFFER 1028 -#define OPT_V4L2_BUFFER 1029 -#define OPT_TUNNEL_HOST 1030 -#define OPT_TUNNEL_PORT 1031 -#define OPT_NO_CLIPBOARD_AUTOSYNC 1032 -#define OPT_TCPIP 1033 -#define OPT_RAW_KEY_EVENTS 1034 -#define OPT_NO_DOWNSIZE_ON_ERROR 1035 -#define OPT_OTG 1036 -#define OPT_NO_CLEANUP 1037 -#define OPT_PRINT_FPS 1038 -#define OPT_NO_POWER_ON 1039 -#define OPT_CODEC 1040 +enum { + OPT_RENDER_EXPIRED_FRAMES = 1000, + OPT_WINDOW_TITLE, + OPT_PUSH_TARGET, + OPT_ALWAYS_ON_TOP, + OPT_CROP, + OPT_RECORD_FORMAT, + OPT_PREFER_TEXT, + OPT_WINDOW_X, + OPT_WINDOW_Y, + OPT_WINDOW_WIDTH, + OPT_WINDOW_HEIGHT, + OPT_WINDOW_BORDERLESS, + OPT_MAX_FPS, + OPT_LOCK_VIDEO_ORIENTATION, + OPT_DISPLAY_ID, + OPT_ROTATION, + OPT_RENDER_DRIVER, + OPT_NO_MIPMAPS, + OPT_CODEC_OPTIONS, + OPT_FORCE_ADB_FORWARD, + OPT_DISABLE_SCREENSAVER, + OPT_SHORTCUT_MOD, + OPT_NO_KEY_REPEAT, + OPT_FORWARD_ALL_CLICKS, + OPT_LEGACY_PASTE, + OPT_ENCODER_NAME, + OPT_POWER_OFF_ON_CLOSE, + OPT_V4L2_SINK, + OPT_DISPLAY_BUFFER, + OPT_V4L2_BUFFER, + OPT_TUNNEL_HOST, + OPT_TUNNEL_PORT, + OPT_NO_CLIPBOARD_AUTOSYNC, + OPT_TCPIP, + OPT_RAW_KEY_EVENTS, + OPT_NO_DOWNSIZE_ON_ERROR, + OPT_OTG, + OPT_NO_CLEANUP, + OPT_PRINT_FPS, + OPT_NO_POWER_ON, + OPT_CODEC, +}; struct sc_option { char shortopt; From 3e3756a323c947678daa753cd1b8e0b110a833d1 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 21 Feb 2023 19:48:28 +0100 Subject: [PATCH 1333/2244] Add auto-completion for --codec option Add missing command to bash and zsh completion scripts. --- app/data/bash-completion/scrcpy | 5 +++++ app/data/zsh-completion/_scrcpy | 1 + 2 files changed, 6 insertions(+) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index 0d3a2559..61573770 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -3,6 +3,7 @@ _scrcpy() { local opts=" --always-on-top -b --bit-rate= + --codec= --codec-options= --crop= -d --select-usb @@ -64,6 +65,10 @@ _scrcpy() { _init_completion -s || return case "$prev" in + --codec) + COMPREPLY=($(compgen -W 'h264 h265 av1' -- "$cur")) + return + ;; --lock-video-orientation) COMPREPLY=($(compgen -W 'unlocked initial 0 1 2 3' -- "$cur")) return diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 56c13fd0..c57111cc 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -10,6 +10,7 @@ local arguments arguments=( '--always-on-top[Make scrcpy window always on top \(above other windows\)]' {-b,--bit-rate=}'[Encode the video at the given bit-rate]' + '--codec=[Select the video codec]:codec:(h264 h265 av1)' '--codec-options=[Set a list of comma-separated key\:type=value options for the device encoder]' '--crop=[\[width\:height\:x\:y\] Crop the device screen on the server]' {-d,--select-usb}'[Use USB device]' From 3d10fbd9b4b9451ecca5d7305d1e5c42416d63ad Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 21 Feb 2023 19:50:01 +0100 Subject: [PATCH 1334/2244] Fix --bit-rate option in bash completion script The option is --bit-rate, not --bitrate. --- app/data/bash-completion/scrcpy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index 61573770..0ddd8bcb 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -103,7 +103,7 @@ _scrcpy() { COMPREPLY=($(compgen -W "$("${ADB:-adb}" devices | awk '$2 == "device" {print $1}')" -- ${cur})) return ;; - -b|--bitrate \ + -b|--bit-rate \ |--codec-options \ |--crop \ |--display \ From 0cea7fb24cf83b04797a96f579fbc34ab8c3184b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 27 Feb 2023 19:54:43 +0100 Subject: [PATCH 1335/2244] Fix WSAStartup() error check on Windows --- app/src/util/net.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/util/net.c b/app/src/util/net.c index ed4882cd..c762a10f 100644 --- a/app/src/util/net.c +++ b/app/src/util/net.c @@ -30,8 +30,8 @@ bool net_init(void) { #ifdef _WIN32 WSADATA wsa; - int res = WSAStartup(MAKEWORD(2, 2), &wsa) < 0; - if (res < 0) { + int res = WSAStartup(MAKEWORD(2, 2), &wsa); + if (res) { LOGE("WSAStartup failed with error %d", res); return false; } From 0702be86d8b0dbdba2a74f4a0638f40457a30e51 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 27 Feb 2023 19:58:20 +0100 Subject: [PATCH 1336/2244] Accept Windows Sockets from version 1.1 Version 2.2 is probably not necessary (1.1 is the version required by FFmpeg when network is enabled). Refs Refs --- app/src/util/net.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/util/net.c b/app/src/util/net.c index c762a10f..67317ead 100644 --- a/app/src/util/net.c +++ b/app/src/util/net.c @@ -30,7 +30,7 @@ bool net_init(void) { #ifdef _WIN32 WSADATA wsa; - int res = WSAStartup(MAKEWORD(2, 2), &wsa); + int res = WSAStartup(MAKEWORD(1, 1), &wsa); if (res) { LOGE("WSAStartup failed with error %d", res); return false; From 8e8b039a633a848bd2498cb6c937ba5d99972cc2 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 27 Feb 2023 20:03:04 +0100 Subject: [PATCH 1337/2244] Do not use avformat network Scrcpy does not use FFmpeg network features. Initialize network locally instead (useful only for Windows). The include block has been moved to fix the following warning: Please include winsock2.h before windows.h --- app/src/main.c | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/app/src/main.c b/app/src/main.c index b3a468cc..185f1d8f 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -4,10 +4,6 @@ #include #include #include -#ifdef _WIN32 -#include -#include "util/str.h" -#endif #ifdef HAVE_V4L2 # include #endif @@ -19,8 +15,14 @@ #include "scrcpy.h" #include "usb/scrcpy_otg.h" #include "util/log.h" +#include "util/net.h" #include "version.h" +#ifdef _WIN32 +#include +#include "util/str.h" +#endif + int main_scrcpy(int argc, char *argv[]) { #ifdef _WIN32 @@ -69,7 +71,7 @@ main_scrcpy(int argc, char *argv[]) { } #endif - if (avformat_network_init()) { + if (!net_init()) { return SCRCPY_EXIT_FAILURE; } @@ -80,8 +82,6 @@ main_scrcpy(int argc, char *argv[]) { enum scrcpy_exit_code ret = scrcpy(&args.opts); #endif - avformat_network_deinit(); // ignore failure - return ret; } From 6b422e21bf82d5b2a7913598d14af2da5573173e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 27 Feb 2023 20:51:12 +0100 Subject: [PATCH 1338/2244] Fix error message on icon loading failure --- app/src/icon.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/icon.c b/app/src/icon.c index e709678f..a8588dd8 100644 --- a/app/src/icon.c +++ b/app/src/icon.c @@ -69,7 +69,7 @@ decode_image(const char *path) { } if (avformat_open_input(&ctx, path, NULL, NULL) < 0) { - LOGE("Could not open image codec: %s", path); + LOGE("Could not open icon image: %s", path); goto free_ctx; } From 3c3c07db05088963210c3522b21069dbe72d429c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 28 Feb 2023 12:34:34 +0100 Subject: [PATCH 1339/2244] Initialize server->serial in all cases Running scrcpy --tcpip on a device already connected via TCP/IP did not initialize server->serial. As a consequence, in debug mode, an assertion failed: scrcpy: ../app/src/server.c:770: run_server: Assertion `server->serial' failed. In release mode, scrcpy failed with this error: adb: -s requires an argument --- app/src/server.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/server.c b/app/src/server.c index 96bff77e..83c177dc 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -689,6 +689,11 @@ sc_server_configure_tcpip_unknown_address(struct sc_server *server, if (is_already_tcpip) { // Nothing to do LOGI("Device already connected via TCP/IP: %s", serial); + server->serial = strdup(serial); + if (!server->serial) { + LOG_OOM(); + return false; + } return true; } From 389dd77b5060af9989527729bad5f72c0818d2e6 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 28 Feb 2023 23:52:26 +0100 Subject: [PATCH 1340/2244] Fix MIN/MAX macros Expressions like "x < MAX(y, z)" were broken. --- app/src/common.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/common.h b/app/src/common.h index dccc8316..0382d094 100644 --- a/app/src/common.h +++ b/app/src/common.h @@ -5,8 +5,8 @@ #include "compat.h" #define ARRAY_LEN(a) (sizeof(a) / sizeof(a[0])) -#define MIN(X,Y) (X) < (Y) ? (X) : (Y) -#define MAX(X,Y) (X) > (Y) ? (X) : (Y) +#define MIN(X,Y) ((X) < (Y) ? (X) : (Y)) +#define MAX(X,Y) ((X) > (Y) ? (X) : (Y)) #define CLAMP(V,X,Y) MIN( MAX((V),(X)), (Y) ) #define container_of(ptr, type, member) \ From b5d41ad4f6017cf76fb7fad9e1af919eac34330f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 3 Mar 2023 11:03:48 +0100 Subject: [PATCH 1341/2244] Fix useless garbage initialization The variable `p` was initialized with a garbage value (a `const char **` casted to `char *`). Fortunately, it was never read. Refs --- 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 83c177dc..c9164972 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -206,7 +206,7 @@ execute_server(struct sc_server *server, unsigned dyn_idx = count; // from there, the strings are allocated #define ADD_PARAM(fmt, ...) { \ - char *p = (char *) &cmd[count]; \ + char *p; \ if (asprintf(&p, fmt, ## __VA_ARGS__) == -1) { \ goto end; \ } \ From a252194161a20b7aaf30e4f4a16f99f0057ac9e0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 26 Feb 2023 22:09:54 +0100 Subject: [PATCH 1342/2244] Upgrade gradle build tools to 7.4.0 Plugin version 7.4.0. Gradle version 7.5. Refs --- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index ecc7972e..f7e29b22 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.2.2' + classpath 'com.android.tools.build:gradle:7.4.0' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 669386b8..2ec77e51 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-7.3.3-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 5bf52a98ed701ca8bb9ce1b9e769acedf9fa3315 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 26 Feb 2023 22:40:26 +0100 Subject: [PATCH 1343/2244] Remove manifest package name As reported by gradle: > Setting the namespace via a source AndroidManifest.xml's package > attribute is deprecated. > > Please instead set the namespace (or testNamespace) in the module's > build.gradle file, as described here: > https://developer.android.com/studio/build/configure-app-module#set-namespace --- server/build.gradle | 1 + server/src/main/AndroidManifest.xml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/server/build.gradle b/server/build.gradle index 44bd78e8..1d80d15f 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -1,6 +1,7 @@ apply plugin: 'com.android.application' android { + namespace 'com.genymobile.scrcpy' compileSdkVersion 33 defaultConfig { applicationId "com.genymobile.scrcpy" diff --git a/server/src/main/AndroidManifest.xml b/server/src/main/AndroidManifest.xml index ccd69d2f..a94ad86b 100644 --- a/server/src/main/AndroidManifest.xml +++ b/server/src/main/AndroidManifest.xml @@ -1,2 +1,2 @@ - + From 14a85fd61e8fba2f4cfbf97dfd639d19b7adb70e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 26 Feb 2023 22:36:13 +0100 Subject: [PATCH 1344/2244] Silence lint warning about constant in API 29 MediaFormat.MIMETYPE_VIDEO_AV1 has been added in API 29, but it is not a problem to inline the constant in older versions. --- server/src/main/java/com/genymobile/scrcpy/VideoCodec.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/src/main/java/com/genymobile/scrcpy/VideoCodec.java b/server/src/main/java/com/genymobile/scrcpy/VideoCodec.java index 809d7dbc..e19b27f0 100644 --- a/server/src/main/java/com/genymobile/scrcpy/VideoCodec.java +++ b/server/src/main/java/com/genymobile/scrcpy/VideoCodec.java @@ -1,10 +1,12 @@ package com.genymobile.scrcpy; +import android.annotation.SuppressLint; import android.media.MediaFormat; public enum VideoCodec { H264(0x68_32_36_34, "h264", MediaFormat.MIMETYPE_VIDEO_AVC), H265(0x68_32_36_35, "h265", MediaFormat.MIMETYPE_VIDEO_HEVC), + @SuppressLint("InlinedApi") // introduced in API 21 AV1(0x00_61_76_31, "av1", MediaFormat.MIMETYPE_VIDEO_AV1); private final int id; // 4-byte ASCII representation of the name From a20615066d435cd3160f80196d25a2d7fb3e8ccc Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 27 Feb 2023 21:43:34 +0100 Subject: [PATCH 1345/2244] Simplify libusb prebuilt scripts In theory, include/ might be slightly different for win32 and win64 builds. Use each one separately to simplify. --- app/meson.build | 5 ++--- app/prebuilt-deps/prepare-libusb.sh | 7 +++---- cross_win32.txt | 3 +-- cross_win64.txt | 3 +-- release.mk | 4 ++-- 5 files changed, 9 insertions(+), 13 deletions(-) diff --git a/app/meson.build b/app/meson.build index 5d779756..a16a000b 100644 --- a/app/meson.build +++ b/app/meson.build @@ -144,9 +144,8 @@ else ) prebuilt_libusb = meson.get_cross_property('prebuilt_libusb') - prebuilt_libusb_root = meson.get_cross_property('prebuilt_libusb_root') - libusb_bin_dir = meson.current_source_dir() + '/prebuilt-deps/data/' + prebuilt_libusb - libusb_include_dir = 'prebuilt-deps/data/' + prebuilt_libusb_root + '/include' + libusb_bin_dir = meson.current_source_dir() + '/prebuilt-deps/data/' + prebuilt_libusb + '/bin' + libusb_include_dir = 'prebuilt-deps/data/' + prebuilt_libusb + '/include' libusb = declare_dependency( dependencies: [ diff --git a/app/prebuilt-deps/prepare-libusb.sh b/app/prebuilt-deps/prepare-libusb.sh index a0c3721d..47cf1df4 100755 --- a/app/prebuilt-deps/prepare-libusb.sh +++ b/app/prebuilt-deps/prepare-libusb.sh @@ -22,13 +22,12 @@ get_file "https://github.com/libusb/libusb/releases/download/v1.0.26/$FILENAME" mkdir "$DEP_DIR" cd "$DEP_DIR" -# include/ is the same in all folders of the archive 7z x "../$FILENAME" \ libusb-1.0.26-binaries/libusb-MinGW-Win32/bin/msys-usb-1.0.dll \ + libusb-1.0.26-binaries/libusb-MinGW-Win32/include/ \ libusb-1.0.26-binaries/libusb-MinGW-x64/bin/msys-usb-1.0.dll \ libusb-1.0.26-binaries/libusb-MinGW-x64/include/ -mv libusb-1.0.26-binaries/libusb-MinGW-Win32/bin MinGW-Win32 -mv libusb-1.0.26-binaries/libusb-MinGW-x64/bin MinGW-x64 -mv libusb-1.0.26-binaries/libusb-MinGW-x64/include . +mv libusb-1.0.26-binaries/libusb-MinGW-Win32 . +mv libusb-1.0.26-binaries/libusb-MinGW-x64 . rm -rf libusb-1.0.26-binaries diff --git a/cross_win32.txt b/cross_win32.txt index 32226949..e50c0bc8 100644 --- a/cross_win32.txt +++ b/cross_win32.txt @@ -21,5 +21,4 @@ ffmpeg_avformat = 'avformat-58' ffmpeg_avutil = 'avutil-56' prebuilt_ffmpeg = 'ffmpeg-win32-4.3.1' prebuilt_sdl2 = 'SDL2-2.26.1/i686-w64-mingw32' -prebuilt_libusb_root = 'libusb-1.0.26' -prebuilt_libusb = 'libusb-1.0.26/MinGW-Win32' +prebuilt_libusb = 'libusb-1.0.26/libusb-MinGW-Win32' diff --git a/cross_win64.txt b/cross_win64.txt index 4dde4ab1..2dc876a6 100644 --- a/cross_win64.txt +++ b/cross_win64.txt @@ -21,5 +21,4 @@ ffmpeg_avformat = 'avformat-59' ffmpeg_avutil = 'avutil-57' prebuilt_ffmpeg = 'ffmpeg-win64-5.1.2' prebuilt_sdl2 = 'SDL2-2.26.1/x86_64-w64-mingw32' -prebuilt_libusb_root = 'libusb-1.0.26' -prebuilt_libusb = 'libusb-1.0.26/MinGW-x64' +prebuilt_libusb = 'libusb-1.0.26/libusb-MinGW-x64' diff --git a/release.mk b/release.mk index 06443e1a..67578022 100644 --- a/release.mk +++ b/release.mk @@ -109,7 +109,7 @@ dist-win32: build-server build-win32 cp app/prebuilt-deps/data/platform-tools-33.0.3/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-33.0.3/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/SDL2-2.26.1/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/libusb-1.0.26/MinGW-Win32/msys-usb-1.0.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/libusb-1.0.26/libusb-MinGW-Win32/bin/msys-usb-1.0.dll "$(DIST)/$(WIN32_TARGET_DIR)/" dist-win64: build-server build-win64 mkdir -p "$(DIST)/$(WIN64_TARGET_DIR)" @@ -128,7 +128,7 @@ dist-win64: build-server build-win64 cp app/prebuilt-deps/data/platform-tools-33.0.3/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-33.0.3/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/SDL2-2.26.1/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/libusb-1.0.26/MinGW-x64/msys-usb-1.0.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/libusb-1.0.26/libusb-MinGW-x64/bin/msys-usb-1.0.dll "$(DIST)/$(WIN64_TARGET_DIR)/" zip-win32: dist-win32 cd "$(DIST)"; \ From 0fc62bfcd63fdccba759c1421ca7e4b5a2278cb9 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 27 Feb 2023 21:43:48 +0100 Subject: [PATCH 1346/2244] Use minimal prebuilt FFmpeg for Windows On the scrcpy-deps repo, I built FFmpeg 5.1.2 binaries for Windows with only the features used by scrcpy. For comparison, here are the sizes of the dll for FFmpeg 5.1.2: - before: 89M - after: 4.7M It also allows to upgrade the old FFmpeg version (4.3.1) used for win32. Refs Refs --- app/meson.build | 11 ++---- app/prebuilt-deps/prepare-ffmpeg-win32.sh | 45 ----------------------- app/prebuilt-deps/prepare-ffmpeg-win64.sh | 36 ------------------ app/prebuilt-deps/prepare-ffmpeg.sh | 30 +++++++++++++++ cross_win32.txt | 5 +-- cross_win64.txt | 5 +-- release.mk | 36 ++++++++---------- 7 files changed, 50 insertions(+), 118 deletions(-) delete mode 100755 app/prebuilt-deps/prepare-ffmpeg-win32.sh delete mode 100755 app/prebuilt-deps/prepare-ffmpeg-win64.sh create mode 100755 app/prebuilt-deps/prepare-ffmpeg.sh diff --git a/app/meson.build b/app/meson.build index a16a000b..f070db72 100644 --- a/app/meson.build +++ b/app/meson.build @@ -129,16 +129,11 @@ else ffmpeg_bin_dir = meson.current_source_dir() + '/prebuilt-deps/data/' + prebuilt_ffmpeg + '/bin' ffmpeg_include_dir = 'prebuilt-deps/data/' + prebuilt_ffmpeg + '/include' - # ffmpeg versions are different for win32 and win64 builds - ffmpeg_avcodec = meson.get_cross_property('ffmpeg_avcodec') - ffmpeg_avformat = meson.get_cross_property('ffmpeg_avformat') - ffmpeg_avutil = meson.get_cross_property('ffmpeg_avutil') - ffmpeg = declare_dependency( dependencies: [ - cc.find_library(ffmpeg_avcodec, dirs: ffmpeg_bin_dir), - cc.find_library(ffmpeg_avformat, dirs: ffmpeg_bin_dir), - cc.find_library(ffmpeg_avutil, dirs: ffmpeg_bin_dir), + cc.find_library('avcodec-59', dirs: ffmpeg_bin_dir), + cc.find_library('avformat-59', dirs: ffmpeg_bin_dir), + cc.find_library('avutil-57', dirs: ffmpeg_bin_dir), ], include_directories: include_directories(ffmpeg_include_dir) ) diff --git a/app/prebuilt-deps/prepare-ffmpeg-win32.sh b/app/prebuilt-deps/prepare-ffmpeg-win32.sh deleted file mode 100755 index 2a6a3841..00000000 --- a/app/prebuilt-deps/prepare-ffmpeg-win32.sh +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env bash -set -e -DIR=$(dirname ${BASH_SOURCE[0]}) -cd "$DIR" -. common -mkdir -p "$PREBUILT_DATA_DIR" -cd "$PREBUILT_DATA_DIR" - -DEP_DIR=ffmpeg-win32-4.3.1 - -FILENAME_SHARED=ffmpeg-4.3.1-win32-shared.zip -SHA256SUM_SHARED=357af9901a456f4dcbacd107e83a934d344c9cb07ddad8aaf80612eeab7d26d2 - -FILENAME_DEV=ffmpeg-4.3.1-win32-dev.zip -SHA256SUM_DEV=230efb08e9bcf225bd474da29676c70e591fc94d8790a740ca801408fddcb78b - -if [[ -d "$DEP_DIR" ]] -then - echo "$DEP_DIR" found - exit 0 -fi - -get_file "https://github.com/Genymobile/scrcpy/releases/download/v1.16/$FILENAME_SHARED" \ - "$FILENAME_SHARED" "$SHA256SUM_SHARED" -get_file "https://github.com/Genymobile/scrcpy/releases/download/v1.16/$FILENAME_DEV" \ - "$FILENAME_DEV" "$SHA256SUM_DEV" - -mkdir "$DEP_DIR" -cd "$DEP_DIR" - -ZIP_PREFIX_SHARED=ffmpeg-4.3.1-win32-shared -unzip "../$FILENAME_SHARED" \ - "$ZIP_PREFIX_SHARED"/bin/avutil-56.dll \ - "$ZIP_PREFIX_SHARED"/bin/avcodec-58.dll \ - "$ZIP_PREFIX_SHARED"/bin/avformat-58.dll \ - "$ZIP_PREFIX_SHARED"/bin/swresample-3.dll \ - "$ZIP_PREFIX_SHARED"/bin/swscale-5.dll - -ZIP_PREFIX_DEV=ffmpeg-4.3.1-win32-dev -unzip "../$FILENAME_DEV" \ - "$ZIP_PREFIX_DEV/include/*" - -mv "$ZIP_PREFIX_SHARED"/* . -mv "$ZIP_PREFIX_DEV"/* . -rmdir "$ZIP_PREFIX_SHARED" "$ZIP_PREFIX_DEV" diff --git a/app/prebuilt-deps/prepare-ffmpeg-win64.sh b/app/prebuilt-deps/prepare-ffmpeg-win64.sh deleted file mode 100755 index f5d56e6f..00000000 --- a/app/prebuilt-deps/prepare-ffmpeg-win64.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env bash -set -e -DIR=$(dirname ${BASH_SOURCE[0]}) -cd "$DIR" -. common -mkdir -p "$PREBUILT_DATA_DIR" -cd "$PREBUILT_DATA_DIR" - -VERSION=5.1.2 -DEP_DIR=ffmpeg-win64-$VERSION - -FILENAME=ffmpeg-$VERSION-full_build-shared.7z -SHA256SUM=d9eb97b72d7cfdae4d0f7eaea59ccffb8c364d67d88018ea715d5e2e193f00e9 - -if [[ -d "$DEP_DIR" ]] -then - echo "$DEP_DIR" found - exit 0 -fi - -get_file "https://github.com/GyanD/codexffmpeg/releases/download/$VERSION/$FILENAME" \ - "$FILENAME" "$SHA256SUM" - -mkdir "$DEP_DIR" -cd "$DEP_DIR" - -ZIP_PREFIX=ffmpeg-$VERSION-full_build-shared -7z x "../$FILENAME" \ - "$ZIP_PREFIX"/bin/avutil-57.dll \ - "$ZIP_PREFIX"/bin/avcodec-59.dll \ - "$ZIP_PREFIX"/bin/avformat-59.dll \ - "$ZIP_PREFIX"/bin/swresample-4.dll \ - "$ZIP_PREFIX"/bin/swscale-6.dll \ - "$ZIP_PREFIX"/include -mv "$ZIP_PREFIX"/* . -rmdir "$ZIP_PREFIX" diff --git a/app/prebuilt-deps/prepare-ffmpeg.sh b/app/prebuilt-deps/prepare-ffmpeg.sh new file mode 100755 index 00000000..dc8b1ca2 --- /dev/null +++ b/app/prebuilt-deps/prepare-ffmpeg.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +set -e +DIR=$(dirname ${BASH_SOURCE[0]}) +cd "$DIR" +. common +mkdir -p "$PREBUILT_DATA_DIR" +cd "$PREBUILT_DATA_DIR" + +VERSION=5.1.2-scrcpy +DEP_DIR="ffmpeg-$VERSION" + +FILENAME="$DEP_DIR".7z +SHA256SUM=93f32ffc29ddb3466d669f7078d3fd8030c3388bc8a18bcfeefb6428fc5ceef1 + +if [[ -d "$DEP_DIR" ]] +then + echo "$DEP_DIR" found + exit 0 +fi + +get_file "https://github.com/rom1v/scrcpy-deps/releases/download/$VERSION/$FILENAME" \ + "$FILENAME" "$SHA256SUM" + +mkdir "$DEP_DIR" +cd "$DEP_DIR" + +ZIP_PREFIX=ffmpeg +7z x "../$FILENAME" +mv "$ZIP_PREFIX"/* . +rmdir "$ZIP_PREFIX" diff --git a/cross_win32.txt b/cross_win32.txt index e50c0bc8..f89463d9 100644 --- a/cross_win32.txt +++ b/cross_win32.txt @@ -16,9 +16,6 @@ cpu = 'i686' endian = 'little' [properties] -ffmpeg_avcodec = 'avcodec-58' -ffmpeg_avformat = 'avformat-58' -ffmpeg_avutil = 'avutil-56' -prebuilt_ffmpeg = 'ffmpeg-win32-4.3.1' +prebuilt_ffmpeg = 'ffmpeg-5.1.2-scrcpy/win32' prebuilt_sdl2 = 'SDL2-2.26.1/i686-w64-mingw32' prebuilt_libusb = 'libusb-1.0.26/libusb-MinGW-Win32' diff --git a/cross_win64.txt b/cross_win64.txt index 2dc876a6..c30c46f2 100644 --- a/cross_win64.txt +++ b/cross_win64.txt @@ -16,9 +16,6 @@ cpu = 'x86_64' endian = 'little' [properties] -ffmpeg_avcodec = 'avcodec-59' -ffmpeg_avformat = 'avformat-59' -ffmpeg_avutil = 'avutil-57' -prebuilt_ffmpeg = 'ffmpeg-win64-5.1.2' +prebuilt_ffmpeg = 'ffmpeg-5.1.2-scrcpy/win64' prebuilt_sdl2 = 'SDL2-2.26.1/x86_64-w64-mingw32' prebuilt_libusb = 'libusb-1.0.26/libusb-MinGW-x64' diff --git a/release.mk b/release.mk index 67578022..9c2820c7 100644 --- a/release.mk +++ b/release.mk @@ -11,7 +11,7 @@ .PHONY: default clean \ test \ build-server \ - prepare-deps-win32 prepare-deps-win64 \ + prepare-deps \ build-win32 build-win64 \ dist-win32 dist-win64 \ zip-win32 zip-win64 \ @@ -62,19 +62,13 @@ build-server: meson setup "$(SERVER_BUILD_DIR)" --buildtype release -Dcompile_app=false ) ninja -C "$(SERVER_BUILD_DIR)" -prepare-deps-win32: +prepare-deps: @app/prebuilt-deps/prepare-adb.sh @app/prebuilt-deps/prepare-sdl.sh - @app/prebuilt-deps/prepare-ffmpeg-win32.sh + @app/prebuilt-deps/prepare-ffmpeg.sh @app/prebuilt-deps/prepare-libusb.sh -prepare-deps-win64: - @app/prebuilt-deps/prepare-adb.sh - @app/prebuilt-deps/prepare-sdl.sh - @app/prebuilt-deps/prepare-ffmpeg-win64.sh - @app/prebuilt-deps/prepare-libusb.sh - -build-win32: prepare-deps-win32 +build-win32: prepare-deps [ -d "$(WIN32_BUILD_DIR)" ] || ( mkdir "$(WIN32_BUILD_DIR)" && \ meson setup "$(WIN32_BUILD_DIR)" \ --cross-file cross_win32.txt \ @@ -83,7 +77,7 @@ build-win32: prepare-deps-win32 -Dportable=true ) ninja -C "$(WIN32_BUILD_DIR)" -build-win64: prepare-deps-win64 +build-win64: prepare-deps [ -d "$(WIN64_BUILD_DIR)" ] || ( mkdir "$(WIN64_BUILD_DIR)" && \ meson setup "$(WIN64_BUILD_DIR)" \ --cross-file cross_win64.txt \ @@ -100,11 +94,11 @@ dist-win32: build-server build-win32 cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN32_TARGET_DIR)" cp app/data/icon.png "$(DIST)/$(WIN32_TARGET_DIR)" cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN32_TARGET_DIR)" - cp app/prebuilt-deps/data/ffmpeg-win32-4.3.1/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-win32-4.3.1/bin/avcodec-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-win32-4.3.1/bin/avformat-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-win32-4.3.1/bin/swresample-3.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-win32-4.3.1/bin/swscale-5.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-5.1.2-scrcpy/win32/bin/avutil-57.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-5.1.2-scrcpy/win32/bin/avcodec-59.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-5.1.2-scrcpy/win32/bin/avformat-59.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-5.1.2-scrcpy/win32/bin/swresample-4.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-5.1.2-scrcpy/win32/bin/zlib1.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-33.0.3/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-33.0.3/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-33.0.3/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" @@ -119,11 +113,11 @@ dist-win64: build-server build-win64 cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN64_TARGET_DIR)" cp app/data/icon.png "$(DIST)/$(WIN64_TARGET_DIR)" cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN64_TARGET_DIR)" - cp app/prebuilt-deps/data/ffmpeg-win64-5.1.2/bin/avutil-57.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-win64-5.1.2/bin/avcodec-59.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-win64-5.1.2/bin/avformat-59.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-win64-5.1.2/bin/swresample-4.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-win64-5.1.2/bin/swscale-6.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-5.1.2-scrcpy/win64/bin/avutil-57.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-5.1.2-scrcpy/win64/bin/avcodec-59.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-5.1.2-scrcpy/win64/bin/avformat-59.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-5.1.2-scrcpy/win64/bin/swresample-4.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-5.1.2-scrcpy/win64/bin/zlib1.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-33.0.3/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-33.0.3/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-33.0.3/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" From 9d60d7880bdc467545c883ccfc8d2609927894cb Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 28 Feb 2023 11:56:42 +0100 Subject: [PATCH 1347/2244] Upgrade FFmpeg (6.0) for Windows Use the latest version (specifically built for scrcpy). Refs --- app/meson.build | 6 +++--- app/prebuilt-deps/prepare-ffmpeg.sh | 4 ++-- cross_win32.txt | 2 +- cross_win64.txt | 2 +- release.mk | 20 ++++++++++---------- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/app/meson.build b/app/meson.build index f070db72..b6a772a9 100644 --- a/app/meson.build +++ b/app/meson.build @@ -131,9 +131,9 @@ else ffmpeg = declare_dependency( dependencies: [ - cc.find_library('avcodec-59', dirs: ffmpeg_bin_dir), - cc.find_library('avformat-59', dirs: ffmpeg_bin_dir), - cc.find_library('avutil-57', dirs: ffmpeg_bin_dir), + cc.find_library('avcodec-60', dirs: ffmpeg_bin_dir), + cc.find_library('avformat-60', dirs: ffmpeg_bin_dir), + cc.find_library('avutil-58', dirs: ffmpeg_bin_dir), ], include_directories: include_directories(ffmpeg_include_dir) ) diff --git a/app/prebuilt-deps/prepare-ffmpeg.sh b/app/prebuilt-deps/prepare-ffmpeg.sh index dc8b1ca2..10e33903 100755 --- a/app/prebuilt-deps/prepare-ffmpeg.sh +++ b/app/prebuilt-deps/prepare-ffmpeg.sh @@ -6,11 +6,11 @@ cd "$DIR" mkdir -p "$PREBUILT_DATA_DIR" cd "$PREBUILT_DATA_DIR" -VERSION=5.1.2-scrcpy +VERSION=6.0-scrcpy DEP_DIR="ffmpeg-$VERSION" FILENAME="$DEP_DIR".7z -SHA256SUM=93f32ffc29ddb3466d669f7078d3fd8030c3388bc8a18bcfeefb6428fc5ceef1 +SHA256SUM=f3956295b4325a84aada05447ba3f314fbed96697811666d495de4de40d59f98 if [[ -d "$DEP_DIR" ]] then diff --git a/cross_win32.txt b/cross_win32.txt index f89463d9..deb70b77 100644 --- a/cross_win32.txt +++ b/cross_win32.txt @@ -16,6 +16,6 @@ cpu = 'i686' endian = 'little' [properties] -prebuilt_ffmpeg = 'ffmpeg-5.1.2-scrcpy/win32' +prebuilt_ffmpeg = 'ffmpeg-6.0-scrcpy/win32' prebuilt_sdl2 = 'SDL2-2.26.1/i686-w64-mingw32' prebuilt_libusb = 'libusb-1.0.26/libusb-MinGW-Win32' diff --git a/cross_win64.txt b/cross_win64.txt index c30c46f2..3c4409dc 100644 --- a/cross_win64.txt +++ b/cross_win64.txt @@ -16,6 +16,6 @@ cpu = 'x86_64' endian = 'little' [properties] -prebuilt_ffmpeg = 'ffmpeg-5.1.2-scrcpy/win64' +prebuilt_ffmpeg = 'ffmpeg-6.0-scrcpy/win64' prebuilt_sdl2 = 'SDL2-2.26.1/x86_64-w64-mingw32' prebuilt_libusb = 'libusb-1.0.26/libusb-MinGW-x64' diff --git a/release.mk b/release.mk index 9c2820c7..f1084bd4 100644 --- a/release.mk +++ b/release.mk @@ -94,11 +94,11 @@ dist-win32: build-server build-win32 cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN32_TARGET_DIR)" cp app/data/icon.png "$(DIST)/$(WIN32_TARGET_DIR)" cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN32_TARGET_DIR)" - cp app/prebuilt-deps/data/ffmpeg-5.1.2-scrcpy/win32/bin/avutil-57.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-5.1.2-scrcpy/win32/bin/avcodec-59.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-5.1.2-scrcpy/win32/bin/avformat-59.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-5.1.2-scrcpy/win32/bin/swresample-4.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-5.1.2-scrcpy/win32/bin/zlib1.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy/win32/bin/avutil-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy/win32/bin/avcodec-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy/win32/bin/avformat-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy/win32/bin/swresample-4.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy/win32/bin/zlib1.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-33.0.3/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-33.0.3/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-33.0.3/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" @@ -113,11 +113,11 @@ dist-win64: build-server build-win64 cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN64_TARGET_DIR)" cp app/data/icon.png "$(DIST)/$(WIN64_TARGET_DIR)" cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN64_TARGET_DIR)" - cp app/prebuilt-deps/data/ffmpeg-5.1.2-scrcpy/win64/bin/avutil-57.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-5.1.2-scrcpy/win64/bin/avcodec-59.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-5.1.2-scrcpy/win64/bin/avformat-59.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-5.1.2-scrcpy/win64/bin/swresample-4.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-5.1.2-scrcpy/win64/bin/zlib1.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy/win64/bin/avutil-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy/win64/bin/avcodec-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy/win64/bin/avformat-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy/win64/bin/swresample-4.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy/win64/bin/zlib1.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-33.0.3/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-33.0.3/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-33.0.3/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" From f30fd963a104649e7d388cc68f3fbc5d635baa4a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 3 Mar 2023 19:46:13 +0100 Subject: [PATCH 1348/2244] Upgrade FFmpeg custom builds for Windows Use a build which includes the pcm_s16le decoder, to support RAW audio. Refs --- app/prebuilt-deps/prepare-ffmpeg.sh | 4 ++-- cross_win32.txt | 2 +- cross_win64.txt | 2 +- release.mk | 20 ++++++++++---------- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/app/prebuilt-deps/prepare-ffmpeg.sh b/app/prebuilt-deps/prepare-ffmpeg.sh index 10e33903..b156099a 100755 --- a/app/prebuilt-deps/prepare-ffmpeg.sh +++ b/app/prebuilt-deps/prepare-ffmpeg.sh @@ -6,11 +6,11 @@ cd "$DIR" mkdir -p "$PREBUILT_DATA_DIR" cd "$PREBUILT_DATA_DIR" -VERSION=6.0-scrcpy +VERSION=6.0-scrcpy-2 DEP_DIR="ffmpeg-$VERSION" FILENAME="$DEP_DIR".7z -SHA256SUM=f3956295b4325a84aada05447ba3f314fbed96697811666d495de4de40d59f98 +SHA256SUM=98ef97f8607c97a5c4f9c5a0a991b78f105d002a3619145011d16ffb92501b14 if [[ -d "$DEP_DIR" ]] then diff --git a/cross_win32.txt b/cross_win32.txt index deb70b77..a02e798a 100644 --- a/cross_win32.txt +++ b/cross_win32.txt @@ -16,6 +16,6 @@ cpu = 'i686' endian = 'little' [properties] -prebuilt_ffmpeg = 'ffmpeg-6.0-scrcpy/win32' +prebuilt_ffmpeg = 'ffmpeg-6.0-scrcpy-2/win32' prebuilt_sdl2 = 'SDL2-2.26.1/i686-w64-mingw32' prebuilt_libusb = 'libusb-1.0.26/libusb-MinGW-Win32' diff --git a/cross_win64.txt b/cross_win64.txt index 3c4409dc..126de36e 100644 --- a/cross_win64.txt +++ b/cross_win64.txt @@ -16,6 +16,6 @@ cpu = 'x86_64' endian = 'little' [properties] -prebuilt_ffmpeg = 'ffmpeg-6.0-scrcpy/win64' +prebuilt_ffmpeg = 'ffmpeg-6.0-scrcpy-2/win64' prebuilt_sdl2 = 'SDL2-2.26.1/x86_64-w64-mingw32' prebuilt_libusb = 'libusb-1.0.26/libusb-MinGW-x64' diff --git a/release.mk b/release.mk index f1084bd4..75e5a9c0 100644 --- a/release.mk +++ b/release.mk @@ -94,11 +94,11 @@ dist-win32: build-server build-win32 cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN32_TARGET_DIR)" cp app/data/icon.png "$(DIST)/$(WIN32_TARGET_DIR)" cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN32_TARGET_DIR)" - cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy/win32/bin/avutil-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy/win32/bin/avcodec-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy/win32/bin/avformat-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy/win32/bin/swresample-4.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy/win32/bin/zlib1.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win32/bin/avutil-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win32/bin/avcodec-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win32/bin/avformat-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win32/bin/swresample-4.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win32/bin/zlib1.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-33.0.3/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-33.0.3/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-33.0.3/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" @@ -113,11 +113,11 @@ dist-win64: build-server build-win64 cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN64_TARGET_DIR)" cp app/data/icon.png "$(DIST)/$(WIN64_TARGET_DIR)" cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN64_TARGET_DIR)" - cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy/win64/bin/avutil-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy/win64/bin/avcodec-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy/win64/bin/avformat-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy/win64/bin/swresample-4.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy/win64/bin/zlib1.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win64/bin/avutil-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win64/bin/avcodec-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win64/bin/avformat-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win64/bin/swresample-4.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win64/bin/zlib1.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-33.0.3/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-33.0.3/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-33.0.3/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" From 10e8295aea0163f80d88eab234be5b918a7571eb Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 23 Feb 2023 12:49:42 +0100 Subject: [PATCH 1349/2244] Move FFmpeg callback initialization Configure FFmpeg log redirection on start from a log helper. --- app/src/main.c | 2 ++ app/src/scrcpy.c | 39 --------------------------------------- app/src/util/log.c | 44 ++++++++++++++++++++++++++++++++++++++++++++ app/src/util/log.h | 3 +++ 4 files changed, 49 insertions(+), 39 deletions(-) diff --git a/app/src/main.c b/app/src/main.c index 185f1d8f..cc3a85a7 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -75,6 +75,8 @@ main_scrcpy(int argc, char *argv[]) { return SCRCPY_EXIT_FAILURE; } + sc_log_configure(); + #ifdef HAVE_USB enum scrcpy_exit_code ret = args.opts.otg ? scrcpy_otg(&args.opts) : scrcpy(&args.opts); diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 8932dd1d..afd04d6d 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -198,43 +198,6 @@ await_for_server(bool *connected) { return false; } -static SDL_LogPriority -sdl_priority_from_av_level(int level) { - switch (level) { - case AV_LOG_PANIC: - case AV_LOG_FATAL: - return SDL_LOG_PRIORITY_CRITICAL; - case AV_LOG_ERROR: - return SDL_LOG_PRIORITY_ERROR; - case AV_LOG_WARNING: - return SDL_LOG_PRIORITY_WARN; - case AV_LOG_INFO: - return SDL_LOG_PRIORITY_INFO; - } - // do not forward others, which are too verbose - return 0; -} - -static void -av_log_callback(void *avcl, int level, const char *fmt, va_list vl) { - (void) avcl; - SDL_LogPriority priority = sdl_priority_from_av_level(level); - if (priority == 0) { - return; - } - - size_t fmt_len = strlen(fmt); - char *local_fmt = malloc(fmt_len + 10); - if (!local_fmt) { - LOG_OOM(); - return; - } - memcpy(local_fmt, "[FFmpeg] ", 9); // do not write the final '\0' - memcpy(local_fmt + 9, fmt, fmt_len + 1); // include '\0' - SDL_LogMessageV(SDL_LOG_CATEGORY_VIDEO, priority, local_fmt, vl); - free(local_fmt); -} - static void sc_demuxer_on_ended(struct sc_demuxer *demuxer, bool eos, void *userdata) { (void) demuxer; @@ -426,8 +389,6 @@ scrcpy(struct scrcpy_options *options) { recorder_initialized = true; } - av_log_set_callback(av_log_callback); - static const struct sc_demuxer_callbacks demuxer_cbs = { .on_ended = sc_demuxer_on_ended, }; diff --git a/app/src/util/log.c b/app/src/util/log.c index 72cd2877..ef11d2d1 100644 --- a/app/src/util/log.c +++ b/app/src/util/log.c @@ -4,6 +4,7 @@ # include #endif #include +#include static SDL_LogPriority log_level_sc_to_sdl(enum sc_log_level level) { @@ -85,3 +86,46 @@ sc_log_windows_error(const char *prefix, int error) { return true; } #endif + +static SDL_LogPriority +sdl_priority_from_av_level(int level) { + switch (level) { + case AV_LOG_PANIC: + case AV_LOG_FATAL: + return SDL_LOG_PRIORITY_CRITICAL; + case AV_LOG_ERROR: + return SDL_LOG_PRIORITY_ERROR; + case AV_LOG_WARNING: + return SDL_LOG_PRIORITY_WARN; + case AV_LOG_INFO: + return SDL_LOG_PRIORITY_INFO; + } + // do not forward others, which are too verbose + return 0; +} + +static void +sc_av_log_callback(void *avcl, int level, const char *fmt, va_list vl) { + (void) avcl; + SDL_LogPriority priority = sdl_priority_from_av_level(level); + if (priority == 0) { + return; + } + + size_t fmt_len = strlen(fmt); + char *local_fmt = malloc(fmt_len + 10); + if (!local_fmt) { + LOG_OOM(); + return; + } + memcpy(local_fmt, "[FFmpeg] ", 9); // do not write the final '\0' + memcpy(local_fmt + 9, fmt, fmt_len + 1); // include '\0' + SDL_LogMessageV(SDL_LOG_CATEGORY_VIDEO, priority, local_fmt, vl); + free(local_fmt); +} + +void +sc_log_configure() { + // Redirect FFmpeg logs to SDL logs + av_log_set_callback(sc_av_log_callback); +} diff --git a/app/src/util/log.h b/app/src/util/log.h index 6bd8506c..8e1b73a2 100644 --- a/app/src/util/log.h +++ b/app/src/util/log.h @@ -35,4 +35,7 @@ bool sc_log_windows_error(const char *prefix, int error); #endif +void +sc_log_configure(); + #endif From e30e692b3640e5e8db4fb5c31445194d1fad31b6 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 23 Feb 2023 13:25:04 +0100 Subject: [PATCH 1350/2244] Print FFmpeg logs FFmpeg logs are redirected to a specific SDL log category. Initialize the log level for this category to print them as expected. --- app/src/util/log.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/util/log.c b/app/src/util/log.c index ef11d2d1..25b1f26e 100644 --- a/app/src/util/log.c +++ b/app/src/util/log.c @@ -48,6 +48,7 @@ void sc_set_log_level(enum sc_log_level level) { SDL_LogPriority sdl_log = log_level_sc_to_sdl(level); SDL_LogSetPriority(SDL_LOG_CATEGORY_APPLICATION, sdl_log); + SDL_LogSetPriority(SDL_LOG_CATEGORY_CUSTOM, sdl_log); } enum sc_log_level @@ -120,7 +121,7 @@ sc_av_log_callback(void *avcl, int level, const char *fmt, va_list vl) { } memcpy(local_fmt, "[FFmpeg] ", 9); // do not write the final '\0' memcpy(local_fmt + 9, fmt, fmt_len + 1); // include '\0' - SDL_LogMessageV(SDL_LOG_CATEGORY_VIDEO, priority, local_fmt, vl); + SDL_LogMessageV(SDL_LOG_CATEGORY_CUSTOM, priority, local_fmt, vl); free(local_fmt); } From c78254fcd1c8d88fbc258d2a50ee925c282c5c64 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 22 Feb 2023 18:41:22 +0100 Subject: [PATCH 1351/2244] Split server stop() and join() For consistency with the other components, call stop() and join() separately. This allows to stop all components, then join them all. --- app/src/scrcpy.c | 4 ++++ app/src/server.c | 3 +++ app/src/server.h | 4 ++++ 3 files changed, 11 insertions(+) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index afd04d6d..e96fa187 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -698,6 +698,10 @@ end: sc_file_pusher_destroy(&s->file_pusher); } + if (server_started) { + sc_server_join(&s->server); + } + sc_server_destroy(&s->server); return ret; diff --git a/app/src/server.c b/app/src/server.c index c9164972..413f02ee 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -909,7 +909,10 @@ sc_server_stop(struct sc_server *server) { sc_cond_signal(&server->cond_stopped); sc_intr_interrupt(&server->intr); sc_mutex_unlock(&server->mutex); +} +void +sc_server_join(struct sc_server *server) { sc_thread_join(&server->thread, NULL); } diff --git a/app/src/server.h b/app/src/server.h index d6b1401e..c05b1e5b 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -108,6 +108,10 @@ sc_server_start(struct sc_server *server); void sc_server_stop(struct sc_server *server); +// join the server thread +void +sc_server_join(struct sc_server *server); + // close and release sockets void sc_server_destroy(struct sc_server *server); From 9f8e96e895b8af51be2b1123a483c7d4d3decdf7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 22 Feb 2023 19:08:24 +0100 Subject: [PATCH 1352/2244] Fix --no-clipboard-autosync bash completion Fix typo. --- app/data/bash-completion/scrcpy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index 0ddd8bcb..4590b6a8 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -24,7 +24,7 @@ _scrcpy() { -M --hid-mouse -m --max-size= --no-cleanup - --no-clipboard-on-error + --no-clipboard-autosync --no-downsize-on-error -n --no-control -N --no-display From b43938fa66bb854e45c97a76b4600edecdb0fb9b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 19 Feb 2023 19:36:46 +0100 Subject: [PATCH 1353/2244] Do not print stacktraces when unnecessary User-friendly error messages are printed on specific configuration exceptions. In that case, do not print the stacktrace. Also handle the user-friendly error message directly where the error occurs, and print multiline messages in a single log call, to avoid confusing interleaving. --- .../scrcpy/ConfigurationException.java | 7 ++++ .../java/com/genymobile/scrcpy/Device.java | 18 +++++++++-- .../scrcpy/InvalidDisplayIdException.java | 21 ------------ .../scrcpy/InvalidEncoderException.java | 23 ------------- .../com/genymobile/scrcpy/ScreenEncoder.java | 20 +++++++++--- .../java/com/genymobile/scrcpy/Server.java | 32 ++++--------------- 6 files changed, 44 insertions(+), 77 deletions(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/ConfigurationException.java delete mode 100644 server/src/main/java/com/genymobile/scrcpy/InvalidDisplayIdException.java delete mode 100644 server/src/main/java/com/genymobile/scrcpy/InvalidEncoderException.java diff --git a/server/src/main/java/com/genymobile/scrcpy/ConfigurationException.java b/server/src/main/java/com/genymobile/scrcpy/ConfigurationException.java new file mode 100644 index 00000000..76c8f52e --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/ConfigurationException.java @@ -0,0 +1,7 @@ +package com.genymobile.scrcpy; + +public class ConfigurationException extends Exception { + public ConfigurationException(String message) { + super(message); + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index 30e64fd7..c7f7c1f8 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -61,12 +61,12 @@ public final class Device { private final boolean supportsInputEvents; - public Device(Options options) { + public Device(Options options) throws ConfigurationException { displayId = options.getDisplayId(); DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(displayId); if (displayInfo == null) { - int[] displayIds = ServiceManager.getDisplayManager().getDisplayIds(); - throw new InvalidDisplayIdException(displayId, displayIds); + Ln.e(buildUnknownDisplayIdMessage(displayId)); + throw new ConfigurationException("Unknown display id: " + displayId); } int displayInfoFlags = displayInfo.getFlags(); @@ -130,6 +130,18 @@ public final class Device { } } + private static String buildUnknownDisplayIdMessage(int displayId) { + StringBuilder msg = new StringBuilder("Display ").append(displayId).append(" not found"); + int[] displayIds = ServiceManager.getDisplayManager().getDisplayIds(); + if (displayIds != null && displayIds.length > 0) { + msg.append("\nTry to use one of the available display ids:"); + for (int id : displayIds) { + msg.append("\n scrcpy --display=").append(id); + } + } + return msg.toString(); + } + public synchronized void setMaxSize(int newMaxSize) { maxSize = newMaxSize; screenInfo = ScreenInfo.computeScreenInfo(screenInfo.getReverseVideoRotation(), deviceSize, crop, newMaxSize, lockVideoOrientation); diff --git a/server/src/main/java/com/genymobile/scrcpy/InvalidDisplayIdException.java b/server/src/main/java/com/genymobile/scrcpy/InvalidDisplayIdException.java deleted file mode 100644 index 81e3b903..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/InvalidDisplayIdException.java +++ /dev/null @@ -1,21 +0,0 @@ -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/InvalidEncoderException.java b/server/src/main/java/com/genymobile/scrcpy/InvalidEncoderException.java deleted file mode 100644 index b38e29b1..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/InvalidEncoderException.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.genymobile.scrcpy; - -import android.media.MediaCodecInfo; - -public class InvalidEncoderException extends RuntimeException { - - private final String name; - private final MediaCodecInfo[] availableEncoders; - - public InvalidEncoderException(String name, MediaCodecInfo[] availableEncoders) { - super("There is no encoder having name '" + name + "'"); - this.name = name; - this.availableEncoders = availableEncoders; - } - - public String getName() { - return name; - } - - public MediaCodecInfo[] getAvailableEncoders() { - return availableEncoders; - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index fed6f6c3..44ba75d1 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -63,7 +63,7 @@ public class ScreenEncoder implements Device.RotationListener { return rotationChanged.getAndSet(false); } - public void streamScreen(Device device, Callbacks callbacks) throws IOException { + public void streamScreen(Device device, Callbacks callbacks) throws IOException, ConfigurationException { MediaCodec codec = createCodec(videoMimeType, encoderName); MediaFormat format = createFormat(videoMimeType, bitRate, maxFps, codecOptions); IBinder display = createDisplay(); @@ -207,14 +207,14 @@ public class ScreenEncoder implements Device.RotationListener { return result.toArray(new MediaCodecInfo[result.size()]); } - private static MediaCodec createCodec(String videoMimeType, String encoderName) throws IOException { + private static MediaCodec createCodec(String videoMimeType, String encoderName) throws IOException, ConfigurationException { if (encoderName != null) { Ln.d("Creating encoder by name: '" + encoderName + "'"); try { return MediaCodec.createByCodecName(encoderName); } catch (IllegalArgumentException e) { - MediaCodecInfo[] encoders = listEncoders(videoMimeType); - throw new InvalidEncoderException(encoderName, encoders); + Ln.e(buildUnknownEncoderMessage(videoMimeType, encoderName)); + throw new ConfigurationException("Unknown encoder: " + encoderName); } } MediaCodec codec = MediaCodec.createEncoderByType(videoMimeType); @@ -222,6 +222,18 @@ public class ScreenEncoder implements Device.RotationListener { return codec; } + private static String buildUnknownEncoderMessage(String videoMimeType, String encoderName) { + StringBuilder msg = new StringBuilder("Encoder '").append(encoderName).append("' not found"); + MediaCodecInfo[] encoders = listEncoders(videoMimeType); + if (encoders != null && encoders.length > 0) { + msg.append("\nTry to use one of the available encoders:"); + for (MediaCodecInfo encoder : encoders) { + msg.append("\n scrcpy --encoder='").append(encoder.getName()).append("'"); + } + } + return msg.toString(); + } + private static void setCodecOption(MediaFormat format, CodecOption codecOption) { String key = codecOption.getKey(); Object value = codecOption.getValue(); diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 5a092061..027050af 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -1,7 +1,6 @@ package com.genymobile.scrcpy; import android.graphics.Rect; -import android.media.MediaCodecInfo; import android.os.BatteryManager; import android.os.Build; @@ -59,7 +58,7 @@ public final class Server { } } - private static void scrcpy(Options options) throws IOException { + private static void scrcpy(Options options) throws IOException, ConfigurationException { Ln.i("Device: " + Build.MANUFACTURER + " " + Build.MODEL + " (Android " + Build.VERSION.RELEASE + ")"); final Device device = new Device(options); List codecOptions = options.getCodecOptions(); @@ -299,38 +298,19 @@ public final class Server { return new Rect(x, y, x + width, y + height); } - private static void suggestFix(Throwable e) { - 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); - } - } - } else if (e instanceof InvalidEncoderException) { - InvalidEncoderException iee = (InvalidEncoderException) e; - MediaCodecInfo[] encoders = iee.getAvailableEncoders(); - if (encoders != null && encoders.length > 0) { - Ln.e("Try to use one of the available encoders:"); - for (MediaCodecInfo encoder : encoders) { - Ln.e(" scrcpy --encoder='" + encoder.getName() + "'"); - } - } - } - } - public static void main(String... args) throws Exception { Thread.setDefaultUncaughtExceptionHandler((t, e) -> { Ln.e("Exception on thread " + t, e); - suggestFix(e); }); Options options = createOptions(args); Ln.initLogLevel(options.getLogLevel()); - scrcpy(options); + try { + scrcpy(options); + } catch (ConfigurationException e) { + // Do not print stack trace, a user-friendly error-message has already been logged + } } } From fa9976366895c1adbfdbf98fb624107e71d4b24e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 19 Feb 2023 20:06:30 +0100 Subject: [PATCH 1354/2244] Fix --encoder documentation Mention that it depends on the codec provided by --codec (which is not necessarily H264 anymore). --- app/data/zsh-completion/_scrcpy | 2 +- app/scrcpy.1 | 2 +- app/src/cli.c | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index c57111cc..961565e7 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -18,7 +18,7 @@ arguments=( '--display=[Specify the display id to mirror]' '--display-buffer=[Add a buffering delay \(in milliseconds\) before displaying]' {-e,--select-tcpip}'[Use TCP/IP device]' - '--encoder=[Use a specific MediaCodec encoder \(must be a H.264 encoder\)]' + '--encoder=[Use a specific MediaCodec encoder]' '--force-adb-forward[Do not attempt to use \"adb reverse\" to connect to the device]' '--forward-all-clicks[Forward clicks to device]' {-f,--fullscreen}'[Start in fullscreen]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 8f028d7c..0f1147da 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -82,7 +82,7 @@ Also see \fB\-d\fR (\fB\-\-select\-usb\fR). .TP .BI "\-\-encoder " name -Use a specific MediaCodec encoder (must be a H.264 encoder). +Use a specific MediaCodec encoder (depending on the codec provided by \fB\-\-codec\fR). .TP .B \-\-force\-adb\-forward diff --git a/app/src/cli.c b/app/src/cli.c index 1851bad6..ab460732 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -175,7 +175,8 @@ static const struct sc_option options[] = { .longopt_id = OPT_ENCODER_NAME, .longopt = "encoder", .argdesc = "name", - .text = "Use a specific MediaCodec encoder (must be a H.264 encoder).", + .text = "Use a specific MediaCodec encoder (depending on the codec " + "provided by --codec).", }, { .longopt_id = OPT_FORCE_ADB_FORWARD, From 181fb555bb72ac5ba5460db6ec8efbf7366a12f3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 17 Feb 2023 08:41:03 +0100 Subject: [PATCH 1355/2244] Change PTS origin type from uint64_t to int64_t It is initialized from AVPacket.pts, which is an int64_t. --- app/src/recorder.c | 6 ++---- app/src/recorder.h | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/app/src/recorder.c b/app/src/recorder.c index 455e1db1..d75f1b12 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -11,8 +11,6 @@ /** Downcast packet_sink to recorder */ #define DOWNCAST(SINK) container_of(SINK, struct sc_recorder, packet_sink) -#define SC_PTS_ORIGIN_NONE UINT64_C(-1) - static const AVRational SCRCPY_TIME_BASE = {1, 1000000}; // timestamps in us static const AVOutputFormat * @@ -171,7 +169,7 @@ run_recorder(void *data) { sc_mutex_unlock(&recorder->mutex); - if (recorder->pts_origin == SC_PTS_ORIGIN_NONE + if (recorder->pts_origin == AV_NOPTS_VALUE && rec->packet->pts != AV_NOPTS_VALUE) { // First PTS received recorder->pts_origin = rec->packet->pts; @@ -257,7 +255,7 @@ sc_recorder_open(struct sc_recorder *recorder, const AVCodec *input_codec) { recorder->failed = false; recorder->header_written = false; recorder->previous = NULL; - recorder->pts_origin = SC_PTS_ORIGIN_NONE; + recorder->pts_origin = AV_NOPTS_VALUE; const char *format_name = sc_recorder_get_format_name(recorder->format); assert(format_name); diff --git a/app/src/recorder.h b/app/src/recorder.h index a03c91d7..e6c66f99 100644 --- a/app/src/recorder.h +++ b/app/src/recorder.h @@ -28,7 +28,7 @@ struct sc_recorder { struct sc_size declared_frame_size; bool header_written; - uint64_t pts_origin; + int64_t pts_origin; sc_thread thread; sc_mutex mutex; From b6744e788708e330ceef618d0961e1a2ce0a98e8 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 17 Feb 2023 08:46:11 +0100 Subject: [PATCH 1356/2244] Move pts_origin to a local variable It is only used from run_recorder(). --- app/src/recorder.c | 9 +++++---- app/src/recorder.h | 2 -- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/app/src/recorder.c b/app/src/recorder.c index d75f1b12..f71f6322 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -136,6 +136,8 @@ static int run_recorder(void *data) { struct sc_recorder *recorder = data; + int64_t pts_origin = AV_NOPTS_VALUE; + for (;;) { sc_mutex_lock(&recorder->mutex); @@ -169,15 +171,15 @@ run_recorder(void *data) { sc_mutex_unlock(&recorder->mutex); - if (recorder->pts_origin == AV_NOPTS_VALUE + if (pts_origin == AV_NOPTS_VALUE && rec->packet->pts != AV_NOPTS_VALUE) { // First PTS received - recorder->pts_origin = rec->packet->pts; + pts_origin = rec->packet->pts; } if (rec->packet->pts != AV_NOPTS_VALUE) { // Set PTS relatve to the origin - rec->packet->pts -= recorder->pts_origin; + rec->packet->pts -= pts_origin; rec->packet->dts = rec->packet->pts; } @@ -255,7 +257,6 @@ sc_recorder_open(struct sc_recorder *recorder, const AVCodec *input_codec) { recorder->failed = false; recorder->header_written = false; recorder->previous = NULL; - recorder->pts_origin = AV_NOPTS_VALUE; const char *format_name = sc_recorder_get_format_name(recorder->format); assert(format_name); diff --git a/app/src/recorder.h b/app/src/recorder.h index e6c66f99..373278e6 100644 --- a/app/src/recorder.h +++ b/app/src/recorder.h @@ -28,8 +28,6 @@ struct sc_recorder { struct sc_size declared_frame_size; bool header_written; - int64_t pts_origin; - sc_thread thread; sc_mutex mutex; sc_cond queue_cond; From db5751a76a9303e13adc470b45c0a203fc72980c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 17 Feb 2023 09:01:48 +0100 Subject: [PATCH 1357/2244] Move previous packet to a local variable It is only used from run_recorder(). --- app/src/recorder.c | 14 ++++++++------ app/src/recorder.h | 6 ------ 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/app/src/recorder.c b/app/src/recorder.c index f71f6322..c52bbb4d 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -138,6 +138,10 @@ run_recorder(void *data) { int64_t pts_origin = AV_NOPTS_VALUE; + // We can write a packet only once we received the next one so that we can + // set its duration (next_pts - current_pts) + struct sc_record_packet *previous = NULL; + for (;;) { sc_mutex_lock(&recorder->mutex); @@ -150,7 +154,7 @@ run_recorder(void *data) { if (recorder->stopped && sc_queue_is_empty(&recorder->queue)) { sc_mutex_unlock(&recorder->mutex); - struct sc_record_packet *last = recorder->previous; + struct sc_record_packet *last = previous; if (last) { // assign an arbitrary duration to the last packet last->packet->duration = 100000; @@ -183,12 +187,9 @@ run_recorder(void *data) { rec->packet->dts = rec->packet->pts; } - // recorder->previous is only written from this thread, no need to lock - struct sc_record_packet *previous = recorder->previous; - recorder->previous = rec; - if (!previous) { // we just received the first packet + previous = rec; continue; } @@ -212,6 +213,8 @@ run_recorder(void *data) { sc_mutex_unlock(&recorder->mutex); break; } + + previous = rec; } if (!recorder->failed) { @@ -256,7 +259,6 @@ sc_recorder_open(struct sc_recorder *recorder, const AVCodec *input_codec) { recorder->stopped = false; recorder->failed = false; recorder->header_written = false; - recorder->previous = NULL; const char *format_name = sc_recorder_get_format_name(recorder->format); assert(format_name); diff --git a/app/src/recorder.h b/app/src/recorder.h index 373278e6..05d3857e 100644 --- a/app/src/recorder.h +++ b/app/src/recorder.h @@ -34,12 +34,6 @@ struct sc_recorder { bool stopped; // set on recorder_close() bool failed; // set on packet write failure struct sc_recorder_queue queue; - - // we can write a packet only once we received the next one so that we can - // set its duration (next_pts - current_pts) - // "previous" is only accessed from the recorder thread, so it does not - // need to be protected by the mutex - struct sc_record_packet *previous; }; bool From b1b33e3eaf00b94d6a7d7a3c625b8bbc8c7c8014 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 10 Feb 2023 18:10:24 +0100 Subject: [PATCH 1358/2244] Report recorder errors Stop scrcpy on recorder errors. It was previously indirectly stopped by the demuxer, which failed to push packets to a recorder in error. Report it directly instead: - it avoids to wait for the next demuxer call; - it will allow to open the target file from a separate thread and stop immediately on any I/O error. --- app/src/events.h | 1 + app/src/recorder.c | 13 ++++++++++--- app/src/recorder.h | 11 ++++++++++- app/src/scrcpy.c | 24 ++++++++++++++++++++---- 4 files changed, 41 insertions(+), 8 deletions(-) diff --git a/app/src/events.h b/app/src/events.h index 7fa10761..0a45b652 100644 --- a/app/src/events.h +++ b/app/src/events.h @@ -4,3 +4,4 @@ #define SC_EVENT_SERVER_CONNECTED (SDL_USEREVENT + 3) #define SC_EVENT_USB_DEVICE_DISCONNECTED (SDL_USEREVENT + 4) #define SC_EVENT_DEMUXER_ERROR (SDL_USEREVENT + 5) +#define SC_EVENT_RECORDER_ERROR (SDL_USEREVENT + 6) diff --git a/app/src/recorder.c b/app/src/recorder.c index c52bbb4d..beca48aa 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -240,6 +240,9 @@ run_recorder(void *data) { LOGD("Recorder thread ended"); + recorder->cbs->on_ended(recorder, !recorder->failed, + recorder->cbs_userdata); + return 0; } @@ -387,10 +390,10 @@ sc_recorder_packet_sink_push(struct sc_packet_sink *sink, } bool -sc_recorder_init(struct sc_recorder *recorder, - const char *filename, +sc_recorder_init(struct sc_recorder *recorder, const char *filename, enum sc_record_format format, - struct sc_size declared_frame_size) { + struct sc_size declared_frame_size, + const struct sc_recorder_callbacks *cbs, void *cbs_userdata) { recorder->filename = strdup(filename); if (!recorder->filename) { LOG_OOM(); @@ -400,6 +403,10 @@ sc_recorder_init(struct sc_recorder *recorder, recorder->format = format; recorder->declared_frame_size = declared_frame_size; + assert(cbs && cbs->on_ended); + recorder->cbs = cbs; + recorder->cbs_userdata = cbs_userdata; + static const struct sc_packet_sink_ops ops = { .open = sc_recorder_packet_sink_open, .close = sc_recorder_packet_sink_close, diff --git a/app/src/recorder.h b/app/src/recorder.h index 05d3857e..de5827e3 100644 --- a/app/src/recorder.h +++ b/app/src/recorder.h @@ -34,12 +34,21 @@ struct sc_recorder { bool stopped; // set on recorder_close() bool failed; // set on packet write failure struct sc_recorder_queue queue; + + const struct sc_recorder_callbacks *cbs; + void *cbs_userdata; +}; + +struct sc_recorder_callbacks { + void (*on_ended)(struct sc_recorder *recorder, bool success, + void *userdata); }; bool sc_recorder_init(struct sc_recorder *recorder, const char *filename, enum sc_record_format format, - struct sc_size declared_frame_size); + struct sc_size declared_frame_size, + const struct sc_recorder_callbacks *cbs, void *cbs_userdata); void sc_recorder_destroy(struct sc_recorder *recorder); diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index e96fa187..5a43a313 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -161,6 +161,9 @@ event_loop(struct scrcpy *s) { case SC_EVENT_DEMUXER_ERROR: LOGE("Demuxer error"); return SCRCPY_EXIT_FAILURE; + case SC_EVENT_RECORDER_ERROR: + LOGE("Recorder error"); + return SCRCPY_EXIT_FAILURE; case SDL_QUIT: LOGD("User requested to quit"); return SCRCPY_EXIT_SUCCESS; @@ -198,6 +201,17 @@ await_for_server(bool *connected) { return false; } +static void +sc_recorder_on_ended(struct sc_recorder *recorder, bool success, + void *userdata) { + (void) recorder; + (void) userdata; + + if (!success) { + PUSH_EVENT(SC_EVENT_RECORDER_ERROR); + } +} + static void sc_demuxer_on_ended(struct sc_demuxer *demuxer, bool eos, void *userdata) { (void) demuxer; @@ -379,10 +393,12 @@ scrcpy(struct scrcpy_options *options) { struct sc_recorder *rec = NULL; if (options->record_filename) { - if (!sc_recorder_init(&s->recorder, - options->record_filename, - options->record_format, - info->frame_size)) { + static const struct sc_recorder_callbacks recorder_cbs = { + .on_ended = sc_recorder_on_ended, + }; + if (!sc_recorder_init(&s->recorder, options->record_filename, + options->record_format, info->frame_size, + &recorder_cbs, NULL)) { goto end; } rec = &s->recorder; From fb2913559144c1f3b062f08b7225216f0e31b3ee Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 14 Feb 2023 08:40:44 +0100 Subject: [PATCH 1359/2244] Initialize recorder fields from init() The recorder has two initialization phases: one to initialize the concrete recorder object, and one to open its packet_sink trait. Initialize mutex and condvar as part of the object initialization. If there were several packet_sink traits (spoiler: one for video, one for audio), then the mutex and condvar would still be initialized only once. --- app/src/recorder.c | 53 ++++++++++++++++++++++++---------------------- 1 file changed, 28 insertions(+), 25 deletions(-) diff --git a/app/src/recorder.c b/app/src/recorder.c index beca48aa..2488a3b5 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -248,33 +248,18 @@ run_recorder(void *data) { static bool sc_recorder_open(struct sc_recorder *recorder, const AVCodec *input_codec) { - bool ok = sc_mutex_init(&recorder->mutex); - if (!ok) { - return false; - } - - ok = sc_cond_init(&recorder->queue_cond); - if (!ok) { - goto error_mutex_destroy; - } - - sc_queue_init(&recorder->queue); - recorder->stopped = false; - recorder->failed = false; - recorder->header_written = false; - const char *format_name = sc_recorder_get_format_name(recorder->format); assert(format_name); const AVOutputFormat *format = find_muxer(format_name); if (!format) { LOGE("Could not find muxer"); - goto error_cond_destroy; + return false; } recorder->ctx = avformat_alloc_context(); if (!recorder->ctx) { LOG_OOM(); - goto error_cond_destroy; + return false; } // contrary to the deprecated API (av_oformat_next()), av_muxer_iterate() @@ -306,8 +291,8 @@ sc_recorder_open(struct sc_recorder *recorder, const AVCodec *input_codec) { } LOGD("Starting recorder thread"); - ok = sc_thread_create(&recorder->thread, run_recorder, "scrcpy-recorder", - recorder); + bool ok = sc_thread_create(&recorder->thread, run_recorder, + "scrcpy-recorder", recorder); if (!ok) { LOGE("Could not start recorder thread"); goto error_avio_close; @@ -321,10 +306,6 @@ error_avio_close: avio_close(recorder->ctx->pb); error_avformat_free_context: avformat_free_context(recorder->ctx); -error_cond_destroy: - sc_cond_destroy(&recorder->queue_cond); -error_mutex_destroy: - sc_mutex_destroy(&recorder->mutex); return false; } @@ -340,8 +321,6 @@ sc_recorder_close(struct sc_recorder *recorder) { avio_close(recorder->ctx->pb); avformat_free_context(recorder->ctx); - sc_cond_destroy(&recorder->queue_cond); - sc_mutex_destroy(&recorder->mutex); } static bool @@ -400,6 +379,21 @@ sc_recorder_init(struct sc_recorder *recorder, const char *filename, return false; } + bool ok = sc_mutex_init(&recorder->mutex); + if (!ok) { + goto error_free_filename; + } + + ok = sc_cond_init(&recorder->queue_cond); + if (!ok) { + goto error_mutex_destroy; + } + + sc_queue_init(&recorder->queue); + recorder->stopped = false; + recorder->failed = false; + recorder->header_written = false; + recorder->format = format; recorder->declared_frame_size = declared_frame_size; @@ -416,9 +410,18 @@ sc_recorder_init(struct sc_recorder *recorder, const char *filename, recorder->packet_sink.ops = &ops; return true; + +error_mutex_destroy: + sc_mutex_destroy(&recorder->mutex); +error_free_filename: + free(recorder->filename); + + return false; } void sc_recorder_destroy(struct sc_recorder *recorder) { + sc_cond_destroy(&recorder->queue_cond); + sc_mutex_destroy(&recorder->mutex); free(recorder->filename); } From 6b5dfef92357421fc033bf99a5ceeb006c322811 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 14 Feb 2023 09:37:36 +0100 Subject: [PATCH 1360/2244] Inline packet_sink impl in recorder Remove useless wrappers. --- app/src/recorder.c | 39 ++++++++++++++------------------------- 1 file changed, 14 insertions(+), 25 deletions(-) diff --git a/app/src/recorder.c b/app/src/recorder.c index 2488a3b5..998a2f7b 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -247,7 +247,11 @@ run_recorder(void *data) { } static bool -sc_recorder_open(struct sc_recorder *recorder, const AVCodec *input_codec) { +sc_recorder_packet_sink_open(struct sc_packet_sink *sink, + const AVCodec *codec) { + struct sc_recorder *recorder = DOWNCAST(sink); + assert(codec); + const char *format_name = sc_recorder_get_format_name(recorder->format); assert(format_name); const AVOutputFormat *format = find_muxer(format_name); @@ -271,13 +275,13 @@ sc_recorder_open(struct sc_recorder *recorder, const AVCodec *input_codec) { av_dict_set(&recorder->ctx->metadata, "comment", "Recorded by scrcpy " SCRCPY_VERSION, 0); - AVStream *ostream = avformat_new_stream(recorder->ctx, input_codec); + AVStream *ostream = avformat_new_stream(recorder->ctx, codec); if (!ostream) { goto error_avformat_free_context; } ostream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO; - ostream->codecpar->codec_id = input_codec->id; + ostream->codecpar->codec_id = codec->id; ostream->codecpar->format = AV_PIX_FMT_YUV420P; ostream->codecpar->width = recorder->declared_frame_size.width; ostream->codecpar->height = recorder->declared_frame_size.height; @@ -311,7 +315,9 @@ error_avformat_free_context: } static void -sc_recorder_close(struct sc_recorder *recorder) { +sc_recorder_packet_sink_close(struct sc_packet_sink *sink) { + struct sc_recorder *recorder = DOWNCAST(sink); + sc_mutex_lock(&recorder->mutex); recorder->stopped = true; sc_cond_signal(&recorder->queue_cond); @@ -324,7 +330,10 @@ sc_recorder_close(struct sc_recorder *recorder) { } static bool -sc_recorder_push(struct sc_recorder *recorder, const AVPacket *packet) { +sc_recorder_packet_sink_push(struct sc_packet_sink *sink, + const AVPacket *packet) { + struct sc_recorder *recorder = DOWNCAST(sink); + sc_mutex_lock(&recorder->mutex); assert(!recorder->stopped); @@ -348,26 +357,6 @@ sc_recorder_push(struct sc_recorder *recorder, const AVPacket *packet) { return true; } -static bool -sc_recorder_packet_sink_open(struct sc_packet_sink *sink, - const AVCodec *codec) { - struct sc_recorder *recorder = DOWNCAST(sink); - return sc_recorder_open(recorder, codec); -} - -static void -sc_recorder_packet_sink_close(struct sc_packet_sink *sink) { - struct sc_recorder *recorder = DOWNCAST(sink); - sc_recorder_close(recorder); -} - -static bool -sc_recorder_packet_sink_push(struct sc_packet_sink *sink, - const AVPacket *packet) { - struct sc_recorder *recorder = DOWNCAST(sink); - return sc_recorder_push(recorder, packet); -} - bool sc_recorder_init(struct sc_recorder *recorder, const char *filename, enum sc_record_format format, From a039124d5d21c3886aacb4463a747edd69da1a46 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 14 Feb 2023 09:25:50 +0100 Subject: [PATCH 1361/2244] Open recording file from the recorder thread The recorder opened the target file from the packet sink open() callback, called by the demuxer. Only then the recorder thread was started. One golden rule for the recorder is to never block the demuxer for I/O, because it would impact mirroring. This rule is respected on recording packets, but not for the initial recorder opening. Therefore, start the recorder thread from sc_recorder_init(), open the file immediately from the recorder thread, then make it wait for the stream to start (on packet sink open()). Now that the recorder can report errors directly (rather than making the demuxer call fail), it is possible to report file opening error even before the packet sink is open. --- app/src/recorder.c | 250 +++++++++++++++++++++++------------- app/src/recorder.h | 14 +- app/src/scrcpy.c | 4 + app/src/trait/packet_sink.h | 1 + 4 files changed, 178 insertions(+), 91 deletions(-) diff --git a/app/src/recorder.c b/app/src/recorder.c index 998a2f7b..784ef7ee 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -132,10 +132,76 @@ sc_recorder_write(struct sc_recorder *recorder, AVPacket *packet) { return av_write_frame(recorder->ctx, packet) >= 0; } -static int -run_recorder(void *data) { - struct sc_recorder *recorder = data; +static bool +sc_recorder_open_output_file(struct sc_recorder *recorder) { + const char *format_name = sc_recorder_get_format_name(recorder->format); + assert(format_name); + const AVOutputFormat *format = find_muxer(format_name); + if (!format) { + LOGE("Could not find muxer"); + return false; + } + recorder->ctx = avformat_alloc_context(); + if (!recorder->ctx) { + LOG_OOM(); + return false; + } + + int ret = avio_open(&recorder->ctx->pb, recorder->filename, + AVIO_FLAG_WRITE); + if (ret < 0) { + LOGE("Failed to open output file: %s", recorder->filename); + avformat_free_context(recorder->ctx); + return false; + } + + // contrary to the deprecated API (av_oformat_next()), av_muxer_iterate() + // returns (on purpose) a pointer-to-const, but AVFormatContext.oformat + // still expects a pointer-to-non-const (it has not be updated accordingly) + // + recorder->ctx->oformat = (AVOutputFormat *) format; + + av_dict_set(&recorder->ctx->metadata, "comment", + "Recorded by scrcpy " SCRCPY_VERSION, 0); + + LOGI("Recording started to %s file: %s", format_name, recorder->filename); + return true; +} + +static void +sc_recorder_close_output_file(struct sc_recorder *recorder) { + avio_close(recorder->ctx->pb); + avformat_free_context(recorder->ctx); +} + +static bool +sc_recorder_wait_video_stream(struct sc_recorder *recorder) { + sc_mutex_lock(&recorder->mutex); + while (!recorder->codec && !recorder->stopped) { + sc_cond_wait(&recorder->stream_cond, &recorder->mutex); + } + const AVCodec *codec = recorder->codec; + sc_mutex_unlock(&recorder->mutex); + + if (codec) { + AVStream *ostream = avformat_new_stream(recorder->ctx, codec); + if (!ostream) { + return false; + } + + ostream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO; + ostream->codecpar->codec_id = codec->id; + ostream->codecpar->format = AV_PIX_FMT_YUV420P; + ostream->codecpar->width = recorder->declared_frame_size.width; + ostream->codecpar->height = recorder->declared_frame_size.height; + } + + return true; +} + +static bool +sc_recorder_process_packets(struct sc_recorder *recorder) { int64_t pts_origin = AV_NOPTS_VALUE; // We can write a packet only once we received the next one so that we can @@ -206,42 +272,70 @@ run_recorder(void *data) { if (!ok) { LOGE("Could not record packet"); - sc_mutex_lock(&recorder->mutex); - recorder->failed = true; - // discard pending packets - sc_recorder_queue_clear(&recorder->queue); - sc_mutex_unlock(&recorder->mutex); - break; + return false; } previous = rec; } - if (!recorder->failed) { - if (recorder->header_written) { - int ret = av_write_trailer(recorder->ctx); - if (ret < 0) { - LOGE("Failed to write trailer to %s", recorder->filename); - recorder->failed = true; - } - } else { - // the recorded file is empty - recorder->failed = true; - } + if (!recorder->header_written) { + // the recorded file is empty + return false; } - if (recorder->failed) { - LOGE("Recording failed to %s", recorder->filename); - } else { + int ret = av_write_trailer(recorder->ctx); + if (ret < 0) { + LOGE("Failed to write trailer to %s", recorder->filename); + return false; + } + + return true; +} + +static bool +sc_recorder_record(struct sc_recorder *recorder) { + bool ok = sc_recorder_open_output_file(recorder); + if (!ok) { + return false; + } + + ok = sc_recorder_wait_video_stream(recorder); + if (!ok) { + sc_recorder_close_output_file(recorder); + return false; + } + + // If recorder->stopped, process any queued packet anyway + + ok = sc_recorder_process_packets(recorder); + sc_recorder_close_output_file(recorder); + return ok; +} + +static int +run_recorder(void *data) { + struct sc_recorder *recorder = data; + + bool success = sc_recorder_record(recorder); + + sc_mutex_lock(&recorder->mutex); + // Prevent the producer to push any new packet + recorder->stopped = true; + // Discard pending packets + sc_recorder_queue_clear(&recorder->queue); + sc_mutex_unlock(&recorder->mutex); + + if (success) { const char *format_name = sc_recorder_get_format_name(recorder->format); LOGI("Recording complete to %s file: %s", format_name, recorder->filename); + } else { + LOGE("Recording failed to %s", recorder->filename); } LOGD("Recorder thread ended"); - recorder->cbs->on_ended(recorder, !recorder->failed, - recorder->cbs_userdata); + recorder->cbs->on_ended(recorder, success, recorder->cbs_userdata); return 0; } @@ -252,66 +346,17 @@ sc_recorder_packet_sink_open(struct sc_packet_sink *sink, struct sc_recorder *recorder = DOWNCAST(sink); assert(codec); - const char *format_name = sc_recorder_get_format_name(recorder->format); - assert(format_name); - const AVOutputFormat *format = find_muxer(format_name); - if (!format) { - LOGE("Could not find muxer"); + sc_mutex_lock(&recorder->mutex); + if (recorder->stopped) { + sc_mutex_unlock(&recorder->mutex); return false; } - recorder->ctx = avformat_alloc_context(); - if (!recorder->ctx) { - LOG_OOM(); - return false; - } - - // contrary to the deprecated API (av_oformat_next()), av_muxer_iterate() - // returns (on purpose) a pointer-to-const, but AVFormatContext.oformat - // still expects a pointer-to-non-const (it has not be updated accordingly) - // - recorder->ctx->oformat = (AVOutputFormat *) format; - - av_dict_set(&recorder->ctx->metadata, "comment", - "Recorded by scrcpy " SCRCPY_VERSION, 0); - - AVStream *ostream = avformat_new_stream(recorder->ctx, codec); - if (!ostream) { - goto error_avformat_free_context; - } - - ostream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO; - ostream->codecpar->codec_id = codec->id; - ostream->codecpar->format = AV_PIX_FMT_YUV420P; - ostream->codecpar->width = recorder->declared_frame_size.width; - ostream->codecpar->height = recorder->declared_frame_size.height; - - int ret = avio_open(&recorder->ctx->pb, recorder->filename, - AVIO_FLAG_WRITE); - if (ret < 0) { - LOGE("Failed to open output file: %s", recorder->filename); - // ostream will be cleaned up during context cleaning - goto error_avformat_free_context; - } - - LOGD("Starting recorder thread"); - bool ok = sc_thread_create(&recorder->thread, run_recorder, - "scrcpy-recorder", recorder); - if (!ok) { - LOGE("Could not start recorder thread"); - goto error_avio_close; - } - - LOGI("Recording started to %s file: %s", format_name, recorder->filename); + recorder->codec = codec; + sc_cond_signal(&recorder->stream_cond); + sc_mutex_unlock(&recorder->mutex); return true; - -error_avio_close: - avio_close(recorder->ctx->pb); -error_avformat_free_context: - avformat_free_context(recorder->ctx); - - return false; } static void @@ -319,14 +364,10 @@ sc_recorder_packet_sink_close(struct sc_packet_sink *sink) { struct sc_recorder *recorder = DOWNCAST(sink); sc_mutex_lock(&recorder->mutex); + // EOS also stops the recorder recorder->stopped = true; sc_cond_signal(&recorder->queue_cond); sc_mutex_unlock(&recorder->mutex); - - sc_thread_join(&recorder->thread, NULL); - - avio_close(recorder->ctx->pb); - avformat_free_context(recorder->ctx); } static bool @@ -335,10 +376,9 @@ sc_recorder_packet_sink_push(struct sc_packet_sink *sink, struct sc_recorder *recorder = DOWNCAST(sink); sc_mutex_lock(&recorder->mutex); - assert(!recorder->stopped); - if (recorder->failed) { - // reject any new packet (this will stop the stream) + if (recorder->stopped) { + // reject any new packet sc_mutex_unlock(&recorder->mutex); return false; } @@ -378,11 +418,17 @@ sc_recorder_init(struct sc_recorder *recorder, const char *filename, goto error_mutex_destroy; } + ok = sc_cond_init(&recorder->stream_cond); + if (!ok) { + goto error_queue_cond_destroy; + } + sc_queue_init(&recorder->queue); recorder->stopped = false; - recorder->failed = false; recorder->header_written = false; + recorder->codec = NULL; + recorder->format = format; recorder->declared_frame_size = declared_frame_size; @@ -398,8 +444,19 @@ sc_recorder_init(struct sc_recorder *recorder, const char *filename, recorder->packet_sink.ops = &ops; + ok = sc_thread_create(&recorder->thread, run_recorder, "scrcpy-recorder", + recorder); + if (!ok) { + LOGE("Could not start recorder thread"); + goto error_stream_cond_destroy; + } + return true; +error_stream_cond_destroy: + sc_cond_destroy(&recorder->stream_cond); +error_queue_cond_destroy: + sc_cond_destroy(&recorder->queue_cond); error_mutex_destroy: sc_mutex_destroy(&recorder->mutex); error_free_filename: @@ -408,8 +465,23 @@ error_free_filename: return false; } +void +sc_recorder_stop(struct sc_recorder *recorder) { + sc_mutex_lock(&recorder->mutex); + recorder->stopped = true; + sc_cond_signal(&recorder->queue_cond); + sc_cond_signal(&recorder->stream_cond); + sc_mutex_unlock(&recorder->mutex); +} + +void +sc_recorder_join(struct sc_recorder *recorder) { + sc_thread_join(&recorder->thread, NULL); +} + void sc_recorder_destroy(struct sc_recorder *recorder) { + sc_cond_destroy(&recorder->stream_cond); sc_cond_destroy(&recorder->queue_cond); sc_mutex_destroy(&recorder->mutex); free(recorder->filename); diff --git a/app/src/recorder.h b/app/src/recorder.h index de5827e3..cab71678 100644 --- a/app/src/recorder.h +++ b/app/src/recorder.h @@ -31,10 +31,14 @@ struct sc_recorder { sc_thread thread; sc_mutex mutex; sc_cond queue_cond; - bool stopped; // set on recorder_close() - bool failed; // set on packet write failure + // set on sc_recorder_stop(), packet_sink close or recording failure + bool stopped; struct sc_recorder_queue queue; + // wake up the recorder thread once the codec in known + sc_cond stream_cond; + const AVCodec *codec; + const struct sc_recorder_callbacks *cbs; void *cbs_userdata; }; @@ -50,6 +54,12 @@ sc_recorder_init(struct sc_recorder *recorder, const char *filename, struct sc_size declared_frame_size, const struct sc_recorder_callbacks *cbs, void *cbs_userdata); +void +sc_recorder_stop(struct sc_recorder *recorder); + +void +sc_recorder_join(struct sc_recorder *recorder); + void sc_recorder_destroy(struct sc_recorder *recorder); diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 5a43a313..90c6bd9b 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -660,6 +660,9 @@ end: if (file_pusher_initialized) { sc_file_pusher_stop(&s->file_pusher); } + if (recorder_initialized) { + sc_recorder_stop(&s->recorder); + } if (screen_initialized) { sc_screen_interrupt(&s->screen); } @@ -706,6 +709,7 @@ end: } if (recorder_initialized) { + sc_recorder_join(&s->recorder); sc_recorder_destroy(&s->recorder); } diff --git a/app/src/trait/packet_sink.h b/app/src/trait/packet_sink.h index 1fef765f..9fc9fd24 100644 --- a/app/src/trait/packet_sink.h +++ b/app/src/trait/packet_sink.h @@ -19,6 +19,7 @@ struct sc_packet_sink { }; struct sc_packet_sink_ops { + /* The codec instance is static, it is valid until the end of the program */ bool (*open)(struct sc_packet_sink *sink, const AVCodec *codec); void (*close)(struct sc_packet_sink *sink); bool (*push)(struct sc_packet_sink *sink, const AVPacket *packet); From 3c407773e9ddf9debd1de71ff0d12f571bd36718 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 23 Feb 2023 11:00:34 +0100 Subject: [PATCH 1362/2244] Add start() function for recorder For consistency with the other components, do not start the internal thread from an init() function. --- app/src/recorder.c | 21 ++++++++++++--------- app/src/recorder.h | 3 +++ app/src/scrcpy.c | 13 +++++++++++-- 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/app/src/recorder.c b/app/src/recorder.c index 784ef7ee..aa3aea0e 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -444,17 +444,8 @@ sc_recorder_init(struct sc_recorder *recorder, const char *filename, recorder->packet_sink.ops = &ops; - ok = sc_thread_create(&recorder->thread, run_recorder, "scrcpy-recorder", - recorder); - if (!ok) { - LOGE("Could not start recorder thread"); - goto error_stream_cond_destroy; - } - return true; -error_stream_cond_destroy: - sc_cond_destroy(&recorder->stream_cond); error_queue_cond_destroy: sc_cond_destroy(&recorder->queue_cond); error_mutex_destroy: @@ -465,6 +456,18 @@ error_free_filename: return false; } +bool +sc_recorder_start(struct sc_recorder *recorder) { + bool ok = sc_thread_create(&recorder->thread, run_recorder, + "scrcpy-recorder", recorder); + if (!ok) { + LOGE("Could not start recorder thread"); + return false; + } + + return true; +} + void sc_recorder_stop(struct sc_recorder *recorder) { sc_mutex_lock(&recorder->mutex); diff --git a/app/src/recorder.h b/app/src/recorder.h index cab71678..e98d0ea2 100644 --- a/app/src/recorder.h +++ b/app/src/recorder.h @@ -54,6 +54,9 @@ sc_recorder_init(struct sc_recorder *recorder, const char *filename, struct sc_size declared_frame_size, const struct sc_recorder_callbacks *cbs, void *cbs_userdata); +bool +sc_recorder_start(struct sc_recorder *recorder); + void sc_recorder_stop(struct sc_recorder *recorder); diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 90c6bd9b..b636a03a 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -277,6 +277,7 @@ scrcpy(struct scrcpy_options *options) { bool server_started = false; bool file_pusher_initialized = false; bool recorder_initialized = false; + bool recorder_started = false; #ifdef HAVE_V4L2 bool v4l2_sink_initialized = false; #endif @@ -401,8 +402,14 @@ scrcpy(struct scrcpy_options *options) { &recorder_cbs, NULL)) { goto end; } - rec = &s->recorder; recorder_initialized = true; + + if (!sc_recorder_start(&s->recorder)) { + goto end; + } + recorder_started = true; + + rec = &s->recorder; } static const struct sc_demuxer_callbacks demuxer_cbs = { @@ -708,8 +715,10 @@ end: sc_controller_destroy(&s->controller); } - if (recorder_initialized) { + if (recorder_started) { sc_recorder_join(&s->recorder); + } + if (recorder_initialized) { sc_recorder_destroy(&s->recorder); } From 4b246cd963ebe0cafa2fe2743e4f9b066dcec293 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 18 Feb 2023 12:07:05 +0100 Subject: [PATCH 1363/2244] Move last packet recording Write the last packet at the end. --- app/src/recorder.c | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/app/src/recorder.c b/app/src/recorder.c index aa3aea0e..85f0fd2a 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -220,19 +220,6 @@ sc_recorder_process_packets(struct sc_recorder *recorder) { if (recorder->stopped && sc_queue_is_empty(&recorder->queue)) { sc_mutex_unlock(&recorder->mutex); - struct sc_record_packet *last = previous; - if (last) { - // assign an arbitrary duration to the last packet - last->packet->duration = 100000; - bool ok = sc_recorder_write(recorder, last->packet); - if (!ok) { - // failing to write the last frame is not very serious, no - // future frame may depend on it, so the resulting file - // will still be valid - LOGW("Could not record last packet"); - } - sc_record_packet_delete(last); - } break; } @@ -283,6 +270,21 @@ sc_recorder_process_packets(struct sc_recorder *recorder) { return false; } + // Write the last packet + struct sc_record_packet *last = previous; + if (last) { + // assign an arbitrary duration to the last packet + last->packet->duration = 100000; + bool ok = sc_recorder_write(recorder, last->packet); + if (!ok) { + // failing to write the last frame is not very serious, no + // future frame may depend on it, so the resulting file + // will still be valid + LOGW("Could not record last packet"); + } + sc_record_packet_delete(last); + } + int ret = av_write_trailer(recorder->ctx); if (ret < 0) { LOGE("Failed to write trailer to %s", recorder->filename); From f9efe48aac25c37c4b974b2794fbe61a84d9e6db Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 18 Feb 2023 12:07:30 +0100 Subject: [PATCH 1364/2244] Refactor recorder logic Process the initial config packet (necessary to write the header) separately. --- app/src/recorder.c | 127 ++++++++++++++++++++++++--------------------- app/src/recorder.h | 1 - 2 files changed, 69 insertions(+), 59 deletions(-) diff --git a/app/src/recorder.c b/app/src/recorder.c index 85f0fd2a..7cc69778 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -93,13 +93,7 @@ sc_recorder_write_header(struct sc_recorder *recorder, const AVPacket *packet) { ostream->codecpar->extradata = extradata; ostream->codecpar->extradata_size = packet->size; - int ret = avformat_write_header(recorder->ctx, NULL); - if (ret < 0) { - LOGE("Failed to write header to %s", recorder->filename); - return false; - } - - return true; + return avformat_write_header(recorder->ctx, NULL) >= 0; } static void @@ -110,24 +104,6 @@ sc_recorder_rescale_packet(struct sc_recorder *recorder, AVPacket *packet) { static bool sc_recorder_write(struct sc_recorder *recorder, AVPacket *packet) { - if (!recorder->header_written) { - if (packet->pts != AV_NOPTS_VALUE) { - LOGE("The first packet is not a config packet"); - return false; - } - bool ok = sc_recorder_write_header(recorder, packet); - if (!ok) { - return false; - } - recorder->header_written = true; - return true; - } - - if (packet->pts == AV_NOPTS_VALUE) { - // ignore config packets - return true; - } - sc_recorder_rescale_packet(recorder, packet); return av_write_frame(recorder->ctx, packet) >= 0; } @@ -200,6 +176,40 @@ sc_recorder_wait_video_stream(struct sc_recorder *recorder) { return true; } +static bool +sc_recorder_process_header(struct sc_recorder *recorder) { + sc_mutex_lock(&recorder->mutex); + + while (!recorder->stopped && sc_queue_is_empty(&recorder->queue)) { + sc_cond_wait(&recorder->queue_cond, &recorder->mutex); + } + + if (recorder->stopped && sc_queue_is_empty(&recorder->queue)) { + sc_mutex_unlock(&recorder->mutex); + return false; + } + + struct sc_record_packet *rec; + sc_queue_take(&recorder->queue, next, &rec); + + sc_mutex_unlock(&recorder->mutex); + + if (rec->packet->pts != AV_NOPTS_VALUE) { + LOGE("The first packet is not a config packet"); + sc_record_packet_delete(rec); + return false; + } + + bool ok = sc_recorder_write_header(recorder, rec->packet); + sc_record_packet_delete(rec); + if (!ok) { + LOGE("Failed to write header to %s", recorder->filename); + return false; + } + + return true; +} + static bool sc_recorder_process_packets(struct sc_recorder *recorder) { int64_t pts_origin = AV_NOPTS_VALUE; @@ -208,6 +218,11 @@ sc_recorder_process_packets(struct sc_recorder *recorder) { // set its duration (next_pts - current_pts) struct sc_record_packet *previous = NULL; + bool header_written = sc_recorder_process_header(recorder); + if (!header_written) { + return false; + } + for (;;) { sc_mutex_lock(&recorder->mutex); @@ -228,46 +243,43 @@ sc_recorder_process_packets(struct sc_recorder *recorder) { sc_mutex_unlock(&recorder->mutex); - if (pts_origin == AV_NOPTS_VALUE - && rec->packet->pts != AV_NOPTS_VALUE) { - // First PTS received - pts_origin = rec->packet->pts; - } + if (rec->packet->pts == AV_NOPTS_VALUE) { + // Ignore further config packets (e.g. on device orientation + // change). The next non-config packet will have the config packet + // data prepended. + sc_record_packet_delete(rec); + } else { + assert(rec->packet->pts != AV_NOPTS_VALUE); + + if (!previous) { + // This is the first non-config packet + assert(pts_origin == AV_NOPTS_VALUE); + pts_origin = rec->packet->pts; + rec->packet->pts = 0; + rec->packet->dts = 0; + previous = rec; + continue; + } + + assert(previous); + assert(pts_origin != AV_NOPTS_VALUE); - if (rec->packet->pts != AV_NOPTS_VALUE) { - // Set PTS relatve to the origin rec->packet->pts -= pts_origin; rec->packet->dts = rec->packet->pts; - } - if (!previous) { - // we just received the first packet - previous = rec; - continue; - } - - // config packets have no PTS, we must ignore them - if (rec->packet->pts != AV_NOPTS_VALUE - && previous->packet->pts != AV_NOPTS_VALUE) { // we now know the duration of the previous packet previous->packet->duration = rec->packet->pts - previous->packet->pts; + + bool ok = sc_recorder_write(recorder, previous->packet); + sc_record_packet_delete(previous); + if (!ok) { + LOGE("Could not record packet"); + return false; + } + + previous = rec; } - - bool ok = sc_recorder_write(recorder, previous->packet); - sc_record_packet_delete(previous); - if (!ok) { - LOGE("Could not record packet"); - - return false; - } - - previous = rec; - } - - if (!recorder->header_written) { - // the recorded file is empty - return false; } // Write the last packet @@ -427,7 +439,6 @@ sc_recorder_init(struct sc_recorder *recorder, const char *filename, sc_queue_init(&recorder->queue); recorder->stopped = false; - recorder->header_written = false; recorder->codec = NULL; diff --git a/app/src/recorder.h b/app/src/recorder.h index e98d0ea2..287030bc 100644 --- a/app/src/recorder.h +++ b/app/src/recorder.h @@ -26,7 +26,6 @@ struct sc_recorder { enum sc_record_format format; AVFormatContext *ctx; struct sc_size declared_frame_size; - bool header_written; sc_thread thread; sc_mutex mutex; From ef6a3b97a73f3aa85cb2226b21f4cc1b4372fb12 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 23 Feb 2023 12:36:59 +0100 Subject: [PATCH 1365/2244] Reorder initialization Initialize components in the pipeline order: demuxer first, decoder and recorder second. --- app/src/scrcpy.c | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index b636a03a..68665125 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -382,17 +382,20 @@ scrcpy(struct scrcpy_options *options) { file_pusher_initialized = true; } - struct sc_decoder *dec = NULL; + static const struct sc_demuxer_callbacks demuxer_cbs = { + .on_ended = sc_demuxer_on_ended, + }; + sc_demuxer_init(&s->demuxer, s->server.video_socket, &demuxer_cbs, NULL); + bool needs_decoder = options->display; #ifdef HAVE_V4L2 needs_decoder |= !!options->v4l2_device; #endif if (needs_decoder) { sc_decoder_init(&s->decoder); - dec = &s->decoder; + sc_demuxer_add_sink(&s->demuxer, &s->decoder.packet_sink); } - struct sc_recorder *rec = NULL; if (options->record_filename) { static const struct sc_recorder_callbacks recorder_cbs = { .on_ended = sc_recorder_on_ended, @@ -409,20 +412,7 @@ scrcpy(struct scrcpy_options *options) { } recorder_started = true; - rec = &s->recorder; - } - - static const struct sc_demuxer_callbacks demuxer_cbs = { - .on_ended = sc_demuxer_on_ended, - }; - sc_demuxer_init(&s->demuxer, s->server.video_socket, &demuxer_cbs, NULL); - - if (dec) { - sc_demuxer_add_sink(&s->demuxer, &dec->packet_sink); - } - - if (rec) { - sc_demuxer_add_sink(&s->demuxer, &rec->packet_sink); + sc_demuxer_add_sink(&s->demuxer, &s->recorder.packet_sink); } struct sc_controller *controller = NULL; From 5b2ec662220d87aa95d3794e7748a00c1163bcd6 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 3 Feb 2023 16:53:44 +0100 Subject: [PATCH 1366/2244] Simplify error handling on socket creation On any error, all previously opened sockets must be closed. Handle these errors in a single catch-block. Currently, there are only 2 sockets, but this will simplify even more with more sockets. Note: this commit is better displayed with --ignore-space-change (-b). --- .../genymobile/scrcpy/DesktopConnection.java | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java b/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java index 1f8f46e4..3cb36a09 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java +++ b/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java @@ -58,34 +58,34 @@ public final class DesktopConnection implements Closeable { public static DesktopConnection open(int scid, boolean tunnelForward, boolean control, boolean sendDummyByte) throws IOException { String socketName = getSocketName(scid); - LocalSocket videoSocket; + LocalSocket videoSocket = null; LocalSocket controlSocket = null; - if (tunnelForward) { - try (LocalServerSocket localServerSocket = new LocalServerSocket(socketName)) { - videoSocket = localServerSocket.accept(); - if (sendDummyByte) { - // send one byte so the client may read() to detect a connection error - videoSocket.getOutputStream().write(0); - } - if (control) { - try { + try { + if (tunnelForward) { + try (LocalServerSocket localServerSocket = new LocalServerSocket(socketName)) { + videoSocket = localServerSocket.accept(); + if (sendDummyByte) { + // send one byte so the client may read() to detect a connection error + videoSocket.getOutputStream().write(0); + } + if (control) { controlSocket = localServerSocket.accept(); - } catch (IOException | RuntimeException e) { - videoSocket.close(); - throw e; } } - } - } else { - videoSocket = connect(socketName); - if (control) { - try { + } else { + videoSocket = connect(socketName); + if (control) { controlSocket = connect(socketName); - } catch (IOException | RuntimeException e) { - videoSocket.close(); - throw e; } } + } catch (IOException | RuntimeException e) { + if (videoSocket != null) { + videoSocket.close(); + } + if (controlSocket != null) { + controlSocket.close(); + } + throw e; } return new DesktopConnection(videoSocket, controlSocket); From ae29e5b56200035a81c7321c1617fe05928fc5b0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 6 Feb 2023 11:44:18 +0100 Subject: [PATCH 1367/2244] Use VideoStreamer directly from ScreenEncoder The Callbacks interface notifies new packets. But in addition, the screen encoder will need to write headers on start. We could add a function onStart(), but for simplicity, just remove the interface, which brings no value, and call the streamer directly. Refs 87972e2022686b1176bfaf0c678e703856c2b027 --- .../java/com/genymobile/scrcpy/ScreenEncoder.java | 12 ++++-------- .../java/com/genymobile/scrcpy/VideoStreamer.java | 5 ++--- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index 44ba75d1..3ca2e80c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -21,10 +21,6 @@ import java.util.concurrent.atomic.AtomicBoolean; public class ScreenEncoder implements Device.RotationListener { - public interface Callbacks { - void onPacket(ByteBuffer codecBuffer, MediaCodec.BufferInfo bufferInfo) throws IOException; - } - 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"; @@ -63,7 +59,7 @@ public class ScreenEncoder implements Device.RotationListener { return rotationChanged.getAndSet(false); } - public void streamScreen(Device device, Callbacks callbacks) throws IOException, ConfigurationException { + public void streamScreen(Device device, VideoStreamer streamer) throws IOException, ConfigurationException { MediaCodec codec = createCodec(videoMimeType, encoderName); MediaFormat format = createFormat(videoMimeType, bitRate, maxFps, codecOptions); IBinder display = createDisplay(); @@ -92,7 +88,7 @@ public class ScreenEncoder implements Device.RotationListener { codec.start(); - alive = encode(codec, callbacks); + alive = encode(codec, streamer); // do not call stop() on exception, it would trigger an IllegalStateException codec.stop(); } catch (IllegalStateException | IllegalArgumentException e) { @@ -161,7 +157,7 @@ public class ScreenEncoder implements Device.RotationListener { return 0; } - private boolean encode(MediaCodec codec, Callbacks callbacks) throws IOException { + private boolean encode(MediaCodec codec, VideoStreamer streamer) throws IOException { boolean eof = false; MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); @@ -184,7 +180,7 @@ public class ScreenEncoder implements Device.RotationListener { consecutiveErrors = 0; } - callbacks.onPacket(codecBuffer, bufferInfo); + streamer.writePacket(codecBuffer, bufferInfo); } } finally { if (outputBufferId >= 0) { diff --git a/server/src/main/java/com/genymobile/scrcpy/VideoStreamer.java b/server/src/main/java/com/genymobile/scrcpy/VideoStreamer.java index 943c641d..cbde141e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/VideoStreamer.java +++ b/server/src/main/java/com/genymobile/scrcpy/VideoStreamer.java @@ -6,7 +6,7 @@ import java.io.FileDescriptor; import java.io.IOException; import java.nio.ByteBuffer; -public final class VideoStreamer implements ScreenEncoder.Callbacks { +public final class VideoStreamer { private static final long PACKET_FLAG_CONFIG = 1L << 63; private static final long PACKET_FLAG_KEY_FRAME = 1L << 62; @@ -28,8 +28,7 @@ public final class VideoStreamer implements ScreenEncoder.Callbacks { IO.writeFully(fd, buffer); } - @Override - public void onPacket(ByteBuffer codecBuffer, MediaCodec.BufferInfo bufferInfo) throws IOException { + public void writePacket(ByteBuffer codecBuffer, MediaCodec.BufferInfo bufferInfo) throws IOException { if (sendFrameMeta) { writeFrameMeta(fd, bufferInfo, codecBuffer.remaining()); } From 51628201b77c7f7c990e26471aa88c5ef930061c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 6 Feb 2023 11:57:37 +0100 Subject: [PATCH 1368/2244] Write streamer header from ScreenEncoder The screen encoder is responsible for writing data to the video streamer. --- .../com/genymobile/scrcpy/ScreenEncoder.java | 3 +++ .../java/com/genymobile/scrcpy/Server.java | 5 +---- .../com/genymobile/scrcpy/VideoStreamer.java | 18 ++++++++++++------ 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index 3ca2e80c..c86bc9a5 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -64,6 +64,9 @@ public class ScreenEncoder implements Device.RotationListener { MediaFormat format = createFormat(videoMimeType, bitRate, maxFps, codecOptions); IBinder display = createDisplay(); device.setRotationListener(this); + + streamer.writeHeader(); + boolean alive; try { do { diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 027050af..d75e8310 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -104,10 +104,7 @@ public final class Server { try { // synchronous - VideoStreamer videoStreamer = new VideoStreamer(connection.getVideoFd(), options.getSendFrameMeta()); - if (options.getSendCodecId()) { - videoStreamer.writeHeader(codec.getId()); - } + VideoStreamer videoStreamer = new VideoStreamer(connection.getVideoFd(), codec, options.getSendCodecId(), options.getSendFrameMeta()); screenEncoder.streamScreen(device, videoStreamer); } catch (IOException e) { // Broken pipe is expected on close, because the socket is closed by the client diff --git a/server/src/main/java/com/genymobile/scrcpy/VideoStreamer.java b/server/src/main/java/com/genymobile/scrcpy/VideoStreamer.java index cbde141e..5858d7d8 100644 --- a/server/src/main/java/com/genymobile/scrcpy/VideoStreamer.java +++ b/server/src/main/java/com/genymobile/scrcpy/VideoStreamer.java @@ -12,20 +12,26 @@ public final class VideoStreamer { private static final long PACKET_FLAG_KEY_FRAME = 1L << 62; private final FileDescriptor fd; + private final VideoCodec codec; + private final boolean sendCodecId; private final boolean sendFrameMeta; private final ByteBuffer headerBuffer = ByteBuffer.allocate(12); - public VideoStreamer(FileDescriptor fd, boolean sendFrameMeta) { + public VideoStreamer(FileDescriptor fd, VideoCodec codec, boolean sendCodecId, boolean sendFrameMeta) { this.fd = fd; + this.codec = codec; + this.sendCodecId = sendCodecId; this.sendFrameMeta = sendFrameMeta; } - public void writeHeader(int codecId) throws IOException { - ByteBuffer buffer = ByteBuffer.allocate(4); - buffer.putInt(codecId); - buffer.flip(); - IO.writeFully(fd, buffer); + public void writeHeader() throws IOException { + if (sendCodecId) { + ByteBuffer buffer = ByteBuffer.allocate(4); + buffer.putInt(codec.getId()); + buffer.flip(); + IO.writeFully(fd, buffer); + } } public void writePacket(ByteBuffer codecBuffer, MediaCodec.BufferInfo bufferInfo) throws IOException { From ae9b08b90540d0f357255b97e5b2d3d51b9b0d4e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 6 Feb 2023 14:09:20 +0100 Subject: [PATCH 1369/2244] Move screen encoder initialization This prepares further refactors. --- server/src/main/java/com/genymobile/scrcpy/Server.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index d75e8310..ed1dc1bb 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -91,8 +91,6 @@ public final class Server { Size videoSize = device.getScreenInfo().getVideoSize(); connection.sendDeviceMeta(Device.getDeviceName(), videoSize.getWidth(), videoSize.getHeight()); } - ScreenEncoder screenEncoder = new ScreenEncoder(codec.getMimeType(), options.getBitRate(), options.getMaxFps(), codecOptions, - options.getEncoderName(), options.getDownsizeOnError()); if (control) { controller = new Controller(device, connection, options.getClipboardAutosync(), options.getPowerOn()); @@ -102,9 +100,11 @@ public final class Server { device.setClipboardListener(text -> controllerRef.getSender().pushClipboardText(text)); } + VideoStreamer videoStreamer = new VideoStreamer(connection.getVideoFd(), codec, options.getSendCodecId(), options.getSendFrameMeta()); + ScreenEncoder screenEncoder = new ScreenEncoder(codec.getMimeType(), options.getBitRate(), options.getMaxFps(), codecOptions, + options.getEncoderName(), options.getDownsizeOnError()); try { // synchronous - VideoStreamer videoStreamer = new VideoStreamer(connection.getVideoFd(), codec, options.getSendCodecId(), options.getSendFrameMeta()); screenEncoder.streamScreen(device, videoStreamer); } catch (IOException e) { // Broken pipe is expected on close, because the socket is closed by the client From c2267979917215415befb67115bce5cbca1e5006 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 6 Feb 2023 13:46:19 +0100 Subject: [PATCH 1370/2244] Pass all args to ScreenEncoder constructor There is no good reason to pass some of them in the constructor and some others as parameters of the streamScreen() method. --- .../java/com/genymobile/scrcpy/ScreenEncoder.java | 12 ++++++++---- .../src/main/java/com/genymobile/scrcpy/Server.java | 4 ++-- .../java/com/genymobile/scrcpy/VideoStreamer.java | 4 ++++ 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index c86bc9a5..6f47c7f0 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -31,7 +31,8 @@ public class ScreenEncoder implements Device.RotationListener { private final AtomicBoolean rotationChanged = new AtomicBoolean(); - private final String videoMimeType; + private final Device device; + private final VideoStreamer streamer; private final String encoderName; private final List codecOptions; private final int bitRate; @@ -41,8 +42,10 @@ public class ScreenEncoder implements Device.RotationListener { private boolean firstFrameSent; private int consecutiveErrors; - public ScreenEncoder(String videoMimeType, int bitRate, int maxFps, List codecOptions, String encoderName, boolean downsizeOnError) { - this.videoMimeType = videoMimeType; + public ScreenEncoder(Device device, VideoStreamer streamer, int bitRate, int maxFps, List codecOptions, String encoderName, + boolean downsizeOnError) { + this.device = device; + this.streamer = streamer; this.bitRate = bitRate; this.maxFps = maxFps; this.codecOptions = codecOptions; @@ -59,7 +62,8 @@ public class ScreenEncoder implements Device.RotationListener { return rotationChanged.getAndSet(false); } - public void streamScreen(Device device, VideoStreamer streamer) throws IOException, ConfigurationException { + public void streamScreen() throws IOException, ConfigurationException { + String videoMimeType = streamer.getCodec().getMimeType(); MediaCodec codec = createCodec(videoMimeType, encoderName); MediaFormat format = createFormat(videoMimeType, bitRate, maxFps, codecOptions); IBinder display = createDisplay(); diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index ed1dc1bb..85234c4e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -101,11 +101,11 @@ public final class Server { } VideoStreamer videoStreamer = new VideoStreamer(connection.getVideoFd(), codec, options.getSendCodecId(), options.getSendFrameMeta()); - ScreenEncoder screenEncoder = new ScreenEncoder(codec.getMimeType(), options.getBitRate(), options.getMaxFps(), codecOptions, + ScreenEncoder screenEncoder = new ScreenEncoder(device, videoStreamer, options.getBitRate(), options.getMaxFps(), codecOptions, options.getEncoderName(), options.getDownsizeOnError()); try { // synchronous - screenEncoder.streamScreen(device, videoStreamer); + screenEncoder.streamScreen(); } catch (IOException e) { // Broken pipe is expected on close, because the socket is closed by the client if (!IO.isBrokenPipe(e)) { diff --git a/server/src/main/java/com/genymobile/scrcpy/VideoStreamer.java b/server/src/main/java/com/genymobile/scrcpy/VideoStreamer.java index 5858d7d8..24f48082 100644 --- a/server/src/main/java/com/genymobile/scrcpy/VideoStreamer.java +++ b/server/src/main/java/com/genymobile/scrcpy/VideoStreamer.java @@ -25,6 +25,10 @@ public final class VideoStreamer { this.sendFrameMeta = sendFrameMeta; } + public VideoCodec getCodec() { + return codec; + } + public void writeHeader() throws IOException { if (sendCodecId) { ByteBuffer buffer = ByteBuffer.allocate(4); From 10ce0f376a89579408337b084be3e05ebcd18d92 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 6 Feb 2023 14:52:09 +0100 Subject: [PATCH 1371/2244] Make streamer independent of codec type Rename VideoStreamer to Streamer, and extract a Codec interface which will also support audio codecs. PR #3757 --- .../main/java/com/genymobile/scrcpy/Codec.java | 16 ++++++++++++++++ .../com/genymobile/scrcpy/ScreenEncoder.java | 6 +++--- .../main/java/com/genymobile/scrcpy/Server.java | 6 +++--- .../scrcpy/{VideoStreamer.java => Streamer.java} | 8 ++++---- .../java/com/genymobile/scrcpy/VideoCodec.java | 14 +++++++++++++- 5 files changed, 39 insertions(+), 11 deletions(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/Codec.java rename server/src/main/java/com/genymobile/scrcpy/{VideoStreamer.java => Streamer.java} (89%) diff --git a/server/src/main/java/com/genymobile/scrcpy/Codec.java b/server/src/main/java/com/genymobile/scrcpy/Codec.java new file mode 100644 index 00000000..50e8acad --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/Codec.java @@ -0,0 +1,16 @@ +package com.genymobile.scrcpy; + +public interface Codec { + + enum Type { + VIDEO, + } + + Type getType(); + + int getId(); + + String getName(); + + String getMimeType(); +} diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index 6f47c7f0..a79ce079 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -32,7 +32,7 @@ public class ScreenEncoder implements Device.RotationListener { private final AtomicBoolean rotationChanged = new AtomicBoolean(); private final Device device; - private final VideoStreamer streamer; + private final Streamer streamer; private final String encoderName; private final List codecOptions; private final int bitRate; @@ -42,7 +42,7 @@ public class ScreenEncoder implements Device.RotationListener { private boolean firstFrameSent; private int consecutiveErrors; - public ScreenEncoder(Device device, VideoStreamer streamer, int bitRate, int maxFps, List codecOptions, String encoderName, + public ScreenEncoder(Device device, Streamer streamer, int bitRate, int maxFps, List codecOptions, String encoderName, boolean downsizeOnError) { this.device = device; this.streamer = streamer; @@ -164,7 +164,7 @@ public class ScreenEncoder implements Device.RotationListener { return 0; } - private boolean encode(MediaCodec codec, VideoStreamer streamer) throws IOException { + private boolean encode(MediaCodec codec, Streamer streamer) throws IOException { boolean eof = false; MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 85234c4e..d570a9fb 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -100,9 +100,9 @@ public final class Server { device.setClipboardListener(text -> controllerRef.getSender().pushClipboardText(text)); } - VideoStreamer videoStreamer = new VideoStreamer(connection.getVideoFd(), codec, options.getSendCodecId(), options.getSendFrameMeta()); - ScreenEncoder screenEncoder = new ScreenEncoder(device, videoStreamer, options.getBitRate(), options.getMaxFps(), codecOptions, - options.getEncoderName(), options.getDownsizeOnError()); + Streamer videoStreamer = new Streamer(connection.getVideoFd(), codec, options.getSendCodecId(), options.getSendFrameMeta()); + ScreenEncoder screenEncoder = new ScreenEncoder(device, videoStreamer, options.getBitRate(), options.getMaxFps(), + codecOptions, options.getEncoderName(), options.getDownsizeOnError()); try { // synchronous screenEncoder.streamScreen(); diff --git a/server/src/main/java/com/genymobile/scrcpy/VideoStreamer.java b/server/src/main/java/com/genymobile/scrcpy/Streamer.java similarity index 89% rename from server/src/main/java/com/genymobile/scrcpy/VideoStreamer.java rename to server/src/main/java/com/genymobile/scrcpy/Streamer.java index 24f48082..d6bf6780 100644 --- a/server/src/main/java/com/genymobile/scrcpy/VideoStreamer.java +++ b/server/src/main/java/com/genymobile/scrcpy/Streamer.java @@ -6,26 +6,26 @@ import java.io.FileDescriptor; import java.io.IOException; import java.nio.ByteBuffer; -public final class VideoStreamer { +public final class Streamer { private static final long PACKET_FLAG_CONFIG = 1L << 63; private static final long PACKET_FLAG_KEY_FRAME = 1L << 62; private final FileDescriptor fd; - private final VideoCodec codec; + private final Codec codec; private final boolean sendCodecId; private final boolean sendFrameMeta; private final ByteBuffer headerBuffer = ByteBuffer.allocate(12); - public VideoStreamer(FileDescriptor fd, VideoCodec codec, boolean sendCodecId, boolean sendFrameMeta) { + public Streamer(FileDescriptor fd, Codec codec, boolean sendCodecId, boolean sendFrameMeta) { this.fd = fd; this.codec = codec; this.sendCodecId = sendCodecId; this.sendFrameMeta = sendFrameMeta; } - public VideoCodec getCodec() { + public Codec getCodec() { return codec; } diff --git a/server/src/main/java/com/genymobile/scrcpy/VideoCodec.java b/server/src/main/java/com/genymobile/scrcpy/VideoCodec.java index e19b27f0..43531f1e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/VideoCodec.java +++ b/server/src/main/java/com/genymobile/scrcpy/VideoCodec.java @@ -3,7 +3,7 @@ package com.genymobile.scrcpy; import android.annotation.SuppressLint; import android.media.MediaFormat; -public enum VideoCodec { +public enum VideoCodec implements Codec { H264(0x68_32_36_34, "h264", MediaFormat.MIMETYPE_VIDEO_AVC), H265(0x68_32_36_35, "h265", MediaFormat.MIMETYPE_VIDEO_HEVC), @SuppressLint("InlinedApi") // introduced in API 21 @@ -19,10 +19,22 @@ public enum VideoCodec { this.mimeType = mimeType; } + @Override + public Type getType() { + return Type.VIDEO; + } + + @Override public int getId() { return id; } + @Override + public String getName() { + return name; + } + + @Override public String getMimeType() { return mimeType; } From 90d88a6927901345afbe81550c7a489304a61e63 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 19 Feb 2023 19:51:50 +0100 Subject: [PATCH 1372/2244] Rename "codec" variable to "mediaCodec" This will allow to use "codec" for the Codec type. PR #3757 --- .../com/genymobile/scrcpy/ScreenEncoder.java | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index a79ce079..c697cfa2 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -64,7 +64,7 @@ public class ScreenEncoder implements Device.RotationListener { public void streamScreen() throws IOException, ConfigurationException { String videoMimeType = streamer.getCodec().getMimeType(); - MediaCodec codec = createCodec(videoMimeType, encoderName); + MediaCodec mediaCodec = createMediaCodec(videoMimeType, encoderName); MediaFormat format = createFormat(videoMimeType, bitRate, maxFps, codecOptions); IBinder display = createDisplay(); device.setRotationListener(this); @@ -84,8 +84,8 @@ public class ScreenEncoder implements Device.RotationListener { Surface surface = null; try { - codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); - surface = codec.createInputSurface(); + mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); + surface = mediaCodec.createInputSurface(); // does not include the locked video orientation Rect unlockedVideoRect = screenInfo.getUnlockedVideoSize().toRect(); @@ -93,11 +93,11 @@ public class ScreenEncoder implements Device.RotationListener { int layerStack = device.getLayerStack(); setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack); - codec.start(); + mediaCodec.start(); - alive = encode(codec, streamer); + alive = encode(mediaCodec, streamer); // do not call stop() on exception, it would trigger an IllegalStateException - codec.stop(); + mediaCodec.stop(); } catch (IllegalStateException | IllegalArgumentException e) { Ln.e("Encoding error: " + e.getClass().getName() + ": " + e.getMessage()); if (!prepareRetry(device, screenInfo)) { @@ -106,14 +106,14 @@ public class ScreenEncoder implements Device.RotationListener { Ln.i("Retrying..."); alive = true; } finally { - codec.reset(); + mediaCodec.reset(); if (surface != null) { surface.release(); } } } while (alive); } finally { - codec.release(); + mediaCodec.release(); device.setRotationListener(null); SurfaceControl.destroyDisplay(display); } @@ -210,7 +210,7 @@ public class ScreenEncoder implements Device.RotationListener { return result.toArray(new MediaCodecInfo[result.size()]); } - private static MediaCodec createCodec(String videoMimeType, String encoderName) throws IOException, ConfigurationException { + private static MediaCodec createMediaCodec(String videoMimeType, String encoderName) throws IOException, ConfigurationException { if (encoderName != null) { Ln.d("Creating encoder by name: '" + encoderName + "'"); try { @@ -220,9 +220,9 @@ public class ScreenEncoder implements Device.RotationListener { throw new ConfigurationException("Unknown encoder: " + encoderName); } } - MediaCodec codec = MediaCodec.createEncoderByType(videoMimeType); - Ln.d("Using encoder: '" + codec.getName() + "'"); - return codec; + MediaCodec mediaCodec = MediaCodec.createEncoderByType(videoMimeType); + Ln.d("Using encoder: '" + mediaCodec.getName() + "'"); + return mediaCodec; } private static String buildUnknownEncoderMessage(String videoMimeType, String encoderName) { From 10ef8da95d9f5d66b3755b88b4d1fda032163260 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 19 Feb 2023 19:58:05 +0100 Subject: [PATCH 1373/2244] Improve error message for unknown encoder The provided encoder name depends on the selected codec. Improve the error message and the suggestions. PR #3757 --- .../com/genymobile/scrcpy/ScreenEncoder.java | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index c697cfa2..fdd23bf3 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -63,9 +63,9 @@ public class ScreenEncoder implements Device.RotationListener { } public void streamScreen() throws IOException, ConfigurationException { - String videoMimeType = streamer.getCodec().getMimeType(); - MediaCodec mediaCodec = createMediaCodec(videoMimeType, encoderName); - MediaFormat format = createFormat(videoMimeType, bitRate, maxFps, codecOptions); + Codec codec = streamer.getCodec(); + MediaCodec mediaCodec = createMediaCodec(codec, encoderName); + MediaFormat format = createFormat(codec.getMimeType(), bitRate, maxFps, codecOptions); IBinder display = createDisplay(); device.setRotationListener(this); @@ -210,28 +210,28 @@ public class ScreenEncoder implements Device.RotationListener { return result.toArray(new MediaCodecInfo[result.size()]); } - private static MediaCodec createMediaCodec(String videoMimeType, String encoderName) throws IOException, ConfigurationException { + private static MediaCodec createMediaCodec(Codec codec, String encoderName) throws IOException, ConfigurationException { if (encoderName != null) { Ln.d("Creating encoder by name: '" + encoderName + "'"); try { return MediaCodec.createByCodecName(encoderName); } catch (IllegalArgumentException e) { - Ln.e(buildUnknownEncoderMessage(videoMimeType, encoderName)); + Ln.e(buildUnknownEncoderMessage(codec, encoderName)); throw new ConfigurationException("Unknown encoder: " + encoderName); } } - MediaCodec mediaCodec = MediaCodec.createEncoderByType(videoMimeType); + MediaCodec mediaCodec = MediaCodec.createEncoderByType(codec.getMimeType()); Ln.d("Using encoder: '" + mediaCodec.getName() + "'"); return mediaCodec; } - private static String buildUnknownEncoderMessage(String videoMimeType, String encoderName) { - StringBuilder msg = new StringBuilder("Encoder '").append(encoderName).append("' not found"); - MediaCodecInfo[] encoders = listEncoders(videoMimeType); + private static String buildUnknownEncoderMessage(Codec codec, String encoderName) { + StringBuilder msg = new StringBuilder("Encoder '").append(encoderName).append("' for ").append(codec.getName()).append(" not found"); + MediaCodecInfo[] encoders = listEncoders(codec.getMimeType()); if (encoders != null && encoders.length > 0) { msg.append("\nTry to use one of the available encoders:"); for (MediaCodecInfo encoder : encoders) { - msg.append("\n scrcpy --encoder='").append(encoder.getName()).append("'"); + msg.append("\n scrcpy --codec=").append(codec.getName()).append(" --encoder='").append(encoder.getName()).append("'"); } } return msg.toString(); From a5541b3476fe32306de7cb02472cf9077dd45498 Mon Sep 17 00:00:00 2001 From: Simon Chan <1330321+yume-chan@users.noreply.github.com> Date: Fri, 27 Jan 2023 20:13:37 +0800 Subject: [PATCH 1374/2244] Add a fake Android Context Since scrcpy-server is not an Android application (it's a java executable), it has no Context. Some features will require a Context instance to get the package name and the UID. Add a FakeContext for this purpose. PR #3757 Co-authored-by: Romain Vimont Signed-off-by: Romain Vimont --- .../com/genymobile/scrcpy/FakeContext.java | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 server/src/main/java/com/genymobile/scrcpy/FakeContext.java diff --git a/server/src/main/java/com/genymobile/scrcpy/FakeContext.java b/server/src/main/java/com/genymobile/scrcpy/FakeContext.java new file mode 100644 index 00000000..ddd6177b --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/FakeContext.java @@ -0,0 +1,40 @@ +package com.genymobile.scrcpy; + +import android.annotation.TargetApi; +import android.content.AttributionSource; +import android.content.ContextWrapper; +import android.os.Build; +import android.os.Process; + +public final class FakeContext extends ContextWrapper { + + public static final String PACKAGE_NAME = "com.android.shell"; + + private static final FakeContext INSTANCE = new FakeContext(); + + public static FakeContext get() { + return INSTANCE; + } + + private FakeContext() { + super(null); + } + + @Override + public String getPackageName() { + return PACKAGE_NAME; + } + + @Override + public String getOpPackageName() { + return PACKAGE_NAME; + } + + @TargetApi(Build.VERSION_CODES.S) + @Override + public AttributionSource getAttributionSource() { + AttributionSource.Builder builder = new AttributionSource.Builder(Process.SHELL_UID); + builder.setPackageName(PACKAGE_NAME); + return builder.build(); + } +} From 9a815ceba8eccc53c724ef0eb79c6ab277446ad9 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 31 Jan 2023 22:31:15 +0100 Subject: [PATCH 1375/2244] Use AttributionSource from FakeContext FakeContext already provides an AttributeSource instance. PR #3757 Co-authored-by: Simon Chan <1330321+yume-chan@users.noreply.github.com> --- .../scrcpy/wrappers/ContentProvider.java | 55 ++++++++----------- 1 file changed, 23 insertions(+), 32 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java index 47eae64d..854d4136 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java @@ -1,9 +1,12 @@ package com.genymobile.scrcpy.wrappers; +import com.genymobile.scrcpy.FakeContext; import com.genymobile.scrcpy.Ln; import com.genymobile.scrcpy.SettingsException; import android.annotation.SuppressLint; +import android.content.AttributionSource; +import android.os.Build; import android.os.Bundle; import android.os.IBinder; @@ -51,11 +54,10 @@ public class ContentProvider implements Closeable { @SuppressLint("PrivateApi") private Method getCallMethod() throws NoSuchMethodException { if (callMethod == null) { - try { - Class attributionSourceClass = Class.forName("android.content.AttributionSource"); - callMethod = provider.getClass().getMethod("call", attributionSourceClass, String.class, String.class, String.class, Bundle.class); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + callMethod = provider.getClass().getMethod("call", AttributionSource.class, String.class, String.class, String.class, Bundle.class); callMethodVersion = 0; - } catch (NoSuchMethodException | ClassNotFoundException e0) { + } else { // old versions try { callMethod = provider.getClass() @@ -75,40 +77,29 @@ public class ContentProvider implements Closeable { return callMethod; } - @SuppressLint("PrivateApi") - private Object getAttributionSource() - throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { - if (attributionSource == null) { - Class cl = Class.forName("android.content.AttributionSource$Builder"); - Object builder = cl.getConstructor(int.class).newInstance(ServiceManager.USER_ID); - cl.getDeclaredMethod("setPackageName", String.class).invoke(builder, ServiceManager.PACKAGE_NAME); - attributionSource = cl.getDeclaredMethod("build").invoke(builder); - } - - return attributionSource; - } - private Bundle call(String callMethod, String arg, Bundle extras) - throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { + throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { try { Method method = getCallMethod(); Object[] args; - switch (callMethodVersion) { - case 0: - args = new Object[]{getAttributionSource(), "settings", callMethod, arg, extras}; - break; - case 1: - args = new Object[]{ServiceManager.PACKAGE_NAME, null, "settings", callMethod, arg, extras}; - break; - case 2: - args = new Object[]{ServiceManager.PACKAGE_NAME, "settings", callMethod, arg, extras}; - break; - default: - args = new Object[]{ServiceManager.PACKAGE_NAME, callMethod, arg, extras}; - break; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && callMethodVersion == 0) { + args = new Object[]{FakeContext.get().getAttributionSource(), "settings", callMethod, arg, extras}; + } else { + switch (callMethodVersion) { + case 1: + args = new Object[]{ServiceManager.PACKAGE_NAME, null, "settings", callMethod, arg, extras}; + break; + case 2: + args = new Object[]{ServiceManager.PACKAGE_NAME, "settings", callMethod, arg, extras}; + break; + default: + args = new Object[]{ServiceManager.PACKAGE_NAME, callMethod, arg, extras}; + break; + } } return (Bundle) method.invoke(provider, args); - } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException | ClassNotFoundException | InstantiationException e) { + } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { Ln.e("Could not invoke method", e); throw e; } From 820189b6a6b570388187f166b8def499d73ced8a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 31 Jan 2023 22:32:33 +0100 Subject: [PATCH 1376/2244] Use PACKAGE_NAME from FakeContext Remove duplicated constant. PR #3757 --- .../scrcpy/wrappers/ClipboardManager.java | 19 ++++++++++--------- .../scrcpy/wrappers/ContentProvider.java | 6 +++--- .../scrcpy/wrappers/ServiceManager.java | 1 - 3 files changed, 13 insertions(+), 13 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 f43a76bc..e0af7923 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java @@ -1,5 +1,6 @@ package com.genymobile.scrcpy.wrappers; +import com.genymobile.scrcpy.FakeContext; import com.genymobile.scrcpy.Ln; import android.content.ClipData; @@ -58,22 +59,22 @@ public class ClipboardManager { private static ClipData getPrimaryClip(Method method, boolean alternativeMethod, IInterface manager) throws InvocationTargetException, IllegalAccessException { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { - return (ClipData) method.invoke(manager, ServiceManager.PACKAGE_NAME); + return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME); } if (alternativeMethod) { - return (ClipData) method.invoke(manager, ServiceManager.PACKAGE_NAME, null, ServiceManager.USER_ID); + return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, ServiceManager.USER_ID); } - return (ClipData) method.invoke(manager, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID); + return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, ServiceManager.USER_ID); } private static void setPrimaryClip(Method method, boolean alternativeMethod, IInterface manager, ClipData clipData) throws InvocationTargetException, IllegalAccessException { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { - method.invoke(manager, clipData, ServiceManager.PACKAGE_NAME); + method.invoke(manager, clipData, FakeContext.PACKAGE_NAME); } else if (alternativeMethod) { - method.invoke(manager, clipData, ServiceManager.PACKAGE_NAME, null, ServiceManager.USER_ID); + method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, ServiceManager.USER_ID); } else { - method.invoke(manager, clipData, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID); + method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, ServiceManager.USER_ID); } } @@ -106,11 +107,11 @@ public class ClipboardManager { private static void addPrimaryClipChangedListener(Method method, boolean alternativeMethod, IInterface manager, IOnPrimaryClipChangedListener listener) throws InvocationTargetException, IllegalAccessException { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { - method.invoke(manager, listener, ServiceManager.PACKAGE_NAME); + method.invoke(manager, listener, FakeContext.PACKAGE_NAME); } else if (alternativeMethod) { - method.invoke(manager, listener, ServiceManager.PACKAGE_NAME, null, ServiceManager.USER_ID); + method.invoke(manager, listener, FakeContext.PACKAGE_NAME, null, ServiceManager.USER_ID); } else { - method.invoke(manager, listener, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID); + method.invoke(manager, listener, FakeContext.PACKAGE_NAME, ServiceManager.USER_ID); } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java index 854d4136..2bdbe175 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java @@ -88,13 +88,13 @@ public class ContentProvider implements Closeable { } else { switch (callMethodVersion) { case 1: - args = new Object[]{ServiceManager.PACKAGE_NAME, null, "settings", callMethod, arg, extras}; + args = new Object[]{FakeContext.PACKAGE_NAME, null, "settings", callMethod, arg, extras}; break; case 2: - args = new Object[]{ServiceManager.PACKAGE_NAME, "settings", callMethod, arg, extras}; + args = new Object[]{FakeContext.PACKAGE_NAME, "settings", callMethod, arg, extras}; break; default: - args = new Object[]{ServiceManager.PACKAGE_NAME, callMethod, arg, extras}; + args = new Object[]{FakeContext.PACKAGE_NAME, callMethod, arg, extras}; break; } } 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 cb6863b6..ff4b5e23 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java @@ -10,7 +10,6 @@ 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 static final Method GET_SERVICE_METHOD; From 42285ae86906447708def6783d3edee433624406 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 26 Feb 2023 22:29:58 +0100 Subject: [PATCH 1377/2244] Use ROOT_UID from FakeContext Remove USER_ID from ServiceManager, and replace it by a constant in FakeContext. This is the same as android.os.Process.ROOT_UID, but this constant has been introduced in API 29. PR #3757 --- .../main/java/com/genymobile/scrcpy/FakeContext.java | 1 + .../genymobile/scrcpy/wrappers/ActivityManager.java | 5 +++-- .../genymobile/scrcpy/wrappers/ClipboardManager.java | 12 ++++++------ .../genymobile/scrcpy/wrappers/ContentProvider.java | 4 ++-- .../genymobile/scrcpy/wrappers/ServiceManager.java | 2 -- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/FakeContext.java b/server/src/main/java/com/genymobile/scrcpy/FakeContext.java index ddd6177b..844d6bd8 100644 --- a/server/src/main/java/com/genymobile/scrcpy/FakeContext.java +++ b/server/src/main/java/com/genymobile/scrcpy/FakeContext.java @@ -9,6 +9,7 @@ import android.os.Process; public final class FakeContext extends ContextWrapper { public static final String PACKAGE_NAME = "com.android.shell"; + public static final int ROOT_UID = 0; // Like android.os.Process.ROOT_UID, but before API 29 private static final FakeContext INSTANCE = new FakeContext(); diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java index 93ed4528..76aab5f1 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java @@ -1,5 +1,6 @@ package com.genymobile.scrcpy.wrappers; +import com.genymobile.scrcpy.FakeContext; import com.genymobile.scrcpy.Ln; import android.os.Binder; @@ -48,10 +49,10 @@ public class ActivityManager { Object[] args; if (getContentProviderExternalMethodNewVersion) { // new version - args = new Object[]{name, ServiceManager.USER_ID, token, null}; + args = new Object[]{name, FakeContext.ROOT_UID, token, null}; } else { // old version - args = new Object[]{name, ServiceManager.USER_ID, token}; + args = new Object[]{name, FakeContext.ROOT_UID, token}; } // ContentProviderHolder providerHolder = getContentProviderExternal(...); Object providerHolder = method.invoke(manager, args); 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 e0af7923..0c1777ec 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java @@ -62,9 +62,9 @@ public class ClipboardManager { return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME); } if (alternativeMethod) { - return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, ServiceManager.USER_ID); + return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID); } - return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, ServiceManager.USER_ID); + return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID); } private static void setPrimaryClip(Method method, boolean alternativeMethod, IInterface manager, ClipData clipData) @@ -72,9 +72,9 @@ public class ClipboardManager { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { method.invoke(manager, clipData, FakeContext.PACKAGE_NAME); } else if (alternativeMethod) { - method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, ServiceManager.USER_ID); + method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID); } else { - method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, ServiceManager.USER_ID); + method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID); } } @@ -109,9 +109,9 @@ public class ClipboardManager { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { method.invoke(manager, listener, FakeContext.PACKAGE_NAME); } else if (alternativeMethod) { - method.invoke(manager, listener, FakeContext.PACKAGE_NAME, null, ServiceManager.USER_ID); + method.invoke(manager, listener, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID); } else { - method.invoke(manager, listener, FakeContext.PACKAGE_NAME, ServiceManager.USER_ID); + method.invoke(manager, listener, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID); } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java index 2bdbe175..4917f5eb 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java @@ -138,7 +138,7 @@ public class ContentProvider implements Closeable { public String getValue(String table, String key) throws SettingsException { String method = getGetMethod(table); Bundle arg = new Bundle(); - arg.putInt(CALL_METHOD_USER_KEY, ServiceManager.USER_ID); + arg.putInt(CALL_METHOD_USER_KEY, FakeContext.ROOT_UID); try { Bundle bundle = call(method, key, arg); if (bundle == null) { @@ -154,7 +154,7 @@ public class ContentProvider implements Closeable { public void putValue(String table, String key, String value) throws SettingsException { String method = getPutMethod(table); Bundle arg = new Bundle(); - arg.putInt(CALL_METHOD_USER_KEY, ServiceManager.USER_ID); + arg.putInt(CALL_METHOD_USER_KEY, FakeContext.ROOT_UID); arg.putString(NAME_VALUE_TABLE_VALUE, value); try { call(method, key, arg); 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 ff4b5e23..ee2f0fa9 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java @@ -10,8 +10,6 @@ import java.lang.reflect.Method; @SuppressLint("PrivateApi,DiscouragedPrivateApi") public final class ServiceManager { - public static final int USER_ID = 0; - private static final Method GET_SERVICE_METHOD; static { try { From 84ba6435bb39c1bee194fd658a7aeb913f4ca0d9 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 31 Jan 2023 22:33:30 +0100 Subject: [PATCH 1378/2244] Use shell package name for workarounds For consistency. PR #3757 --- 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 0f473bc1..e2345e11 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java +++ b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java @@ -50,7 +50,7 @@ public final class Workarounds { Object appBindData = appBindDataConstructor.newInstance(); ApplicationInfo applicationInfo = new ApplicationInfo(); - applicationInfo.packageName = "com.genymobile.scrcpy"; + applicationInfo.packageName = FakeContext.PACKAGE_NAME; // appBindData.appInfo = applicationInfo; Field appInfoField = appBindDataClass.getDeclaredField("appInfo"); From 8487ddba644b5d2aad762bea586654f9a7891b5e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 31 Jan 2023 22:48:50 +0100 Subject: [PATCH 1379/2244] Use FakeContext for Application instance This will expose the correct package name and UID to the application context. PR #3757 --- .../java/com/genymobile/scrcpy/Workarounds.java | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java index e2345e11..64cc1272 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java +++ b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java @@ -2,14 +2,12 @@ package com.genymobile.scrcpy; import android.annotation.SuppressLint; import android.app.Application; -import android.app.Instrumentation; -import android.content.Context; +import android.content.ContextWrapper; import android.content.pm.ApplicationInfo; import android.os.Looper; import java.lang.reflect.Constructor; import java.lang.reflect.Field; -import java.lang.reflect.Method; public final class Workarounds { private Workarounds() { @@ -62,11 +60,10 @@ public final class Workarounds { mBoundApplicationField.setAccessible(true); mBoundApplicationField.set(activityThread, appBindData); - // Context ctx = activityThread.getSystemContext(); - Method getSystemContextMethod = activityThreadClass.getDeclaredMethod("getSystemContext"); - Context ctx = (Context) getSystemContextMethod.invoke(activityThread); - - Application app = Instrumentation.newApplication(Application.class, ctx); + Application app = Application.class.newInstance(); + Field baseField = ContextWrapper.class.getDeclaredField("mBase"); + baseField.setAccessible(true); + baseField.set(app, FakeContext.get()); // activityThread.mInitialApplication = app; Field mInitialApplicationField = activityThreadClass.getDeclaredField("mInitialApplication"); From 3cf03e4a4bea7b93766f6e5fb66d8e8878a6a906 Mon Sep 17 00:00:00 2001 From: Simon Chan <1330321+yume-chan@users.noreply.github.com> Date: Fri, 3 Feb 2023 16:27:34 +0100 Subject: [PATCH 1380/2244] Add --no-audio option Audio will be enabled by default (when supported). Add an option to disable it. PR #3757 Co-authored-by: Romain Vimont Signed-off-by: Romain Vimont --- app/data/bash-completion/scrcpy | 1 + app/data/zsh-completion/_scrcpy | 1 + app/src/cli.c | 9 +++++++++ app/src/options.c | 1 + app/src/options.h | 1 + app/src/scrcpy.c | 1 + app/src/server.c | 3 +++ app/src/server.h | 1 + server/src/main/java/com/genymobile/scrcpy/Options.java | 9 +++++++++ server/src/main/java/com/genymobile/scrcpy/Server.java | 4 ++++ 10 files changed, 31 insertions(+) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index 4590b6a8..22dc4cee 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -23,6 +23,7 @@ _scrcpy() { --max-fps= -M --hid-mouse -m --max-size= + --no-audio --no-cleanup --no-clipboard-autosync --no-downsize-on-error diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 961565e7..17e1de9f 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -29,6 +29,7 @@ arguments=( '--max-fps=[Limit the frame rate of screen capture]' {-M,--hid-mouse}'[Simulate a physical mouse by using HID over AOAv2]' {-m,--max-size=}'[Limit both the width and height of the video to value]' + '--no-audio[Disable audio forwarding]' '--no-cleanup[Disable device cleanup actions on exit]' '--no-clipboard-autosync[Disable automatic clipboard synchronization]' '--no-downsize-on-error[Disable lowering definition on MediaCodec error]' diff --git a/app/src/cli.c b/app/src/cli.c index ab460732..c1ee1e6d 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -59,6 +59,7 @@ enum { OPT_PRINT_FPS, OPT_NO_POWER_ON, OPT_CODEC, + OPT_NO_AUDIO, }; struct sc_option { @@ -267,6 +268,11 @@ static const struct sc_option options[] = { "is preserved.\n" "Default is 0 (unlimited).", }, + { + .longopt_id = OPT_NO_AUDIO, + .longopt = "no-audio", + .text = "Disable audio forwarding.", + }, { .longopt_id = OPT_NO_CLEANUP, .longopt = "no-cleanup", @@ -1630,6 +1636,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], case OPT_NO_DOWNSIZE_ON_ERROR: opts->downsize_on_error = false; break; + case OPT_NO_AUDIO: + opts->audio = false; + break; case OPT_NO_CLEANUP: opts->cleanup = false; break; diff --git a/app/src/options.c b/app/src/options.c index a75e584e..0854067f 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -67,4 +67,5 @@ const struct scrcpy_options scrcpy_options_default = { .cleanup = true, .start_fps_counter = false, .power_on = true, + .audio = true, }; diff --git a/app/src/options.h b/app/src/options.h index b9d237e0..7bf30011 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -147,6 +147,7 @@ struct scrcpy_options { bool cleanup; bool start_fps_counter; bool power_on; + bool audio; }; extern const struct scrcpy_options scrcpy_options_default; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 68665125..6d63a7a1 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -312,6 +312,7 @@ scrcpy(struct scrcpy_options *options) { .lock_video_orientation = options->lock_video_orientation, .control = options->control, .display_id = options->display_id, + .audio = options->audio, .show_touches = options->show_touches, .stay_awake = options->stay_awake, .codec_options = options->codec_options, diff --git a/app/src/server.c b/app/src/server.c index 413f02ee..0ff0d3c8 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -217,6 +217,9 @@ execute_server(struct sc_server *server, ADD_PARAM("log_level=%s", log_level_to_server_string(params->log_level)); ADD_PARAM("bit_rate=%" PRIu32, params->bit_rate); + if (!params->audio) { + ADD_PARAM("audio=false"); + } if (params->codec != SC_CODEC_H264) { ADD_PARAM("codec=%s", sc_server_get_codec_name(params->codec)); } diff --git a/app/src/server.h b/app/src/server.h index c05b1e5b..95e24b41 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -38,6 +38,7 @@ struct sc_server_params { int8_t lock_video_orientation; bool control; uint32_t display_id; + bool audio; bool show_touches; bool stay_awake; bool force_adb_forward; diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 5c59ec8e..07789974 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -8,6 +8,7 @@ public class Options { private Ln.Level logLevel = Ln.Level.DEBUG; private int scid = -1; // 31-bit non-negative value, or -1 + private boolean audio = true; private int maxSize; private VideoCodec codec = VideoCodec.H264; private int bitRate = 8000000; @@ -49,6 +50,14 @@ public class Options { this.scid = scid; } + public boolean getAudio() { + return audio; + } + + public void setAudio(boolean audio) { + this.audio = audio; + } + 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 d570a9fb..2aeeb79e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -169,6 +169,10 @@ public final class Server { Ln.Level level = Ln.Level.valueOf(value.toUpperCase(Locale.ENGLISH)); options.setLogLevel(level); break; + case "audio": + boolean audio = Boolean.parseBoolean(value); + options.setAudio(audio); + break; case "codec": VideoCodec codec = VideoCodec.findByName(value); if (codec == null) { From e841241a8ea135772a339687b9e7c45779c7864c Mon Sep 17 00:00:00 2001 From: Simon Chan <1330321+yume-chan@users.noreply.github.com> Date: Fri, 3 Feb 2023 16:50:42 +0100 Subject: [PATCH 1381/2244] Add a new socket for audio stream When audio is enabled, open a new socket to send the audio stream from the device to the client. PR #3757 Co-authored-by: Romain Vimont Signed-off-by: Romain Vimont --- app/src/server.c | 39 +++++++++++++++++++ app/src/server.h | 1 + .../genymobile/scrcpy/DesktopConnection.java | 30 ++++++++++++-- .../java/com/genymobile/scrcpy/Server.java | 3 +- 4 files changed, 69 insertions(+), 4 deletions(-) diff --git a/app/src/server.c b/app/src/server.c index 0ff0d3c8..88e3421f 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -391,6 +391,7 @@ sc_server_init(struct sc_server *server, const struct sc_server_params *params, server->stopped = false; server->video_socket = SC_SOCKET_NONE; + server->audio_socket = SC_SOCKET_NONE; server->control_socket = SC_SOCKET_NONE; sc_adb_tunnel_init(&server->tunnel); @@ -434,9 +435,11 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) { const char *serial = server->serial; assert(serial); + bool audio = server->params.audio; bool control = server->params.control; sc_socket video_socket = SC_SOCKET_NONE; + sc_socket audio_socket = SC_SOCKET_NONE; sc_socket control_socket = SC_SOCKET_NONE; if (!tunnel->forward) { video_socket = net_accept_intr(&server->intr, tunnel->server_socket); @@ -444,6 +447,14 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) { goto fail; } + if (audio) { + audio_socket = + net_accept_intr(&server->intr, tunnel->server_socket); + if (audio_socket == SC_SOCKET_NONE) { + goto fail; + } + } + if (control) { control_socket = net_accept_intr(&server->intr, tunnel->server_socket); @@ -470,6 +481,18 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) { goto fail; } + if (audio) { + audio_socket = net_socket(); + if (audio_socket == SC_SOCKET_NONE) { + goto fail; + } + bool ok = net_connect_intr(&server->intr, audio_socket, tunnel_host, + tunnel_port); + if (!ok) { + goto fail; + } + } + if (control) { // we know that the device is listening, we don't need several // attempts @@ -496,9 +519,11 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) { } assert(video_socket != SC_SOCKET_NONE); + assert(!audio || audio_socket != SC_SOCKET_NONE); assert(!control || control_socket != SC_SOCKET_NONE); server->video_socket = video_socket; + server->audio_socket = audio_socket; server->control_socket = control_socket; return true; @@ -510,6 +535,12 @@ fail: } } + if (audio_socket != SC_SOCKET_NONE) { + if (!net_close(audio_socket)) { + LOGW("Could not close audio socket"); + } + } + if (control_socket != SC_SOCKET_NONE) { if (!net_close(control_socket)) { LOGW("Could not close control socket"); @@ -860,6 +891,11 @@ run_server(void *data) { assert(server->video_socket != SC_SOCKET_NONE); net_interrupt(server->video_socket); + if (server->audio_socket != SC_SOCKET_NONE) { + // There is no audio_socket if --no-audio is set + net_interrupt(server->audio_socket); + } + if (server->control_socket != SC_SOCKET_NONE) { // There is no control_socket if --no-control is set net_interrupt(server->control_socket); @@ -924,6 +960,9 @@ sc_server_destroy(struct sc_server *server) { if (server->video_socket != SC_SOCKET_NONE) { net_close(server->video_socket); } + if (server->audio_socket != SC_SOCKET_NONE) { + net_close(server->audio_socket); + } if (server->control_socket != SC_SOCKET_NONE) { net_close(server->control_socket); } diff --git a/app/src/server.h b/app/src/server.h index 95e24b41..3005ebd2 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -70,6 +70,7 @@ struct sc_server { struct sc_adb_tunnel tunnel; sc_socket video_socket; + sc_socket audio_socket; sc_socket control_socket; const struct sc_server_callbacks *cbs; diff --git a/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java b/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java index 3cb36a09..3e743621 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java +++ b/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java @@ -20,6 +20,9 @@ public final class DesktopConnection implements Closeable { private final LocalSocket videoSocket; private final FileDescriptor videoFd; + private final LocalSocket audioSocket; + private final FileDescriptor audioFd; + private final LocalSocket controlSocket; private final InputStream controlInputStream; private final OutputStream controlOutputStream; @@ -27,9 +30,10 @@ public final class DesktopConnection implements Closeable { private final ControlMessageReader reader = new ControlMessageReader(); private final DeviceMessageWriter writer = new DeviceMessageWriter(); - private DesktopConnection(LocalSocket videoSocket, LocalSocket controlSocket) throws IOException { + private DesktopConnection(LocalSocket videoSocket, LocalSocket audioSocket, LocalSocket controlSocket) throws IOException { this.videoSocket = videoSocket; this.controlSocket = controlSocket; + this.audioSocket = audioSocket; if (controlSocket != null) { controlInputStream = controlSocket.getInputStream(); controlOutputStream = controlSocket.getOutputStream(); @@ -38,6 +42,7 @@ public final class DesktopConnection implements Closeable { controlOutputStream = null; } videoFd = videoSocket.getFileDescriptor(); + audioFd = audioSocket != null ? audioSocket.getFileDescriptor() : null; } private static LocalSocket connect(String abstractName) throws IOException { @@ -55,10 +60,11 @@ public final class DesktopConnection implements Closeable { return SOCKET_NAME_PREFIX + String.format("_%08x", scid); } - public static DesktopConnection open(int scid, boolean tunnelForward, boolean control, boolean sendDummyByte) throws IOException { + public static DesktopConnection open(int scid, boolean tunnelForward, boolean audio, boolean control, boolean sendDummyByte) throws IOException { String socketName = getSocketName(scid); LocalSocket videoSocket = null; + LocalSocket audioSocket = null; LocalSocket controlSocket = null; try { if (tunnelForward) { @@ -68,12 +74,18 @@ public final class DesktopConnection implements Closeable { // send one byte so the client may read() to detect a connection error videoSocket.getOutputStream().write(0); } + if (audio) { + audioSocket = localServerSocket.accept(); + } if (control) { controlSocket = localServerSocket.accept(); } } } else { videoSocket = connect(socketName); + if (audio) { + audioSocket = connect(socketName); + } if (control) { controlSocket = connect(socketName); } @@ -82,19 +94,27 @@ public final class DesktopConnection implements Closeable { if (videoSocket != null) { videoSocket.close(); } + if (audioSocket != null) { + audioSocket.close(); + } if (controlSocket != null) { controlSocket.close(); } throw e; } - return new DesktopConnection(videoSocket, controlSocket); + return new DesktopConnection(videoSocket, audioSocket, controlSocket); } public void close() throws IOException { videoSocket.shutdownInput(); videoSocket.shutdownOutput(); videoSocket.close(); + if (audioSocket != null) { + audioSocket.shutdownInput(); + audioSocket.shutdownOutput(); + audioSocket.close(); + } if (controlSocket != null) { controlSocket.shutdownInput(); controlSocket.shutdownOutput(); @@ -121,6 +141,10 @@ public final class DesktopConnection implements Closeable { return videoFd; } + public FileDescriptor getAudioFd() { + return audioFd; + } + public ControlMessage receiveControlMessage() throws IOException { ControlMessage msg = reader.next(); while (msg == null) { diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 2aeeb79e..55b38c6d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -68,6 +68,7 @@ public final class Server { int scid = options.getScid(); boolean tunnelForward = options.isTunnelForward(); boolean control = options.getControl(); + boolean audio = options.getAudio(); boolean sendDummyByte = options.getSendDummyByte(); Workarounds.prepareMainLooper(); @@ -85,7 +86,7 @@ public final class Server { Controller controller = null; - try (DesktopConnection connection = DesktopConnection.open(scid, tunnelForward, control, sendDummyByte)) { + try (DesktopConnection connection = DesktopConnection.open(scid, tunnelForward, audio, control, sendDummyByte)) { VideoCodec codec = options.getCodec(); if (options.getSendDeviceMeta()) { Size videoSize = device.getScreenInfo().getVideoSize(); From 11d32616a91f90ec49515a8f72f386bb9322766b Mon Sep 17 00:00:00 2001 From: Simon Chan <1330321+yume-chan@users.noreply.github.com> Date: Fri, 27 Jan 2023 20:13:37 +0800 Subject: [PATCH 1382/2244] Capture device audio Create an AudioRecorder to capture the audio source REMOTE_SUBMIX. For now, the captured packets are just logged into the console. PR #3757 Co-authored-by: Romain Vimont Signed-off-by: Romain Vimont --- .../com/genymobile/scrcpy/AudioEncoder.java | 80 +++++++++++++++++++ .../java/com/genymobile/scrcpy/Server.java | 38 ++++++--- 2 files changed, 109 insertions(+), 9 deletions(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java b/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java new file mode 100644 index 00000000..45e217fc --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java @@ -0,0 +1,80 @@ +package com.genymobile.scrcpy; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.media.AudioFormat; +import android.media.AudioRecord; +import android.media.MediaRecorder; +import android.os.Build; + +public final class AudioEncoder { + + private static final int SAMPLE_RATE = 48000; + private static final int CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_STEREO; + private static final int CHANNELS = 2; + private static final int FORMAT = AudioFormat.ENCODING_PCM_16BIT; + private static final int BYTES_PER_SAMPLE = 2; + + private static final int READ_MS = 5; // milliseconds + private static final int READ_SIZE = SAMPLE_RATE * CHANNELS * BYTES_PER_SAMPLE * READ_MS / 1000; + + private Thread thread; + + private static AudioFormat createAudioFormat() { + AudioFormat.Builder builder = new AudioFormat.Builder(); + builder.setEncoding(FORMAT); + builder.setSampleRate(SAMPLE_RATE); + builder.setChannelMask(CHANNEL_CONFIG); + return builder.build(); + } + + @TargetApi(Build.VERSION_CODES.M) + @SuppressLint({"WrongConstant", "MissingPermission"}) + private static AudioRecord createAudioRecord() { + AudioRecord.Builder builder = new AudioRecord.Builder(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + // On older APIs, Workarounds.fillAppInfo() must be called beforehand + builder.setContext(FakeContext.get()); + } + builder.setAudioSource(MediaRecorder.AudioSource.REMOTE_SUBMIX); + builder.setAudioFormat(createAudioFormat()); + int minBufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, FORMAT); + // This buffer size does not impact latency + builder.setBufferSizeInBytes(8 * minBufferSize); + return builder.build(); + } + + public void start() { + AudioRecord recorder = createAudioRecord(); + + thread = new Thread(() -> { + recorder.startRecording(); + try { + byte[] buf = new byte[READ_SIZE]; + while (!Thread.currentThread().isInterrupted()) { + int r = recorder.read(buf, 0, READ_SIZE); + if (r > 0) { + Ln.i("Audio captured: " + r + " bytes"); + } else { + Ln.e("Audio capture error: " + r); + } + } + } finally { + recorder.stop(); + } + }); + thread.start(); + } + + public void stop() { + if (thread != null) { + thread.interrupt(); + } + } + + public void join() throws InterruptedException { + if (thread != null) { + thread.join(); + } + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 55b38c6d..c32e4612 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -72,19 +72,28 @@ public final class Server { boolean sendDummyByte = options.getSendDummyByte(); Workarounds.prepareMainLooper(); - if (Build.BRAND.equalsIgnoreCase("meizu")) { - // Workarounds must be applied for Meizu phones: - // - - // - - // - - // - // But only apply when strictly necessary, since workarounds can cause other issues: - // - - // - + + // Workarounds must be applied for Meizu phones: + // - + // - + // - + // + // But only apply when strictly necessary, since workarounds can cause other issues: + // - + // - + boolean mustFillAppInfo = Build.BRAND.equalsIgnoreCase("meizu"); + + // Before Android 11, audio is not supported. + // Since Android 12, we can properly set a context on the AudioRecord. + // Only on Android 11 we must fill app info for the AudioRecord to work. + mustFillAppInfo |= audio && Build.VERSION.SDK_INT == Build.VERSION_CODES.R; + + if (mustFillAppInfo) { Workarounds.fillAppInfo(); } Controller controller = null; + AudioEncoder audioEncoder = null; try (DesktopConnection connection = DesktopConnection.open(scid, tunnelForward, audio, control, sendDummyByte)) { VideoCodec codec = options.getCodec(); @@ -101,6 +110,11 @@ public final class Server { device.setClipboardListener(text -> controllerRef.getSender().pushClipboardText(text)); } + if (audio) { + audioEncoder = new AudioEncoder(); + audioEncoder.start(); + } + Streamer videoStreamer = new Streamer(connection.getVideoFd(), codec, options.getSendCodecId(), options.getSendFrameMeta()); ScreenEncoder screenEncoder = new ScreenEncoder(device, videoStreamer, options.getBitRate(), options.getMaxFps(), codecOptions, options.getEncoderName(), options.getDownsizeOnError()); @@ -116,12 +130,18 @@ public final class Server { } finally { Ln.d("Screen streaming stopped"); initThread.interrupt(); + if (audioEncoder != null) { + audioEncoder.stop(); + } if (controller != null) { controller.stop(); } try { initThread.join(); + if (audioEncoder != null) { + audioEncoder.join(); + } if (controller != null) { controller.join(); } From 464a35b05e7896ce5173a1fa3e27f00e0899f19e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 3 Mar 2023 14:07:32 +0100 Subject: [PATCH 1383/2244] Make streamer more generic Expose a method to write a packet from raw metadata (without BufferInfo). --- .../java/com/genymobile/scrcpy/Streamer.java | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Streamer.java b/server/src/main/java/com/genymobile/scrcpy/Streamer.java index d6bf6780..ae437104 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Streamer.java +++ b/server/src/main/java/com/genymobile/scrcpy/Streamer.java @@ -38,23 +38,30 @@ public final class Streamer { } } - public void writePacket(ByteBuffer codecBuffer, MediaCodec.BufferInfo bufferInfo) throws IOException { + public void writePacket(ByteBuffer buffer, long pts, boolean config, boolean keyFrame) throws IOException { if (sendFrameMeta) { - writeFrameMeta(fd, bufferInfo, codecBuffer.remaining()); + writeFrameMeta(fd, buffer.remaining(), pts, config, keyFrame); } - IO.writeFully(fd, codecBuffer); + IO.writeFully(fd, buffer); } - private void writeFrameMeta(FileDescriptor fd, MediaCodec.BufferInfo bufferInfo, int packetSize) throws IOException { + public void writePacket(ByteBuffer codecBuffer, MediaCodec.BufferInfo bufferInfo) throws IOException { + long pts = bufferInfo.presentationTimeUs; + boolean config = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0; + boolean keyFrame = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0; + writePacket(codecBuffer, pts, config, keyFrame); + } + + private void writeFrameMeta(FileDescriptor fd, int packetSize, long pts, boolean config, boolean keyFrame) throws IOException { headerBuffer.clear(); long ptsAndFlags; - if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) { + if (config) { ptsAndFlags = PACKET_FLAG_CONFIG; // non-media data packet } else { - ptsAndFlags = bufferInfo.presentationTimeUs; - if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0) { + ptsAndFlags = pts; + if (keyFrame) { ptsAndFlags |= PACKET_FLAG_KEY_FRAME; } } From 5eed2c52c2f38192544d909ee639a3f02720fe56 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 7 Feb 2023 23:08:57 +0100 Subject: [PATCH 1384/2244] Encode recorded audio on the device For now, the encoded packets are just logged into the console. PR #3757 --- .../com/genymobile/scrcpy/AudioEncoder.java | 276 +++++++++++++++++- 1 file changed, 261 insertions(+), 15 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java b/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java index 45e217fc..7b571126 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java @@ -4,21 +4,63 @@ import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.media.AudioFormat; import android.media.AudioRecord; +import android.media.AudioTimestamp; +import android.media.MediaCodec; +import android.media.MediaFormat; import android.media.MediaRecorder; import android.os.Build; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; public final class AudioEncoder { + private static class InputTask { + private final int index; + + InputTask(int index) { + this.index = index; + } + } + + private static class OutputTask { + private final int index; + private final MediaCodec.BufferInfo bufferInfo; + + OutputTask(int index, MediaCodec.BufferInfo bufferInfo) { + this.index = index; + this.bufferInfo = bufferInfo; + } + } + + private static final String MIMETYPE = MediaFormat.MIMETYPE_AUDIO_OPUS; private static final int SAMPLE_RATE = 48000; private static final int CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_STEREO; private static final int CHANNELS = 2; private static final int FORMAT = AudioFormat.ENCODING_PCM_16BIT; private static final int BYTES_PER_SAMPLE = 2; + private static final int BIT_RATE = 128000; private static final int READ_MS = 5; // milliseconds private static final int READ_SIZE = SAMPLE_RATE * CHANNELS * BYTES_PER_SAMPLE * READ_MS / 1000; + // Capacity of 64 is in practice "infinite" (it is limited by the number of available MediaCodec buffers, typically 4). + // So many pending tasks would lead to an unacceptable delay anyway. + private final BlockingQueue inputTasks = new ArrayBlockingQueue<>(64); + private final BlockingQueue outputTasks = new ArrayBlockingQueue<>(64); + private Thread thread; + private HandlerThread mediaCodecThread; + + private Thread inputThread; + private Thread outputThread; + + private boolean ended; private static AudioFormat createAudioFormat() { AudioFormat.Builder builder = new AudioFormat.Builder(); @@ -44,23 +86,80 @@ public final class AudioEncoder { return builder.build(); } - public void start() { - AudioRecord recorder = createAudioRecord(); + private static MediaFormat createFormat() { + MediaFormat format = new MediaFormat(); + format.setString(MediaFormat.KEY_MIME, MIMETYPE); + format.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE); + format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, CHANNELS); + format.setInteger(MediaFormat.KEY_SAMPLE_RATE, SAMPLE_RATE); + return format; + } - thread = new Thread(() -> { - recorder.startRecording(); - try { - byte[] buf = new byte[READ_SIZE]; - while (!Thread.currentThread().isInterrupted()) { - int r = recorder.read(buf, 0, READ_SIZE); - if (r > 0) { - Ln.i("Audio captured: " + r + " bytes"); - } else { - Ln.e("Audio capture error: " + r); - } + @TargetApi(Build.VERSION_CODES.N) + private void inputThread(MediaCodec mediaCodec, AudioRecord recorder) throws IOException, InterruptedException { + final AudioTimestamp timestamp = new AudioTimestamp(); + long previousPts = 0; + long nextPts = 0; + + while (!Thread.currentThread().isInterrupted()) { + InputTask task = inputTasks.take(); + ByteBuffer buffer = mediaCodec.getInputBuffer(task.index); + int r = recorder.read(buffer, READ_SIZE); + if (r < 0) { + throw new IOException("Could not read audio: " + r); + } + + long pts; + + int ret = recorder.getTimestamp(timestamp, AudioTimestamp.TIMEBASE_MONOTONIC); + if (ret == AudioRecord.SUCCESS) { + pts = timestamp.nanoTime / 1000; + } else { + if (nextPts == 0) { + Ln.w("Could not get any audio timestamp"); } + // compute from previous timestamp and packet size + pts = nextPts; + } + + long durationUs = r * 1000000 / (CHANNELS * BYTES_PER_SAMPLE * SAMPLE_RATE); + nextPts = pts + durationUs; + + if (previousPts != 0 && pts < previousPts) { + // Audio PTS may come from two sources: + // - recorder.getTimestamp() if the call works; + // - an estimation from the previous PTS and the packet size as a fallback. + // + // Therefore, the property that PTS are monotonically increasing is no guaranteed in corner cases, so enforce it. + pts = previousPts + 1; + } + + previousPts = pts; + + mediaCodec.queueInputBuffer(task.index, 0, r, pts, 0); + } + } + + private void outputThread(MediaCodec mediaCodec) throws IOException, InterruptedException { + while (!Thread.currentThread().isInterrupted()) { + OutputTask task = outputTasks.take(); + ByteBuffer buffer = mediaCodec.getOutputBuffer(task.index); + try { + Ln.i("Audio packet [pts=" + task.bufferInfo.presentationTimeUs + "] " + buffer.remaining() + " bytes"); } finally { - recorder.stop(); + mediaCodec.releaseOutputBuffer(task.index, false); + } + } + } + + public void start() { + thread = new Thread(() -> { + try { + encode(); + } catch (IOException e) { + Ln.e("Audio encoding error", e); + } finally { + Ln.d("Audio encoder stopped"); } }); thread.start(); @@ -68,7 +167,8 @@ public final class AudioEncoder { public void stop() { if (thread != null) { - thread.interrupt(); + // Just wake up the blocking wait from the thread, so that it properly releases all its resources and terminates + end(); } } @@ -77,4 +177,150 @@ public final class AudioEncoder { thread.join(); } } + + private synchronized void end() { + ended = true; + notify(); + } + + private synchronized void waitEnded() { + try { + while (!ended) { + wait(); + } + } catch (InterruptedException e) { + // ignore + } + } + + @TargetApi(Build.VERSION_CODES.M) + public void encode() throws IOException { + MediaCodec mediaCodec = null; + AudioRecord recorder = null; + + boolean mediaCodecStarted = false; + boolean recorderStarted = false; + try { + mediaCodec = MediaCodec.createEncoderByType(MIMETYPE); // may throw IOException + + mediaCodecThread = new HandlerThread("AudioEncoder"); + mediaCodecThread.start(); + + MediaFormat format = createFormat(); + mediaCodec.setCallback(new EncoderCallback(), new Handler(mediaCodecThread.getLooper())); + mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); + + recorder = createAudioRecord(); + recorder.startRecording(); + recorderStarted = true; + + final MediaCodec mediaCodecRef = mediaCodec; + final AudioRecord recorderRef = recorder; + inputThread = new Thread(() -> { + try { + inputThread(mediaCodecRef, recorderRef); + } catch (IOException | InterruptedException e) { + Ln.e("Audio capture error", e); + } finally { + end(); + } + }); + + outputThread = new Thread(() -> { + try { + outputThread(mediaCodecRef); + } catch (InterruptedException e) { + // this is expected on close + } catch (IOException e) { + // Broken pipe is expected on close, because the socket is closed by the client + if (!IO.isBrokenPipe(e)) { + Ln.e("Audio encoding error", e); + } + } finally { + end(); + } + }); + + mediaCodec.start(); + mediaCodecStarted = true; + inputThread.start(); + outputThread.start(); + + waitEnded(); + } finally { + // Cleanup everything (either at the end or on error at any step of the initialization) + if (mediaCodecThread != null) { + Looper looper = mediaCodecThread.getLooper(); + if (looper != null) { + looper.quitSafely(); + } + } + if (inputThread != null) { + inputThread.interrupt(); + } + if (outputThread != null) { + outputThread.interrupt(); + } + + try { + if (mediaCodecThread != null) { + mediaCodecThread.join(); + } + if (inputThread != null) { + inputThread.join(); + } + if (outputThread != null) { + outputThread.join(); + } + } catch (InterruptedException e) { + // Should never happen + throw new AssertionError(e); + } + + if (mediaCodec != null) { + if (mediaCodecStarted) { + mediaCodec.stop(); + } + mediaCodec.release(); + } + if (recorder != null) { + if (recorderStarted) { + recorder.stop(); + } + recorder.release(); + } + } + } + + private class EncoderCallback extends MediaCodec.Callback { + @TargetApi(Build.VERSION_CODES.N) + @Override + public void onInputBufferAvailable(MediaCodec codec, int index) { + try { + inputTasks.put(new InputTask(index)); + } catch (InterruptedException e) { + end(); + } + } + + @Override + public void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo bufferInfo) { + try { + outputTasks.put(new OutputTask(index, bufferInfo)); + } catch (InterruptedException e) { + end(); + } + } + + @Override + public void onError(MediaCodec codec, MediaCodec.CodecException e) { + Ln.e("MediaCodec error", e); + end(); + } + + @Override + public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) { + // ignore + } + } } From 7cf5cf5875b217a5d2e141214d1dc344bdde4e73 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 9 Feb 2023 21:37:16 +0100 Subject: [PATCH 1385/2244] Use a streamer to send the audio stream Send each encoded audio packet using a streamer. PR #3757 --- .../com/genymobile/scrcpy/AudioCodec.java | 46 +++++++++++++++++++ .../com/genymobile/scrcpy/AudioEncoder.java | 10 +++- .../java/com/genymobile/scrcpy/Codec.java | 1 + .../java/com/genymobile/scrcpy/Server.java | 3 +- 4 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/AudioCodec.java diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioCodec.java b/server/src/main/java/com/genymobile/scrcpy/AudioCodec.java new file mode 100644 index 00000000..4d9e3201 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/AudioCodec.java @@ -0,0 +1,46 @@ +package com.genymobile.scrcpy; + +import android.media.MediaFormat; + +public enum AudioCodec implements Codec { + OPUS(0x6f_70_75_73, "opus", MediaFormat.MIMETYPE_AUDIO_OPUS); + + private final int id; // 4-byte ASCII representation of the name + private final String name; + private final String mimeType; + + AudioCodec(int id, String name, String mimeType) { + this.id = id; + this.name = name; + this.mimeType = mimeType; + } + + @Override + public Type getType() { + return Type.AUDIO; + } + + @Override + public int getId() { + return id; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getMimeType() { + return mimeType; + } + + public static AudioCodec findByName(String name) { + for (AudioCodec codec : values()) { + if (codec.name.equals(name)) { + return codec; + } + } + return null; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java b/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java index 7b571126..74481f99 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java @@ -49,6 +49,8 @@ public final class AudioEncoder { private static final int READ_MS = 5; // milliseconds private static final int READ_SIZE = SAMPLE_RATE * CHANNELS * BYTES_PER_SAMPLE * READ_MS / 1000; + private final Streamer streamer; + // Capacity of 64 is in practice "infinite" (it is limited by the number of available MediaCodec buffers, typically 4). // So many pending tasks would lead to an unacceptable delay anyway. private final BlockingQueue inputTasks = new ArrayBlockingQueue<>(64); @@ -62,6 +64,10 @@ public final class AudioEncoder { private boolean ended; + public AudioEncoder(Streamer streamer) { + this.streamer = streamer; + } + private static AudioFormat createAudioFormat() { AudioFormat.Builder builder = new AudioFormat.Builder(); builder.setEncoding(FORMAT); @@ -141,11 +147,13 @@ public final class AudioEncoder { } private void outputThread(MediaCodec mediaCodec) throws IOException, InterruptedException { + streamer.writeHeader(); + while (!Thread.currentThread().isInterrupted()) { OutputTask task = outputTasks.take(); ByteBuffer buffer = mediaCodec.getOutputBuffer(task.index); try { - Ln.i("Audio packet [pts=" + task.bufferInfo.presentationTimeUs + "] " + buffer.remaining() + " bytes"); + streamer.writePacket(buffer, task.bufferInfo); } finally { mediaCodec.releaseOutputBuffer(task.index, false); } diff --git a/server/src/main/java/com/genymobile/scrcpy/Codec.java b/server/src/main/java/com/genymobile/scrcpy/Codec.java index 50e8acad..7e905af3 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Codec.java +++ b/server/src/main/java/com/genymobile/scrcpy/Codec.java @@ -4,6 +4,7 @@ public interface Codec { enum Type { VIDEO, + AUDIO, } Type getType(); diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index c32e4612..eb0c1384 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -111,7 +111,8 @@ public final class Server { } if (audio) { - audioEncoder = new AudioEncoder(); + Streamer audioStreamer = new Streamer(connection.getAudioFd(), AudioCodec.OPUS, options.getSendCodecId(), options.getSendFrameMeta()); + audioEncoder = new AudioEncoder(audioStreamer); audioEncoder.start(); } From 15556d1f3be9aa0cf98c43205b516e622e9c7856 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 18 Feb 2023 17:35:54 +0100 Subject: [PATCH 1386/2244] Extract OPUS extradata For OPUS codec, FFmpeg expects the raw extradata, but MediaCodec wraps it in some structure. Fix the config packet to send only the raw extradata. PR #3757 --- .../java/com/genymobile/scrcpy/Streamer.java | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/server/src/main/java/com/genymobile/scrcpy/Streamer.java b/server/src/main/java/com/genymobile/scrcpy/Streamer.java index ae437104..77d9eefa 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Streamer.java +++ b/server/src/main/java/com/genymobile/scrcpy/Streamer.java @@ -11,6 +11,8 @@ public final class Streamer { private static final long PACKET_FLAG_CONFIG = 1L << 63; private static final long PACKET_FLAG_KEY_FRAME = 1L << 62; + private static final long AOPUSHDR = 0x5244485355504F41L; // "AOPUSHDR" in ASCII (little-endian) + private final FileDescriptor fd; private final Codec codec; private final boolean sendCodecId; @@ -39,6 +41,10 @@ public final class Streamer { } public void writePacket(ByteBuffer buffer, long pts, boolean config, boolean keyFrame) throws IOException { + if (config && codec == AudioCodec.OPUS) { + fixOpusConfigPacket(buffer); + } + if (sendFrameMeta) { writeFrameMeta(fd, buffer.remaining(), pts, config, keyFrame); } @@ -71,4 +77,44 @@ public final class Streamer { headerBuffer.flip(); IO.writeFully(fd, headerBuffer); } + + private static void fixOpusConfigPacket(ByteBuffer buffer) throws IOException { + // Here is an example of the config packet received for an OPUS stream: + // + // 00000000 41 4f 50 55 53 48 44 52 13 00 00 00 00 00 00 00 |AOPUSHDR........| + // -------------- BELOW IS THE PART WE MUST PUT AS EXTRADATA ------------------- + // 00000010 4f 70 75 73 48 65 61 64 01 01 38 01 80 bb 00 00 |OpusHead..8.....| + // 00000020 00 00 00 |... | + // ------------------------------------------------------------------------------ + // 00000020 41 4f 50 55 53 44 4c 59 08 00 00 00 00 | AOPUSDLY.....| + // 00000030 00 00 00 a0 2e 63 00 00 00 00 00 41 4f 50 55 53 |.....c.....AOPUS| + // 00000040 50 52 4c 08 00 00 00 00 00 00 00 00 b4 c4 04 00 |PRL.............| + // 00000050 00 00 00 |...| + // + // Each "section" is prefixed by a 64-bit ID and a 64-bit length. + // + // + + if (buffer.remaining() < 16) { + throw new IOException("Not enough data in OPUS config packet"); + } + + long id = buffer.getLong(); + if (id != AOPUSHDR) { + throw new IOException("OPUS header not found"); + } + + long sizeLong = buffer.getLong(); + if (sizeLong < 0 || sizeLong >= 0x7FFFFFFF) { + throw new IOException("Invalid block size in OPUS header: " + sizeLong); + } + + int size = (int) sizeLong; + if (buffer.remaining() < size) { + throw new IOException("Not enough data in OPUS header (invalid size: " + size + ")"); + } + + // Set the buffer to point to the OPUS header slice + buffer.limit(buffer.position() + size); + } } From e9876788c9901d43d75031b090c81c0e604ed994 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 6 Feb 2023 10:08:01 +0100 Subject: [PATCH 1387/2244] Rename demuxer to video_demuxer There will be another demuxer instance for audio. PR #3757 --- app/src/scrcpy.c | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 6d63a7a1..e4e1355c 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -40,7 +40,7 @@ struct scrcpy { struct sc_server server; struct sc_screen screen; - struct sc_demuxer demuxer; + struct sc_demuxer video_demuxer; struct sc_decoder decoder; struct sc_recorder recorder; #ifdef HAVE_V4L2 @@ -213,7 +213,8 @@ sc_recorder_on_ended(struct sc_recorder *recorder, bool success, } static void -sc_demuxer_on_ended(struct sc_demuxer *demuxer, bool eos, void *userdata) { +sc_video_demuxer_on_ended(struct sc_demuxer *demuxer, bool eos, + void *userdata) { (void) demuxer; (void) userdata; @@ -281,7 +282,7 @@ scrcpy(struct scrcpy_options *options) { #ifdef HAVE_V4L2 bool v4l2_sink_initialized = false; #endif - bool demuxer_started = false; + bool video_demuxer_started = false; #ifdef HAVE_USB bool aoa_hid_initialized = false; bool hid_keyboard_initialized = false; @@ -383,10 +384,11 @@ scrcpy(struct scrcpy_options *options) { file_pusher_initialized = true; } - static const struct sc_demuxer_callbacks demuxer_cbs = { - .on_ended = sc_demuxer_on_ended, + static const struct sc_demuxer_callbacks video_demuxer_cbs = { + .on_ended = sc_video_demuxer_on_ended, }; - sc_demuxer_init(&s->demuxer, s->server.video_socket, &demuxer_cbs, NULL); + sc_demuxer_init(&s->video_demuxer, s->server.video_socket, + &video_demuxer_cbs, NULL); bool needs_decoder = options->display; #ifdef HAVE_V4L2 @@ -394,7 +396,7 @@ scrcpy(struct scrcpy_options *options) { #endif if (needs_decoder) { sc_decoder_init(&s->decoder); - sc_demuxer_add_sink(&s->demuxer, &s->decoder.packet_sink); + sc_demuxer_add_sink(&s->video_demuxer, &s->decoder.packet_sink); } if (options->record_filename) { @@ -413,7 +415,7 @@ scrcpy(struct scrcpy_options *options) { } recorder_started = true; - sc_demuxer_add_sink(&s->demuxer, &s->recorder.packet_sink); + sc_demuxer_add_sink(&s->video_demuxer, &s->recorder.packet_sink); } struct sc_controller *controller = NULL; @@ -621,17 +623,17 @@ aoa_hid_end: #endif // now we consumed the header values, the socket receives the video stream - // start the demuxer - if (!sc_demuxer_start(&s->demuxer)) { + // start the video demuxer + if (!sc_demuxer_start(&s->video_demuxer)) { goto end; } - demuxer_started = true; + video_demuxer_started = true; ret = event_loop(s); LOGD("quit..."); // Close the window immediately on closing, because screen_destroy() may - // only be called once the demuxer thread is joined (it may take time) + // only be called once the video demuxer thread is joined (it may take time) sc_screen_hide_window(&s->screen); end: @@ -672,8 +674,8 @@ end: // now that the sockets are shutdown, the demuxer and controller are // interrupted, we can join them - if (demuxer_started) { - sc_demuxer_join(&s->demuxer); + if (video_demuxer_started) { + sc_demuxer_join(&s->video_demuxer); } #ifdef HAVE_V4L2 @@ -692,8 +694,9 @@ end: } #endif - // Destroy the screen only after the demuxer is guaranteed to be finished, - // because otherwise the screen could receive new frames after destruction + // Destroy the screen only after the video demuxer is guaranteed to be + // finished, because otherwise the screen could receive new frames after + // destruction if (screen_initialized) { sc_screen_join(&s->screen); sc_screen_destroy(&s->screen); From d499f890e7fe019ac3c60bec7ceb61d39aaca97b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 19 Feb 2023 00:13:54 +0100 Subject: [PATCH 1388/2244] Give a name to demuxer instances This will be useful in logs. PR #3757 --- app/src/demuxer.c | 13 +++++++------ app/src/demuxer.h | 5 ++++- app/src/scrcpy.c | 2 +- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/app/src/demuxer.c b/app/src/demuxer.c index c83d6bfa..e5f07628 100644 --- a/app/src/demuxer.c +++ b/app/src/demuxer.c @@ -122,7 +122,7 @@ static bool sc_demuxer_push_packet(struct sc_demuxer *demuxer, AVPacket *packet) { bool ok = push_packet_to_sinks(demuxer, packet); if (!ok) { - LOGE("Could not process packet"); + LOGE("Demuxer '%s': could not process packet", demuxer->name); return false; } @@ -177,7 +177,7 @@ run_demuxer(void *data) { const AVCodec *codec = avcodec_find_decoder(codec_id); if (!codec) { - LOGE("Decoder not found"); + LOGE("Demuxer '%s': decoder not found", demuxer->name); goto end; } @@ -217,7 +217,7 @@ run_demuxer(void *data) { } } - LOGD("End of frames"); + LOGD("Demuxer '%s': end of frames", demuxer->name); sc_packet_merger_destroy(&merger); @@ -231,8 +231,9 @@ end: } void -sc_demuxer_init(struct sc_demuxer *demuxer, sc_socket socket, +sc_demuxer_init(struct sc_demuxer *demuxer, const char *name, sc_socket socket, const struct sc_demuxer_callbacks *cbs, void *cbs_userdata) { + demuxer->name = name; // statically allocated demuxer->socket = socket; demuxer->sink_count = 0; @@ -252,12 +253,12 @@ sc_demuxer_add_sink(struct sc_demuxer *demuxer, struct sc_packet_sink *sink) { bool sc_demuxer_start(struct sc_demuxer *demuxer) { - LOGD("Starting demuxer thread"); + LOGD("Demuxer '%s': starting thread", demuxer->name); bool ok = sc_thread_create(&demuxer->thread, run_demuxer, "scrcpy-demuxer", demuxer); if (!ok) { - LOGE("Could not start demuxer thread"); + LOGE("Demuxer '%s': could not start thread", demuxer->name); return false; } return true; diff --git a/app/src/demuxer.h b/app/src/demuxer.h index e403fe35..73166b41 100644 --- a/app/src/demuxer.h +++ b/app/src/demuxer.h @@ -15,6 +15,8 @@ #define SC_DEMUXER_MAX_SINKS 2 struct sc_demuxer { + const char *name; // must be statically allocated (e.g. a string literal) + sc_socket socket; sc_thread thread; @@ -29,8 +31,9 @@ struct sc_demuxer_callbacks { void (*on_ended)(struct sc_demuxer *demuxer, bool eos, void *userdata); }; +// The name must be statically allocated (e.g. a string literal) void -sc_demuxer_init(struct sc_demuxer *demuxer, sc_socket socket, +sc_demuxer_init(struct sc_demuxer *demuxer, const char *name, sc_socket socket, const struct sc_demuxer_callbacks *cbs, void *cbs_userdata); void diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index e4e1355c..820f3328 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -387,7 +387,7 @@ scrcpy(struct scrcpy_options *options) { static const struct sc_demuxer_callbacks video_demuxer_cbs = { .on_ended = sc_video_demuxer_on_ended, }; - sc_demuxer_init(&s->video_demuxer, s->server.video_socket, + sc_demuxer_init(&s->video_demuxer, "video", s->server.video_socket, &video_demuxer_cbs, NULL); bool needs_decoder = options->display; From f60b5767f4aec15d07e8053e25d5a68961618411 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 2 Mar 2023 13:49:53 +0100 Subject: [PATCH 1389/2244] Force --no-audio if no display and no recording The client does not use the audio stream if there is no display and no recording (i.e. only V4L2), so disable audio so that the device does not attempt to capture it. PR #3757 --- app/src/cli.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/cli.c b/app/src/cli.c index c1ee1e6d..4f023da0 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -1740,6 +1740,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } #endif + if (opts->audio && !opts->display && !opts->record_filename) { + LOGI("No display and no recording: audio disabled"); + opts->audio = false; + } + if ((opts->tunnel_host || opts->tunnel_port) && !opts->force_adb_forward) { LOGI("Tunnel host/port is set, " "--force-adb-forward automatically enabled."); From de430bc4aa3b63e8a602032a42613d9672f7c181 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 6 Feb 2023 10:33:47 +0100 Subject: [PATCH 1390/2244] Add an audio demuxer Add a demuxer which will read the stream from the audio socket. PR #3757 --- app/src/demuxer.c | 5 +++++ app/src/scrcpy.c | 31 +++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/app/src/demuxer.c b/app/src/demuxer.c index e5f07628..c5898060 100644 --- a/app/src/demuxer.c +++ b/app/src/demuxer.c @@ -23,6 +23,7 @@ sc_demuxer_to_avcodec_id(uint32_t codec_id) { #define SC_CODEC_ID_H264 UINT32_C(0x68323634) // "h264" in ASCII #define SC_CODEC_ID_H265 UINT32_C(0x68323635) // "h265" in ASCII #define SC_CODEC_ID_AV1 UINT32_C(0x00617631) // "av1" in ASCII +#define SC_CODEC_ID_OPUS UINT32_C(0x6f707573) // "opus" in ASCII switch (codec_id) { case SC_CODEC_ID_H264: return AV_CODEC_ID_H264; @@ -30,6 +31,8 @@ sc_demuxer_to_avcodec_id(uint32_t codec_id) { return AV_CODEC_ID_HEVC; case SC_CODEC_ID_AV1: return AV_CODEC_ID_AV1; + case SC_CODEC_ID_OPUS: + return AV_CODEC_ID_OPUS; default: LOGE("Unknown codec id 0x%08" PRIx32, codec_id); return AV_CODEC_ID_NONE; @@ -233,6 +236,8 @@ end: void sc_demuxer_init(struct sc_demuxer *demuxer, const char *name, sc_socket socket, const struct sc_demuxer_callbacks *cbs, void *cbs_userdata) { + assert(socket != SC_SOCKET_NONE); + demuxer->name = name; // statically allocated demuxer->socket = socket; demuxer->sink_count = 0; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 820f3328..0c846123 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -41,6 +41,7 @@ struct scrcpy { struct sc_server server; struct sc_screen screen; struct sc_demuxer video_demuxer; + struct sc_demuxer audio_demuxer; struct sc_decoder decoder; struct sc_recorder recorder; #ifdef HAVE_V4L2 @@ -225,6 +226,16 @@ sc_video_demuxer_on_ended(struct sc_demuxer *demuxer, bool eos, } } +static void +sc_audio_demuxer_on_ended(struct sc_demuxer *demuxer, bool eos, + void *userdata) { + (void) demuxer; + (void) eos; + (void) userdata; + + // Contrary to the video demuxer, keep mirroring if only the audio fails +} + static void sc_server_on_connection_failed(struct sc_server *server, void *userdata) { (void) server; @@ -283,6 +294,7 @@ scrcpy(struct scrcpy_options *options) { bool v4l2_sink_initialized = false; #endif bool video_demuxer_started = false; + bool audio_demuxer_started = false; #ifdef HAVE_USB bool aoa_hid_initialized = false; bool hid_keyboard_initialized = false; @@ -390,6 +402,14 @@ scrcpy(struct scrcpy_options *options) { sc_demuxer_init(&s->video_demuxer, "video", s->server.video_socket, &video_demuxer_cbs, NULL); + if (options->audio) { + static const struct sc_demuxer_callbacks audio_demuxer_cbs = { + .on_ended = sc_audio_demuxer_on_ended, + }; + sc_demuxer_init(&s->audio_demuxer, "audio", s->server.audio_socket, + &audio_demuxer_cbs, NULL); + } + bool needs_decoder = options->display; #ifdef HAVE_V4L2 needs_decoder |= !!options->v4l2_device; @@ -629,6 +649,13 @@ aoa_hid_end: } video_demuxer_started = true; + if (options->audio) { + if (!sc_demuxer_start(&s->audio_demuxer)) { + goto end; + } + audio_demuxer_started = true; + } + ret = event_loop(s); LOGD("quit..."); @@ -678,6 +705,10 @@ end: sc_demuxer_join(&s->video_demuxer); } + if (audio_demuxer_started) { + sc_demuxer_join(&s->audio_demuxer); + } + #ifdef HAVE_V4L2 if (v4l2_sink_initialized) { sc_v4l2_sink_destroy(&s->v4l2_sink); From 609b098a97322b73de8ec6cd375926ac1f6f8307 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 18 Feb 2023 17:37:58 +0100 Subject: [PATCH 1391/2244] Do not merge config audio packets For video streams (at least H.264 and H.265), the config packet containing SPS/PPS must be prepended to the next packet (the following keyframe). For audio streams (at least OPUS), they must not be merged. PR #3757 --- app/src/demuxer.c | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/app/src/demuxer.c b/app/src/demuxer.c index c5898060..968e6db0 100644 --- a/app/src/demuxer.c +++ b/app/src/demuxer.c @@ -188,8 +188,15 @@ run_demuxer(void *data) { goto end; } + // Config packets must be merged with the next non-config packet only for + // video streams + bool must_merge_config_packet = codec->type == AVMEDIA_TYPE_VIDEO; + struct sc_packet_merger merger; - sc_packet_merger_init(&merger); + + if (must_merge_config_packet) { + sc_packet_merger_init(&merger); + } AVPacket *packet = av_packet_alloc(); if (!packet) { @@ -205,11 +212,13 @@ run_demuxer(void *data) { break; } - // Prepend any config packet to the next media packet - ok = sc_packet_merger_merge(&merger, packet); - if (!ok) { - av_packet_unref(packet); - break; + if (must_merge_config_packet) { + // Prepend any config packet to the next media packet + ok = sc_packet_merger_merge(&merger, packet); + if (!ok) { + av_packet_unref(packet); + break; + } } ok = sc_demuxer_push_packet(demuxer, packet); @@ -222,7 +231,9 @@ run_demuxer(void *data) { LOGD("Demuxer '%s': end of frames", demuxer->name); - sc_packet_merger_destroy(&merger); + if (must_merge_config_packet) { + sc_packet_merger_destroy(&merger); + } av_packet_free(&packet); finally_close_sinks: From 3d29f6ef0684aa9ef7a47690aa82326b6f830293 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 15 Feb 2023 10:06:10 +0100 Subject: [PATCH 1392/2244] Rename video-specific variables in recorder This paves the way to add audio-specific variables. PR #3757 --- app/src/recorder.c | 58 ++++++++++++++++++++++++---------------------- app/src/recorder.h | 8 +++---- app/src/scrcpy.c | 2 +- 3 files changed, 35 insertions(+), 33 deletions(-) diff --git a/app/src/recorder.c b/app/src/recorder.c index 7cc69778..86817681 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -8,8 +8,9 @@ #include "util/log.h" #include "util/str.h" -/** Downcast packet_sink to recorder */ -#define DOWNCAST(SINK) container_of(SINK, struct sc_recorder, packet_sink) +/** Downcast video packet_sink to recorder */ +#define DOWNCAST_VIDEO(SINK) \ + container_of(SINK, struct sc_recorder, video_packet_sink) static const AVRational SCRCPY_TIME_BASE = {1, 1000000}; // timestamps in us @@ -154,10 +155,10 @@ sc_recorder_close_output_file(struct sc_recorder *recorder) { static bool sc_recorder_wait_video_stream(struct sc_recorder *recorder) { sc_mutex_lock(&recorder->mutex); - while (!recorder->codec && !recorder->stopped) { + while (!recorder->video_codec && !recorder->stopped) { sc_cond_wait(&recorder->stream_cond, &recorder->mutex); } - const AVCodec *codec = recorder->codec; + const AVCodec *codec = recorder->video_codec; sc_mutex_unlock(&recorder->mutex); if (codec) { @@ -180,17 +181,17 @@ static bool sc_recorder_process_header(struct sc_recorder *recorder) { sc_mutex_lock(&recorder->mutex); - while (!recorder->stopped && sc_queue_is_empty(&recorder->queue)) { + while (!recorder->stopped && sc_queue_is_empty(&recorder->video_queue)) { sc_cond_wait(&recorder->queue_cond, &recorder->mutex); } - if (recorder->stopped && sc_queue_is_empty(&recorder->queue)) { + if (recorder->stopped && sc_queue_is_empty(&recorder->video_queue)) { sc_mutex_unlock(&recorder->mutex); return false; } struct sc_record_packet *rec; - sc_queue_take(&recorder->queue, next, &rec); + sc_queue_take(&recorder->video_queue, next, &rec); sc_mutex_unlock(&recorder->mutex); @@ -226,20 +227,21 @@ sc_recorder_process_packets(struct sc_recorder *recorder) { for (;;) { sc_mutex_lock(&recorder->mutex); - while (!recorder->stopped && sc_queue_is_empty(&recorder->queue)) { + while (!recorder->stopped + && sc_queue_is_empty(&recorder->video_queue)) { sc_cond_wait(&recorder->queue_cond, &recorder->mutex); } // if stopped is set, continue to process the remaining events (to // finish the recording) before actually stopping - if (recorder->stopped && sc_queue_is_empty(&recorder->queue)) { + if (recorder->stopped && sc_queue_is_empty(&recorder->video_queue)) { sc_mutex_unlock(&recorder->mutex); break; } struct sc_record_packet *rec; - sc_queue_take(&recorder->queue, next, &rec); + sc_queue_take(&recorder->video_queue, next, &rec); sc_mutex_unlock(&recorder->mutex); @@ -336,7 +338,7 @@ run_recorder(void *data) { // Prevent the producer to push any new packet recorder->stopped = true; // Discard pending packets - sc_recorder_queue_clear(&recorder->queue); + sc_recorder_queue_clear(&recorder->video_queue); sc_mutex_unlock(&recorder->mutex); if (success) { @@ -355,9 +357,9 @@ run_recorder(void *data) { } static bool -sc_recorder_packet_sink_open(struct sc_packet_sink *sink, - const AVCodec *codec) { - struct sc_recorder *recorder = DOWNCAST(sink); +sc_recorder_video_packet_sink_open(struct sc_packet_sink *sink, + const AVCodec *codec) { + struct sc_recorder *recorder = DOWNCAST_VIDEO(sink); assert(codec); sc_mutex_lock(&recorder->mutex); @@ -366,7 +368,7 @@ sc_recorder_packet_sink_open(struct sc_packet_sink *sink, return false; } - recorder->codec = codec; + recorder->video_codec = codec; sc_cond_signal(&recorder->stream_cond); sc_mutex_unlock(&recorder->mutex); @@ -374,8 +376,8 @@ sc_recorder_packet_sink_open(struct sc_packet_sink *sink, } static void -sc_recorder_packet_sink_close(struct sc_packet_sink *sink) { - struct sc_recorder *recorder = DOWNCAST(sink); +sc_recorder_video_packet_sink_close(struct sc_packet_sink *sink) { + struct sc_recorder *recorder = DOWNCAST_VIDEO(sink); sc_mutex_lock(&recorder->mutex); // EOS also stops the recorder @@ -385,9 +387,9 @@ sc_recorder_packet_sink_close(struct sc_packet_sink *sink) { } static bool -sc_recorder_packet_sink_push(struct sc_packet_sink *sink, - const AVPacket *packet) { - struct sc_recorder *recorder = DOWNCAST(sink); +sc_recorder_video_packet_sink_push(struct sc_packet_sink *sink, + const AVPacket *packet) { + struct sc_recorder *recorder = DOWNCAST_VIDEO(sink); sc_mutex_lock(&recorder->mutex); @@ -404,7 +406,7 @@ sc_recorder_packet_sink_push(struct sc_packet_sink *sink, return false; } - sc_queue_push(&recorder->queue, next, rec); + sc_queue_push(&recorder->video_queue, next, rec); sc_cond_signal(&recorder->queue_cond); sc_mutex_unlock(&recorder->mutex); @@ -437,10 +439,10 @@ sc_recorder_init(struct sc_recorder *recorder, const char *filename, goto error_queue_cond_destroy; } - sc_queue_init(&recorder->queue); + sc_queue_init(&recorder->video_queue); recorder->stopped = false; - recorder->codec = NULL; + recorder->video_codec = NULL; recorder->format = format; recorder->declared_frame_size = declared_frame_size; @@ -449,13 +451,13 @@ sc_recorder_init(struct sc_recorder *recorder, const char *filename, recorder->cbs = cbs; recorder->cbs_userdata = cbs_userdata; - static const struct sc_packet_sink_ops ops = { - .open = sc_recorder_packet_sink_open, - .close = sc_recorder_packet_sink_close, - .push = sc_recorder_packet_sink_push, + static const struct sc_packet_sink_ops video_ops = { + .open = sc_recorder_video_packet_sink_open, + .close = sc_recorder_video_packet_sink_close, + .push = sc_recorder_video_packet_sink_push, }; - recorder->packet_sink.ops = &ops; + recorder->video_packet_sink.ops = &video_ops; return true; diff --git a/app/src/recorder.h b/app/src/recorder.h index 287030bc..b7a10e5e 100644 --- a/app/src/recorder.h +++ b/app/src/recorder.h @@ -20,7 +20,7 @@ struct sc_record_packet { struct sc_recorder_queue SC_QUEUE(struct sc_record_packet); struct sc_recorder { - struct sc_packet_sink packet_sink; // packet sink trait + struct sc_packet_sink video_packet_sink; // packet sink trait char *filename; enum sc_record_format format; @@ -32,11 +32,11 @@ struct sc_recorder { sc_cond queue_cond; // set on sc_recorder_stop(), packet_sink close or recording failure bool stopped; - struct sc_recorder_queue queue; + struct sc_recorder_queue video_queue; - // wake up the recorder thread once the codec in known + // wake up the recorder thread once the video codec is known sc_cond stream_cond; - const AVCodec *codec; + const AVCodec *video_codec; const struct sc_recorder_callbacks *cbs; void *cbs_userdata; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 0c846123..437ae650 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -435,7 +435,7 @@ scrcpy(struct scrcpy_options *options) { } recorder_started = true; - sc_demuxer_add_sink(&s->video_demuxer, &s->recorder.packet_sink); + sc_demuxer_add_sink(&s->video_demuxer, &s->recorder.video_packet_sink); } struct sc_controller *controller = NULL; From 7de062221465fef6c6be5998b3884bdc833c8526 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 18 Feb 2023 18:02:43 +0100 Subject: [PATCH 1393/2244] Add audio recording support Make the recorder accept two input sources (video and audio), and mux them into a single file. PR #3757 --- app/src/recorder.c | 393 ++++++++++++++++++++++++++++++++++++--------- app/src/recorder.h | 14 +- app/src/scrcpy.c | 8 +- 3 files changed, 334 insertions(+), 81 deletions(-) diff --git a/app/src/recorder.c b/app/src/recorder.c index 86817681..93c3321f 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -8,9 +8,11 @@ #include "util/log.h" #include "util/str.h" -/** Downcast video packet_sink to recorder */ +/** Downcast packet sinks to recorder */ #define DOWNCAST_VIDEO(SINK) \ container_of(SINK, struct sc_recorder, video_packet_sink) +#define DOWNCAST_AUDIO(SINK) \ + container_of(SINK, struct sc_recorder, audio_packet_sink) static const AVRational SCRCPY_TIME_BASE = {1, 1000000}; // timestamps in us @@ -79,9 +81,7 @@ sc_recorder_get_format_name(enum sc_record_format format) { } static bool -sc_recorder_write_header(struct sc_recorder *recorder, const AVPacket *packet) { - AVStream *ostream = recorder->ctx->streams[0]; - +sc_recorder_set_extradata(AVStream *ostream, const AVPacket *packet) { uint8_t *extradata = av_malloc(packet->size * sizeof(uint8_t)); if (!extradata) { LOG_OOM(); @@ -93,20 +93,32 @@ sc_recorder_write_header(struct sc_recorder *recorder, const AVPacket *packet) { ostream->codecpar->extradata = extradata; ostream->codecpar->extradata_size = packet->size; - - return avformat_write_header(recorder->ctx, NULL) >= 0; + return true; } -static void -sc_recorder_rescale_packet(struct sc_recorder *recorder, AVPacket *packet) { - AVStream *ostream = recorder->ctx->streams[0]; - av_packet_rescale_ts(packet, SCRCPY_TIME_BASE, ostream->time_base); +static inline void +sc_recorder_rescale_packet(AVStream *stream, AVPacket *packet) { + av_packet_rescale_ts(packet, SCRCPY_TIME_BASE, stream->time_base); } static bool -sc_recorder_write(struct sc_recorder *recorder, AVPacket *packet) { - sc_recorder_rescale_packet(recorder, packet); - return av_write_frame(recorder->ctx, packet) >= 0; +sc_recorder_write_stream(struct sc_recorder *recorder, int stream_index, + AVPacket *packet) { + AVStream *stream = recorder->ctx->streams[stream_index]; + sc_recorder_rescale_packet(stream, packet); + return av_interleaved_write_frame(recorder->ctx, packet) >= 0; +} + +static inline bool +sc_recorder_write_video(struct sc_recorder *recorder, AVPacket *packet) { + return sc_recorder_write_stream(recorder, recorder->video_stream_index, + packet); +} + +static inline bool +sc_recorder_write_audio(struct sc_recorder *recorder, AVPacket *packet) { + return sc_recorder_write_stream(recorder, recorder->audio_stream_index, + packet); } static bool @@ -162,134 +174,270 @@ sc_recorder_wait_video_stream(struct sc_recorder *recorder) { sc_mutex_unlock(&recorder->mutex); if (codec) { - AVStream *ostream = avformat_new_stream(recorder->ctx, codec); - if (!ostream) { + AVStream *stream = avformat_new_stream(recorder->ctx, codec); + if (!stream) { return false; } - ostream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO; - ostream->codecpar->codec_id = codec->id; - ostream->codecpar->format = AV_PIX_FMT_YUV420P; - ostream->codecpar->width = recorder->declared_frame_size.width; - ostream->codecpar->height = recorder->declared_frame_size.height; + stream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO; + stream->codecpar->codec_id = codec->id; + stream->codecpar->format = AV_PIX_FMT_YUV420P; + stream->codecpar->width = recorder->declared_frame_size.width; + stream->codecpar->height = recorder->declared_frame_size.height; + + recorder->video_stream_index = stream->index; } return true; } +static bool +sc_recorder_wait_audio_stream(struct sc_recorder *recorder) { + sc_mutex_lock(&recorder->mutex); + while (!recorder->audio_codec && !recorder->stopped) { + sc_cond_wait(&recorder->stream_cond, &recorder->mutex); + } + const AVCodec *codec = recorder->audio_codec; + sc_mutex_unlock(&recorder->mutex); + + if (codec) { + AVStream *stream = avformat_new_stream(recorder->ctx, codec); + if (!stream) { + return false; + } + + stream->codecpar->codec_type = AVMEDIA_TYPE_AUDIO; + stream->codecpar->codec_id = codec->id; + stream->codecpar->ch_layout.nb_channels = 2; + stream->codecpar->sample_rate = 48000; + + recorder->audio_stream_index = stream->index; + } + + return true; +} + +static inline bool +sc_recorder_has_empty_queues(struct sc_recorder *recorder) { + if (sc_queue_is_empty(&recorder->video_queue)) { + // The video queue is empty + return true; + } + + if (recorder->audio && sc_queue_is_empty(&recorder->audio_queue)) { + // The audio queue is empty (when audio is enabled) + return true; + } + + // No queue is empty + return false; +} + static bool sc_recorder_process_header(struct sc_recorder *recorder) { sc_mutex_lock(&recorder->mutex); - while (!recorder->stopped && sc_queue_is_empty(&recorder->video_queue)) { + while (!recorder->stopped && sc_recorder_has_empty_queues(recorder)) { sc_cond_wait(&recorder->queue_cond, &recorder->mutex); } - if (recorder->stopped && sc_queue_is_empty(&recorder->video_queue)) { + if (sc_recorder_has_empty_queues(recorder)) { + assert(recorder->stopped); sc_mutex_unlock(&recorder->mutex); return false; } - struct sc_record_packet *rec; - sc_queue_take(&recorder->video_queue, next, &rec); + struct sc_record_packet *video_pkt; + sc_queue_take(&recorder->video_queue, next, &video_pkt); + + struct sc_record_packet *audio_pkt; + if (recorder->audio) { + sc_queue_take(&recorder->audio_queue, next, &audio_pkt); + } sc_mutex_unlock(&recorder->mutex); - if (rec->packet->pts != AV_NOPTS_VALUE) { - LOGE("The first packet is not a config packet"); - sc_record_packet_delete(rec); - return false; + int ret = false; + + if (video_pkt->packet->pts != AV_NOPTS_VALUE) { + LOGE("The first video packet is not a config packet"); + goto end; } - bool ok = sc_recorder_write_header(recorder, rec->packet); - sc_record_packet_delete(rec); + assert(recorder->video_stream_index >= 0); + AVStream *video_stream = + recorder->ctx->streams[recorder->video_stream_index]; + bool ok = sc_recorder_set_extradata(video_stream, video_pkt->packet); + if (!ok) { + goto end; + } + + if (recorder->audio) { + if (audio_pkt->packet->pts != AV_NOPTS_VALUE) { + LOGE("The first audio packet is not a config packet"); + goto end; + } + + assert(recorder->audio_stream_index >= 0); + AVStream *audio_stream = + recorder->ctx->streams[recorder->audio_stream_index]; + ok = sc_recorder_set_extradata(audio_stream, audio_pkt->packet); + if (!ok) { + goto end; + } + } + + ok = avformat_write_header(recorder->ctx, NULL) >= 0; if (!ok) { LOGE("Failed to write header to %s", recorder->filename); - return false; + goto end; } - return true; + ret = true; + +end: + sc_record_packet_delete(video_pkt); + if (recorder->audio) { + sc_record_packet_delete(audio_pkt); + } + + return ret; } static bool sc_recorder_process_packets(struct sc_recorder *recorder) { int64_t pts_origin = AV_NOPTS_VALUE; - // We can write a packet only once we received the next one so that we can - // set its duration (next_pts - current_pts) - struct sc_record_packet *previous = NULL; - bool header_written = sc_recorder_process_header(recorder); if (!header_written) { return false; } + struct sc_record_packet *video_pkt = NULL; + struct sc_record_packet *audio_pkt = NULL; + + // We can write a video packet only once we received the next one so that + // we can set its duration (next_pts - current_pts) + struct sc_record_packet *video_pkt_previous = NULL; + + bool error = false; + for (;;) { sc_mutex_lock(&recorder->mutex); - while (!recorder->stopped - && sc_queue_is_empty(&recorder->video_queue)) { + while (!recorder->stopped) { + if (!video_pkt && !sc_queue_is_empty(&recorder->video_queue)) { + // A new packet may be assigned to video_pkt and be processed + break; + } + if (recorder->audio && !audio_pkt + && !sc_queue_is_empty(&recorder->audio_queue)) { + // A new packet may be assigned to audio_pkt and be processed + break; + } sc_cond_wait(&recorder->queue_cond, &recorder->mutex); } - // if stopped is set, continue to process the remaining events (to - // finish the recording) before actually stopping + // If stopped is set, continue to process the remaining events (to + // finish the recording) before actually stopping. - if (recorder->stopped && sc_queue_is_empty(&recorder->video_queue)) { + // If there is no audio, then the audio_queue will remain empty forever + // and audio_pkt will always be NULL. + assert(recorder->audio + || (!audio_pkt && sc_queue_is_empty(&recorder->audio_queue))); + + if (!video_pkt && !sc_queue_is_empty(&recorder->video_queue)) { + sc_queue_take(&recorder->video_queue, next, &video_pkt); + } + + if (!audio_pkt && !sc_queue_is_empty(&recorder->audio_queue)) { + sc_queue_take(&recorder->audio_queue, next, &audio_pkt); + } + + if (recorder->stopped && !video_pkt && !audio_pkt) { + assert(sc_queue_is_empty(&recorder->video_queue)); + assert(sc_queue_is_empty(&recorder->audio_queue)); sc_mutex_unlock(&recorder->mutex); break; } - struct sc_record_packet *rec; - sc_queue_take(&recorder->video_queue, next, &rec); + assert(video_pkt || audio_pkt); // at least one sc_mutex_unlock(&recorder->mutex); - if (rec->packet->pts == AV_NOPTS_VALUE) { - // Ignore further config packets (e.g. on device orientation - // change). The next non-config packet will have the config packet - // data prepended. - sc_record_packet_delete(rec); - } else { - assert(rec->packet->pts != AV_NOPTS_VALUE); + // Ignore further config packets (e.g. on device orientation + // change). The next non-config packet will have the config packet + // data prepended. + if (video_pkt && video_pkt->packet->pts == AV_NOPTS_VALUE) { + sc_record_packet_delete(video_pkt); + video_pkt = NULL; + } - if (!previous) { - // This is the first non-config packet - assert(pts_origin == AV_NOPTS_VALUE); - pts_origin = rec->packet->pts; - rec->packet->pts = 0; - rec->packet->dts = 0; - previous = rec; + if (audio_pkt && audio_pkt->packet->pts == AV_NOPTS_VALUE) { + sc_record_packet_delete(audio_pkt); + audio_pkt= NULL; + } + + if (pts_origin == AV_NOPTS_VALUE) { + if (!recorder->audio) { + assert(video_pkt); + pts_origin = video_pkt->packet->pts; + } else if (video_pkt && audio_pkt) { + pts_origin = + MIN(video_pkt->packet->pts, audio_pkt->packet->pts); + } else { + // We need both video and audio packets to initialize pts_origin continue; } + } - assert(previous); - assert(pts_origin != AV_NOPTS_VALUE); + assert(pts_origin != AV_NOPTS_VALUE); - rec->packet->pts -= pts_origin; - rec->packet->dts = rec->packet->pts; + if (video_pkt) { + video_pkt->packet->pts -= pts_origin; + video_pkt->packet->dts = video_pkt->packet->pts; - // we now know the duration of the previous packet - previous->packet->duration = - rec->packet->pts - previous->packet->pts; + if (video_pkt_previous) { + // we now know the duration of the previous packet + video_pkt_previous->packet->duration = + video_pkt->packet->pts - video_pkt_previous->packet->pts; - bool ok = sc_recorder_write(recorder, previous->packet); - sc_record_packet_delete(previous); - if (!ok) { - LOGE("Could not record packet"); - return false; + bool ok = sc_recorder_write_video(recorder, + video_pkt_previous->packet); + sc_record_packet_delete(video_pkt_previous); + if (!ok) { + LOGE("Could not record video packet"); + error = true; + goto end; + } } - previous = rec; + video_pkt_previous = video_pkt; + video_pkt = NULL; + } + + if (audio_pkt) { + audio_pkt->packet->pts -= pts_origin; + audio_pkt->packet->dts = audio_pkt->packet->pts; + + bool ok = sc_recorder_write_audio(recorder, audio_pkt->packet); + if (!ok) { + LOGE("Could not record audio packet"); + error = true; + goto end; + } + + sc_record_packet_delete(audio_pkt); + audio_pkt = NULL; } } - // Write the last packet - struct sc_record_packet *last = previous; + // Write the last video packet + struct sc_record_packet *last = video_pkt_previous; if (last) { // assign an arbitrary duration to the last packet last->packet->duration = 100000; - bool ok = sc_recorder_write(recorder, last->packet); + bool ok = sc_recorder_write_video(recorder, last->packet); if (!ok) { // failing to write the last frame is not very serious, no // future frame may depend on it, so the resulting file @@ -302,10 +450,18 @@ sc_recorder_process_packets(struct sc_recorder *recorder) { int ret = av_write_trailer(recorder->ctx); if (ret < 0) { LOGE("Failed to write trailer to %s", recorder->filename); - return false; + error = false; } - return true; +end: + if (video_pkt) { + sc_record_packet_delete(video_pkt); + } + if (audio_pkt) { + sc_record_packet_delete(audio_pkt); + } + + return !error; } static bool @@ -321,6 +477,14 @@ sc_recorder_record(struct sc_recorder *recorder) { return false; } + if (recorder->audio) { + ok = sc_recorder_wait_audio_stream(recorder); + if (!ok) { + sc_recorder_close_output_file(recorder); + return false; + } + } + // If recorder->stopped, process any queued packet anyway ok = sc_recorder_process_packets(recorder); @@ -339,6 +503,7 @@ run_recorder(void *data) { recorder->stopped = true; // Discard pending packets sc_recorder_queue_clear(&recorder->video_queue); + sc_recorder_queue_clear(&recorder->audio_queue); sc_mutex_unlock(&recorder->mutex); if (success) { @@ -406,6 +571,8 @@ sc_recorder_video_packet_sink_push(struct sc_packet_sink *sink, return false; } + rec->packet->stream_index = recorder->video_stream_index; + sc_queue_push(&recorder->video_queue, next, rec); sc_cond_signal(&recorder->queue_cond); @@ -413,9 +580,66 @@ sc_recorder_video_packet_sink_push(struct sc_packet_sink *sink, return true; } +static bool +sc_recorder_audio_packet_sink_open(struct sc_packet_sink *sink, + const AVCodec *codec) { + struct sc_recorder *recorder = DOWNCAST_AUDIO(sink); + assert(recorder->audio); + assert(codec); + + sc_mutex_lock(&recorder->mutex); + recorder->audio_codec = codec; + sc_cond_signal(&recorder->stream_cond); + sc_mutex_unlock(&recorder->mutex); + + return true; +} + +static void +sc_recorder_audio_packet_sink_close(struct sc_packet_sink *sink) { + struct sc_recorder *recorder = DOWNCAST_AUDIO(sink); + assert(recorder->audio); + + sc_mutex_lock(&recorder->mutex); + // EOS also stops the recorder + recorder->stopped = true; + sc_cond_signal(&recorder->queue_cond); + sc_mutex_unlock(&recorder->mutex); +} + +static bool +sc_recorder_audio_packet_sink_push(struct sc_packet_sink *sink, + const AVPacket *packet) { + struct sc_recorder *recorder = DOWNCAST_AUDIO(sink); + assert(recorder->audio); + + sc_mutex_lock(&recorder->mutex); + + if (recorder->stopped) { + // reject any new packet + sc_mutex_unlock(&recorder->mutex); + return false; + } + + struct sc_record_packet *rec = sc_record_packet_new(packet); + if (!rec) { + LOG_OOM(); + sc_mutex_unlock(&recorder->mutex); + return false; + } + + rec->packet->stream_index = recorder->audio_stream_index; + + sc_queue_push(&recorder->audio_queue, next, rec); + sc_cond_signal(&recorder->queue_cond); + + sc_mutex_unlock(&recorder->mutex); + return true; +} + bool sc_recorder_init(struct sc_recorder *recorder, const char *filename, - enum sc_record_format format, + enum sc_record_format format, bool audio, struct sc_size declared_frame_size, const struct sc_recorder_callbacks *cbs, void *cbs_userdata) { recorder->filename = strdup(filename); @@ -439,10 +663,17 @@ sc_recorder_init(struct sc_recorder *recorder, const char *filename, goto error_queue_cond_destroy; } + recorder->audio = audio; + sc_queue_init(&recorder->video_queue); + sc_queue_init(&recorder->audio_queue); recorder->stopped = false; recorder->video_codec = NULL; + recorder->audio_codec = NULL; + + recorder->video_stream_index = -1; + recorder->audio_stream_index = -1; recorder->format = format; recorder->declared_frame_size = declared_frame_size; @@ -459,6 +690,16 @@ sc_recorder_init(struct sc_recorder *recorder, const char *filename, recorder->video_packet_sink.ops = &video_ops; + if (audio) { + static const struct sc_packet_sink_ops audio_ops = { + .open = sc_recorder_audio_packet_sink_open, + .close = sc_recorder_audio_packet_sink_close, + .push = sc_recorder_audio_packet_sink_push, + }; + + recorder->audio_packet_sink.ops = &audio_ops; + } + return true; error_queue_cond_destroy: diff --git a/app/src/recorder.h b/app/src/recorder.h index b7a10e5e..ed880de0 100644 --- a/app/src/recorder.h +++ b/app/src/recorder.h @@ -20,7 +20,10 @@ struct sc_record_packet { struct sc_recorder_queue SC_QUEUE(struct sc_record_packet); struct sc_recorder { - struct sc_packet_sink video_packet_sink; // packet sink trait + struct sc_packet_sink video_packet_sink; + struct sc_packet_sink audio_packet_sink; + + bool audio; char *filename; enum sc_record_format format; @@ -33,10 +36,15 @@ struct sc_recorder { // set on sc_recorder_stop(), packet_sink close or recording failure bool stopped; struct sc_recorder_queue video_queue; + struct sc_recorder_queue audio_queue; - // wake up the recorder thread once the video codec is known + // wake up the recorder thread once the video or audio codec is known sc_cond stream_cond; const AVCodec *video_codec; + const AVCodec *audio_codec; + + int video_stream_index; + int audio_stream_index; const struct sc_recorder_callbacks *cbs; void *cbs_userdata; @@ -49,7 +57,7 @@ struct sc_recorder_callbacks { bool sc_recorder_init(struct sc_recorder *recorder, const char *filename, - enum sc_record_format format, + enum sc_record_format format, bool audio, struct sc_size declared_frame_size, const struct sc_recorder_callbacks *cbs, void *cbs_userdata); diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 437ae650..6750f6a1 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -424,8 +424,8 @@ scrcpy(struct scrcpy_options *options) { .on_ended = sc_recorder_on_ended, }; if (!sc_recorder_init(&s->recorder, options->record_filename, - options->record_format, info->frame_size, - &recorder_cbs, NULL)) { + options->record_format, options->audio, + info->frame_size, &recorder_cbs, NULL)) { goto end; } recorder_initialized = true; @@ -436,6 +436,10 @@ scrcpy(struct scrcpy_options *options) { recorder_started = true; sc_demuxer_add_sink(&s->video_demuxer, &s->recorder.video_packet_sink); + if (options->audio) { + sc_demuxer_add_sink(&s->audio_demuxer, + &s->recorder.audio_packet_sink); + } } struct sc_controller *controller = NULL; From 13a3395a332eca55c884edc05a5052457b519f60 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 18 Feb 2023 18:09:18 +0100 Subject: [PATCH 1394/2244] Disable audio on initialization error By default, audio is enabled (--no-audio must be explicitly passed to disable it). However, some devices may not support audio capture (typically devices below Android 11, or Android 11 when the shell application is not foreground on start). In that case, make the server notify the client to dynamically disable audio forwarding so that it does not wait indefinitely for an audio stream. Also disable audio on unknown codec or missing decoder on the client-side, for the same reasons. PR #3757 --- app/src/demuxer.c | 28 +++++++++++++-- app/src/recorder.c | 34 ++++++++++++++++++- app/src/recorder.h | 11 ++++++ app/src/trait/packet_sink.h | 10 ++++++ .../com/genymobile/scrcpy/AudioEncoder.java | 4 +++ .../java/com/genymobile/scrcpy/Streamer.java | 6 ++++ 6 files changed, 90 insertions(+), 3 deletions(-) diff --git a/app/src/demuxer.c b/app/src/demuxer.c index 968e6db0..482f2e04 100644 --- a/app/src/demuxer.c +++ b/app/src/demuxer.c @@ -158,6 +158,16 @@ sc_demuxer_open_sinks(struct sc_demuxer *demuxer, const AVCodec *codec) { return true; } +static void +sc_demuxer_disable_sinks(struct sc_demuxer *demuxer) { + for (unsigned i = 0; i < demuxer->sink_count; ++i) { + struct sc_packet_sink *sink = demuxer->sinks[i]; + if (sink->ops->disable) { + sink->ops->disable(sink); + } + } +} + static int run_demuxer(void *data) { struct sc_demuxer *demuxer = data; @@ -168,19 +178,33 @@ run_demuxer(void *data) { uint32_t raw_codec_id; bool ok = sc_demuxer_recv_codec_id(demuxer, &raw_codec_id); if (!ok) { + LOGE("Demuxer '%s': stream disabled due to connection error", + demuxer->name); + eos = true; + goto end; + } + + if (raw_codec_id == 0) { + LOGW("Demuxer '%s': stream explicitly disabled by the device", + demuxer->name); + sc_demuxer_disable_sinks(demuxer); eos = true; goto end; } enum AVCodecID codec_id = sc_demuxer_to_avcodec_id(raw_codec_id); if (codec_id == AV_CODEC_ID_NONE) { - // Error already logged + LOGE("Demuxer '%s': stream disabled due to unsupported codec", + demuxer->name); + sc_demuxer_disable_sinks(demuxer); goto end; } const AVCodec *codec = avcodec_find_decoder(codec_id); if (!codec) { - LOGE("Demuxer '%s': decoder not found", demuxer->name); + LOGE("Demuxer '%s': stream disabled due to missing decoder", + demuxer->name); + sc_demuxer_disable_sinks(demuxer); goto end; } diff --git a/app/src/recorder.c b/app/src/recorder.c index 93c3321f..c694f022 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -194,9 +194,17 @@ sc_recorder_wait_video_stream(struct sc_recorder *recorder) { static bool sc_recorder_wait_audio_stream(struct sc_recorder *recorder) { sc_mutex_lock(&recorder->mutex); - while (!recorder->audio_codec && !recorder->stopped) { + while (!recorder->audio_codec && !recorder->audio_disabled + && !recorder->stopped) { sc_cond_wait(&recorder->stream_cond, &recorder->mutex); } + + if (recorder->audio_disabled) { + // Reset audio flag. From there, the recorder thread may access this + // flag without any mutex. + recorder->audio = false; + } + const AVCodec *codec = recorder->audio_codec; sc_mutex_unlock(&recorder->mutex); @@ -585,6 +593,8 @@ sc_recorder_audio_packet_sink_open(struct sc_packet_sink *sink, const AVCodec *codec) { struct sc_recorder *recorder = DOWNCAST_AUDIO(sink); assert(recorder->audio); + // only written from this thread, no need to lock + assert(!recorder->audio_disabled); assert(codec); sc_mutex_lock(&recorder->mutex); @@ -599,6 +609,8 @@ static void sc_recorder_audio_packet_sink_close(struct sc_packet_sink *sink) { struct sc_recorder *recorder = DOWNCAST_AUDIO(sink); assert(recorder->audio); + // only written from this thread, no need to lock + assert(!recorder->audio_disabled); sc_mutex_lock(&recorder->mutex); // EOS also stops the recorder @@ -612,6 +624,8 @@ sc_recorder_audio_packet_sink_push(struct sc_packet_sink *sink, const AVPacket *packet) { struct sc_recorder *recorder = DOWNCAST_AUDIO(sink); assert(recorder->audio); + // only written from this thread, no need to lock + assert(!recorder->audio_disabled); sc_mutex_lock(&recorder->mutex); @@ -637,6 +651,22 @@ sc_recorder_audio_packet_sink_push(struct sc_packet_sink *sink, return true; } +static void +sc_recorder_audio_packet_sink_disable(struct sc_packet_sink *sink) { + struct sc_recorder *recorder = DOWNCAST_AUDIO(sink); + assert(recorder->audio); + // only written from this thread, no need to lock + assert(!recorder->audio_disabled); + assert(!recorder->audio_codec); + + LOGW("Audio stream recording disabled"); + + sc_mutex_lock(&recorder->mutex); + recorder->audio_disabled = true; + sc_cond_signal(&recorder->stream_cond); + sc_mutex_unlock(&recorder->mutex); +} + bool sc_recorder_init(struct sc_recorder *recorder, const char *filename, enum sc_record_format format, bool audio, @@ -671,6 +701,7 @@ sc_recorder_init(struct sc_recorder *recorder, const char *filename, recorder->video_codec = NULL; recorder->audio_codec = NULL; + recorder->audio_disabled = false; recorder->video_stream_index = -1; recorder->audio_stream_index = -1; @@ -695,6 +726,7 @@ sc_recorder_init(struct sc_recorder *recorder, const char *filename, .open = sc_recorder_audio_packet_sink_open, .close = sc_recorder_audio_packet_sink_close, .push = sc_recorder_audio_packet_sink_push, + .disable = sc_recorder_audio_packet_sink_disable, }; recorder->audio_packet_sink.ops = &audio_ops; diff --git a/app/src/recorder.h b/app/src/recorder.h index ed880de0..6fe72401 100644 --- a/app/src/recorder.h +++ b/app/src/recorder.h @@ -23,6 +23,14 @@ struct sc_recorder { struct sc_packet_sink video_packet_sink; struct sc_packet_sink audio_packet_sink; + /* The audio flag is unprotected: + * - it is initialized from sc_recorder_init() from the main thread; + * - it may be reset once from the recorder thread if the audio is + * disabled dynamically. + * + * Therefore, once the recorder thread is started, only the recorder thread + * may access it without data races. + */ bool audio; char *filename; @@ -42,6 +50,9 @@ struct sc_recorder { sc_cond stream_cond; const AVCodec *video_codec; const AVCodec *audio_codec; + // Instead of providing an audio_codec, the demuxer may notify that the + // stream is disabled if the device could not capture audio + bool audio_disabled; int video_stream_index; int audio_stream_index; diff --git a/app/src/trait/packet_sink.h b/app/src/trait/packet_sink.h index 9fc9fd24..099c8c52 100644 --- a/app/src/trait/packet_sink.h +++ b/app/src/trait/packet_sink.h @@ -23,6 +23,16 @@ struct sc_packet_sink_ops { bool (*open)(struct sc_packet_sink *sink, const AVCodec *codec); void (*close)(struct sc_packet_sink *sink); bool (*push)(struct sc_packet_sink *sink, const AVPacket *packet); + + /*/ + * Called when the input stream has been disabled at runtime. + * + * If it is called, then open(), close() and push() will never be called. + * + * It is useful to notify the recorder that the requested audio stream has + * finally been disabled because the device could not capture it. + */ + void (*disable)(struct sc_packet_sink *sink); }; #endif diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java b/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java index 74481f99..8bc25e8e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java @@ -255,6 +255,10 @@ public final class AudioEncoder { outputThread.start(); waitEnded(); + } catch (Throwable e) { + // Notify the client that the audio could not be captured + streamer.writeDisableStream(); + throw e; } finally { // Cleanup everything (either at the end or on error at any step of the initialization) if (mediaCodecThread != null) { diff --git a/server/src/main/java/com/genymobile/scrcpy/Streamer.java b/server/src/main/java/com/genymobile/scrcpy/Streamer.java index 77d9eefa..7cc065eb 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Streamer.java +++ b/server/src/main/java/com/genymobile/scrcpy/Streamer.java @@ -40,6 +40,12 @@ public final class Streamer { } } + public void writeDisableStream() throws IOException { + // Writing 0 (32-bit) as codec-id means that the device disables the stream (because it could not capture) + byte[] zeros = new byte[4]; + IO.writeFully(fd, zeros, 0, zeros.length); + } + public void writePacket(ByteBuffer buffer, long pts, boolean config, boolean keyFrame) throws IOException { if (config && codec == AudioCodec.OPUS) { fixOpusConfigPacket(buffer); From 80c0780b7779acc27a563ee5acdd3ac645108763 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 19 Feb 2023 15:24:08 +0100 Subject: [PATCH 1395/2244] Disable audio before Android 11 The permission "android.permission.RECORD_AUDIO" has been added for shell in Android 11. Moreover, on lower versions, it may make the server segfault on the device (happened on a Nexus 5 with Android 6.0.1). Refs PR #3757 --- .../src/main/java/com/genymobile/scrcpy/AudioEncoder.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java b/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java index 8bc25e8e..d06898d6 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java @@ -203,6 +203,12 @@ public final class AudioEncoder { @TargetApi(Build.VERSION_CODES.M) public void encode() throws IOException { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + Ln.w("Audio disabled: it is not supported before Android 11"); + streamer.writeDisableStream(); + return; + } + MediaCodec mediaCodec = null; AudioRecord recorder = null; From 17d5301c0fd6f93c5b9d4dd325e4303739d93ffb Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 19 Feb 2023 00:55:36 +0100 Subject: [PATCH 1396/2244] Record at least video packets on stop If the recorder is stopped while it has not received any audio packet yet, make sure the video stream is correctly recorded. PR #3757 --- app/src/recorder.c | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/app/src/recorder.c b/app/src/recorder.c index c694f022..9fc15dac 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -249,8 +249,10 @@ sc_recorder_process_header(struct sc_recorder *recorder) { sc_cond_wait(&recorder->queue_cond, &recorder->mutex); } - if (sc_recorder_has_empty_queues(recorder)) { + if (sc_queue_is_empty(&recorder->video_queue)) { assert(recorder->stopped); + // Don't process anything if there are not at least video packets (when + // the recorder is stopped) sc_mutex_unlock(&recorder->mutex); return false; } @@ -258,8 +260,9 @@ sc_recorder_process_header(struct sc_recorder *recorder) { struct sc_record_packet *video_pkt; sc_queue_take(&recorder->video_queue, next, &video_pkt); - struct sc_record_packet *audio_pkt; - if (recorder->audio) { + struct sc_record_packet *audio_pkt = NULL; + if (!sc_queue_is_empty(&recorder->audio_queue)) { + assert(recorder->audio); sc_queue_take(&recorder->audio_queue, next, &audio_pkt); } @@ -280,7 +283,7 @@ sc_recorder_process_header(struct sc_recorder *recorder) { goto end; } - if (recorder->audio) { + if (audio_pkt) { if (audio_pkt->packet->pts != AV_NOPTS_VALUE) { LOGE("The first audio packet is not a config packet"); goto end; @@ -305,7 +308,7 @@ sc_recorder_process_header(struct sc_recorder *recorder) { end: sc_record_packet_delete(video_pkt); - if (recorder->audio) { + if (audio_pkt) { sc_record_packet_delete(audio_pkt); } @@ -393,6 +396,16 @@ sc_recorder_process_packets(struct sc_recorder *recorder) { } else if (video_pkt && audio_pkt) { pts_origin = MIN(video_pkt->packet->pts, audio_pkt->packet->pts); + } else if (recorder->stopped) { + if (video_pkt) { + // The recorder is stopped without audio, record the video + // packets + pts_origin = video_pkt->packet->pts; + } else { + // Fail if there is no video + error = true; + goto end; + } } else { // We need both video and audio packets to initialize pts_origin continue; From a1802dab763d5031bc26576e45ab1d388a8904fc Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 18 Feb 2023 18:21:14 +0100 Subject: [PATCH 1397/2244] Remove default bit-rate on client side If no bit-rate is passed, let the server use the default value (8Mbps). This avoids to define a default value on both sides, and to pass the default bit-rate as an argument when starting the server. PR #3757 --- app/meson.build | 4 ---- app/scrcpy.1 | 2 +- app/src/cli.c | 2 +- app/src/options.c | 2 +- app/src/server.c | 4 +++- 5 files changed, 6 insertions(+), 8 deletions(-) diff --git a/app/meson.build b/app/meson.build index b6a772a9..2ea3b317 100644 --- a/app/meson.build +++ b/app/meson.build @@ -195,10 +195,6 @@ conf.set('PORTABLE', get_option('portable')) conf.set('DEFAULT_LOCAL_PORT_RANGE_FIRST', '27183') conf.set('DEFAULT_LOCAL_PORT_RANGE_LAST', '27199') -# the default video bitrate, in bits/second -# overridden by option --bit-rate -conf.set('DEFAULT_BIT_RATE', '8000000') # 8Mbps - # run a server debugger and wait for a client to be attached conf.set('SERVER_DEBUGGER', get_option('server_debugger')) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 0f1147da..186d8ad5 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -23,7 +23,7 @@ Make scrcpy window always on top (above other windows). .BI "\-b, \-\-bit\-rate " value Encode the video at the given bit\-rate, expressed in bits/s. Unit suffixes are supported: '\fBK\fR' (x1000) and '\fBM\fR' (x1000000). -Default is 8000000. +Default is 8M (8000000). .TP .BI "\-\-codec " name diff --git a/app/src/cli.c b/app/src/cli.c index 4f023da0..d7a0a7ae 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -107,7 +107,7 @@ static const struct sc_option options[] = { .argdesc = "value", .text = "Encode the video at the given bit-rate, expressed in bits/s. " "Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n" - "Default is " STR(DEFAULT_BIT_RATE) ".", + "Default is 8M (8000000).", }, { .longopt_id = OPT_CODEC, diff --git a/app/src/options.c b/app/src/options.c index 0854067f..64ec5b3b 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -28,7 +28,7 @@ const struct scrcpy_options scrcpy_options_default = { .count = 2, }, .max_size = 0, - .bit_rate = DEFAULT_BIT_RATE, + .bit_rate = 0, .max_fps = 0, .lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_UNLOCKED, .rotation = 0, diff --git a/app/src/server.c b/app/src/server.c index 88e3421f..6bf0eb6e 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -215,8 +215,10 @@ execute_server(struct sc_server *server, ADD_PARAM("scid=%08x", params->scid); ADD_PARAM("log_level=%s", log_level_to_server_string(params->log_level)); - ADD_PARAM("bit_rate=%" PRIu32, params->bit_rate); + if (params->bit_rate) { + ADD_PARAM("bit_rate=%" PRIu32, params->bit_rate); + } if (!params->audio) { ADD_PARAM("audio=false"); } From cee40ca047f02c68e59372ae7305fce6d6ead8f0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 20 Feb 2023 21:19:36 +0100 Subject: [PATCH 1398/2244] Rename --codec to --video-codec This prepares the introduction of --audio-codec. PR #3757 --- README.md | 10 +++---- app/data/bash-completion/scrcpy | 4 +-- app/data/zsh-completion/_scrcpy | 2 +- app/scrcpy.1 | 14 +++++----- app/src/cli.c | 27 ++++++++++++++----- app/src/options.c | 2 +- app/src/options.h | 2 +- app/src/scrcpy.c | 2 +- app/src/server.c | 5 ++-- app/src/server.h | 2 +- .../java/com/genymobile/scrcpy/Options.java | 10 +++---- .../com/genymobile/scrcpy/ScreenEncoder.java | 2 +- .../java/com/genymobile/scrcpy/Server.java | 16 +++++------ 13 files changed, 56 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index a19aec0f..4b898d26 100644 --- a/README.md +++ b/README.md @@ -258,9 +258,9 @@ The video codec can be selected. The possible values are `h264` (default), `h265` and `av1`: ```bash -scrcpy --codec=h264 # default -scrcpy --codec=h265 -scrcpy --codec=av1 +scrcpy --video-codec=h264 # default +scrcpy --video-codec=h265 +scrcpy --video-codec=av1 ``` @@ -277,8 +277,8 @@ To list the available encoders, you can pass an invalid encoder name; the error will give the available encoders: ```bash -scrcpy --encoder=_ # for the default codec -scrcpy --codec=h265 --encoder=_ # for a specific codec +scrcpy --encoder=_ # for the default codec +scrcpy --video-codec=h265 --encoder=_ # for a specific codec ``` ### Capture diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index 22dc4cee..d92cf009 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -3,7 +3,6 @@ _scrcpy() { local opts=" --always-on-top -b --bit-rate= - --codec= --codec-options= --crop= -d --select-usb @@ -55,6 +54,7 @@ _scrcpy() { --v4l2-sink= -V --verbosity= -v --version + --video-codec= -w --stay-awake --window-borderless --window-title= @@ -66,7 +66,7 @@ _scrcpy() { _init_completion -s || return case "$prev" in - --codec) + --video-codec) COMPREPLY=($(compgen -W 'h264 h265 av1' -- "$cur")) return ;; diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 17e1de9f..b9c94e1e 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -10,7 +10,6 @@ local arguments arguments=( '--always-on-top[Make scrcpy window always on top \(above other windows\)]' {-b,--bit-rate=}'[Encode the video at the given bit-rate]' - '--codec=[Select the video codec]:codec:(h264 h265 av1)' '--codec-options=[Set a list of comma-separated key\:type=value options for the device encoder]' '--crop=[\[width\:height\:x\:y\] Crop the device screen on the server]' {-d,--select-usb}'[Use USB device]' @@ -60,6 +59,7 @@ arguments=( '--v4l2-sink=[\[\/dev\/videoN\] Output to v4l2loopback device]' {-V,--verbosity=}'[Set the log level]:verbosity:(verbose debug info warn error)' {-v,--version}'[Print the version of scrcpy]' + '--video-codec=[Select the video codec]:codec:(h264 h265 av1)' {-w,--stay-awake}'[Keep the device on while scrcpy is running, when the device is plugged in]' '--window-borderless[Disable window decorations \(display borderless window\)]' '--window-title=[Set a custom window title]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 186d8ad5..32bb8464 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -25,12 +25,6 @@ Encode the video at the given bit\-rate, expressed in bits/s. Unit suffixes are Default is 8M (8000000). -.TP -.BI "\-\-codec " name -Select a video codec (h264, h265 or av1). - -Default is h264. - .TP .BI "\-\-codec\-options " key\fR[:\fItype\fR]=\fIvalue\fR[,...] Set a list of comma-separated key:type=value options for the device encoder. @@ -82,7 +76,7 @@ Also see \fB\-d\fR (\fB\-\-select\-usb\fR). .TP .BI "\-\-encoder " name -Use a specific MediaCodec encoder (depending on the codec provided by \fB\-\-codec\fR). +Use a specific MediaCodec encoder (depending on the codec provided by \fB\-\-video\-codec\fR). .TP .B \-\-force\-adb\-forward @@ -329,6 +323,12 @@ Default is "info" for release builds, "debug" for debug builds. .B \-v, \-\-version Print the version of scrcpy. +.TP +.BI "\-\-video\-codec " name +Select a video codec (h264, h265 or av1). + +Default is h264. + .TP .B \-w, \-\-stay-awake Keep the device on while scrcpy is running, when the device is plugged in. diff --git a/app/src/cli.c b/app/src/cli.c index d7a0a7ae..9163ba60 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -59,6 +59,7 @@ enum { OPT_PRINT_FPS, OPT_NO_POWER_ON, OPT_CODEC, + OPT_VIDEO_CODEC, OPT_NO_AUDIO, }; @@ -110,11 +111,13 @@ static const struct sc_option options[] = { "Default is 8M (8000000).", }, { + // Not really deprecated (--codec has never been released), but without + // declaring an explicit --codec option, getopt_long() partial matching + // behavior would consider --codec to be equivalent to --codec-options, + // which would be confusing. .longopt_id = OPT_CODEC, .longopt = "codec", - .argdesc = "name", - .text = "Select a video codec (h264, h265 or av1).\n" - "Default is h264.", + .argdesc = "value", }, { .longopt_id = OPT_CODEC_OPTIONS, @@ -177,7 +180,7 @@ static const struct sc_option options[] = { .longopt = "encoder", .argdesc = "name", .text = "Use a specific MediaCodec encoder (depending on the codec " - "provided by --codec).", + "provided by --video-codec).", }, { .longopt_id = OPT_FORCE_ADB_FORWARD, @@ -519,6 +522,13 @@ static const struct sc_option options[] = { .longopt = "version", .text = "Print the version of scrcpy.", }, + { + .longopt_id = OPT_VIDEO_CODEC, + .longopt = "video-codec", + .argdesc = "name", + .text = "Select a video codec (h264, h265 or av1).\n" + "Default is h264.", + }, { .shortopt = 'w', .longopt = "stay-awake", @@ -1395,7 +1405,7 @@ guess_record_format(const char *filename) { } static bool -parse_codec(const char *optarg, enum sc_codec *codec) { +parse_video_codec(const char *optarg, enum sc_codec *codec) { if (!strcmp(optarg, "h264")) { *codec = SC_CODEC_H264; return true; @@ -1408,7 +1418,7 @@ parse_codec(const char *optarg, enum sc_codec *codec) { *codec = SC_CODEC_AV1; return true; } - LOGE("Unsupported codec: %s (expected h264, h265 or av1)", optarg); + LOGE("Unsupported video codec: %s (expected h264, h265 or av1)", optarg); return false; } @@ -1649,7 +1659,10 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], opts->start_fps_counter = true; break; case OPT_CODEC: - if (!parse_codec(optarg, &opts->codec)) { + LOGW("--codec is deprecated, use --video-codec instead."); + // fall through + case OPT_VIDEO_CODEC: + if (!parse_video_codec(optarg, &opts->video_codec)) { return false; } break; diff --git a/app/src/options.c b/app/src/options.c index 64ec5b3b..0368ffcc 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -13,7 +13,7 @@ const struct scrcpy_options scrcpy_options_default = { .v4l2_device = NULL, #endif .log_level = SC_LOG_LEVEL_INFO, - .codec = SC_CODEC_H264, + .video_codec = SC_CODEC_H264, .record_format = SC_RECORD_FORMAT_AUTO, .keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_INJECT, .mouse_input_mode = SC_MOUSE_INPUT_MODE_INJECT, diff --git a/app/src/options.h b/app/src/options.h index 7bf30011..f6ba324b 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -99,7 +99,7 @@ struct scrcpy_options { const char *v4l2_device; #endif enum sc_log_level log_level; - enum sc_codec codec; + enum sc_codec video_codec; enum sc_record_format record_format; enum sc_keyboard_input_mode keyboard_input_mode; enum sc_mouse_input_mode mouse_input_mode; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 6750f6a1..35b999a8 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -314,7 +314,7 @@ scrcpy(struct scrcpy_options *options) { .select_usb = options->select_usb, .select_tcpip = options->select_tcpip, .log_level = options->log_level, - .codec = options->codec, + .video_codec = options->video_codec, .crop = options->crop, .port_range = options->port_range, .tunnel_host = options->tunnel_host, diff --git a/app/src/server.c b/app/src/server.c index 6bf0eb6e..20433ea0 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -222,8 +222,9 @@ execute_server(struct sc_server *server, if (!params->audio) { ADD_PARAM("audio=false"); } - if (params->codec != SC_CODEC_H264) { - ADD_PARAM("codec=%s", sc_server_get_codec_name(params->codec)); + if (params->video_codec != SC_CODEC_H264) { + ADD_PARAM("video_codec=%s", + sc_server_get_codec_name(params->video_codec)); } if (params->max_size) { ADD_PARAM("max_size=%" PRIu16, params->max_size); diff --git a/app/src/server.h b/app/src/server.h index 3005ebd2..8914349f 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -25,7 +25,7 @@ struct sc_server_params { uint32_t scid; const char *req_serial; enum sc_log_level log_level; - enum sc_codec codec; + enum sc_codec video_codec; const char *crop; const char *codec_options; const char *encoder_name; diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 07789974..73a303d8 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -10,7 +10,7 @@ public class Options { private int scid = -1; // 31-bit non-negative value, or -1 private boolean audio = true; private int maxSize; - private VideoCodec codec = VideoCodec.H264; + private VideoCodec videoCodec = VideoCodec.H264; private int bitRate = 8000000; private int maxFps; private int lockVideoOrientation = -1; @@ -66,12 +66,12 @@ public class Options { this.maxSize = maxSize; } - public VideoCodec getCodec() { - return codec; + public VideoCodec getVideoCodec() { + return videoCodec; } - public void setCodec(VideoCodec codec) { - this.codec = codec; + public void setVideoCodec(VideoCodec videoCodec) { + this.videoCodec = videoCodec; } public int getBitRate() { diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index fdd23bf3..30e988f0 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -231,7 +231,7 @@ public class ScreenEncoder implements Device.RotationListener { if (encoders != null && encoders.length > 0) { msg.append("\nTry to use one of the available encoders:"); for (MediaCodecInfo encoder : encoders) { - msg.append("\n scrcpy --codec=").append(codec.getName()).append(" --encoder='").append(encoder.getName()).append("'"); + msg.append("\n scrcpy --video-codec=").append(codec.getName()).append(" --encoder='").append(encoder.getName()).append("'"); } } return msg.toString(); diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index eb0c1384..a926f443 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -96,7 +96,6 @@ public final class Server { AudioEncoder audioEncoder = null; try (DesktopConnection connection = DesktopConnection.open(scid, tunnelForward, audio, control, sendDummyByte)) { - VideoCodec codec = options.getCodec(); if (options.getSendDeviceMeta()) { Size videoSize = device.getScreenInfo().getVideoSize(); connection.sendDeviceMeta(Device.getDeviceName(), videoSize.getWidth(), videoSize.getHeight()); @@ -116,9 +115,10 @@ public final class Server { audioEncoder.start(); } - Streamer videoStreamer = new Streamer(connection.getVideoFd(), codec, options.getSendCodecId(), options.getSendFrameMeta()); - ScreenEncoder screenEncoder = new ScreenEncoder(device, videoStreamer, options.getBitRate(), options.getMaxFps(), - codecOptions, options.getEncoderName(), options.getDownsizeOnError()); + Streamer videoStreamer = new Streamer(connection.getVideoFd(), options.getVideoCodec(), options.getSendCodecId(), + options.getSendFrameMeta()); + ScreenEncoder screenEncoder = new ScreenEncoder(device, videoStreamer, options.getBitRate(), options.getMaxFps(), codecOptions, + options.getEncoderName(), options.getDownsizeOnError()); try { // synchronous screenEncoder.streamScreen(); @@ -195,12 +195,12 @@ public final class Server { boolean audio = Boolean.parseBoolean(value); options.setAudio(audio); break; - case "codec": - VideoCodec codec = VideoCodec.findByName(value); - if (codec == null) { + case "video_codec": + VideoCodec videoCodec = VideoCodec.findByName(value); + if (videoCodec == null) { throw new IllegalArgumentException("Video codec " + value + " not supported"); } - options.setCodec(codec); + options.setVideoCodec(videoCodec); break; case "max_size": int maxSize = Integer.parseInt(value) & ~7; // multiple of 8 From 9087e85c3f076c54b81a8895a9936bbb49a41812 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 21 Feb 2023 19:56:44 +0100 Subject: [PATCH 1399/2244] Rename --bit-rate to --video-bit-rate This prepares the introduction of --audio-bit-rate. PR #3757 --- README.md | 4 ++-- app/data/bash-completion/scrcpy | 4 ++-- app/data/zsh-completion/_scrcpy | 2 +- app/scrcpy.1 | 2 +- app/src/cli.c | 14 ++++++++++++-- app/src/options.c | 2 +- app/src/options.h | 2 +- app/src/scrcpy.c | 2 +- app/src/server.c | 4 ++-- app/src/server.h | 2 +- app/tests/test_cli.c | 4 ++-- .../main/java/com/genymobile/scrcpy/Options.java | 10 +++++----- .../java/com/genymobile/scrcpy/ScreenEncoder.java | 8 ++++---- .../main/java/com/genymobile/scrcpy/Server.java | 8 ++++---- 14 files changed, 39 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 4b898d26..81371b92 100644 --- a/README.md +++ b/README.md @@ -199,7 +199,7 @@ preserved. That way, a device in 1920×1080 will be mirrored at 1024×576. The default bit-rate is 8 Mbps. To change the video bitrate (e.g. to 2 Mbps): ```bash -scrcpy --bit-rate=2M +scrcpy --video-bit-rate=2M scrcpy -b 2M # short version ``` @@ -444,7 +444,7 @@ none found, try running `adb disconnect`, and then run those two commands again. It may be useful to decrease the bit-rate and the resolution: ```bash -scrcpy --bit-rate=2M --max-size=800 +scrcpy --video-bit-rate=2M --max-size=800 scrcpy -b2M -m800 # short version ``` diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index d92cf009..1fe79765 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -2,7 +2,7 @@ _scrcpy() { local cur prev words cword local opts=" --always-on-top - -b --bit-rate= + -b --video-bit-rate= --codec-options= --crop= -d --select-usb @@ -104,7 +104,7 @@ _scrcpy() { COMPREPLY=($(compgen -W "$("${ADB:-adb}" devices | awk '$2 == "device" {print $1}')" -- ${cur})) return ;; - -b|--bit-rate \ + -b|--video-bit-rate \ |--codec-options \ |--crop \ |--display \ diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index b9c94e1e..ac3ec023 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -9,7 +9,7 @@ local arguments arguments=( '--always-on-top[Make scrcpy window always on top \(above other windows\)]' - {-b,--bit-rate=}'[Encode the video at the given bit-rate]' + {-b,--video-bit-rate=}'[Encode the video at the given bit-rate]' '--codec-options=[Set a list of comma-separated key\:type=value options for the device encoder]' '--crop=[\[width\:height\:x\:y\] Crop the device screen on the server]' {-d,--select-usb}'[Use USB device]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 32bb8464..41ce28a4 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -20,7 +20,7 @@ provides display and control of Android devices connected on USB (or over TCP/IP Make scrcpy window always on top (above other windows). .TP -.BI "\-b, \-\-bit\-rate " value +.BI "\-b, \-\-video\-bit\-rate " value Encode the video at the given bit\-rate, expressed in bits/s. Unit suffixes are supported: '\fBK\fR' (x1000) and '\fBM\fR' (x1000000). Default is 8M (8000000). diff --git a/app/src/cli.c b/app/src/cli.c index 9163ba60..57e85aa4 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -19,6 +19,7 @@ enum { OPT_RENDER_EXPIRED_FRAMES = 1000, + OPT_BIT_RATE, OPT_WINDOW_TITLE, OPT_PUSH_TARGET, OPT_ALWAYS_ON_TOP, @@ -104,12 +105,18 @@ static const struct sc_option options[] = { }, { .shortopt = 'b', - .longopt = "bit-rate", + .longopt = "video-bit-rate", .argdesc = "value", .text = "Encode the video at the given bit-rate, expressed in bits/s. " "Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n" "Default is 8M (8000000).", }, + { + // deprecated + .longopt_id = OPT_BIT_RATE, + .longopt = "bit-rate", + .argdesc = "value", + }, { // Not really deprecated (--codec has never been released), but without // declaring an explicit --codec option, getopt_long() partial matching @@ -1432,8 +1439,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], int c; while ((c = getopt_long(argc, argv, optstring, longopts, NULL)) != -1) { switch (c) { + case OPT_BIT_RATE: + LOGW("--bit-rate is deprecated, use --video-bit-rate instead."); + // fall through case 'b': - if (!parse_bit_rate(optarg, &opts->bit_rate)) { + if (!parse_bit_rate(optarg, &opts->video_bit_rate)) { return false; } break; diff --git a/app/src/options.c b/app/src/options.c index 0368ffcc..a087f507 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -28,7 +28,7 @@ const struct scrcpy_options scrcpy_options_default = { .count = 2, }, .max_size = 0, - .bit_rate = 0, + .video_bit_rate = 0, .max_fps = 0, .lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_UNLOCKED, .rotation = 0, diff --git a/app/src/options.h b/app/src/options.h index f6ba324b..d22078e4 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -108,7 +108,7 @@ struct scrcpy_options { uint16_t tunnel_port; struct sc_shortcut_mods shortcut_mods; uint16_t max_size; - uint32_t bit_rate; + uint32_t video_bit_rate; uint16_t max_fps; enum sc_lock_video_orientation lock_video_orientation; uint8_t rotation; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 35b999a8..b09de541 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -320,7 +320,7 @@ scrcpy(struct scrcpy_options *options) { .tunnel_host = options->tunnel_host, .tunnel_port = options->tunnel_port, .max_size = options->max_size, - .bit_rate = options->bit_rate, + .video_bit_rate = options->video_bit_rate, .max_fps = options->max_fps, .lock_video_orientation = options->lock_video_orientation, .control = options->control, diff --git a/app/src/server.c b/app/src/server.c index 20433ea0..7c8fba8d 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -216,8 +216,8 @@ execute_server(struct sc_server *server, ADD_PARAM("scid=%08x", params->scid); ADD_PARAM("log_level=%s", log_level_to_server_string(params->log_level)); - if (params->bit_rate) { - ADD_PARAM("bit_rate=%" PRIu32, params->bit_rate); + if (params->video_bit_rate) { + ADD_PARAM("video_bit_rate=%" PRIu32, params->video_bit_rate); } if (!params->audio) { ADD_PARAM("audio=false"); diff --git a/app/src/server.h b/app/src/server.h index 8914349f..d3019288 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -33,7 +33,7 @@ struct sc_server_params { uint32_t tunnel_host; uint16_t tunnel_port; uint16_t max_size; - uint32_t bit_rate; + uint32_t video_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 5ea54b7f..3e9a248a 100644 --- a/app/tests/test_cli.c +++ b/app/tests/test_cli.c @@ -46,7 +46,7 @@ static void test_options(void) { char *argv[] = { "scrcpy", "--always-on-top", - "--bit-rate", "5M", + "--video-bit-rate", "5M", "--crop", "100:200:300:400", "--fullscreen", "--max-fps", "30", @@ -75,7 +75,7 @@ static void test_options(void) { const struct scrcpy_options *opts = &args.opts; assert(opts->always_on_top); - assert(opts->bit_rate == 5000000); + assert(opts->video_bit_rate == 5000000); assert(!strcmp(opts->crop, "100:200:300:400")); assert(opts->fullscreen); assert(opts->max_fps == 30); diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 73a303d8..53257cf3 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -11,7 +11,7 @@ public class Options { private boolean audio = true; private int maxSize; private VideoCodec videoCodec = VideoCodec.H264; - private int bitRate = 8000000; + private int videoBitRate = 8000000; private int maxFps; private int lockVideoOrientation = -1; private boolean tunnelForward; @@ -74,12 +74,12 @@ public class Options { this.videoCodec = videoCodec; } - public int getBitRate() { - return bitRate; + public int getVideoBitRate() { + return videoBitRate; } - public void setBitRate(int bitRate) { - this.bitRate = bitRate; + public void setVideoBitRate(int videoBitRate) { + this.videoBitRate = videoBitRate; } public int getMaxFps() { diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index 30e988f0..d646995b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -35,18 +35,18 @@ public class ScreenEncoder implements Device.RotationListener { private final Streamer streamer; private final String encoderName; private final List codecOptions; - private final int bitRate; + private final int videoBitRate; private final int maxFps; private final boolean downsizeOnError; private boolean firstFrameSent; private int consecutiveErrors; - public ScreenEncoder(Device device, Streamer streamer, int bitRate, int maxFps, List codecOptions, String encoderName, + public ScreenEncoder(Device device, Streamer streamer, int videoBitRate, int maxFps, List codecOptions, String encoderName, boolean downsizeOnError) { this.device = device; this.streamer = streamer; - this.bitRate = bitRate; + this.videoBitRate = videoBitRate; this.maxFps = maxFps; this.codecOptions = codecOptions; this.encoderName = encoderName; @@ -65,7 +65,7 @@ public class ScreenEncoder implements Device.RotationListener { public void streamScreen() throws IOException, ConfigurationException { Codec codec = streamer.getCodec(); MediaCodec mediaCodec = createMediaCodec(codec, encoderName); - MediaFormat format = createFormat(codec.getMimeType(), bitRate, maxFps, codecOptions); + MediaFormat format = createFormat(codec.getMimeType(), videoBitRate, maxFps, codecOptions); IBinder display = createDisplay(); device.setRotationListener(this); diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index a926f443..f764804c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -117,7 +117,7 @@ public final class Server { Streamer videoStreamer = new Streamer(connection.getVideoFd(), options.getVideoCodec(), options.getSendCodecId(), options.getSendFrameMeta()); - ScreenEncoder screenEncoder = new ScreenEncoder(device, videoStreamer, options.getBitRate(), options.getMaxFps(), codecOptions, + ScreenEncoder screenEncoder = new ScreenEncoder(device, videoStreamer, options.getVideoBitRate(), options.getMaxFps(), codecOptions, options.getEncoderName(), options.getDownsizeOnError()); try { // synchronous @@ -206,9 +206,9 @@ public final class Server { int maxSize = Integer.parseInt(value) & ~7; // multiple of 8 options.setMaxSize(maxSize); break; - case "bit_rate": - int bitRate = Integer.parseInt(value); - options.setBitRate(bitRate); + case "video_bit_rate": + int videoBitRate = Integer.parseInt(value); + options.setVideoBitRate(videoBitRate); break; case "max_fps": int maxFps = Integer.parseInt(value); From 31555fa5309b80f6e65a19dc3aa8932fe9265b8d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 21 Feb 2023 21:46:34 +0100 Subject: [PATCH 1400/2244] Rename --codec-options to --video-codec-options This prepares the introduction of --audio-codec-options. PR #3757 --- app/data/bash-completion/scrcpy | 2 +- app/data/zsh-completion/_scrcpy | 2 +- app/scrcpy.1 | 20 +++++++------- app/src/cli.c | 27 +++++++++++++------ app/src/options.c | 2 +- app/src/options.h | 2 +- app/src/scrcpy.c | 2 +- app/src/server.c | 8 +++--- app/src/server.h | 2 +- .../java/com/genymobile/scrcpy/Options.java | 10 +++---- .../java/com/genymobile/scrcpy/Server.java | 11 ++++---- 11 files changed, 49 insertions(+), 39 deletions(-) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index 1fe79765..167f736f 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -3,7 +3,6 @@ _scrcpy() { local opts=" --always-on-top -b --video-bit-rate= - --codec-options= --crop= -d --select-usb --disable-screensaver @@ -55,6 +54,7 @@ _scrcpy() { -V --verbosity= -v --version --video-codec= + --video-codec-options= -w --stay-awake --window-borderless --window-title= diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index ac3ec023..29bec42d 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -10,7 +10,6 @@ local arguments arguments=( '--always-on-top[Make scrcpy window always on top \(above other windows\)]' {-b,--video-bit-rate=}'[Encode the video at the given bit-rate]' - '--codec-options=[Set a list of comma-separated key\:type=value options for the device encoder]' '--crop=[\[width\:height\:x\:y\] Crop the device screen on the server]' {-d,--select-usb}'[Use USB device]' '--disable-screensaver[Disable screensaver while scrcpy is running]' @@ -60,6 +59,7 @@ arguments=( {-V,--verbosity=}'[Set the log level]:verbosity:(verbose debug info warn error)' {-v,--version}'[Print the version of scrcpy]' '--video-codec=[Select the video codec]:codec:(h264 h265 av1)' + '--video-codec-options=[Set a list of comma-separated key\:type=value options for the device video encoder]' {-w,--stay-awake}'[Keep the device on while scrcpy is running, when the device is plugged in]' '--window-borderless[Disable window decorations \(display borderless window\)]' '--window-title=[Set a custom window title]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 41ce28a4..49d05a14 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -25,16 +25,6 @@ Encode the video at the given bit\-rate, expressed in bits/s. Unit suffixes are Default is 8M (8000000). -.TP -.BI "\-\-codec\-options " key\fR[:\fItype\fR]=\fIvalue\fR[,...] -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. @@ -329,6 +319,16 @@ Select a video codec (h264, h265 or av1). Default is h264. +.TP +.BI "\-\-video\-codec\-options " key\fR[:\fItype\fR]=\fIvalue\fR[,...] +Set a list of comma-separated key:type=value options for the device video 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 .B \-w, \-\-stay-awake Keep the device on while scrcpy is running, when the device is plugged in. diff --git a/app/src/cli.c b/app/src/cli.c index 57e85aa4..cb1af46e 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -38,6 +38,7 @@ enum { OPT_RENDER_DRIVER, OPT_NO_MIPMAPS, OPT_CODEC_OPTIONS, + OPT_VIDEO_CODEC_OPTIONS, OPT_FORCE_ADB_FORWARD, OPT_DISABLE_SCREENSAVER, OPT_SHORTCUT_MOD, @@ -127,16 +128,10 @@ static const struct sc_option options[] = { .argdesc = "value", }, { + // deprecated .longopt_id = OPT_CODEC_OPTIONS, .longopt = "codec-options", .argdesc = "key[:type]=value[,...]", - .text = "Set a list of comma-separated key:type=value options for the " - "device encoder.\n" - "The possible values for 'type' are 'int' (default), 'long', " - "'float' and 'string'.\n" - "The list of possible codec options is available in the " - "Android documentation: " - "", }, { .longopt_id = OPT_CROP, @@ -536,6 +531,18 @@ static const struct sc_option options[] = { .text = "Select a video codec (h264, h265 or av1).\n" "Default is h264.", }, + { + .longopt_id = OPT_VIDEO_CODEC_OPTIONS, + .longopt = "video-codec-options", + .argdesc = "key[:type]=value[,...]", + .text = "Set a list of comma-separated key:type=value options for the " + "device video encoder.\n" + "The possible values for 'type' are 'int' (default), 'long', " + "'float' and 'string'.\n" + "The list of possible codec options is available in the " + "Android documentation: " + "", + }, { .shortopt = 'w', .longopt = "stay-awake", @@ -1616,7 +1623,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], opts->forward_key_repeat = false; break; case OPT_CODEC_OPTIONS: - opts->codec_options = optarg; + LOGW("--codec-options is deprecated, use --video-codec-options " + "instead."); + // fall through + case OPT_VIDEO_CODEC_OPTIONS: + opts->video_codec_options = optarg; break; case OPT_ENCODER_NAME: opts->encoder_name = optarg; diff --git a/app/src/options.c b/app/src/options.c index a087f507..0547da1b 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -7,7 +7,7 @@ const struct scrcpy_options scrcpy_options_default = { .window_title = NULL, .push_target = NULL, .render_driver = NULL, - .codec_options = NULL, + .video_codec_options = NULL, .encoder_name = NULL, #ifdef HAVE_V4L2 .v4l2_device = NULL, diff --git a/app/src/options.h b/app/src/options.h index d22078e4..bde79687 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -93,7 +93,7 @@ struct scrcpy_options { const char *window_title; const char *push_target; const char *render_driver; - const char *codec_options; + const char *video_codec_options; const char *encoder_name; #ifdef HAVE_V4L2 const char *v4l2_device; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index b09de541..2bb5794c 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -328,7 +328,7 @@ scrcpy(struct scrcpy_options *options) { .audio = options->audio, .show_touches = options->show_touches, .stay_awake = options->stay_awake, - .codec_options = options->codec_options, + .video_codec_options = options->video_codec_options, .encoder_name = options->encoder_name, .force_adb_forward = options->force_adb_forward, .power_off_on_close = options->power_off_on_close, diff --git a/app/src/server.c b/app/src/server.c index 7c8fba8d..eb91b2b2 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -71,7 +71,7 @@ sc_server_params_destroy(struct sc_server_params *params) { // The server stores a copy of the params provided by the user free((char *) params->req_serial); free((char *) params->crop); - free((char *) params->codec_options); + free((char *) params->video_codec_options); free((char *) params->encoder_name); free((char *) params->tcpip_dst); } @@ -95,7 +95,7 @@ sc_server_params_copy(struct sc_server_params *dst, COPY(req_serial); COPY(crop); - COPY(codec_options); + COPY(video_codec_options); COPY(encoder_name); COPY(tcpip_dst); #undef COPY @@ -255,8 +255,8 @@ execute_server(struct sc_server *server, if (params->stay_awake) { ADD_PARAM("stay_awake=true"); } - if (params->codec_options) { - ADD_PARAM("codec_options=%s", params->codec_options); + if (params->video_codec_options) { + ADD_PARAM("video_codec_options=%s", params->video_codec_options); } if (params->encoder_name) { ADD_PARAM("encoder_name=%s", params->encoder_name); diff --git a/app/src/server.h b/app/src/server.h index d3019288..352d2cae 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -27,7 +27,7 @@ struct sc_server_params { enum sc_log_level log_level; enum sc_codec video_codec; const char *crop; - const char *codec_options; + const char *video_codec_options; const char *encoder_name; struct sc_port_range port_range; uint32_t tunnel_host; diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 53257cf3..9cfb7871 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -20,7 +20,7 @@ public class Options { private int displayId; private boolean showTouches; private boolean stayAwake; - private List codecOptions; + private List videoCodecOptions; private String encoderName; private boolean powerOffScreenOnClose; private boolean clipboardAutosync = true; @@ -146,12 +146,12 @@ public class Options { this.stayAwake = stayAwake; } - public List getCodecOptions() { - return codecOptions; + public List getVideoCodecOptions() { + return videoCodecOptions; } - public void setCodecOptions(List codecOptions) { - this.codecOptions = codecOptions; + public void setVideoCodecOptions(List videoCodecOptions) { + this.videoCodecOptions = videoCodecOptions; } public String getEncoderName() { diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index f764804c..4ff938b4 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -61,7 +61,6 @@ public final class Server { private static void scrcpy(Options options) throws IOException, ConfigurationException { Ln.i("Device: " + Build.MANUFACTURER + " " + Build.MODEL + " (Android " + Build.VERSION.RELEASE + ")"); final Device device = new Device(options); - List codecOptions = options.getCodecOptions(); Thread initThread = startInitThread(options); @@ -117,8 +116,8 @@ public final class Server { Streamer videoStreamer = new Streamer(connection.getVideoFd(), options.getVideoCodec(), options.getSendCodecId(), options.getSendFrameMeta()); - ScreenEncoder screenEncoder = new ScreenEncoder(device, videoStreamer, options.getVideoBitRate(), options.getMaxFps(), codecOptions, - options.getEncoderName(), options.getDownsizeOnError()); + ScreenEncoder screenEncoder = new ScreenEncoder(device, videoStreamer, options.getVideoBitRate(), options.getMaxFps(), + options.getVideoCodecOptions(), options.getEncoderName(), options.getDownsizeOnError()); try { // synchronous screenEncoder.streamScreen(); @@ -242,9 +241,9 @@ public final class Server { boolean stayAwake = Boolean.parseBoolean(value); options.setStayAwake(stayAwake); break; - case "codec_options": - List codecOptions = CodecOption.parse(value); - options.setCodecOptions(codecOptions); + case "video_codec_options": + List videoCodecOptions = CodecOption.parse(value); + options.setVideoCodecOptions(videoCodecOptions); break; case "encoder_name": if (!value.isEmpty()) { From e694619d53d1f38ed02b269be1649c7a67a11916 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 22 Feb 2023 22:44:01 +0100 Subject: [PATCH 1401/2244] Rename --encoder to --video-encoder This prepares the introduction of --audio-encoder. PR #3757 --- README.md | 6 ++--- app/data/bash-completion/scrcpy | 2 +- app/data/zsh-completion/_scrcpy | 2 +- app/scrcpy.1 | 8 +++---- app/src/cli.c | 22 ++++++++++++++----- app/src/options.c | 2 +- app/src/options.h | 2 +- app/src/scrcpy.c | 2 +- app/src/server.c | 8 +++---- app/src/server.h | 2 +- .../java/com/genymobile/scrcpy/Options.java | 10 ++++----- .../java/com/genymobile/scrcpy/Server.java | 6 ++--- 12 files changed, 41 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 81371b92..8eabafa9 100644 --- a/README.md +++ b/README.md @@ -270,15 +270,15 @@ Some devices have more than one encoder for a specific codec, and some of them may cause issues or crash. It is possible to select a different encoder: ```bash -scrcpy --encoder=OMX.qcom.video.encoder.avc +scrcpy --video-encoder=OMX.qcom.video.encoder.avc ``` To list the available encoders, you can pass an invalid encoder name; the error will give the available encoders: ```bash -scrcpy --encoder=_ # for the default codec -scrcpy --video-codec=h265 --encoder=_ # for a specific codec +scrcpy --video-encoder=_ # for the default codec +scrcpy --video-codec=h265 --video-encoder=_ # for a specific codec ``` ### Capture diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index 167f736f..450bd32d 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -9,7 +9,6 @@ _scrcpy() { --display= --display-buffer= -e --select-tcpip - --encoder= --force-adb-forward --forward-all-clicks -f --fullscreen @@ -55,6 +54,7 @@ _scrcpy() { -v --version --video-codec= --video-codec-options= + --video-encoder= -w --stay-awake --window-borderless --window-title= diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 29bec42d..86d9ffbf 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -16,7 +16,6 @@ arguments=( '--display=[Specify the display id to mirror]' '--display-buffer=[Add a buffering delay \(in milliseconds\) before displaying]' {-e,--select-tcpip}'[Use TCP/IP device]' - '--encoder=[Use a specific MediaCodec encoder]' '--force-adb-forward[Do not attempt to use \"adb reverse\" to connect to the device]' '--forward-all-clicks[Forward clicks to device]' {-f,--fullscreen}'[Start in fullscreen]' @@ -60,6 +59,7 @@ arguments=( {-v,--version}'[Print the version of scrcpy]' '--video-codec=[Select the video codec]:codec:(h264 h265 av1)' '--video-codec-options=[Set a list of comma-separated key\:type=value options for the device video encoder]' + '--video-encoder=[Use a specific MediaCodec video encoder]' {-w,--stay-awake}'[Keep the device on while scrcpy is running, when the device is plugged in]' '--window-borderless[Disable window decorations \(display borderless window\)]' '--window-title=[Set a custom window title]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 49d05a14..34bb750e 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -64,10 +64,6 @@ Use TCP/IP device (if there is exactly one, like adb -e). Also see \fB\-d\fR (\fB\-\-select\-usb\fR). -.TP -.BI "\-\-encoder " name -Use a specific MediaCodec encoder (depending on the codec provided by \fB\-\-video\-codec\fR). - .TP .B \-\-force\-adb\-forward Do not attempt to use "adb reverse" to connect to the device. @@ -329,6 +325,10 @@ The list of possible codec options is available in the Android documentation .UR https://d.android.com/reference/android/media/MediaFormat .UE . +.TP +.BI "\-\-video\-encoder " name +Use a specific MediaCodec video encoder (depending on the codec provided by \fB\-\-video\-codec\fR). + .TP .B \-w, \-\-stay-awake Keep the device on while scrcpy is running, when the device is plugged in. diff --git a/app/src/cli.c b/app/src/cli.c index cb1af46e..685c2f3d 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -45,7 +45,8 @@ enum { OPT_NO_KEY_REPEAT, OPT_FORWARD_ALL_CLICKS, OPT_LEGACY_PASTE, - OPT_ENCODER_NAME, + OPT_ENCODER, + OPT_VIDEO_ENCODER, OPT_POWER_OFF_ON_CLOSE, OPT_V4L2_SINK, OPT_DISPLAY_BUFFER, @@ -178,11 +179,10 @@ static const struct sc_option options[] = { "Also see -d (--select-usb).", }, { - .longopt_id = OPT_ENCODER_NAME, + // deprecated + .longopt_id = OPT_ENCODER, .longopt = "encoder", .argdesc = "name", - .text = "Use a specific MediaCodec encoder (depending on the codec " - "provided by --video-codec).", }, { .longopt_id = OPT_FORCE_ADB_FORWARD, @@ -543,6 +543,13 @@ static const struct sc_option options[] = { "Android documentation: " "", }, + { + .longopt_id = OPT_VIDEO_ENCODER, + .longopt = "video-encoder", + .argdesc = "name", + .text = "Use a specific MediaCodec video encoder (depending on the " + "codec provided by --video-codec).", + }, { .shortopt = 'w', .longopt = "stay-awake", @@ -1629,8 +1636,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], case OPT_VIDEO_CODEC_OPTIONS: opts->video_codec_options = optarg; break; - case OPT_ENCODER_NAME: - opts->encoder_name = optarg; + case OPT_ENCODER: + LOGW("--encoder is deprecated, use --video-encoder instead."); + // fall through + case OPT_VIDEO_ENCODER: + opts->video_encoder = optarg; break; case OPT_FORCE_ADB_FORWARD: opts->force_adb_forward = true; diff --git a/app/src/options.c b/app/src/options.c index 0547da1b..fa025dea 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -8,7 +8,7 @@ const struct scrcpy_options scrcpy_options_default = { .push_target = NULL, .render_driver = NULL, .video_codec_options = NULL, - .encoder_name = NULL, + .video_encoder = NULL, #ifdef HAVE_V4L2 .v4l2_device = NULL, #endif diff --git a/app/src/options.h b/app/src/options.h index bde79687..3c602b7e 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -94,7 +94,7 @@ struct scrcpy_options { const char *push_target; const char *render_driver; const char *video_codec_options; - const char *encoder_name; + const char *video_encoder; #ifdef HAVE_V4L2 const char *v4l2_device; #endif diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 2bb5794c..776f5d13 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -329,7 +329,7 @@ scrcpy(struct scrcpy_options *options) { .show_touches = options->show_touches, .stay_awake = options->stay_awake, .video_codec_options = options->video_codec_options, - .encoder_name = options->encoder_name, + .video_encoder = options->video_encoder, .force_adb_forward = options->force_adb_forward, .power_off_on_close = options->power_off_on_close, .clipboard_autosync = options->clipboard_autosync, diff --git a/app/src/server.c b/app/src/server.c index eb91b2b2..583c338e 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -72,7 +72,7 @@ sc_server_params_destroy(struct sc_server_params *params) { free((char *) params->req_serial); free((char *) params->crop); free((char *) params->video_codec_options); - free((char *) params->encoder_name); + free((char *) params->video_encoder); free((char *) params->tcpip_dst); } @@ -96,7 +96,7 @@ sc_server_params_copy(struct sc_server_params *dst, COPY(req_serial); COPY(crop); COPY(video_codec_options); - COPY(encoder_name); + COPY(video_encoder); COPY(tcpip_dst); #undef COPY @@ -258,8 +258,8 @@ execute_server(struct sc_server *server, if (params->video_codec_options) { ADD_PARAM("video_codec_options=%s", params->video_codec_options); } - if (params->encoder_name) { - ADD_PARAM("encoder_name=%s", params->encoder_name); + if (params->video_encoder) { + ADD_PARAM("video_encoder=%s", params->video_encoder); } if (params->power_off_on_close) { ADD_PARAM("power_off_on_close=true"); diff --git a/app/src/server.h b/app/src/server.h index 352d2cae..97c9aea2 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -28,7 +28,7 @@ struct sc_server_params { enum sc_codec video_codec; const char *crop; const char *video_codec_options; - const char *encoder_name; + const char *video_encoder; struct sc_port_range port_range; uint32_t tunnel_host; uint16_t tunnel_port; diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 9cfb7871..c518bf07 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -21,7 +21,7 @@ public class Options { private boolean showTouches; private boolean stayAwake; private List videoCodecOptions; - private String encoderName; + private String videoEncoder; private boolean powerOffScreenOnClose; private boolean clipboardAutosync = true; private boolean downsizeOnError = true; @@ -154,12 +154,12 @@ public class Options { this.videoCodecOptions = videoCodecOptions; } - public String getEncoderName() { - return encoderName; + public String getVideoEncoder() { + return videoEncoder; } - public void setEncoderName(String encoderName) { - this.encoderName = encoderName; + public void setVideoEncoder(String videoEncoder) { + this.videoEncoder = videoEncoder; } public void setPowerOffScreenOnClose(boolean powerOffScreenOnClose) { diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 4ff938b4..b809e90f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -117,7 +117,7 @@ public final class Server { Streamer videoStreamer = new Streamer(connection.getVideoFd(), options.getVideoCodec(), options.getSendCodecId(), options.getSendFrameMeta()); ScreenEncoder screenEncoder = new ScreenEncoder(device, videoStreamer, options.getVideoBitRate(), options.getMaxFps(), - options.getVideoCodecOptions(), options.getEncoderName(), options.getDownsizeOnError()); + options.getVideoCodecOptions(), options.getVideoEncoder(), options.getDownsizeOnError()); try { // synchronous screenEncoder.streamScreen(); @@ -245,9 +245,9 @@ public final class Server { List videoCodecOptions = CodecOption.parse(value); options.setVideoCodecOptions(videoCodecOptions); break; - case "encoder_name": + case "video_encoder": if (!value.isEmpty()) { - options.setEncoderName(value); + options.setVideoEncoder(value); } break; case "power_off_on_close": From 8e640dc90f66e9b7f6b22feeb7e5bc9d2bb3e6f0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 19 Feb 2023 02:27:39 +0100 Subject: [PATCH 1402/2244] Disable MethodLength checkstyle on createOptions() This method will grow as needed to initialize options. PR #3757 --- 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 b809e90f..800b2fb6 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -157,6 +157,7 @@ public final class Server { return thread; } + @SuppressWarnings("MethodLength") private static Options createOptions(String... args) { if (args.length < 1) { throw new IllegalArgumentException("Missing client version"); From 0870b8c8be18261b0930721dfa8e9f5454bd3081 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 18 Feb 2023 18:32:43 +0100 Subject: [PATCH 1403/2244] Add --audio-bit-rate Add an option to configure the audio bit-rate. PR #3757 --- app/data/bash-completion/scrcpy | 1 + app/data/zsh-completion/_scrcpy | 1 + app/scrcpy.1 | 6 ++++++ app/src/cli.c | 14 ++++++++++++++ app/src/options.c | 1 + app/src/options.h | 1 + app/src/scrcpy.c | 1 + app/src/server.c | 2 ++ app/src/server.h | 1 + .../java/com/genymobile/scrcpy/AudioEncoder.java | 11 ++++++----- .../main/java/com/genymobile/scrcpy/Options.java | 9 +++++++++ .../main/java/com/genymobile/scrcpy/Server.java | 6 +++++- 12 files changed, 48 insertions(+), 6 deletions(-) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index 450bd32d..02ade8d0 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -2,6 +2,7 @@ _scrcpy() { local cur prev words cword local opts=" --always-on-top + --audio-bit-rate= -b --video-bit-rate= --crop= -d --select-usb diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 86d9ffbf..28d017e3 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -9,6 +9,7 @@ local arguments arguments=( '--always-on-top[Make scrcpy window always on top \(above other windows\)]' + '--audio-bit-rate=[Encode the audio at the given bit-rate]' {-b,--video-bit-rate=}'[Encode the video at the given bit-rate]' '--crop=[\[width\:height\:x\:y\] Crop the device screen on the server]' {-d,--select-usb}'[Use USB device]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 34bb750e..7c11f6e5 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -19,6 +19,12 @@ provides display and control of Android devices connected on USB (or over TCP/IP .B \-\-always\-on\-top Make scrcpy window always on top (above other windows). +.TP +.BI "\-\-audio\-bit\-rate " value +Encode the audio at the given bit\-rate, expressed in bits/s. Unit suffixes are supported: '\fBK\fR' (x1000) and '\fBM\fR' (x1000000). + +Default is 128K (128000). + .TP .BI "\-b, \-\-video\-bit\-rate " value Encode the video at the given bit\-rate, expressed in bits/s. Unit suffixes are supported: '\fBK\fR' (x1000) and '\fBM\fR' (x1000000). diff --git a/app/src/cli.c b/app/src/cli.c index 685c2f3d..7187b878 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -64,6 +64,7 @@ enum { OPT_CODEC, OPT_VIDEO_CODEC, OPT_NO_AUDIO, + OPT_AUDIO_BIT_RATE, }; struct sc_option { @@ -105,6 +106,14 @@ static const struct sc_option options[] = { .longopt = "always-on-top", .text = "Make scrcpy window always on top (above other windows).", }, + { + .longopt_id = OPT_AUDIO_BIT_RATE, + .longopt = "audio-bit-rate", + .argdesc = "value", + .text = "Encode the audio at the given bit-rate, expressed in bits/s. " + "Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n" + "Default is 128K (128000).", + }, { .shortopt = 'b', .longopt = "video-bit-rate", @@ -1461,6 +1470,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; } break; + case OPT_AUDIO_BIT_RATE: + if (!parse_bit_rate(optarg, &opts->audio_bit_rate)) { + return false; + } + break; case OPT_CROP: opts->crop = optarg; break; diff --git a/app/src/options.c b/app/src/options.c index fa025dea..70d26a6f 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -29,6 +29,7 @@ const struct scrcpy_options scrcpy_options_default = { }, .max_size = 0, .video_bit_rate = 0, + .audio_bit_rate = 0, .max_fps = 0, .lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_UNLOCKED, .rotation = 0, diff --git a/app/src/options.h b/app/src/options.h index 3c602b7e..92a53653 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -109,6 +109,7 @@ struct scrcpy_options { struct sc_shortcut_mods shortcut_mods; uint16_t max_size; uint32_t video_bit_rate; + uint32_t audio_bit_rate; uint16_t max_fps; enum sc_lock_video_orientation lock_video_orientation; uint8_t rotation; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 776f5d13..478f0e87 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -321,6 +321,7 @@ scrcpy(struct scrcpy_options *options) { .tunnel_port = options->tunnel_port, .max_size = options->max_size, .video_bit_rate = options->video_bit_rate, + .audio_bit_rate = options->audio_bit_rate, .max_fps = options->max_fps, .lock_video_orientation = options->lock_video_orientation, .control = options->control, diff --git a/app/src/server.c b/app/src/server.c index 583c338e..fa8d8300 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -221,6 +221,8 @@ execute_server(struct sc_server *server, } if (!params->audio) { ADD_PARAM("audio=false"); + } else if (params->audio_bit_rate) { + ADD_PARAM("audio_bit_rate=%" PRIu32, params->audio_bit_rate); } if (params->video_codec != SC_CODEC_H264) { ADD_PARAM("video_codec=%s", diff --git a/app/src/server.h b/app/src/server.h index 97c9aea2..805bdaf2 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -34,6 +34,7 @@ struct sc_server_params { uint16_t tunnel_port; uint16_t max_size; uint32_t video_bit_rate; + uint32_t audio_bit_rate; uint16_t max_fps; int8_t lock_video_orientation; bool control; diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java b/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java index d06898d6..5704f768 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java @@ -44,12 +44,12 @@ public final class AudioEncoder { private static final int CHANNELS = 2; private static final int FORMAT = AudioFormat.ENCODING_PCM_16BIT; private static final int BYTES_PER_SAMPLE = 2; - private static final int BIT_RATE = 128000; private static final int READ_MS = 5; // milliseconds private static final int READ_SIZE = SAMPLE_RATE * CHANNELS * BYTES_PER_SAMPLE * READ_MS / 1000; private final Streamer streamer; + private final int bitRate; // Capacity of 64 is in practice "infinite" (it is limited by the number of available MediaCodec buffers, typically 4). // So many pending tasks would lead to an unacceptable delay anyway. @@ -64,8 +64,9 @@ public final class AudioEncoder { private boolean ended; - public AudioEncoder(Streamer streamer) { + public AudioEncoder(Streamer streamer, int bitRate) { this.streamer = streamer; + this.bitRate = bitRate; } private static AudioFormat createAudioFormat() { @@ -92,10 +93,10 @@ public final class AudioEncoder { return builder.build(); } - private static MediaFormat createFormat() { + private static MediaFormat createFormat(int bitRate) { MediaFormat format = new MediaFormat(); format.setString(MediaFormat.KEY_MIME, MIMETYPE); - format.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE); + format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate); format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, CHANNELS); format.setInteger(MediaFormat.KEY_SAMPLE_RATE, SAMPLE_RATE); return format; @@ -220,7 +221,7 @@ public final class AudioEncoder { mediaCodecThread = new HandlerThread("AudioEncoder"); mediaCodecThread.start(); - MediaFormat format = createFormat(); + MediaFormat format = createFormat(bitRate); mediaCodec.setCallback(new EncoderCallback(), new Handler(mediaCodecThread.getLooper())); mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index c518bf07..44bc73ec 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 int maxSize; private VideoCodec videoCodec = VideoCodec.H264; private int videoBitRate = 8000000; + private int audioBitRate = 128000; private int maxFps; private int lockVideoOrientation = -1; private boolean tunnelForward; @@ -82,6 +83,14 @@ public class Options { this.videoBitRate = videoBitRate; } + public int getAudioBitRate() { + return audioBitRate; + } + + public void setAudioBitRate(int audioBitRate) { + this.audioBitRate = audioBitRate; + } + public int getMaxFps() { return maxFps; } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 800b2fb6..c10e3209 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -110,7 +110,7 @@ public final class Server { if (audio) { Streamer audioStreamer = new Streamer(connection.getAudioFd(), AudioCodec.OPUS, options.getSendCodecId(), options.getSendFrameMeta()); - audioEncoder = new AudioEncoder(audioStreamer); + audioEncoder = new AudioEncoder(audioStreamer, options.getAudioBitRate()); audioEncoder.start(); } @@ -210,6 +210,10 @@ public final class Server { int videoBitRate = Integer.parseInt(value); options.setVideoBitRate(videoBitRate); break; + case "audio_bit_rate": + int audioBitRate = Integer.parseInt(value); + options.setAudioBitRate(audioBitRate); + break; case "max_fps": int maxFps = Integer.parseInt(value); options.setMaxFps(maxFps); From 839b842aa71a848510d9407dacc7742c61a17fea Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 18 Feb 2023 19:05:43 +0100 Subject: [PATCH 1404/2244] Add --audio-codec Introduce the selection mechanism. Alternative codecs will be added later. PR #3757 --- app/data/bash-completion/scrcpy | 5 ++++ app/data/zsh-completion/_scrcpy | 1 + app/scrcpy.1 | 6 +++++ app/src/cli.c | 23 +++++++++++++++++++ app/src/options.c | 1 + app/src/options.h | 2 ++ app/src/scrcpy.c | 1 + app/src/server.c | 6 +++++ app/src/server.h | 1 + .../com/genymobile/scrcpy/AudioEncoder.java | 10 ++++---- .../java/com/genymobile/scrcpy/Options.java | 9 ++++++++ .../java/com/genymobile/scrcpy/Server.java | 10 +++++++- 12 files changed, 69 insertions(+), 6 deletions(-) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index 02ade8d0..5a50f6c5 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -3,6 +3,7 @@ _scrcpy() { local opts=" --always-on-top --audio-bit-rate= + --audio-codec= -b --video-bit-rate= --crop= -d --select-usb @@ -71,6 +72,10 @@ _scrcpy() { COMPREPLY=($(compgen -W 'h264 h265 av1' -- "$cur")) return ;; + --audio-codec) + COMPREPLY=($(compgen -W 'opus' -- "$cur")) + return + ;; --lock-video-orientation) COMPREPLY=($(compgen -W 'unlocked initial 0 1 2 3' -- "$cur")) return diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 28d017e3..4f7ad5ef 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -10,6 +10,7 @@ local arguments arguments=( '--always-on-top[Make scrcpy window always on top \(above other windows\)]' '--audio-bit-rate=[Encode the audio at the given bit-rate]' + '--audio-codec=[Select the audio codec]:codec:(opus)' {-b,--video-bit-rate=}'[Encode the video at the given bit-rate]' '--crop=[\[width\:height\:x\:y\] Crop the device screen on the server]' {-d,--select-usb}'[Use USB device]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 7c11f6e5..89533a1f 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -25,6 +25,12 @@ Encode the audio at the given bit\-rate, expressed in bits/s. Unit suffixes are Default is 128K (128000). +.TP +.BI "\-\-audio\-codec " name +Select an audio codec (opus). + +Default is opus. + .TP .BI "\-b, \-\-video\-bit\-rate " value Encode the video at the given bit\-rate, expressed in bits/s. Unit suffixes are supported: '\fBK\fR' (x1000) and '\fBM\fR' (x1000000). diff --git a/app/src/cli.c b/app/src/cli.c index 7187b878..5f28164e 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -65,6 +65,7 @@ enum { OPT_VIDEO_CODEC, OPT_NO_AUDIO, OPT_AUDIO_BIT_RATE, + OPT_AUDIO_CODEC, }; struct sc_option { @@ -114,6 +115,13 @@ static const struct sc_option options[] = { "Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n" "Default is 128K (128000).", }, + { + .longopt_id = OPT_AUDIO_CODEC, + .longopt = "audio-codec", + .argdesc = "name", + .text = "Select an audio codec (opus).\n" + "Default is opus.", + }, { .shortopt = 'b', .longopt = "video-bit-rate", @@ -1452,6 +1460,16 @@ parse_video_codec(const char *optarg, enum sc_codec *codec) { return false; } +static bool +parse_audio_codec(const char *optarg, enum sc_codec *codec) { + if (!strcmp(optarg, "opus")) { + *codec = SC_CODEC_OPUS; + return true; + } + LOGE("Unsupported audio codec: %s (expected opus)", optarg); + return false; +} + static bool parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], const char *optstring, const struct option *longopts) { @@ -1711,6 +1729,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; } break; + case OPT_AUDIO_CODEC: + if (!parse_audio_codec(optarg, &opts->audio_codec)) { + return false; + } + break; case OPT_OTG: #ifdef HAVE_USB opts->otg = true; diff --git a/app/src/options.c b/app/src/options.c index 70d26a6f..72f34e43 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -14,6 +14,7 @@ const struct scrcpy_options scrcpy_options_default = { #endif .log_level = SC_LOG_LEVEL_INFO, .video_codec = SC_CODEC_H264, + .audio_codec = SC_CODEC_OPUS, .record_format = SC_RECORD_FORMAT_AUTO, .keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_INJECT, .mouse_input_mode = SC_MOUSE_INPUT_MODE_INJECT, diff --git a/app/src/options.h b/app/src/options.h index 92a53653..c698e6e3 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -27,6 +27,7 @@ enum sc_codec { SC_CODEC_H264, SC_CODEC_H265, SC_CODEC_AV1, + SC_CODEC_OPUS, }; enum sc_lock_video_orientation { @@ -100,6 +101,7 @@ struct scrcpy_options { #endif enum sc_log_level log_level; enum sc_codec video_codec; + enum sc_codec audio_codec; enum sc_record_format record_format; enum sc_keyboard_input_mode keyboard_input_mode; enum sc_mouse_input_mode mouse_input_mode; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 478f0e87..8b96477c 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -315,6 +315,7 @@ scrcpy(struct scrcpy_options *options) { .select_tcpip = options->select_tcpip, .log_level = options->log_level, .video_codec = options->video_codec, + .audio_codec = options->audio_codec, .crop = options->crop, .port_range = options->port_range, .tunnel_host = options->tunnel_host, diff --git a/app/src/server.c b/app/src/server.c index fa8d8300..a797f01d 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -165,6 +165,8 @@ sc_server_get_codec_name(enum sc_codec codec) { return "h265"; case SC_CODEC_AV1: return "av1"; + case SC_CODEC_OPUS: + return "opus"; default: return NULL; } @@ -228,6 +230,10 @@ execute_server(struct sc_server *server, ADD_PARAM("video_codec=%s", sc_server_get_codec_name(params->video_codec)); } + if (params->audio_codec != SC_CODEC_OPUS) { + ADD_PARAM("audio_codec=%s", + sc_server_get_codec_name(params->audio_codec)); + } if (params->max_size) { ADD_PARAM("max_size=%" PRIu16, params->max_size); } diff --git a/app/src/server.h b/app/src/server.h index 805bdaf2..55a86605 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -26,6 +26,7 @@ struct sc_server_params { const char *req_serial; enum sc_log_level log_level; enum sc_codec video_codec; + enum sc_codec audio_codec; const char *crop; const char *video_codec_options; const char *video_encoder; diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java b/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java index 5704f768..710e5f7d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java @@ -38,7 +38,6 @@ public final class AudioEncoder { } } - private static final String MIMETYPE = MediaFormat.MIMETYPE_AUDIO_OPUS; private static final int SAMPLE_RATE = 48000; private static final int CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_STEREO; private static final int CHANNELS = 2; @@ -93,9 +92,9 @@ public final class AudioEncoder { return builder.build(); } - private static MediaFormat createFormat(int bitRate) { + private static MediaFormat createFormat(String mimeType, int bitRate) { MediaFormat format = new MediaFormat(); - format.setString(MediaFormat.KEY_MIME, MIMETYPE); + format.setString(MediaFormat.KEY_MIME, mimeType); format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate); format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, CHANNELS); format.setInteger(MediaFormat.KEY_SAMPLE_RATE, SAMPLE_RATE); @@ -216,12 +215,13 @@ public final class AudioEncoder { boolean mediaCodecStarted = false; boolean recorderStarted = false; try { - mediaCodec = MediaCodec.createEncoderByType(MIMETYPE); // may throw IOException + String mimeType = streamer.getCodec().getMimeType(); + mediaCodec = MediaCodec.createEncoderByType(mimeType); // may throw IOException mediaCodecThread = new HandlerThread("AudioEncoder"); mediaCodecThread.start(); - MediaFormat format = createFormat(bitRate); + MediaFormat format = createFormat(mimeType, bitRate); mediaCodec.setCallback(new EncoderCallback(), new Handler(mediaCodecThread.getLooper())); mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 44bc73ec..bdeab851 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 boolean audio = true; private int maxSize; private VideoCodec videoCodec = VideoCodec.H264; + private AudioCodec audioCodec = AudioCodec.OPUS; private int videoBitRate = 8000000; private int audioBitRate = 128000; private int maxFps; @@ -75,6 +76,14 @@ public class Options { this.videoCodec = videoCodec; } + public AudioCodec getAudioCodec() { + return audioCodec; + } + + public void setAudioCodec(AudioCodec audioCodec) { + this.audioCodec = audioCodec; + } + public int getVideoBitRate() { return videoBitRate; } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index c10e3209..4c15bd39 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -109,7 +109,8 @@ public final class Server { } if (audio) { - Streamer audioStreamer = new Streamer(connection.getAudioFd(), AudioCodec.OPUS, options.getSendCodecId(), options.getSendFrameMeta()); + Streamer audioStreamer = new Streamer(connection.getAudioFd(), options.getAudioCodec(), options.getSendCodecId(), + options.getSendFrameMeta()); audioEncoder = new AudioEncoder(audioStreamer, options.getAudioBitRate()); audioEncoder.start(); } @@ -202,6 +203,13 @@ public final class Server { } options.setVideoCodec(videoCodec); break; + case "audio_codec": + AudioCodec audioCodec = AudioCodec.findByName(value); + if (audioCodec == null) { + throw new IllegalArgumentException("Audio codec " + value + " not supported"); + } + options.setAudioCodec(audioCodec); + break; case "max_size": int maxSize = Integer.parseInt(value) & ~7; // multiple of 8 options.setMaxSize(maxSize); From 4601735e51b14e85b67fdc557868f022653b4230 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 18 Feb 2023 19:30:36 +0100 Subject: [PATCH 1405/2244] Add support for AAC audio codec Add option --audio-codec=aac. PR #3757 --- app/data/bash-completion/scrcpy | 2 +- app/data/zsh-completion/_scrcpy | 2 +- app/scrcpy.1 | 2 +- app/src/cli.c | 8 ++++++-- app/src/demuxer.c | 3 +++ app/src/options.h | 1 + app/src/server.c | 2 ++ .../src/main/java/com/genymobile/scrcpy/AudioCodec.java | 3 ++- 8 files changed, 17 insertions(+), 6 deletions(-) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index 5a50f6c5..f303ff66 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -73,7 +73,7 @@ _scrcpy() { return ;; --audio-codec) - COMPREPLY=($(compgen -W 'opus' -- "$cur")) + COMPREPLY=($(compgen -W 'opus aac' -- "$cur")) return ;; --lock-video-orientation) diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 4f7ad5ef..a0d83a05 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -10,7 +10,7 @@ local arguments arguments=( '--always-on-top[Make scrcpy window always on top \(above other windows\)]' '--audio-bit-rate=[Encode the audio at the given bit-rate]' - '--audio-codec=[Select the audio codec]:codec:(opus)' + '--audio-codec=[Select the audio codec]:codec:(opus aac)' {-b,--video-bit-rate=}'[Encode the video at the given bit-rate]' '--crop=[\[width\:height\:x\:y\] Crop the device screen on the server]' {-d,--select-usb}'[Use USB device]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 89533a1f..3ccbb111 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -27,7 +27,7 @@ Default is 128K (128000). .TP .BI "\-\-audio\-codec " name -Select an audio codec (opus). +Select an audio codec (opus or aac). Default is opus. diff --git a/app/src/cli.c b/app/src/cli.c index 5f28164e..afd060b8 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -119,7 +119,7 @@ static const struct sc_option options[] = { .longopt_id = OPT_AUDIO_CODEC, .longopt = "audio-codec", .argdesc = "name", - .text = "Select an audio codec (opus).\n" + .text = "Select an audio codec (opus or aac).\n" "Default is opus.", }, { @@ -1466,7 +1466,11 @@ parse_audio_codec(const char *optarg, enum sc_codec *codec) { *codec = SC_CODEC_OPUS; return true; } - LOGE("Unsupported audio codec: %s (expected opus)", optarg); + if (!strcmp(optarg, "aac")) { + *codec = SC_CODEC_AAC; + return true; + } + LOGE("Unsupported audio codec: %s (expected opus or aac)", optarg); return false; } diff --git a/app/src/demuxer.c b/app/src/demuxer.c index 482f2e04..64bf30a3 100644 --- a/app/src/demuxer.c +++ b/app/src/demuxer.c @@ -24,6 +24,7 @@ sc_demuxer_to_avcodec_id(uint32_t codec_id) { #define SC_CODEC_ID_H265 UINT32_C(0x68323635) // "h265" in ASCII #define SC_CODEC_ID_AV1 UINT32_C(0x00617631) // "av1" in ASCII #define SC_CODEC_ID_OPUS UINT32_C(0x6f707573) // "opus" in ASCII +#define SC_CODEC_ID_AAC UINT32_C(0x00616163) // "aac in ASCII" switch (codec_id) { case SC_CODEC_ID_H264: return AV_CODEC_ID_H264; @@ -33,6 +34,8 @@ sc_demuxer_to_avcodec_id(uint32_t codec_id) { return AV_CODEC_ID_AV1; case SC_CODEC_ID_OPUS: return AV_CODEC_ID_OPUS; + case SC_CODEC_ID_AAC: + return AV_CODEC_ID_AAC; default: LOGE("Unknown codec id 0x%08" PRIx32, codec_id); return AV_CODEC_ID_NONE; diff --git a/app/src/options.h b/app/src/options.h index c698e6e3..3efa2dd6 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -28,6 +28,7 @@ enum sc_codec { SC_CODEC_H265, SC_CODEC_AV1, SC_CODEC_OPUS, + SC_CODEC_AAC, }; enum sc_lock_video_orientation { diff --git a/app/src/server.c b/app/src/server.c index a797f01d..36146d86 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -167,6 +167,8 @@ sc_server_get_codec_name(enum sc_codec codec) { return "av1"; case SC_CODEC_OPUS: return "opus"; + case SC_CODEC_AAC: + return "aac"; default: return NULL; } diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioCodec.java b/server/src/main/java/com/genymobile/scrcpy/AudioCodec.java index 4d9e3201..dc000e98 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioCodec.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioCodec.java @@ -3,7 +3,8 @@ package com.genymobile.scrcpy; import android.media.MediaFormat; public enum AudioCodec implements Codec { - OPUS(0x6f_70_75_73, "opus", MediaFormat.MIMETYPE_AUDIO_OPUS); + OPUS(0x6f_70_75_73, "opus", MediaFormat.MIMETYPE_AUDIO_OPUS), + AAC(0x00_61_61_63, "aac", MediaFormat.MIMETYPE_AUDIO_AAC); private final int id; // 4-byte ASCII representation of the name private final String name; From 58cf8e540199ca5a1a25bb0e9bdac56f8de17a14 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 19 Feb 2023 20:03:04 +0100 Subject: [PATCH 1406/2244] Extract application of codec options This will allow to reuse the same code for audio codec options. PR #3757 --- .../com/genymobile/scrcpy/CodecUtils.java | 28 +++++++++++++++++++ .../com/genymobile/scrcpy/ScreenEncoder.java | 22 +++------------ 2 files changed, 32 insertions(+), 18 deletions(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/CodecUtils.java diff --git a/server/src/main/java/com/genymobile/scrcpy/CodecUtils.java b/server/src/main/java/com/genymobile/scrcpy/CodecUtils.java new file mode 100644 index 00000000..2a808c59 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/CodecUtils.java @@ -0,0 +1,28 @@ +package com.genymobile.scrcpy; + +import android.media.MediaCodecInfo; +import android.media.MediaCodecList; +import android.media.MediaFormat; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public final class CodecUtils { + + private CodecUtils() { + // not instantiable + } + + public static void setCodecOption(MediaFormat format, String key, Object value) { + 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); + } + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index d646995b..1c3ccf72 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -237,23 +237,6 @@ public class ScreenEncoder implements Device.RotationListener { return msg.toString(); } - 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(String videoMimeType, int bitRate, int maxFps, List codecOptions) { MediaFormat format = new MediaFormat(); format.setString(MediaFormat.KEY_MIME, videoMimeType); @@ -273,7 +256,10 @@ public class ScreenEncoder implements Device.RotationListener { if (codecOptions != null) { for (CodecOption option : codecOptions) { - setCodecOption(format, option); + String key = option.getKey(); + Object value = option.getValue(); + CodecUtils.setCodecOption(format, key, value); + Ln.d("Video codec option set: " + key + " (" + value.getClass().getSimpleName() + ") = " + value); } } From b03c864c70c4388ae788c494cfffeed2c00aabb8 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 22 Feb 2023 22:48:23 +0100 Subject: [PATCH 1407/2244] Add --audio-codec-options Similar to --video-codec-options, but for audio. PR #3757 --- app/data/bash-completion/scrcpy | 1 + app/data/zsh-completion/_scrcpy | 1 + app/scrcpy.1 | 10 ++++++++++ app/src/cli.c | 16 ++++++++++++++++ app/src/options.c | 1 + app/src/options.h | 1 + app/src/scrcpy.c | 1 + app/src/server.c | 5 +++++ app/src/server.h | 1 + .../com/genymobile/scrcpy/AudioEncoder.java | 19 ++++++++++++++++--- .../java/com/genymobile/scrcpy/Options.java | 10 ++++++++++ .../java/com/genymobile/scrcpy/Server.java | 6 +++++- 12 files changed, 68 insertions(+), 4 deletions(-) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index f303ff66..da245acc 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -4,6 +4,7 @@ _scrcpy() { --always-on-top --audio-bit-rate= --audio-codec= + --audio-codec-options= -b --video-bit-rate= --crop= -d --select-usb diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index a0d83a05..aa7928c6 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -11,6 +11,7 @@ arguments=( '--always-on-top[Make scrcpy window always on top \(above other windows\)]' '--audio-bit-rate=[Encode the audio at the given bit-rate]' '--audio-codec=[Select the audio codec]:codec:(opus aac)' + '--audio-codec-options=[Set a list of comma-separated key\:type=value options for the device audio encoder]' {-b,--video-bit-rate=}'[Encode the video at the given bit-rate]' '--crop=[\[width\:height\:x\:y\] Crop the device screen on the server]' {-d,--select-usb}'[Use USB device]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 3ccbb111..fd7746c4 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -31,6 +31,16 @@ Select an audio codec (opus or aac). Default is opus. +.TP +.BI "\-\-audio\-codec\-options " key\fR[:\fItype\fR]=\fIvalue\fR[,...] +Set a list of comma-separated key:type=value options for the device audio 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 "\-b, \-\-video\-bit\-rate " value Encode the video at the given bit\-rate, expressed in bits/s. Unit suffixes are supported: '\fBK\fR' (x1000) and '\fBM\fR' (x1000000). diff --git a/app/src/cli.c b/app/src/cli.c index afd060b8..9f61e6cb 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -66,6 +66,7 @@ enum { OPT_NO_AUDIO, OPT_AUDIO_BIT_RATE, OPT_AUDIO_CODEC, + OPT_AUDIO_CODEC_OPTIONS, }; struct sc_option { @@ -122,6 +123,18 @@ static const struct sc_option options[] = { .text = "Select an audio codec (opus or aac).\n" "Default is opus.", }, + { + .longopt_id = OPT_AUDIO_CODEC_OPTIONS, + .longopt = "audio-codec-options", + .argdesc = "key[:type]=value[,...]", + .text = "Set a list of comma-separated key:type=value options for the " + "device audio encoder.\n" + "The possible values for 'type' are 'int' (default), 'long', " + "'float' and 'string'.\n" + "The list of possible codec options is available in the " + "Android documentation: " + "", + }, { .shortopt = 'b', .longopt = "video-bit-rate", @@ -1672,6 +1685,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], case OPT_VIDEO_CODEC_OPTIONS: opts->video_codec_options = optarg; break; + case OPT_AUDIO_CODEC_OPTIONS: + opts->audio_codec_options = optarg; + break; case OPT_ENCODER: LOGW("--encoder is deprecated, use --video-encoder instead."); // fall through diff --git a/app/src/options.c b/app/src/options.c index 72f34e43..a9be5dfa 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -8,6 +8,7 @@ const struct scrcpy_options scrcpy_options_default = { .push_target = NULL, .render_driver = NULL, .video_codec_options = NULL, + .audio_codec_options = NULL, .video_encoder = NULL, #ifdef HAVE_V4L2 .v4l2_device = NULL, diff --git a/app/src/options.h b/app/src/options.h index 3efa2dd6..bbb52eb7 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -96,6 +96,7 @@ struct scrcpy_options { const char *push_target; const char *render_driver; const char *video_codec_options; + const char *audio_codec_options; const char *video_encoder; #ifdef HAVE_V4L2 const char *v4l2_device; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 8b96477c..a43c2687 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -331,6 +331,7 @@ scrcpy(struct scrcpy_options *options) { .show_touches = options->show_touches, .stay_awake = options->stay_awake, .video_codec_options = options->video_codec_options, + .audio_codec_options = options->audio_codec_options, .video_encoder = options->video_encoder, .force_adb_forward = options->force_adb_forward, .power_off_on_close = options->power_off_on_close, diff --git a/app/src/server.c b/app/src/server.c index 36146d86..95e4670d 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -72,6 +72,7 @@ sc_server_params_destroy(struct sc_server_params *params) { free((char *) params->req_serial); free((char *) params->crop); free((char *) params->video_codec_options); + free((char *) params->audio_codec_options); free((char *) params->video_encoder); free((char *) params->tcpip_dst); } @@ -96,6 +97,7 @@ sc_server_params_copy(struct sc_server_params *dst, COPY(req_serial); COPY(crop); COPY(video_codec_options); + COPY(audio_codec_options); COPY(video_encoder); COPY(tcpip_dst); #undef COPY @@ -268,6 +270,9 @@ execute_server(struct sc_server *server, if (params->video_codec_options) { ADD_PARAM("video_codec_options=%s", params->video_codec_options); } + if (params->audio_codec_options) { + ADD_PARAM("audio_codec_options=%s", params->audio_codec_options); + } if (params->video_encoder) { ADD_PARAM("video_encoder=%s", params->video_encoder); } diff --git a/app/src/server.h b/app/src/server.h index 55a86605..d96f997e 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -29,6 +29,7 @@ struct sc_server_params { enum sc_codec audio_codec; const char *crop; const char *video_codec_options; + const char *audio_codec_options; const char *video_encoder; struct sc_port_range port_range; uint32_t tunnel_host; diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java b/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java index 710e5f7d..56ff207f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java @@ -15,6 +15,7 @@ import android.os.Looper; import java.io.IOException; import java.nio.ByteBuffer; +import java.util.List; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; @@ -49,6 +50,7 @@ public final class AudioEncoder { private final Streamer streamer; private final int bitRate; + private final List codecOptions; // Capacity of 64 is in practice "infinite" (it is limited by the number of available MediaCodec buffers, typically 4). // So many pending tasks would lead to an unacceptable delay anyway. @@ -63,9 +65,10 @@ public final class AudioEncoder { private boolean ended; - public AudioEncoder(Streamer streamer, int bitRate) { + public AudioEncoder(Streamer streamer, int bitRate, List codecOptions) { this.streamer = streamer; this.bitRate = bitRate; + this.codecOptions = codecOptions; } private static AudioFormat createAudioFormat() { @@ -92,12 +95,22 @@ public final class AudioEncoder { return builder.build(); } - private static MediaFormat createFormat(String mimeType, int bitRate) { + private static MediaFormat createFormat(String mimeType, int bitRate, List codecOptions) { MediaFormat format = new MediaFormat(); format.setString(MediaFormat.KEY_MIME, mimeType); format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate); format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, CHANNELS); format.setInteger(MediaFormat.KEY_SAMPLE_RATE, SAMPLE_RATE); + + if (codecOptions != null) { + for (CodecOption option : codecOptions) { + String key = option.getKey(); + Object value = option.getValue(); + CodecUtils.setCodecOption(format, key, value); + Ln.d("Audio codec option set: " + key + " (" + value.getClass().getSimpleName() + ") = " + value); + } + } + return format; } @@ -221,7 +234,7 @@ public final class AudioEncoder { mediaCodecThread = new HandlerThread("AudioEncoder"); mediaCodecThread.start(); - MediaFormat format = createFormat(mimeType, bitRate); + MediaFormat format = createFormat(mimeType, bitRate, codecOptions); mediaCodec.setCallback(new EncoderCallback(), new Handler(mediaCodecThread.getLooper())); mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index bdeab851..4cb21e28 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -23,6 +23,8 @@ public class Options { private boolean showTouches; private boolean stayAwake; private List videoCodecOptions; + private List audioCodecOptions; + private String videoEncoder; private boolean powerOffScreenOnClose; private boolean clipboardAutosync = true; @@ -172,6 +174,14 @@ public class Options { this.videoCodecOptions = videoCodecOptions; } + public List getAudioCodecOptions() { + return audioCodecOptions; + } + + public void setAudioCodecOptions(List audioCodecOptions) { + this.audioCodecOptions = audioCodecOptions; + } + public String getVideoEncoder() { return videoEncoder; } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 4c15bd39..f4e36bff 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -111,7 +111,7 @@ public final class Server { if (audio) { Streamer audioStreamer = new Streamer(connection.getAudioFd(), options.getAudioCodec(), options.getSendCodecId(), options.getSendFrameMeta()); - audioEncoder = new AudioEncoder(audioStreamer, options.getAudioBitRate()); + audioEncoder = new AudioEncoder(audioStreamer, options.getAudioBitRate(), options.getAudioCodecOptions()); audioEncoder.start(); } @@ -258,6 +258,10 @@ public final class Server { List videoCodecOptions = CodecOption.parse(value); options.setVideoCodecOptions(videoCodecOptions); break; + case "audio_codec_options": + List audioCodecOptions = CodecOption.parse(value); + options.setAudioCodecOptions(audioCodecOptions); + break; case "video_encoder": if (!value.isEmpty()) { options.setVideoEncoder(value); From 6f332a2bc73853884c894ee2400ad9c92aabc34c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 19 Feb 2023 20:03:04 +0100 Subject: [PATCH 1408/2244] Extract unknown encoder error message This will allow to reuse the same code for audio encoder selection. PR #3757 --- .../com/genymobile/scrcpy/CodecUtils.java | 25 +++++++++++++++++ .../com/genymobile/scrcpy/ScreenEncoder.java | 28 +------------------ 2 files changed, 26 insertions(+), 27 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/CodecUtils.java b/server/src/main/java/com/genymobile/scrcpy/CodecUtils.java index 2a808c59..96887c14 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CodecUtils.java +++ b/server/src/main/java/com/genymobile/scrcpy/CodecUtils.java @@ -25,4 +25,29 @@ public final class CodecUtils { format.setString(key, (String) value); } } + + public static String buildUnknownEncoderMessage(Codec codec, String encoderName) { + StringBuilder msg = new StringBuilder("Encoder '").append(encoderName).append("' for ").append(codec.getName()).append(" not found"); + MediaCodecInfo[] encoders = listEncoders(codec.getMimeType()); + if (encoders != null && encoders.length > 0) { + msg.append("\nTry to use one of the available encoders:"); + String codecOption = codec.getType() == Codec.Type.VIDEO ? "video-codec" : "audio-codec"; + for (MediaCodecInfo encoder : encoders) { + msg.append("\n scrcpy --").append(codecOption).append("=").append(codec.getName()); + msg.append(" --encoder='").append(encoder.getName()).append("'"); + } + } + return msg.toString(); + } + + private static MediaCodecInfo[] listEncoders(String mimeType) { + List result = new ArrayList<>(); + MediaCodecList list = new MediaCodecList(MediaCodecList.REGULAR_CODECS); + for (MediaCodecInfo codecInfo : list.getCodecInfos()) { + if (codecInfo.isEncoder() && Arrays.asList(codecInfo.getSupportedTypes()).contains(mimeType)) { + result.add(codecInfo); + } + } + return result.toArray(new MediaCodecInfo[result.size()]); + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index 1c3ccf72..77cd1de4 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -5,7 +5,6 @@ import com.genymobile.scrcpy.wrappers.SurfaceControl; import android.graphics.Rect; import android.media.MediaCodec; import android.media.MediaCodecInfo; -import android.media.MediaCodecList; import android.media.MediaFormat; import android.os.Build; import android.os.IBinder; @@ -14,8 +13,6 @@ import android.view.Surface; import java.io.IOException; import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; @@ -199,24 +196,13 @@ public class ScreenEncoder implements Device.RotationListener { return !eof; } - private static MediaCodecInfo[] listEncoders(String videoMimeType) { - List result = new ArrayList<>(); - MediaCodecList list = new MediaCodecList(MediaCodecList.REGULAR_CODECS); - for (MediaCodecInfo codecInfo : list.getCodecInfos()) { - if (codecInfo.isEncoder() && Arrays.asList(codecInfo.getSupportedTypes()).contains(videoMimeType)) { - result.add(codecInfo); - } - } - return result.toArray(new MediaCodecInfo[result.size()]); - } - private static MediaCodec createMediaCodec(Codec codec, String encoderName) throws IOException, ConfigurationException { if (encoderName != null) { Ln.d("Creating encoder by name: '" + encoderName + "'"); try { return MediaCodec.createByCodecName(encoderName); } catch (IllegalArgumentException e) { - Ln.e(buildUnknownEncoderMessage(codec, encoderName)); + Ln.e(CodecUtils.buildUnknownEncoderMessage(codec, encoderName)); throw new ConfigurationException("Unknown encoder: " + encoderName); } } @@ -225,18 +211,6 @@ public class ScreenEncoder implements Device.RotationListener { return mediaCodec; } - private static String buildUnknownEncoderMessage(Codec codec, String encoderName) { - StringBuilder msg = new StringBuilder("Encoder '").append(encoderName).append("' for ").append(codec.getName()).append(" not found"); - MediaCodecInfo[] encoders = listEncoders(codec.getMimeType()); - if (encoders != null && encoders.length > 0) { - msg.append("\nTry to use one of the available encoders:"); - for (MediaCodecInfo encoder : encoders) { - msg.append("\n scrcpy --video-codec=").append(codec.getName()).append(" --encoder='").append(encoder.getName()).append("'"); - } - } - return msg.toString(); - } - private static MediaFormat createFormat(String videoMimeType, int bitRate, int maxFps, List codecOptions) { MediaFormat format = new MediaFormat(); format.setString(MediaFormat.KEY_MIME, videoMimeType); From f9960e959fa7b46cc1ed6b15115fe2958724766d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 19 Feb 2023 20:20:29 +0100 Subject: [PATCH 1409/2244] Add --audio-encoder Similar to --video-encoder, but for audio. PR #3757 --- app/data/bash-completion/scrcpy | 1 + app/data/zsh-completion/_scrcpy | 1 + app/scrcpy.1 | 4 +++ app/src/cli.c | 11 +++++++ app/src/options.c | 1 + app/src/options.h | 1 + app/src/scrcpy.c | 1 + app/src/server.c | 5 ++++ app/src/server.h | 1 + .../com/genymobile/scrcpy/AudioEncoder.java | 29 +++++++++++++++---- .../java/com/genymobile/scrcpy/Options.java | 9 ++++++ .../java/com/genymobile/scrcpy/Server.java | 6 +++- 12 files changed, 64 insertions(+), 6 deletions(-) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index da245acc..c860707f 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -5,6 +5,7 @@ _scrcpy() { --audio-bit-rate= --audio-codec= --audio-codec-options= + --audio-encoder= -b --video-bit-rate= --crop= -d --select-usb diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index aa7928c6..b122587f 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -12,6 +12,7 @@ arguments=( '--audio-bit-rate=[Encode the audio at the given bit-rate]' '--audio-codec=[Select the audio codec]:codec:(opus aac)' '--audio-codec-options=[Set a list of comma-separated key\:type=value options for the device audio encoder]' + '--audio-encoder=[Use a specific MediaCodec audio encoder]' {-b,--video-bit-rate=}'[Encode the video at the given bit-rate]' '--crop=[\[width\:height\:x\:y\] Crop the device screen on the server]' {-d,--select-usb}'[Use USB device]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index fd7746c4..ef17465a 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -41,6 +41,10 @@ The list of possible codec options is available in the Android documentation .UR https://d.android.com/reference/android/media/MediaFormat .UE . +.TP +.BI "\-\-audio\-encoder " name +Use a specific MediaCodec audio encoder (depending on the codec provided by \fB\-\-audio\-codec\fR). + .TP .BI "\-b, \-\-video\-bit\-rate " value Encode the video at the given bit\-rate, expressed in bits/s. Unit suffixes are supported: '\fBK\fR' (x1000) and '\fBM\fR' (x1000000). diff --git a/app/src/cli.c b/app/src/cli.c index 9f61e6cb..68629fd2 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -67,6 +67,7 @@ enum { OPT_AUDIO_BIT_RATE, OPT_AUDIO_CODEC, OPT_AUDIO_CODEC_OPTIONS, + OPT_AUDIO_ENCODER, }; struct sc_option { @@ -135,6 +136,13 @@ static const struct sc_option options[] = { "Android documentation: " "", }, + { + .longopt_id = OPT_AUDIO_ENCODER, + .longopt = "audio-encoder", + .argdesc = "name", + .text = "Use a specific MediaCodec audio encoder (depending on the " + "codec provided by --audio-codec).", + }, { .shortopt = 'b', .longopt = "video-bit-rate", @@ -1694,6 +1702,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], case OPT_VIDEO_ENCODER: opts->video_encoder = optarg; break; + case OPT_AUDIO_ENCODER: + opts->audio_encoder = optarg; + break; case OPT_FORCE_ADB_FORWARD: opts->force_adb_forward = true; break; diff --git a/app/src/options.c b/app/src/options.c index a9be5dfa..40f84fdd 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -10,6 +10,7 @@ const struct scrcpy_options scrcpy_options_default = { .video_codec_options = NULL, .audio_codec_options = NULL, .video_encoder = NULL, + .audio_encoder = NULL, #ifdef HAVE_V4L2 .v4l2_device = NULL, #endif diff --git a/app/src/options.h b/app/src/options.h index bbb52eb7..804fba93 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -98,6 +98,7 @@ struct scrcpy_options { const char *video_codec_options; const char *audio_codec_options; const char *video_encoder; + const char *audio_encoder; #ifdef HAVE_V4L2 const char *v4l2_device; #endif diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index a43c2687..6bfed295 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -333,6 +333,7 @@ scrcpy(struct scrcpy_options *options) { .video_codec_options = options->video_codec_options, .audio_codec_options = options->audio_codec_options, .video_encoder = options->video_encoder, + .audio_encoder = options->audio_encoder, .force_adb_forward = options->force_adb_forward, .power_off_on_close = options->power_off_on_close, .clipboard_autosync = options->clipboard_autosync, diff --git a/app/src/server.c b/app/src/server.c index 95e4670d..b50003c9 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -74,6 +74,7 @@ sc_server_params_destroy(struct sc_server_params *params) { free((char *) params->video_codec_options); free((char *) params->audio_codec_options); free((char *) params->video_encoder); + free((char *) params->audio_encoder); free((char *) params->tcpip_dst); } @@ -99,6 +100,7 @@ sc_server_params_copy(struct sc_server_params *dst, COPY(video_codec_options); COPY(audio_codec_options); COPY(video_encoder); + COPY(audio_encoder); COPY(tcpip_dst); #undef COPY @@ -276,6 +278,9 @@ execute_server(struct sc_server *server, if (params->video_encoder) { ADD_PARAM("video_encoder=%s", params->video_encoder); } + if (params->audio_encoder) { + ADD_PARAM("audio_encoder=%s", params->audio_encoder); + } if (params->power_off_on_close) { ADD_PARAM("power_off_on_close=true"); } diff --git a/app/src/server.h b/app/src/server.h index d96f997e..c20508e0 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -31,6 +31,7 @@ struct sc_server_params { const char *video_codec_options; const char *audio_codec_options; const char *video_encoder; + const char *audio_encoder; struct sc_port_range port_range; uint32_t tunnel_host; uint16_t tunnel_port; diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java b/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java index 56ff207f..a70a475b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java @@ -51,6 +51,7 @@ public final class AudioEncoder { private final Streamer streamer; private final int bitRate; private final List codecOptions; + private final String encoderName; // Capacity of 64 is in practice "infinite" (it is limited by the number of available MediaCodec buffers, typically 4). // So many pending tasks would lead to an unacceptable delay anyway. @@ -65,10 +66,11 @@ public final class AudioEncoder { private boolean ended; - public AudioEncoder(Streamer streamer, int bitRate, List codecOptions) { + public AudioEncoder(Streamer streamer, int bitRate, List codecOptions, String encoderName) { this.streamer = streamer; this.bitRate = bitRate; this.codecOptions = codecOptions; + this.encoderName = encoderName; } private static AudioFormat createAudioFormat() { @@ -177,6 +179,8 @@ public final class AudioEncoder { thread = new Thread(() -> { try { encode(); + } catch (ConfigurationException e) { + // Do not print stack trace, a user-friendly error-message has already been logged } catch (IOException e) { Ln.e("Audio encoding error", e); } finally { @@ -215,7 +219,7 @@ public final class AudioEncoder { } @TargetApi(Build.VERSION_CODES.M) - public void encode() throws IOException { + public void encode() throws IOException, ConfigurationException { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { Ln.w("Audio disabled: it is not supported before Android 11"); streamer.writeDisableStream(); @@ -228,13 +232,13 @@ public final class AudioEncoder { boolean mediaCodecStarted = false; boolean recorderStarted = false; try { - String mimeType = streamer.getCodec().getMimeType(); - mediaCodec = MediaCodec.createEncoderByType(mimeType); // may throw IOException + Codec codec = streamer.getCodec(); + mediaCodec = createMediaCodec(codec, encoderName); mediaCodecThread = new HandlerThread("AudioEncoder"); mediaCodecThread.start(); - MediaFormat format = createFormat(mimeType, bitRate, codecOptions); + MediaFormat format = createFormat(codec.getMimeType(), bitRate, codecOptions); mediaCodec.setCallback(new EncoderCallback(), new Handler(mediaCodecThread.getLooper())); mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); @@ -324,6 +328,21 @@ public final class AudioEncoder { } } + private static MediaCodec createMediaCodec(Codec codec, String encoderName) throws IOException, ConfigurationException { + if (encoderName != null) { + Ln.d("Creating audio encoder by name: '" + encoderName + "'"); + try { + return MediaCodec.createByCodecName(encoderName); + } catch (IllegalArgumentException e) { + Ln.e(CodecUtils.buildUnknownEncoderMessage(codec, encoderName)); + throw new ConfigurationException("Unknown encoder: " + encoderName); + } + } + MediaCodec mediaCodec = MediaCodec.createEncoderByType(codec.getMimeType()); + Ln.d("Using audio encoder: '" + mediaCodec.getName() + "'"); + return mediaCodec; + } + private class EncoderCallback extends MediaCodec.Callback { @TargetApi(Build.VERSION_CODES.N) @Override diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 4cb21e28..86838022 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -26,6 +26,7 @@ public class Options { private List audioCodecOptions; private String videoEncoder; + private String audioEncoder; private boolean powerOffScreenOnClose; private boolean clipboardAutosync = true; private boolean downsizeOnError = true; @@ -190,6 +191,14 @@ public class Options { this.videoEncoder = videoEncoder; } + public String getAudioEncoder() { + return audioEncoder; + } + + public void setAudioEncoder(String audioEncoder) { + this.audioEncoder = audioEncoder; + } + public void setPowerOffScreenOnClose(boolean powerOffScreenOnClose) { this.powerOffScreenOnClose = powerOffScreenOnClose; } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index f4e36bff..f30d65f6 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -111,7 +111,7 @@ public final class Server { if (audio) { Streamer audioStreamer = new Streamer(connection.getAudioFd(), options.getAudioCodec(), options.getSendCodecId(), options.getSendFrameMeta()); - audioEncoder = new AudioEncoder(audioStreamer, options.getAudioBitRate(), options.getAudioCodecOptions()); + audioEncoder = new AudioEncoder(audioStreamer, options.getAudioBitRate(), options.getAudioCodecOptions(), options.getAudioEncoder()); audioEncoder.start(); } @@ -267,6 +267,10 @@ public final class Server { options.setVideoEncoder(value); } break; + case "audio_encoder": + if (!value.isEmpty()) { + options.setAudioEncoder(value); + } case "power_off_on_close": boolean powerOffScreenOnClose = Boolean.parseBoolean(value); options.setPowerOffScreenOnClose(powerOffScreenOnClose); From b7e5284adf1074d6d851a0a7d164461325ccaa58 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 22 Feb 2023 23:12:21 +0100 Subject: [PATCH 1410/2244] Move await_for_server() logs Print the logs on the caller side. This will allow to call the function in another context without printing the logs. PR #3757 --- 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 6bfed295..81affe47 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -183,14 +183,11 @@ await_for_server(bool *connected) { while (SDL_WaitEvent(&event)) { switch (event.type) { case SDL_QUIT: - LOGD("User requested to quit"); *connected = false; return true; case SC_EVENT_SERVER_CONNECTION_FAILED: - LOGE("Server connection failed"); return false; case SC_EVENT_SERVER_CONNECTED: - LOGD("Server connected"); *connected = true; return true; default: @@ -374,15 +371,19 @@ scrcpy(struct scrcpy_options *options) { // Await for server without blocking Ctrl+C handling bool connected; if (!await_for_server(&connected)) { + LOGE("Server connection failed"); goto end; } if (!connected) { // This is not an error, user requested to quit + LOGD("User requested to quit"); ret = SCRCPY_EXIT_SUCCESS; goto end; } + LOGD("Server connected"); + // It is necessarily initialized here, since the device is connected struct sc_server_info *info = &s->server.info; From 9196dc156376760eb637024d5c0448ceeaa2a8ff Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 22 Feb 2023 23:15:15 +0100 Subject: [PATCH 1411/2244] Add --list-encoders Add an option to list the device encoders properly. PR #3757 --- README.md | 6 +- app/data/bash-completion/scrcpy | 1 + app/data/zsh-completion/_scrcpy | 1 + app/scrcpy.1 | 8 ++ app/src/cli.c | 15 +++- app/src/options.c | 1 + app/src/options.h | 1 + app/src/scrcpy.c | 15 +++- app/src/server.c | 27 +++++-- app/src/server.h | 1 + .../com/genymobile/scrcpy/AudioEncoder.java | 2 +- .../java/com/genymobile/scrcpy/CleanUp.java | 2 +- .../com/genymobile/scrcpy/CodecUtils.java | 79 ++++++++++++++++--- .../java/com/genymobile/scrcpy/Options.java | 10 +++ .../com/genymobile/scrcpy/ScreenEncoder.java | 2 +- .../java/com/genymobile/scrcpy/Server.java | 15 ++++ 16 files changed, 157 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 8eabafa9..5575fc4d 100644 --- a/README.md +++ b/README.md @@ -273,12 +273,10 @@ may cause issues or crash. It is possible to select a different encoder: scrcpy --video-encoder=OMX.qcom.video.encoder.avc ``` -To list the available encoders, you can pass an invalid encoder name; the -error will give the available encoders: +To list the available encoders: ```bash -scrcpy --video-encoder=_ # for the default codec -scrcpy --video-codec=h265 --video-encoder=_ # for a specific codec +scrcpy --list-encoders ``` ### Capture diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index c860707f..70695019 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -19,6 +19,7 @@ _scrcpy() { -K --hid-keyboard -h --help --legacy-paste + --list-encoders --lock-video-orientation --lock-video-orientation= --max-fps= diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index b122587f..268aa626 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -26,6 +26,7 @@ arguments=( {-K,--hid-keyboard}'[Simulate a physical keyboard by using HID over AOAv2]' {-h,--help}'[Print the help]' '--legacy-paste[Inject computer clipboard text as a sequence of key events on Ctrl+v]' + '--list-encoders[List video and audio encoders available on the device]' '--lock-video-orientation=[Lock video orientation]:orientation:(unlocked initial 0 1 2 3)' '--max-fps=[Limit the frame rate of screen capture]' {-M,--hid-mouse}'[Simulate a physical mouse by using HID over AOAv2]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index ef17465a..add263c2 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -45,6 +45,8 @@ The list of possible codec options is available in the Android documentation .BI "\-\-audio\-encoder " name Use a specific MediaCodec audio encoder (depending on the codec provided by \fB\-\-audio\-codec\fR). +The available encoders can be listed by \-\-list\-encoders. + .TP .BI "\-b, \-\-video\-bit\-rate " value Encode the video at the given bit\-rate, expressed in bits/s. Unit suffixes are supported: '\fBK\fR' (x1000) and '\fBM\fR' (x1000000). @@ -128,6 +130,10 @@ Inject computer clipboard text as a sequence of key events on Ctrl+v (like MOD+S This is a workaround for some devices not behaving as expected when setting the device clipboard programmatically. +.TP +.B \-\-list\-encoders +List video and audio encoders available on the device. + .TP \fB\-\-lock\-video\-orientation\fR[=\fIvalue\fR] Lock video orientation to \fIvalue\fR. Possible values are "unlocked", "initial" (locked to the initial orientation), 0, 1, 2 and 3. Natural device orientation is 0, and each increment adds a 90 degrees rotation counterclockwise. @@ -355,6 +361,8 @@ The list of possible codec options is available in the Android documentation .BI "\-\-video\-encoder " name Use a specific MediaCodec video encoder (depending on the codec provided by \fB\-\-video\-codec\fR). +The available encoders can be listed by \-\-list\-encoders. + .TP .B \-w, \-\-stay-awake Keep the device on while scrcpy is running, when the device is plugged in. diff --git a/app/src/cli.c b/app/src/cli.c index 68629fd2..edb694a6 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -68,6 +68,7 @@ enum { OPT_AUDIO_CODEC, OPT_AUDIO_CODEC_OPTIONS, OPT_AUDIO_ENCODER, + OPT_LIST_ENCODERS, }; struct sc_option { @@ -141,7 +142,8 @@ static const struct sc_option options[] = { .longopt = "audio-encoder", .argdesc = "name", .text = "Use a specific MediaCodec audio encoder (depending on the " - "codec provided by --audio-codec).", + "codec provided by --audio-codec).\n" + "The available encoders can be listed by --list-encoders.", }, { .shortopt = 'b', @@ -270,6 +272,11 @@ static const struct sc_option options[] = { "This is a workaround for some devices not behaving as " "expected when setting the device clipboard programmatically.", }, + { + .longopt_id = OPT_LIST_ENCODERS, + .longopt = "list-encoders", + .text = "List video and audio encoders available on the device.", + }, { .longopt_id = OPT_LOCK_VIDEO_ORIENTATION, .longopt = "lock-video-orientation", @@ -586,7 +593,8 @@ static const struct sc_option options[] = { .longopt = "video-encoder", .argdesc = "name", .text = "Use a specific MediaCodec video encoder (depending on the " - "codec provided by --video-codec).", + "codec provided by --video-codec).\n" + "The available encoders can be listed by --list-encoders.", }, { .shortopt = 'w', @@ -1792,6 +1800,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], LOGE("V4L2 (--v4l2-buffer) is only available on Linux."); return false; #endif + case OPT_LIST_ENCODERS: + opts->list_encoders = true; + break; default: // getopt prints the error message on stderr return false; diff --git a/app/src/options.c b/app/src/options.c index 40f84fdd..1839df6e 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -72,4 +72,5 @@ const struct scrcpy_options scrcpy_options_default = { .start_fps_counter = false, .power_on = true, .audio = true, + .list_encoders = false, }; diff --git a/app/src/options.h b/app/src/options.h index 804fba93..568b8155 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -154,6 +154,7 @@ struct scrcpy_options { bool start_fps_counter; bool power_on; bool audio; + bool list_encoders; }; extern const struct scrcpy_options scrcpy_options_default; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 81affe47..6d0fac9e 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -183,12 +183,16 @@ await_for_server(bool *connected) { while (SDL_WaitEvent(&event)) { switch (event.type) { case SDL_QUIT: - *connected = false; + if (connected) { + *connected = false; + } return true; case SC_EVENT_SERVER_CONNECTION_FAILED: return false; case SC_EVENT_SERVER_CONNECTED: - *connected = true; + if (connected) { + *connected = true; + } return true; default: break; @@ -339,6 +343,7 @@ scrcpy(struct scrcpy_options *options) { .tcpip_dst = options->tcpip_dst, .cleanup = options->cleanup, .power_on = options->power_on, + .list_encoders = options->list_encoders, }; static const struct sc_server_callbacks cbs = { @@ -356,6 +361,12 @@ scrcpy(struct scrcpy_options *options) { server_started = true; + if (options->list_encoders) { + bool ok = await_for_server(NULL); + ret = ok ? SCRCPY_EXIT_SUCCESS : SCRCPY_EXIT_FAILURE; + goto end; + } + if (options->display) { sdl_set_hints(options->render_driver); } diff --git a/app/src/server.c b/app/src/server.c index b50003c9..077614a8 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -300,6 +300,9 @@ execute_server(struct sc_server *server, // By default, power_on is true ADD_PARAM("power_on=false"); } + if (params->list_encoders) { + ADD_PARAM("list_encoders=true"); + } #undef ADD_PARAM @@ -848,6 +851,25 @@ run_server(void *data) { assert(serial); LOGD("Device serial: %s", serial); + ok = push_server(&server->intr, serial); + if (!ok) { + goto error_connection_failed; + } + + // If --list-encoders is passed, then the server just prints the encoders + // then exits. + if (params->list_encoders) { + sc_pid pid = execute_server(server, params); + if (pid == SC_PROCESS_NONE) { + goto error_connection_failed; + } + sc_process_wait(pid, NULL); // ignore exit code + sc_process_close(pid); + // Wake up await_for_server() + server->cbs->on_connected(server, server->cbs_userdata); + return 0; + } + int r = asprintf(&server->device_socket_name, SC_SOCKET_NAME_PREFIX "%08x", params->scid); if (r == -1) { @@ -857,11 +879,6 @@ run_server(void *data) { assert(r == sizeof(SC_SOCKET_NAME_PREFIX) - 1 + 8); assert(server->device_socket_name); - ok = push_server(&server->intr, serial); - if (!ok) { - goto error_connection_failed; - } - ok = sc_adb_tunnel_open(&server->tunnel, &server->intr, serial, server->device_socket_name, params->port_range, params->force_adb_forward); diff --git a/app/src/server.h b/app/src/server.h index c20508e0..ada04baa 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -55,6 +55,7 @@ struct sc_server_params { bool select_tcpip; bool cleanup; bool power_on; + bool list_encoders; }; struct sc_server { diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java b/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java index a70a475b..540d8306 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java @@ -334,7 +334,7 @@ public final class AudioEncoder { try { return MediaCodec.createByCodecName(encoderName); } catch (IllegalArgumentException e) { - Ln.e(CodecUtils.buildUnknownEncoderMessage(codec, encoderName)); + Ln.e("Encoder '" + encoderName + "' for " + codec.getName() + " not found\n" + CodecUtils.buildAudioEncoderListMessage()); throw new ConfigurationException("Unknown encoder: " + encoderName); } } diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java index 831dc994..0bcd1a54 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java +++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java @@ -139,7 +139,7 @@ public final class CleanUp { builder.start(); } - private static void unlinkSelf() { + public static void unlinkSelf() { try { new File(SERVER_PATH).delete(); } catch (Exception e) { diff --git a/server/src/main/java/com/genymobile/scrcpy/CodecUtils.java b/server/src/main/java/com/genymobile/scrcpy/CodecUtils.java index 96887c14..aca54d20 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CodecUtils.java +++ b/server/src/main/java/com/genymobile/scrcpy/CodecUtils.java @@ -10,6 +10,24 @@ import java.util.List; public final class CodecUtils { + public static final class DeviceEncoder { + private final Codec codec; + private final MediaCodecInfo info; + + DeviceEncoder(Codec codec, MediaCodecInfo info) { + this.codec = codec; + this.info = info; + } + + public Codec getCodec() { + return codec; + } + + public MediaCodecInfo getInfo() { + return info; + } + } + private CodecUtils() { // not instantiable } @@ -26,28 +44,63 @@ public final class CodecUtils { } } - public static String buildUnknownEncoderMessage(Codec codec, String encoderName) { - StringBuilder msg = new StringBuilder("Encoder '").append(encoderName).append("' for ").append(codec.getName()).append(" not found"); - MediaCodecInfo[] encoders = listEncoders(codec.getMimeType()); - if (encoders != null && encoders.length > 0) { - msg.append("\nTry to use one of the available encoders:"); - String codecOption = codec.getType() == Codec.Type.VIDEO ? "video-codec" : "audio-codec"; - for (MediaCodecInfo encoder : encoders) { - msg.append("\n scrcpy --").append(codecOption).append("=").append(codec.getName()); - msg.append(" --encoder='").append(encoder.getName()).append("'"); + public static String buildVideoEncoderListMessage() { + StringBuilder builder = new StringBuilder("List of video encoders:"); + List videoEncoders = CodecUtils.listVideoEncoders(); + if (videoEncoders.isEmpty()) { + builder.append("\n (none)"); + } else { + for (CodecUtils.DeviceEncoder encoder : videoEncoders) { + builder.append("\n --video-codec=").append(encoder.getCodec().getName()); + builder.append(" --video-encoder='").append(encoder.getInfo().getName()).append("'"); } } - return msg.toString(); + return builder.toString(); } - private static MediaCodecInfo[] listEncoders(String mimeType) { + public static String buildAudioEncoderListMessage() { + StringBuilder builder = new StringBuilder("List of audio encoders:"); + List audioEncoders = CodecUtils.listAudioEncoders(); + if (audioEncoders.isEmpty()) { + builder.append("\n (none)"); + } else { + for (CodecUtils.DeviceEncoder encoder : audioEncoders) { + builder.append("\n --audio-codec=").append(encoder.getCodec().getName()); + builder.append(" --audio-encoder='").append(encoder.getInfo().getName()).append("'"); + } + } + return builder.toString(); + } + + private static MediaCodecInfo[] getEncoders(MediaCodecList codecs, String mimeType) { List result = new ArrayList<>(); - MediaCodecList list = new MediaCodecList(MediaCodecList.REGULAR_CODECS); - for (MediaCodecInfo codecInfo : list.getCodecInfos()) { + for (MediaCodecInfo codecInfo : codecs.getCodecInfos()) { if (codecInfo.isEncoder() && Arrays.asList(codecInfo.getSupportedTypes()).contains(mimeType)) { result.add(codecInfo); } } return result.toArray(new MediaCodecInfo[result.size()]); } + + public static List listVideoEncoders() { + List encoders = new ArrayList<>(); + MediaCodecList codecs = new MediaCodecList(MediaCodecList.REGULAR_CODECS); + for (VideoCodec codec : VideoCodec.values()) { + for (MediaCodecInfo info : getEncoders(codecs, codec.getMimeType())) { + encoders.add(new DeviceEncoder(codec, info)); + } + } + return encoders; + } + + public static List listAudioEncoders() { + List encoders = new ArrayList<>(); + MediaCodecList codecs = new MediaCodecList(MediaCodecList.REGULAR_CODECS); + for (AudioCodec codec : AudioCodec.values()) { + for (MediaCodecInfo info : getEncoders(codecs, codec.getMimeType())) { + encoders.add(new DeviceEncoder(codec, info)); + } + } + return encoders; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 86838022..8cac5e2c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -33,6 +33,8 @@ public class Options { private boolean cleanup = true; private boolean powerOn = true; + private boolean listEncoders; + // Options not used by the scrcpy client, but useful to use scrcpy-server directly private boolean sendDeviceMeta = true; // send device name and size private boolean sendFrameMeta = true; // send PTS so that the client may record properly @@ -239,6 +241,14 @@ public class Options { this.powerOn = powerOn; } + public boolean getListEncoders() { + return listEncoders; + } + + public void setListEncoders(boolean listEncoders) { + this.listEncoders = listEncoders; + } + public boolean getSendDeviceMeta() { return sendDeviceMeta; } diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index 77cd1de4..668a4ed0 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -202,7 +202,7 @@ public class ScreenEncoder implements Device.RotationListener { try { return MediaCodec.createByCodecName(encoderName); } catch (IllegalArgumentException e) { - Ln.e(CodecUtils.buildUnknownEncoderMessage(codec, encoderName)); + Ln.e("Encoder '" + encoderName + "' for " + codec.getName() + " not found\n" + CodecUtils.buildVideoEncoderListMessage()); throw new ConfigurationException("Unknown encoder: " + encoderName); } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index f30d65f6..adfbef2a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -291,6 +291,10 @@ public final class Server { boolean powerOn = Boolean.parseBoolean(value); options.setPowerOn(powerOn); break; + case "list_encoders": + boolean listEncoders = Boolean.parseBoolean(value); + options.setListEncoders(listEncoders); + break; case "send_device_meta": boolean sendDeviceMeta = Boolean.parseBoolean(value); options.setSendDeviceMeta(sendDeviceMeta); @@ -350,6 +354,17 @@ public final class Server { Ln.initLogLevel(options.getLogLevel()); + if (options.getListEncoders()) { + if (options.getCleanup()) { + CleanUp.unlinkSelf(); + } + + Ln.i(CodecUtils.buildVideoEncoderListMessage()); + Ln.i(CodecUtils.buildAudioEncoderListMessage()); + // Just print the available encoders, do not mirror + return; + } + try { scrcpy(options); } catch (ConfigurationException e) { From 50d56a9a2bb75398c562e71b131de5c3ed43e95e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 23 Feb 2023 21:27:11 +0100 Subject: [PATCH 1412/2244] Quit on audio configuration failure When audio capture fails on the device, scrcpy continues mirroring the video stream. This allows to enable audio by default only when supported. However, if an audio configuration occurs (for example the user explicitly selected an unknown audio encoder), this must be treated as an error and scrcpy must exit. PR #3757 --- app/src/demuxer.c | 6 ++++++ app/src/scrcpy.c | 13 +++++++++++-- .../java/com/genymobile/scrcpy/AudioEncoder.java | 8 ++++++-- .../main/java/com/genymobile/scrcpy/Streamer.java | 13 +++++++++---- 4 files changed, 32 insertions(+), 8 deletions(-) diff --git a/app/src/demuxer.c b/app/src/demuxer.c index 64bf30a3..d80a5dda 100644 --- a/app/src/demuxer.c +++ b/app/src/demuxer.c @@ -195,6 +195,12 @@ run_demuxer(void *data) { goto end; } + if (raw_codec_id == 1) { + LOGE("Demuxer '%s': stream configuration error on the device", + demuxer->name); + goto end; + } + enum AVCodecID codec_id = sc_demuxer_to_avcodec_id(raw_codec_id); if (codec_id == AV_CODEC_ID_NONE) { LOGE("Demuxer '%s': stream disabled due to unsupported codec", diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 6d0fac9e..5739c3b2 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -231,10 +231,19 @@ static void sc_audio_demuxer_on_ended(struct sc_demuxer *demuxer, bool eos, void *userdata) { (void) demuxer; - (void) eos; (void) userdata; - // Contrary to the video demuxer, keep mirroring if only the audio fails + // Contrary to the video demuxer, keep mirroring if only the audio fails. + // 'eos' is true on end-of-stream, including when audio capture is not + // possible on the device (so that scrcpy continue to mirror video without + // failing). + // However, if an audio configuration failure occurs (for example the user + // explicitly selected an unknown audio encoder), 'eos' is false and scrcpy + // must exit. + + if (!eos) { + PUSH_EVENT(SC_EVENT_DEMUXER_ERROR); + } } static void diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java b/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java index 540d8306..66950004 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java @@ -222,7 +222,7 @@ public final class AudioEncoder { public void encode() throws IOException, ConfigurationException { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { Ln.w("Audio disabled: it is not supported before Android 11"); - streamer.writeDisableStream(); + streamer.writeDisableStream(false); return; } @@ -279,9 +279,13 @@ public final class AudioEncoder { outputThread.start(); waitEnded(); + } catch (ConfigurationException e) { + // Notify the error to make scrcpy exit + streamer.writeDisableStream(true); + throw e; } catch (Throwable e) { // Notify the client that the audio could not be captured - streamer.writeDisableStream(); + streamer.writeDisableStream(false); throw e; } finally { // Cleanup everything (either at the end or on error at any step of the initialization) diff --git a/server/src/main/java/com/genymobile/scrcpy/Streamer.java b/server/src/main/java/com/genymobile/scrcpy/Streamer.java index 7cc065eb..9bfe7e91 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Streamer.java +++ b/server/src/main/java/com/genymobile/scrcpy/Streamer.java @@ -40,10 +40,15 @@ public final class Streamer { } } - public void writeDisableStream() throws IOException { - // Writing 0 (32-bit) as codec-id means that the device disables the stream (because it could not capture) - byte[] zeros = new byte[4]; - IO.writeFully(fd, zeros, 0, zeros.length); + public void writeDisableStream(boolean error) throws IOException { + // Writing a specific code as codec-id means that the device disables the stream + // code 0: it explicitly disables the stream (because it could not capture audio), scrcpy should continue mirroring video only + // code 1: a configuration error occurred, scrcpy must be stopped + byte[] code = new byte[4]; + if (error) { + code[3] = 1; + } + IO.writeFully(fd, code, 0, code.length); } public void writePacket(ByteBuffer buffer, long pts, boolean config, boolean keyFrame) throws IOException { From 2596ca02f0d9b53ddf322873ad141197216f00f4 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 23 Feb 2023 23:09:25 +0100 Subject: [PATCH 1413/2244] Move log message helpers to LogUtils This class will also contain other log helpers. --- .../com/genymobile/scrcpy/AudioEncoder.java | 2 +- .../com/genymobile/scrcpy/CodecUtils.java | 28 -------------- .../java/com/genymobile/scrcpy/LogUtils.java | 38 +++++++++++++++++++ .../com/genymobile/scrcpy/ScreenEncoder.java | 2 +- .../java/com/genymobile/scrcpy/Server.java | 4 +- 5 files changed, 42 insertions(+), 32 deletions(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/LogUtils.java diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java b/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java index 66950004..1ce4107f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java @@ -338,7 +338,7 @@ public final class AudioEncoder { try { return MediaCodec.createByCodecName(encoderName); } catch (IllegalArgumentException e) { - Ln.e("Encoder '" + encoderName + "' for " + codec.getName() + " not found\n" + CodecUtils.buildAudioEncoderListMessage()); + Ln.e("Encoder '" + encoderName + "' for " + codec.getName() + " not found\n" + LogUtils.buildAudioEncoderListMessage()); throw new ConfigurationException("Unknown encoder: " + encoderName); } } diff --git a/server/src/main/java/com/genymobile/scrcpy/CodecUtils.java b/server/src/main/java/com/genymobile/scrcpy/CodecUtils.java index aca54d20..afb6f904 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CodecUtils.java +++ b/server/src/main/java/com/genymobile/scrcpy/CodecUtils.java @@ -44,34 +44,6 @@ public final class CodecUtils { } } - public static String buildVideoEncoderListMessage() { - StringBuilder builder = new StringBuilder("List of video encoders:"); - List videoEncoders = CodecUtils.listVideoEncoders(); - if (videoEncoders.isEmpty()) { - builder.append("\n (none)"); - } else { - for (CodecUtils.DeviceEncoder encoder : videoEncoders) { - builder.append("\n --video-codec=").append(encoder.getCodec().getName()); - builder.append(" --video-encoder='").append(encoder.getInfo().getName()).append("'"); - } - } - return builder.toString(); - } - - public static String buildAudioEncoderListMessage() { - StringBuilder builder = new StringBuilder("List of audio encoders:"); - List audioEncoders = CodecUtils.listAudioEncoders(); - if (audioEncoders.isEmpty()) { - builder.append("\n (none)"); - } else { - for (CodecUtils.DeviceEncoder encoder : audioEncoders) { - builder.append("\n --audio-codec=").append(encoder.getCodec().getName()); - builder.append(" --audio-encoder='").append(encoder.getInfo().getName()).append("'"); - } - } - return builder.toString(); - } - private static MediaCodecInfo[] getEncoders(MediaCodecList codecs, String mimeType) { List result = new ArrayList<>(); for (MediaCodecInfo codecInfo : codecs.getCodecInfos()) { diff --git a/server/src/main/java/com/genymobile/scrcpy/LogUtils.java b/server/src/main/java/com/genymobile/scrcpy/LogUtils.java new file mode 100644 index 00000000..e74b7e97 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/LogUtils.java @@ -0,0 +1,38 @@ +package com.genymobile.scrcpy; + +import java.util.List; + +public final class LogUtils { + + private LogUtils() { + // not instantiable + } + + public static String buildVideoEncoderListMessage() { + StringBuilder builder = new StringBuilder("List of video encoders:"); + List videoEncoders = CodecUtils.listVideoEncoders(); + if (videoEncoders.isEmpty()) { + builder.append("\n (none)"); + } else { + for (CodecUtils.DeviceEncoder encoder : videoEncoders) { + builder.append("\n --video-codec=").append(encoder.getCodec().getName()); + builder.append(" --video-encoder='").append(encoder.getInfo().getName()).append("'"); + } + } + return builder.toString(); + } + + public static String buildAudioEncoderListMessage() { + StringBuilder builder = new StringBuilder("List of audio encoders:"); + List audioEncoders = CodecUtils.listAudioEncoders(); + if (audioEncoders.isEmpty()) { + builder.append("\n (none)"); + } else { + for (CodecUtils.DeviceEncoder encoder : audioEncoders) { + builder.append("\n --audio-codec=").append(encoder.getCodec().getName()); + builder.append(" --audio-encoder='").append(encoder.getInfo().getName()).append("'"); + } + } + return builder.toString(); + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index 668a4ed0..f5f996ba 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -202,7 +202,7 @@ public class ScreenEncoder implements Device.RotationListener { try { return MediaCodec.createByCodecName(encoderName); } catch (IllegalArgumentException e) { - Ln.e("Encoder '" + encoderName + "' for " + codec.getName() + " not found\n" + CodecUtils.buildVideoEncoderListMessage()); + Ln.e("Encoder '" + encoderName + "' for " + codec.getName() + " not found\n" + LogUtils.buildVideoEncoderListMessage()); throw new ConfigurationException("Unknown encoder: " + encoderName); } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index adfbef2a..f46cf308 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -359,8 +359,8 @@ public final class Server { CleanUp.unlinkSelf(); } - Ln.i(CodecUtils.buildVideoEncoderListMessage()); - Ln.i(CodecUtils.buildAudioEncoderListMessage()); + Ln.i(LogUtils.buildVideoEncoderListMessage()); + Ln.i(LogUtils.buildAudioEncoderListMessage()); // Just print the available encoders, do not mirror return; } From b65301f672e852fe2f3fded1fac2091c85679c35 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 23 Feb 2023 23:10:15 +0100 Subject: [PATCH 1414/2244] Add --list-displays Add an option to list the device displays properly. --- README.md | 2 +- app/data/bash-completion/scrcpy | 1 + app/data/zsh-completion/_scrcpy | 1 + app/scrcpy.1 | 9 ++++++--- app/src/cli.c | 16 ++++++++++++---- app/src/options.c | 1 + app/src/options.h | 1 + app/src/scrcpy.c | 3 ++- app/src/server.c | 7 +++++-- app/src/server.h | 1 + .../main/java/com/genymobile/scrcpy/Device.java | 14 +------------- .../java/com/genymobile/scrcpy/LogUtils.java | 15 +++++++++++++++ .../java/com/genymobile/scrcpy/Options.java | 9 +++++++++ .../main/java/com/genymobile/scrcpy/Server.java | 17 +++++++++++++---- 14 files changed, 69 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 5575fc4d..a2e275f9 100644 --- a/README.md +++ b/README.md @@ -718,7 +718,7 @@ scrcpy --display=1 The list of display ids can be retrieved by: ```bash -adb shell dumpsys display # search "mDisplayId=" in the output +scrcpy --list-displays ``` The secondary display may only be controlled if the device runs at least Android diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index 70695019..fa95ce6e 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -19,6 +19,7 @@ _scrcpy() { -K --hid-keyboard -h --help --legacy-paste + --list-displays --list-encoders --lock-video-orientation --lock-video-orientation= diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 268aa626..231405ce 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -26,6 +26,7 @@ arguments=( {-K,--hid-keyboard}'[Simulate a physical keyboard by using HID over AOAv2]' {-h,--help}'[Print the help]' '--legacy-paste[Inject computer clipboard text as a sequence of key events on Ctrl+v]' + '--list-displays[List displays available on the device]' '--list-encoders[List video and audio encoders available on the device]' '--lock-video-orientation=[Lock video orientation]:orientation:(unlocked initial 0 1 2 3)' '--max-fps=[Limit the frame rate of screen capture]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index add263c2..40b8158c 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -73,10 +73,9 @@ Disable screensaver while scrcpy is running. .TP .BI "\-\-display " id -Specify the display id to mirror. +Specify the device display id to mirror. -The list of possible display ids can be listed by "adb shell dumpsys display" -(search "mDisplayId=" in the output). +The available display ids can be listed by \-\-list\-displays. Default is 0. @@ -134,6 +133,10 @@ This is a workaround for some devices not behaving as expected when setting the .B \-\-list\-encoders List video and audio encoders available on the device. +.TP +.B \-\-list\-displays +List displays available on the device. + .TP \fB\-\-lock\-video\-orientation\fR[=\fIvalue\fR] Lock video orientation to \fIvalue\fR. Possible values are "unlocked", "initial" (locked to the initial orientation), 0, 1, 2 and 3. Natural device orientation is 0, and each increment adds a 90 degrees rotation counterclockwise. diff --git a/app/src/cli.c b/app/src/cli.c index edb694a6..8dfcdc79 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -69,6 +69,7 @@ enum { OPT_AUDIO_CODEC_OPTIONS, OPT_AUDIO_ENCODER, OPT_LIST_ENCODERS, + OPT_LIST_DISPLAYS, }; struct sc_option { @@ -198,10 +199,9 @@ static const struct sc_option options[] = { .longopt_id = OPT_DISPLAY_ID, .longopt = "display", .argdesc = "id", - .text = "Specify the display id to mirror.\n" - "The list of possible display ids can be listed by:\n" - " adb shell dumpsys display\n" - "(search \"mDisplayId=\" in the output)\n" + .text = "Specify the device display id to mirror.\n" + "The available display ids can be listed by:\n" + " scrcpy --list-displays\n" "Default is 0.", }, { @@ -272,6 +272,11 @@ static const struct sc_option options[] = { "This is a workaround for some devices not behaving as " "expected when setting the device clipboard programmatically.", }, + { + .longopt_id = OPT_LIST_DISPLAYS, + .longopt = "list-displays", + .text = "List device displays.", + }, { .longopt_id = OPT_LIST_ENCODERS, .longopt = "list-encoders", @@ -1803,6 +1808,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], case OPT_LIST_ENCODERS: opts->list_encoders = true; break; + case OPT_LIST_DISPLAYS: + opts->list_displays = true; + break; default: // getopt prints the error message on stderr return false; diff --git a/app/src/options.c b/app/src/options.c index 1839df6e..8560b37b 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -73,4 +73,5 @@ const struct scrcpy_options scrcpy_options_default = { .power_on = true, .audio = true, .list_encoders = false, + .list_displays = false, }; diff --git a/app/src/options.h b/app/src/options.h index 568b8155..a15d51f8 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -155,6 +155,7 @@ struct scrcpy_options { bool power_on; bool audio; bool list_encoders; + bool list_displays; }; extern const struct scrcpy_options scrcpy_options_default; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 5739c3b2..4d68fb29 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -353,6 +353,7 @@ scrcpy(struct scrcpy_options *options) { .cleanup = options->cleanup, .power_on = options->power_on, .list_encoders = options->list_encoders, + .list_displays = options->list_displays, }; static const struct sc_server_callbacks cbs = { @@ -370,7 +371,7 @@ scrcpy(struct scrcpy_options *options) { server_started = true; - if (options->list_encoders) { + if (options->list_encoders || options->list_displays) { bool ok = await_for_server(NULL); ret = ok ? SCRCPY_EXIT_SUCCESS : SCRCPY_EXIT_FAILURE; goto end; diff --git a/app/src/server.c b/app/src/server.c index 077614a8..9d4fb098 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -303,6 +303,9 @@ execute_server(struct sc_server *server, if (params->list_encoders) { ADD_PARAM("list_encoders=true"); } + if (params->list_displays) { + ADD_PARAM("list_displays=true"); + } #undef ADD_PARAM @@ -856,9 +859,9 @@ run_server(void *data) { goto error_connection_failed; } - // If --list-encoders is passed, then the server just prints the encoders + // If --list-* is passed, then the server just prints the requested data // then exits. - if (params->list_encoders) { + if (params->list_encoders || params->list_displays) { sc_pid pid = execute_server(server, params); if (pid == SC_PROCESS_NONE) { goto error_connection_failed; diff --git a/app/src/server.h b/app/src/server.h index ada04baa..8edf2666 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -56,6 +56,7 @@ struct sc_server_params { bool cleanup; bool power_on; bool list_encoders; + bool list_displays; }; struct sc_server { diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index c7f7c1f8..b66474b7 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -65,7 +65,7 @@ public final class Device { displayId = options.getDisplayId(); DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(displayId); if (displayInfo == null) { - Ln.e(buildUnknownDisplayIdMessage(displayId)); + Ln.e("Display " + displayId + " not found\n" + LogUtils.buildDisplayListMessage()); throw new ConfigurationException("Unknown display id: " + displayId); } @@ -130,18 +130,6 @@ public final class Device { } } - private static String buildUnknownDisplayIdMessage(int displayId) { - StringBuilder msg = new StringBuilder("Display ").append(displayId).append(" not found"); - int[] displayIds = ServiceManager.getDisplayManager().getDisplayIds(); - if (displayIds != null && displayIds.length > 0) { - msg.append("\nTry to use one of the available display ids:"); - for (int id : displayIds) { - msg.append("\n scrcpy --display=").append(id); - } - } - return msg.toString(); - } - public synchronized void setMaxSize(int newMaxSize) { maxSize = newMaxSize; screenInfo = ScreenInfo.computeScreenInfo(screenInfo.getReverseVideoRotation(), deviceSize, crop, newMaxSize, lockVideoOrientation); diff --git a/server/src/main/java/com/genymobile/scrcpy/LogUtils.java b/server/src/main/java/com/genymobile/scrcpy/LogUtils.java index e74b7e97..c073336d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/LogUtils.java +++ b/server/src/main/java/com/genymobile/scrcpy/LogUtils.java @@ -1,5 +1,7 @@ package com.genymobile.scrcpy; +import com.genymobile.scrcpy.wrappers.ServiceManager; + import java.util.List; public final class LogUtils { @@ -35,4 +37,17 @@ public final class LogUtils { } return builder.toString(); } + + public static String buildDisplayListMessage() { + StringBuilder builder = new StringBuilder("List of displays:"); + int[] displayIds = ServiceManager.getDisplayManager().getDisplayIds(); + if (displayIds == null || displayIds.length == 0) { + builder.append("\n (none)"); + } else { + for (int id : displayIds) { + builder.append("\n --display=").append(id); + } + } + return builder.toString(); + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 8cac5e2c..bcf235ed 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -34,6 +34,7 @@ public class Options { private boolean powerOn = true; private boolean listEncoders; + private boolean listDisplays; // Options not used by the scrcpy client, but useful to use scrcpy-server directly private boolean sendDeviceMeta = true; // send device name and size @@ -249,6 +250,14 @@ public class Options { this.listEncoders = listEncoders; } + public boolean getListDisplays() { + return listDisplays; + } + + public void setListDisplays(boolean listDisplays) { + this.listDisplays = listDisplays; + } + public boolean getSendDeviceMeta() { return sendDeviceMeta; } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index f46cf308..35da6965 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -295,6 +295,10 @@ public final class Server { boolean listEncoders = Boolean.parseBoolean(value); options.setListEncoders(listEncoders); break; + case "list_displays": + boolean listDisplays = Boolean.parseBoolean(value); + options.setListDisplays(listDisplays); + break; case "send_device_meta": boolean sendDeviceMeta = Boolean.parseBoolean(value); options.setSendDeviceMeta(sendDeviceMeta); @@ -354,14 +358,19 @@ public final class Server { Ln.initLogLevel(options.getLogLevel()); - if (options.getListEncoders()) { + if (options.getListEncoders() || options.getListDisplays()) { if (options.getCleanup()) { CleanUp.unlinkSelf(); } - Ln.i(LogUtils.buildVideoEncoderListMessage()); - Ln.i(LogUtils.buildAudioEncoderListMessage()); - // Just print the available encoders, do not mirror + if (options.getListEncoders()) { + Ln.i(LogUtils.buildVideoEncoderListMessage()); + Ln.i(LogUtils.buildAudioEncoderListMessage()); + } + if (options.getListDisplays()) { + Ln.i(LogUtils.buildDisplayListMessage()); + } + // Just print the requested data, do not mirror return; } From a205ff6c8b5ac1cae5a5b2763742247da84cd4b5 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 23 Feb 2023 23:12:24 +0100 Subject: [PATCH 1415/2244] Log display sizes in display list This is more convenient than just the display id alone. --- .../main/java/com/genymobile/scrcpy/LogUtils.java | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/LogUtils.java b/server/src/main/java/com/genymobile/scrcpy/LogUtils.java index c073336d..243a156b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/LogUtils.java +++ b/server/src/main/java/com/genymobile/scrcpy/LogUtils.java @@ -1,5 +1,6 @@ package com.genymobile.scrcpy; +import com.genymobile.scrcpy.wrappers.DisplayManager; import com.genymobile.scrcpy.wrappers.ServiceManager; import java.util.List; @@ -40,12 +41,21 @@ public final class LogUtils { public static String buildDisplayListMessage() { StringBuilder builder = new StringBuilder("List of displays:"); - int[] displayIds = ServiceManager.getDisplayManager().getDisplayIds(); + DisplayManager displayManager = ServiceManager.getDisplayManager(); + int[] displayIds = displayManager.getDisplayIds(); if (displayIds == null || displayIds.length == 0) { builder.append("\n (none)"); } else { for (int id : displayIds) { - builder.append("\n --display=").append(id); + builder.append("\n --display=").append(id).append(" ("); + DisplayInfo displayInfo = displayManager.getDisplayInfo(id); + if (displayInfo != null) { + Size size = displayInfo.getSize(); + builder.append(size.getWidth()).append("x").append(size.getHeight()); + } else { + builder.append("size unknown"); + } + builder.append(")"); } } return builder.toString(); From 99837fa600af9f128fa0830b9b007a6944b9808f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 24 Feb 2023 21:13:56 +0100 Subject: [PATCH 1416/2244] Rename decoder to video_decoder This prepares the introduction of audio_decoder. PR #3757 --- app/src/scrcpy.c | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 4d68fb29..578943f9 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -42,7 +42,7 @@ struct scrcpy { struct sc_screen screen; struct sc_demuxer video_demuxer; struct sc_demuxer audio_demuxer; - struct sc_decoder decoder; + struct sc_decoder video_decoder; struct sc_recorder recorder; #ifdef HAVE_V4L2 struct sc_v4l2_sink v4l2_sink; @@ -436,13 +436,13 @@ scrcpy(struct scrcpy_options *options) { &audio_demuxer_cbs, NULL); } - bool needs_decoder = options->display; + bool needs_video_decoder = options->display; #ifdef HAVE_V4L2 - needs_decoder |= !!options->v4l2_device; + needs_video_decoder |= !!options->v4l2_device; #endif - if (needs_decoder) { - sc_decoder_init(&s->decoder); - sc_demuxer_add_sink(&s->video_demuxer, &s->decoder.packet_sink); + if (needs_video_decoder) { + sc_decoder_init(&s->video_decoder); + sc_demuxer_add_sink(&s->video_demuxer, &s->video_decoder.packet_sink); } if (options->record_filename) { @@ -656,7 +656,7 @@ aoa_hid_end: } screen_initialized = true; - sc_decoder_add_sink(&s->decoder, &s->screen.frame_sink); + sc_decoder_add_sink(&s->video_decoder, &s->screen.frame_sink); } #ifdef HAVE_V4L2 @@ -666,7 +666,7 @@ aoa_hid_end: goto end; } - sc_decoder_add_sink(&s->decoder, &s->v4l2_sink.frame_sink); + sc_decoder_add_sink(&s->video_decoder, &s->v4l2_sink.frame_sink); v4l2_sink_initialized = true; } From 05f0e35d2a9074ecb214d70303f3f271631af645 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 24 Feb 2023 21:22:35 +0100 Subject: [PATCH 1417/2244] Give a name to decoder instances This will be useful in logs. PR #3757 --- app/src/decoder.c | 11 +++++++---- app/src/decoder.h | 5 ++++- app/src/scrcpy.c | 2 +- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/app/src/decoder.c b/app/src/decoder.c index 337aa329..d750253c 100644 --- a/app/src/decoder.c +++ b/app/src/decoder.c @@ -48,7 +48,7 @@ sc_decoder_open(struct sc_decoder *decoder, const AVCodec *codec) { decoder->codec_ctx->flags |= AV_CODEC_FLAG_LOW_DELAY; if (avcodec_open2(decoder->codec_ctx, codec, NULL) < 0) { - LOGE("Could not open codec"); + LOGE("Decoder '%s': could not open codec", decoder->name); avcodec_free_context(&decoder->codec_ctx); return false; } @@ -101,7 +101,8 @@ sc_decoder_push(struct sc_decoder *decoder, const AVPacket *packet) { int ret = avcodec_send_packet(decoder->codec_ctx, packet); if (ret < 0 && ret != AVERROR(EAGAIN)) { - LOGE("Could not send video packet: %d", ret); + LOGE("Decoder '%s': could not send video packet: %d", + decoder->name, ret); return false; } ret = avcodec_receive_frame(decoder->codec_ctx, decoder->frame); @@ -114,7 +115,8 @@ sc_decoder_push(struct sc_decoder *decoder, const AVPacket *packet) { av_frame_unref(decoder->frame); } else if (ret != AVERROR(EAGAIN)) { - LOGE("Could not receive video frame: %d", ret); + LOGE("Decoder '%s', could not receive video frame: %d", + decoder->name, ret); return false; } return true; @@ -140,7 +142,8 @@ sc_decoder_packet_sink_push(struct sc_packet_sink *sink, } void -sc_decoder_init(struct sc_decoder *decoder) { +sc_decoder_init(struct sc_decoder *decoder, const char *name) { + decoder->name = name; // statically allocated decoder->sink_count = 0; static const struct sc_packet_sink_ops ops = { diff --git a/app/src/decoder.h b/app/src/decoder.h index 16adc5ec..aace1af6 100644 --- a/app/src/decoder.h +++ b/app/src/decoder.h @@ -14,6 +14,8 @@ struct sc_decoder { struct sc_packet_sink packet_sink; // packet sink trait + const char *name; // must be statically allocated (e.g. a string literal) + struct sc_frame_sink *sinks[SC_DECODER_MAX_SINKS]; unsigned sink_count; @@ -21,8 +23,9 @@ struct sc_decoder { AVFrame *frame; }; +// The name must be statically allocated (e.g. a string literal) void -sc_decoder_init(struct sc_decoder *decoder); +sc_decoder_init(struct sc_decoder *decoder, const char *name); void sc_decoder_add_sink(struct sc_decoder *decoder, struct sc_frame_sink *sink); diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 578943f9..944d5f05 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -441,7 +441,7 @@ scrcpy(struct scrcpy_options *options) { needs_video_decoder |= !!options->v4l2_device; #endif if (needs_video_decoder) { - sc_decoder_init(&s->video_decoder); + sc_decoder_init(&s->video_decoder, "video"); sc_demuxer_add_sink(&s->video_demuxer, &s->video_decoder.packet_sink); } From e22660d698900f949cfc6afdb41d49cb371f7cff Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 24 Feb 2023 21:31:39 +0100 Subject: [PATCH 1418/2244] Add an audio decoder PR #3757 --- app/src/scrcpy.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 944d5f05..eb70749a 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -43,6 +43,7 @@ struct scrcpy { struct sc_demuxer video_demuxer; struct sc_demuxer audio_demuxer; struct sc_decoder video_decoder; + struct sc_decoder audio_decoder; struct sc_recorder recorder; #ifdef HAVE_V4L2 struct sc_v4l2_sink v4l2_sink; @@ -437,6 +438,7 @@ scrcpy(struct scrcpy_options *options) { } bool needs_video_decoder = options->display; + bool needs_audio_decoder = options->audio && options->display; #ifdef HAVE_V4L2 needs_video_decoder |= !!options->v4l2_device; #endif @@ -444,6 +446,10 @@ scrcpy(struct scrcpy_options *options) { sc_decoder_init(&s->video_decoder, "video"); sc_demuxer_add_sink(&s->video_demuxer, &s->video_decoder.packet_sink); } + if (needs_audio_decoder) { + sc_decoder_init(&s->audio_decoder, "audio"); + sc_demuxer_add_sink(&s->audio_demuxer, &s->audio_decoder.packet_sink); + } if (options->record_filename) { static const struct sc_recorder_callbacks recorder_cbs = { From 619730edafe7ab2fd2fb022f9eb6e350bfe1bb52 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 25 Feb 2023 16:19:58 +0100 Subject: [PATCH 1419/2244] Pass AVCodecContext to frame sinks Frame consumers may need details about the frame format. PR #3757 --- app/src/decoder.c | 11 ++++++++--- app/src/screen.c | 6 +++++- app/src/trait/frame_sink.h | 3 ++- app/src/v4l2_sink.c | 9 ++++++--- 4 files changed, 21 insertions(+), 8 deletions(-) diff --git a/app/src/decoder.c b/app/src/decoder.c index d750253c..96d4a010 100644 --- a/app/src/decoder.c +++ b/app/src/decoder.c @@ -25,10 +25,10 @@ sc_decoder_close_sinks(struct sc_decoder *decoder) { } static bool -sc_decoder_open_sinks(struct sc_decoder *decoder) { +sc_decoder_open_sinks(struct sc_decoder *decoder, const AVCodecContext *ctx) { for (unsigned i = 0; i < decoder->sink_count; ++i) { struct sc_frame_sink *sink = decoder->sinks[i]; - if (!sink->ops->open(sink)) { + if (!sink->ops->open(sink, ctx)) { sc_decoder_close_first_sinks(decoder, i); return false; } @@ -47,6 +47,11 @@ sc_decoder_open(struct sc_decoder *decoder, const AVCodec *codec) { decoder->codec_ctx->flags |= AV_CODEC_FLAG_LOW_DELAY; + if (codec->type == AVMEDIA_TYPE_VIDEO) { + // Hardcoded video properties + decoder->codec_ctx->pix_fmt = AV_PIX_FMT_YUV420P; + } + if (avcodec_open2(decoder->codec_ctx, codec, NULL) < 0) { LOGE("Decoder '%s': could not open codec", decoder->name); avcodec_free_context(&decoder->codec_ctx); @@ -61,7 +66,7 @@ sc_decoder_open(struct sc_decoder *decoder, const AVCodec *codec) { return false; } - if (!sc_decoder_open_sinks(decoder)) { + if (!sc_decoder_open_sinks(decoder, decoder->codec_ctx)) { av_frame_free(&decoder->frame); avcodec_close(decoder->codec_ctx); avcodec_free_context(&decoder->codec_ctx); diff --git a/app/src/screen.c b/app/src/screen.c index 425ba2c3..a9a48eae 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -330,7 +330,11 @@ event_watcher(void *data, SDL_Event *event) { #endif static bool -sc_screen_frame_sink_open(struct sc_frame_sink *sink) { +sc_screen_frame_sink_open(struct sc_frame_sink *sink, + const AVCodecContext *ctx) { + assert(ctx->pix_fmt == AV_PIX_FMT_YUV420P); + (void) ctx; + struct sc_screen *screen = DOWNCAST(sink); (void) screen; #ifndef NDEBUG diff --git a/app/src/trait/frame_sink.h b/app/src/trait/frame_sink.h index 0214ab3e..30bf0d37 100644 --- a/app/src/trait/frame_sink.h +++ b/app/src/trait/frame_sink.h @@ -5,6 +5,7 @@ #include #include +#include typedef struct AVFrame AVFrame; @@ -18,7 +19,7 @@ struct sc_frame_sink { }; struct sc_frame_sink_ops { - bool (*open)(struct sc_frame_sink *sink); + bool (*open)(struct sc_frame_sink *sink, const AVCodecContext *ctx); void (*close)(struct sc_frame_sink *sink); bool (*push)(struct sc_frame_sink *sink, const AVFrame *frame); }; diff --git a/app/src/v4l2_sink.c b/app/src/v4l2_sink.c index 9a0011f2..ba876b2b 100644 --- a/app/src/v4l2_sink.c +++ b/app/src/v4l2_sink.c @@ -156,7 +156,10 @@ sc_video_buffer_on_new_frame(struct sc_video_buffer *vb, bool previous_skipped, } static bool -sc_v4l2_sink_open(struct sc_v4l2_sink *vs) { +sc_v4l2_sink_open(struct sc_v4l2_sink *vs, const AVCodecContext *ctx) { + assert(ctx->pix_fmt == AV_PIX_FMT_YUV420P); + (void) ctx; + static const struct sc_video_buffer_callbacks cbs = { .on_new_frame = sc_video_buffer_on_new_frame, }; @@ -336,9 +339,9 @@ sc_v4l2_sink_push(struct sc_v4l2_sink *vs, const AVFrame *frame) { } static bool -sc_v4l2_frame_sink_open(struct sc_frame_sink *sink) { +sc_v4l2_frame_sink_open(struct sc_frame_sink *sink, const AVCodecContext *ctx) { struct sc_v4l2_sink *vs = DOWNCAST(sink); - return sc_v4l2_sink_open(vs); + return sc_v4l2_sink_open(vs, ctx); } static void From 20d41fdd7e236b33fc70a12758dacdf47099c83e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 25 Feb 2023 14:32:02 +0100 Subject: [PATCH 1420/2244] Introduce bytebuf util Add a ring-buffer for bytes. It will be useful for audio buffering. PR #3757 --- app/meson.build | 5 +++ app/src/util/bytebuf.c | 75 +++++++++++++++++++++++++++++++++ app/src/util/bytebuf.h | 90 ++++++++++++++++++++++++++++++++++++++++ app/tests/test_bytebuf.c | 82 ++++++++++++++++++++++++++++++++++++ 4 files changed, 252 insertions(+) create mode 100644 app/src/util/bytebuf.c create mode 100644 app/src/util/bytebuf.h create mode 100644 app/tests/test_bytebuf.c diff --git a/app/meson.build b/app/meson.build index 2ea3b317..8be917e9 100644 --- a/app/meson.build +++ b/app/meson.build @@ -30,6 +30,7 @@ src = [ 'src/version.c', 'src/video_buffer.c', 'src/util/acksync.c', + 'src/util/bytebuf.c', 'src/util/file.c', 'src/util/intmap.c', 'src/util/intr.c', @@ -254,6 +255,10 @@ if get_option('buildtype') == 'debug' ['test_binary', [ 'tests/test_binary.c', ]], + ['test_bytebuf', [ + 'tests/test_bytebuf.c', + 'src/util/bytebuf.c', + ]], ['test_cbuf', [ 'tests/test_cbuf.c', ]], diff --git a/app/src/util/bytebuf.c b/app/src/util/bytebuf.c new file mode 100644 index 00000000..61814376 --- /dev/null +++ b/app/src/util/bytebuf.c @@ -0,0 +1,75 @@ +#include "bytebuf.h" + +#include +#include +#include + +#include "util/log.h" + +bool +sc_bytebuf_init(struct sc_bytebuf *buf, size_t alloc_size) { + assert(alloc_size); + buf->data = malloc(alloc_size); + if (!buf->data) { + LOG_OOM(); + return false; + } + + buf->alloc_size = alloc_size; + buf->head = 0; + buf->tail = 0; + + return true; +} + +void +sc_bytebuf_destroy(struct sc_bytebuf *buf) { + free(buf->data); +} + +void +sc_bytebuf_read(struct sc_bytebuf *buf, uint8_t *to, size_t len) { + assert(len); + assert(len <= sc_bytebuf_read_available(buf)); + assert(buf->tail != buf->head); // the buffer could not be empty + + size_t right_limit = buf->tail < buf->head ? buf->head : buf->alloc_size; + size_t right_len = right_limit - buf->tail; + if (len < right_len) { + right_len = len; + } + memcpy(to, buf->data + buf->tail, right_len); + + if (len > right_len) { + memcpy(to + right_len, buf->data, len - right_len); + } + + buf->tail = (buf->tail + len) % buf->alloc_size; +} + +void +sc_bytebuf_skip(struct sc_bytebuf *buf, size_t len) { + assert(len); + assert(len <= sc_bytebuf_read_available(buf)); + assert(buf->tail != buf->head); // the buffer could not be empty + + buf->tail = (buf->tail + len) % buf->alloc_size; +} + +void +sc_bytebuf_write(struct sc_bytebuf *buf, const uint8_t *from, size_t len) { + assert(len); + assert(len <= sc_bytebuf_write_available(buf)); + + size_t right_len = buf->alloc_size - buf->head; + if (len < right_len) { + right_len = len; + } + memcpy(buf->data + buf->head, from, right_len); + + if (len > right_len) { + memcpy(buf->data, from + right_len, len - right_len); + } + + buf->head = (buf->head + len) % buf->alloc_size; +} diff --git a/app/src/util/bytebuf.h b/app/src/util/bytebuf.h new file mode 100644 index 00000000..fcebc2d3 --- /dev/null +++ b/app/src/util/bytebuf.h @@ -0,0 +1,90 @@ +#ifndef SC_BYTEBUF_H +#define SC_BYTEBUF_H + +#include "common.h" + +#include +#include + +struct sc_bytebuf { + uint8_t *data; + // The actual capacity is (allocated - 1) so that head == tail is + // non-ambiguous + size_t alloc_size; + size_t head; // writter cursor + size_t tail; // reader cursor + // empty: tail == head + // full: ((tail + 1) % alloc_size) == head +}; + +bool +sc_bytebuf_init(struct sc_bytebuf *buf, size_t alloc_size); + +/** + * Copy from the bytebuf to a user-provided array + * + * The caller must check that len <= sc_bytebuf_read_available() (it is an + * error to attempt to read more bytes than available). + * + * This function is guaranteed not to write to buf->head. + */ +void +sc_bytebuf_read(struct sc_bytebuf *buf, uint8_t *to, size_t len); + +/** + * Drop len bytes from the buffer + * + * The caller must check that len <= sc_bytebuf_read_available() (it is an + * error to attempt to skip more bytes than available). + * + * This function is guaranteed not to write to buf->head. + * + * It is equivalent to call sc_bytebuf_read() to some array and discard the + * array (but this function is more efficient since there is no copy). + */ +void +sc_bytebuf_skip(struct sc_bytebuf *buf, size_t len); + +/** + * Copy the user-provided array to the bytebuf + * + * The caller must check that len <= sc_bytebuf_write_available() (it is an + * error to write more bytes than the remaining available space). + * + * This function is guaranteed not to write to buf->tail. + */ +void +sc_bytebuf_write(struct sc_bytebuf *buf, const uint8_t *from, size_t len); + +/** + * Return the number of bytes which can be read + * + * It is an error to read more bytes than available. + */ +static inline size_t +sc_bytebuf_read_available(struct sc_bytebuf *buf) { + return (buf->alloc_size + buf->head - buf->tail) % buf->alloc_size; +} + +/** + * Return the number of bytes which can be written + * + * It is an error to write more bytes than available. + */ +static inline size_t +sc_bytebuf_write_available(struct sc_bytebuf *buf) { + return (buf->alloc_size + buf->tail - buf->head - 1) % buf->alloc_size; +} + +/** + * Return the actual capacity of the buffer (read available + write available) + */ +static inline size_t +sc_bytebuf_capacity(struct sc_bytebuf *buf) { + return buf->alloc_size - 1; +} + +void +sc_bytebuf_destroy(struct sc_bytebuf *buf); + +#endif diff --git a/app/tests/test_bytebuf.c b/app/tests/test_bytebuf.c new file mode 100644 index 00000000..fbb33765 --- /dev/null +++ b/app/tests/test_bytebuf.c @@ -0,0 +1,82 @@ +#include "common.h" + +#include +#include + +#include "util/bytebuf.h" + +void test_bytebuf_simple(void) { + struct sc_bytebuf buf; + uint8_t data[20]; + + bool ok = sc_bytebuf_init(&buf, 20); + assert(ok); + + sc_bytebuf_write(&buf, (uint8_t *) "hello", sizeof("hello") - 1); + assert(sc_bytebuf_read_available(&buf) == 5); + + sc_bytebuf_read(&buf, data, 4); + assert(!strncmp((char *) data, "hell", 4)); + + sc_bytebuf_write(&buf, (uint8_t *) " world", sizeof(" world") - 1); + assert(sc_bytebuf_read_available(&buf) == 7); + + sc_bytebuf_write(&buf, (uint8_t *) "!", 1); + assert(sc_bytebuf_read_available(&buf) == 8); + + sc_bytebuf_read(&buf, &data[4], 8); + assert(sc_bytebuf_read_available(&buf) == 0); + + data[12] = '\0'; + assert(!strcmp((char *) data, "hello world!")); + assert(sc_bytebuf_read_available(&buf) == 0); + + sc_bytebuf_destroy(&buf); +} + +void test_bytebuf_boundaries(void) { + struct sc_bytebuf buf; + uint8_t data[20]; + + bool ok = sc_bytebuf_init(&buf, 20); + assert(ok); + + sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1); + assert(sc_bytebuf_read_available(&buf) == 6); + + sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1); + assert(sc_bytebuf_read_available(&buf) == 12); + + sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1); + assert(sc_bytebuf_read_available(&buf) == 18); + + sc_bytebuf_read(&buf, data, 9); + assert(!strncmp((char *) data, "hello hel", 9)); + assert(sc_bytebuf_read_available(&buf) == 9); + + sc_bytebuf_write(&buf, (uint8_t *) "world", sizeof("world") - 1); + assert(sc_bytebuf_read_available(&buf) == 14); + + sc_bytebuf_write(&buf, (uint8_t *) "!", 1); + assert(sc_bytebuf_read_available(&buf) == 15); + + sc_bytebuf_skip(&buf, 3); + assert(sc_bytebuf_read_available(&buf) == 12); + + sc_bytebuf_read(&buf, data, 12); + data[12] = '\0'; + assert(!strcmp((char *) data, "hello world!")); + assert(sc_bytebuf_read_available(&buf) == 0); + + sc_bytebuf_destroy(&buf); +} + +int main(int argc, char *argv[]) { + (void) argc; + (void) argv; + + test_bytebuf_simple(); + test_bytebuf_boundaries(); + + return 0; +} From b60a8aa657f62a596e528f244f1c101dc2f054b7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 25 Feb 2023 18:45:05 +0100 Subject: [PATCH 1421/2244] Add two-step write feature to bytebuf If there is exactly one producer, then it can assume that the remaining space in the buffer will only increase until it writes something. This assumption may allow the producer to write to the buffer (up to a known safe size) without any synchronization mechanism, thus allowing to read and write different parts of the buffer in parallel. The producer can then commit the write with a lock held, and update its knowledge of the safe empty remaining space. PR #3757 --- app/src/util/bytebuf.c | 39 ++++++++++++++++++++++++++++++----- app/src/util/bytebuf.h | 24 ++++++++++++++++++++++ app/tests/test_bytebuf.c | 44 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 102 insertions(+), 5 deletions(-) diff --git a/app/src/util/bytebuf.c b/app/src/util/bytebuf.c index 61814376..eac69e9c 100644 --- a/app/src/util/bytebuf.c +++ b/app/src/util/bytebuf.c @@ -56,11 +56,9 @@ sc_bytebuf_skip(struct sc_bytebuf *buf, size_t len) { buf->tail = (buf->tail + len) % buf->alloc_size; } -void -sc_bytebuf_write(struct sc_bytebuf *buf, const uint8_t *from, size_t len) { - assert(len); - assert(len <= sc_bytebuf_write_available(buf)); - +static inline void +sc_bytebuf_write_step0(struct sc_bytebuf *buf, const uint8_t *from, + size_t len) { size_t right_len = buf->alloc_size - buf->head; if (len < right_len) { right_len = len; @@ -70,6 +68,37 @@ sc_bytebuf_write(struct sc_bytebuf *buf, const uint8_t *from, size_t len) { if (len > right_len) { memcpy(buf->data, from + right_len, len - right_len); } +} +static inline void +sc_bytebuf_write_step1(struct sc_bytebuf *buf, size_t len) { buf->head = (buf->head + len) % buf->alloc_size; } + +void +sc_bytebuf_write(struct sc_bytebuf *buf, const uint8_t *from, size_t len) { + assert(len); + assert(len <= sc_bytebuf_write_available(buf)); + + sc_bytebuf_write_step0(buf, from, len); + sc_bytebuf_write_step1(buf, len); +} + +void +sc_bytebuf_prepare_write(struct sc_bytebuf *buf, const uint8_t *from, + size_t len) { + // *This function MUST NOT access buf->tail (even in assert()).* + // The purpose of this function is to allow a reader and a writer to access + // different parts of the buffer in parallel simultaneously. It is intended + // to be called without lock (only sc_bytebuf_commit_write() is intended to + // be called with lock held). + + assert(len < buf->alloc_size - 1); + sc_bytebuf_write_step0(buf, from, len); +} + +void +sc_bytebuf_commit_write(struct sc_bytebuf *buf, size_t len) { + assert(len <= sc_bytebuf_write_available(buf)); + sc_bytebuf_write_step1(buf, len); +} diff --git a/app/src/util/bytebuf.h b/app/src/util/bytebuf.h index fcebc2d3..e8279ef8 100644 --- a/app/src/util/bytebuf.h +++ b/app/src/util/bytebuf.h @@ -56,6 +56,30 @@ sc_bytebuf_skip(struct sc_bytebuf *buf, size_t len); void sc_bytebuf_write(struct sc_bytebuf *buf, const uint8_t *from, size_t len); +/** + * Copy the user-provided array to the bytebuf, but do not advance the cursor + * + * The caller must check that len <= sc_bytebuf_write_available() (it is an + * error to write more bytes than the remaining available space). + * + * After this function is called, the write must be committed with + * sc_bytebuf_commit_write(). + * + * The purpose of this mechanism is to acquire a lock only to commit the write, + * but not to perform the actual copy. + * + * This function is guaranteed not to access buf->tail. + */ +void +sc_bytebuf_prepare_write(struct sc_bytebuf *buf, const uint8_t *from, + size_t len); + +/** + * Commit a prepared write + */ +void +sc_bytebuf_commit_write(struct sc_bytebuf *buf, size_t len); + /** * Return the number of bytes which can be read * diff --git a/app/tests/test_bytebuf.c b/app/tests/test_bytebuf.c index fbb33765..75af3073 100644 --- a/app/tests/test_bytebuf.c +++ b/app/tests/test_bytebuf.c @@ -71,12 +71,56 @@ void test_bytebuf_boundaries(void) { sc_bytebuf_destroy(&buf); } +void test_bytebuf_two_steps_write(void) { + struct sc_bytebuf buf; + uint8_t data[20]; + + bool ok = sc_bytebuf_init(&buf, 20); + assert(ok); + + sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1); + assert(sc_bytebuf_read_available(&buf) == 6); + + sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1); + assert(sc_bytebuf_read_available(&buf) == 12); + + sc_bytebuf_prepare_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1); + assert(sc_bytebuf_read_available(&buf) == 12); // write not committed yet + + sc_bytebuf_read(&buf, data, 9); + assert(!strncmp((char *) data, "hello hel", 3)); + assert(sc_bytebuf_read_available(&buf) == 3); + + sc_bytebuf_commit_write(&buf, sizeof("hello ") - 1); + assert(sc_bytebuf_read_available(&buf) == 9); + + sc_bytebuf_prepare_write(&buf, (uint8_t *) "world", sizeof("world") - 1); + assert(sc_bytebuf_read_available(&buf) == 9); // write not committed yet + + sc_bytebuf_commit_write(&buf, sizeof("world") - 1); + assert(sc_bytebuf_read_available(&buf) == 14); + + sc_bytebuf_write(&buf, (uint8_t *) "!", 1); + assert(sc_bytebuf_read_available(&buf) == 15); + + sc_bytebuf_skip(&buf, 3); + assert(sc_bytebuf_read_available(&buf) == 12); + + sc_bytebuf_read(&buf, data, 12); + data[12] = '\0'; + assert(!strcmp((char *) data, "hello world!")); + assert(sc_bytebuf_read_available(&buf) == 0); + + sc_bytebuf_destroy(&buf); +} + int main(int argc, char *argv[]) { (void) argc; (void) argv; test_bytebuf_simple(); test_bytebuf_boundaries(); + test_bytebuf_two_steps_write(); return 0; } From de40cac6ad929f1d150b332f0e987c0d70ba5d38 Mon Sep 17 00:00:00 2001 From: Simon Chan <1330321+yume-chan@users.noreply.github.com> Date: Mon, 30 Jan 2023 00:42:09 +0800 Subject: [PATCH 1422/2244] Add workaround to capture audio on Android 11 On Android 11, it is possible to start the capture only when the running app is in foreground. But scrcpy is not an app, it's a Java application started from shell. As a workaround, start an existing Android shell existing activity just to start the capture, then close it immediately. PR #3757 Co-authored-by: Romain Vimont Signed-off-by: Romain Vimont --- .../AudioCaptureForegroundException.java | 7 +++ .../com/genymobile/scrcpy/AudioEncoder.java | 51 ++++++++++++++-- .../scrcpy/wrappers/ActivityManager.java | 60 +++++++++++++++++++ 3 files changed, 114 insertions(+), 4 deletions(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/AudioCaptureForegroundException.java diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioCaptureForegroundException.java b/server/src/main/java/com/genymobile/scrcpy/AudioCaptureForegroundException.java new file mode 100644 index 00000000..baa7d846 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/AudioCaptureForegroundException.java @@ -0,0 +1,7 @@ +package com.genymobile.scrcpy; + +/** + * Exception thrown if audio capture failed on Android 11 specifically because the running App (shell) was not in foreground. + */ +public class AudioCaptureForegroundException extends Exception { +} diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java b/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java index 1ce4107f..cc786bdb 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java @@ -1,7 +1,11 @@ package com.genymobile.scrcpy; +import com.genymobile.scrcpy.wrappers.ServiceManager; + import android.annotation.SuppressLint; import android.annotation.TargetApi; +import android.content.ComponentName; +import android.content.Intent; import android.media.AudioFormat; import android.media.AudioRecord; import android.media.AudioTimestamp; @@ -12,6 +16,7 @@ import android.os.Build; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; +import android.os.SystemClock; import java.io.IOException; import java.nio.ByteBuffer; @@ -179,7 +184,7 @@ public final class AudioEncoder { thread = new Thread(() -> { try { encode(); - } catch (ConfigurationException e) { + } catch (ConfigurationException | AudioCaptureForegroundException e) { // Do not print stack trace, a user-friendly error-message has already been logged } catch (IOException e) { Ln.e("Audio encoding error", e); @@ -218,8 +223,34 @@ public final class AudioEncoder { } } + private static void startWorkaroundAndroid11() { + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { + // Android 11 requires Apps to be at foreground to record audio. + // Normally, each App has its own user ID, so Android checks whether the requesting App has the user ID that's at the foreground. + // But scrcpy server is NOT an App, it's a Java application started from Android shell, so it has the same user ID (2000) with Android + // shell ("com.android.shell"). + // If there is an Activity from Android shell running at foreground, then the permission system will believe scrcpy is also in the + // foreground. + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { + Intent intent = new Intent(Intent.ACTION_MAIN); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.addCategory(Intent.CATEGORY_LAUNCHER); + intent.setComponent(new ComponentName(FakeContext.PACKAGE_NAME, "com.android.shell.HeapDumpActivity")); + ServiceManager.getActivityManager().startActivityAsUserWithFeature(intent); + // Wait for activity to start + SystemClock.sleep(150); + } + } + } + + private static void stopWorkaroundAndroid11() { + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { + ServiceManager.getActivityManager().forceStopPackage(FakeContext.PACKAGE_NAME); + } + } + @TargetApi(Build.VERSION_CODES.M) - public void encode() throws IOException, ConfigurationException { + public void encode() throws IOException, ConfigurationException, AudioCaptureForegroundException { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { Ln.w("Audio disabled: it is not supported before Android 11"); streamer.writeDisableStream(false); @@ -242,8 +273,20 @@ public final class AudioEncoder { mediaCodec.setCallback(new EncoderCallback(), new Handler(mediaCodecThread.getLooper())); mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); - recorder = createAudioRecord(); - recorder.startRecording(); + startWorkaroundAndroid11(); + try { + recorder = createAudioRecord(); + recorder.startRecording(); + } catch (UnsupportedOperationException e) { + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { + Ln.e("Failed to start audio capture"); + Ln.e("On Android 11, it is only possible to capture in foreground, make sure that the device is unlocked when starting scrcpy."); + throw new AudioCaptureForegroundException(); + } + throw e; + } finally { + stopWorkaroundAndroid11(); + } recorderStarted = true; final MediaCodec mediaCodecRef = mediaCodec; diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java index 76aab5f1..aaf83d66 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java @@ -3,7 +3,12 @@ package com.genymobile.scrcpy.wrappers; import com.genymobile.scrcpy.FakeContext; import com.genymobile.scrcpy.Ln; +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.content.Intent; import android.os.Binder; +import android.os.Build; +import android.os.Bundle; import android.os.IBinder; import android.os.IInterface; @@ -11,12 +16,15 @@ import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +@SuppressLint("PrivateApi,DiscouragedPrivateApi") public class ActivityManager { private final IInterface manager; private Method getContentProviderExternalMethod; private boolean getContentProviderExternalMethodNewVersion = true; private Method removeContentProviderExternalMethod; + private Method startActivityAsUserWithFeatureMethod; + private Method forceStopPackageMethod; public ActivityManager(IInterface manager) { this.manager = manager; @@ -43,6 +51,7 @@ public class ActivityManager { return removeContentProviderExternalMethod; } + @TargetApi(Build.VERSION_CODES.Q) private ContentProvider getContentProviderExternal(String name, IBinder token) { try { Method method = getGetContentProviderExternalMethod(); @@ -85,4 +94,55 @@ public class ActivityManager { public ContentProvider createSettingsProvider() { return getContentProviderExternal("settings", new Binder()); } + + private Method getStartActivityAsUserWithFeatureMethod() throws NoSuchMethodException, ClassNotFoundException { + if (startActivityAsUserWithFeatureMethod == null) { + Class iApplicationThreadClass = Class.forName("android.app.IApplicationThread"); + Class profilerInfo = Class.forName("android.app.ProfilerInfo"); + startActivityAsUserWithFeatureMethod = manager.getClass() + .getMethod("startActivityAsUserWithFeature", iApplicationThreadClass, String.class, String.class, Intent.class, String.class, + IBinder.class, String.class, int.class, int.class, profilerInfo, Bundle.class, int.class); + } + return startActivityAsUserWithFeatureMethod; + } + + @SuppressWarnings("ConstantConditions") + public int startActivityAsUserWithFeature(Intent intent) { + try { + Method method = getStartActivityAsUserWithFeatureMethod(); + return (int) method.invoke( + /* this */ manager, + /* caller */ null, + /* callingPackage */ FakeContext.PACKAGE_NAME, + /* callingFeatureId */ null, + /* intent */ intent, + /* resolvedType */ null, + /* resultTo */ null, + /* resultWho */ null, + /* requestCode */ 0, + /* startFlags */ 0, + /* profilerInfo */ null, + /* bOptions */ null, + /* userId */ /* UserHandle.USER_CURRENT */ -2); + } catch (Throwable e) { + Ln.e("Could not invoke method", e); + return 0; + } + } + + private Method getForceStopPackageMethod() throws NoSuchMethodException { + if (forceStopPackageMethod == null) { + forceStopPackageMethod = manager.getClass().getMethod("forceStopPackage", String.class, int.class); + } + return forceStopPackageMethod; + } + + public void forceStopPackage(String packageName) { + try { + Method method = getForceStopPackageMethod(); + method.invoke(manager, packageName, /* userId */ /* UserHandle.USER_CURRENT */ -2); + } catch (Throwable e) { + Ln.e("Could not invoke method", e); + } + } } From c1528cdca92d72d98f8f0d1b5cd088a1dd79467c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 28 Feb 2023 21:19:43 +0100 Subject: [PATCH 1423/2244] Add --require-audio By default, scrcpy mirrors only the video when audio capture fails on the device. Add an option to force scrcpy to fail if audio is enabled but does not work. PR #3757 --- app/data/bash-completion/scrcpy | 1 + app/data/zsh-completion/_scrcpy | 1 + app/scrcpy.1 | 4 ++++ app/src/cli.c | 11 +++++++++++ app/src/demuxer.c | 9 ++++----- app/src/demuxer.h | 9 ++++++++- app/src/options.c | 1 + app/src/options.h | 1 + app/src/scrcpy.c | 30 +++++++++++++++--------------- 9 files changed, 46 insertions(+), 21 deletions(-) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index fa95ce6e..74c3ee57 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -45,6 +45,7 @@ _scrcpy() { -r --record= --record-format= --render-driver= + --require-audio --rotation= -s --serial= --shortcut-mod= diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 231405ce..b28201a4 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -51,6 +51,7 @@ arguments=( {-r,--record=}'[Record screen to file]:record file:_files' '--record-format=[Force recording format]:format:(mp4 mkv)' '--render-driver=[Request SDL to use the given render driver]:driver name:(direct3d opengl opengles2 opengles metal software)' + '--require-audio=[Make scrcpy fail if audio is enabled but does not work]' '--rotation=[Set the initial display rotation]:rotation values:(0 1 2 3)' {-s,--serial=}'[The device serial number \(mandatory for multiple devices only\)]:serial:($("${ADB-adb}" devices | awk '\''$2 == "device" {print $1}'\''))' '--shortcut-mod=[\[key1,key2+key3,...\] Specify the modifiers to use for scrcpy shortcuts]:shortcut mod:(lctrl rctrl lalt ralt lsuper rsuper)' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 40b8158c..91258414 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -272,6 +272,10 @@ Supported names are currently "direct3d", "opengl", "opengles2", "opengles", "me .UR https://wiki.libsdl.org/SDL_HINT_RENDER_DRIVER .UE +.TP +.B \-\-require\-audio +By default, scrcpy mirrors only the video if audio capture fails on the device. This option makes scrcpy fail if audio is enabled but does not work. + .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. diff --git a/app/src/cli.c b/app/src/cli.c index 8dfcdc79..18f3b83b 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -70,6 +70,7 @@ enum { OPT_AUDIO_ENCODER, OPT_LIST_ENCODERS, OPT_LIST_DISPLAYS, + OPT_REQUIRE_AUDIO, }; struct sc_option { @@ -465,6 +466,13 @@ static const struct sc_option options[] = { .longopt_id = OPT_RENDER_EXPIRED_FRAMES, .longopt = "render-expired-frames", }, + { + .longopt_id = OPT_REQUIRE_AUDIO, + .longopt = "require-audio", + .text = "By default, scrcpy mirrors only the video when audio capture " + "fails on the device. This option makes scrcpy fail if audio " + "is enabled but does not work." + }, { .longopt_id = OPT_ROTATION, .longopt = "rotation", @@ -1811,6 +1819,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], case OPT_LIST_DISPLAYS: opts->list_displays = true; break; + case OPT_REQUIRE_AUDIO: + opts->require_audio = true; + break; default: // getopt prints the error message on stderr return false; diff --git a/app/src/demuxer.c b/app/src/demuxer.c index d80a5dda..5977a28a 100644 --- a/app/src/demuxer.c +++ b/app/src/demuxer.c @@ -176,14 +176,13 @@ run_demuxer(void *data) { struct sc_demuxer *demuxer = data; // Flag to report end-of-stream (i.e. device disconnected) - bool eos = false; + enum sc_demuxer_status status = SC_DEMUXER_STATUS_ERROR; uint32_t raw_codec_id; bool ok = sc_demuxer_recv_codec_id(demuxer, &raw_codec_id); if (!ok) { LOGE("Demuxer '%s': stream disabled due to connection error", demuxer->name); - eos = true; goto end; } @@ -191,7 +190,7 @@ run_demuxer(void *data) { LOGW("Demuxer '%s': stream explicitly disabled by the device", demuxer->name); sc_demuxer_disable_sinks(demuxer); - eos = true; + status = SC_DEMUXER_STATUS_DISABLED; goto end; } @@ -241,7 +240,7 @@ run_demuxer(void *data) { bool ok = sc_demuxer_recv_packet(demuxer, packet); if (!ok) { // end of stream - eos = true; + status = SC_DEMUXER_STATUS_EOS; break; } @@ -272,7 +271,7 @@ run_demuxer(void *data) { finally_close_sinks: sc_demuxer_close_sinks(demuxer); end: - demuxer->cbs->on_ended(demuxer, eos, demuxer->cbs_userdata); + demuxer->cbs->on_ended(demuxer, status, demuxer->cbs_userdata); return 0; } diff --git a/app/src/demuxer.h b/app/src/demuxer.h index 73166b41..d0e41add 100644 --- a/app/src/demuxer.h +++ b/app/src/demuxer.h @@ -27,8 +27,15 @@ struct sc_demuxer { void *cbs_userdata; }; +enum sc_demuxer_status { + SC_DEMUXER_STATUS_EOS, + SC_DEMUXER_STATUS_DISABLED, + SC_DEMUXER_STATUS_ERROR, +}; + struct sc_demuxer_callbacks { - void (*on_ended)(struct sc_demuxer *demuxer, bool eos, void *userdata); + void (*on_ended)(struct sc_demuxer *demuxer, enum sc_demuxer_status, + void *userdata); }; // The name must be statically allocated (e.g. a string literal) diff --git a/app/src/options.c b/app/src/options.c index 8560b37b..5dd655ce 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -72,6 +72,7 @@ const struct scrcpy_options scrcpy_options_default = { .start_fps_counter = false, .power_on = true, .audio = true, + .require_audio = false, .list_encoders = false, .list_displays = false, }; diff --git a/app/src/options.h b/app/src/options.h index a15d51f8..5fcaf016 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -154,6 +154,7 @@ struct scrcpy_options { bool start_fps_counter; bool power_on; bool audio; + bool require_audio; bool list_encoders; bool list_displays; }; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index eb70749a..4355d71b 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -216,12 +216,15 @@ sc_recorder_on_ended(struct sc_recorder *recorder, bool success, } static void -sc_video_demuxer_on_ended(struct sc_demuxer *demuxer, bool eos, - void *userdata) { +sc_video_demuxer_on_ended(struct sc_demuxer *demuxer, + enum sc_demuxer_status status, void *userdata) { (void) demuxer; (void) userdata; - if (eos) { + // The device may not decide to disable the video + assert(status != SC_DEMUXER_STATUS_DISABLED); + + if (status == SC_DEMUXER_STATUS_EOS) { PUSH_EVENT(SC_EVENT_DEVICE_DISCONNECTED); } else { PUSH_EVENT(SC_EVENT_DEMUXER_ERROR); @@ -229,20 +232,17 @@ sc_video_demuxer_on_ended(struct sc_demuxer *demuxer, bool eos, } static void -sc_audio_demuxer_on_ended(struct sc_demuxer *demuxer, bool eos, - void *userdata) { +sc_audio_demuxer_on_ended(struct sc_demuxer *demuxer, + enum sc_demuxer_status status, void *userdata) { (void) demuxer; - (void) userdata; - // Contrary to the video demuxer, keep mirroring if only the audio fails. - // 'eos' is true on end-of-stream, including when audio capture is not - // possible on the device (so that scrcpy continue to mirror video without - // failing). - // However, if an audio configuration failure occurs (for example the user - // explicitly selected an unknown audio encoder), 'eos' is false and scrcpy - // must exit. + const struct scrcpy_options *options = userdata; - if (!eos) { + // Contrary to the video demuxer, keep mirroring if only the audio fails + // (unless --require-audio is set). + if (status == SC_DEMUXER_STATUS_ERROR + || (status == SC_DEMUXER_STATUS_DISABLED + && options->require_audio)) { PUSH_EVENT(SC_EVENT_DEMUXER_ERROR); } } @@ -434,7 +434,7 @@ scrcpy(struct scrcpy_options *options) { .on_ended = sc_audio_demuxer_on_ended, }; sc_demuxer_init(&s->audio_demuxer, "audio", s->server.audio_socket, - &audio_demuxer_cbs, NULL); + &audio_demuxer_cbs, options); } bool needs_video_decoder = options->display; From 6e05d7047a9914bdba0bd833e909bc781d5b1a81 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 28 Feb 2023 21:20:28 +0100 Subject: [PATCH 1424/2244] Call avcodec_receive_frame() in a loop Since in scrcpy a video packet passed to avcodec_send_packet() is always a complete video frame, it is sufficient to call avcodec_receive_frame() exactly once. In practice, it also works for audio packets: the decoder produces exactly 1 frame for 1 input packet. In theory, it is an implementation detail though, so avcodec_receive_frame() should be called in a loop. --- app/src/decoder.c | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/app/src/decoder.c b/app/src/decoder.c index 96d4a010..e4d59628 100644 --- a/app/src/decoder.c +++ b/app/src/decoder.c @@ -110,8 +110,19 @@ sc_decoder_push(struct sc_decoder *decoder, const AVPacket *packet) { decoder->name, ret); return false; } - ret = avcodec_receive_frame(decoder->codec_ctx, decoder->frame); - if (!ret) { + + for (;;) { + ret = avcodec_receive_frame(decoder->codec_ctx, decoder->frame); + if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) { + break; + } + + if (ret) { + LOGE("Decoder '%s', could not receive video frame: %d", + decoder->name, ret); + return false; + } + // a frame was received bool ok = push_frame_to_sinks(decoder, decoder->frame); // A frame lost should not make the whole pipeline fail. The error, if @@ -119,11 +130,8 @@ sc_decoder_push(struct sc_decoder *decoder, const AVPacket *packet) { (void) ok; av_frame_unref(decoder->frame); - } else if (ret != AVERROR(EAGAIN)) { - LOGE("Decoder '%s', could not receive video frame: %d", - decoder->name, ret); - return false; } + return true; } From 6dceb328173ed9dc9c7fcdfc250621b4c57ae415 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 28 Feb 2023 21:43:19 +0100 Subject: [PATCH 1425/2244] Add compat for reallocarray() This function fails safely in the case where the multiplication would overflow. --- app/meson.build | 1 + app/src/compat.c | 15 ++++++++++++++- app/src/compat.h | 4 ++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/app/meson.build b/app/meson.build index 8be917e9..7bdd288d 100644 --- a/app/meson.build +++ b/app/meson.build @@ -169,6 +169,7 @@ check_functions = [ 'vasprintf', 'nrand48', 'jrand48', + 'reallocarray', ] foreach f : check_functions diff --git a/app/src/compat.c b/app/src/compat.c index bb0152aa..785f843c 100644 --- a/app/src/compat.c +++ b/app/src/compat.c @@ -3,6 +3,9 @@ #include "config.h" #include +#ifndef HAVE_REALLOCARRAY +# include +#endif #include #include #include @@ -93,5 +96,15 @@ long jrand48(unsigned short xsubi[3]) { return v.i; } #endif - +#endif + +#ifndef HAVE_REALLOCARRAY +void *reallocarray(void *ptr, size_t nmemb, size_t size) { + size_t bytes; + if (__builtin_mul_overflow(nmemb, size, &bytes)) { + errno = ENOMEM; + return NULL; + } + return realloc(ptr, bytes); +} #endif diff --git a/app/src/compat.h b/app/src/compat.h index 857623e6..ea44437d 100644 --- a/app/src/compat.h +++ b/app/src/compat.h @@ -67,4 +67,8 @@ long nrand48(unsigned short xsubi[3]); long jrand48(unsigned short xsubi[3]); #endif +#ifndef HAVE_REALLOCARRAY +void *reallocarray(void *ptr, size_t nmemb, size_t size); +#endif + #endif From c735b8c127bba489df8d45e397fc47089ae9f00b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 28 Feb 2023 21:49:44 +0100 Subject: [PATCH 1426/2244] Use reallocarray() in sc_vector This fails safely in case of overflow. --- app/src/util/vector.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/util/vector.h b/app/src/util/vector.h index 0c6cab98..97d7c389 100644 --- a/app/src/util/vector.h +++ b/app/src/util/vector.h @@ -118,7 +118,7 @@ static inline void * sc_vector_reallocdata_(void *ptr, size_t count, size_t size, size_t *restrict pcap, size_t *restrict psize) { - void *p = realloc(ptr, count * size); + void *p = reallocarray(ptr, count, size); if (!p) { return NULL; } From 457385d5f468f87c31b44ad8d0fec1cd743c627c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 28 Feb 2023 21:48:18 +0100 Subject: [PATCH 1427/2244] Add sc_allocarray() util Add a function to allocate an array, which fails safely in the case where the multiplication would overflow. --- app/meson.build | 1 + app/src/util/memory.c | 14 ++++++++++++++ app/src/util/memory.h | 15 +++++++++++++++ 3 files changed, 30 insertions(+) create mode 100644 app/src/util/memory.c create mode 100644 app/src/util/memory.h diff --git a/app/meson.build b/app/meson.build index 7bdd288d..c24a17de 100644 --- a/app/meson.build +++ b/app/meson.build @@ -35,6 +35,7 @@ src = [ 'src/util/intmap.c', 'src/util/intr.c', 'src/util/log.c', + 'src/util/memory.c', 'src/util/net.c', 'src/util/net_intr.c', 'src/util/process.c', diff --git a/app/src/util/memory.c b/app/src/util/memory.c new file mode 100644 index 00000000..64ee616e --- /dev/null +++ b/app/src/util/memory.c @@ -0,0 +1,14 @@ +#include "memory.h" + +#include +#include + +void * +sc_allocarray(size_t nmemb, size_t size) { + size_t bytes; + if (__builtin_mul_overflow(nmemb, size, &bytes)) { + errno = ENOMEM; + return NULL; + } + return malloc(bytes); +} diff --git a/app/src/util/memory.h b/app/src/util/memory.h new file mode 100644 index 00000000..0fb6bc64 --- /dev/null +++ b/app/src/util/memory.h @@ -0,0 +1,15 @@ +#ifndef SC_MEMORY_H +#define SC_MEMORY_H + +#include + +/** + * Allocate an array of `nmemb` items of `size` bytes each + * + * Like calloc(), but without initialization. + * Like reallocarray(), but without reallocation. + */ +void * +sc_allocarray(size_t nmemb, size_t size); + +#endif From 33df484912f8ac4af759a1cf865a2cebbdecbf34 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 28 Feb 2023 22:56:37 +0100 Subject: [PATCH 1428/2244] Introduce VecDeque Introduce a double-ended queue implemented with a growable ring buffer. Inspired from the Rust VecDeque type: --- app/meson.build | 4 + app/src/util/vecdeque.h | 379 ++++++++++++++++++++++++++++++++++++++ app/tests/test_vecdeque.c | 197 ++++++++++++++++++++ 3 files changed, 580 insertions(+) create mode 100644 app/src/util/vecdeque.h create mode 100644 app/tests/test_vecdeque.c diff --git a/app/meson.build b/app/meson.build index c24a17de..a238eb8f 100644 --- a/app/meson.build +++ b/app/meson.build @@ -300,6 +300,10 @@ if get_option('buildtype') == 'debug' 'src/util/str.c', 'src/util/strbuf.c', ]], + ['test_vecdeque', [ + 'tests/test_vecdeque.c', + 'src/util/memory.c', + ]], ['test_vector', [ 'tests/test_vector.c', ]], diff --git a/app/src/util/vecdeque.h b/app/src/util/vecdeque.h new file mode 100644 index 00000000..e5372e02 --- /dev/null +++ b/app/src/util/vecdeque.h @@ -0,0 +1,379 @@ +#ifndef SC_VECDEQUE_H +#define SC_VECDEQUE_H + +#include "common.h" + +#include +#include +#include +#include +#include + +#include "util/memory.h" + +/** + * A double-ended queue implemented with a growable ring buffer. + * + * Inspired from the Rust VecDeque type: + * + */ + +/** + * VecDeque struct body + * + * A VecDeque is a dynamic ring-buffer, managed by the sc_vecdeque_* helpers. + * + * It is generic over the type of its items, so it is implemented via macros. + * + * To use a VecDeque, a new type must be defined: + * + * struct vecdeque_int SC_VECDEQUE(int); + * + * The struct may be anonymous: + * + * struct SC_VECDEQUE(const char *) names; + * + * Functions and macros having name ending with '_' are private. + */ +#define SC_VECDEQUE(type) { \ + size_t cap; \ + size_t origin; \ + size_t size; \ + type *data; \ +} + +/** + * Static initializer for a VecDeque + */ +#define SC_VECDEQUE_INITIALIZER { 0, 0, 0, NULL } + +/** + * Initialize an empty VecDeque + */ +#define sc_vecdeque_init(pv) \ +({ \ + (pv)->cap = 0; \ + (pv)->origin = 0; \ + (pv)->size = 0; \ + (pv)->data = NULL; \ +}) + +/** + * Destroy a VecDeque + */ +#define sc_vecdeque_destroy(pv) \ + free((pv)->data) + +/** + * Clear a VecDeque + * + * Remove all items. + */ +#define sc_vecdeque_clear(pv) \ +(void) ({ \ + sc_vecdeque_destroy(pv); \ + sc_vecdeque_init(pv); \ +}) + +/** + * Returns the content size + */ +#define sc_vecdeque_size(pv) \ + (pv)->size + +/** + * Return whether the VecDeque is empty (i.e. its size is 0) + */ +#define sc_vecdeque_is_empty(pv) \ + ((pv)->size == 0) + +/** + * Return whether the VecDeque is full + * + * A VecDeque is full when its size equals its current capacity. However, it + * does not prevent to push a new item (with sc_vecdeque_push()), since this + * will increase its capacity. + */ +#define sc_vecdeque_is_full(pv) \ + ((pv)->size == (pv)->cap) + +/** + * The minimal allocation size, in number of items + * + * Private. + */ +#define SC_VECDEQUE_MINCAP_ ((size_t) 10) + +/** + * The maximal allocation size, in number of items + * + * Use SIZE_MAX/2 to fit in ssize_t, and so that cap*1.5 does not overflow. + * + * Private. + */ +#define sc_vecdeque_max_cap_(pv) (SIZE_MAX / 2 / sizeof(*(pv)->data)) + +/** + * Realloc the internal array to a specific capacity + * + * On reallocation success, update the VecDeque capacity (`*pcap`) and origin + * (`*porigin`), and return the reallocated data. + * + * On reallocation failure, return NULL without any change. + * + * Private. + * + * \param ptr the current `data` field of the SC_VECDEQUE to realloc + * \param newcap the requested capacity, in number of items + * \param item_size the size of one item (the generic type is unknown from this + * function) + * \param pcap a pointer to the `cap` field of the SC_VECDEQUE [IN/OUT] + * \param porigin a pointer to pv->origin [IN/OUT] + * \param size the `size` field of the SC_VECDEQUE + * \return the new array to assign to the `data` field of the SC_VECDEQUE (if + * not NULL) + */ +static inline void * +sc_vecdeque_reallocdata_(void *ptr, size_t newcap, size_t item_size, + size_t *pcap, size_t *porigin, size_t size) { + + size_t oldcap = *pcap; + size_t oldorigin = *porigin; + + assert(newcap > oldcap); // Could only grow + + if (oldorigin + size <= oldcap) { + // The current content will stay in place, just realloc + // + // As an example, here is the content of a ring-buffer (oldcap=10) + // before the realloc: + // + // _ _ 2 3 4 5 6 7 _ _ + // ^ + // origin + // + // It is resized (newcap=15), e.g. with sc_vecdeque_reserve(): + // + // _ _ 2 3 4 5 6 7 _ _ _ _ _ _ _ + // ^ + // origin + + void *newptr = reallocarray(ptr, newcap, item_size); + if (!newptr) { + return NULL; + } + + *pcap = newcap; + return newptr; + } + + // Copy the current content to the new array + // + // As an example, here is the content of a ring-buffer (oldcap=10) before + // the realloc: + // + // 5 6 7 _ _ 0 1 2 3 4 + // ^ + // origin + // + // It is resized (newcap=15), e.g. with sc_vecdeque_reserve(): + // + // 0 1 2 3 4 5 6 7 _ _ _ _ _ _ _ + // ^ + // origin + + assert(size); + void *newptr = sc_allocarray(newcap, item_size); + if (!newptr) { + return NULL; + } + + size_t right_len = MIN(size, oldcap - oldorigin); + assert(right_len); + memcpy(newptr, ptr + (oldorigin * item_size), right_len * item_size); + + if (size > right_len) { + memcpy(newptr + (right_len * item_size), ptr, + (size - right_len) * item_size); + } + + free(ptr); + + *pcap = newcap; + *porigin = 0; + return newptr; +} + +/** + * Macro to realloc the internal data to a new capacity + * + * Private. + * + * \retval true on success + * \retval false on allocation failure (the VecDeque is left untouched) + */ +#define sc_vecdeque_realloc_(pv, newcap) \ +({ \ + void *p = sc_vecdeque_reallocdata_((pv)->data, newcap, \ + sizeof(*(pv)->data), &(pv)->cap, \ + &(pv)->origin, (pv)->size); \ + if (p) { \ + (pv)->data = p; \ + } \ + (bool) p; \ +}); + +static inline size_t +sc_vecdeque_growsize_(size_t value) +{ + /* integer multiplication by 1.5 */ + return value + (value >> 1); +} + +/** + * Increase the capacity of the VecDeque to at least `mincap` + * + * \param pv a pointer to the VecDeque + * \param mincap (`size_t`) the requested capacity + * \retval true on success + * \retval false on allocation failure (the VecDeque is left untouched) + */ +#define sc_vecdeque_reserve(pv, mincap) \ +({ \ + assert(mincap <= sc_vecdeque_max_cap_(pv)); \ + bool ok; \ + /* avoid to allocate tiny arrays (< SC_VECDEQUE_MINCAP_) */ \ + size_t mincap_ = MAX(mincap, SC_VECDEQUE_MINCAP_); \ + if (mincap_ <= (pv)->cap) { \ + /* nothing to do */ \ + ok = true; \ + } else if (mincap_ <= sc_vecdeque_max_cap_(pv)) { \ + /* not too big */ \ + size_t newsize = sc_vecdeque_growsize_((pv)->cap); \ + newsize = CLAMP(newsize, mincap_, sc_vecdeque_max_cap_(pv)); \ + ok = sc_vecdeque_realloc_(pv, newsize); \ + } else { \ + ok = false; \ + } \ + ok; \ +}) + +/** + * Automatically grow the VecDeque capacity + * + * Private. + * + * \retval true on success + * \retval false on allocation failure (the VecDeque is left untouched) + */ +#define sc_vecdeque_grow_(pv) \ +({ \ + bool ok; \ + if ((pv)->cap < sc_vecdeque_max_cap_(pv)) { \ + size_t newsize = sc_vecdeque_growsize_((pv)->cap); \ + newsize = CLAMP(newsize, SC_VECDEQUE_MINCAP_, \ + sc_vecdeque_max_cap_(pv)); \ + ok = sc_vecdeque_realloc_(pv, newsize); \ + } else { \ + ok = false; \ + } \ + ok; \ +}) + +/** + * Grow the VecDeque capacity if it is full + * + * Private. + * + * \retval true on success + * \retval false on allocation failure (the VecDeque is left untouched) + */ +#define sc_vecdeque_grow_if_needed_(pv) \ + (!sc_vecdeque_is_full(pv) || sc_vecdeque_grow_(pv)) + +/** + * Push an uninitialized item, and return a pointer to it + * + * It does not attempt to resize the VecDeque. It is an error to this function + * if the VecDeque is full. + * + * This function may not fail. It returns a valid non-NULL pointer to the + * uninitialized item just pushed. + */ +#define sc_vecdeque_push_hole_noresize(pv) \ +({ \ + assert(!sc_vecdeque_is_full(pv)); \ + ++(pv)->size; \ + &(pv)->data[((pv)->origin + (pv)->size - 1) % (pv)->cap]; \ +}) + +/** + * Push an uninitialized item, and return a pointer to it + * + * If the VecDeque is full, it is resized. + * + * This function returns either a valid non-NULL pointer to the uninitialized + * item just pushed, or NULL on reallocation failure. + */ +#define sc_vecdeque_push_hole(pv) \ + (sc_vecdeque_grow_if_needed_(pv) ? \ + sc_vecdeque_push_hole_noresize(pv) : NULL) + +/** + * Push an item + * + * It does not attempt to resize the VecDeque. It is an error to this function + * if the VecDeque is full. + * + * This function may not fail. + */ +#define sc_vecdeque_push_noresize(pv, item) \ +(void) ({ \ + assert(!sc_vecdeque_is_full(pv)); \ + ++(pv)->size; \ + (pv)->data[((pv)->origin + (pv)->size - 1) % (pv)->cap] = item; \ +}) + +/** + * Push an item + * + * If the VecDeque is full, it is resized. + * + * \retval true on success + * \retval false on allocation failure (the VecDeque is left untouched) + */ +#define sc_vecdeque_push(pv, item) \ +({ \ + bool ok = sc_vecdeque_grow_if_needed_(pv); \ + if (ok) { \ + sc_vecdeque_push_noresize(pv, item); \ + } \ + ok; \ +}) + +/** + * Pop an item and return a pointer to it (still in the VecDeque) + * + * Returning a pointer allows the caller to destroy it in place without copy + * (especially if the item type is big). + * + * It is an error to call this function if the VecDeque is empty. + */ +#define sc_vecdeque_popref(pv) \ +({ \ + assert(!sc_vecdeque_is_empty(pv)); \ + size_t pos = (pv)->origin; \ + (pv)->origin = ((pv)->origin + 1) % (pv)->cap; \ + --(pv)->size; \ + &(pv)->data[pos]; \ +}) + +/** + * Pop an item and return it + * + * It is an error to call this function if the VecDeque is empty. + */ +#define sc_vecdeque_pop(pv) \ + (*sc_vecdeque_popref(pv)) + +#endif diff --git a/app/tests/test_vecdeque.c b/app/tests/test_vecdeque.c new file mode 100644 index 00000000..fa3ba963 --- /dev/null +++ b/app/tests/test_vecdeque.c @@ -0,0 +1,197 @@ +#include "common.h" + +#include + +#include "util/vecdeque.h" + +#define pr(pv) \ +({ \ + fprintf(stderr, "cap=%lu origin=%lu size=%lu\n", (pv)->cap, (pv)->origin, (pv)->size); \ + for (size_t i = 0; i < (pv)->cap; ++i) \ + fprintf(stderr, "%d ", (pv)->data[i]); \ + fprintf(stderr, "\n"); \ +}) + +static void test_vecdeque_push_pop(void) { + struct SC_VECDEQUE(int) vdq = SC_VECDEQUE_INITIALIZER; + + assert(sc_vecdeque_is_empty(&vdq)); + assert(sc_vecdeque_size(&vdq) == 0); + + bool ok = sc_vecdeque_push(&vdq, 5); + assert(ok); + assert(sc_vecdeque_size(&vdq) == 1); + + ok = sc_vecdeque_push(&vdq, 12); + assert(ok); + assert(sc_vecdeque_size(&vdq) == 2); + + int v = sc_vecdeque_pop(&vdq); + assert(v == 5); + assert(sc_vecdeque_size(&vdq) == 1); + + ok = sc_vecdeque_push(&vdq, 7); + assert(ok); + assert(sc_vecdeque_size(&vdq) == 2); + + int *p = sc_vecdeque_popref(&vdq); + assert(p); + assert(*p == 12); + assert(sc_vecdeque_size(&vdq) == 1); + + v = sc_vecdeque_pop(&vdq); + assert(v == 7); + assert(sc_vecdeque_size(&vdq) == 0); + assert(sc_vecdeque_is_empty(&vdq)); + + sc_vecdeque_destroy(&vdq); +} + +static void test_vecdeque_reserve(void) { + struct SC_VECDEQUE(int) vdq = SC_VECDEQUE_INITIALIZER; + + bool ok = sc_vecdeque_reserve(&vdq, 20); + assert(ok); + assert(vdq.cap == 20); + + assert(sc_vecdeque_size(&vdq) == 0); + + for (size_t i = 0; i < 20; ++i) { + ok = sc_vecdeque_push(&vdq, i); + assert(ok); + } + + assert(sc_vecdeque_size(&vdq) == 20); + + // It is now full + + for (int i = 0; i < 5; ++i) { + int v = sc_vecdeque_pop(&vdq); + assert(v == i); + } + assert(sc_vecdeque_size(&vdq) == 15); + + for (int i = 20; i < 25; ++i) { + ok = sc_vecdeque_push(&vdq, i); + assert(ok); + } + + assert(sc_vecdeque_size(&vdq) == 20); + assert(vdq.cap == 20); + + // Now, the content wraps around the ring buffer: + // 20 21 22 23 24 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 + // ^ + // origin + + // It is now full, let's reserve some space + ok = sc_vecdeque_reserve(&vdq, 30); + assert(ok); + assert(vdq.cap == 30); + + assert(sc_vecdeque_size(&vdq) == 20); + + for (int i = 0; i < 20; ++i) { + // We should retrieve the items we inserted in order + int v = sc_vecdeque_pop(&vdq); + assert(v == i + 5); + } + + assert(sc_vecdeque_size(&vdq) == 0); + + sc_vecdeque_destroy(&vdq); +} + +static void test_vecdeque_grow() { + struct SC_VECDEQUE(int) vdq = SC_VECDEQUE_INITIALIZER; + + bool ok = sc_vecdeque_reserve(&vdq, 20); + assert(ok); + assert(vdq.cap == 20); + + assert(sc_vecdeque_size(&vdq) == 0); + + for (int i = 0; i < 500; ++i) { + ok = sc_vecdeque_push(&vdq, i); + assert(ok); + } + + assert(sc_vecdeque_size(&vdq) == 500); + + for (int i = 0; i < 100; ++i) { + int v = sc_vecdeque_pop(&vdq); + assert(v == i); + } + + assert(sc_vecdeque_size(&vdq) == 400); + + for (int i = 500; i < 1000; ++i) { + ok = sc_vecdeque_push(&vdq, i); + assert(ok); + } + + assert(sc_vecdeque_size(&vdq) == 900); + + for (int i = 100; i < 1000; ++i) { + int v = sc_vecdeque_pop(&vdq); + assert(v == i); + } + + assert(sc_vecdeque_size(&vdq) == 0); + + sc_vecdeque_destroy(&vdq); +} + +static void test_vecdeque_push_hole() { + struct SC_VECDEQUE(int) vdq = SC_VECDEQUE_INITIALIZER; + + bool ok = sc_vecdeque_reserve(&vdq, 20); + assert(ok); + assert(vdq.cap == 20); + + assert(sc_vecdeque_size(&vdq) == 0); + + for (int i = 0; i < 20; ++i) { + int *p = sc_vecdeque_push_hole(&vdq); + assert(p); + *p = i * 10; + } + + assert(sc_vecdeque_size(&vdq) == 20); + + for (int i = 0; i < 10; ++i) { + int v = sc_vecdeque_pop(&vdq); + assert(v == i * 10); + } + + assert(sc_vecdeque_size(&vdq) == 10); + + for (int i = 20; i < 30; ++i) { + int *p = sc_vecdeque_push_hole(&vdq); + assert(p); + *p = i * 10; + } + + assert(sc_vecdeque_size(&vdq) == 20); + + for (int i = 10; i < 30; ++i) { + int v = sc_vecdeque_pop(&vdq); + assert(v == i * 10); + } + + assert(sc_vecdeque_size(&vdq) == 0); + + sc_vecdeque_destroy(&vdq); +} + +int main(int argc, char *argv[]) { + (void) argc; + (void) argv; + + test_vecdeque_push_pop(); + test_vecdeque_reserve(); + test_vecdeque_grow(); + test_vecdeque_push_hole(); + + return 0; +} From efc15744da576206f407099927bceea8421b79bb Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 1 Mar 2023 21:42:51 +0100 Subject: [PATCH 1429/2244] Use VecDeque in recorder The packets queued for recording were wrapped in a dynamically allocated structure with a "next" field. To avoid this additional layer of allocation and indirection, use a VecDeque. --- app/src/recorder.c | 169 ++++++++++++++++++++++----------------------- app/src/recorder.h | 9 +-- 2 files changed, 84 insertions(+), 94 deletions(-) diff --git a/app/src/recorder.c b/app/src/recorder.c index 9fc15dac..bd7c50f2 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -33,41 +33,27 @@ find_muxer(const char *name) { return oformat; } -static struct sc_record_packet * -sc_record_packet_new(const AVPacket *packet) { - struct sc_record_packet *rec = malloc(sizeof(*rec)); - if (!rec) { +static AVPacket * +sc_recorder_packet_ref(const AVPacket *packet) { + AVPacket *p = av_packet_alloc(); + if (!p) { LOG_OOM(); return NULL; } - rec->packet = av_packet_alloc(); - if (!rec->packet) { - LOG_OOM(); - free(rec); + if (av_packet_ref(p, packet)) { + av_packet_free(&p); return NULL; } - if (av_packet_ref(rec->packet, packet)) { - av_packet_free(&rec->packet); - free(rec); - return NULL; - } - return rec; -} - -static void -sc_record_packet_delete(struct sc_record_packet *rec) { - av_packet_free(&rec->packet); - free(rec); + return p; } static void sc_recorder_queue_clear(struct sc_recorder_queue *queue) { - while (!sc_queue_is_empty(queue)) { - struct sc_record_packet *rec; - sc_queue_take(queue, next, &rec); - sc_record_packet_delete(rec); + while (!sc_vecdeque_is_empty(queue)) { + AVPacket *p = sc_vecdeque_pop(queue); + av_packet_free(&p); } } @@ -227,12 +213,12 @@ sc_recorder_wait_audio_stream(struct sc_recorder *recorder) { static inline bool sc_recorder_has_empty_queues(struct sc_recorder *recorder) { - if (sc_queue_is_empty(&recorder->video_queue)) { + if (sc_vecdeque_is_empty(&recorder->video_queue)) { // The video queue is empty return true; } - if (recorder->audio && sc_queue_is_empty(&recorder->audio_queue)) { + if (recorder->audio && sc_vecdeque_is_empty(&recorder->audio_queue)) { // The audio queue is empty (when audio is enabled) return true; } @@ -249,28 +235,27 @@ sc_recorder_process_header(struct sc_recorder *recorder) { sc_cond_wait(&recorder->queue_cond, &recorder->mutex); } - if (sc_queue_is_empty(&recorder->video_queue)) { + if (sc_vecdeque_is_empty(&recorder->video_queue)) { assert(recorder->stopped); - // Don't process anything if there are not at least video packets (when - // the recorder is stopped) + // If the recorder is stopped, don't process anything if there are not + // at least video packets sc_mutex_unlock(&recorder->mutex); return false; } - struct sc_record_packet *video_pkt; - sc_queue_take(&recorder->video_queue, next, &video_pkt); + AVPacket *video_pkt = sc_vecdeque_pop(&recorder->video_queue); - struct sc_record_packet *audio_pkt = NULL; - if (!sc_queue_is_empty(&recorder->audio_queue)) { + AVPacket *audio_pkt = NULL; + if (!sc_vecdeque_is_empty(&recorder->audio_queue)) { assert(recorder->audio); - sc_queue_take(&recorder->audio_queue, next, &audio_pkt); + audio_pkt = sc_vecdeque_pop(&recorder->audio_queue); } sc_mutex_unlock(&recorder->mutex); int ret = false; - if (video_pkt->packet->pts != AV_NOPTS_VALUE) { + if (video_pkt->pts != AV_NOPTS_VALUE) { LOGE("The first video packet is not a config packet"); goto end; } @@ -278,13 +263,13 @@ sc_recorder_process_header(struct sc_recorder *recorder) { assert(recorder->video_stream_index >= 0); AVStream *video_stream = recorder->ctx->streams[recorder->video_stream_index]; - bool ok = sc_recorder_set_extradata(video_stream, video_pkt->packet); + bool ok = sc_recorder_set_extradata(video_stream, video_pkt); if (!ok) { goto end; } if (audio_pkt) { - if (audio_pkt->packet->pts != AV_NOPTS_VALUE) { + if (audio_pkt->pts != AV_NOPTS_VALUE) { LOGE("The first audio packet is not a config packet"); goto end; } @@ -292,7 +277,7 @@ sc_recorder_process_header(struct sc_recorder *recorder) { assert(recorder->audio_stream_index >= 0); AVStream *audio_stream = recorder->ctx->streams[recorder->audio_stream_index]; - ok = sc_recorder_set_extradata(audio_stream, audio_pkt->packet); + ok = sc_recorder_set_extradata(audio_stream, audio_pkt); if (!ok) { goto end; } @@ -307,9 +292,9 @@ sc_recorder_process_header(struct sc_recorder *recorder) { ret = true; end: - sc_record_packet_delete(video_pkt); + av_packet_free(&video_pkt); if (audio_pkt) { - sc_record_packet_delete(audio_pkt); + av_packet_free(&audio_pkt); } return ret; @@ -324,12 +309,12 @@ sc_recorder_process_packets(struct sc_recorder *recorder) { return false; } - struct sc_record_packet *video_pkt = NULL; - struct sc_record_packet *audio_pkt = NULL; + AVPacket *video_pkt = NULL; + AVPacket *audio_pkt = NULL; // We can write a video packet only once we received the next one so that // we can set its duration (next_pts - current_pts) - struct sc_record_packet *video_pkt_previous = NULL; + AVPacket *video_pkt_previous = NULL; bool error = false; @@ -337,12 +322,12 @@ sc_recorder_process_packets(struct sc_recorder *recorder) { sc_mutex_lock(&recorder->mutex); while (!recorder->stopped) { - if (!video_pkt && !sc_queue_is_empty(&recorder->video_queue)) { + if (!video_pkt && !sc_vecdeque_is_empty(&recorder->video_queue)) { // A new packet may be assigned to video_pkt and be processed break; } if (recorder->audio && !audio_pkt - && !sc_queue_is_empty(&recorder->audio_queue)) { + && !sc_vecdeque_is_empty(&recorder->audio_queue)) { // A new packet may be assigned to audio_pkt and be processed break; } @@ -354,20 +339,20 @@ sc_recorder_process_packets(struct sc_recorder *recorder) { // If there is no audio, then the audio_queue will remain empty forever // and audio_pkt will always be NULL. - assert(recorder->audio - || (!audio_pkt && sc_queue_is_empty(&recorder->audio_queue))); + assert(recorder->audio || (!audio_pkt + && sc_vecdeque_is_empty(&recorder->audio_queue))); - if (!video_pkt && !sc_queue_is_empty(&recorder->video_queue)) { - sc_queue_take(&recorder->video_queue, next, &video_pkt); + if (!video_pkt && !sc_vecdeque_is_empty(&recorder->video_queue)) { + video_pkt = sc_vecdeque_pop(&recorder->video_queue); } - if (!audio_pkt && !sc_queue_is_empty(&recorder->audio_queue)) { - sc_queue_take(&recorder->audio_queue, next, &audio_pkt); + if (!audio_pkt && !sc_vecdeque_is_empty(&recorder->audio_queue)) { + audio_pkt = sc_vecdeque_pop(&recorder->audio_queue); } if (recorder->stopped && !video_pkt && !audio_pkt) { - assert(sc_queue_is_empty(&recorder->video_queue)); - assert(sc_queue_is_empty(&recorder->audio_queue)); + assert(sc_vecdeque_is_empty(&recorder->video_queue)); + assert(sc_vecdeque_is_empty(&recorder->audio_queue)); sc_mutex_unlock(&recorder->mutex); break; } @@ -379,28 +364,27 @@ sc_recorder_process_packets(struct sc_recorder *recorder) { // Ignore further config packets (e.g. on device orientation // change). The next non-config packet will have the config packet // data prepended. - if (video_pkt && video_pkt->packet->pts == AV_NOPTS_VALUE) { - sc_record_packet_delete(video_pkt); + if (video_pkt && video_pkt->pts == AV_NOPTS_VALUE) { + av_packet_free(&video_pkt); video_pkt = NULL; } - if (audio_pkt && audio_pkt->packet->pts == AV_NOPTS_VALUE) { - sc_record_packet_delete(audio_pkt); - audio_pkt= NULL; + if (audio_pkt && audio_pkt->pts == AV_NOPTS_VALUE) { + av_packet_free(&audio_pkt); + audio_pkt = NULL; } if (pts_origin == AV_NOPTS_VALUE) { if (!recorder->audio) { assert(video_pkt); - pts_origin = video_pkt->packet->pts; + pts_origin = video_pkt->pts; } else if (video_pkt && audio_pkt) { - pts_origin = - MIN(video_pkt->packet->pts, audio_pkt->packet->pts); + pts_origin = MIN(video_pkt->pts, audio_pkt->pts); } else if (recorder->stopped) { if (video_pkt) { // The recorder is stopped without audio, record the video // packets - pts_origin = video_pkt->packet->pts; + pts_origin = video_pkt->pts; } else { // Fail if there is no video error = true; @@ -415,17 +399,16 @@ sc_recorder_process_packets(struct sc_recorder *recorder) { assert(pts_origin != AV_NOPTS_VALUE); if (video_pkt) { - video_pkt->packet->pts -= pts_origin; - video_pkt->packet->dts = video_pkt->packet->pts; + video_pkt->pts -= pts_origin; + video_pkt->dts = video_pkt->pts; if (video_pkt_previous) { // we now know the duration of the previous packet - video_pkt_previous->packet->duration = - video_pkt->packet->pts - video_pkt_previous->packet->pts; + video_pkt_previous->duration = video_pkt->pts + - video_pkt_previous->pts; - bool ok = sc_recorder_write_video(recorder, - video_pkt_previous->packet); - sc_record_packet_delete(video_pkt_previous); + bool ok = sc_recorder_write_video(recorder, video_pkt_previous); + av_packet_free(&video_pkt_previous); if (!ok) { LOGE("Could not record video packet"); error = true; @@ -438,34 +421,34 @@ sc_recorder_process_packets(struct sc_recorder *recorder) { } if (audio_pkt) { - audio_pkt->packet->pts -= pts_origin; - audio_pkt->packet->dts = audio_pkt->packet->pts; + audio_pkt->pts -= pts_origin; + audio_pkt->dts = audio_pkt->pts; - bool ok = sc_recorder_write_audio(recorder, audio_pkt->packet); + bool ok = sc_recorder_write_audio(recorder, audio_pkt); if (!ok) { LOGE("Could not record audio packet"); error = true; goto end; } - sc_record_packet_delete(audio_pkt); + av_packet_free(&audio_pkt); audio_pkt = NULL; } } // Write the last video packet - struct sc_record_packet *last = video_pkt_previous; + AVPacket *last = video_pkt_previous; if (last) { // assign an arbitrary duration to the last packet - last->packet->duration = 100000; - bool ok = sc_recorder_write_video(recorder, last->packet); + last->duration = 100000; + bool ok = sc_recorder_write_video(recorder, last); if (!ok) { // failing to write the last frame is not very serious, no // future frame may depend on it, so the resulting file // will still be valid LOGW("Could not record last packet"); } - sc_record_packet_delete(last); + av_packet_free(&last); } int ret = av_write_trailer(recorder->ctx); @@ -476,10 +459,10 @@ sc_recorder_process_packets(struct sc_recorder *recorder) { end: if (video_pkt) { - sc_record_packet_delete(video_pkt); + av_packet_free(&video_pkt); } if (audio_pkt) { - sc_record_packet_delete(audio_pkt); + av_packet_free(&audio_pkt); } return !error; @@ -585,16 +568,22 @@ sc_recorder_video_packet_sink_push(struct sc_packet_sink *sink, return false; } - struct sc_record_packet *rec = sc_record_packet_new(packet); + AVPacket *rec = sc_recorder_packet_ref(packet); if (!rec) { LOG_OOM(); sc_mutex_unlock(&recorder->mutex); return false; } - rec->packet->stream_index = recorder->video_stream_index; + rec->stream_index = recorder->video_stream_index; + + bool ok = sc_vecdeque_push(&recorder->video_queue, rec); + if (!ok) { + LOG_OOM(); + sc_mutex_unlock(&recorder->mutex); + return false; + } - sc_queue_push(&recorder->video_queue, next, rec); sc_cond_signal(&recorder->queue_cond); sc_mutex_unlock(&recorder->mutex); @@ -648,16 +637,22 @@ sc_recorder_audio_packet_sink_push(struct sc_packet_sink *sink, return false; } - struct sc_record_packet *rec = sc_record_packet_new(packet); + AVPacket *rec = sc_recorder_packet_ref(packet); if (!rec) { LOG_OOM(); sc_mutex_unlock(&recorder->mutex); return false; } - rec->packet->stream_index = recorder->audio_stream_index; + rec->stream_index = recorder->audio_stream_index; + + bool ok = sc_vecdeque_push(&recorder->audio_queue, rec); + if (!ok) { + LOG_OOM(); + sc_mutex_unlock(&recorder->mutex); + return false; + } - sc_queue_push(&recorder->audio_queue, next, rec); sc_cond_signal(&recorder->queue_cond); sc_mutex_unlock(&recorder->mutex); @@ -708,8 +703,8 @@ sc_recorder_init(struct sc_recorder *recorder, const char *filename, recorder->audio = audio; - sc_queue_init(&recorder->video_queue); - sc_queue_init(&recorder->audio_queue); + sc_vecdeque_init(&recorder->video_queue); + sc_vecdeque_init(&recorder->audio_queue); recorder->stopped = false; recorder->video_codec = NULL; diff --git a/app/src/recorder.h b/app/src/recorder.h index 6fe72401..e3d5f018 100644 --- a/app/src/recorder.h +++ b/app/src/recorder.h @@ -9,15 +9,10 @@ #include "coords.h" #include "options.h" #include "trait/packet_sink.h" -#include "util/queue.h" #include "util/thread.h" +#include "util/vecdeque.h" -struct sc_record_packet { - AVPacket *packet; - struct sc_record_packet *next; -}; - -struct sc_recorder_queue SC_QUEUE(struct sc_record_packet); +struct sc_recorder_queue SC_VECDEQUE(AVPacket *); struct sc_recorder { struct sc_packet_sink video_packet_sink; From f25a67f3424406c9cbf33b7b11956ad7dcf2ddb3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 1 Mar 2023 22:21:43 +0100 Subject: [PATCH 1430/2244] Use VecDeque in video_buffer The packets queued for buffering were wrapped in a dynamically allocated structure with a "next" field. To avoid this additional layer of allocation and indirection, use a VecDeque. --- app/src/video_buffer.c | 63 ++++++++++++++++++++---------------------- app/src/video_buffer.h | 5 ++-- 2 files changed, 32 insertions(+), 36 deletions(-) diff --git a/app/src/video_buffer.c b/app/src/video_buffer.c index 11f76479..b3b29098 100644 --- a/app/src/video_buffer.c +++ b/app/src/video_buffer.c @@ -10,35 +10,26 @@ #define SC_BUFFERING_NDEBUG // comment to debug -static struct sc_video_buffer_frame * -sc_video_buffer_frame_new(const AVFrame *frame) { - struct sc_video_buffer_frame *vb_frame = malloc(sizeof(*vb_frame)); - if (!vb_frame) { - LOG_OOM(); - return NULL; - } - +static bool +sc_video_buffer_frame_init(struct sc_video_buffer_frame *vb_frame, + const AVFrame *frame) { vb_frame->frame = av_frame_alloc(); if (!vb_frame->frame) { - LOG_OOM(); - free(vb_frame); - return NULL; + return false; } if (av_frame_ref(vb_frame->frame, frame)) { av_frame_free(&vb_frame->frame); - free(vb_frame); - return NULL; + return false; } - return vb_frame; + return true; } static void -sc_video_buffer_frame_delete(struct sc_video_buffer_frame *vb_frame) { +sc_video_buffer_frame_destroy(struct sc_video_buffer_frame *vb_frame) { av_frame_unref(vb_frame->frame); av_frame_free(&vb_frame->frame); - free(vb_frame); } static bool @@ -62,7 +53,7 @@ run_buffering(void *data) { for (;;) { sc_mutex_lock(&vb->b.mutex); - while (!vb->b.stopped && sc_queue_is_empty(&vb->b.queue)) { + while (!vb->b.stopped && sc_vecdeque_is_empty(&vb->b.queue)) { sc_cond_wait(&vb->b.queue_cond, &vb->b.mutex); } @@ -71,12 +62,11 @@ run_buffering(void *data) { goto stopped; } - struct sc_video_buffer_frame *vb_frame; - sc_queue_take(&vb->b.queue, next, &vb_frame); + struct sc_video_buffer_frame vb_frame = sc_vecdeque_pop(&vb->b.queue); sc_tick max_deadline = sc_tick_now() + vb->buffering_time; // PTS (written by the server) are expressed in microseconds - sc_tick pts = SC_TICK_TO_US(vb_frame->frame->pts); + sc_tick pts = SC_TICK_TO_US(vb_frame.frame->pts); bool timed_out = false; while (!vb->b.stopped && !timed_out) { @@ -91,7 +81,7 @@ run_buffering(void *data) { } if (vb->b.stopped) { - sc_video_buffer_frame_delete(vb_frame); + sc_video_buffer_frame_destroy(&vb_frame); sc_mutex_unlock(&vb->b.mutex); goto stopped; } @@ -100,20 +90,19 @@ run_buffering(void *data) { #ifndef SC_BUFFERING_NDEBUG LOGD("Buffering: %" PRItick ";%" PRItick ";%" PRItick, - pts, vb_frame->push_date, sc_tick_now()); + pts, vb_frame.push_date, sc_tick_now()); #endif - sc_video_buffer_offer(vb, vb_frame->frame); + sc_video_buffer_offer(vb, vb_frame.frame); - sc_video_buffer_frame_delete(vb_frame); + sc_video_buffer_frame_destroy(&vb_frame); } stopped: // Flush queue - while (!sc_queue_is_empty(&vb->b.queue)) { - struct sc_video_buffer_frame *vb_frame; - sc_queue_take(&vb->b.queue, next, &vb_frame); - sc_video_buffer_frame_delete(vb_frame); + while (!sc_vecdeque_is_empty(&vb->b.queue)) { + struct sc_video_buffer_frame *p = sc_vecdeque_popref(&vb->b.queue); + sc_video_buffer_frame_destroy(p); } LOGD("Buffering thread ended"); @@ -154,7 +143,7 @@ sc_video_buffer_init(struct sc_video_buffer *vb, sc_tick buffering_time, } sc_clock_init(&vb->b.clock); - sc_queue_init(&vb->b.queue); + sc_vecdeque_init(&vb->b.queue); } assert(cbs); @@ -230,17 +219,25 @@ sc_video_buffer_push(struct sc_video_buffer *vb, const AVFrame *frame) { return sc_video_buffer_offer(vb, frame); } - struct sc_video_buffer_frame *vb_frame = sc_video_buffer_frame_new(frame); - if (!vb_frame) { + struct sc_video_buffer_frame vb_frame; + bool ok = sc_video_buffer_frame_init(&vb_frame, frame); + if (!ok) { sc_mutex_unlock(&vb->b.mutex); LOG_OOM(); return false; } #ifndef SC_BUFFERING_NDEBUG - vb_frame->push_date = sc_tick_now(); + vb_frame.push_date = sc_tick_now(); #endif - sc_queue_push(&vb->b.queue, next, vb_frame); + + ok = sc_vecdeque_push(&vb->b.queue, vb_frame); + if (!ok) { + sc_mutex_unlock(&vb->b.mutex); + LOG_OOM(); + return false; + } + sc_cond_signal(&vb->b.queue_cond); sc_mutex_unlock(&vb->b.mutex); diff --git a/app/src/video_buffer.h b/app/src/video_buffer.h index 48777703..41b09434 100644 --- a/app/src/video_buffer.h +++ b/app/src/video_buffer.h @@ -7,22 +7,21 @@ #include "clock.h" #include "frame_buffer.h" -#include "util/queue.h" #include "util/thread.h" #include "util/tick.h" +#include "util/vecdeque.h" // forward declarations typedef struct AVFrame AVFrame; struct sc_video_buffer_frame { AVFrame *frame; - struct sc_video_buffer_frame *next; #ifndef NDEBUG sc_tick push_date; #endif }; -struct sc_video_buffer_frame_queue SC_QUEUE(struct sc_video_buffer_frame); +struct sc_video_buffer_frame_queue SC_VECDEQUE(struct sc_video_buffer_frame); struct sc_video_buffer { struct sc_frame_buffer fb; From 4d989de9ae27a88b08677d2cb44c0cce86380ee4 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 1 Mar 2023 22:39:11 +0100 Subject: [PATCH 1431/2244] Use VecDeque in controller Replace cbuf by VecDeque in controller. --- app/src/controller.c | 48 ++++++++++++++++++++++++++++++-------------- app/src/controller.h | 4 ++-- 2 files changed, 35 insertions(+), 17 deletions(-) diff --git a/app/src/controller.c b/app/src/controller.c index 4a1d2b1d..0139e42c 100644 --- a/app/src/controller.c +++ b/app/src/controller.c @@ -4,19 +4,28 @@ #include "util/log.h" +#define SC_CONTROL_MSG_QUEUE_MAX 64 + bool sc_controller_init(struct sc_controller *controller, sc_socket control_socket, struct sc_acksync *acksync) { - cbuf_init(&controller->queue); + sc_vecdeque_init(&controller->queue); - bool ok = sc_receiver_init(&controller->receiver, control_socket, acksync); + bool ok = sc_vecdeque_reserve(&controller->queue, SC_CONTROL_MSG_QUEUE_MAX); if (!ok) { return false; } + ok = sc_receiver_init(&controller->receiver, control_socket, acksync); + if (!ok) { + sc_vecdeque_destroy(&controller->queue); + return false; + } + ok = sc_mutex_init(&controller->mutex); if (!ok) { sc_receiver_destroy(&controller->receiver); + sc_vecdeque_destroy(&controller->queue); return false; } @@ -24,6 +33,7 @@ sc_controller_init(struct sc_controller *controller, sc_socket control_socket, if (!ok) { sc_receiver_destroy(&controller->receiver); sc_mutex_destroy(&controller->mutex); + sc_vecdeque_destroy(&controller->queue); return false; } @@ -38,10 +48,12 @@ sc_controller_destroy(struct sc_controller *controller) { sc_cond_destroy(&controller->msg_cond); sc_mutex_destroy(&controller->mutex); - struct sc_control_msg msg; - while (cbuf_take(&controller->queue, &msg)) { - sc_control_msg_destroy(&msg); + while (!sc_vecdeque_is_empty(&controller->queue)) { + struct sc_control_msg *msg = sc_vecdeque_popref(&controller->queue); + assert(msg); + sc_control_msg_destroy(msg); } + sc_vecdeque_destroy(&controller->queue); sc_receiver_destroy(&controller->receiver); } @@ -54,13 +66,19 @@ sc_controller_push_msg(struct sc_controller *controller, } sc_mutex_lock(&controller->mutex); - bool was_empty = cbuf_is_empty(&controller->queue); - bool res = cbuf_push(&controller->queue, *msg); - if (was_empty) { - sc_cond_signal(&controller->msg_cond); + bool full = sc_vecdeque_is_full(&controller->queue); + if (!full) { + bool was_empty = sc_vecdeque_is_empty(&controller->queue); + sc_vecdeque_push_noresize(&controller->queue, *msg); + if (was_empty) { + sc_cond_signal(&controller->msg_cond); + } } + // Otherwise (if the queue is full), the msg is discarded + sc_mutex_unlock(&controller->mutex); - return res; + + return !full; } static bool @@ -82,7 +100,8 @@ run_controller(void *data) { for (;;) { sc_mutex_lock(&controller->mutex); - while (!controller->stopped && cbuf_is_empty(&controller->queue)) { + while (!controller->stopped + && sc_vecdeque_is_empty(&controller->queue)) { sc_cond_wait(&controller->msg_cond, &controller->mutex); } if (controller->stopped) { @@ -90,10 +109,9 @@ run_controller(void *data) { sc_mutex_unlock(&controller->mutex); break; } - struct sc_control_msg msg; - bool non_empty = cbuf_take(&controller->queue, &msg); - assert(non_empty); - (void) non_empty; + + assert(!sc_vecdeque_is_empty(&controller->queue)); + struct sc_control_msg msg = sc_vecdeque_pop(&controller->queue); sc_mutex_unlock(&controller->mutex); bool ok = process_msg(controller, &msg); diff --git a/app/src/controller.h b/app/src/controller.h index 67c3c58d..a044b2bf 100644 --- a/app/src/controller.h +++ b/app/src/controller.h @@ -8,11 +8,11 @@ #include "control_msg.h" #include "receiver.h" #include "util/acksync.h" -#include "util/cbuf.h" #include "util/net.h" #include "util/thread.h" +#include "util/vecdeque.h" -struct sc_control_msg_queue CBUF(struct sc_control_msg, 64); +struct sc_control_msg_queue SC_VECDEQUE(struct sc_control_msg); struct sc_controller { sc_socket control_socket; From a0a65b3c4da6b56ca320959451fdf2979a34738d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 1 Mar 2023 22:46:10 +0100 Subject: [PATCH 1432/2244] Use VecDeque in file_pusher Replace cbuf by VecDeque in file_pusher. As a side-effect, the new implementation does not limit the queue to an arbitrary value. --- app/src/file_pusher.c | 31 +++++++++++++++++++------------ app/src/file_pusher.h | 6 +++--- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/app/src/file_pusher.c b/app/src/file_pusher.c index f6757870..b49e93e5 100644 --- a/app/src/file_pusher.c +++ b/app/src/file_pusher.c @@ -19,7 +19,7 @@ sc_file_pusher_init(struct sc_file_pusher *fp, const char *serial, const char *push_target) { assert(serial); - cbuf_init(&fp->queue); + sc_vecdeque_init(&fp->queue); bool ok = sc_mutex_init(&fp->mutex); if (!ok) { @@ -65,9 +65,10 @@ sc_file_pusher_destroy(struct sc_file_pusher *fp) { sc_intr_destroy(&fp->intr); free(fp->serial); - struct sc_file_pusher_request req; - while (cbuf_take(&fp->queue, &req)) { - sc_file_pusher_request_destroy(&req); + while (!sc_vecdeque_is_empty(&fp->queue)) { + struct sc_file_pusher_request *req = sc_vecdeque_popref(&fp->queue); + assert(req); + sc_file_pusher_request_destroy(req); } } @@ -91,13 +92,20 @@ sc_file_pusher_request(struct sc_file_pusher *fp, }; sc_mutex_lock(&fp->mutex); - bool was_empty = cbuf_is_empty(&fp->queue); - bool res = cbuf_push(&fp->queue, req); + bool was_empty = sc_vecdeque_is_empty(&fp->queue); + bool res = sc_vecdeque_push(&fp->queue, req); + if (!res) { + LOG_OOM(); + sc_mutex_unlock(&fp->mutex); + return false; + } + if (was_empty) { sc_cond_signal(&fp->event_cond); } sc_mutex_unlock(&fp->mutex); - return res; + + return true; } static int @@ -113,7 +121,7 @@ run_file_pusher(void *data) { for (;;) { sc_mutex_lock(&fp->mutex); - while (!fp->stopped && cbuf_is_empty(&fp->queue)) { + while (!fp->stopped && sc_vecdeque_is_empty(&fp->queue)) { sc_cond_wait(&fp->event_cond, &fp->mutex); } if (fp->stopped) { @@ -121,10 +129,9 @@ run_file_pusher(void *data) { sc_mutex_unlock(&fp->mutex); break; } - struct sc_file_pusher_request req; - bool non_empty = cbuf_take(&fp->queue, &req); - assert(non_empty); - (void) non_empty; + + assert(!sc_vecdeque_is_empty(&fp->queue)); + struct sc_file_pusher_request req = sc_vecdeque_pop(&fp->queue); sc_mutex_unlock(&fp->mutex); if (req.action == SC_FILE_PUSHER_ACTION_INSTALL_APK) { diff --git a/app/src/file_pusher.h b/app/src/file_pusher.h index 0d934d6c..0ffb3721 100644 --- a/app/src/file_pusher.h +++ b/app/src/file_pusher.h @@ -5,9 +5,9 @@ #include -#include "util/cbuf.h" -#include "util/thread.h" #include "util/intr.h" +#include "util/thread.h" +#include "util/vecdeque.h" enum sc_file_pusher_action { SC_FILE_PUSHER_ACTION_INSTALL_APK, @@ -19,7 +19,7 @@ struct sc_file_pusher_request { char *file; }; -struct sc_file_pusher_request_queue CBUF(struct sc_file_pusher_request, 16); +struct sc_file_pusher_request_queue SC_VECDEQUE(struct sc_file_pusher_request); struct sc_file_pusher { char *serial; From f978e4d6dea2958b449906e33fdb965ef5bc26d4 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 1 Mar 2023 22:50:56 +0100 Subject: [PATCH 1433/2244] Use VecDeque in aoa_hid Replace cbuf by VecDeque in aoa_hid --- app/src/usb/aoa_hid.c | 41 +++++++++++++++++++++++++++-------------- app/src/usb/aoa_hid.h | 4 ++-- 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/app/src/usb/aoa_hid.c b/app/src/usb/aoa_hid.c index 0007169d..fb64e57c 100644 --- a/app/src/usb/aoa_hid.c +++ b/app/src/usb/aoa_hid.c @@ -14,6 +14,8 @@ #define DEFAULT_TIMEOUT 1000 +#define SC_HID_EVENT_QUEUE_MAX 64 + static void sc_hid_event_log(const struct sc_hid_event *event) { // HID Event: [00] FF FF FF FF... @@ -48,14 +50,20 @@ sc_hid_event_destroy(struct sc_hid_event *hid_event) { bool sc_aoa_init(struct sc_aoa *aoa, struct sc_usb *usb, struct sc_acksync *acksync) { - cbuf_init(&aoa->queue); + sc_vecdeque_init(&aoa->queue); + + if (!sc_vecdeque_reserve(&aoa->queue, SC_HID_EVENT_QUEUE_MAX)) { + return false; + } if (!sc_mutex_init(&aoa->mutex)) { + sc_vecdeque_destroy(&aoa->queue); return false; } if (!sc_cond_init(&aoa->event_cond)) { sc_mutex_destroy(&aoa->mutex); + sc_vecdeque_destroy(&aoa->queue); return false; } @@ -69,9 +77,10 @@ sc_aoa_init(struct sc_aoa *aoa, struct sc_usb *usb, void sc_aoa_destroy(struct sc_aoa *aoa) { // Destroy remaining events - struct sc_hid_event event; - while (cbuf_take(&aoa->queue, &event)) { - sc_hid_event_destroy(&event); + while (!sc_vecdeque_is_empty(&aoa->queue)) { + struct sc_hid_event *event = sc_vecdeque_popref(&aoa->queue); + assert(event); + sc_hid_event_destroy(event); } sc_cond_destroy(&aoa->event_cond); @@ -212,13 +221,19 @@ sc_aoa_push_hid_event(struct sc_aoa *aoa, const struct sc_hid_event *event) { } sc_mutex_lock(&aoa->mutex); - bool was_empty = cbuf_is_empty(&aoa->queue); - bool res = cbuf_push(&aoa->queue, *event); - if (was_empty) { - sc_cond_signal(&aoa->event_cond); + bool full = sc_vecdeque_is_full(&aoa->queue); + if (!full) { + bool was_empty = sc_vecdeque_is_empty(&aoa->queue); + sc_vecdeque_push_noresize(&aoa->queue, *event); + if (was_empty) { + sc_cond_signal(&aoa->event_cond); + } } + // Otherwise (if the queue is full), the event is discarded + sc_mutex_unlock(&aoa->mutex); - return res; + + return !full; } static int @@ -227,7 +242,7 @@ run_aoa_thread(void *data) { for (;;) { sc_mutex_lock(&aoa->mutex); - while (!aoa->stopped && cbuf_is_empty(&aoa->queue)) { + while (!aoa->stopped && sc_vecdeque_is_empty(&aoa->queue)) { sc_cond_wait(&aoa->event_cond, &aoa->mutex); } if (aoa->stopped) { @@ -235,11 +250,9 @@ run_aoa_thread(void *data) { sc_mutex_unlock(&aoa->mutex); break; } - struct sc_hid_event event; - bool non_empty = cbuf_take(&aoa->queue, &event); - assert(non_empty); - (void) non_empty; + assert(!sc_vecdeque_is_empty(&aoa->queue)); + struct sc_hid_event event = sc_vecdeque_pop(&aoa->queue); uint64_t ack_to_wait = event.ack_to_wait; sc_mutex_unlock(&aoa->mutex); diff --git a/app/src/usb/aoa_hid.h b/app/src/usb/aoa_hid.h index d785a0e9..8803c1d9 100644 --- a/app/src/usb/aoa_hid.h +++ b/app/src/usb/aoa_hid.h @@ -8,9 +8,9 @@ #include "usb.h" #include "util/acksync.h" -#include "util/cbuf.h" #include "util/thread.h" #include "util/tick.h" +#include "util/vecdeque.h" struct sc_hid_event { uint16_t accessory_id; @@ -27,7 +27,7 @@ sc_hid_event_init(struct sc_hid_event *hid_event, uint16_t accessory_id, void sc_hid_event_destroy(struct sc_hid_event *hid_event); -struct sc_hid_event_queue CBUF(struct sc_hid_event, 64); +struct sc_hid_event_queue SC_VECDEQUE(struct sc_hid_event); struct sc_aoa { struct sc_usb *usb; From 338310677e2ef441d09048e19a30c94ecf4fbca4 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 1 Mar 2023 22:54:08 +0100 Subject: [PATCH 1434/2244] Remove cbuf All uses have been replaced by VecDeque. --- app/meson.build | 3 -- app/src/util/cbuf.h | 52 ----------------------------- app/tests/test_cbuf.c | 78 ------------------------------------------- 3 files changed, 133 deletions(-) delete mode 100644 app/src/util/cbuf.h delete mode 100644 app/tests/test_cbuf.c diff --git a/app/meson.build b/app/meson.build index a238eb8f..35934131 100644 --- a/app/meson.build +++ b/app/meson.build @@ -261,9 +261,6 @@ if get_option('buildtype') == 'debug' 'tests/test_bytebuf.c', 'src/util/bytebuf.c', ]], - ['test_cbuf', [ - 'tests/test_cbuf.c', - ]], ['test_cli', [ 'tests/test_cli.c', 'src/cli.c', diff --git a/app/src/util/cbuf.h b/app/src/util/cbuf.h deleted file mode 100644 index 2a756171..00000000 --- a/app/src/util/cbuf.h +++ /dev/null @@ -1,52 +0,0 @@ -// generic circular buffer (bounded queue) implementation -#ifndef SC_CBUF_H -#define SC_CBUF_H - -#include "common.h" - -#include -#include - -// To define a circular buffer type of 20 ints: -// struct cbuf_int CBUF(int, 20); -// -// data has length CAP + 1 to distinguish empty vs full. -#define CBUF(TYPE, CAP) { \ - TYPE data[(CAP) + 1]; \ - size_t head; \ - size_t tail; \ -} - -#define cbuf_size_(PCBUF) \ - (sizeof((PCBUF)->data) / sizeof(*(PCBUF)->data)) - -#define cbuf_is_empty(PCBUF) \ - ((PCBUF)->head == (PCBUF)->tail) - -#define cbuf_is_full(PCBUF) \ - (((PCBUF)->head + 1) % cbuf_size_(PCBUF) == (PCBUF)->tail) - -#define cbuf_init(PCBUF) \ - (void) ((PCBUF)->head = (PCBUF)->tail = 0) - -#define cbuf_push(PCBUF, ITEM) \ - ({ \ - bool ok = !cbuf_is_full(PCBUF); \ - if (ok) { \ - (PCBUF)->data[(PCBUF)->head] = (ITEM); \ - (PCBUF)->head = ((PCBUF)->head + 1) % cbuf_size_(PCBUF); \ - } \ - ok; \ - }) - -#define cbuf_take(PCBUF, PITEM) \ - ({ \ - bool ok = !cbuf_is_empty(PCBUF); \ - if (ok) { \ - *(PITEM) = (PCBUF)->data[(PCBUF)->tail]; \ - (PCBUF)->tail = ((PCBUF)->tail + 1) % cbuf_size_(PCBUF); \ - } \ - ok; \ - }) - -#endif diff --git a/app/tests/test_cbuf.c b/app/tests/test_cbuf.c deleted file mode 100644 index 16674e92..00000000 --- a/app/tests/test_cbuf.c +++ /dev/null @@ -1,78 +0,0 @@ -#include "common.h" - -#include -#include - -#include "util/cbuf.h" - -struct int_queue CBUF(int, 32); - -static void test_cbuf_empty(void) { - struct int_queue queue; - cbuf_init(&queue); - - assert(cbuf_is_empty(&queue)); - - bool push_ok = cbuf_push(&queue, 42); - assert(push_ok); - assert(!cbuf_is_empty(&queue)); - - int item; - bool take_ok = cbuf_take(&queue, &item); - assert(take_ok); - assert(cbuf_is_empty(&queue)); - - bool take_empty_ok = cbuf_take(&queue, &item); - assert(!take_empty_ok); // the queue is empty -} - -static void test_cbuf_full(void) { - struct int_queue queue; - cbuf_init(&queue); - - assert(!cbuf_is_full(&queue)); - - // fill the queue - for (int i = 0; i < 32; ++i) { - bool ok = cbuf_push(&queue, i); - assert(ok); - } - bool ok = cbuf_push(&queue, 42); - assert(!ok); // the queue if full - - int item; - bool take_ok = cbuf_take(&queue, &item); - assert(take_ok); - assert(!cbuf_is_full(&queue)); -} - -static void test_cbuf_push_take(void) { - struct int_queue queue; - cbuf_init(&queue); - - bool push1_ok = cbuf_push(&queue, 42); - assert(push1_ok); - - bool push2_ok = cbuf_push(&queue, 35); - assert(push2_ok); - - int item; - - bool take1_ok = cbuf_take(&queue, &item); - assert(take1_ok); - assert(item == 42); - - bool take2_ok = cbuf_take(&queue, &item); - assert(take2_ok); - assert(item == 35); -} - -int main(int argc, char *argv[]) { - (void) argc; - (void) argv; - - test_cbuf_empty(); - test_cbuf_full(); - test_cbuf_push_take(); - return 0; -} From 6f38c6311b4db17858e221f66cbe9be1c7da12ca Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 1 Mar 2023 23:05:17 +0100 Subject: [PATCH 1435/2244] Remove sc_queue All uses have been replaced by VecDeque. --- app/meson.build | 3 -- app/src/util/queue.h | 77 ------------------------------------------ app/tests/test_queue.c | 43 ----------------------- 3 files changed, 123 deletions(-) delete mode 100644 app/src/util/queue.h delete mode 100644 app/tests/test_queue.c diff --git a/app/meson.build b/app/meson.build index 35934131..e34f7cc1 100644 --- a/app/meson.build +++ b/app/meson.build @@ -285,9 +285,6 @@ if get_option('buildtype') == 'debug' 'tests/test_device_msg_deserialize.c', 'src/device_msg.c', ]], - ['test_queue', [ - 'tests/test_queue.c', - ]], ['test_strbuf', [ 'tests/test_strbuf.c', 'src/util/strbuf.c', diff --git a/app/src/util/queue.h b/app/src/util/queue.h deleted file mode 100644 index 2233eca0..00000000 --- a/app/src/util/queue.h +++ /dev/null @@ -1,77 +0,0 @@ -// generic intrusive FIFO queue -#ifndef SC_QUEUE_H -#define SC_QUEUE_H - -#include "common.h" - -#include -#include -#include - -// To define a queue type of "struct foo": -// struct queue_foo QUEUE(struct foo); -#define SC_QUEUE(TYPE) { \ - TYPE *first; \ - TYPE *last; \ -} - -#define sc_queue_init(PQ) \ - (void) ((PQ)->first = (PQ)->last = NULL) - -#define sc_queue_is_empty(PQ) \ - !(PQ)->first - -// NEXTFIELD is the field in the ITEM type used for intrusive linked-list -// -// For example: -// struct foo { -// int value; -// struct foo *next; -// }; -// -// // define the type "struct my_queue" -// struct my_queue SC_QUEUE(struct foo); -// -// struct my_queue queue; -// sc_queue_init(&queue); -// -// struct foo v1 = { .value = 42 }; -// struct foo v2 = { .value = 27 }; -// -// sc_queue_push(&queue, next, v1); -// sc_queue_push(&queue, next, v2); -// -// struct foo *foo; -// sc_queue_take(&queue, next, &foo); -// assert(foo->value == 42); -// sc_queue_take(&queue, next, &foo); -// assert(foo->value == 27); -// assert(sc_queue_is_empty(&queue)); -// - -// push a new item into the queue -#define sc_queue_push(PQ, NEXTFIELD, ITEM) \ - (void) ({ \ - (ITEM)->NEXTFIELD = NULL; \ - if (sc_queue_is_empty(PQ)) { \ - (PQ)->first = (PQ)->last = (ITEM); \ - } else { \ - (PQ)->last->NEXTFIELD = (ITEM); \ - (PQ)->last = (ITEM); \ - } \ - }) - -// take the next item and remove it from the queue (the queue must not be empty) -// the result is stored in *(PITEM) -// (without typeof(), we could not store a local variable having the correct -// type so that we can "return" it) -#define sc_queue_take(PQ, NEXTFIELD, PITEM) \ - (void) ({ \ - assert(!sc_queue_is_empty(PQ)); \ - *(PITEM) = (PQ)->first; \ - (PQ)->first = (PQ)->first->NEXTFIELD; \ - }) - // no need to update (PQ)->last if the queue is left empty: - // (PQ)->last is undefined if !(PQ)->first anyway - -#endif diff --git a/app/tests/test_queue.c b/app/tests/test_queue.c deleted file mode 100644 index d8b2b4ec..00000000 --- a/app/tests/test_queue.c +++ /dev/null @@ -1,43 +0,0 @@ -#include "common.h" - -#include - -#include "util/queue.h" - -struct foo { - int value; - struct foo *next; -}; - -static void test_queue(void) { - struct my_queue SC_QUEUE(struct foo) queue; - sc_queue_init(&queue); - - assert(sc_queue_is_empty(&queue)); - - struct foo v1 = { .value = 42 }; - struct foo v2 = { .value = 27 }; - - sc_queue_push(&queue, next, &v1); - sc_queue_push(&queue, next, &v2); - - struct foo *foo; - - assert(!sc_queue_is_empty(&queue)); - sc_queue_take(&queue, next, &foo); - assert(foo->value == 42); - - assert(!sc_queue_is_empty(&queue)); - sc_queue_take(&queue, next, &foo); - assert(foo->value == 27); - - assert(sc_queue_is_empty(&queue)); -} - -int main(int argc, char *argv[]) { - (void) argc; - (void) argv; - - test_queue(); - return 0; -} From a3703340fc5f2d10d8b8d1a248200f302831c432 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 1 Mar 2023 18:24:31 +0100 Subject: [PATCH 1436/2244] Fix possible race condition on video_buffer end The video_buffer thread clears the queue once it is stopped, but new frames might still be pushed asynchronously. To avoid the problem, do not push any frame once the video_buffer is stopped. --- app/src/video_buffer.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/src/video_buffer.c b/app/src/video_buffer.c index b3b29098..a8f7f20a 100644 --- a/app/src/video_buffer.c +++ b/app/src/video_buffer.c @@ -99,6 +99,8 @@ run_buffering(void *data) { } stopped: + assert(vb->b.stopped); + // Flush queue while (!sc_vecdeque_is_empty(&vb->b.queue)) { struct sc_video_buffer_frame *p = sc_vecdeque_popref(&vb->b.queue); @@ -206,6 +208,11 @@ sc_video_buffer_push(struct sc_video_buffer *vb, const AVFrame *frame) { sc_mutex_lock(&vb->b.mutex); + if (vb->b.stopped) { + sc_mutex_unlock(&vb->b.mutex); + return false; + } + sc_tick pts = SC_TICK_FROM_US(frame->pts); sc_clock_update(&vb->b.clock, sc_tick_now(), pts); sc_cond_signal(&vb->b.wait_cond); From ad94ccca0bbed9cd8ddaaa931da2c7ed18bf060c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 1 Mar 2023 18:33:03 +0100 Subject: [PATCH 1437/2244] Stop the video buffer on error If an error occurs from the video buffer thread (typically an out-of-memory error), then stop. --- app/src/video_buffer.c | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/app/src/video_buffer.c b/app/src/video_buffer.c index a8f7f20a..49c01839 100644 --- a/app/src/video_buffer.c +++ b/app/src/video_buffer.c @@ -93,9 +93,16 @@ run_buffering(void *data) { pts, vb_frame.push_date, sc_tick_now()); #endif - sc_video_buffer_offer(vb, vb_frame.frame); - + bool ok = sc_video_buffer_offer(vb, vb_frame.frame); sc_video_buffer_frame_destroy(&vb_frame); + if (!ok) { + LOGE("Delayed frame could not be pushed, stopping"); + sc_mutex_lock(&vb->b.mutex); + // Prevent to push any new packet + vb->b.stopped = true; + sc_mutex_unlock(&vb->b.mutex); + goto stopped; + } } stopped: From 4540f1d69e7435b78af2c968c3b07b06729a714f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 1 Mar 2023 18:45:22 +0100 Subject: [PATCH 1438/2244] Report video buffer downstream errors Make the video buffer stop if its consumer could not receive a frame. --- app/src/screen.c | 21 +++++---------------- app/src/screen.h | 2 -- app/src/v4l2_sink.c | 4 +++- app/src/video_buffer.c | 3 +-- app/src/video_buffer.h | 2 +- 5 files changed, 10 insertions(+), 22 deletions(-) diff --git a/app/src/screen.c b/app/src/screen.c index a9a48eae..ce2e74bb 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -362,27 +362,17 @@ sc_screen_frame_sink_push(struct sc_frame_sink *sink, const AVFrame *frame) { return sc_video_buffer_push(&screen->vb, frame); } -static void +static bool sc_video_buffer_on_new_frame(struct sc_video_buffer *vb, bool previous_skipped, void *userdata) { (void) vb; struct sc_screen *screen = userdata; - // event_failed implies previous_skipped (the previous frame may not have - // been consumed if the event was not sent) - assert(!screen->event_failed || previous_skipped); - - bool need_new_event; if (previous_skipped) { sc_fps_counter_add_skipped_frame(&screen->fps_counter); // The SC_EVENT_NEW_FRAME triggered for the previous frame will consume - // this new frame instead, unless the previous event failed - need_new_event = screen->event_failed; + // this new frame instead } else { - need_new_event = true; - } - - if (need_new_event) { static SDL_Event new_frame_event = { .type = SC_EVENT_NEW_FRAME, }; @@ -391,11 +381,11 @@ sc_video_buffer_on_new_frame(struct sc_video_buffer *vb, bool previous_skipped, int ret = SDL_PushEvent(&new_frame_event); if (ret < 0) { LOGW("Could not post new frame event: %s", SDL_GetError()); - screen->event_failed = true; - } else { - screen->event_failed = false; + return false; } } + + return true; } bool @@ -405,7 +395,6 @@ sc_screen_init(struct sc_screen *screen, screen->has_frame = false; screen->fullscreen = false; screen->maximized = false; - screen->event_failed = false; screen->mouse_capture_key_pressed = 0; screen->req.x = params->window_x; diff --git a/app/src/screen.h b/app/src/screen.h index 222e418f..0952c79c 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -59,8 +59,6 @@ struct sc_screen { bool maximized; bool mipmaps; - bool event_failed; // in case SDL_PushEvent() returned an error - // To enable/disable mouse capture, a mouse capture key (LALT, LGUI or // RGUI) must be pressed. This variable tracks the pressed capture key. SDL_Keycode mouse_capture_key_pressed; diff --git a/app/src/v4l2_sink.c b/app/src/v4l2_sink.c index ba876b2b..5dfe37bc 100644 --- a/app/src/v4l2_sink.c +++ b/app/src/v4l2_sink.c @@ -141,7 +141,7 @@ run_v4l2_sink(void *data) { return 0; } -static void +static bool sc_video_buffer_on_new_frame(struct sc_video_buffer *vb, bool previous_skipped, void *userdata) { (void) vb; @@ -153,6 +153,8 @@ sc_video_buffer_on_new_frame(struct sc_video_buffer *vb, bool previous_skipped, sc_cond_signal(&vs->cond); sc_mutex_unlock(&vs->mutex); } + + return true; } static bool diff --git a/app/src/video_buffer.c b/app/src/video_buffer.c index 49c01839..7f771179 100644 --- a/app/src/video_buffer.c +++ b/app/src/video_buffer.c @@ -40,8 +40,7 @@ sc_video_buffer_offer(struct sc_video_buffer *vb, const AVFrame *frame) { return false; } - vb->cbs->on_new_frame(vb, previous_skipped, vb->cbs_userdata); - return true; + return vb->cbs->on_new_frame(vb, previous_skipped, vb->cbs_userdata); } static int diff --git a/app/src/video_buffer.h b/app/src/video_buffer.h index 41b09434..d183a484 100644 --- a/app/src/video_buffer.h +++ b/app/src/video_buffer.h @@ -45,7 +45,7 @@ struct sc_video_buffer { }; struct sc_video_buffer_callbacks { - void (*on_new_frame)(struct sc_video_buffer *vb, bool previous_skipped, + bool (*on_new_frame)(struct sc_video_buffer *vb, bool previous_skipped, void *userdata); }; From 6379c08012454300e399006e665a5e4f7d436202 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 9 Mar 2023 09:05:46 +0100 Subject: [PATCH 1439/2244] Fix buffering pts conversion The mistake had no effect, because tick is also internally expressed in microseconds. --- app/src/video_buffer.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/video_buffer.c b/app/src/video_buffer.c index 7f771179..74a4b042 100644 --- a/app/src/video_buffer.c +++ b/app/src/video_buffer.c @@ -65,7 +65,7 @@ run_buffering(void *data) { sc_tick max_deadline = sc_tick_now() + vb->buffering_time; // PTS (written by the server) are expressed in microseconds - sc_tick pts = SC_TICK_TO_US(vb_frame.frame->pts); + sc_tick pts = SC_TICK_FROM_US(vb_frame.frame->pts); bool timed_out = false; while (!vb->b.stopped && !timed_out) { From f410f2bdc468cd32dd099679471af08ca6a76a54 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 2 Mar 2023 00:31:43 +0100 Subject: [PATCH 1440/2244] Extract sc_delay_buffer A video buffer had 2 responsibilities: - handle the frame delaying mechanism (queuing packets and pushing them after the expected delay); - keep only the most recent frame (using a sc_frame_buffer). In order to be able to reuse only the frame delaying mechanism, extract it to a separate component, sc_delay_buffer. --- app/meson.build | 1 + app/src/delay_buffer.c | 246 +++++++++++++++++++++++++++++++++++++++++ app/src/delay_buffer.h | 69 ++++++++++++ app/src/video_buffer.c | 216 +++--------------------------------- app/src/video_buffer.h | 34 +----- 5 files changed, 337 insertions(+), 229 deletions(-) create mode 100644 app/src/delay_buffer.c create mode 100644 app/src/delay_buffer.h diff --git a/app/meson.build b/app/meson.build index e34f7cc1..7749d664 100644 --- a/app/meson.build +++ b/app/meson.build @@ -10,6 +10,7 @@ src = [ 'src/control_msg.c', 'src/controller.c', 'src/decoder.c', + 'src/delay_buffer.c', 'src/demuxer.c', 'src/device_msg.c', 'src/icon.c', diff --git a/app/src/delay_buffer.c b/app/src/delay_buffer.c new file mode 100644 index 00000000..95d47c9c --- /dev/null +++ b/app/src/delay_buffer.c @@ -0,0 +1,246 @@ +#include "delay_buffer.h" + +#include +#include + +#include +#include + +#include "util/log.h" + +#define SC_BUFFERING_NDEBUG // comment to debug + +static bool +sc_delayed_frame_init(struct sc_delayed_frame *dframe, const AVFrame *frame) { + dframe->frame = av_frame_alloc(); + if (!dframe->frame) { + LOG_OOM(); + return false; + } + + if (av_frame_ref(dframe->frame, frame)) { + LOG_OOM(); + av_frame_free(&dframe->frame); + return false; + } + + return true; +} + +static void +sc_delayed_frame_destroy(struct sc_delayed_frame *dframe) { + av_frame_unref(dframe->frame); + av_frame_free(&dframe->frame); +} + +static bool +sc_delay_buffer_offer(struct sc_delay_buffer *db, const AVFrame *frame) { + return db->cbs->on_new_frame(db, frame, db->cbs_userdata); +} + +static int +run_buffering(void *data) { + struct sc_delay_buffer *db = data; + + assert(db->delay > 0); + + for (;;) { + sc_mutex_lock(&db->b.mutex); + + while (!db->b.stopped && sc_vecdeque_is_empty(&db->b.queue)) { + sc_cond_wait(&db->b.queue_cond, &db->b.mutex); + } + + if (db->b.stopped) { + sc_mutex_unlock(&db->b.mutex); + goto stopped; + } + + struct sc_delayed_frame dframe = sc_vecdeque_pop(&db->b.queue); + + sc_tick max_deadline = sc_tick_now() + db->delay; + // PTS (written by the server) are expressed in microseconds + sc_tick pts = SC_TICK_FROM_US(dframe.frame->pts); + + bool timed_out = false; + while (!db->b.stopped && !timed_out) { + sc_tick deadline = sc_clock_to_system_time(&db->b.clock, pts) + + db->delay; + if (deadline > max_deadline) { + deadline = max_deadline; + } + + timed_out = + !sc_cond_timedwait(&db->b.wait_cond, &db->b.mutex, deadline); + } + + bool stopped = db->b.stopped; + sc_mutex_unlock(&db->b.mutex); + + if (stopped) { + sc_delayed_frame_destroy(&dframe); + goto stopped; + } + +#ifndef SC_BUFFERING_NDEBUG + LOGD("Buffering: %" PRItick ";%" PRItick ";%" PRItick, + pts, dframe.push_date, sc_tick_now()); +#endif + + bool ok = sc_delay_buffer_offer(db, dframe.frame); + sc_delayed_frame_destroy(&dframe); + if (!ok) { + LOGE("Delayed frame could not be pushed, stopping"); + sc_mutex_lock(&db->b.mutex); + // Prevent to push any new packet + db->b.stopped = true; + sc_mutex_unlock(&db->b.mutex); + goto stopped; + } + } + +stopped: + assert(db->b.stopped); + + // Flush queue + while (!sc_vecdeque_is_empty(&db->b.queue)) { + struct sc_delayed_frame *dframe = sc_vecdeque_popref(&db->b.queue); + sc_delayed_frame_destroy(dframe); + } + + LOGD("Buffering thread ended"); + + return 0; +} + +bool +sc_delay_buffer_init(struct sc_delay_buffer *db, sc_tick delay, + const struct sc_delay_buffer_callbacks *cbs, + void *cbs_userdata) { + assert(delay >= 0); + + if (delay) { + bool ok = sc_mutex_init(&db->b.mutex); + if (!ok) { + return false; + } + + ok = sc_cond_init(&db->b.queue_cond); + if (!ok) { + sc_mutex_destroy(&db->b.mutex); + return false; + } + + ok = sc_cond_init(&db->b.wait_cond); + if (!ok) { + sc_cond_destroy(&db->b.queue_cond); + sc_mutex_destroy(&db->b.mutex); + return false; + } + + sc_clock_init(&db->b.clock); + sc_vecdeque_init(&db->b.queue); + } + + assert(cbs); + assert(cbs->on_new_frame); + + db->delay = delay; + db->cbs = cbs; + db->cbs_userdata = cbs_userdata; + + return true; +} + +bool +sc_delay_buffer_start(struct sc_delay_buffer *db) { + if (db->delay) { + bool ok = + sc_thread_create(&db->b.thread, run_buffering, "scrcpy-dbuf", db); + if (!ok) { + LOGE("Could not start buffering thread"); + return false; + } + } + + return true; +} + +void +sc_delay_buffer_stop(struct sc_delay_buffer *db) { + if (db->delay) { + sc_mutex_lock(&db->b.mutex); + db->b.stopped = true; + sc_cond_signal(&db->b.queue_cond); + sc_cond_signal(&db->b.wait_cond); + sc_mutex_unlock(&db->b.mutex); + } +} + +void +sc_delay_buffer_join(struct sc_delay_buffer *db) { + if (db->delay) { + sc_thread_join(&db->b.thread, NULL); + } +} + +void +sc_delay_buffer_destroy(struct sc_delay_buffer *db) { + if (db->delay) { + sc_cond_destroy(&db->b.wait_cond); + sc_cond_destroy(&db->b.queue_cond); + sc_mutex_destroy(&db->b.mutex); + } +} + +bool +sc_delay_buffer_push(struct sc_delay_buffer *db, const AVFrame *frame) { + if (!db->delay) { + // No buffering + return sc_delay_buffer_offer(db, frame); + } + + sc_mutex_lock(&db->b.mutex); + + if (db->b.stopped) { + sc_mutex_unlock(&db->b.mutex); + return false; + } + + sc_tick pts = SC_TICK_FROM_US(frame->pts); + sc_clock_update(&db->b.clock, sc_tick_now(), pts); + sc_cond_signal(&db->b.wait_cond); + + if (db->b.clock.count == 1) { + sc_mutex_unlock(&db->b.mutex); + // First frame, offer it immediately, for two reasons: + // - not to delay the opening of the scrcpy window + // - the buffering estimation needs at least two clock points, so it + // could not handle the first frame + return sc_delay_buffer_offer(db, frame); + } + + struct sc_delayed_frame dframe; + bool ok = sc_delayed_frame_init(&dframe, frame); + if (!ok) { + sc_mutex_unlock(&db->b.mutex); + return false; + } + +#ifndef SC_BUFFERING_NDEBUG + dframe.push_date = sc_tick_now(); +#endif + + ok = sc_vecdeque_push(&db->b.queue, dframe); + if (!ok) { + sc_mutex_unlock(&db->b.mutex); + LOG_OOM(); + return false; + } + + sc_cond_signal(&db->b.queue_cond); + + sc_mutex_unlock(&db->b.mutex); + + return true; +} diff --git a/app/src/delay_buffer.h b/app/src/delay_buffer.h new file mode 100644 index 00000000..9e5347c7 --- /dev/null +++ b/app/src/delay_buffer.h @@ -0,0 +1,69 @@ +#ifndef SC_DELAY_BUFFER_H +#define SC_DELAY_BUFFER_H + +#include "common.h" + +#include + +#include "clock.h" +#include "util/thread.h" +#include "util/tick.h" +#include "util/vecdeque.h" + +// forward declarations +typedef struct AVFrame AVFrame; + +struct sc_delayed_frame { + AVFrame *frame; +#ifndef NDEBUG + sc_tick push_date; +#endif +}; + +struct sc_delayed_frame_queue SC_VECDEQUE(struct sc_delayed_frame); + +struct sc_delay_buffer { + sc_tick delay; + + // only if delay > 0 + struct { + sc_thread thread; + sc_mutex mutex; + sc_cond queue_cond; + sc_cond wait_cond; + + struct sc_clock clock; + struct sc_delayed_frame_queue queue; + bool stopped; + } b; // buffering + + const struct sc_delay_buffer_callbacks *cbs; + void *cbs_userdata; +}; + +struct sc_delay_buffer_callbacks { + bool (*on_new_frame)(struct sc_delay_buffer *db, const AVFrame *frame, + void *userdata); +}; + +bool +sc_delay_buffer_init(struct sc_delay_buffer *db, sc_tick delay, + const struct sc_delay_buffer_callbacks *cbs, + void *cbs_userdata); + +bool +sc_delay_buffer_start(struct sc_delay_buffer *db); + +void +sc_delay_buffer_stop(struct sc_delay_buffer *db); + +void +sc_delay_buffer_join(struct sc_delay_buffer *db); + +void +sc_delay_buffer_destroy(struct sc_delay_buffer *db); + +bool +sc_delay_buffer_push(struct sc_delay_buffer *db, const AVFrame *frame); + +#endif diff --git a/app/src/video_buffer.c b/app/src/video_buffer.c index 74a4b042..da47a0b5 100644 --- a/app/src/video_buffer.c +++ b/app/src/video_buffer.c @@ -8,32 +8,13 @@ #include "util/log.h" -#define SC_BUFFERING_NDEBUG // comment to debug - static bool -sc_video_buffer_frame_init(struct sc_video_buffer_frame *vb_frame, - const AVFrame *frame) { - vb_frame->frame = av_frame_alloc(); - if (!vb_frame->frame) { - return false; - } +sc_delay_buffer_on_new_frame(struct sc_delay_buffer *db, const AVFrame *frame, + void *userdata) { + (void) db; - if (av_frame_ref(vb_frame->frame, frame)) { - av_frame_free(&vb_frame->frame); - return false; - } + struct sc_video_buffer *vb = userdata; - return true; -} - -static void -sc_video_buffer_frame_destroy(struct sc_video_buffer_frame *vb_frame) { - av_frame_unref(vb_frame->frame); - av_frame_free(&vb_frame->frame); -} - -static bool -sc_video_buffer_offer(struct sc_video_buffer *vb, const AVFrame *frame) { bool previous_skipped; bool ok = sc_frame_buffer_push(&vb->fb, frame, &previous_skipped); if (!ok) { @@ -43,83 +24,8 @@ sc_video_buffer_offer(struct sc_video_buffer *vb, const AVFrame *frame) { return vb->cbs->on_new_frame(vb, previous_skipped, vb->cbs_userdata); } -static int -run_buffering(void *data) { - struct sc_video_buffer *vb = data; - - assert(vb->buffering_time > 0); - - for (;;) { - sc_mutex_lock(&vb->b.mutex); - - while (!vb->b.stopped && sc_vecdeque_is_empty(&vb->b.queue)) { - sc_cond_wait(&vb->b.queue_cond, &vb->b.mutex); - } - - if (vb->b.stopped) { - sc_mutex_unlock(&vb->b.mutex); - goto stopped; - } - - struct sc_video_buffer_frame vb_frame = sc_vecdeque_pop(&vb->b.queue); - - sc_tick max_deadline = sc_tick_now() + vb->buffering_time; - // PTS (written by the server) are expressed in microseconds - sc_tick pts = SC_TICK_FROM_US(vb_frame.frame->pts); - - bool timed_out = false; - while (!vb->b.stopped && !timed_out) { - sc_tick deadline = sc_clock_to_system_time(&vb->b.clock, pts) - + vb->buffering_time; - if (deadline > max_deadline) { - deadline = max_deadline; - } - - timed_out = - !sc_cond_timedwait(&vb->b.wait_cond, &vb->b.mutex, deadline); - } - - if (vb->b.stopped) { - sc_video_buffer_frame_destroy(&vb_frame); - sc_mutex_unlock(&vb->b.mutex); - goto stopped; - } - - sc_mutex_unlock(&vb->b.mutex); - -#ifndef SC_BUFFERING_NDEBUG - LOGD("Buffering: %" PRItick ";%" PRItick ";%" PRItick, - pts, vb_frame.push_date, sc_tick_now()); -#endif - - bool ok = sc_video_buffer_offer(vb, vb_frame.frame); - sc_video_buffer_frame_destroy(&vb_frame); - if (!ok) { - LOGE("Delayed frame could not be pushed, stopping"); - sc_mutex_lock(&vb->b.mutex); - // Prevent to push any new packet - vb->b.stopped = true; - sc_mutex_unlock(&vb->b.mutex); - goto stopped; - } - } - -stopped: - assert(vb->b.stopped); - - // Flush queue - while (!sc_vecdeque_is_empty(&vb->b.queue)) { - struct sc_video_buffer_frame *p = sc_vecdeque_popref(&vb->b.queue); - sc_video_buffer_frame_destroy(p); - } - - LOGD("Buffering thread ended"); - - return 0; -} - bool -sc_video_buffer_init(struct sc_video_buffer *vb, sc_tick buffering_time, +sc_video_buffer_init(struct sc_video_buffer *vb, sc_tick delay, const struct sc_video_buffer_callbacks *cbs, void *cbs_userdata) { bool ok = sc_frame_buffer_init(&vb->fb); @@ -127,135 +33,49 @@ sc_video_buffer_init(struct sc_video_buffer *vb, sc_tick buffering_time, return false; } - assert(buffering_time >= 0); - if (buffering_time) { - ok = sc_mutex_init(&vb->b.mutex); - if (!ok) { - sc_frame_buffer_destroy(&vb->fb); - return false; - } + static const struct sc_delay_buffer_callbacks db_cbs = { + .on_new_frame = sc_delay_buffer_on_new_frame, + }; - ok = sc_cond_init(&vb->b.queue_cond); - if (!ok) { - sc_mutex_destroy(&vb->b.mutex); - sc_frame_buffer_destroy(&vb->fb); - return false; - } - - ok = sc_cond_init(&vb->b.wait_cond); - if (!ok) { - sc_cond_destroy(&vb->b.queue_cond); - sc_mutex_destroy(&vb->b.mutex); - sc_frame_buffer_destroy(&vb->fb); - return false; - } - - sc_clock_init(&vb->b.clock); - sc_vecdeque_init(&vb->b.queue); + ok = sc_delay_buffer_init(&vb->db, delay, &db_cbs, vb); + if (!ok) { + sc_frame_buffer_destroy(&vb->fb); + return false; } assert(cbs); assert(cbs->on_new_frame); - vb->buffering_time = buffering_time; vb->cbs = cbs; vb->cbs_userdata = cbs_userdata; + return true; } bool sc_video_buffer_start(struct sc_video_buffer *vb) { - if (vb->buffering_time) { - bool ok = - sc_thread_create(&vb->b.thread, run_buffering, "scrcpy-vbuf", vb); - if (!ok) { - LOGE("Could not start buffering thread"); - return false; - } - } - - return true; + return sc_delay_buffer_start(&vb->db); } void sc_video_buffer_stop(struct sc_video_buffer *vb) { - if (vb->buffering_time) { - sc_mutex_lock(&vb->b.mutex); - vb->b.stopped = true; - sc_cond_signal(&vb->b.queue_cond); - sc_cond_signal(&vb->b.wait_cond); - sc_mutex_unlock(&vb->b.mutex); - } + return sc_delay_buffer_stop(&vb->db); } void sc_video_buffer_join(struct sc_video_buffer *vb) { - if (vb->buffering_time) { - sc_thread_join(&vb->b.thread, NULL); - } + return sc_delay_buffer_join(&vb->db); } void sc_video_buffer_destroy(struct sc_video_buffer *vb) { sc_frame_buffer_destroy(&vb->fb); - if (vb->buffering_time) { - sc_cond_destroy(&vb->b.wait_cond); - sc_cond_destroy(&vb->b.queue_cond); - sc_mutex_destroy(&vb->b.mutex); - } + sc_delay_buffer_destroy(&vb->db); } bool sc_video_buffer_push(struct sc_video_buffer *vb, const AVFrame *frame) { - if (!vb->buffering_time) { - // No buffering - return sc_video_buffer_offer(vb, frame); - } - - sc_mutex_lock(&vb->b.mutex); - - if (vb->b.stopped) { - sc_mutex_unlock(&vb->b.mutex); - return false; - } - - sc_tick pts = SC_TICK_FROM_US(frame->pts); - sc_clock_update(&vb->b.clock, sc_tick_now(), pts); - sc_cond_signal(&vb->b.wait_cond); - - if (vb->b.clock.count == 1) { - sc_mutex_unlock(&vb->b.mutex); - // First frame, offer it immediately, for two reasons: - // - not to delay the opening of the scrcpy window - // - the buffering estimation needs at least two clock points, so it - // could not handle the first frame - return sc_video_buffer_offer(vb, frame); - } - - struct sc_video_buffer_frame vb_frame; - bool ok = sc_video_buffer_frame_init(&vb_frame, frame); - if (!ok) { - sc_mutex_unlock(&vb->b.mutex); - LOG_OOM(); - return false; - } - -#ifndef SC_BUFFERING_NDEBUG - vb_frame.push_date = sc_tick_now(); -#endif - - ok = sc_vecdeque_push(&vb->b.queue, vb_frame); - if (!ok) { - sc_mutex_unlock(&vb->b.mutex); - LOG_OOM(); - return false; - } - - sc_cond_signal(&vb->b.queue_cond); - - sc_mutex_unlock(&vb->b.mutex); - - return true; + return sc_delay_buffer_push(&vb->db, frame); } void diff --git a/app/src/video_buffer.h b/app/src/video_buffer.h index d183a484..ebca3915 100644 --- a/app/src/video_buffer.h +++ b/app/src/video_buffer.h @@ -5,41 +5,13 @@ #include -#include "clock.h" +#include "delay_buffer.h" #include "frame_buffer.h" -#include "util/thread.h" -#include "util/tick.h" -#include "util/vecdeque.h" - -// forward declarations -typedef struct AVFrame AVFrame; - -struct sc_video_buffer_frame { - AVFrame *frame; -#ifndef NDEBUG - sc_tick push_date; -#endif -}; - -struct sc_video_buffer_frame_queue SC_VECDEQUE(struct sc_video_buffer_frame); struct sc_video_buffer { + struct sc_delay_buffer db; struct sc_frame_buffer fb; - sc_tick buffering_time; - - // only if buffering_time > 0 - struct { - sc_thread thread; - sc_mutex mutex; - sc_cond queue_cond; - sc_cond wait_cond; - - struct sc_clock clock; - struct sc_video_buffer_frame_queue queue; - bool stopped; - } b; // buffering - const struct sc_video_buffer_callbacks *cbs; void *cbs_userdata; }; @@ -50,7 +22,7 @@ struct sc_video_buffer_callbacks { }; bool -sc_video_buffer_init(struct sc_video_buffer *vb, sc_tick buffering_time, +sc_video_buffer_init(struct sc_video_buffer *vb, sc_tick delay, const struct sc_video_buffer_callbacks *cbs, void *cbs_userdata); From c39054a63da52ab266ed26d78e2329c48e4e0216 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 2 Mar 2023 09:07:25 +0100 Subject: [PATCH 1441/2244] Introduce packet source trait There was a packet sink trait, implemented by components able to receive AVPackets, but each packet source had to manually send packets to sinks. In order to mutualise sink management, add a packet source trait. --- app/meson.build | 1 + app/src/trait/packet_source.c | 70 +++++++++++++++++++++++++++++++++++ app/src/trait/packet_source.h | 41 ++++++++++++++++++++ 3 files changed, 112 insertions(+) create mode 100644 app/src/trait/packet_source.c create mode 100644 app/src/trait/packet_source.h diff --git a/app/meson.build b/app/meson.build index 7749d664..82105876 100644 --- a/app/meson.build +++ b/app/meson.build @@ -30,6 +30,7 @@ src = [ 'src/server.c', 'src/version.c', 'src/video_buffer.c', + 'src/trait/packet_source.c', 'src/util/acksync.c', 'src/util/bytebuf.c', 'src/util/file.c', diff --git a/app/src/trait/packet_source.c b/app/src/trait/packet_source.c new file mode 100644 index 00000000..df678e16 --- /dev/null +++ b/app/src/trait/packet_source.c @@ -0,0 +1,70 @@ +#include "packet_source.h" + +void +sc_packet_source_init(struct sc_packet_source *source) { + source->sink_count = 0; +} + +void +sc_packet_source_add_sink(struct sc_packet_source *source, + struct sc_packet_sink *sink) { + assert(source->sink_count < SC_PACKET_SOURCE_MAX_SINKS); + assert(sink); + assert(sink->ops); + source->sinks[source->sink_count++] = sink; +} + +static void +sc_packet_source_sinks_close_firsts(struct sc_packet_source *source, + unsigned count) { + while (count) { + struct sc_packet_sink *sink = source->sinks[--count]; + sink->ops->close(sink); + } +} + +bool +sc_packet_source_sinks_open(struct sc_packet_source *source, + const AVCodec *codec) { + assert(source->sink_count); + for (unsigned i = 0; i < source->sink_count; ++i) { + struct sc_packet_sink *sink = source->sinks[i]; + if (!sink->ops->open(sink, codec)) { + sc_packet_source_sinks_close_firsts(source, i); + return false; + } + } + + return true; +} + +void +sc_packet_source_sinks_close(struct sc_packet_source *source) { + assert(source->sink_count); + sc_packet_source_sinks_close_firsts(source, source->sink_count); +} + +bool +sc_packet_source_sinks_push(struct sc_packet_source *source, + const AVPacket *packet) { + assert(source->sink_count); + for (unsigned i = 0; i < source->sink_count; ++i) { + struct sc_packet_sink *sink = source->sinks[i]; + if (!sink->ops->push(sink, packet)) { + return false; + } + } + + return true; +} + +void +sc_packet_source_sinks_disable(struct sc_packet_source *source) { + assert(source->sink_count); + for (unsigned i = 0; i < source->sink_count; ++i) { + struct sc_packet_sink *sink = source->sinks[i]; + if (sink->ops->disable) { + sink->ops->disable(sink); + } + } +} diff --git a/app/src/trait/packet_source.h b/app/src/trait/packet_source.h new file mode 100644 index 00000000..c34aa5d3 --- /dev/null +++ b/app/src/trait/packet_source.h @@ -0,0 +1,41 @@ +#ifndef SC_PACKET_SOURCE_H +#define SC_PACKET_SOURCE_H + +#include "common.h" + +#include "packet_sink.h" + +#define SC_PACKET_SOURCE_MAX_SINKS 2 + +/** + * Packet source trait + * + * Component able to send AVPackets should implement this trait. + */ +struct sc_packet_source { + struct sc_packet_sink *sinks[SC_PACKET_SOURCE_MAX_SINKS]; + unsigned sink_count; +}; + +void +sc_packet_source_init(struct sc_packet_source *source); + +void +sc_packet_source_add_sink(struct sc_packet_source *source, + struct sc_packet_sink *sink); + +bool +sc_packet_source_sinks_open(struct sc_packet_source *source, + const AVCodec *codec); + +void +sc_packet_source_sinks_close(struct sc_packet_source *source); + +bool +sc_packet_source_sinks_push(struct sc_packet_source *source, + const AVPacket *packet); + +void +sc_packet_source_sinks_disable(struct sc_packet_source *source); + +#endif From f3197e178d297544774c37c766907b1992929e67 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 2 Mar 2023 09:20:37 +0100 Subject: [PATCH 1442/2244] Use packet source trait in demuxer --- app/src/demuxer.c | 83 +++++------------------------------------------ app/src/demuxer.h | 11 ++----- app/src/scrcpy.c | 13 +++++--- 3 files changed, 19 insertions(+), 88 deletions(-) diff --git a/app/src/demuxer.c b/app/src/demuxer.c index 5977a28a..15a595a0 100644 --- a/app/src/demuxer.c +++ b/app/src/demuxer.c @@ -112,65 +112,6 @@ sc_demuxer_recv_packet(struct sc_demuxer *demuxer, AVPacket *packet) { return true; } -static bool -push_packet_to_sinks(struct sc_demuxer *demuxer, const AVPacket *packet) { - for (unsigned i = 0; i < demuxer->sink_count; ++i) { - struct sc_packet_sink *sink = demuxer->sinks[i]; - if (!sink->ops->push(sink, packet)) { - return false; - } - } - - return true; -} - -static bool -sc_demuxer_push_packet(struct sc_demuxer *demuxer, AVPacket *packet) { - bool ok = push_packet_to_sinks(demuxer, packet); - if (!ok) { - LOGE("Demuxer '%s': could not process packet", demuxer->name); - return false; - } - - return true; -} - -static void -sc_demuxer_close_first_sinks(struct sc_demuxer *demuxer, unsigned count) { - while (count) { - struct sc_packet_sink *sink = demuxer->sinks[--count]; - sink->ops->close(sink); - } -} - -static inline void -sc_demuxer_close_sinks(struct sc_demuxer *demuxer) { - sc_demuxer_close_first_sinks(demuxer, demuxer->sink_count); -} - -static bool -sc_demuxer_open_sinks(struct sc_demuxer *demuxer, const AVCodec *codec) { - for (unsigned i = 0; i < demuxer->sink_count; ++i) { - struct sc_packet_sink *sink = demuxer->sinks[i]; - if (!sink->ops->open(sink, codec)) { - sc_demuxer_close_first_sinks(demuxer, i); - return false; - } - } - - return true; -} - -static void -sc_demuxer_disable_sinks(struct sc_demuxer *demuxer) { - for (unsigned i = 0; i < demuxer->sink_count; ++i) { - struct sc_packet_sink *sink = demuxer->sinks[i]; - if (sink->ops->disable) { - sink->ops->disable(sink); - } - } -} - static int run_demuxer(void *data) { struct sc_demuxer *demuxer = data; @@ -189,7 +130,7 @@ run_demuxer(void *data) { if (raw_codec_id == 0) { LOGW("Demuxer '%s': stream explicitly disabled by the device", demuxer->name); - sc_demuxer_disable_sinks(demuxer); + sc_packet_source_sinks_disable(&demuxer->packet_source); status = SC_DEMUXER_STATUS_DISABLED; goto end; } @@ -204,7 +145,7 @@ run_demuxer(void *data) { if (codec_id == AV_CODEC_ID_NONE) { LOGE("Demuxer '%s': stream disabled due to unsupported codec", demuxer->name); - sc_demuxer_disable_sinks(demuxer); + sc_packet_source_sinks_disable(&demuxer->packet_source); goto end; } @@ -212,11 +153,11 @@ run_demuxer(void *data) { if (!codec) { LOGE("Demuxer '%s': stream disabled due to missing decoder", demuxer->name); - sc_demuxer_disable_sinks(demuxer); + sc_packet_source_sinks_disable(&demuxer->packet_source); goto end; } - if (!sc_demuxer_open_sinks(demuxer, codec)) { + if (!sc_packet_source_sinks_open(&demuxer->packet_source, codec)) { goto end; } @@ -253,10 +194,10 @@ run_demuxer(void *data) { } } - ok = sc_demuxer_push_packet(demuxer, packet); + ok = sc_packet_source_sinks_push(&demuxer->packet_source, packet); av_packet_unref(packet); if (!ok) { - // cannot process packet (error already logged) + // The sink already logged its concrete error break; } } @@ -269,7 +210,7 @@ run_demuxer(void *data) { av_packet_free(&packet); finally_close_sinks: - sc_demuxer_close_sinks(demuxer); + sc_packet_source_sinks_close(&demuxer->packet_source); end: demuxer->cbs->on_ended(demuxer, status, demuxer->cbs_userdata); @@ -283,7 +224,7 @@ sc_demuxer_init(struct sc_demuxer *demuxer, const char *name, sc_socket socket, demuxer->name = name; // statically allocated demuxer->socket = socket; - demuxer->sink_count = 0; + sc_packet_source_init(&demuxer->packet_source); assert(cbs && cbs->on_ended); @@ -291,14 +232,6 @@ sc_demuxer_init(struct sc_demuxer *demuxer, const char *name, sc_socket socket, demuxer->cbs_userdata = cbs_userdata; } -void -sc_demuxer_add_sink(struct sc_demuxer *demuxer, struct sc_packet_sink *sink) { - assert(demuxer->sink_count < SC_DEMUXER_MAX_SINKS); - assert(sink); - assert(sink->ops); - demuxer->sinks[demuxer->sink_count++] = sink; -} - bool sc_demuxer_start(struct sc_demuxer *demuxer) { LOGD("Demuxer '%s': starting thread", demuxer->name); diff --git a/app/src/demuxer.h b/app/src/demuxer.h index d0e41add..5587d12d 100644 --- a/app/src/demuxer.h +++ b/app/src/demuxer.h @@ -8,21 +8,19 @@ #include #include +#include "trait/packet_source.h" #include "trait/packet_sink.h" #include "util/net.h" #include "util/thread.h" -#define SC_DEMUXER_MAX_SINKS 2 - struct sc_demuxer { + struct sc_packet_source packet_source; // packet source trait + const char *name; // must be statically allocated (e.g. a string literal) sc_socket socket; sc_thread thread; - struct sc_packet_sink *sinks[SC_DEMUXER_MAX_SINKS]; - unsigned sink_count; - const struct sc_demuxer_callbacks *cbs; void *cbs_userdata; }; @@ -43,9 +41,6 @@ void sc_demuxer_init(struct sc_demuxer *demuxer, const char *name, sc_socket socket, const struct sc_demuxer_callbacks *cbs, void *cbs_userdata); -void -sc_demuxer_add_sink(struct sc_demuxer *demuxer, struct sc_packet_sink *sink); - bool sc_demuxer_start(struct sc_demuxer *demuxer); diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 4355d71b..d9625a44 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -444,11 +444,13 @@ scrcpy(struct scrcpy_options *options) { #endif if (needs_video_decoder) { sc_decoder_init(&s->video_decoder, "video"); - sc_demuxer_add_sink(&s->video_demuxer, &s->video_decoder.packet_sink); + sc_packet_source_add_sink(&s->video_demuxer.packet_source, + &s->video_decoder.packet_sink); } if (needs_audio_decoder) { sc_decoder_init(&s->audio_decoder, "audio"); - sc_demuxer_add_sink(&s->audio_demuxer, &s->audio_decoder.packet_sink); + sc_packet_source_add_sink(&s->audio_demuxer.packet_source, + &s->audio_decoder.packet_sink); } if (options->record_filename) { @@ -467,10 +469,11 @@ scrcpy(struct scrcpy_options *options) { } recorder_started = true; - sc_demuxer_add_sink(&s->video_demuxer, &s->recorder.video_packet_sink); + sc_packet_source_add_sink(&s->video_demuxer.packet_source, + &s->recorder.video_packet_sink); if (options->audio) { - sc_demuxer_add_sink(&s->audio_demuxer, - &s->recorder.audio_packet_sink); + sc_packet_source_add_sink(&s->audio_demuxer.packet_source, + &s->recorder.audio_packet_sink); } } From 6543964f12b9089d2b5cf57a9aab7e7fe624842f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 2 Mar 2023 09:25:25 +0100 Subject: [PATCH 1443/2244] Introduce frame source trait There was a frame sink trait, implemented by components able to receive AVFrames, but each frame source had to manually send frame to sinks. In order to mutualise sink management, add a frame sink trait. --- app/meson.build | 1 + app/src/trait/frame_source.c | 59 ++++++++++++++++++++++++++++++++++++ app/src/trait/frame_source.h | 38 +++++++++++++++++++++++ 3 files changed, 98 insertions(+) create mode 100644 app/src/trait/frame_source.c create mode 100644 app/src/trait/frame_source.h diff --git a/app/meson.build b/app/meson.build index 82105876..9f73b434 100644 --- a/app/meson.build +++ b/app/meson.build @@ -30,6 +30,7 @@ src = [ 'src/server.c', 'src/version.c', 'src/video_buffer.c', + 'src/trait/frame_source.c', 'src/trait/packet_source.c', 'src/util/acksync.c', 'src/util/bytebuf.c', diff --git a/app/src/trait/frame_source.c b/app/src/trait/frame_source.c new file mode 100644 index 00000000..416eccd9 --- /dev/null +++ b/app/src/trait/frame_source.c @@ -0,0 +1,59 @@ +#include "frame_source.h" + +void +sc_frame_source_init(struct sc_frame_source *source) { + source->sink_count = 0; +} + +void +sc_frame_source_add_sink(struct sc_frame_source *source, + struct sc_frame_sink *sink) { + assert(source->sink_count < SC_FRAME_SOURCE_MAX_SINKS); + assert(sink); + assert(sink->ops); + source->sinks[source->sink_count++] = sink; +} + +static void +sc_frame_source_sinks_close_firsts(struct sc_frame_source *source, + unsigned count) { + while (count) { + struct sc_frame_sink *sink = source->sinks[--count]; + sink->ops->close(sink); + } +} + +bool +sc_frame_source_sinks_open(struct sc_frame_source *source, + const AVCodecContext *ctx) { + assert(source->sink_count); + for (unsigned i = 0; i < source->sink_count; ++i) { + struct sc_frame_sink *sink = source->sinks[i]; + if (!sink->ops->open(sink, ctx)) { + sc_frame_source_sinks_close_firsts(source, i); + return false; + } + } + + return true; +} + +void +sc_frame_source_sinks_close(struct sc_frame_source *source) { + assert(source->sink_count); + sc_frame_source_sinks_close_firsts(source, source->sink_count); +} + +bool +sc_frame_source_sinks_push(struct sc_frame_source *source, + const AVFrame *frame) { + assert(source->sink_count); + for (unsigned i = 0; i < source->sink_count; ++i) { + struct sc_frame_sink *sink = source->sinks[i]; + if (!sink->ops->push(sink, frame)) { + return false; + } + } + + return true; +} diff --git a/app/src/trait/frame_source.h b/app/src/trait/frame_source.h new file mode 100644 index 00000000..94222af0 --- /dev/null +++ b/app/src/trait/frame_source.h @@ -0,0 +1,38 @@ +#ifndef SC_FRAME_SOURCE_H +#define SC_FRAME_SOURCE_H + +#include "common.h" + +#include "frame_sink.h" + +#define SC_FRAME_SOURCE_MAX_SINKS 2 + +/** + * Frame source trait + * + * Component able to send AVFrames should implement this trait. + */ +struct sc_frame_source { + struct sc_frame_sink *sinks[SC_FRAME_SOURCE_MAX_SINKS]; + unsigned sink_count; +}; + +void +sc_frame_source_init(struct sc_frame_source *source); + +void +sc_frame_source_add_sink(struct sc_frame_source *source, + struct sc_frame_sink *sink); + +bool +sc_frame_source_sinks_open(struct sc_frame_source *source, + const AVCodecContext *ctx); + +void +sc_frame_source_sinks_close(struct sc_frame_source *source); + +bool +sc_frame_source_sinks_push(struct sc_frame_source *source, + const AVFrame *frame); + +#endif From 974227a3fcfa5b60921aeb71d43da4ac84c73d91 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 2 Mar 2023 09:37:36 +0100 Subject: [PATCH 1444/2244] Use frame source trait in decoder --- app/src/decoder.c | 56 +++++------------------------------------------ app/src/decoder.h | 10 ++------- app/src/scrcpy.c | 6 +++-- 3 files changed, 12 insertions(+), 60 deletions(-) diff --git a/app/src/decoder.c b/app/src/decoder.c index e4d59628..2931c1ec 100644 --- a/app/src/decoder.c +++ b/app/src/decoder.c @@ -11,32 +11,6 @@ /** Downcast packet_sink to decoder */ #define DOWNCAST(SINK) container_of(SINK, struct sc_decoder, packet_sink) -static void -sc_decoder_close_first_sinks(struct sc_decoder *decoder, unsigned count) { - while (count) { - struct sc_frame_sink *sink = decoder->sinks[--count]; - sink->ops->close(sink); - } -} - -static inline void -sc_decoder_close_sinks(struct sc_decoder *decoder) { - sc_decoder_close_first_sinks(decoder, decoder->sink_count); -} - -static bool -sc_decoder_open_sinks(struct sc_decoder *decoder, const AVCodecContext *ctx) { - for (unsigned i = 0; i < decoder->sink_count; ++i) { - struct sc_frame_sink *sink = decoder->sinks[i]; - if (!sink->ops->open(sink, ctx)) { - sc_decoder_close_first_sinks(decoder, i); - return false; - } - } - - return true; -} - static bool sc_decoder_open(struct sc_decoder *decoder, const AVCodec *codec) { decoder->codec_ctx = avcodec_alloc_context3(codec); @@ -66,7 +40,8 @@ sc_decoder_open(struct sc_decoder *decoder, const AVCodec *codec) { return false; } - if (!sc_decoder_open_sinks(decoder, decoder->codec_ctx)) { + if (!sc_frame_source_sinks_open(&decoder->frame_source, + decoder->codec_ctx)) { av_frame_free(&decoder->frame); avcodec_close(decoder->codec_ctx); avcodec_free_context(&decoder->codec_ctx); @@ -78,24 +53,12 @@ sc_decoder_open(struct sc_decoder *decoder, const AVCodec *codec) { static void sc_decoder_close(struct sc_decoder *decoder) { - sc_decoder_close_sinks(decoder); + sc_frame_source_sinks_close(&decoder->frame_source); av_frame_free(&decoder->frame); avcodec_close(decoder->codec_ctx); avcodec_free_context(&decoder->codec_ctx); } -static bool -push_frame_to_sinks(struct sc_decoder *decoder, const AVFrame *frame) { - for (unsigned i = 0; i < decoder->sink_count; ++i) { - struct sc_frame_sink *sink = decoder->sinks[i]; - if (!sink->ops->push(sink, frame)) { - return false; - } - } - - return true; -} - static bool sc_decoder_push(struct sc_decoder *decoder, const AVPacket *packet) { bool is_config = packet->pts == AV_NOPTS_VALUE; @@ -124,7 +87,8 @@ sc_decoder_push(struct sc_decoder *decoder, const AVPacket *packet) { } // a frame was received - bool ok = push_frame_to_sinks(decoder, decoder->frame); + bool ok = sc_frame_source_sinks_push(&decoder->frame_source, + decoder->frame); // A frame lost should not make the whole pipeline fail. The error, if // any, is already logged. (void) ok; @@ -157,7 +121,7 @@ sc_decoder_packet_sink_push(struct sc_packet_sink *sink, void sc_decoder_init(struct sc_decoder *decoder, const char *name) { decoder->name = name; // statically allocated - decoder->sink_count = 0; + sc_frame_source_init(&decoder->frame_source); static const struct sc_packet_sink_ops ops = { .open = sc_decoder_packet_sink_open, @@ -167,11 +131,3 @@ sc_decoder_init(struct sc_decoder *decoder, const char *name) { decoder->packet_sink.ops = &ops; } - -void -sc_decoder_add_sink(struct sc_decoder *decoder, struct sc_frame_sink *sink) { - assert(decoder->sink_count < SC_DECODER_MAX_SINKS); - assert(sink); - assert(sink->ops); - decoder->sinks[decoder->sink_count++] = sink; -} diff --git a/app/src/decoder.h b/app/src/decoder.h index aace1af6..87aaf6a2 100644 --- a/app/src/decoder.h +++ b/app/src/decoder.h @@ -3,22 +3,19 @@ #include "common.h" +#include "trait/frame_source.h" #include "trait/packet_sink.h" #include #include #include -#define SC_DECODER_MAX_SINKS 2 - struct sc_decoder { struct sc_packet_sink packet_sink; // packet sink trait + struct sc_frame_source frame_source; // frame source trait const char *name; // must be statically allocated (e.g. a string literal) - struct sc_frame_sink *sinks[SC_DECODER_MAX_SINKS]; - unsigned sink_count; - AVCodecContext *codec_ctx; AVFrame *frame; }; @@ -27,7 +24,4 @@ struct sc_decoder { void sc_decoder_init(struct sc_decoder *decoder, const char *name); -void -sc_decoder_add_sink(struct sc_decoder *decoder, struct sc_frame_sink *sink); - #endif diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index d9625a44..54858c01 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -665,7 +665,8 @@ aoa_hid_end: } screen_initialized = true; - sc_decoder_add_sink(&s->video_decoder, &s->screen.frame_sink); + sc_frame_source_add_sink(&s->video_decoder.frame_source, + &s->screen.frame_sink); } #ifdef HAVE_V4L2 @@ -675,7 +676,8 @@ aoa_hid_end: goto end; } - sc_decoder_add_sink(&s->video_decoder, &s->v4l2_sink.frame_sink); + sc_frame_source_add_sink(&s->video_decoder.frame_source, + &s->v4l2_sink.frame_sink); v4l2_sink_initialized = true; } From 1230149fdd73e9e90230b0f592612edbe078650a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 2 Mar 2023 21:30:24 +0100 Subject: [PATCH 1445/2244] Use delay buffer as a frame source/sink The components needing delayed frames (sc_screen and sc_v4l2_sink) managed a sc_video_buffer instance, which itself embedded a sc_frame_buffer instance (to keep only the most recent frame). In theory, these components should not be aware of delaying: they should just receive AVFrames later, and only handle a sc_frame_buffer. Therefore, refactor sc_delay_buffer as a frame source (it consumes) frames) and a frame sink (it produces frames, after some delay), and plug an instance in the pipeline only when a delay is requested. This also removes the need for a specific sc_video_buffer. PR #3757 --- app/meson.build | 1 - app/src/decoder.c | 1 - app/src/delay_buffer.c | 164 ++++++++++++++++++++--------------------- app/src/delay_buffer.h | 33 +++------ app/src/scrcpy.c | 26 +++++-- app/src/screen.c | 40 +++------- app/src/screen.h | 6 +- app/src/v4l2_sink.c | 61 ++++++--------- app/src/v4l2_sink.h | 7 +- app/src/video_buffer.c | 84 --------------------- app/src/video_buffer.h | 47 ------------ 11 files changed, 150 insertions(+), 320 deletions(-) delete mode 100644 app/src/video_buffer.c delete mode 100644 app/src/video_buffer.h diff --git a/app/meson.build b/app/meson.build index 9f73b434..392fa6d0 100644 --- a/app/meson.build +++ b/app/meson.build @@ -29,7 +29,6 @@ src = [ 'src/screen.c', 'src/server.c', 'src/version.c', - 'src/video_buffer.c', 'src/trait/frame_source.c', 'src/trait/packet_source.c', 'src/util/acksync.c', diff --git a/app/src/decoder.c b/app/src/decoder.c index 2931c1ec..a8168f66 100644 --- a/app/src/decoder.c +++ b/app/src/decoder.c @@ -4,7 +4,6 @@ #include #include "events.h" -#include "video_buffer.h" #include "trait/frame_sink.h" #include "util/log.h" diff --git a/app/src/delay_buffer.c b/app/src/delay_buffer.c index 95d47c9c..72af3672 100644 --- a/app/src/delay_buffer.c +++ b/app/src/delay_buffer.c @@ -10,6 +10,9 @@ #define SC_BUFFERING_NDEBUG // comment to debug +/** Downcast frame_sink to sc_delay_buffer */ +#define DOWNCAST(SINK) container_of(SINK, struct sc_delay_buffer, frame_sink) + static bool sc_delayed_frame_init(struct sc_delayed_frame *dframe, const AVFrame *frame) { dframe->frame = av_frame_alloc(); @@ -33,11 +36,6 @@ sc_delayed_frame_destroy(struct sc_delayed_frame *dframe) { av_frame_free(&dframe->frame); } -static bool -sc_delay_buffer_offer(struct sc_delay_buffer *db, const AVFrame *frame) { - return db->cbs->on_new_frame(db, frame, db->cbs_userdata); -} - static int run_buffering(void *data) { struct sc_delay_buffer *db = data; @@ -87,12 +85,12 @@ run_buffering(void *data) { pts, dframe.push_date, sc_tick_now()); #endif - bool ok = sc_delay_buffer_offer(db, dframe.frame); + bool ok = sc_frame_source_sinks_push(&db->frame_source, dframe.frame); sc_delayed_frame_destroy(&dframe); if (!ok) { LOGE("Delayed frame could not be pushed, stopping"); sc_mutex_lock(&db->b.mutex); - // Prevent to push any new packet + // Prevent to push any new frame db->b.stopped = true; sc_mutex_unlock(&db->b.mutex); goto stopped; @@ -113,92 +111,77 @@ stopped: return 0; } -bool -sc_delay_buffer_init(struct sc_delay_buffer *db, sc_tick delay, - const struct sc_delay_buffer_callbacks *cbs, - void *cbs_userdata) { - assert(delay >= 0); +static bool +sc_delay_buffer_frame_sink_open(struct sc_frame_sink *sink, + const AVCodecContext *ctx) { + struct sc_delay_buffer *db = DOWNCAST(sink); + (void) ctx; - if (delay) { - bool ok = sc_mutex_init(&db->b.mutex); - if (!ok) { - return false; - } - - ok = sc_cond_init(&db->b.queue_cond); - if (!ok) { - sc_mutex_destroy(&db->b.mutex); - return false; - } - - ok = sc_cond_init(&db->b.wait_cond); - if (!ok) { - sc_cond_destroy(&db->b.queue_cond); - sc_mutex_destroy(&db->b.mutex); - return false; - } - - sc_clock_init(&db->b.clock); - sc_vecdeque_init(&db->b.queue); + bool ok = sc_mutex_init(&db->b.mutex); + if (!ok) { + return false; } - assert(cbs); - assert(cbs->on_new_frame); + ok = sc_cond_init(&db->b.queue_cond); + if (!ok) { + goto error_destroy_mutex; + } - db->delay = delay; - db->cbs = cbs; - db->cbs_userdata = cbs_userdata; + ok = sc_cond_init(&db->b.wait_cond); + if (!ok) { + goto error_destroy_queue_cond; + } - return true; -} + sc_clock_init(&db->b.clock); + sc_vecdeque_init(&db->b.queue); -bool -sc_delay_buffer_start(struct sc_delay_buffer *db) { - if (db->delay) { - bool ok = - sc_thread_create(&db->b.thread, run_buffering, "scrcpy-dbuf", db); - if (!ok) { - LOGE("Could not start buffering thread"); - return false; - } + if (!sc_frame_source_sinks_open(&db->frame_source, ctx)) { + goto error_destroy_wait_cond; + } + + ok = sc_thread_create(&db->b.thread, run_buffering, "scrcpy-dbuf", db); + if (!ok) { + LOGE("Could not start buffering thread"); + goto error_close_sinks; } return true; + +error_close_sinks: + sc_frame_source_sinks_close(&db->frame_source); +error_destroy_wait_cond: + sc_cond_destroy(&db->b.wait_cond); +error_destroy_queue_cond: + sc_cond_destroy(&db->b.queue_cond); +error_destroy_mutex: + sc_mutex_destroy(&db->b.mutex); + + return false; } -void -sc_delay_buffer_stop(struct sc_delay_buffer *db) { - if (db->delay) { - sc_mutex_lock(&db->b.mutex); - db->b.stopped = true; - sc_cond_signal(&db->b.queue_cond); - sc_cond_signal(&db->b.wait_cond); - sc_mutex_unlock(&db->b.mutex); - } +static void +sc_delay_buffer_frame_sink_close(struct sc_frame_sink *sink) { + struct sc_delay_buffer *db = DOWNCAST(sink); + + sc_mutex_lock(&db->b.mutex); + db->b.stopped = true; + sc_cond_signal(&db->b.queue_cond); + sc_cond_signal(&db->b.wait_cond); + sc_mutex_unlock(&db->b.mutex); + + sc_thread_join(&db->b.thread, NULL); + + sc_frame_source_sinks_close(&db->frame_source); + + sc_cond_destroy(&db->b.wait_cond); + sc_cond_destroy(&db->b.queue_cond); + sc_mutex_destroy(&db->b.mutex); } -void -sc_delay_buffer_join(struct sc_delay_buffer *db) { - if (db->delay) { - sc_thread_join(&db->b.thread, NULL); - } -} - -void -sc_delay_buffer_destroy(struct sc_delay_buffer *db) { - if (db->delay) { - sc_cond_destroy(&db->b.wait_cond); - sc_cond_destroy(&db->b.queue_cond); - sc_mutex_destroy(&db->b.mutex); - } -} - -bool -sc_delay_buffer_push(struct sc_delay_buffer *db, const AVFrame *frame) { - if (!db->delay) { - // No buffering - return sc_delay_buffer_offer(db, frame); - } +static bool +sc_delay_buffer_frame_sink_push(struct sc_frame_sink *sink, + const AVFrame *frame) { + struct sc_delay_buffer *db = DOWNCAST(sink); sc_mutex_lock(&db->b.mutex); @@ -213,11 +196,11 @@ sc_delay_buffer_push(struct sc_delay_buffer *db, const AVFrame *frame) { if (db->b.clock.count == 1) { sc_mutex_unlock(&db->b.mutex); - // First frame, offer it immediately, for two reasons: + // First frame, push it immediately, for two reasons: // - not to delay the opening of the scrcpy window // - the buffering estimation needs at least two clock points, so it // could not handle the first frame - return sc_delay_buffer_offer(db, frame); + return sc_frame_source_sinks_push(&db->frame_source, frame); } struct sc_delayed_frame dframe; @@ -244,3 +227,20 @@ sc_delay_buffer_push(struct sc_delay_buffer *db, const AVFrame *frame) { return true; } + +void +sc_delay_buffer_init(struct sc_delay_buffer *db, sc_tick delay) { + assert(delay > 0); + + db->delay = delay; + + sc_frame_source_init(&db->frame_source); + + static const struct sc_frame_sink_ops ops = { + .open = sc_delay_buffer_frame_sink_open, + .close = sc_delay_buffer_frame_sink_close, + .push = sc_delay_buffer_frame_sink_push, + }; + + db->frame_sink.ops = &ops; +} diff --git a/app/src/delay_buffer.h b/app/src/delay_buffer.h index 9e5347c7..4cb981c8 100644 --- a/app/src/delay_buffer.h +++ b/app/src/delay_buffer.h @@ -6,6 +6,8 @@ #include #include "clock.h" +#include "trait/frame_source.h" +#include "trait/frame_sink.h" #include "util/thread.h" #include "util/tick.h" #include "util/vecdeque.h" @@ -23,9 +25,11 @@ struct sc_delayed_frame { struct sc_delayed_frame_queue SC_VECDEQUE(struct sc_delayed_frame); struct sc_delay_buffer { + struct sc_frame_source frame_source; // frame source trait + struct sc_frame_sink frame_sink; // frame sink trait + sc_tick delay; - // only if delay > 0 struct { sc_thread thread; sc_mutex mutex; @@ -36,9 +40,6 @@ struct sc_delay_buffer { struct sc_delayed_frame_queue queue; bool stopped; } b; // buffering - - const struct sc_delay_buffer_callbacks *cbs; - void *cbs_userdata; }; struct sc_delay_buffer_callbacks { @@ -46,24 +47,12 @@ struct sc_delay_buffer_callbacks { void *userdata); }; -bool -sc_delay_buffer_init(struct sc_delay_buffer *db, sc_tick delay, - const struct sc_delay_buffer_callbacks *cbs, - void *cbs_userdata); - -bool -sc_delay_buffer_start(struct sc_delay_buffer *db); - +/** + * Initialize a delay buffer. + * + * \param delay a (strictly) positive delay + */ void -sc_delay_buffer_stop(struct sc_delay_buffer *db); - -void -sc_delay_buffer_join(struct sc_delay_buffer *db); - -void -sc_delay_buffer_destroy(struct sc_delay_buffer *db); - -bool -sc_delay_buffer_push(struct sc_delay_buffer *db, const AVFrame *frame); +sc_delay_buffer_init(struct sc_delay_buffer *db, sc_tick delay); #endif diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 54858c01..2688cab6 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -15,6 +15,7 @@ #include "controller.h" #include "decoder.h" +#include "delay_buffer.h" #include "demuxer.h" #include "events.h" #include "file_pusher.h" @@ -45,8 +46,10 @@ struct scrcpy { struct sc_decoder video_decoder; struct sc_decoder audio_decoder; struct sc_recorder recorder; + struct sc_delay_buffer display_buffer; #ifdef HAVE_V4L2 struct sc_v4l2_sink v4l2_sink; + struct sc_delay_buffer v4l2_buffer; #endif struct sc_controller controller; struct sc_file_pusher file_pusher; @@ -657,7 +660,6 @@ aoa_hid_end: .mipmaps = options->mipmaps, .fullscreen = options->fullscreen, .start_fps_counter = options->start_fps_counter, - .buffering_time = options->display_buffer, }; if (!sc_screen_init(&s->screen, &screen_params)) { @@ -665,19 +667,31 @@ aoa_hid_end: } screen_initialized = true; - sc_frame_source_add_sink(&s->video_decoder.frame_source, - &s->screen.frame_sink); + struct sc_frame_source *src = &s->video_decoder.frame_source; + if (options->display_buffer) { + sc_delay_buffer_init(&s->display_buffer, options->display_buffer); + sc_frame_source_add_sink(src, &s->display_buffer.frame_sink); + src = &s->display_buffer.frame_source; + } + + sc_frame_source_add_sink(src, &s->screen.frame_sink); } #ifdef HAVE_V4L2 if (options->v4l2_device) { if (!sc_v4l2_sink_init(&s->v4l2_sink, options->v4l2_device, - info->frame_size, options->v4l2_buffer)) { + info->frame_size)) { goto end; } - sc_frame_source_add_sink(&s->video_decoder.frame_source, - &s->v4l2_sink.frame_sink); + struct sc_frame_source *src = &s->video_decoder.frame_source; + if (options->v4l2_buffer) { + sc_delay_buffer_init(&s->v4l2_buffer, options->v4l2_buffer); + sc_frame_source_add_sink(src, &s->v4l2_buffer.frame_sink); + src = &s->v4l2_buffer.frame_source; + } + + sc_frame_source_add_sink(src, &s->v4l2_sink.frame_sink); v4l2_sink_initialized = true; } diff --git a/app/src/screen.c b/app/src/screen.c index ce2e74bb..b814ada1 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -7,7 +7,6 @@ #include "events.h" #include "icon.h" #include "options.h" -#include "video_buffer.h" #include "util/log.h" #define DISPLAY_MARGINS 96 @@ -359,14 +358,12 @@ sc_screen_frame_sink_close(struct sc_frame_sink *sink) { static bool sc_screen_frame_sink_push(struct sc_frame_sink *sink, const AVFrame *frame) { struct sc_screen *screen = DOWNCAST(sink); - return sc_video_buffer_push(&screen->vb, frame); -} -static bool -sc_video_buffer_on_new_frame(struct sc_video_buffer *vb, bool previous_skipped, - void *userdata) { - (void) vb; - struct sc_screen *screen = userdata; + bool previous_skipped; + bool ok = sc_frame_buffer_push(&screen->fb, frame, &previous_skipped); + if (!ok) { + return false; + } if (previous_skipped) { sc_fps_counter_add_skipped_frame(&screen->fps_counter); @@ -404,23 +401,13 @@ sc_screen_init(struct sc_screen *screen, screen->req.fullscreen = params->fullscreen; screen->req.start_fps_counter = params->start_fps_counter; - static const struct sc_video_buffer_callbacks cbs = { - .on_new_frame = sc_video_buffer_on_new_frame, - }; - - bool ok = sc_video_buffer_init(&screen->vb, params->buffering_time, &cbs, - screen); + bool ok = sc_frame_buffer_init(&screen->fb); if (!ok) { return false; } - ok = sc_video_buffer_start(&screen->vb); - if (!ok) { - goto error_destroy_video_buffer; - } - if (!sc_fps_counter_init(&screen->fps_counter)) { - goto error_stop_and_join_video_buffer; + goto error_destroy_frame_buffer; } screen->frame_size = params->frame_size; @@ -552,11 +539,8 @@ error_destroy_window: SDL_DestroyWindow(screen->window); error_destroy_fps_counter: sc_fps_counter_destroy(&screen->fps_counter); -error_stop_and_join_video_buffer: - sc_video_buffer_stop(&screen->vb); - sc_video_buffer_join(&screen->vb); -error_destroy_video_buffer: - sc_video_buffer_destroy(&screen->vb); +error_destroy_frame_buffer: + sc_frame_buffer_destroy(&screen->fb); return false; } @@ -593,13 +577,11 @@ sc_screen_hide_window(struct sc_screen *screen) { void sc_screen_interrupt(struct sc_screen *screen) { - sc_video_buffer_stop(&screen->vb); sc_fps_counter_interrupt(&screen->fps_counter); } void sc_screen_join(struct sc_screen *screen) { - sc_video_buffer_join(&screen->vb); sc_fps_counter_join(&screen->fps_counter); } @@ -613,7 +595,7 @@ sc_screen_destroy(struct sc_screen *screen) { SDL_DestroyRenderer(screen->renderer); SDL_DestroyWindow(screen->window); sc_fps_counter_destroy(&screen->fps_counter); - sc_video_buffer_destroy(&screen->vb); + sc_frame_buffer_destroy(&screen->fb); } static void @@ -719,7 +701,7 @@ update_texture(struct sc_screen *screen, const AVFrame *frame) { static bool sc_screen_update_frame(struct sc_screen *screen) { av_frame_unref(screen->frame); - sc_video_buffer_consume(&screen->vb, screen->frame); + sc_frame_buffer_consume(&screen->fb, screen->frame); AVFrame *frame = screen->frame; sc_fps_counter_add_rendered_frame(&screen->fps_counter); diff --git a/app/src/screen.h b/app/src/screen.h index 0952c79c..28afea40 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -10,12 +10,12 @@ #include "controller.h" #include "coords.h" #include "fps_counter.h" +#include "frame_buffer.h" #include "input_manager.h" #include "opengl.h" #include "trait/key_processor.h" #include "trait/frame_sink.h" #include "trait/mouse_processor.h" -#include "video_buffer.h" struct sc_screen { struct sc_frame_sink frame_sink; // frame sink trait @@ -25,7 +25,7 @@ struct sc_screen { #endif struct sc_input_manager im; - struct sc_video_buffer vb; + struct sc_frame_buffer fb; struct sc_fps_counter fps_counter; // The initial requested window properties @@ -93,8 +93,6 @@ struct sc_screen_params { bool fullscreen; bool start_fps_counter; - - sc_tick buffering_time; }; // initialize screen, create window, renderer and texture (window is hidden) diff --git a/app/src/v4l2_sink.c b/app/src/v4l2_sink.c index 5dfe37bc..fe11614a 100644 --- a/app/src/v4l2_sink.c +++ b/app/src/v4l2_sink.c @@ -126,7 +126,7 @@ run_v4l2_sink(void *data) { vs->has_frame = false; sc_mutex_unlock(&vs->mutex); - sc_video_buffer_consume(&vs->vb, vs->frame); + sc_frame_buffer_consume(&vs->fb, vs->frame); bool ok = encode_and_write_frame(vs, vs->frame); av_frame_unref(vs->frame); @@ -141,44 +141,19 @@ run_v4l2_sink(void *data) { return 0; } -static bool -sc_video_buffer_on_new_frame(struct sc_video_buffer *vb, bool previous_skipped, - void *userdata) { - (void) vb; - struct sc_v4l2_sink *vs = userdata; - - if (!previous_skipped) { - sc_mutex_lock(&vs->mutex); - vs->has_frame = true; - sc_cond_signal(&vs->cond); - sc_mutex_unlock(&vs->mutex); - } - - return true; -} - static bool sc_v4l2_sink_open(struct sc_v4l2_sink *vs, const AVCodecContext *ctx) { assert(ctx->pix_fmt == AV_PIX_FMT_YUV420P); (void) ctx; - static const struct sc_video_buffer_callbacks cbs = { - .on_new_frame = sc_video_buffer_on_new_frame, - }; - - bool ok = sc_video_buffer_init(&vs->vb, vs->buffering_time, &cbs, vs); + bool ok = sc_frame_buffer_init(&vs->fb); if (!ok) { return false; } - ok = sc_video_buffer_start(&vs->vb); - if (!ok) { - goto error_video_buffer_destroy; - } - ok = sc_mutex_init(&vs->mutex); if (!ok) { - goto error_video_buffer_stop_and_join; + goto error_frame_buffer_destroy; } ok = sc_cond_init(&vs->cond); @@ -303,11 +278,8 @@ error_cond_destroy: sc_cond_destroy(&vs->cond); error_mutex_destroy: sc_mutex_destroy(&vs->mutex); -error_video_buffer_stop_and_join: - sc_video_buffer_stop(&vs->vb); - sc_video_buffer_join(&vs->vb); -error_video_buffer_destroy: - sc_video_buffer_destroy(&vs->vb); +error_frame_buffer_destroy: + sc_frame_buffer_destroy(&vs->fb); return false; } @@ -319,10 +291,7 @@ sc_v4l2_sink_close(struct sc_v4l2_sink *vs) { sc_cond_signal(&vs->cond); sc_mutex_unlock(&vs->mutex); - sc_video_buffer_stop(&vs->vb); - sc_thread_join(&vs->thread, NULL); - sc_video_buffer_join(&vs->vb); av_packet_free(&vs->packet); av_frame_free(&vs->frame); @@ -332,12 +301,25 @@ sc_v4l2_sink_close(struct sc_v4l2_sink *vs) { avformat_free_context(vs->format_ctx); sc_cond_destroy(&vs->cond); sc_mutex_destroy(&vs->mutex); - sc_video_buffer_destroy(&vs->vb); + sc_frame_buffer_destroy(&vs->fb); } static bool sc_v4l2_sink_push(struct sc_v4l2_sink *vs, const AVFrame *frame) { - return sc_video_buffer_push(&vs->vb, frame); + bool previous_skipped; + bool ok = sc_frame_buffer_push(&vs->fb, frame, &previous_skipped); + if (!ok) { + return false; + } + + if (!previous_skipped) { + sc_mutex_lock(&vs->mutex); + vs->has_frame = true; + sc_cond_signal(&vs->cond); + sc_mutex_unlock(&vs->mutex); + } + + return true; } static bool @@ -360,7 +342,7 @@ sc_v4l2_frame_sink_push(struct sc_frame_sink *sink, const AVFrame *frame) { bool sc_v4l2_sink_init(struct sc_v4l2_sink *vs, const char *device_name, - struct sc_size frame_size, sc_tick buffering_time) { + struct sc_size frame_size) { vs->device_name = strdup(device_name); if (!vs->device_name) { LOGE("Could not strdup v4l2 device name"); @@ -368,7 +350,6 @@ sc_v4l2_sink_init(struct sc_v4l2_sink *vs, const char *device_name, } vs->frame_size = frame_size; - vs->buffering_time = buffering_time; static const struct sc_frame_sink_ops ops = { .open = sc_v4l2_frame_sink_open, diff --git a/app/src/v4l2_sink.h b/app/src/v4l2_sink.h index 339a61f2..789e31c3 100644 --- a/app/src/v4l2_sink.h +++ b/app/src/v4l2_sink.h @@ -8,19 +8,18 @@ #include "coords.h" #include "trait/frame_sink.h" -#include "video_buffer.h" +#include "frame_buffer.h" #include "util/tick.h" struct sc_v4l2_sink { struct sc_frame_sink frame_sink; // frame sink trait - struct sc_video_buffer vb; + struct sc_frame_buffer fb; AVFormatContext *format_ctx; AVCodecContext *encoder_ctx; char *device_name; struct sc_size frame_size; - sc_tick buffering_time; sc_thread thread; sc_mutex mutex; @@ -35,7 +34,7 @@ struct sc_v4l2_sink { bool sc_v4l2_sink_init(struct sc_v4l2_sink *vs, const char *device_name, - struct sc_size frame_size, sc_tick buffering_time); + struct sc_size frame_size); void sc_v4l2_sink_destroy(struct sc_v4l2_sink *vs); diff --git a/app/src/video_buffer.c b/app/src/video_buffer.c deleted file mode 100644 index da47a0b5..00000000 --- a/app/src/video_buffer.c +++ /dev/null @@ -1,84 +0,0 @@ -#include "video_buffer.h" - -#include -#include - -#include -#include - -#include "util/log.h" - -static bool -sc_delay_buffer_on_new_frame(struct sc_delay_buffer *db, const AVFrame *frame, - void *userdata) { - (void) db; - - struct sc_video_buffer *vb = userdata; - - bool previous_skipped; - bool ok = sc_frame_buffer_push(&vb->fb, frame, &previous_skipped); - if (!ok) { - return false; - } - - return vb->cbs->on_new_frame(vb, previous_skipped, vb->cbs_userdata); -} - -bool -sc_video_buffer_init(struct sc_video_buffer *vb, sc_tick delay, - const struct sc_video_buffer_callbacks *cbs, - void *cbs_userdata) { - bool ok = sc_frame_buffer_init(&vb->fb); - if (!ok) { - return false; - } - - static const struct sc_delay_buffer_callbacks db_cbs = { - .on_new_frame = sc_delay_buffer_on_new_frame, - }; - - ok = sc_delay_buffer_init(&vb->db, delay, &db_cbs, vb); - if (!ok) { - sc_frame_buffer_destroy(&vb->fb); - return false; - } - - assert(cbs); - assert(cbs->on_new_frame); - - vb->cbs = cbs; - vb->cbs_userdata = cbs_userdata; - - return true; -} - -bool -sc_video_buffer_start(struct sc_video_buffer *vb) { - return sc_delay_buffer_start(&vb->db); -} - -void -sc_video_buffer_stop(struct sc_video_buffer *vb) { - return sc_delay_buffer_stop(&vb->db); -} - -void -sc_video_buffer_join(struct sc_video_buffer *vb) { - return sc_delay_buffer_join(&vb->db); -} - -void -sc_video_buffer_destroy(struct sc_video_buffer *vb) { - sc_frame_buffer_destroy(&vb->fb); - sc_delay_buffer_destroy(&vb->db); -} - -bool -sc_video_buffer_push(struct sc_video_buffer *vb, const AVFrame *frame) { - return sc_delay_buffer_push(&vb->db, frame); -} - -void -sc_video_buffer_consume(struct sc_video_buffer *vb, AVFrame *dst) { - sc_frame_buffer_consume(&vb->fb, dst); -} diff --git a/app/src/video_buffer.h b/app/src/video_buffer.h deleted file mode 100644 index ebca3915..00000000 --- a/app/src/video_buffer.h +++ /dev/null @@ -1,47 +0,0 @@ -#ifndef SC_VIDEO_BUFFER_H -#define SC_VIDEO_BUFFER_H - -#include "common.h" - -#include - -#include "delay_buffer.h" -#include "frame_buffer.h" - -struct sc_video_buffer { - struct sc_delay_buffer db; - struct sc_frame_buffer fb; - - const struct sc_video_buffer_callbacks *cbs; - void *cbs_userdata; -}; - -struct sc_video_buffer_callbacks { - bool (*on_new_frame)(struct sc_video_buffer *vb, bool previous_skipped, - void *userdata); -}; - -bool -sc_video_buffer_init(struct sc_video_buffer *vb, sc_tick delay, - const struct sc_video_buffer_callbacks *cbs, - void *cbs_userdata); - -bool -sc_video_buffer_start(struct sc_video_buffer *vb); - -void -sc_video_buffer_stop(struct sc_video_buffer *vb); - -void -sc_video_buffer_join(struct sc_video_buffer *vb); - -void -sc_video_buffer_destroy(struct sc_video_buffer *vb); - -bool -sc_video_buffer_push(struct sc_video_buffer *vb, const AVFrame *frame); - -void -sc_video_buffer_consume(struct sc_video_buffer *vb, AVFrame *dst); - -#endif From 48a537d45c48bdff45c85197584f5570e9d9dc57 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 2 Mar 2023 21:30:24 +0100 Subject: [PATCH 1446/2244] Remove anonymous struct in delay buffer For clarity, the fields used only when a delay was set were wrapped in an anonymous structure. Now that the delay buffer has been extracted to a separate component, the delay is necessarily set (it may not be 0), so the fields are always used. PR #3757 --- app/src/delay_buffer.c | 94 +++++++++++++++++++++--------------------- app/src/delay_buffer.h | 16 ++++--- 2 files changed, 54 insertions(+), 56 deletions(-) diff --git a/app/src/delay_buffer.c b/app/src/delay_buffer.c index 72af3672..2694eb01 100644 --- a/app/src/delay_buffer.c +++ b/app/src/delay_buffer.c @@ -43,37 +43,37 @@ run_buffering(void *data) { assert(db->delay > 0); for (;;) { - sc_mutex_lock(&db->b.mutex); + sc_mutex_lock(&db->mutex); - while (!db->b.stopped && sc_vecdeque_is_empty(&db->b.queue)) { - sc_cond_wait(&db->b.queue_cond, &db->b.mutex); + while (!db->stopped && sc_vecdeque_is_empty(&db->queue)) { + sc_cond_wait(&db->queue_cond, &db->mutex); } - if (db->b.stopped) { - sc_mutex_unlock(&db->b.mutex); + if (db->stopped) { + sc_mutex_unlock(&db->mutex); goto stopped; } - struct sc_delayed_frame dframe = sc_vecdeque_pop(&db->b.queue); + struct sc_delayed_frame dframe = sc_vecdeque_pop(&db->queue); sc_tick max_deadline = sc_tick_now() + db->delay; // PTS (written by the server) are expressed in microseconds sc_tick pts = SC_TICK_FROM_US(dframe.frame->pts); bool timed_out = false; - while (!db->b.stopped && !timed_out) { - sc_tick deadline = sc_clock_to_system_time(&db->b.clock, pts) + while (!db->stopped && !timed_out) { + sc_tick deadline = sc_clock_to_system_time(&db->clock, pts) + db->delay; if (deadline > max_deadline) { deadline = max_deadline; } timed_out = - !sc_cond_timedwait(&db->b.wait_cond, &db->b.mutex, deadline); + !sc_cond_timedwait(&db->wait_cond, &db->mutex, deadline); } - bool stopped = db->b.stopped; - sc_mutex_unlock(&db->b.mutex); + bool stopped = db->stopped; + sc_mutex_unlock(&db->mutex); if (stopped) { sc_delayed_frame_destroy(&dframe); @@ -89,20 +89,20 @@ run_buffering(void *data) { sc_delayed_frame_destroy(&dframe); if (!ok) { LOGE("Delayed frame could not be pushed, stopping"); - sc_mutex_lock(&db->b.mutex); + sc_mutex_lock(&db->mutex); // Prevent to push any new frame - db->b.stopped = true; - sc_mutex_unlock(&db->b.mutex); + db->stopped = true; + sc_mutex_unlock(&db->mutex); goto stopped; } } stopped: - assert(db->b.stopped); + assert(db->stopped); // Flush queue - while (!sc_vecdeque_is_empty(&db->b.queue)) { - struct sc_delayed_frame *dframe = sc_vecdeque_popref(&db->b.queue); + while (!sc_vecdeque_is_empty(&db->queue)) { + struct sc_delayed_frame *dframe = sc_vecdeque_popref(&db->queue); sc_delayed_frame_destroy(dframe); } @@ -117,29 +117,29 @@ sc_delay_buffer_frame_sink_open(struct sc_frame_sink *sink, struct sc_delay_buffer *db = DOWNCAST(sink); (void) ctx; - bool ok = sc_mutex_init(&db->b.mutex); + bool ok = sc_mutex_init(&db->mutex); if (!ok) { return false; } - ok = sc_cond_init(&db->b.queue_cond); + ok = sc_cond_init(&db->queue_cond); if (!ok) { goto error_destroy_mutex; } - ok = sc_cond_init(&db->b.wait_cond); + ok = sc_cond_init(&db->wait_cond); if (!ok) { goto error_destroy_queue_cond; } - sc_clock_init(&db->b.clock); - sc_vecdeque_init(&db->b.queue); + sc_clock_init(&db->clock); + sc_vecdeque_init(&db->queue); if (!sc_frame_source_sinks_open(&db->frame_source, ctx)) { goto error_destroy_wait_cond; } - ok = sc_thread_create(&db->b.thread, run_buffering, "scrcpy-dbuf", db); + ok = sc_thread_create(&db->thread, run_buffering, "scrcpy-dbuf", db); if (!ok) { LOGE("Could not start buffering thread"); goto error_close_sinks; @@ -150,11 +150,11 @@ sc_delay_buffer_frame_sink_open(struct sc_frame_sink *sink, error_close_sinks: sc_frame_source_sinks_close(&db->frame_source); error_destroy_wait_cond: - sc_cond_destroy(&db->b.wait_cond); + sc_cond_destroy(&db->wait_cond); error_destroy_queue_cond: - sc_cond_destroy(&db->b.queue_cond); + sc_cond_destroy(&db->queue_cond); error_destroy_mutex: - sc_mutex_destroy(&db->b.mutex); + sc_mutex_destroy(&db->mutex); return false; } @@ -163,19 +163,19 @@ static void sc_delay_buffer_frame_sink_close(struct sc_frame_sink *sink) { struct sc_delay_buffer *db = DOWNCAST(sink); - sc_mutex_lock(&db->b.mutex); - db->b.stopped = true; - sc_cond_signal(&db->b.queue_cond); - sc_cond_signal(&db->b.wait_cond); - sc_mutex_unlock(&db->b.mutex); + sc_mutex_lock(&db->mutex); + db->stopped = true; + sc_cond_signal(&db->queue_cond); + sc_cond_signal(&db->wait_cond); + sc_mutex_unlock(&db->mutex); - sc_thread_join(&db->b.thread, NULL); + sc_thread_join(&db->thread, NULL); sc_frame_source_sinks_close(&db->frame_source); - sc_cond_destroy(&db->b.wait_cond); - sc_cond_destroy(&db->b.queue_cond); - sc_mutex_destroy(&db->b.mutex); + sc_cond_destroy(&db->wait_cond); + sc_cond_destroy(&db->queue_cond); + sc_mutex_destroy(&db->mutex); } static bool @@ -183,19 +183,19 @@ sc_delay_buffer_frame_sink_push(struct sc_frame_sink *sink, const AVFrame *frame) { struct sc_delay_buffer *db = DOWNCAST(sink); - sc_mutex_lock(&db->b.mutex); + sc_mutex_lock(&db->mutex); - if (db->b.stopped) { - sc_mutex_unlock(&db->b.mutex); + if (db->stopped) { + sc_mutex_unlock(&db->mutex); return false; } sc_tick pts = SC_TICK_FROM_US(frame->pts); - sc_clock_update(&db->b.clock, sc_tick_now(), pts); - sc_cond_signal(&db->b.wait_cond); + sc_clock_update(&db->clock, sc_tick_now(), pts); + sc_cond_signal(&db->wait_cond); - if (db->b.clock.count == 1) { - sc_mutex_unlock(&db->b.mutex); + if (db->clock.count == 1) { + sc_mutex_unlock(&db->mutex); // First frame, push it immediately, for two reasons: // - not to delay the opening of the scrcpy window // - the buffering estimation needs at least two clock points, so it @@ -206,7 +206,7 @@ sc_delay_buffer_frame_sink_push(struct sc_frame_sink *sink, struct sc_delayed_frame dframe; bool ok = sc_delayed_frame_init(&dframe, frame); if (!ok) { - sc_mutex_unlock(&db->b.mutex); + sc_mutex_unlock(&db->mutex); return false; } @@ -214,16 +214,16 @@ sc_delay_buffer_frame_sink_push(struct sc_frame_sink *sink, dframe.push_date = sc_tick_now(); #endif - ok = sc_vecdeque_push(&db->b.queue, dframe); + ok = sc_vecdeque_push(&db->queue, dframe); if (!ok) { - sc_mutex_unlock(&db->b.mutex); + sc_mutex_unlock(&db->mutex); LOG_OOM(); return false; } - sc_cond_signal(&db->b.queue_cond); + sc_cond_signal(&db->queue_cond); - sc_mutex_unlock(&db->b.mutex); + sc_mutex_unlock(&db->mutex); return true; } diff --git a/app/src/delay_buffer.h b/app/src/delay_buffer.h index 4cb981c8..96fbaa3d 100644 --- a/app/src/delay_buffer.h +++ b/app/src/delay_buffer.h @@ -30,16 +30,14 @@ struct sc_delay_buffer { sc_tick delay; - struct { - sc_thread thread; - sc_mutex mutex; - sc_cond queue_cond; - sc_cond wait_cond; + sc_thread thread; + sc_mutex mutex; + sc_cond queue_cond; + sc_cond wait_cond; - struct sc_clock clock; - struct sc_delayed_frame_queue queue; - bool stopped; - } b; // buffering + struct sc_clock clock; + struct sc_delayed_frame_queue queue; + bool stopped; }; struct sc_delay_buffer_callbacks { From 9b3ca208bfe21ceb303798c2f05e2364aa364880 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 2 Mar 2023 22:13:48 +0100 Subject: [PATCH 1447/2244] Accept clock estimation with a single point If there is only one point, assume the slope is 1. PR #3757 --- app/src/clock.c | 21 +++++++++++++-------- app/src/delay_buffer.c | 6 ++---- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/app/src/clock.c b/app/src/clock.c index bb2430fd..3e1a794d 100644 --- a/app/src/clock.c +++ b/app/src/clock.c @@ -18,7 +18,15 @@ sc_clock_init(struct sc_clock *clock) { static void sc_clock_estimate(struct sc_clock *clock, double *out_slope, sc_tick *out_offset) { - assert(clock->count > 1); // two points are necessary + assert(clock->count); + + if (clock->count == 1) { + // If there is only 1 point, we can't compute a slope. Assume it is 1. + struct sc_clock_point *single_point = &clock->right_sum; + *out_slope = 1; + *out_offset = single_point->system - single_point->stream; + return; + } struct sc_clock_point left_avg = { .system = clock->left_sum.system / (clock->count / 2), @@ -93,19 +101,16 @@ sc_clock_update(struct sc_clock *clock, sc_tick system, sc_tick stream) { clock->head = (clock->head + 1) % SC_CLOCK_RANGE; - if (clock->count > 1) { - // Update estimation - sc_clock_estimate(clock, &clock->slope, &clock->offset); + // Update estimation + sc_clock_estimate(clock, &clock->slope, &clock->offset); #ifndef SC_CLOCK_NDEBUG - LOGD("Clock estimation: %f * pts + %" PRItick, - clock->slope, clock->offset); + LOGD("Clock estimation: %f * pts + %" PRItick, clock->slope, clock->offset); #endif - } } sc_tick sc_clock_to_system_time(struct sc_clock *clock, sc_tick stream) { - assert(clock->count > 1); // sc_clock_update() must have been called + assert(clock->count); // sc_clock_update() must have been called return (sc_tick) (stream * clock->slope) + clock->offset; } diff --git a/app/src/delay_buffer.c b/app/src/delay_buffer.c index 2694eb01..360e2b66 100644 --- a/app/src/delay_buffer.c +++ b/app/src/delay_buffer.c @@ -196,10 +196,8 @@ sc_delay_buffer_frame_sink_push(struct sc_frame_sink *sink, if (db->clock.count == 1) { sc_mutex_unlock(&db->mutex); - // First frame, push it immediately, for two reasons: - // - not to delay the opening of the scrcpy window - // - the buffering estimation needs at least two clock points, so it - // could not handle the first frame + // First frame, push it immediately, not to delay the opening of the + // scrcpy window return sc_frame_source_sinks_push(&db->frame_source, frame); } From e1333f6f3b29fd68239f3991271e63bff316abc0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 2 Mar 2023 22:33:31 +0100 Subject: [PATCH 1448/2244] Optionally do not delay the first frame A delay buffer delayed all the frames except the first one, to open the scrcpy window immediately and get a picture. Make this feature optional, so that the delay buffer might also be used for audio (especially for simulating a high delay for debugging). PR #3757 --- app/src/delay_buffer.c | 8 ++++---- app/src/delay_buffer.h | 6 +++++- app/src/scrcpy.c | 5 +++-- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/app/src/delay_buffer.c b/app/src/delay_buffer.c index 360e2b66..9d4690a2 100644 --- a/app/src/delay_buffer.c +++ b/app/src/delay_buffer.c @@ -194,10 +194,8 @@ sc_delay_buffer_frame_sink_push(struct sc_frame_sink *sink, sc_clock_update(&db->clock, sc_tick_now(), pts); sc_cond_signal(&db->wait_cond); - if (db->clock.count == 1) { + if (db->first_frame_asap && db->clock.count == 1) { sc_mutex_unlock(&db->mutex); - // First frame, push it immediately, not to delay the opening of the - // scrcpy window return sc_frame_source_sinks_push(&db->frame_source, frame); } @@ -227,10 +225,12 @@ sc_delay_buffer_frame_sink_push(struct sc_frame_sink *sink, } void -sc_delay_buffer_init(struct sc_delay_buffer *db, sc_tick delay) { +sc_delay_buffer_init(struct sc_delay_buffer *db, sc_tick delay, + bool first_frame_asap) { assert(delay > 0); db->delay = delay; + db->first_frame_asap = first_frame_asap; sc_frame_source_init(&db->frame_source); diff --git a/app/src/delay_buffer.h b/app/src/delay_buffer.h index 96fbaa3d..53592372 100644 --- a/app/src/delay_buffer.h +++ b/app/src/delay_buffer.h @@ -29,6 +29,7 @@ struct sc_delay_buffer { struct sc_frame_sink frame_sink; // frame sink trait sc_tick delay; + bool first_frame_asap; sc_thread thread; sc_mutex mutex; @@ -49,8 +50,11 @@ struct sc_delay_buffer_callbacks { * Initialize a delay buffer. * * \param delay a (strictly) positive delay + * \param first_frame_asap if true, do not delay the first frame (useful for + a video stream). */ void -sc_delay_buffer_init(struct sc_delay_buffer *db, sc_tick delay); +sc_delay_buffer_init(struct sc_delay_buffer *db, sc_tick delay, + bool first_frame_asap); #endif diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 2688cab6..dba1bad9 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -669,7 +669,8 @@ aoa_hid_end: struct sc_frame_source *src = &s->video_decoder.frame_source; if (options->display_buffer) { - sc_delay_buffer_init(&s->display_buffer, options->display_buffer); + sc_delay_buffer_init(&s->display_buffer, options->display_buffer, + true); sc_frame_source_add_sink(src, &s->display_buffer.frame_sink); src = &s->display_buffer.frame_source; } @@ -686,7 +687,7 @@ aoa_hid_end: struct sc_frame_source *src = &s->video_decoder.frame_source; if (options->v4l2_buffer) { - sc_delay_buffer_init(&s->v4l2_buffer, options->v4l2_buffer); + sc_delay_buffer_init(&s->v4l2_buffer, options->v4l2_buffer, true); sc_frame_source_add_sink(src, &s->v4l2_buffer.frame_sink); src = &s->v4l2_buffer.frame_source; } From fbe0f951e113e6055c4365c2986663cb3ef16d0e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 3 Mar 2023 00:43:20 +0100 Subject: [PATCH 1449/2244] Add audio player Play the decoded audio using SDL. The audio player frame sink receives the audio frames, resample them and write them to a byte buffer (introduced by this commit). On SDL audio callback (from an internal SDL thread), copy samples from this byte buffer to the SDL audio buffer. The byte buffer is protected by the SDL_AudioDeviceLock(), but it has been designed so that the producer and the consumer may write and read in parallel, provided that they don't access the same slices of the ring-buffer buffer. PR #3757 Co-authored-by: Simon Chan <1330321+yume-chan@users.noreply.github.com> --- BUILD.md | 4 +- app/meson.build | 4 + app/src/audio_player.c | 409 +++++++++++++++++++++++++++++++++++++++++ app/src/audio_player.h | 78 ++++++++ app/src/decoder.c | 6 + app/src/scrcpy.c | 21 ++- app/src/util/average.c | 26 +++ app/src/util/average.h | 40 ++++ 8 files changed, 583 insertions(+), 5 deletions(-) create mode 100644 app/src/audio_player.c create mode 100644 app/src/audio_player.h create mode 100644 app/src/util/average.c create mode 100644 app/src/util/average.h diff --git a/BUILD.md b/BUILD.md index 0c708bde..51f8141e 100644 --- a/BUILD.md +++ b/BUILD.md @@ -15,7 +15,7 @@ First, you need to install the required packages: sudo apt install ffmpeg libsdl2-2.0-0 adb wget \ gcc git pkg-config meson ninja-build libsdl2-dev \ libavcodec-dev libavdevice-dev libavformat-dev libavutil-dev \ - libusb-1.0-0 libusb-1.0-0-dev + libswresample-dev libusb-1.0-0 libusb-1.0-0-dev ``` Then clone the repo and execute the installation script @@ -94,7 +94,7 @@ sudo apt install ffmpeg libsdl2-2.0-0 adb libusb-1.0-0 # client build dependencies sudo apt install gcc git pkg-config meson ninja-build libsdl2-dev \ libavcodec-dev libavdevice-dev libavformat-dev libavutil-dev \ - libusb-1.0-0-dev + libswresample-dev libusb-1.0-0-dev # server build dependencies sudo apt install openjdk-11-jdk diff --git a/app/meson.build b/app/meson.build index 392fa6d0..723274c9 100644 --- a/app/meson.build +++ b/app/meson.build @@ -4,6 +4,7 @@ src = [ 'src/adb/adb_device.c', 'src/adb/adb_parser.c', 'src/adb/adb_tunnel.c', + 'src/audio_player.c', 'src/cli.c', 'src/clock.c', 'src/compat.c', @@ -32,6 +33,7 @@ src = [ 'src/trait/frame_source.c', 'src/trait/packet_source.c', 'src/util/acksync.c', + 'src/util/average.c', 'src/util/bytebuf.c', 'src/util/file.c', 'src/util/intmap.c', @@ -103,6 +105,7 @@ if not crossbuild_windows dependency('libavformat', version: '>= 57.33'), dependency('libavcodec', version: '>= 57.37'), dependency('libavutil'), + dependency('libswresample'), dependency('sdl2', version: '>= 2.0.5'), ] @@ -138,6 +141,7 @@ else cc.find_library('avcodec-60', dirs: ffmpeg_bin_dir), cc.find_library('avformat-60', dirs: ffmpeg_bin_dir), cc.find_library('avutil-58', dirs: ffmpeg_bin_dir), + cc.find_library('swresample-4', dirs: ffmpeg_bin_dir), ], include_directories: include_directories(ffmpeg_include_dir) ) diff --git a/app/src/audio_player.c b/app/src/audio_player.c new file mode 100644 index 00000000..78a8ffe1 --- /dev/null +++ b/app/src/audio_player.c @@ -0,0 +1,409 @@ +#include "audio_player.h" + +#include + +#include "util/log.h" + +#define SC_AUDIO_PLAYER_NDEBUG // comment to debug + +/** Downcast frame_sink to sc_audio_player */ +#define DOWNCAST(SINK) container_of(SINK, struct sc_audio_player, frame_sink) + +#define SC_AV_SAMPLE_FMT AV_SAMPLE_FMT_FLT +#define SC_SDL_SAMPLE_FMT AUDIO_F32 + +#define SC_AUDIO_OUTPUT_BUFFER_SAMPLES 240 // 5ms at 48000Hz + +static inline uint32_t +bytes_to_samples(struct sc_audio_player *ap, size_t bytes) { + assert(bytes % (ap->nb_channels * ap->out_bytes_per_sample) == 0); + return bytes / (ap->nb_channels * ap->out_bytes_per_sample); +} + +static inline size_t +samples_to_bytes(struct sc_audio_player *ap, uint32_t samples) { + return samples * ap->nb_channels * ap->out_bytes_per_sample; +} + +static void SDLCALL +sc_audio_player_sdl_callback(void *userdata, uint8_t *stream, int len_int) { + struct sc_audio_player *ap = userdata; + + // This callback is called with the lock used by SDL_AudioDeviceLock(), so + // the bytebuf is protected + + assert(len_int > 0); + size_t len = len_int; + +#ifndef SC_AUDIO_PLAYER_NDEBUG + LOGD("[Audio] SDL callback requests %" PRIu32 " samples", + bytes_to_samples(ap, len)); +#endif + + size_t read_avail = sc_bytebuf_read_available(&ap->buf); + if (!ap->played) { + uint32_t buffered_samples = bytes_to_samples(ap, read_avail); + + // Part of the buffering is handled by inserting initial silence. The + // remaining (margin) last samples will be handled by compensation. + uint32_t margin = 30 * ap->sample_rate / 1000; // 30ms + if (buffered_samples + margin < ap->target_buffering) { + LOGV("[Audio] Inserting initial buffering silence: %" PRIu32 + " samples", bytes_to_samples(ap, len)); + // Delay playback starting to reach the target buffering. Fill the + // whole buffer with silence (len is small compared to the + // arbitrary margin value). + memset(stream, 0, len); + return; + } + } + + size_t read = MIN(read_avail, len); + if (read) { + sc_bytebuf_read(&ap->buf, stream, read); + } + + if (read < len) { + size_t silence_bytes = len - read; + uint32_t silence_samples = bytes_to_samples(ap, silence_bytes); + // Insert silence. In theory, the inserted silent samples replace the + // missing real samples, which will arrive later, so they should be + // dropped to keep the latency minimal. However, this would cause very + // audible glitches, so let the clock compensation restore the target + // latency. + LOGD("[Audio] Buffer underflow, inserting silence: %" PRIu32 " samples", + silence_samples); + memset(stream + read, 0, silence_bytes); + + if (ap->received) { + // Inserting additional samples immediately increases buffering + ap->avg_buffering.avg += silence_samples; + } + } + + ap->played = true; +} + +static uint8_t * +sc_audio_player_get_swr_buf(struct sc_audio_player *ap, uint32_t min_samples) { + size_t min_buf_size = samples_to_bytes(ap, min_samples); + if (min_buf_size > ap->swr_buf_alloc_size) { + size_t new_size = min_buf_size + 4096; + uint8_t *buf = realloc(ap->swr_buf, new_size); + if (!buf) { + LOG_OOM(); + // Could not realloc to the requested size + return NULL; + } + ap->swr_buf = buf; + ap->swr_buf_alloc_size = new_size; + } + + return ap->swr_buf; +} + +static bool +sc_audio_player_frame_sink_push(struct sc_frame_sink *sink, + const AVFrame *frame) { + struct sc_audio_player *ap = DOWNCAST(sink); + + SwrContext *swr_ctx = ap->swr_ctx; + + int64_t swr_delay = swr_get_delay(swr_ctx, ap->sample_rate); + // No need to av_rescale_rnd(), input and output sample rates are the same. + // Add more space (256) for clock compensation. + int dst_nb_samples = swr_delay + frame->nb_samples + 256; + + uint8_t *swr_buf = sc_audio_player_get_swr_buf(ap, dst_nb_samples); + if (!swr_buf) { + return false; + } + + int ret = swr_convert(swr_ctx, &swr_buf, dst_nb_samples, + (const uint8_t **) frame->data, frame->nb_samples); + if (ret < 0) { + LOGE("Resampling failed: %d", ret); + return false; + } + + // swr_convert() returns the number of samples which would have been + // written if the buffer was big enough. + uint32_t samples_written = MIN(ret, dst_nb_samples); + size_t swr_buf_size = samples_to_bytes(ap, samples_written); +#ifndef SC_AUDIO_PLAYER_NDEBUG + LOGD("[Audio] %" PRIu32 " samples written to buffer", samples_written); +#endif + + // Since this function is the only writer, the current available space is + // at least the previous available space. In practice, it should almost + // always be possible to write without lock. + bool lockless_write = swr_buf_size <= ap->previous_write_avail; + if (lockless_write) { + sc_bytebuf_prepare_write(&ap->buf, swr_buf, swr_buf_size); + } + + SDL_LockAudioDevice(ap->device); + + size_t read_avail = sc_bytebuf_read_available(&ap->buf); + uint32_t buffered_samples = bytes_to_samples(ap, read_avail); + + if (lockless_write) { + sc_bytebuf_commit_write(&ap->buf, swr_buf_size); + } else { + // Take care to keep full samples + size_t align = ap->nb_channels * ap->out_bytes_per_sample; + size_t write_avail = + sc_bytebuf_write_available(&ap->buf) / align * align; + if (swr_buf_size > write_avail) { + // Entering this branch is very unlikely, the ring-buffer (bytebuf) + // is allocated with a size sufficient to store 1 second more than + // the target buffering. If this happens, though, we have to skip + // old samples. + size_t cap = sc_bytebuf_capacity(&ap->buf) / align * align; + if (swr_buf_size > cap) { + // Very very unlikely: a single resampled frame should never + // exceed the ring-buffer size (or something is very wrong). + // Ignore the first bytes in swr_buf + swr_buf += swr_buf_size - cap; + swr_buf_size = cap; + // This change in samples_written will impact the + // instant_compensation below + samples_written -= bytes_to_samples(ap, swr_buf_size - cap); + } + + assert(swr_buf_size >= write_avail); + if (swr_buf_size > write_avail) { + sc_bytebuf_skip(&ap->buf, swr_buf_size - write_avail); + uint32_t skip_samples = + bytes_to_samples(ap, swr_buf_size - write_avail); + assert(buffered_samples >= skip_samples); + buffered_samples -= skip_samples; + if (ap->played) { + // Dropping input samples instantly decreases buffering + ap->avg_buffering.avg -= skip_samples; + } + } + + // It should remain exactly the expected size to write the new + // samples. + assert((sc_bytebuf_write_available(&ap->buf) / align * align) + == swr_buf_size); + } + + sc_bytebuf_write(&ap->buf, swr_buf, swr_buf_size); + } + + buffered_samples += samples_written; + assert(samples_to_bytes(ap, buffered_samples) + == sc_bytebuf_read_available(&ap->buf)); + + // Read with lock held, to be used after unlocking + bool played = ap->played; + if (played) { + uint32_t max_buffered_samples = ap->target_buffering + + 12 * SC_AUDIO_OUTPUT_BUFFER_SAMPLES + + ap->target_buffering / 10; + if (buffered_samples > max_buffered_samples) { + uint32_t skip_samples = buffered_samples - max_buffered_samples; + size_t skip_bytes = samples_to_bytes(ap, skip_samples); + sc_bytebuf_skip(&ap->buf, skip_bytes); +#ifndef SC_AUDIO_PLAYER_NDEBUG + LOGD("[Audio] Buffering threshold exceeded, skipping %" PRIu32 + " samples", skip_samples); +#endif + } + + // Number of samples added (or removed, if negative) for compensation + int32_t instant_compensation = + (int32_t) samples_written - frame->nb_samples; + + // The compensation must apply instantly, it must not be smoothed + ap->avg_buffering.avg += instant_compensation; + + // However, the buffering level must be smoothed + sc_average_push(&ap->avg_buffering, buffered_samples); + +#ifndef SC_AUDIO_PLAYER_NDEBUG + LOGD("[Audio] buffered_samples=%" PRIu32 " avg_buffering=%f", + buffered_samples, sc_average_get(&ap->avg_buffering)); +#endif + } else { + // SDL playback not started yet, do not accumulate more than + // max_initial_buffering samples, this would cause unnecessary delay + // (and glitches to compensate) on start. + uint32_t max_initial_buffering = ap->target_buffering + + 2 * SC_AUDIO_OUTPUT_BUFFER_SAMPLES; + if (buffered_samples > max_initial_buffering) { + uint32_t skip_samples = buffered_samples - max_initial_buffering; + size_t skip_bytes = samples_to_bytes(ap, skip_samples); + sc_bytebuf_skip(&ap->buf, skip_bytes); +#ifndef SC_AUDIO_PLAYER_NDEBUG + LOGD("[Audio] Playback not started, skipping %" PRIu32 " samples", + skip_samples); +#endif + } + } + + ap->previous_write_avail = sc_bytebuf_write_available(&ap->buf); + ap->received = true; + + SDL_UnlockAudioDevice(ap->device); + + if (played) { + ap->samples_since_resync += samples_written; + if (ap->samples_since_resync >= ap->sample_rate) { + // Recompute compensation every second + ap->samples_since_resync = 0; + + float avg = sc_average_get(&ap->avg_buffering); + int diff = ap->target_buffering - avg; + if (diff < 0 && buffered_samples < ap->target_buffering) { + // Do not accelerate if the instant buffering level is below + // the average, this would increase underflow + diff = 0; + } + // Compensate the diff over 4 seconds (but will be recomputed after + // 1 second) + int distance = 4 * ap->sample_rate; + // Limit compensation rate to 2% + int abs_max_diff = distance / 50; + diff = CLAMP(diff, -abs_max_diff, abs_max_diff); + LOGV("[Audio] Buffering: target=%" PRIu32 " avg=%f cur=%" PRIu32 + " compensation=%d", ap->target_buffering, avg, + buffered_samples, diff); + int ret = swr_set_compensation(swr_ctx, diff, distance); + if (ret < 0) { + LOGW("Resampling compensation failed: %d", ret); + // not fatal + } + } + } + + return true; +} + +static bool +sc_audio_player_frame_sink_open(struct sc_frame_sink *sink, + const AVCodecContext *ctx) { + struct sc_audio_player *ap = DOWNCAST(sink); + + SDL_AudioSpec desired = { + .freq = ctx->sample_rate, + .format = SC_SDL_SAMPLE_FMT, + .channels = ctx->ch_layout.nb_channels, + .samples = SC_AUDIO_OUTPUT_BUFFER_SAMPLES, + .callback = sc_audio_player_sdl_callback, + .userdata = ap, + }; + SDL_AudioSpec obtained; + + ap->device = SDL_OpenAudioDevice(NULL, 0, &desired, &obtained, 0); + if (!ap->device) { + LOGE("Could not open audio device: %s", SDL_GetError()); + return false; + } + + SwrContext *swr_ctx = swr_alloc(); + if (!swr_ctx) { + LOG_OOM(); + goto error_close_audio_device; + } + ap->swr_ctx = swr_ctx; + + assert(ctx->sample_rate > 0); + assert(ctx->ch_layout.nb_channels > 0); + assert(!av_sample_fmt_is_planar(SC_AV_SAMPLE_FMT)); + int out_bytes_per_sample = av_get_bytes_per_sample(SC_AV_SAMPLE_FMT); + assert(out_bytes_per_sample > 0); + + av_opt_set_chlayout(swr_ctx, "in_chlayout", &ctx->ch_layout, 0); + av_opt_set_chlayout(swr_ctx, "out_chlayout", &ctx->ch_layout, 0); + + av_opt_set_int(swr_ctx, "in_sample_rate", ctx->sample_rate, 0); + av_opt_set_int(swr_ctx, "out_sample_rate", ctx->sample_rate, 0); + + av_opt_set_sample_fmt(swr_ctx, "in_sample_fmt", ctx->sample_fmt, 0); + av_opt_set_sample_fmt(swr_ctx, "out_sample_fmt", SC_AV_SAMPLE_FMT, 0); + + int ret = swr_init(swr_ctx); + if (ret) { + LOGE("Failed to initialize the resampling context"); + goto error_free_swr_ctx; + } + + ap->sample_rate = ctx->sample_rate; + ap->nb_channels = ctx->ch_layout.nb_channels; + ap->out_bytes_per_sample = out_bytes_per_sample; + + ap->target_buffering = ap->target_buffering_delay * ap->sample_rate + / SC_TICK_FREQ; + + // Use a ring-buffer of the target buffering size plus 1 second between the + // producer and the consumer. It's too big on purpose, to guarantee that + // the producer and the consumer will be able to access it in parallel + // without locking. + size_t bytebuf_samples = ap->target_buffering + ap->sample_rate; + size_t bytebuf_size = samples_to_bytes(ap, bytebuf_samples); + + bool ok = sc_bytebuf_init(&ap->buf, bytebuf_size); + if (!ok) { + goto error_free_swr_ctx; + } + + size_t initial_swr_buf_size = samples_to_bytes(ap, 4096); + ap->swr_buf = malloc(initial_swr_buf_size); + if (!ap->swr_buf) { + LOG_OOM(); + goto error_destroy_bytebuf; + } + ap->swr_buf_alloc_size = initial_swr_buf_size; + + ap->previous_write_avail = sc_bytebuf_write_available(&ap->buf); + + // Samples are produced and consumed by blocks, so the buffering must be + // smoothed to get a relatively stable value. + sc_average_init(&ap->avg_buffering, 32); + ap->samples_since_resync = 0; + + ap->received = false; + ap->played = false; + + SDL_PauseAudioDevice(ap->device, 0); + + return true; + +error_destroy_bytebuf: + sc_bytebuf_destroy(&ap->buf); +error_free_swr_ctx: + swr_free(&ap->swr_ctx); +error_close_audio_device: + SDL_CloseAudioDevice(ap->device); + + return false; +} + +static void +sc_audio_player_frame_sink_close(struct sc_frame_sink *sink) { + struct sc_audio_player *ap = DOWNCAST(sink); + + assert(ap->device); + SDL_PauseAudioDevice(ap->device, 1); + SDL_CloseAudioDevice(ap->device); + + free(ap->swr_buf); + sc_bytebuf_destroy(&ap->buf); + swr_free(&ap->swr_ctx); +} + +void +sc_audio_player_init(struct sc_audio_player *ap, sc_tick target_buffering) { + ap->target_buffering_delay = target_buffering; + + static const struct sc_frame_sink_ops ops = { + .open = sc_audio_player_frame_sink_open, + .close = sc_audio_player_frame_sink_close, + .push = sc_audio_player_frame_sink_push, + }; + + ap->frame_sink.ops = &ops; +} diff --git a/app/src/audio_player.h b/app/src/audio_player.h new file mode 100644 index 00000000..c64760ec --- /dev/null +++ b/app/src/audio_player.h @@ -0,0 +1,78 @@ +#ifndef SC_AUDIO_PLAYER_H +#define SC_AUDIO_PLAYER_H + +#include "common.h" + +#include +#include "trait/frame_sink.h" +#include +#include +#include +#include + +#include +#include +#include + +struct sc_audio_player { + struct sc_frame_sink frame_sink; + + SDL_AudioDeviceID device; + + // The target buffering between the producer and the consumer. This value + // is directly use for compensation. + // Since audio capture and/or encoding on the device typically produce + // blocks of 960 samples (20ms) or 1024 samples (~21.3ms), this target + // value should be higher. + sc_tick target_buffering_delay; + uint32_t target_buffering; // in samples + + // Audio buffer to communicate between the receiver and the SDL audio + // callback (protected by SDL_AudioDeviceLock()) + struct sc_bytebuf buf; + + // The previous number of bytes available in the buffer (only used by the + // receiver thread) + size_t previous_write_avail; + + // Resampler (only used from the receiver thread) + struct SwrContext *swr_ctx; + + // The sample rate is the same for input and output + unsigned sample_rate; + // The number of channels is the same for input and output + unsigned nb_channels; + // The number of bytes per sample for a single channel + unsigned out_bytes_per_sample; + + // Target buffer for resampling (only used by the receiver thread) + uint8_t *swr_buf; + size_t swr_buf_alloc_size; + + // Number of buffered samples (may be negative on underflow) (only used by + // the receiver thread) + struct sc_average avg_buffering; + // Count the number of samples to trigger a compensation update regularly + // (only used by the receiver thread) + uint32_t samples_since_resync; + + // Set to true the first time a sample is received (protected by + // SDL_AudioDeviceLock()) + bool received; + + // Set to true the first time the SDL callback is called (protected by + // SDL_AudioDeviceLock()) + bool played; + + const struct sc_audio_player_callbacks *cbs; + void *cbs_userdata; +}; + +struct sc_audio_player_callbacks { + void (*on_ended)(struct sc_audio_player *ap, bool success, void *userdata); +}; + +void +sc_audio_player_init(struct sc_audio_player *ap, sc_tick target_buffering); + +#endif diff --git a/app/src/decoder.c b/app/src/decoder.c index a8168f66..4384186d 100644 --- a/app/src/decoder.c +++ b/app/src/decoder.c @@ -2,6 +2,7 @@ #include #include +#include #include "events.h" #include "trait/frame_sink.h" @@ -23,6 +24,11 @@ sc_decoder_open(struct sc_decoder *decoder, const AVCodec *codec) { if (codec->type == AVMEDIA_TYPE_VIDEO) { // Hardcoded video properties decoder->codec_ctx->pix_fmt = AV_PIX_FMT_YUV420P; + } else { + // Hardcoded audio properties + decoder->codec_ctx->ch_layout = + (AVChannelLayout) AV_CHANNEL_LAYOUT_STEREO; + decoder->codec_ctx->sample_rate = 48000; } if (avcodec_open2(decoder->codec_ctx, codec, NULL) < 0) { diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index dba1bad9..3f3a34f0 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -13,6 +13,7 @@ # include #endif +#include "audio_player.h" #include "controller.h" #include "decoder.h" #include "delay_buffer.h" @@ -41,6 +42,7 @@ struct scrcpy { struct sc_server server; struct sc_screen screen; + struct sc_audio_player audio_player; struct sc_demuxer video_demuxer; struct sc_demuxer audio_demuxer; struct sc_decoder video_decoder; @@ -386,9 +388,16 @@ scrcpy(struct scrcpy_options *options) { } // Initialize SDL video in addition if display is enabled - if (options->display && SDL_Init(SDL_INIT_VIDEO)) { - LOGE("Could not initialize SDL: %s", SDL_GetError()); - goto end; + if (options->display) { + if (SDL_Init(SDL_INIT_VIDEO)) { + LOGE("Could not initialize SDL video: %s", SDL_GetError()); + goto end; + } + + if (options->audio && SDL_Init(SDL_INIT_AUDIO)) { + LOGE("Could not initialize SDL audio: %s", SDL_GetError()); + goto end; + } } sdl_configure(options->display, options->disable_screensaver); @@ -676,6 +685,12 @@ aoa_hid_end: } sc_frame_source_add_sink(src, &s->screen.frame_sink); + + if (options->audio) { + sc_audio_player_init(&s->audio_player, SC_TICK_FROM_MS(50)); + sc_frame_source_add_sink(&s->audio_decoder.frame_source, + &s->audio_player.frame_sink); + } } #ifdef HAVE_V4L2 diff --git a/app/src/util/average.c b/app/src/util/average.c new file mode 100644 index 00000000..ace23d45 --- /dev/null +++ b/app/src/util/average.c @@ -0,0 +1,26 @@ +#include "average.h" + +#include + +void +sc_average_init(struct sc_average *avg, unsigned range) { + avg->range = range; + avg->avg = 0; + avg->count = 0; +} + +void +sc_average_push(struct sc_average *avg, float value) { + if (avg->count < avg->range) { + ++avg->count; + } + + assert(avg->count); + avg->avg = ((avg->count - 1) * avg->avg + value) / avg->count; +} + +float +sc_average_get(struct sc_average *avg) { + assert(avg->count); + return avg->avg; +} diff --git a/app/src/util/average.h b/app/src/util/average.h new file mode 100644 index 00000000..59fae7d1 --- /dev/null +++ b/app/src/util/average.h @@ -0,0 +1,40 @@ +#ifndef SC_AVERAGE +#define SC_AVERAGE + +#include "common.h" + +#include +#include + +struct sc_average { + // Current average value + float avg; + + // Target range, to update the average as follow: + // avg = ((range - 1) * avg + new_value) / range + unsigned range; + + // Number of values pushed when less than range (count <= range). + // The purpose is to handle the first (range - 1) values properly. + unsigned count; +}; + +void +sc_average_init(struct sc_average *avg, unsigned range); + +/** + * Push a new value to update the "rolling" average + */ +void +sc_average_push(struct sc_average *avg, float value); + +/** + * Get the current average value + * + * It is an error to call this function if sc_average_push() has not been + * called at least once. + */ +float +sc_average_get(struct sc_average *avg); + +#endif From d66b0b3dccc118600b83988173e6631ad661df51 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 27 Feb 2023 21:41:27 +0100 Subject: [PATCH 1450/2244] Add compat support for FFmpeg < 5.1 The new chlayout API has been introduced in FFmpeg 5.1. Use the old channel_layout API on older versions. PR #3757 --- app/src/audio_player.c | 20 +++++++++++++++++--- app/src/compat.h | 7 +++++++ app/src/decoder.c | 5 +++++ app/src/recorder.c | 5 +++++ 4 files changed, 34 insertions(+), 3 deletions(-) diff --git a/app/src/audio_player.c b/app/src/audio_player.c index 78a8ffe1..de218f1e 100644 --- a/app/src/audio_player.c +++ b/app/src/audio_player.c @@ -286,11 +286,19 @@ static bool sc_audio_player_frame_sink_open(struct sc_frame_sink *sink, const AVCodecContext *ctx) { struct sc_audio_player *ap = DOWNCAST(sink); +#ifdef SCRCPY_LAVU_HAS_CHLAYOUT + assert(ctx->ch_layout.nb_channels > 0); + unsigned nb_channels = ctx->ch_layout.nb_channels; +#else + int tmp = av_get_channel_layout_nb_channels(ctx->channel_layout); + assert(tmp > 0); + unsigned nb_channels = tmp; +#endif SDL_AudioSpec desired = { .freq = ctx->sample_rate, .format = SC_SDL_SAMPLE_FMT, - .channels = ctx->ch_layout.nb_channels, + .channels = nb_channels, .samples = SC_AUDIO_OUTPUT_BUFFER_SAMPLES, .callback = sc_audio_player_sdl_callback, .userdata = ap, @@ -311,13 +319,19 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink, ap->swr_ctx = swr_ctx; assert(ctx->sample_rate > 0); - assert(ctx->ch_layout.nb_channels > 0); assert(!av_sample_fmt_is_planar(SC_AV_SAMPLE_FMT)); int out_bytes_per_sample = av_get_bytes_per_sample(SC_AV_SAMPLE_FMT); assert(out_bytes_per_sample > 0); +#ifdef SCRCPY_LAVU_HAS_CHLAYOUT av_opt_set_chlayout(swr_ctx, "in_chlayout", &ctx->ch_layout, 0); av_opt_set_chlayout(swr_ctx, "out_chlayout", &ctx->ch_layout, 0); +#else + av_opt_set_channel_layout(swr_ctx, "in_channel_layout", + ctx->channel_layout, 0); + av_opt_set_channel_layout(swr_ctx, "out_channel_layout", + ctx->channel_layout, 0); +#endif av_opt_set_int(swr_ctx, "in_sample_rate", ctx->sample_rate, 0); av_opt_set_int(swr_ctx, "out_sample_rate", ctx->sample_rate, 0); @@ -332,7 +346,7 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink, } ap->sample_rate = ctx->sample_rate; - ap->nb_channels = ctx->ch_layout.nb_channels; + ap->nb_channels = nb_channels; ap->out_bytes_per_sample = out_bytes_per_sample; ap->target_buffering = ap->target_buffering_delay * ap->sample_rate diff --git a/app/src/compat.h b/app/src/compat.h index ea44437d..22563421 100644 --- a/app/src/compat.h +++ b/app/src/compat.h @@ -37,6 +37,13 @@ # define SCRCPY_LAVF_HAS_AVFORMATCONTEXT_URL #endif +// Not documented in ffmpeg/doc/APIchanges, but the channel_layout API +// has been replaced by chlayout in FFmpeg commit +// f423497b455da06c1337846902c770028760e094. +#if LIBAVUTIL_VERSION_INT >= AV_VERSION_INT(57, 23, 100) +# define SCRCPY_LAVU_HAS_CHLAYOUT +#endif + #if SDL_VERSION_ATLEAST(2, 0, 6) // # define SCRCPY_SDL_HAS_HINT_TOUCH_MOUSE_EVENTS diff --git a/app/src/decoder.c b/app/src/decoder.c index 4384186d..e87cfd6b 100644 --- a/app/src/decoder.c +++ b/app/src/decoder.c @@ -26,8 +26,13 @@ sc_decoder_open(struct sc_decoder *decoder, const AVCodec *codec) { decoder->codec_ctx->pix_fmt = AV_PIX_FMT_YUV420P; } else { // Hardcoded audio properties +#ifdef SCRCPY_LAVU_HAS_CHLAYOUT decoder->codec_ctx->ch_layout = (AVChannelLayout) AV_CHANNEL_LAYOUT_STEREO; +#else + decoder->codec_ctx->channel_layout = AV_CH_LAYOUT_STEREO; + decoder->codec_ctx->channels = 2; +#endif decoder->codec_ctx->sample_rate = 48000; } diff --git a/app/src/recorder.c b/app/src/recorder.c index bd7c50f2..af5fe510 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -202,7 +202,12 @@ sc_recorder_wait_audio_stream(struct sc_recorder *recorder) { stream->codecpar->codec_type = AVMEDIA_TYPE_AUDIO; stream->codecpar->codec_id = codec->id; +#ifdef SCRCPY_LAVU_HAS_CHLAYOUT stream->codecpar->ch_layout.nb_channels = 2; +#else + stream->codecpar->channel_layout = AV_CH_LAYOUT_STEREO; + stream->codecpar->channels = 2; +#endif stream->codecpar->sample_rate = 48000; recorder->audio_stream_index = stream->index; From df55bc2683e2ca2aec105d6e435cb0b170037ed7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 2 Mar 2023 23:14:01 +0100 Subject: [PATCH 1451/2244] Add --audio-buffer Expose an option to add a buffering delay (in milliseconds) before playing audio. This is similar to the options --display-buffer and --v4l2-buffer for video frames. PR #3757 --- app/scrcpy.1 | 8 ++++++++ app/src/cli.c | 15 +++++++++++++++ app/src/options.c | 1 + app/src/options.h | 1 + app/src/scrcpy.c | 2 +- 5 files changed, 26 insertions(+), 1 deletion(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 91258414..120ea192 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -25,6 +25,14 @@ Encode the audio at the given bit\-rate, expressed in bits/s. Unit suffixes are Default is 128K (128000). +.TP +.BI "\-\-audio\-buffer ms +Configure the audio buffering delay (in milliseconds). + +Lower values decrease the latency, but increase the likelyhood of buffer underrun (causing audio glitches). + +Default is 50. + .TP .BI "\-\-audio\-codec " name Select an audio codec (opus or aac). diff --git a/app/src/cli.c b/app/src/cli.c index 18f3b83b..122a5891 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -71,6 +71,7 @@ enum { OPT_LIST_ENCODERS, OPT_LIST_DISPLAYS, OPT_REQUIRE_AUDIO, + OPT_AUDIO_BUFFER, }; struct sc_option { @@ -120,6 +121,15 @@ static const struct sc_option options[] = { "Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n" "Default is 128K (128000).", }, + { + .longopt_id = OPT_AUDIO_BUFFER, + .longopt = "audio-buffer", + .argdesc = "ms", + .text = "Configure the audio buffering delay (in milliseconds).\n" + "Lower values decrease the latency, but increase the " + "likelyhood of buffer underrun (causing audio glitches).\n" + "Default is 50.", + }, { .longopt_id = OPT_AUDIO_CODEC, .longopt = "audio-codec", @@ -1822,6 +1832,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], case OPT_REQUIRE_AUDIO: opts->require_audio = true; break; + case OPT_AUDIO_BUFFER: + if (!parse_buffering_time(optarg, &opts->audio_buffer)) { + return false; + } + break; default: // getopt prints the error message on stderr return false; diff --git a/app/src/options.c b/app/src/options.c index 5dd655ce..68c16d53 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -43,6 +43,7 @@ const struct scrcpy_options scrcpy_options_default = { .display_id = 0, .display_buffer = 0, .v4l2_buffer = 0, + .audio_buffer = SC_TICK_FROM_MS(50), #ifdef HAVE_USB .otg = false, #endif diff --git a/app/src/options.h b/app/src/options.h index 5fcaf016..d9c2d228 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -125,6 +125,7 @@ struct scrcpy_options { uint32_t display_id; sc_tick display_buffer; sc_tick v4l2_buffer; + sc_tick audio_buffer; #ifdef HAVE_USB bool otg; #endif diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 3f3a34f0..ce045c97 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -687,7 +687,7 @@ aoa_hid_end: sc_frame_source_add_sink(src, &s->screen.frame_sink); if (options->audio) { - sc_audio_player_init(&s->audio_player, SC_TICK_FROM_MS(50)); + sc_audio_player_init(&s->audio_player, options->audio_buffer); sc_frame_source_add_sink(&s->audio_decoder.frame_source, &s->audio_player.frame_sink); } From 02dd1be4a1ab24795a10ca769e2998dec28806f7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 3 Mar 2023 00:42:51 +0100 Subject: [PATCH 1452/2244] Stop on decoder frame push error On push, frame sinks report downstream errors to stop upstream components. Do not ignore the error. --- app/src/decoder.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/decoder.c b/app/src/decoder.c index e87cfd6b..ecad8373 100644 --- a/app/src/decoder.c +++ b/app/src/decoder.c @@ -99,11 +99,11 @@ sc_decoder_push(struct sc_decoder *decoder, const AVPacket *packet) { // a frame was received bool ok = sc_frame_source_sinks_push(&decoder->frame_source, decoder->frame); - // A frame lost should not make the whole pipeline fail. The error, if - // any, is already logged. - (void) ok; - av_frame_unref(decoder->frame); + if (!ok) { + // Error already logged + return false; + } } return true; From 65cc9d765d8d0feb26a41c2287b3abe13dc37d12 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 3 Mar 2023 18:46:59 +0100 Subject: [PATCH 1453/2244] Extract audio capture The audio capture was implemented in AudioEncoder. In order to reuse it without encoding, extract it to a separate class. PR #3757 --- .../com/genymobile/scrcpy/AudioCapture.java | 148 ++++++++++++++++++ .../com/genymobile/scrcpy/AudioEncoder.java | 136 ++-------------- 2 files changed, 161 insertions(+), 123 deletions(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/AudioCapture.java diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java b/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java new file mode 100644 index 00000000..3cef7801 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java @@ -0,0 +1,148 @@ +package com.genymobile.scrcpy; + +import com.genymobile.scrcpy.wrappers.ServiceManager; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.content.ComponentName; +import android.content.Intent; +import android.media.AudioFormat; +import android.media.AudioRecord; +import android.media.AudioTimestamp; +import android.media.MediaCodec; +import android.media.MediaRecorder; +import android.os.Build; +import android.os.SystemClock; + +import java.io.IOException; +import java.nio.ByteBuffer; + +public final class AudioCapture { + + public static final int SAMPLE_RATE = 48000; + public static final int CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_STEREO; + public static final int CHANNELS = 2; + public static final int FORMAT = AudioFormat.ENCODING_PCM_16BIT; + public static final int BYTES_PER_SAMPLE = 2; + + private AudioRecord recorder; + + private final AudioTimestamp timestamp = new AudioTimestamp(); + private long previousPts = 0; + private long nextPts = 0; + + public static int millisToBytes(int millis) { + return SAMPLE_RATE * CHANNELS * BYTES_PER_SAMPLE * millis / 1000; + } + + private static AudioFormat createAudioFormat() { + AudioFormat.Builder builder = new AudioFormat.Builder(); + builder.setEncoding(FORMAT); + builder.setSampleRate(SAMPLE_RATE); + builder.setChannelMask(CHANNEL_CONFIG); + return builder.build(); + } + + @TargetApi(Build.VERSION_CODES.M) + @SuppressLint({"WrongConstant", "MissingPermission"}) + private static AudioRecord createAudioRecord() { + AudioRecord.Builder builder = new AudioRecord.Builder(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + // On older APIs, Workarounds.fillAppInfo() must be called beforehand + builder.setContext(FakeContext.get()); + } + builder.setAudioSource(MediaRecorder.AudioSource.REMOTE_SUBMIX); + builder.setAudioFormat(createAudioFormat()); + int minBufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, FORMAT); + // This buffer size does not impact latency + builder.setBufferSizeInBytes(8 * minBufferSize); + return builder.build(); + } + + private static void startWorkaroundAndroid11() { + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { + // Android 11 requires Apps to be at foreground to record audio. + // Normally, each App has its own user ID, so Android checks whether the requesting App has the user ID that's at the foreground. + // But scrcpy server is NOT an App, it's a Java application started from Android shell, so it has the same user ID (2000) with Android + // shell ("com.android.shell"). + // If there is an Activity from Android shell running at foreground, then the permission system will believe scrcpy is also in the + // foreground. + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { + Intent intent = new Intent(Intent.ACTION_MAIN); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.addCategory(Intent.CATEGORY_LAUNCHER); + intent.setComponent(new ComponentName(FakeContext.PACKAGE_NAME, "com.android.shell.HeapDumpActivity")); + ServiceManager.getActivityManager().startActivityAsUserWithFeature(intent); + // Wait for activity to start + SystemClock.sleep(150); + } + } + } + + private static void stopWorkaroundAndroid11() { + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { + ServiceManager.getActivityManager().forceStopPackage(FakeContext.PACKAGE_NAME); + } + } + + public void start() throws AudioCaptureForegroundException { + startWorkaroundAndroid11(); + try { + recorder = createAudioRecord(); + recorder.startRecording(); + } catch (UnsupportedOperationException e) { + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { + Ln.e("Failed to start audio capture"); + Ln.e("On Android 11, it is only possible to capture in foreground, make sure that the device is unlocked when starting scrcpy."); + throw new AudioCaptureForegroundException(); + } + throw e; + } finally { + stopWorkaroundAndroid11(); + } + } + + public void stop() { + if (recorder != null) { + // Will call .stop() if necessary, without throwing an IllegalStateException + recorder.release(); + } + } + + @TargetApi(Build.VERSION_CODES.N) + public int read(ByteBuffer directBuffer, int size, MediaCodec.BufferInfo outBufferInfo) throws IOException { + int r = recorder.read(directBuffer, size); + if (r < 0) { + return r; + } + + long pts; + + int ret = recorder.getTimestamp(timestamp, AudioTimestamp.TIMEBASE_MONOTONIC); + if (ret == AudioRecord.SUCCESS) { + pts = timestamp.nanoTime / 1000; + } else { + if (nextPts == 0) { + Ln.w("Could not get any audio timestamp"); + } + // compute from previous timestamp and packet size + pts = nextPts; + } + + long durationUs = r * 1000000 / (CHANNELS * BYTES_PER_SAMPLE * SAMPLE_RATE); + nextPts = pts + durationUs; + + if (previousPts != 0 && pts < previousPts) { + // Audio PTS may come from two sources: + // - recorder.getTimestamp() if the call works; + // - an estimation from the previous PTS and the packet size as a fallback. + // + // Therefore, the property that PTS are monotonically increasing is no guaranteed in corner cases, so enforce it. + pts = previousPts + 1; + } + previousPts = pts; + + outBufferInfo.set(0, r, pts, 0); + return r; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java b/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java index cc786bdb..8b60d37e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java @@ -1,22 +1,12 @@ package com.genymobile.scrcpy; -import com.genymobile.scrcpy.wrappers.ServiceManager; - -import android.annotation.SuppressLint; import android.annotation.TargetApi; -import android.content.ComponentName; -import android.content.Intent; -import android.media.AudioFormat; -import android.media.AudioRecord; -import android.media.AudioTimestamp; import android.media.MediaCodec; import android.media.MediaFormat; -import android.media.MediaRecorder; import android.os.Build; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; -import android.os.SystemClock; import java.io.IOException; import java.nio.ByteBuffer; @@ -44,14 +34,11 @@ public final class AudioEncoder { } } - private static final int SAMPLE_RATE = 48000; - private static final int CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_STEREO; - private static final int CHANNELS = 2; - private static final int FORMAT = AudioFormat.ENCODING_PCM_16BIT; - private static final int BYTES_PER_SAMPLE = 2; + private static final int SAMPLE_RATE = AudioCapture.SAMPLE_RATE; + private static final int CHANNELS = AudioCapture.CHANNELS; private static final int READ_MS = 5; // milliseconds - private static final int READ_SIZE = SAMPLE_RATE * CHANNELS * BYTES_PER_SAMPLE * READ_MS / 1000; + private static final int READ_SIZE = AudioCapture.millisToBytes(READ_MS); private final Streamer streamer; private final int bitRate; @@ -78,30 +65,6 @@ public final class AudioEncoder { this.encoderName = encoderName; } - private static AudioFormat createAudioFormat() { - AudioFormat.Builder builder = new AudioFormat.Builder(); - builder.setEncoding(FORMAT); - builder.setSampleRate(SAMPLE_RATE); - builder.setChannelMask(CHANNEL_CONFIG); - return builder.build(); - } - - @TargetApi(Build.VERSION_CODES.M) - @SuppressLint({"WrongConstant", "MissingPermission"}) - private static AudioRecord createAudioRecord() { - AudioRecord.Builder builder = new AudioRecord.Builder(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - // On older APIs, Workarounds.fillAppInfo() must be called beforehand - builder.setContext(FakeContext.get()); - } - builder.setAudioSource(MediaRecorder.AudioSource.REMOTE_SUBMIX); - builder.setAudioFormat(createAudioFormat()); - int minBufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, FORMAT); - // This buffer size does not impact latency - builder.setBufferSizeInBytes(8 * minBufferSize); - return builder.build(); - } - private static MediaFormat createFormat(String mimeType, int bitRate, List codecOptions) { MediaFormat format = new MediaFormat(); format.setString(MediaFormat.KEY_MIME, mimeType); @@ -122,47 +85,18 @@ public final class AudioEncoder { } @TargetApi(Build.VERSION_CODES.N) - private void inputThread(MediaCodec mediaCodec, AudioRecord recorder) throws IOException, InterruptedException { - final AudioTimestamp timestamp = new AudioTimestamp(); - long previousPts = 0; - long nextPts = 0; + private void inputThread(MediaCodec mediaCodec, AudioCapture capture) throws IOException, InterruptedException { + final MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); while (!Thread.currentThread().isInterrupted()) { InputTask task = inputTasks.take(); ByteBuffer buffer = mediaCodec.getInputBuffer(task.index); - int r = recorder.read(buffer, READ_SIZE); + int r = capture.read(buffer, READ_SIZE, bufferInfo); if (r < 0) { throw new IOException("Could not read audio: " + r); } - long pts; - - int ret = recorder.getTimestamp(timestamp, AudioTimestamp.TIMEBASE_MONOTONIC); - if (ret == AudioRecord.SUCCESS) { - pts = timestamp.nanoTime / 1000; - } else { - if (nextPts == 0) { - Ln.w("Could not get any audio timestamp"); - } - // compute from previous timestamp and packet size - pts = nextPts; - } - - long durationUs = r * 1000000 / (CHANNELS * BYTES_PER_SAMPLE * SAMPLE_RATE); - nextPts = pts + durationUs; - - if (previousPts != 0 && pts < previousPts) { - // Audio PTS may come from two sources: - // - recorder.getTimestamp() if the call works; - // - an estimation from the previous PTS and the packet size as a fallback. - // - // Therefore, the property that PTS are monotonically increasing is no guaranteed in corner cases, so enforce it. - pts = previousPts + 1; - } - - previousPts = pts; - - mediaCodec.queueInputBuffer(task.index, 0, r, pts, 0); + mediaCodec.queueInputBuffer(task.index, bufferInfo.offset, bufferInfo.size, bufferInfo.presentationTimeUs, bufferInfo.flags); } } @@ -223,32 +157,6 @@ public final class AudioEncoder { } } - private static void startWorkaroundAndroid11() { - if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { - // Android 11 requires Apps to be at foreground to record audio. - // Normally, each App has its own user ID, so Android checks whether the requesting App has the user ID that's at the foreground. - // But scrcpy server is NOT an App, it's a Java application started from Android shell, so it has the same user ID (2000) with Android - // shell ("com.android.shell"). - // If there is an Activity from Android shell running at foreground, then the permission system will believe scrcpy is also in the - // foreground. - if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { - Intent intent = new Intent(Intent.ACTION_MAIN); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.addCategory(Intent.CATEGORY_LAUNCHER); - intent.setComponent(new ComponentName(FakeContext.PACKAGE_NAME, "com.android.shell.HeapDumpActivity")); - ServiceManager.getActivityManager().startActivityAsUserWithFeature(intent); - // Wait for activity to start - SystemClock.sleep(150); - } - } - } - - private static void stopWorkaroundAndroid11() { - if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { - ServiceManager.getActivityManager().forceStopPackage(FakeContext.PACKAGE_NAME); - } - } - @TargetApi(Build.VERSION_CODES.M) public void encode() throws IOException, ConfigurationException, AudioCaptureForegroundException { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { @@ -258,10 +166,9 @@ public final class AudioEncoder { } MediaCodec mediaCodec = null; - AudioRecord recorder = null; + AudioCapture capture = new AudioCapture(); boolean mediaCodecStarted = false; - boolean recorderStarted = false; try { Codec codec = streamer.getCodec(); mediaCodec = createMediaCodec(codec, encoderName); @@ -273,27 +180,13 @@ public final class AudioEncoder { mediaCodec.setCallback(new EncoderCallback(), new Handler(mediaCodecThread.getLooper())); mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); - startWorkaroundAndroid11(); - try { - recorder = createAudioRecord(); - recorder.startRecording(); - } catch (UnsupportedOperationException e) { - if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { - Ln.e("Failed to start audio capture"); - Ln.e("On Android 11, it is only possible to capture in foreground, make sure that the device is unlocked when starting scrcpy."); - throw new AudioCaptureForegroundException(); - } - throw e; - } finally { - stopWorkaroundAndroid11(); - } - recorderStarted = true; + capture.start(); final MediaCodec mediaCodecRef = mediaCodec; - final AudioRecord recorderRef = recorder; + final AudioCapture captureRef = capture; inputThread = new Thread(() -> { try { - inputThread(mediaCodecRef, recorderRef); + inputThread(mediaCodecRef, captureRef); } catch (IOException | InterruptedException e) { Ln.e("Audio capture error", e); } finally { @@ -366,11 +259,8 @@ public final class AudioEncoder { } mediaCodec.release(); } - if (recorder != null) { - if (recorderStarted) { - recorder.stop(); - } - recorder.release(); + if (capture != null) { + capture.stop(); } } } From dc228eaad0ea53f57c27e9aba2b51a422ed3aa94 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 3 Mar 2023 18:49:05 +0100 Subject: [PATCH 1454/2244] Extract async processor interface On the server side, several components are started, stopped and joined. Extract an interface to handle them generically. This will help to support both encoded and raw audio stream, because they will be two different concrete components, but implementing the same interface. PR #3757 --- .../com/genymobile/scrcpy/AsyncProcessor.java | 7 ++++ .../com/genymobile/scrcpy/AudioEncoder.java | 2 +- .../com/genymobile/scrcpy/Controller.java | 2 +- .../java/com/genymobile/scrcpy/Server.java | 35 +++++++++---------- 4 files changed, 25 insertions(+), 21 deletions(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/AsyncProcessor.java diff --git a/server/src/main/java/com/genymobile/scrcpy/AsyncProcessor.java b/server/src/main/java/com/genymobile/scrcpy/AsyncProcessor.java new file mode 100644 index 00000000..cbc435b0 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/AsyncProcessor.java @@ -0,0 +1,7 @@ +package com.genymobile.scrcpy; + +public interface AsyncProcessor { + void start(); + void stop(); + void join() throws InterruptedException; +} diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java b/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java index 8b60d37e..0ba424ca 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java @@ -14,7 +14,7 @@ import java.util.List; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; -public final class AudioEncoder { +public final class AudioEncoder implements AsyncProcessor { private static class InputTask { private final int index; diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index 02d77cb1..59fae602 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -14,7 +14,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; -public class Controller { +public class Controller implements AsyncProcessor { private static final int DEFAULT_DEVICE_ID = 0; diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 35da6965..3d3e02fd 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -5,6 +5,7 @@ import android.os.BatteryManager; import android.os.Build; import java.io.IOException; +import java.util.ArrayList; import java.util.List; import java.util.Locale; @@ -91,8 +92,7 @@ public final class Server { Workarounds.fillAppInfo(); } - Controller controller = null; - AudioEncoder audioEncoder = null; + List asyncProcessors = new ArrayList<>(); try (DesktopConnection connection = DesktopConnection.open(scid, tunnelForward, audio, control, sendDummyByte)) { if (options.getSendDeviceMeta()) { @@ -101,24 +101,27 @@ public final class Server { } if (control) { - controller = new Controller(device, connection, options.getClipboardAutosync(), options.getPowerOn()); - controller.start(); - - final Controller controllerRef = controller; - device.setClipboardListener(text -> controllerRef.getSender().pushClipboardText(text)); + Controller controller = new Controller(device, connection, options.getClipboardAutosync(), options.getPowerOn()); + device.setClipboardListener(text -> controller.getSender().pushClipboardText(text)); + asyncProcessors.add(controller); } if (audio) { Streamer audioStreamer = new Streamer(connection.getAudioFd(), options.getAudioCodec(), options.getSendCodecId(), options.getSendFrameMeta()); - audioEncoder = new AudioEncoder(audioStreamer, options.getAudioBitRate(), options.getAudioCodecOptions(), options.getAudioEncoder()); - audioEncoder.start(); + AudioEncoder audioRecorder = new AudioEncoder(audioStreamer, options.getAudioBitRate(), options.getAudioCodecOptions(), options.getAudioEncoder()); + asyncProcessors.add(audioRecorder); } Streamer videoStreamer = new Streamer(connection.getVideoFd(), options.getVideoCodec(), options.getSendCodecId(), options.getSendFrameMeta()); ScreenEncoder screenEncoder = new ScreenEncoder(device, videoStreamer, options.getVideoBitRate(), options.getMaxFps(), options.getVideoCodecOptions(), options.getVideoEncoder(), options.getDownsizeOnError()); + + for (AsyncProcessor asyncProcessor : asyncProcessors) { + asyncProcessor.start(); + } + try { // synchronous screenEncoder.streamScreen(); @@ -131,20 +134,14 @@ public final class Server { } finally { Ln.d("Screen streaming stopped"); initThread.interrupt(); - if (audioEncoder != null) { - audioEncoder.stop(); - } - if (controller != null) { - controller.stop(); + for (AsyncProcessor asyncProcessor : asyncProcessors) { + asyncProcessor.stop(); } try { initThread.join(); - if (audioEncoder != null) { - audioEncoder.join(); - } - if (controller != null) { - controller.join(); + for (AsyncProcessor asyncProcessor : asyncProcessors) { + asyncProcessor.join(); } } catch (InterruptedException e) { // ignore From 66b6c06443130a8a11984ca2cd6d80f5cd3db6ec Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 3 Mar 2023 21:14:28 +0100 Subject: [PATCH 1455/2244] Add raw audio recorder Add an alternative AudioRecorder to stream raw packets without encoding. PR #3757 --- .../com/genymobile/scrcpy/AudioCodec.java | 3 +- .../genymobile/scrcpy/AudioRawRecorder.java | 75 +++++++++++++++++++ .../java/com/genymobile/scrcpy/Server.java | 11 ++- 3 files changed, 86 insertions(+), 3 deletions(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioCodec.java b/server/src/main/java/com/genymobile/scrcpy/AudioCodec.java index dc000e98..1f3b07a0 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioCodec.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioCodec.java @@ -4,7 +4,8 @@ import android.media.MediaFormat; public enum AudioCodec implements Codec { OPUS(0x6f_70_75_73, "opus", MediaFormat.MIMETYPE_AUDIO_OPUS), - AAC(0x00_61_61_63, "aac", MediaFormat.MIMETYPE_AUDIO_AAC); + AAC(0x00_61_61_63, "aac", MediaFormat.MIMETYPE_AUDIO_AAC), + RAW(0x00_72_61_77, "raw", MediaFormat.MIMETYPE_AUDIO_RAW); private final int id; // 4-byte ASCII representation of the name private final String name; diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java b/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java new file mode 100644 index 00000000..2e483daa --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java @@ -0,0 +1,75 @@ +package com.genymobile.scrcpy; + +import android.media.MediaCodec; + +import java.io.IOException; +import java.nio.ByteBuffer; + +public final class AudioRawRecorder implements AsyncProcessor { + + private final Streamer streamer; + + private Thread thread; + + private static final int READ_MS = 5; // milliseconds + private static final int READ_SIZE = AudioCapture.millisToBytes(READ_MS); + + public AudioRawRecorder(Streamer streamer) { + this.streamer = streamer; + } + + private void record() throws IOException, AudioCaptureForegroundException { + final ByteBuffer buffer = ByteBuffer.allocateDirect(READ_SIZE); + final MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); + + AudioCapture capture = new AudioCapture(); + try { + capture.start(); + + streamer.writeHeader(); + while (!Thread.currentThread().isInterrupted()) { + buffer.position(0); + int r = capture.read(buffer, READ_SIZE, bufferInfo); + if (r < 0) { + throw new IOException("Could not read audio: " + r); + } + buffer.limit(r); + + streamer.writePacket(buffer, bufferInfo); + } + } catch (Throwable e) { + // Notify the client that the audio could not be captured + streamer.writeDisableStream(false); + throw e; + } finally { + capture.stop(); + } + } + + public void start() { + thread = new Thread(() -> { + try { + record(); + } catch (AudioCaptureForegroundException e) { + // Do not print stack trace, a user-friendly error-message has already been logged + } catch (IOException e) { + Ln.e("Audio recording error", e); + } finally { + Ln.d("Audio recorder stopped"); + } + }); + thread.start(); + } + + public void stop() { + if (thread != null) { + thread.interrupt(); + } + } + + public void join() throws InterruptedException { + if (thread != null) { + thread.join(); + } + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 3d3e02fd..86555e3b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -107,9 +107,16 @@ public final class Server { } if (audio) { - Streamer audioStreamer = new Streamer(connection.getAudioFd(), options.getAudioCodec(), options.getSendCodecId(), + AudioCodec audioCodec = options.getAudioCodec(); + Streamer audioStreamer = new Streamer(connection.getAudioFd(), audioCodec, options.getSendCodecId(), options.getSendFrameMeta()); - AudioEncoder audioRecorder = new AudioEncoder(audioStreamer, options.getAudioBitRate(), options.getAudioCodecOptions(), options.getAudioEncoder()); + AsyncProcessor audioRecorder; + if (audioCodec == AudioCodec.RAW) { + audioRecorder = new AudioRawRecorder(audioStreamer); + } else { + audioRecorder = new AudioEncoder(audioStreamer, options.getAudioBitRate(), options.getAudioCodecOptions(), + options.getAudioEncoder()); + } asyncProcessors.add(audioRecorder); } From d2952c7e93f151d773f38aa368b7163b0ff31cf2 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 3 Mar 2023 21:19:37 +0100 Subject: [PATCH 1456/2244] Add --audio-codec=raw option Add support for raw (PCM S16 LE) audio codec (a raw decoder is included in FFmpeg). PR #3757 --- app/data/bash-completion/scrcpy | 2 +- app/data/zsh-completion/_scrcpy | 2 +- app/scrcpy.1 | 2 +- app/src/cli.c | 13 +++++++++++-- app/src/demuxer.c | 3 +++ app/src/options.h | 1 + app/src/server.c | 2 ++ 7 files changed, 20 insertions(+), 5 deletions(-) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index 74c3ee57..c3649364 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -78,7 +78,7 @@ _scrcpy() { return ;; --audio-codec) - COMPREPLY=($(compgen -W 'opus aac' -- "$cur")) + COMPREPLY=($(compgen -W 'opus aac raw' -- "$cur")) return ;; --lock-video-orientation) diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index b28201a4..d713761c 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -10,7 +10,7 @@ local arguments arguments=( '--always-on-top[Make scrcpy window always on top \(above other windows\)]' '--audio-bit-rate=[Encode the audio at the given bit-rate]' - '--audio-codec=[Select the audio codec]:codec:(opus aac)' + '--audio-codec=[Select the audio codec]:codec:(opus aac raw)' '--audio-codec-options=[Set a list of comma-separated key\:type=value options for the device audio encoder]' '--audio-encoder=[Use a specific MediaCodec audio encoder]' {-b,--video-bit-rate=}'[Encode the video at the given bit-rate]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 120ea192..e8e36188 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -35,7 +35,7 @@ Default is 50. .TP .BI "\-\-audio\-codec " name -Select an audio codec (opus or aac). +Select an audio codec (opus, aac or raw). Default is opus. diff --git a/app/src/cli.c b/app/src/cli.c index 122a5891..d45be878 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -134,7 +134,7 @@ static const struct sc_option options[] = { .longopt_id = OPT_AUDIO_CODEC, .longopt = "audio-codec", .argdesc = "name", - .text = "Select an audio codec (opus or aac).\n" + .text = "Select an audio codec (opus, aac or raw).\n" "Default is opus.", }, { @@ -1522,7 +1522,11 @@ parse_audio_codec(const char *optarg, enum sc_codec *codec) { *codec = SC_CODEC_AAC; return true; } - LOGE("Unsupported audio codec: %s (expected opus or aac)", optarg); + if (!strcmp(optarg, "raw")) { + *codec = SC_CODEC_RAW; + return true; + } + LOGE("Unsupported audio codec: %s (expected opus, aac or raw)", optarg); return false; } @@ -1923,6 +1927,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } } + if (opts->record_filename && opts->audio_codec == SC_CODEC_RAW) { + LOGW("Recording does not support RAW audio codec"); + return false; + } + if (!opts->control) { if (opts->turn_screen_off) { LOGE("Could not request to turn screen off if control is disabled"); diff --git a/app/src/demuxer.c b/app/src/demuxer.c index 15a595a0..a4fa19f4 100644 --- a/app/src/demuxer.c +++ b/app/src/demuxer.c @@ -25,6 +25,7 @@ sc_demuxer_to_avcodec_id(uint32_t codec_id) { #define SC_CODEC_ID_AV1 UINT32_C(0x00617631) // "av1" in ASCII #define SC_CODEC_ID_OPUS UINT32_C(0x6f707573) // "opus" in ASCII #define SC_CODEC_ID_AAC UINT32_C(0x00616163) // "aac in ASCII" +#define SC_CODEC_ID_RAW UINT32_C(0x00726177) // "raw" in ASCII switch (codec_id) { case SC_CODEC_ID_H264: return AV_CODEC_ID_H264; @@ -36,6 +37,8 @@ sc_demuxer_to_avcodec_id(uint32_t codec_id) { return AV_CODEC_ID_OPUS; case SC_CODEC_ID_AAC: return AV_CODEC_ID_AAC; + case SC_CODEC_ID_RAW: + return AV_CODEC_ID_PCM_S16LE; default: LOGE("Unknown codec id 0x%08" PRIx32, codec_id); return AV_CODEC_ID_NONE; diff --git a/app/src/options.h b/app/src/options.h index d9c2d228..06b4ddfa 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -29,6 +29,7 @@ enum sc_codec { SC_CODEC_AV1, SC_CODEC_OPUS, SC_CODEC_AAC, + SC_CODEC_RAW, }; enum sc_lock_video_orientation { diff --git a/app/src/server.c b/app/src/server.c index 9d4fb098..7b503427 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -173,6 +173,8 @@ sc_server_get_codec_name(enum sc_codec codec) { return "opus"; case SC_CODEC_AAC: return "aac"; + case SC_CODEC_RAW: + return "raw"; default: return NULL; } From 7da45c246e919386f7d4fd3facc4597bb056e752 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 3 Mar 2023 21:58:57 +0100 Subject: [PATCH 1457/2244] Warn on ignored audio options For raw audio codec, some audio options are ignored. PR #3757 --- app/src/cli.c | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/app/src/cli.c b/app/src/cli.c index d45be878..8a0b6aa4 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -1932,6 +1932,18 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; } + if (opts->audio_codec == SC_CODEC_RAW) { + if (opts->audio_bit_rate) { + LOGW("--audio-bit-rate is ignored for raw audio codec"); + } + if (opts->audio_codec_options) { + LOGW("--audio-codec-options is ignored for raw audio codec"); + } + if (opts->audio_encoder) { + LOGW("--audio-encoder is ignored for raw audio codec"); + } + } + if (!opts->control) { if (opts->turn_screen_off) { LOGE("Could not request to turn screen off if control is disabled"); From bb56472d4eb15d7c318a05a8236af6d0f5a46a04 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 8 Mar 2023 20:07:03 +0100 Subject: [PATCH 1458/2244] Print server logs and newline in one call System.out.println() first prints the message, then the new line. Between these two calls, the client might print a message, breaking formatting. Instead, call System.out.print() with '\n' appended to the message. --- server/src/main/java/com/genymobile/scrcpy/Ln.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Ln.java b/server/src/main/java/com/genymobile/scrcpy/Ln.java index c39fc621..291f26ff 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Ln.java +++ b/server/src/main/java/com/genymobile/scrcpy/Ln.java @@ -39,28 +39,28 @@ public final class Ln { public static void v(String message) { if (isEnabled(Level.VERBOSE)) { Log.v(TAG, message); - System.out.println(PREFIX + "VERBOSE: " + message); + System.out.print(PREFIX + "VERBOSE: " + message + '\n'); } } public static void d(String message) { if (isEnabled(Level.DEBUG)) { Log.d(TAG, message); - System.out.println(PREFIX + "DEBUG: " + message); + System.out.print(PREFIX + "DEBUG: " + message + '\n'); } } public static void i(String message) { if (isEnabled(Level.INFO)) { Log.i(TAG, message); - System.out.println(PREFIX + "INFO: " + message); + System.out.print(PREFIX + "INFO: " + message + '\n'); } } public static void w(String message, Throwable throwable) { if (isEnabled(Level.WARN)) { Log.w(TAG, message, throwable); - System.out.println(PREFIX + "WARN: " + message); + System.out.print(PREFIX + "WARN: " + message + '\n'); if (throwable != null) { throwable.printStackTrace(); } @@ -74,7 +74,7 @@ public final class Ln { public static void e(String message, Throwable throwable) { if (isEnabled(Level.ERROR)) { Log.e(TAG, message, throwable); - System.out.println(PREFIX + "ERROR: " + message); + System.out.print(PREFIX + "ERROR: " + message + "\n"); if (throwable != null) { throwable.printStackTrace(); } From 4a25f3e53bdac8c2510cc1c7f06e479a17a42e6b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 8 Mar 2023 20:13:08 +0100 Subject: [PATCH 1459/2244] Print info logs to stdout All server logs were printed to stdout, while all client logs were printed to stderr. Instead, use stderr for warnings and errors, stdout for the others: - stdout: verbose, debug, info - stderr: warn, error --- app/src/util/log.c | 22 +++++++++++++++++++ .../main/java/com/genymobile/scrcpy/Ln.java | 4 ++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/app/src/util/log.c b/app/src/util/log.c index 25b1f26e..0975e54a 100644 --- a/app/src/util/log.c +++ b/app/src/util/log.c @@ -125,8 +125,30 @@ sc_av_log_callback(void *avcl, int level, const char *fmt, va_list vl) { free(local_fmt); } +static const char *const sc_sdl_log_priority_names[SDL_NUM_LOG_PRIORITIES] = { + [SDL_LOG_PRIORITY_VERBOSE] = "VERBOSE", + [SDL_LOG_PRIORITY_DEBUG] = "DEBUG", + [SDL_LOG_PRIORITY_INFO] = "INFO", + [SDL_LOG_PRIORITY_WARN] = "WARN", + [SDL_LOG_PRIORITY_ERROR] = "ERROR", + [SDL_LOG_PRIORITY_CRITICAL] = "CRITICAL", +}; + +static void SDLCALL +sc_sdl_log_print(void *userdata, int category, SDL_LogPriority priority, + const char *message) { + (void) userdata; + (void) category; + + FILE *out = priority < SDL_LOG_PRIORITY_WARN ? stdout : stderr; + assert(priority < SDL_NUM_LOG_PRIORITIES); + const char *prio_name = sc_sdl_log_priority_names[priority]; + fprintf(out, "%s: %s\n", prio_name, message); +} + void sc_log_configure() { + SDL_LogSetOutputFunction(sc_sdl_log_print, NULL); // Redirect FFmpeg logs to SDL logs av_log_set_callback(sc_av_log_callback); } diff --git a/server/src/main/java/com/genymobile/scrcpy/Ln.java b/server/src/main/java/com/genymobile/scrcpy/Ln.java index 291f26ff..199c29be 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Ln.java +++ b/server/src/main/java/com/genymobile/scrcpy/Ln.java @@ -60,7 +60,7 @@ public final class Ln { public static void w(String message, Throwable throwable) { if (isEnabled(Level.WARN)) { Log.w(TAG, message, throwable); - System.out.print(PREFIX + "WARN: " + message + '\n'); + System.err.print(PREFIX + "WARN: " + message + '\n'); if (throwable != null) { throwable.printStackTrace(); } @@ -74,7 +74,7 @@ public final class Ln { public static void e(String message, Throwable throwable) { if (isEnabled(Level.ERROR)) { Log.e(TAG, message, throwable); - System.out.print(PREFIX + "ERROR: " + message + "\n"); + System.err.print(PREFIX + "ERROR: " + message + "\n"); if (throwable != null) { throwable.printStackTrace(); } From 5ee59e0f133b079f0652f10b80bb6320b41be278 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 8 Mar 2023 21:34:42 +0100 Subject: [PATCH 1460/2244] Add thread priority API Expose an API to change the priority of the current thread. --- app/src/compat.h | 4 ++++ app/src/util/thread.c | 33 +++++++++++++++++++++++++++++++++ app/src/util/thread.h | 10 ++++++++++ 3 files changed, 47 insertions(+) diff --git a/app/src/compat.h b/app/src/compat.h index 22563421..00cb7204 100644 --- a/app/src/compat.h +++ b/app/src/compat.h @@ -54,6 +54,10 @@ # define SCRCPY_SDL_HAS_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR #endif +#if SDL_VERSION_ATLEAST(2, 0, 16) +# define SCRCPY_SDL_HAS_THREAD_PRIORITY_TIME_CRITICAL +#endif + #ifndef HAVE_STRDUP char *strdup(const char *s); #endif diff --git a/app/src/util/thread.c b/app/src/util/thread.c index f9687add..94921fb7 100644 --- a/app/src/util/thread.c +++ b/app/src/util/thread.c @@ -23,6 +23,39 @@ sc_thread_create(sc_thread *thread, sc_thread_fn fn, const char *name, return true; } +static SDL_ThreadPriority +to_sdl_thread_priority(enum sc_thread_priority priority) { + switch (priority) { + case SC_THREAD_PRIORITY_TIME_CRITICAL: +#ifdef SCRCPY_SDL_HAS_THREAD_PRIORITY_TIME_CRITICAL + return SDL_THREAD_PRIORITY_TIME_CRITICAL; +#else + // fall through +#endif + case SC_THREAD_PRIORITY_HIGH: + return SDL_THREAD_PRIORITY_HIGH; + case SC_THREAD_PRIORITY_NORMAL: + return SDL_THREAD_PRIORITY_NORMAL; + case SC_THREAD_PRIORITY_LOW: + return SDL_THREAD_PRIORITY_LOW; + default: + assert(!"Unknown thread priority"); + return 0; + } +} + +bool +sc_thread_set_priority(enum sc_thread_priority priority) { + SDL_ThreadPriority sdl_priority = to_sdl_thread_priority(priority); + int r = SDL_SetThreadPriority(sdl_priority); + if (r) { + LOGD("Could not set thread priority: %s", SDL_GetError()); + return false; + } + + return true; +} + void sc_thread_join(sc_thread *thread, int *status) { SDL_WaitThread(thread->thread, status); diff --git a/app/src/util/thread.h b/app/src/util/thread.h index 7add6f1c..4183adac 100644 --- a/app/src/util/thread.h +++ b/app/src/util/thread.h @@ -21,6 +21,13 @@ typedef struct sc_thread { SDL_Thread *thread; } sc_thread; +enum sc_thread_priority { + SC_THREAD_PRIORITY_LOW, + SC_THREAD_PRIORITY_NORMAL, + SC_THREAD_PRIORITY_HIGH, + SC_THREAD_PRIORITY_TIME_CRITICAL, +}; + typedef struct sc_mutex { SDL_mutex *mutex; #ifndef NDEBUG @@ -39,6 +46,9 @@ sc_thread_create(sc_thread *thread, sc_thread_fn fn, const char *name, void sc_thread_join(sc_thread *thread, int *status); +bool +sc_thread_set_priority(enum sc_thread_priority priority); + bool sc_mutex_init(sc_mutex *mutex); From aa450ffc3f618a286c0eb7a5f60244b53d650e75 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 8 Mar 2023 21:37:27 +0100 Subject: [PATCH 1461/2244] Increase audio thread priority The audio demuxer thread is the one filling the audio buffer read by the SDL audio thread. It is time critical to avoid buffer underflow. --- app/src/audio_player.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/src/audio_player.c b/app/src/audio_player.c index de218f1e..85de0620 100644 --- a/app/src/audio_player.c +++ b/app/src/audio_player.c @@ -382,6 +382,14 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink, ap->received = false; ap->played = false; + // The thread calling open() is the thread calling push(), which fills the + // audio buffer consumed by the SDL audio thread. + ok = sc_thread_set_priority(SC_THREAD_PRIORITY_TIME_CRITICAL); + if (!ok) { + ok = sc_thread_set_priority(SC_THREAD_PRIORITY_HIGH); + (void) ok; // We don't care if it worked, at least we tried + } + SDL_PauseAudioDevice(ap->device, 0); return true; From 408f45863617465cee36453fb9ce2c7403e159e0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 8 Mar 2023 21:40:39 +0100 Subject: [PATCH 1462/2244] Decrease recorder thread priority Recording is background task, writing the packets to a file is not urgent. --- app/src/recorder.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/recorder.c b/app/src/recorder.c index af5fe510..572d3e24 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -505,6 +505,10 @@ static int run_recorder(void *data) { struct sc_recorder *recorder = data; + // Recording is a background task + bool ok = sc_thread_set_priority(SC_THREAD_PRIORITY_LOW); + (void) ok; // We don't care if it worked + bool success = sc_recorder_record(recorder); sc_mutex_lock(&recorder->mutex); From d93582724ddb300044e0a3e0e7c728390da5762d Mon Sep 17 00:00:00 2001 From: "chengjian.scj" Date: Thu, 2 Mar 2023 17:57:13 +0800 Subject: [PATCH 1463/2244] Initialize interrupted field explicitly The field sc_fps_counter.interrupted was never initialized explicitly. Signed-off-by: Romain Vimont --- app/src/fps_counter.c | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/fps_counter.c b/app/src/fps_counter.c index 85312821..dd4ae1da 100644 --- a/app/src/fps_counter.c +++ b/app/src/fps_counter.c @@ -96,6 +96,7 @@ run_fps_counter(void *data) { bool sc_fps_counter_start(struct sc_fps_counter *counter) { sc_mutex_lock(&counter->mutex); + counter->interrupted = false; counter->next_timestamp = sc_tick_now() + SC_FPS_COUNTER_INTERVAL; counter->nr_rendered = 0; counter->nr_skipped = 0; From 46f691817983c8c251956e5115ace6a8af8e08d4 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 10 Mar 2023 22:42:59 +0100 Subject: [PATCH 1464/2244] Stop and join sc_file_pusher only if initialized The sc_file_pusher is lazy-initialized, but it was stopped and joined in all cases (accessing uninitialized values). Detected by poisoning the struct scrcpy instance with ASAN enabled. --- app/src/file_pusher.c | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/app/src/file_pusher.c b/app/src/file_pusher.c index b49e93e5..06911052 100644 --- a/app/src/file_pusher.c +++ b/app/src/file_pusher.c @@ -172,14 +172,18 @@ sc_file_pusher_start(struct sc_file_pusher *fp) { void sc_file_pusher_stop(struct sc_file_pusher *fp) { - sc_mutex_lock(&fp->mutex); - fp->stopped = true; - sc_cond_signal(&fp->event_cond); - sc_intr_interrupt(&fp->intr); - sc_mutex_unlock(&fp->mutex); + if (fp->initialized) { + sc_mutex_lock(&fp->mutex); + fp->stopped = true; + sc_cond_signal(&fp->event_cond); + sc_intr_interrupt(&fp->intr); + sc_mutex_unlock(&fp->mutex); + } } void sc_file_pusher_join(struct sc_file_pusher *fp) { - sc_thread_join(&fp->thread, NULL); + if (fp->initialized) { + sc_thread_join(&fp->thread, NULL); + } } From 4db50ddbb7b80df2f5b8240027619bf1ed7decb1 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 11 Mar 2023 15:55:44 +0100 Subject: [PATCH 1465/2244] Enable log signaling buffering threshold exceeded It is as important as underflow logs. --- app/src/audio_player.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/audio_player.c b/app/src/audio_player.c index 85de0620..77de0ddb 100644 --- a/app/src/audio_player.c +++ b/app/src/audio_player.c @@ -207,10 +207,8 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink, uint32_t skip_samples = buffered_samples - max_buffered_samples; size_t skip_bytes = samples_to_bytes(ap, skip_samples); sc_bytebuf_skip(&ap->buf, skip_bytes); -#ifndef SC_AUDIO_PLAYER_NDEBUG LOGD("[Audio] Buffering threshold exceeded, skipping %" PRIu32 " samples", skip_samples); -#endif } // Number of samples added (or removed, if negative) for compensation From 4bdf632dfadec432582db64bff898588a4ed6e13 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 10 Mar 2023 19:25:45 +0100 Subject: [PATCH 1466/2244] Pass AVCodecContext to packet sinks Create the codec context from the demuxer, so that it can fill context data for the decoder and recorder. --- app/src/audio_player.c | 1 + app/src/decoder.c | 50 ++++++----------------------------- app/src/decoder.h | 2 +- app/src/demuxer.c | 32 +++++++++++++++++++++- app/src/recorder.c | 10 +++---- app/src/trait/frame_sink.h | 3 +-- app/src/trait/packet_sink.h | 8 +++--- app/src/trait/packet_source.c | 4 +-- app/src/trait/packet_source.h | 2 +- 9 files changed, 52 insertions(+), 60 deletions(-) diff --git a/app/src/audio_player.c b/app/src/audio_player.c index 77de0ddb..7a348f93 100644 --- a/app/src/audio_player.c +++ b/app/src/audio_player.c @@ -1,5 +1,6 @@ #include "audio_player.h" +#include #include #include "util/log.h" diff --git a/app/src/decoder.c b/app/src/decoder.c index ecad8373..5d42b8b0 100644 --- a/app/src/decoder.c +++ b/app/src/decoder.c @@ -12,52 +12,20 @@ #define DOWNCAST(SINK) container_of(SINK, struct sc_decoder, packet_sink) static bool -sc_decoder_open(struct sc_decoder *decoder, const AVCodec *codec) { - decoder->codec_ctx = avcodec_alloc_context3(codec); - if (!decoder->codec_ctx) { - LOG_OOM(); - return false; - } - - decoder->codec_ctx->flags |= AV_CODEC_FLAG_LOW_DELAY; - - if (codec->type == AVMEDIA_TYPE_VIDEO) { - // Hardcoded video properties - decoder->codec_ctx->pix_fmt = AV_PIX_FMT_YUV420P; - } else { - // Hardcoded audio properties -#ifdef SCRCPY_LAVU_HAS_CHLAYOUT - decoder->codec_ctx->ch_layout = - (AVChannelLayout) AV_CHANNEL_LAYOUT_STEREO; -#else - decoder->codec_ctx->channel_layout = AV_CH_LAYOUT_STEREO; - decoder->codec_ctx->channels = 2; -#endif - decoder->codec_ctx->sample_rate = 48000; - } - - if (avcodec_open2(decoder->codec_ctx, codec, NULL) < 0) { - LOGE("Decoder '%s': could not open codec", decoder->name); - avcodec_free_context(&decoder->codec_ctx); - return false; - } - +sc_decoder_open(struct sc_decoder *decoder, AVCodecContext *ctx) { decoder->frame = av_frame_alloc(); if (!decoder->frame) { LOG_OOM(); - avcodec_close(decoder->codec_ctx); - avcodec_free_context(&decoder->codec_ctx); return false; } - if (!sc_frame_source_sinks_open(&decoder->frame_source, - decoder->codec_ctx)) { + if (!sc_frame_source_sinks_open(&decoder->frame_source, ctx)) { av_frame_free(&decoder->frame); - avcodec_close(decoder->codec_ctx); - avcodec_free_context(&decoder->codec_ctx); return false; } + decoder->ctx = ctx; + return true; } @@ -65,8 +33,6 @@ static void sc_decoder_close(struct sc_decoder *decoder) { sc_frame_source_sinks_close(&decoder->frame_source); av_frame_free(&decoder->frame); - avcodec_close(decoder->codec_ctx); - avcodec_free_context(&decoder->codec_ctx); } static bool @@ -77,7 +43,7 @@ sc_decoder_push(struct sc_decoder *decoder, const AVPacket *packet) { return true; } - int ret = avcodec_send_packet(decoder->codec_ctx, packet); + int ret = avcodec_send_packet(decoder->ctx, packet); if (ret < 0 && ret != AVERROR(EAGAIN)) { LOGE("Decoder '%s': could not send video packet: %d", decoder->name, ret); @@ -85,7 +51,7 @@ sc_decoder_push(struct sc_decoder *decoder, const AVPacket *packet) { } for (;;) { - ret = avcodec_receive_frame(decoder->codec_ctx, decoder->frame); + ret = avcodec_receive_frame(decoder->ctx, decoder->frame); if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) { break; } @@ -110,9 +76,9 @@ sc_decoder_push(struct sc_decoder *decoder, const AVPacket *packet) { } static bool -sc_decoder_packet_sink_open(struct sc_packet_sink *sink, const AVCodec *codec) { +sc_decoder_packet_sink_open(struct sc_packet_sink *sink, AVCodecContext *ctx) { struct sc_decoder *decoder = DOWNCAST(sink); - return sc_decoder_open(decoder, codec); + return sc_decoder_open(decoder, ctx); } static void diff --git a/app/src/decoder.h b/app/src/decoder.h index 87aaf6a2..ba8903f4 100644 --- a/app/src/decoder.h +++ b/app/src/decoder.h @@ -16,7 +16,7 @@ struct sc_decoder { const char *name; // must be statically allocated (e.g. a string literal) - AVCodecContext *codec_ctx; + AVCodecContext *ctx; AVFrame *frame; }; diff --git a/app/src/demuxer.c b/app/src/demuxer.c index a4fa19f4..eabcb81e 100644 --- a/app/src/demuxer.c +++ b/app/src/demuxer.c @@ -160,10 +160,37 @@ run_demuxer(void *data) { goto end; } - if (!sc_packet_source_sinks_open(&demuxer->packet_source, codec)) { + AVCodecContext *codec_ctx = avcodec_alloc_context3(codec); + if (!codec_ctx) { + LOG_OOM(); goto end; } + codec_ctx->flags |= AV_CODEC_FLAG_LOW_DELAY; + + if (codec->type == AVMEDIA_TYPE_VIDEO) { + // Hardcoded video properties + codec_ctx->pix_fmt = AV_PIX_FMT_YUV420P; + } else { + // Hardcoded audio properties +#ifdef SCRCPY_LAVU_HAS_CHLAYOUT + codec_ctx->ch_layout = (AVChannelLayout) AV_CHANNEL_LAYOUT_STEREO; +#else + codec_ctx->channel_layout = AV_CH_LAYOUT_STEREO; + codec_ctx->channels = 2; +#endif + codec_ctx->sample_rate = 48000; + } + + if (avcodec_open2(codec_ctx, codec, NULL) < 0) { + LOGE("Demuxer '%s': could not open codec", demuxer->name); + goto finally_free_context; + } + + if (!sc_packet_source_sinks_open(&demuxer->packet_source, codec_ctx)) { + goto finally_free_context; + } + // Config packets must be merged with the next non-config packet only for // video streams bool must_merge_config_packet = codec->type == AVMEDIA_TYPE_VIDEO; @@ -214,6 +241,9 @@ run_demuxer(void *data) { av_packet_free(&packet); finally_close_sinks: sc_packet_source_sinks_close(&demuxer->packet_source); +finally_free_context: + // This also calls avcodec_close() internally + avcodec_free_context(&codec_ctx); end: demuxer->cbs->on_ended(demuxer, status, demuxer->cbs_userdata); diff --git a/app/src/recorder.c b/app/src/recorder.c index 572d3e24..1e89608a 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -536,9 +536,8 @@ run_recorder(void *data) { static bool sc_recorder_video_packet_sink_open(struct sc_packet_sink *sink, - const AVCodec *codec) { + AVCodecContext *ctx) { struct sc_recorder *recorder = DOWNCAST_VIDEO(sink); - assert(codec); sc_mutex_lock(&recorder->mutex); if (recorder->stopped) { @@ -546,7 +545,7 @@ sc_recorder_video_packet_sink_open(struct sc_packet_sink *sink, return false; } - recorder->video_codec = codec; + recorder->video_codec = ctx->codec; sc_cond_signal(&recorder->stream_cond); sc_mutex_unlock(&recorder->mutex); @@ -601,15 +600,14 @@ sc_recorder_video_packet_sink_push(struct sc_packet_sink *sink, static bool sc_recorder_audio_packet_sink_open(struct sc_packet_sink *sink, - const AVCodec *codec) { + AVCodecContext *ctx) { struct sc_recorder *recorder = DOWNCAST_AUDIO(sink); assert(recorder->audio); // only written from this thread, no need to lock assert(!recorder->audio_disabled); - assert(codec); sc_mutex_lock(&recorder->mutex); - recorder->audio_codec = codec; + recorder->audio_codec = ctx->codec; sc_cond_signal(&recorder->stream_cond); sc_mutex_unlock(&recorder->mutex); diff --git a/app/src/trait/frame_sink.h b/app/src/trait/frame_sink.h index 30bf0d37..8ef248b6 100644 --- a/app/src/trait/frame_sink.h +++ b/app/src/trait/frame_sink.h @@ -7,8 +7,6 @@ #include #include -typedef struct AVFrame AVFrame; - /** * Frame sink trait. * @@ -19,6 +17,7 @@ struct sc_frame_sink { }; struct sc_frame_sink_ops { + /* The codec context is valid until the sink is closed */ bool (*open)(struct sc_frame_sink *sink, const AVCodecContext *ctx); void (*close)(struct sc_frame_sink *sink); bool (*push)(struct sc_frame_sink *sink, const AVFrame *frame); diff --git a/app/src/trait/packet_sink.h b/app/src/trait/packet_sink.h index 099c8c52..84cfe814 100644 --- a/app/src/trait/packet_sink.h +++ b/app/src/trait/packet_sink.h @@ -5,9 +5,7 @@ #include #include - -typedef struct AVCodec AVCodec; -typedef struct AVPacket AVPacket; +#include /** * Packet sink trait. @@ -19,8 +17,8 @@ struct sc_packet_sink { }; struct sc_packet_sink_ops { - /* The codec instance is static, it is valid until the end of the program */ - bool (*open)(struct sc_packet_sink *sink, const AVCodec *codec); + /* The codec context is valid until the sink is closed */ + bool (*open)(struct sc_packet_sink *sink, AVCodecContext *ctx); void (*close)(struct sc_packet_sink *sink); bool (*push)(struct sc_packet_sink *sink, const AVPacket *packet); diff --git a/app/src/trait/packet_source.c b/app/src/trait/packet_source.c index df678e16..c0836f1d 100644 --- a/app/src/trait/packet_source.c +++ b/app/src/trait/packet_source.c @@ -25,11 +25,11 @@ sc_packet_source_sinks_close_firsts(struct sc_packet_source *source, bool sc_packet_source_sinks_open(struct sc_packet_source *source, - const AVCodec *codec) { + AVCodecContext *ctx) { assert(source->sink_count); for (unsigned i = 0; i < source->sink_count; ++i) { struct sc_packet_sink *sink = source->sinks[i]; - if (!sink->ops->open(sink, codec)) { + if (!sink->ops->open(sink, ctx)) { sc_packet_source_sinks_close_firsts(source, i); return false; } diff --git a/app/src/trait/packet_source.h b/app/src/trait/packet_source.h index c34aa5d3..16d56e86 100644 --- a/app/src/trait/packet_source.h +++ b/app/src/trait/packet_source.h @@ -26,7 +26,7 @@ sc_packet_source_add_sink(struct sc_packet_source *source, bool sc_packet_source_sinks_open(struct sc_packet_source *source, - const AVCodec *codec); + AVCodecContext *ctx); void sc_packet_source_sinks_close(struct sc_packet_source *source); From 5052e15f7fd5049d9c65712182c1a4a1d349a65e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 10 Mar 2023 21:50:34 +0100 Subject: [PATCH 1467/2244] Create recorder streams from packet sinks ops Previously, the packet sink push() implementation just set the codec and notified a wait condition. Then the recorder thread read the codec and created the AVStream. But this was racy: an AVFrame could be pushed before the creation of the AVStream, causing its video_stream_index or audio_stream_index to be initialized to -1. Also, in the future, the AVStream initialization might need data provided by the packet sink open(), so initialize it there (with a mutex). --- app/src/recorder.c | 128 +++++++++++++++++++-------------------------- app/src/recorder.h | 7 +-- 2 files changed, 57 insertions(+), 78 deletions(-) diff --git a/app/src/recorder.c b/app/src/recorder.c index 1e89608a..8f8a1a89 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -150,70 +150,22 @@ sc_recorder_close_output_file(struct sc_recorder *recorder) { avformat_free_context(recorder->ctx); } -static bool +static void sc_recorder_wait_video_stream(struct sc_recorder *recorder) { sc_mutex_lock(&recorder->mutex); - while (!recorder->video_codec && !recorder->stopped) { + while (!recorder->video_init && !recorder->stopped) { sc_cond_wait(&recorder->stream_cond, &recorder->mutex); } - const AVCodec *codec = recorder->video_codec; sc_mutex_unlock(&recorder->mutex); - - if (codec) { - AVStream *stream = avformat_new_stream(recorder->ctx, codec); - if (!stream) { - return false; - } - - stream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO; - stream->codecpar->codec_id = codec->id; - stream->codecpar->format = AV_PIX_FMT_YUV420P; - stream->codecpar->width = recorder->declared_frame_size.width; - stream->codecpar->height = recorder->declared_frame_size.height; - - recorder->video_stream_index = stream->index; - } - - return true; } -static bool +static void sc_recorder_wait_audio_stream(struct sc_recorder *recorder) { sc_mutex_lock(&recorder->mutex); - while (!recorder->audio_codec && !recorder->audio_disabled - && !recorder->stopped) { + while (!recorder->audio_init && !recorder->stopped) { sc_cond_wait(&recorder->stream_cond, &recorder->mutex); } - - if (recorder->audio_disabled) { - // Reset audio flag. From there, the recorder thread may access this - // flag without any mutex. - recorder->audio = false; - } - - const AVCodec *codec = recorder->audio_codec; sc_mutex_unlock(&recorder->mutex); - - if (codec) { - AVStream *stream = avformat_new_stream(recorder->ctx, codec); - if (!stream) { - return false; - } - - stream->codecpar->codec_type = AVMEDIA_TYPE_AUDIO; - stream->codecpar->codec_id = codec->id; -#ifdef SCRCPY_LAVU_HAS_CHLAYOUT - stream->codecpar->ch_layout.nb_channels = 2; -#else - stream->codecpar->channel_layout = AV_CH_LAYOUT_STEREO; - stream->codecpar->channels = 2; -#endif - stream->codecpar->sample_rate = 48000; - - recorder->audio_stream_index = stream->index; - } - - return true; } static inline bool @@ -480,18 +432,10 @@ sc_recorder_record(struct sc_recorder *recorder) { return false; } - ok = sc_recorder_wait_video_stream(recorder); - if (!ok) { - sc_recorder_close_output_file(recorder); - return false; - } + sc_recorder_wait_video_stream(recorder); if (recorder->audio) { - ok = sc_recorder_wait_audio_stream(recorder); - if (!ok) { - sc_recorder_close_output_file(recorder); - return false; - } + sc_recorder_wait_audio_stream(recorder); } // If recorder->stopped, process any queued packet anyway @@ -538,6 +482,8 @@ static bool sc_recorder_video_packet_sink_open(struct sc_packet_sink *sink, AVCodecContext *ctx) { struct sc_recorder *recorder = DOWNCAST_VIDEO(sink); + // only written from this thread, no need to lock + assert(!recorder->video_init); sc_mutex_lock(&recorder->mutex); if (recorder->stopped) { @@ -545,7 +491,21 @@ sc_recorder_video_packet_sink_open(struct sc_packet_sink *sink, return false; } - recorder->video_codec = ctx->codec; + AVStream *stream = avformat_new_stream(recorder->ctx, ctx->codec); + if (!stream) { + sc_mutex_unlock(&recorder->mutex); + return false; + } + + stream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO; + stream->codecpar->codec_id = ctx->codec->id; + stream->codecpar->format = AV_PIX_FMT_YUV420P; + stream->codecpar->width = recorder->declared_frame_size.width; + stream->codecpar->height = recorder->declared_frame_size.height; + + recorder->video_stream_index = stream->index; + + recorder->video_init = true; sc_cond_signal(&recorder->stream_cond); sc_mutex_unlock(&recorder->mutex); @@ -555,6 +515,8 @@ sc_recorder_video_packet_sink_open(struct sc_packet_sink *sink, static void sc_recorder_video_packet_sink_close(struct sc_packet_sink *sink) { struct sc_recorder *recorder = DOWNCAST_VIDEO(sink); + // only written from this thread, no need to lock + assert(recorder->video_init); sc_mutex_lock(&recorder->mutex); // EOS also stops the recorder @@ -567,6 +529,8 @@ static bool sc_recorder_video_packet_sink_push(struct sc_packet_sink *sink, const AVPacket *packet) { struct sc_recorder *recorder = DOWNCAST_VIDEO(sink); + // only written from this thread, no need to lock + assert(recorder->video_init); sc_mutex_lock(&recorder->mutex); @@ -604,10 +568,29 @@ sc_recorder_audio_packet_sink_open(struct sc_packet_sink *sink, struct sc_recorder *recorder = DOWNCAST_AUDIO(sink); assert(recorder->audio); // only written from this thread, no need to lock - assert(!recorder->audio_disabled); + assert(!recorder->audio_init); sc_mutex_lock(&recorder->mutex); - recorder->audio_codec = ctx->codec; + + AVStream *stream = avformat_new_stream(recorder->ctx, ctx->codec); + if (!stream) { + sc_mutex_unlock(&recorder->mutex); + return false; + } + + stream->codecpar->codec_type = AVMEDIA_TYPE_AUDIO; + stream->codecpar->codec_id = ctx->codec->id; +#ifdef SCRCPY_LAVU_HAS_CHLAYOUT + stream->codecpar->ch_layout.nb_channels = 2; +#else + stream->codecpar->channel_layout = AV_CH_LAYOUT_STEREO; + stream->codecpar->channels = 2; +#endif + stream->codecpar->sample_rate = 48000; + + recorder->audio_stream_index = stream->index; + + recorder->audio_init = true; sc_cond_signal(&recorder->stream_cond); sc_mutex_unlock(&recorder->mutex); @@ -619,7 +602,7 @@ sc_recorder_audio_packet_sink_close(struct sc_packet_sink *sink) { struct sc_recorder *recorder = DOWNCAST_AUDIO(sink); assert(recorder->audio); // only written from this thread, no need to lock - assert(!recorder->audio_disabled); + assert(recorder->audio_init); sc_mutex_lock(&recorder->mutex); // EOS also stops the recorder @@ -634,7 +617,7 @@ sc_recorder_audio_packet_sink_push(struct sc_packet_sink *sink, struct sc_recorder *recorder = DOWNCAST_AUDIO(sink); assert(recorder->audio); // only written from this thread, no need to lock - assert(!recorder->audio_disabled); + assert(recorder->audio_init); sc_mutex_lock(&recorder->mutex); @@ -671,13 +654,13 @@ sc_recorder_audio_packet_sink_disable(struct sc_packet_sink *sink) { struct sc_recorder *recorder = DOWNCAST_AUDIO(sink); assert(recorder->audio); // only written from this thread, no need to lock - assert(!recorder->audio_disabled); - assert(!recorder->audio_codec); + assert(!recorder->audio_init); LOGW("Audio stream recording disabled"); sc_mutex_lock(&recorder->mutex); - recorder->audio_disabled = true; + recorder->audio = false; + recorder->audio_init = true; sc_cond_signal(&recorder->stream_cond); sc_mutex_unlock(&recorder->mutex); } @@ -714,9 +697,8 @@ sc_recorder_init(struct sc_recorder *recorder, const char *filename, sc_vecdeque_init(&recorder->audio_queue); recorder->stopped = false; - recorder->video_codec = NULL; - recorder->audio_codec = NULL; - recorder->audio_disabled = false; + recorder->video_init = false; + recorder->audio_init = false; recorder->video_stream_index = -1; recorder->audio_stream_index = -1; diff --git a/app/src/recorder.h b/app/src/recorder.h index e3d5f018..35758db7 100644 --- a/app/src/recorder.h +++ b/app/src/recorder.h @@ -43,11 +43,8 @@ struct sc_recorder { // wake up the recorder thread once the video or audio codec is known sc_cond stream_cond; - const AVCodec *video_codec; - const AVCodec *audio_codec; - // Instead of providing an audio_codec, the demuxer may notify that the - // stream is disabled if the device could not capture audio - bool audio_disabled; + bool video_init; + bool audio_init; int video_stream_index; int audio_stream_index; From a9f6001f51b9348d65578178a3e1f6a89efc4aa8 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 10 Mar 2023 21:54:28 +0100 Subject: [PATCH 1468/2244] Simplify recorder After the refactor performed by the previous commit, the functions to wait the video stream and the audio stream could be inlined. --- app/src/recorder.c | 32 ++++---------------------------- 1 file changed, 4 insertions(+), 28 deletions(-) diff --git a/app/src/recorder.c b/app/src/recorder.c index 8f8a1a89..9b646055 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -150,24 +150,6 @@ sc_recorder_close_output_file(struct sc_recorder *recorder) { avformat_free_context(recorder->ctx); } -static void -sc_recorder_wait_video_stream(struct sc_recorder *recorder) { - sc_mutex_lock(&recorder->mutex); - while (!recorder->video_init && !recorder->stopped) { - sc_cond_wait(&recorder->stream_cond, &recorder->mutex); - } - sc_mutex_unlock(&recorder->mutex); -} - -static void -sc_recorder_wait_audio_stream(struct sc_recorder *recorder) { - sc_mutex_lock(&recorder->mutex); - while (!recorder->audio_init && !recorder->stopped) { - sc_cond_wait(&recorder->stream_cond, &recorder->mutex); - } - sc_mutex_unlock(&recorder->mutex); -} - static inline bool sc_recorder_has_empty_queues(struct sc_recorder *recorder) { if (sc_vecdeque_is_empty(&recorder->video_queue)) { @@ -188,8 +170,10 @@ static bool sc_recorder_process_header(struct sc_recorder *recorder) { sc_mutex_lock(&recorder->mutex); - while (!recorder->stopped && sc_recorder_has_empty_queues(recorder)) { - sc_cond_wait(&recorder->queue_cond, &recorder->mutex); + while (!recorder->stopped && (!recorder->video_init + || !recorder->audio_init + || sc_recorder_has_empty_queues(recorder))) { + sc_cond_wait(&recorder->stream_cond, &recorder->mutex); } if (sc_vecdeque_is_empty(&recorder->video_queue)) { @@ -432,14 +416,6 @@ sc_recorder_record(struct sc_recorder *recorder) { return false; } - sc_recorder_wait_video_stream(recorder); - - if (recorder->audio) { - sc_recorder_wait_audio_stream(recorder); - } - - // If recorder->stopped, process any queued packet anyway - ok = sc_recorder_process_packets(recorder); sc_recorder_close_output_file(recorder); return ok; From be985b8242e0626288674b84c5039725170f8f0c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 10 Mar 2023 22:46:56 +0100 Subject: [PATCH 1469/2244] Copy codec parameters from context Now that the recorder have access to the codec context, it may automatically initialize the stream codec parameters. The V4L2 sink could do the same. --- app/src/recorder.c | 23 +++++++++++------------ app/src/v4l2_sink.c | 8 +++++--- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/app/src/recorder.c b/app/src/recorder.c index 9b646055..e8484256 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -473,9 +473,12 @@ sc_recorder_video_packet_sink_open(struct sc_packet_sink *sink, return false; } - stream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO; - stream->codecpar->codec_id = ctx->codec->id; - stream->codecpar->format = AV_PIX_FMT_YUV420P; + int r = avcodec_parameters_from_context(stream->codecpar, ctx); + if (r < 0) { + sc_mutex_unlock(&recorder->mutex); + return false; + } + stream->codecpar->width = recorder->declared_frame_size.width; stream->codecpar->height = recorder->declared_frame_size.height; @@ -554,15 +557,11 @@ sc_recorder_audio_packet_sink_open(struct sc_packet_sink *sink, return false; } - stream->codecpar->codec_type = AVMEDIA_TYPE_AUDIO; - stream->codecpar->codec_id = ctx->codec->id; -#ifdef SCRCPY_LAVU_HAS_CHLAYOUT - stream->codecpar->ch_layout.nb_channels = 2; -#else - stream->codecpar->channel_layout = AV_CH_LAYOUT_STEREO; - stream->codecpar->channels = 2; -#endif - stream->codecpar->sample_rate = 48000; + int r = avcodec_parameters_from_context(stream->codecpar, ctx); + if (r < 0) { + sc_mutex_unlock(&recorder->mutex); + return false; + } recorder->audio_stream_index = stream->index; diff --git a/app/src/v4l2_sink.c b/app/src/v4l2_sink.c index fe11614a..c6714d18 100644 --- a/app/src/v4l2_sink.c +++ b/app/src/v4l2_sink.c @@ -205,9 +205,11 @@ sc_v4l2_sink_open(struct sc_v4l2_sink *vs, const AVCodecContext *ctx) { goto error_avformat_free_context; } - ostream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO; - ostream->codecpar->codec_id = encoder->id; - ostream->codecpar->format = AV_PIX_FMT_YUV420P; + int r = avcodec_parameters_from_context(ostream->codecpar, ctx); + if (r < 0) { + goto error_avformat_free_context; + } + ostream->codecpar->width = vs->frame_size.width; ostream->codecpar->height = vs->frame_size.height; From aa1efbc35c468b1748b2ae4c542da443ab0e4714 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 10 Mar 2023 22:47:38 +0100 Subject: [PATCH 1470/2244] Rename sendCodecId to sendCodecMeta This will allow the codec header to contain more than the codec id. --- .../src/main/java/com/genymobile/scrcpy/Options.java | 10 +++++----- .../src/main/java/com/genymobile/scrcpy/Server.java | 12 ++++++------ .../main/java/com/genymobile/scrcpy/Streamer.java | 8 ++++---- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index bcf235ed..2a3de757 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -40,7 +40,7 @@ public class Options { private boolean sendDeviceMeta = true; // send device name and size private boolean sendFrameMeta = true; // send PTS so that the client may record properly private boolean sendDummyByte = true; // write a byte on start to detect connection issues - private boolean sendCodecId = true; // write the codec ID (4 bytes) before the stream + private boolean sendCodecMeta = true; // write the codec metadata before the stream public Ln.Level getLogLevel() { return logLevel; @@ -282,11 +282,11 @@ public class Options { this.sendDummyByte = sendDummyByte; } - public boolean getSendCodecId() { - return sendCodecId; + public boolean getSendCodecMeta() { + return sendCodecMeta; } - public void setSendCodecId(boolean sendCodecId) { - this.sendCodecId = sendCodecId; + public void setSendCodecMeta(boolean sendCodecMeta) { + this.sendCodecMeta = sendCodecMeta; } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 86555e3b..2ece7415 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -108,7 +108,7 @@ public final class Server { if (audio) { AudioCodec audioCodec = options.getAudioCodec(); - Streamer audioStreamer = new Streamer(connection.getAudioFd(), audioCodec, options.getSendCodecId(), + Streamer audioStreamer = new Streamer(connection.getAudioFd(), audioCodec, options.getSendCodecMeta(), options.getSendFrameMeta()); AsyncProcessor audioRecorder; if (audioCodec == AudioCodec.RAW) { @@ -120,7 +120,7 @@ public final class Server { asyncProcessors.add(audioRecorder); } - Streamer videoStreamer = new Streamer(connection.getVideoFd(), options.getVideoCodec(), options.getSendCodecId(), + Streamer videoStreamer = new Streamer(connection.getVideoFd(), options.getVideoCodec(), options.getSendCodecMeta(), options.getSendFrameMeta()); ScreenEncoder screenEncoder = new ScreenEncoder(device, videoStreamer, options.getVideoBitRate(), options.getMaxFps(), options.getVideoCodecOptions(), options.getVideoEncoder(), options.getDownsizeOnError()); @@ -315,9 +315,9 @@ public final class Server { boolean sendDummyByte = Boolean.parseBoolean(value); options.setSendDummyByte(sendDummyByte); break; - case "send_codec_id": - boolean sendCodecId = Boolean.parseBoolean(value); - options.setSendCodecId(sendCodecId); + case "send_codec_meta": + boolean sendCodecMeta = Boolean.parseBoolean(value); + options.setSendCodecMeta(sendCodecMeta); break; case "raw_video_stream": boolean rawVideoStream = Boolean.parseBoolean(value); @@ -325,7 +325,7 @@ public final class Server { options.setSendDeviceMeta(false); options.setSendFrameMeta(false); options.setSendDummyByte(false); - options.setSendCodecId(false); + options.setSendCodecMeta(false); } break; default: diff --git a/server/src/main/java/com/genymobile/scrcpy/Streamer.java b/server/src/main/java/com/genymobile/scrcpy/Streamer.java index 9bfe7e91..f099cf4f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Streamer.java +++ b/server/src/main/java/com/genymobile/scrcpy/Streamer.java @@ -15,15 +15,15 @@ public final class Streamer { private final FileDescriptor fd; private final Codec codec; - private final boolean sendCodecId; + private final boolean sendCodecMeta; private final boolean sendFrameMeta; private final ByteBuffer headerBuffer = ByteBuffer.allocate(12); - public Streamer(FileDescriptor fd, Codec codec, boolean sendCodecId, boolean sendFrameMeta) { + public Streamer(FileDescriptor fd, Codec codec, boolean sendCodecMeta, boolean sendFrameMeta) { this.fd = fd; this.codec = codec; - this.sendCodecId = sendCodecId; + this.sendCodecMeta = sendCodecMeta; this.sendFrameMeta = sendFrameMeta; } @@ -32,7 +32,7 @@ public final class Streamer { } public void writeHeader() throws IOException { - if (sendCodecId) { + if (sendCodecMeta) { ByteBuffer buffer = ByteBuffer.allocate(4); buffer.putInt(codec.getId()); buffer.flip(); From 3a72f3fb4da0dca0ab34824396c3efdf1819e5df Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 10 Mar 2023 22:49:40 +0100 Subject: [PATCH 1471/2244] Report errors on screen event error Make scrcpy fail if an important screen event (like frame update) fails. --- app/src/scrcpy.c | 4 +++- app/src/screen.c | 22 ++++++++++++---------- app/src/screen.h | 3 ++- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index ce045c97..09a8f918 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -175,7 +175,9 @@ event_loop(struct scrcpy *s) { LOGD("User requested to quit"); return SCRCPY_EXIT_SUCCESS; default: - sc_screen_handle_event(&s->screen, &event); + if (!sc_screen_handle_event(&s->screen, &event)) { + return SCRCPY_EXIT_FAILURE; + } break; } } diff --git a/app/src/screen.c b/app/src/screen.c index b814ada1..56463711 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -790,7 +790,7 @@ sc_screen_is_mouse_capture_key(SDL_Keycode key) { return key == SDLK_LALT || key == SDLK_LGUI || key == SDLK_RGUI; } -void +bool sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event) { bool relative_mode = sc_screen_is_relative_mode(screen); @@ -798,14 +798,15 @@ sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event) { case SC_EVENT_NEW_FRAME: { bool ok = sc_screen_update_frame(screen); if (!ok) { - LOGW("Frame update failed\n"); + LOGE("Frame update failed\n"); + return false; } - return; + return true; } case SDL_WINDOWEVENT: if (!screen->has_frame) { // Do nothing - return; + return true; } switch (event->window.event) { case SDL_WINDOWEVENT_EXPOSED: @@ -836,7 +837,7 @@ sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event) { } break; } - return; + return true; case SDL_KEYDOWN: if (relative_mode) { SDL_Keycode key = event->key.keysym.sym; @@ -849,7 +850,7 @@ sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event) { screen->mouse_capture_key_pressed = 0; } // Mouse capture keys are never forwarded to the device - return; + return true; } } break; @@ -865,7 +866,7 @@ sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event) { sc_screen_toggle_mouse_capture(screen); } // Mouse capture keys are never forwarded to the device - return; + return true; } } break; @@ -875,7 +876,7 @@ sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event) { if (relative_mode && !sc_screen_get_mouse_capture(screen)) { // Do not forward to input manager, the mouse will be captured // on SDL_MOUSEBUTTONUP - return; + return true; } break; case SDL_FINGERMOTION: @@ -884,18 +885,19 @@ sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event) { if (relative_mode) { // Touch events are not compatible with relative mode // (coordinates are not relative) - return; + return true; } break; case SDL_MOUSEBUTTONUP: if (relative_mode && !sc_screen_get_mouse_capture(screen)) { sc_screen_set_mouse_capture(screen, true); - return; + return true; } break; } sc_input_manager_handle_event(&screen->im, event); + return true; } struct sc_point diff --git a/app/src/screen.h b/app/src/screen.h index 28afea40..57927894 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -136,7 +136,8 @@ void sc_screen_set_rotation(struct sc_screen *screen, unsigned rotation); // react to SDL events -void +// If this function returns false, scrcpy must exit with an error. +bool sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event); // convert point from window coordinates to frame coordinates From 238ab872ba0ea18c2553dfe3c2202d64d67d8960 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 11 Mar 2023 09:21:49 +0100 Subject: [PATCH 1472/2244] Pass video size as codec metadata On initial connection, scrcpy sent some device metadata: - the device name (to be used as window title) - the initial video size (before any frame or even SPS/PPS) But it is better to provide the initial video size as part as the video stream, so that it can be demuxed and exposed via AVCodecContext to sinks. This avoids to pass an explicit "initial frame size" for the screen, the recorder and the v4l2 sink. --- app/src/demuxer.c | 24 +++++- app/src/events.h | 1 + app/src/recorder.c | 5 -- app/src/recorder.h | 2 - app/src/scrcpy.c | 6 +- app/src/screen.c | 79 +++++++++++++------ app/src/screen.h | 1 - app/src/server.c | 7 +- app/src/server.h | 1 - app/src/v4l2_sink.c | 12 +-- app/src/v4l2_sink.h | 4 +- .../com/genymobile/scrcpy/AudioEncoder.java | 2 +- .../genymobile/scrcpy/AudioRawRecorder.java | 2 +- .../genymobile/scrcpy/DesktopConnection.java | 8 +- .../com/genymobile/scrcpy/ScreenEncoder.java | 2 +- .../java/com/genymobile/scrcpy/Server.java | 3 +- .../java/com/genymobile/scrcpy/Streamer.java | 14 +++- 17 files changed, 104 insertions(+), 69 deletions(-) diff --git a/app/src/demuxer.c b/app/src/demuxer.c index eabcb81e..96303155 100644 --- a/app/src/demuxer.c +++ b/app/src/demuxer.c @@ -57,6 +57,20 @@ sc_demuxer_recv_codec_id(struct sc_demuxer *demuxer, uint32_t *codec_id) { return true; } +static bool +sc_demuxer_recv_video_size(struct sc_demuxer *demuxer, uint32_t *width, + uint32_t *height) { + uint8_t data[8]; + ssize_t r = net_recv_all(demuxer->socket, data, 8); + if (r < 8) { + return false; + } + + *width = sc_read32be(data); + *height = sc_read32be(data + 4); + return true; +} + static bool sc_demuxer_recv_packet(struct sc_demuxer *demuxer, AVPacket *packet) { // The video stream contains raw packets, without time information. When we @@ -169,7 +183,15 @@ run_demuxer(void *data) { codec_ctx->flags |= AV_CODEC_FLAG_LOW_DELAY; if (codec->type == AVMEDIA_TYPE_VIDEO) { - // Hardcoded video properties + uint32_t width; + uint32_t height; + ok = sc_demuxer_recv_video_size(demuxer, &width, &height); + if (!ok) { + goto finally_free_context; + } + + codec_ctx->width = width; + codec_ctx->height = height; codec_ctx->pix_fmt = AV_PIX_FMT_YUV420P; } else { // Hardcoded audio properties diff --git a/app/src/events.h b/app/src/events.h index 0a45b652..609e3198 100644 --- a/app/src/events.h +++ b/app/src/events.h @@ -5,3 +5,4 @@ #define SC_EVENT_USB_DEVICE_DISCONNECTED (SDL_USEREVENT + 4) #define SC_EVENT_DEMUXER_ERROR (SDL_USEREVENT + 5) #define SC_EVENT_RECORDER_ERROR (SDL_USEREVENT + 6) +#define SC_EVENT_SCREEN_INIT_SIZE (SDL_USEREVENT + 7) diff --git a/app/src/recorder.c b/app/src/recorder.c index e8484256..2fc95eca 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -479,9 +479,6 @@ sc_recorder_video_packet_sink_open(struct sc_packet_sink *sink, return false; } - stream->codecpar->width = recorder->declared_frame_size.width; - stream->codecpar->height = recorder->declared_frame_size.height; - recorder->video_stream_index = stream->index; recorder->video_init = true; @@ -643,7 +640,6 @@ sc_recorder_audio_packet_sink_disable(struct sc_packet_sink *sink) { bool sc_recorder_init(struct sc_recorder *recorder, const char *filename, enum sc_record_format format, bool audio, - struct sc_size declared_frame_size, const struct sc_recorder_callbacks *cbs, void *cbs_userdata) { recorder->filename = strdup(filename); if (!recorder->filename) { @@ -679,7 +675,6 @@ sc_recorder_init(struct sc_recorder *recorder, const char *filename, recorder->audio_stream_index = -1; recorder->format = format; - recorder->declared_frame_size = declared_frame_size; assert(cbs && cbs->on_ended); recorder->cbs = cbs; diff --git a/app/src/recorder.h b/app/src/recorder.h index 35758db7..41b8db65 100644 --- a/app/src/recorder.h +++ b/app/src/recorder.h @@ -31,7 +31,6 @@ struct sc_recorder { char *filename; enum sc_record_format format; AVFormatContext *ctx; - struct sc_size declared_frame_size; sc_thread thread; sc_mutex mutex; @@ -61,7 +60,6 @@ struct sc_recorder_callbacks { bool sc_recorder_init(struct sc_recorder *recorder, const char *filename, enum sc_record_format format, bool audio, - struct sc_size declared_frame_size, const struct sc_recorder_callbacks *cbs, void *cbs_userdata); bool diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 09a8f918..9e5ec6f0 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -473,7 +473,7 @@ scrcpy(struct scrcpy_options *options) { }; if (!sc_recorder_init(&s->recorder, options->record_filename, options->record_format, options->audio, - info->frame_size, &recorder_cbs, NULL)) { + &recorder_cbs, NULL)) { goto end; } recorder_initialized = true; @@ -660,7 +660,6 @@ aoa_hid_end: .clipboard_autosync = options->clipboard_autosync, .shortcut_mods = &options->shortcut_mods, .window_title = window_title, - .frame_size = info->frame_size, .always_on_top = options->always_on_top, .window_x = options->window_x, .window_y = options->window_y, @@ -697,8 +696,7 @@ aoa_hid_end: #ifdef HAVE_V4L2 if (options->v4l2_device) { - if (!sc_v4l2_sink_init(&s->v4l2_sink, options->v4l2_device, - info->frame_size)) { + if (!sc_v4l2_sink_init(&s->v4l2_sink, options->v4l2_device)) { goto end; } diff --git a/app/src/screen.c b/app/src/screen.c index 56463711..f74fd8a5 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -239,7 +239,7 @@ sc_screen_update_content_rect(struct sc_screen *screen) { } } -static inline SDL_Texture * +static bool create_texture(struct sc_screen *screen) { SDL_Renderer *renderer = screen->renderer; struct sc_size size = screen->frame_size; @@ -247,7 +247,8 @@ create_texture(struct sc_screen *screen) { SDL_TEXTUREACCESS_STREAMING, size.width, size.height); if (!texture) { - return NULL; + LOGE("Could not create texture: %s", SDL_GetError()); + return false; } if (screen->mipmaps) { @@ -263,7 +264,8 @@ create_texture(struct sc_screen *screen) { SDL_GL_UnbindTexture(texture); } - return texture; + screen->texture = texture; + return true; } // render the texture to the renderer @@ -335,7 +337,25 @@ sc_screen_frame_sink_open(struct sc_frame_sink *sink, (void) ctx; struct sc_screen *screen = DOWNCAST(sink); - (void) screen; + + assert(ctx->width > 0 && ctx->width <= 0xFFFF); + assert(ctx->height > 0 && ctx->height <= 0xFFFF); + // screen->frame_size is never used before the event is pushed, and the + // event acts as a memory barrier so it is safe without mutex + screen->frame_size.width = ctx->width; + screen->frame_size.height = ctx->height; + + static SDL_Event event = { + .type = SC_EVENT_SCREEN_INIT_SIZE, + }; + + // Post the event on the UI thread (the texture must be created from there) + int ret = SDL_PushEvent(&event); + if (ret < 0) { + LOGW("Could not post init size event: %s", SDL_GetError()); + return false; + } + #ifndef NDEBUG screen->open = true; #endif @@ -410,14 +430,10 @@ sc_screen_init(struct sc_screen *screen, goto error_destroy_frame_buffer; } - screen->frame_size = params->frame_size; screen->rotation = params->rotation; if (screen->rotation) { LOGI("Initial display rotation set to %u", screen->rotation); } - struct sc_size content_size = - get_rotated_size(screen->frame_size, screen->rotation); - screen->content_size = content_size; uint32_t window_flags = SDL_WINDOW_HIDDEN | SDL_WINDOW_RESIZABLE @@ -485,18 +501,10 @@ sc_screen_init(struct sc_screen *screen, LOGW("Could not load icon"); } - LOGI("Initial texture: %" PRIu16 "x%" PRIu16, params->frame_size.width, - params->frame_size.height); - screen->texture = create_texture(screen); - if (!screen->texture) { - LOGE("Could not create texture: %s", SDL_GetError()); - goto error_destroy_renderer; - } - screen->frame = av_frame_alloc(); if (!screen->frame) { LOG_OOM(); - goto error_destroy_texture; + goto error_destroy_renderer; } struct sc_input_manager_params im_params = { @@ -531,8 +539,6 @@ sc_screen_init(struct sc_screen *screen, return true; -error_destroy_texture: - SDL_DestroyTexture(screen->texture); error_destroy_renderer: SDL_DestroyRenderer(screen->renderer); error_destroy_window: @@ -591,7 +597,9 @@ sc_screen_destroy(struct sc_screen *screen) { assert(!screen->open); #endif av_frame_free(&screen->frame); - SDL_DestroyTexture(screen->texture); + if (screen->texture) { + SDL_DestroyTexture(screen->texture); + } SDL_DestroyRenderer(screen->renderer); SDL_DestroyWindow(screen->window); sc_fps_counter_destroy(&screen->fps_counter); @@ -655,6 +663,23 @@ sc_screen_set_rotation(struct sc_screen *screen, unsigned rotation) { sc_screen_render(screen, true); } +static bool +sc_screen_init_size(struct sc_screen *screen) { + // Before first frame + assert(!screen->has_frame); + assert(!screen->texture); + + // The requested size is passed via screen->frame_size + + struct sc_size content_size = + get_rotated_size(screen->frame_size, screen->rotation); + screen->content_size = content_size; + + LOGI("Initial texture: %" PRIu16 "x%" PRIu16, + screen->frame_size.width, screen->frame_size.height); + return create_texture(screen); +} + // recreate the texture and resize the window if the frame size has changed static bool prepare_for_frame(struct sc_screen *screen, struct sc_size new_frame_size) { @@ -673,11 +698,7 @@ prepare_for_frame(struct sc_screen *screen, struct sc_size new_frame_size) { LOGI("New texture: %" PRIu16 "x%" PRIu16, screen->frame_size.width, screen->frame_size.height); - screen->texture = create_texture(screen); - if (!screen->texture) { - LOGE("Could not create texture: %s", SDL_GetError()); - return false; - } + return create_texture(screen); } return true; @@ -795,6 +816,14 @@ sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event) { bool relative_mode = sc_screen_is_relative_mode(screen); switch (event->type) { + case SC_EVENT_SCREEN_INIT_SIZE: + // The initial size is passed via screen->frame_size + bool ok = sc_screen_init_size(screen); + if (!ok) { + LOGE("Could not initialize screen size"); + return false; + } + return true; case SC_EVENT_NEW_FRAME: { bool ok = sc_screen_update_frame(screen); if (!ok) { diff --git a/app/src/screen.h b/app/src/screen.h index 57927894..4fca04d8 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -78,7 +78,6 @@ struct sc_screen_params { const struct sc_shortcut_mods *shortcut_mods; const char *window_title; - struct sc_size frame_size; bool always_on_top; int16_t window_x; // accepts SC_WINDOW_POSITION_UNDEFINED diff --git a/app/src/server.c b/app/src/server.c index 7b503427..8c4e9a95 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -441,9 +441,9 @@ sc_server_init(struct sc_server *server, const struct sc_server_params *params, static bool device_read_info(struct sc_intr *intr, sc_socket device_socket, struct sc_server_info *info) { - unsigned char buf[SC_DEVICE_NAME_FIELD_LENGTH + 4]; + unsigned char buf[SC_DEVICE_NAME_FIELD_LENGTH]; ssize_t r = net_recv_all_intr(intr, device_socket, buf, sizeof(buf)); - if (r < SC_DEVICE_NAME_FIELD_LENGTH + 4) { + if (r < SC_DEVICE_NAME_FIELD_LENGTH) { LOGE("Could not retrieve device information"); return false; } @@ -451,9 +451,6 @@ device_read_info(struct sc_intr *intr, sc_socket device_socket, buf[SC_DEVICE_NAME_FIELD_LENGTH - 1] = '\0'; memcpy(info->device_name, (char *) buf, sizeof(info->device_name)); - unsigned char *fields = &buf[SC_DEVICE_NAME_FIELD_LENGTH]; - info->frame_size.width = sc_read16be(fields); - info->frame_size.height = sc_read16be(&fields[2]); return true; } diff --git a/app/src/server.h b/app/src/server.h index 8edf2666..c425856b 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -18,7 +18,6 @@ #define SC_DEVICE_NAME_FIELD_LENGTH 64 struct sc_server_info { char device_name[SC_DEVICE_NAME_FIELD_LENGTH]; - struct sc_size frame_size; }; struct sc_server_params { diff --git a/app/src/v4l2_sink.c b/app/src/v4l2_sink.c index c6714d18..717d2bd5 100644 --- a/app/src/v4l2_sink.c +++ b/app/src/v4l2_sink.c @@ -210,9 +210,6 @@ sc_v4l2_sink_open(struct sc_v4l2_sink *vs, const AVCodecContext *ctx) { goto error_avformat_free_context; } - ostream->codecpar->width = vs->frame_size.width; - ostream->codecpar->height = vs->frame_size.height; - int ret = avio_open(&vs->format_ctx->pb, vs->device_name, AVIO_FLAG_WRITE); if (ret < 0) { LOGE("Failed to open output device: %s", vs->device_name); @@ -226,8 +223,8 @@ sc_v4l2_sink_open(struct sc_v4l2_sink *vs, const AVCodecContext *ctx) { goto error_avio_close; } - vs->encoder_ctx->width = vs->frame_size.width; - vs->encoder_ctx->height = vs->frame_size.height; + vs->encoder_ctx->width = ctx->width; + vs->encoder_ctx->height = ctx->height; vs->encoder_ctx->pix_fmt = AV_PIX_FMT_YUV420P; vs->encoder_ctx->time_base.num = 1; vs->encoder_ctx->time_base.den = 1; @@ -343,16 +340,13 @@ sc_v4l2_frame_sink_push(struct sc_frame_sink *sink, const AVFrame *frame) { } bool -sc_v4l2_sink_init(struct sc_v4l2_sink *vs, const char *device_name, - struct sc_size frame_size) { +sc_v4l2_sink_init(struct sc_v4l2_sink *vs, const char *device_name) { vs->device_name = strdup(device_name); if (!vs->device_name) { LOGE("Could not strdup v4l2 device name"); return false; } - vs->frame_size = frame_size; - static const struct sc_frame_sink_ops ops = { .open = sc_v4l2_frame_sink_open, .close = sc_v4l2_frame_sink_close, diff --git a/app/src/v4l2_sink.h b/app/src/v4l2_sink.h index 789e31c3..365a739d 100644 --- a/app/src/v4l2_sink.h +++ b/app/src/v4l2_sink.h @@ -19,7 +19,6 @@ struct sc_v4l2_sink { AVCodecContext *encoder_ctx; char *device_name; - struct sc_size frame_size; sc_thread thread; sc_mutex mutex; @@ -33,8 +32,7 @@ struct sc_v4l2_sink { }; bool -sc_v4l2_sink_init(struct sc_v4l2_sink *vs, const char *device_name, - struct sc_size frame_size); +sc_v4l2_sink_init(struct sc_v4l2_sink *vs, const char *device_name); void sc_v4l2_sink_destroy(struct sc_v4l2_sink *vs); diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java b/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java index 0ba424ca..24d685c5 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java @@ -101,7 +101,7 @@ public final class AudioEncoder implements AsyncProcessor { } private void outputThread(MediaCodec mediaCodec) throws IOException, InterruptedException { - streamer.writeHeader(); + streamer.writeAudioHeader(); while (!Thread.currentThread().isInterrupted()) { OutputTask task = outputTasks.take(); diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java b/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java index 2e483daa..4b1b5bd0 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java @@ -26,7 +26,7 @@ public final class AudioRawRecorder implements AsyncProcessor { try { capture.start(); - streamer.writeHeader(); + streamer.writeAudioHeader(); while (!Thread.currentThread().isInterrupted()) { buffer.position(0); int r = capture.read(buffer, READ_SIZE, bufferInfo); diff --git a/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java b/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java index 3e743621..4bfff726 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java +++ b/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java @@ -122,18 +122,14 @@ public final class DesktopConnection implements Closeable { } } - public void sendDeviceMeta(String deviceName, int width, int height) throws IOException { - byte[] buffer = new byte[DEVICE_NAME_FIELD_LENGTH + 4]; + public void sendDeviceMeta(String deviceName) throws IOException { + byte[] buffer = new byte[DEVICE_NAME_FIELD_LENGTH]; byte[] deviceNameBytes = deviceName.getBytes(StandardCharsets.UTF_8); int len = StringUtils.getUtf8TruncationIndex(deviceNameBytes, DEVICE_NAME_FIELD_LENGTH - 1); System.arraycopy(deviceNameBytes, 0, buffer, 0, len); // byte[] are always 0-initialized in java, no need to set '\0' explicitly - buffer[DEVICE_NAME_FIELD_LENGTH] = (byte) (width >> 8); - buffer[DEVICE_NAME_FIELD_LENGTH + 1] = (byte) width; - buffer[DEVICE_NAME_FIELD_LENGTH + 2] = (byte) (height >> 8); - buffer[DEVICE_NAME_FIELD_LENGTH + 3] = (byte) height; IO.writeFully(videoFd, buffer, 0, buffer.length); } diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index f5f996ba..015cc993 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -66,7 +66,7 @@ public class ScreenEncoder implements Device.RotationListener { IBinder display = createDisplay(); device.setRotationListener(this); - streamer.writeHeader(); + streamer.writeVideoHeader(device.getScreenInfo().getVideoSize()); boolean alive; try { diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 2ece7415..5800487d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -96,8 +96,7 @@ public final class Server { try (DesktopConnection connection = DesktopConnection.open(scid, tunnelForward, audio, control, sendDummyByte)) { if (options.getSendDeviceMeta()) { - Size videoSize = device.getScreenInfo().getVideoSize(); - connection.sendDeviceMeta(Device.getDeviceName(), videoSize.getWidth(), videoSize.getHeight()); + connection.sendDeviceMeta(Device.getDeviceName()); } if (control) { diff --git a/server/src/main/java/com/genymobile/scrcpy/Streamer.java b/server/src/main/java/com/genymobile/scrcpy/Streamer.java index f099cf4f..39f74fb6 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Streamer.java +++ b/server/src/main/java/com/genymobile/scrcpy/Streamer.java @@ -30,8 +30,7 @@ public final class Streamer { public Codec getCodec() { return codec; } - - public void writeHeader() throws IOException { + public void writeAudioHeader() throws IOException { if (sendCodecMeta) { ByteBuffer buffer = ByteBuffer.allocate(4); buffer.putInt(codec.getId()); @@ -40,6 +39,17 @@ public final class Streamer { } } + public void writeVideoHeader(Size videoSize) throws IOException { + if (sendCodecMeta) { + ByteBuffer buffer = ByteBuffer.allocate(12); + buffer.putInt(codec.getId()); + buffer.putInt(videoSize.getWidth()); + buffer.putInt(videoSize.getHeight()); + buffer.flip(); + IO.writeFully(fd, buffer); + } + } + public void writeDisableStream(boolean error) throws IOException { // Writing a specific code as codec-id means that the device disables the stream // code 0: it explicitly disables the stream (because it could not capture audio), scrcpy should continue mirroring video only From bb509d9317ec073cd650f9ae9796a24b4cfff34c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 11 Mar 2023 09:31:32 +0100 Subject: [PATCH 1473/2244] Define the audio output buffer in milliseconds In theory, this buffer must be dimensioned for a target duration, so its size in bytes should depend on the sample rate. --- app/src/audio_player.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/audio_player.c b/app/src/audio_player.c index 7a348f93..a4a73f8d 100644 --- a/app/src/audio_player.c +++ b/app/src/audio_player.c @@ -13,7 +13,7 @@ #define SC_AV_SAMPLE_FMT AV_SAMPLE_FMT_FLT #define SC_SDL_SAMPLE_FMT AUDIO_F32 -#define SC_AUDIO_OUTPUT_BUFFER_SAMPLES 240 // 5ms at 48000Hz +#define SC_AUDIO_OUTPUT_BUFFER_MS 5 static inline uint32_t bytes_to_samples(struct sc_audio_player *ap, size_t bytes) { @@ -202,8 +202,8 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink, bool played = ap->played; if (played) { uint32_t max_buffered_samples = ap->target_buffering - + 12 * SC_AUDIO_OUTPUT_BUFFER_SAMPLES - + ap->target_buffering / 10; + + 12 * SC_AUDIO_OUTPUT_BUFFER_MS * ap->sample_rate / 1000 + + ap->target_buffering / 10; if (buffered_samples > max_buffered_samples) { uint32_t skip_samples = buffered_samples - max_buffered_samples; size_t skip_bytes = samples_to_bytes(ap, skip_samples); @@ -231,7 +231,7 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink, // max_initial_buffering samples, this would cause unnecessary delay // (and glitches to compensate) on start. uint32_t max_initial_buffering = ap->target_buffering - + 2 * SC_AUDIO_OUTPUT_BUFFER_SAMPLES; + + 2 * SC_AUDIO_OUTPUT_BUFFER_MS * ap->sample_rate / 1000; if (buffered_samples > max_initial_buffering) { uint32_t skip_samples = buffered_samples - max_initial_buffering; size_t skip_bytes = samples_to_bytes(ap, skip_samples); @@ -298,7 +298,7 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink, .freq = ctx->sample_rate, .format = SC_SDL_SAMPLE_FMT, .channels = nb_channels, - .samples = SC_AUDIO_OUTPUT_BUFFER_SAMPLES, + .samples = SC_AUDIO_OUTPUT_BUFFER_MS * ctx->sample_rate / 1000, .callback = sc_audio_player_sdl_callback, .userdata = ap, }; From 14f9d82fdad4bce7887a65603946e43e1a8318e3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 11 Mar 2023 10:13:00 +0100 Subject: [PATCH 1474/2244] Add audio sample ring-buffer Add a thin wrapper around bytebuf to handle samples instead of bytes. This simplifies the audio player, which mostly handles samples. --- app/src/audio_player.c | 120 ++++++++++++++++------------------------ app/src/audio_player.h | 10 ++-- app/src/util/audiobuf.h | 94 +++++++++++++++++++++++++++++++ 3 files changed, 148 insertions(+), 76 deletions(-) create mode 100644 app/src/util/audiobuf.h diff --git a/app/src/audio_player.c b/app/src/audio_player.c index a4a73f8d..320af082 100644 --- a/app/src/audio_player.c +++ b/app/src/audio_player.c @@ -15,42 +15,32 @@ #define SC_AUDIO_OUTPUT_BUFFER_MS 5 -static inline uint32_t -bytes_to_samples(struct sc_audio_player *ap, size_t bytes) { - assert(bytes % (ap->nb_channels * ap->out_bytes_per_sample) == 0); - return bytes / (ap->nb_channels * ap->out_bytes_per_sample); -} - -static inline size_t -samples_to_bytes(struct sc_audio_player *ap, uint32_t samples) { - return samples * ap->nb_channels * ap->out_bytes_per_sample; -} +#define TO_BYTES(SAMPLES) sc_audiobuf_to_bytes(&ap->buf, (SAMPLES)) +#define TO_SAMPLES(BYTES) sc_audiobuf_to_samples(&ap->buf, (BYTES)) static void SDLCALL sc_audio_player_sdl_callback(void *userdata, uint8_t *stream, int len_int) { struct sc_audio_player *ap = userdata; // This callback is called with the lock used by SDL_AudioDeviceLock(), so - // the bytebuf is protected + // the audiobuf is protected assert(len_int > 0); size_t len = len_int; + uint32_t count = TO_SAMPLES(len); #ifndef SC_AUDIO_PLAYER_NDEBUG - LOGD("[Audio] SDL callback requests %" PRIu32 " samples", - bytes_to_samples(ap, len)); + LOGD("[Audio] SDL callback requests %" PRIu32 " samples", count); #endif - size_t read_avail = sc_bytebuf_read_available(&ap->buf); + uint32_t buffered_samples = sc_audiobuf_read_available(&ap->buf); if (!ap->played) { - uint32_t buffered_samples = bytes_to_samples(ap, read_avail); - // Part of the buffering is handled by inserting initial silence. The // remaining (margin) last samples will be handled by compensation. uint32_t margin = 30 * ap->sample_rate / 1000; // 30ms if (buffered_samples + margin < ap->target_buffering) { LOGV("[Audio] Inserting initial buffering silence: %" PRIu32 - " samples", bytes_to_samples(ap, len)); + " samples", count); // Delay playback starting to reach the target buffering. Fill the // whole buffer with silence (len is small compared to the // arbitrary margin value). @@ -59,26 +49,25 @@ sc_audio_player_sdl_callback(void *userdata, uint8_t *stream, int len_int) { } } - size_t read = MIN(read_avail, len); + uint32_t read = MIN(buffered_samples, count); if (read) { - sc_bytebuf_read(&ap->buf, stream, read); + sc_audiobuf_read(&ap->buf, stream, read); } - if (read < len) { - size_t silence_bytes = len - read; - uint32_t silence_samples = bytes_to_samples(ap, silence_bytes); + if (read < count) { + uint32_t silence = count - read; // Insert silence. In theory, the inserted silent samples replace the // missing real samples, which will arrive later, so they should be // dropped to keep the latency minimal. However, this would cause very // audible glitches, so let the clock compensation restore the target // latency. LOGD("[Audio] Buffer underflow, inserting silence: %" PRIu32 " samples", - silence_samples); - memset(stream + read, 0, silence_bytes); + silence); + memset(stream + read, 0, TO_BYTES(silence)); if (ap->received) { // Inserting additional samples immediately increases buffering - ap->avg_buffering.avg += silence_samples; + ap->avg_buffering.avg += silence; } } @@ -87,7 +76,7 @@ sc_audio_player_sdl_callback(void *userdata, uint8_t *stream, int len_int) { static uint8_t * sc_audio_player_get_swr_buf(struct sc_audio_player *ap, uint32_t min_samples) { - size_t min_buf_size = samples_to_bytes(ap, min_samples); + size_t min_buf_size = TO_BYTES(min_samples); if (min_buf_size > ap->swr_buf_alloc_size) { size_t new_size = min_buf_size + 4096; uint8_t *buf = realloc(ap->swr_buf, new_size); @@ -130,7 +119,6 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink, // swr_convert() returns the number of samples which would have been // written if the buffer was big enough. uint32_t samples_written = MIN(ret, dst_nb_samples); - size_t swr_buf_size = samples_to_bytes(ap, samples_written); #ifndef SC_AUDIO_PLAYER_NDEBUG LOGD("[Audio] %" PRIu32 " samples written to buffer", samples_written); #endif @@ -138,46 +126,40 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink, // Since this function is the only writer, the current available space is // at least the previous available space. In practice, it should almost // always be possible to write without lock. - bool lockless_write = swr_buf_size <= ap->previous_write_avail; + bool lockless_write = samples_written <= ap->previous_write_avail; if (lockless_write) { - sc_bytebuf_prepare_write(&ap->buf, swr_buf, swr_buf_size); + sc_audiobuf_prepare_write(&ap->buf, swr_buf, samples_written); } SDL_LockAudioDevice(ap->device); - size_t read_avail = sc_bytebuf_read_available(&ap->buf); - uint32_t buffered_samples = bytes_to_samples(ap, read_avail); + uint32_t buffered_samples = sc_audiobuf_read_available(&ap->buf); if (lockless_write) { - sc_bytebuf_commit_write(&ap->buf, swr_buf_size); + sc_audiobuf_commit_write(&ap->buf, samples_written); } else { - // Take care to keep full samples - size_t align = ap->nb_channels * ap->out_bytes_per_sample; - size_t write_avail = - sc_bytebuf_write_available(&ap->buf) / align * align; - if (swr_buf_size > write_avail) { - // Entering this branch is very unlikely, the ring-buffer (bytebuf) - // is allocated with a size sufficient to store 1 second more than - // the target buffering. If this happens, though, we have to skip - // old samples. - size_t cap = sc_bytebuf_capacity(&ap->buf) / align * align; - if (swr_buf_size > cap) { + uint32_t write_avail = sc_audiobuf_write_available(&ap->buf); + if (samples_written > write_avail) { + // Entering this branch is very unlikely, the audio buffer is + // allocated with a size sufficient to store 1 second more than the + // target buffering. If this happens, though, we have to skip old + // samples. + uint32_t cap = sc_audiobuf_capacity(&ap->buf); + if (samples_written > cap) { // Very very unlikely: a single resampled frame should never - // exceed the ring-buffer size (or something is very wrong). + // exceed the audio buffer size (or something is very wrong). // Ignore the first bytes in swr_buf - swr_buf += swr_buf_size - cap; - swr_buf_size = cap; + swr_buf += TO_BYTES(samples_written - cap); // This change in samples_written will impact the // instant_compensation below - samples_written -= bytes_to_samples(ap, swr_buf_size - cap); + samples_written = cap; } - assert(swr_buf_size >= write_avail); - if (swr_buf_size > write_avail) { - sc_bytebuf_skip(&ap->buf, swr_buf_size - write_avail); - uint32_t skip_samples = - bytes_to_samples(ap, swr_buf_size - write_avail); + assert(samples_written >= write_avail); + if (samples_written > write_avail) { + uint32_t skip_samples = samples_written - write_avail; assert(buffered_samples >= skip_samples); + sc_audiobuf_skip(&ap->buf, skip_samples); buffered_samples -= skip_samples; if (ap->played) { // Dropping input samples instantly decreases buffering @@ -187,16 +169,14 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink, // It should remain exactly the expected size to write the new // samples. - assert((sc_bytebuf_write_available(&ap->buf) / align * align) - == swr_buf_size); + assert(sc_audiobuf_write_available(&ap->buf) == samples_written); } - sc_bytebuf_write(&ap->buf, swr_buf, swr_buf_size); + sc_audiobuf_write(&ap->buf, swr_buf, samples_written); } buffered_samples += samples_written; - assert(samples_to_bytes(ap, buffered_samples) - == sc_bytebuf_read_available(&ap->buf)); + assert(buffered_samples == sc_audiobuf_read_available(&ap->buf)); // Read with lock held, to be used after unlocking bool played = ap->played; @@ -206,8 +186,7 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink, + ap->target_buffering / 10; if (buffered_samples > max_buffered_samples) { uint32_t skip_samples = buffered_samples - max_buffered_samples; - size_t skip_bytes = samples_to_bytes(ap, skip_samples); - sc_bytebuf_skip(&ap->buf, skip_bytes); + sc_audiobuf_skip(&ap->buf, skip_samples); LOGD("[Audio] Buffering threshold exceeded, skipping %" PRIu32 " samples", skip_samples); } @@ -234,8 +213,7 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink, + 2 * SC_AUDIO_OUTPUT_BUFFER_MS * ap->sample_rate / 1000; if (buffered_samples > max_initial_buffering) { uint32_t skip_samples = buffered_samples - max_initial_buffering; - size_t skip_bytes = samples_to_bytes(ap, skip_samples); - sc_bytebuf_skip(&ap->buf, skip_bytes); + sc_audiobuf_skip(&ap->buf, skip_samples); #ifndef SC_AUDIO_PLAYER_NDEBUG LOGD("[Audio] Playback not started, skipping %" PRIu32 " samples", skip_samples); @@ -243,7 +221,7 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink, } } - ap->previous_write_avail = sc_bytebuf_write_available(&ap->buf); + ap->previous_write_avail = sc_audiobuf_write_available(&ap->buf); ap->received = true; SDL_UnlockAudioDevice(ap->device); @@ -355,23 +333,23 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink, // producer and the consumer. It's too big on purpose, to guarantee that // the producer and the consumer will be able to access it in parallel // without locking. - size_t bytebuf_samples = ap->target_buffering + ap->sample_rate; - size_t bytebuf_size = samples_to_bytes(ap, bytebuf_samples); + size_t audiobuf_samples = ap->target_buffering + ap->sample_rate; - bool ok = sc_bytebuf_init(&ap->buf, bytebuf_size); + size_t sample_size = ap->nb_channels * ap->out_bytes_per_sample; + bool ok = sc_audiobuf_init(&ap->buf, sample_size, audiobuf_samples); if (!ok) { goto error_free_swr_ctx; } - size_t initial_swr_buf_size = samples_to_bytes(ap, 4096); + size_t initial_swr_buf_size = TO_BYTES(4096); ap->swr_buf = malloc(initial_swr_buf_size); if (!ap->swr_buf) { LOG_OOM(); - goto error_destroy_bytebuf; + goto error_destroy_audiobuf; } ap->swr_buf_alloc_size = initial_swr_buf_size; - ap->previous_write_avail = sc_bytebuf_write_available(&ap->buf); + ap->previous_write_avail = sc_audiobuf_write_available(&ap->buf); // Samples are produced and consumed by blocks, so the buffering must be // smoothed to get a relatively stable value. @@ -393,8 +371,8 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink, return true; -error_destroy_bytebuf: - sc_bytebuf_destroy(&ap->buf); +error_destroy_audiobuf: + sc_audiobuf_destroy(&ap->buf); error_free_swr_ctx: swr_free(&ap->swr_ctx); error_close_audio_device: @@ -412,7 +390,7 @@ sc_audio_player_frame_sink_close(struct sc_frame_sink *sink) { SDL_CloseAudioDevice(ap->device); free(ap->swr_buf); - sc_bytebuf_destroy(&ap->buf); + sc_audiobuf_destroy(&ap->buf); swr_free(&ap->swr_ctx); } diff --git a/app/src/audio_player.h b/app/src/audio_player.h index c64760ec..e82735c2 100644 --- a/app/src/audio_player.h +++ b/app/src/audio_player.h @@ -5,8 +5,8 @@ #include #include "trait/frame_sink.h" +#include #include -#include #include #include @@ -29,11 +29,11 @@ struct sc_audio_player { // Audio buffer to communicate between the receiver and the SDL audio // callback (protected by SDL_AudioDeviceLock()) - struct sc_bytebuf buf; + struct sc_audiobuf buf; - // The previous number of bytes available in the buffer (only used by the - // receiver thread) - size_t previous_write_avail; + // The previous empty space in the buffer (only used by the receiver + // thread) + uint32_t previous_write_avail; // Resampler (only used from the receiver thread) struct SwrContext *swr_ctx; diff --git a/app/src/util/audiobuf.h b/app/src/util/audiobuf.h new file mode 100644 index 00000000..16bf20d3 --- /dev/null +++ b/app/src/util/audiobuf.h @@ -0,0 +1,94 @@ +#ifndef SC_AUDIOBUF_H +#define SC_AUDIOBUF_H + +#include "common.h" + +#include +#include + +#include "util/bytebuf.h" + +/** + * Wrapper around bytebuf to read and write samples + * + * Each sample takes sample_size bytes. + */ +struct sc_audiobuf { + struct sc_bytebuf buf; + size_t sample_size; +}; + +static inline uint32_t +sc_audiobuf_to_samples(struct sc_audiobuf *buf, size_t bytes) { + assert(bytes % buf->sample_size == 0); + return bytes / buf->sample_size; +} + +static inline size_t +sc_audiobuf_to_bytes(struct sc_audiobuf *buf, uint32_t samples) { + return samples * buf->sample_size; +} + +static inline bool +sc_audiobuf_init(struct sc_audiobuf *buf, size_t sample_size, + uint32_t capacity) { + buf->sample_size = sample_size; + return sc_bytebuf_init(&buf->buf, capacity * sample_size + 1); +} + +static inline void +sc_audiobuf_read(struct sc_audiobuf *buf, uint8_t *to, uint32_t samples) { + size_t bytes = sc_audiobuf_to_bytes(buf, samples); + sc_bytebuf_read(&buf->buf, to, bytes); +} + +static inline void +sc_audiobuf_skip(struct sc_audiobuf *buf, uint32_t samples) { + size_t bytes = sc_audiobuf_to_bytes(buf, samples); + sc_bytebuf_skip(&buf->buf, bytes); +} + +static inline void +sc_audiobuf_write(struct sc_audiobuf *buf, const uint8_t *from, + uint32_t samples) { + size_t bytes = sc_audiobuf_to_bytes(buf, samples); + sc_bytebuf_write(&buf->buf, from, bytes); +} + +static inline void +sc_audiobuf_prepare_write(struct sc_audiobuf *buf, const uint8_t *from, + uint32_t samples) { + size_t bytes = sc_audiobuf_to_bytes(buf, samples); + sc_bytebuf_prepare_write(&buf->buf, from, bytes); +} + +static inline void +sc_audiobuf_commit_write(struct sc_audiobuf *buf, uint32_t samples) { + size_t bytes = sc_audiobuf_to_bytes(buf, samples); + sc_bytebuf_commit_write(&buf->buf, bytes); +} + +static inline uint32_t +sc_audiobuf_read_available(struct sc_audiobuf *buf) { + size_t bytes = sc_bytebuf_read_available(&buf->buf); + return sc_audiobuf_to_samples(buf, bytes); +} + +static inline uint32_t +sc_audiobuf_write_available(struct sc_audiobuf *buf) { + size_t bytes = sc_bytebuf_write_available(&buf->buf); + return sc_audiobuf_to_samples(buf, bytes); +} + +static inline uint32_t +sc_audiobuf_capacity(struct sc_audiobuf *buf) { + size_t bytes = sc_bytebuf_capacity(&buf->buf); + return sc_audiobuf_to_samples(buf, bytes); +} + +static inline void +sc_audiobuf_destroy(struct sc_audiobuf *buf) { + sc_bytebuf_destroy(&buf->buf); +} + +#endif From e06acc1ba23b7a2dd6a822d04e51a031e629aa35 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 11 Mar 2023 10:16:43 +0100 Subject: [PATCH 1475/2244] Simplify bytebuf naming Rename read_available to can_read and write_available to can_write. This is more readable. --- app/src/audio_player.c | 24 ++++++++++----------- app/src/audio_player.h | 2 +- app/src/util/audiobuf.h | 8 +++---- app/src/util/bytebuf.c | 8 +++---- app/src/util/bytebuf.h | 6 +++--- app/tests/test_bytebuf.c | 46 ++++++++++++++++++++-------------------- 6 files changed, 47 insertions(+), 47 deletions(-) diff --git a/app/src/audio_player.c b/app/src/audio_player.c index 320af082..652711c6 100644 --- a/app/src/audio_player.c +++ b/app/src/audio_player.c @@ -33,7 +33,7 @@ sc_audio_player_sdl_callback(void *userdata, uint8_t *stream, int len_int) { LOGD("[Audio] SDL callback requests %" PRIu32 " samples", count); #endif - uint32_t buffered_samples = sc_audiobuf_read_available(&ap->buf); + uint32_t buffered_samples = sc_audiobuf_can_read(&ap->buf); if (!ap->played) { // Part of the buffering is handled by inserting initial silence. The // remaining (margin) last samples will be handled by compensation. @@ -126,20 +126,20 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink, // Since this function is the only writer, the current available space is // at least the previous available space. In practice, it should almost // always be possible to write without lock. - bool lockless_write = samples_written <= ap->previous_write_avail; + bool lockless_write = samples_written <= ap->previous_can_write; if (lockless_write) { sc_audiobuf_prepare_write(&ap->buf, swr_buf, samples_written); } SDL_LockAudioDevice(ap->device); - uint32_t buffered_samples = sc_audiobuf_read_available(&ap->buf); + uint32_t buffered_samples = sc_audiobuf_can_read(&ap->buf); if (lockless_write) { sc_audiobuf_commit_write(&ap->buf, samples_written); } else { - uint32_t write_avail = sc_audiobuf_write_available(&ap->buf); - if (samples_written > write_avail) { + uint32_t can_write = sc_audiobuf_can_write(&ap->buf); + if (samples_written > can_write) { // Entering this branch is very unlikely, the audio buffer is // allocated with a size sufficient to store 1 second more than the // target buffering. If this happens, though, we have to skip old @@ -155,9 +155,9 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink, samples_written = cap; } - assert(samples_written >= write_avail); - if (samples_written > write_avail) { - uint32_t skip_samples = samples_written - write_avail; + assert(samples_written >= can_write); + if (samples_written > can_write) { + uint32_t skip_samples = samples_written - can_write; assert(buffered_samples >= skip_samples); sc_audiobuf_skip(&ap->buf, skip_samples); buffered_samples -= skip_samples; @@ -169,14 +169,14 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink, // It should remain exactly the expected size to write the new // samples. - assert(sc_audiobuf_write_available(&ap->buf) == samples_written); + assert(sc_audiobuf_can_write(&ap->buf) == samples_written); } sc_audiobuf_write(&ap->buf, swr_buf, samples_written); } buffered_samples += samples_written; - assert(buffered_samples == sc_audiobuf_read_available(&ap->buf)); + assert(buffered_samples == sc_audiobuf_can_read(&ap->buf)); // Read with lock held, to be used after unlocking bool played = ap->played; @@ -221,7 +221,7 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink, } } - ap->previous_write_avail = sc_audiobuf_write_available(&ap->buf); + ap->previous_can_write = sc_audiobuf_can_write(&ap->buf); ap->received = true; SDL_UnlockAudioDevice(ap->device); @@ -349,7 +349,7 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink, } ap->swr_buf_alloc_size = initial_swr_buf_size; - ap->previous_write_avail = sc_audiobuf_write_available(&ap->buf); + ap->previous_can_write = sc_audiobuf_can_write(&ap->buf); // Samples are produced and consumed by blocks, so the buffering must be // smoothed to get a relatively stable value. diff --git a/app/src/audio_player.h b/app/src/audio_player.h index e82735c2..f4670939 100644 --- a/app/src/audio_player.h +++ b/app/src/audio_player.h @@ -33,7 +33,7 @@ struct sc_audio_player { // The previous empty space in the buffer (only used by the receiver // thread) - uint32_t previous_write_avail; + uint32_t previous_can_write; // Resampler (only used from the receiver thread) struct SwrContext *swr_ctx; diff --git a/app/src/util/audiobuf.h b/app/src/util/audiobuf.h index 16bf20d3..8616d539 100644 --- a/app/src/util/audiobuf.h +++ b/app/src/util/audiobuf.h @@ -69,14 +69,14 @@ sc_audiobuf_commit_write(struct sc_audiobuf *buf, uint32_t samples) { } static inline uint32_t -sc_audiobuf_read_available(struct sc_audiobuf *buf) { - size_t bytes = sc_bytebuf_read_available(&buf->buf); +sc_audiobuf_can_read(struct sc_audiobuf *buf) { + size_t bytes = sc_bytebuf_can_read(&buf->buf); return sc_audiobuf_to_samples(buf, bytes); } static inline uint32_t -sc_audiobuf_write_available(struct sc_audiobuf *buf) { - size_t bytes = sc_bytebuf_write_available(&buf->buf); +sc_audiobuf_can_write(struct sc_audiobuf *buf) { + size_t bytes = sc_bytebuf_can_write(&buf->buf); return sc_audiobuf_to_samples(buf, bytes); } diff --git a/app/src/util/bytebuf.c b/app/src/util/bytebuf.c index eac69e9c..93544d72 100644 --- a/app/src/util/bytebuf.c +++ b/app/src/util/bytebuf.c @@ -30,7 +30,7 @@ sc_bytebuf_destroy(struct sc_bytebuf *buf) { void sc_bytebuf_read(struct sc_bytebuf *buf, uint8_t *to, size_t len) { assert(len); - assert(len <= sc_bytebuf_read_available(buf)); + assert(len <= sc_bytebuf_can_read(buf)); assert(buf->tail != buf->head); // the buffer could not be empty size_t right_limit = buf->tail < buf->head ? buf->head : buf->alloc_size; @@ -50,7 +50,7 @@ sc_bytebuf_read(struct sc_bytebuf *buf, uint8_t *to, size_t len) { void sc_bytebuf_skip(struct sc_bytebuf *buf, size_t len) { assert(len); - assert(len <= sc_bytebuf_read_available(buf)); + assert(len <= sc_bytebuf_can_read(buf)); assert(buf->tail != buf->head); // the buffer could not be empty buf->tail = (buf->tail + len) % buf->alloc_size; @@ -78,7 +78,7 @@ sc_bytebuf_write_step1(struct sc_bytebuf *buf, size_t len) { void sc_bytebuf_write(struct sc_bytebuf *buf, const uint8_t *from, size_t len) { assert(len); - assert(len <= sc_bytebuf_write_available(buf)); + assert(len <= sc_bytebuf_can_write(buf)); sc_bytebuf_write_step0(buf, from, len); sc_bytebuf_write_step1(buf, len); @@ -99,6 +99,6 @@ sc_bytebuf_prepare_write(struct sc_bytebuf *buf, const uint8_t *from, void sc_bytebuf_commit_write(struct sc_bytebuf *buf, size_t len) { - assert(len <= sc_bytebuf_write_available(buf)); + assert(len <= sc_bytebuf_can_write(buf)); sc_bytebuf_write_step1(buf, len); } diff --git a/app/src/util/bytebuf.h b/app/src/util/bytebuf.h index e8279ef8..1448f752 100644 --- a/app/src/util/bytebuf.h +++ b/app/src/util/bytebuf.h @@ -86,7 +86,7 @@ sc_bytebuf_commit_write(struct sc_bytebuf *buf, size_t len); * It is an error to read more bytes than available. */ static inline size_t -sc_bytebuf_read_available(struct sc_bytebuf *buf) { +sc_bytebuf_can_read(struct sc_bytebuf *buf) { return (buf->alloc_size + buf->head - buf->tail) % buf->alloc_size; } @@ -96,12 +96,12 @@ sc_bytebuf_read_available(struct sc_bytebuf *buf) { * It is an error to write more bytes than available. */ static inline size_t -sc_bytebuf_write_available(struct sc_bytebuf *buf) { +sc_bytebuf_can_write(struct sc_bytebuf *buf) { return (buf->alloc_size + buf->tail - buf->head - 1) % buf->alloc_size; } /** - * Return the actual capacity of the buffer (read available + write available) + * Return the actual capacity of the buffer (can_read() + can_write()) */ static inline size_t sc_bytebuf_capacity(struct sc_bytebuf *buf) { diff --git a/app/tests/test_bytebuf.c b/app/tests/test_bytebuf.c index 75af3073..c85e79ec 100644 --- a/app/tests/test_bytebuf.c +++ b/app/tests/test_bytebuf.c @@ -13,23 +13,23 @@ void test_bytebuf_simple(void) { assert(ok); sc_bytebuf_write(&buf, (uint8_t *) "hello", sizeof("hello") - 1); - assert(sc_bytebuf_read_available(&buf) == 5); + assert(sc_bytebuf_can_read(&buf) == 5); sc_bytebuf_read(&buf, data, 4); assert(!strncmp((char *) data, "hell", 4)); sc_bytebuf_write(&buf, (uint8_t *) " world", sizeof(" world") - 1); - assert(sc_bytebuf_read_available(&buf) == 7); + assert(sc_bytebuf_can_read(&buf) == 7); sc_bytebuf_write(&buf, (uint8_t *) "!", 1); - assert(sc_bytebuf_read_available(&buf) == 8); + assert(sc_bytebuf_can_read(&buf) == 8); sc_bytebuf_read(&buf, &data[4], 8); - assert(sc_bytebuf_read_available(&buf) == 0); + assert(sc_bytebuf_can_read(&buf) == 0); data[12] = '\0'; assert(!strcmp((char *) data, "hello world!")); - assert(sc_bytebuf_read_available(&buf) == 0); + assert(sc_bytebuf_can_read(&buf) == 0); sc_bytebuf_destroy(&buf); } @@ -42,31 +42,31 @@ void test_bytebuf_boundaries(void) { assert(ok); sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1); - assert(sc_bytebuf_read_available(&buf) == 6); + assert(sc_bytebuf_can_read(&buf) == 6); sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1); - assert(sc_bytebuf_read_available(&buf) == 12); + assert(sc_bytebuf_can_read(&buf) == 12); sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1); - assert(sc_bytebuf_read_available(&buf) == 18); + assert(sc_bytebuf_can_read(&buf) == 18); sc_bytebuf_read(&buf, data, 9); assert(!strncmp((char *) data, "hello hel", 9)); - assert(sc_bytebuf_read_available(&buf) == 9); + assert(sc_bytebuf_can_read(&buf) == 9); sc_bytebuf_write(&buf, (uint8_t *) "world", sizeof("world") - 1); - assert(sc_bytebuf_read_available(&buf) == 14); + assert(sc_bytebuf_can_read(&buf) == 14); sc_bytebuf_write(&buf, (uint8_t *) "!", 1); - assert(sc_bytebuf_read_available(&buf) == 15); + assert(sc_bytebuf_can_read(&buf) == 15); sc_bytebuf_skip(&buf, 3); - assert(sc_bytebuf_read_available(&buf) == 12); + assert(sc_bytebuf_can_read(&buf) == 12); sc_bytebuf_read(&buf, data, 12); data[12] = '\0'; assert(!strcmp((char *) data, "hello world!")); - assert(sc_bytebuf_read_available(&buf) == 0); + assert(sc_bytebuf_can_read(&buf) == 0); sc_bytebuf_destroy(&buf); } @@ -79,37 +79,37 @@ void test_bytebuf_two_steps_write(void) { assert(ok); sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1); - assert(sc_bytebuf_read_available(&buf) == 6); + assert(sc_bytebuf_can_read(&buf) == 6); sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1); - assert(sc_bytebuf_read_available(&buf) == 12); + assert(sc_bytebuf_can_read(&buf) == 12); sc_bytebuf_prepare_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1); - assert(sc_bytebuf_read_available(&buf) == 12); // write not committed yet + assert(sc_bytebuf_can_read(&buf) == 12); // write not committed yet sc_bytebuf_read(&buf, data, 9); assert(!strncmp((char *) data, "hello hel", 3)); - assert(sc_bytebuf_read_available(&buf) == 3); + assert(sc_bytebuf_can_read(&buf) == 3); sc_bytebuf_commit_write(&buf, sizeof("hello ") - 1); - assert(sc_bytebuf_read_available(&buf) == 9); + assert(sc_bytebuf_can_read(&buf) == 9); sc_bytebuf_prepare_write(&buf, (uint8_t *) "world", sizeof("world") - 1); - assert(sc_bytebuf_read_available(&buf) == 9); // write not committed yet + assert(sc_bytebuf_can_read(&buf) == 9); // write not committed yet sc_bytebuf_commit_write(&buf, sizeof("world") - 1); - assert(sc_bytebuf_read_available(&buf) == 14); + assert(sc_bytebuf_can_read(&buf) == 14); sc_bytebuf_write(&buf, (uint8_t *) "!", 1); - assert(sc_bytebuf_read_available(&buf) == 15); + assert(sc_bytebuf_can_read(&buf) == 15); sc_bytebuf_skip(&buf, 3); - assert(sc_bytebuf_read_available(&buf) == 12); + assert(sc_bytebuf_can_read(&buf) == 12); sc_bytebuf_read(&buf, data, 12); data[12] = '\0'; assert(!strcmp((char *) data, "hello world!")); - assert(sc_bytebuf_read_available(&buf) == 0); + assert(sc_bytebuf_can_read(&buf) == 0); sc_bytebuf_destroy(&buf); } From 0b8a5ca923bdd17bd551e9f5565979373ec319c4 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 10 Mar 2023 22:15:38 +0100 Subject: [PATCH 1476/2244] Do not read avg_buffering from the player thread On buffer underflow, the average buffering must be updated, but it is intended to be accessed only from the receiver thread. Make the player and the receiver thread communicate the underflow via a new field (ap->underflow). --- app/src/audio_player.c | 8 ++++++-- app/src/audio_player.h | 4 ++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/app/src/audio_player.c b/app/src/audio_player.c index 652711c6..d9aba58a 100644 --- a/app/src/audio_player.c +++ b/app/src/audio_player.c @@ -67,7 +67,7 @@ sc_audio_player_sdl_callback(void *userdata, uint8_t *stream, int len_int) { if (ap->received) { // Inserting additional samples immediately increases buffering - ap->avg_buffering.avg += silence; + ap->underflow += silence; } } @@ -194,9 +194,12 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink, // Number of samples added (or removed, if negative) for compensation int32_t instant_compensation = (int32_t) samples_written - frame->nb_samples; + int32_t inserted_silence = (int32_t) ap->underflow; // The compensation must apply instantly, it must not be smoothed - ap->avg_buffering.avg += instant_compensation; + ap->avg_buffering.avg += instant_compensation + inserted_silence; + + ap->underflow = 0; // reset // However, the buffering level must be smoothed sc_average_push(&ap->avg_buffering, buffered_samples); @@ -358,6 +361,7 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink, ap->received = false; ap->played = false; + ap->underflow = 0; // The thread calling open() is the thread calling push(), which fills the // audio buffer consumed by the SDL audio thread. diff --git a/app/src/audio_player.h b/app/src/audio_player.h index f4670939..3227f2fe 100644 --- a/app/src/audio_player.h +++ b/app/src/audio_player.h @@ -56,6 +56,10 @@ struct sc_audio_player { // (only used by the receiver thread) uint32_t samples_since_resync; + // Number of silence samples inserted since the last received packet + // (protected by SDL_AudioDeviceLock()) + uint32_t underflow; + // Set to true the first time a sample is received (protected by // SDL_AudioDeviceLock()) bool received; From eca87665455f6d5a3092e90150e5a7929045cf1a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 10 Mar 2023 22:19:28 +0100 Subject: [PATCH 1477/2244] Compute buffering and compensation without lock Once underflow has been read with a lock, the buffering and compensation may be performed without shared variables. --- app/src/audio_player.c | 38 +++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/app/src/audio_player.c b/app/src/audio_player.c index d9aba58a..1bff2b76 100644 --- a/app/src/audio_player.c +++ b/app/src/audio_player.c @@ -180,6 +180,8 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink, // Read with lock held, to be used after unlocking bool played = ap->played; + uint32_t underflow = ap->underflow; + if (played) { uint32_t max_buffered_samples = ap->target_buffering + 12 * SC_AUDIO_OUTPUT_BUFFER_MS * ap->sample_rate / 1000 @@ -191,23 +193,8 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink, " samples", skip_samples); } - // Number of samples added (or removed, if negative) for compensation - int32_t instant_compensation = - (int32_t) samples_written - frame->nb_samples; - int32_t inserted_silence = (int32_t) ap->underflow; - - // The compensation must apply instantly, it must not be smoothed - ap->avg_buffering.avg += instant_compensation + inserted_silence; - - ap->underflow = 0; // reset - - // However, the buffering level must be smoothed - sc_average_push(&ap->avg_buffering, buffered_samples); - -#ifndef SC_AUDIO_PLAYER_NDEBUG - LOGD("[Audio] buffered_samples=%" PRIu32 " avg_buffering=%f", - buffered_samples, sc_average_get(&ap->avg_buffering)); -#endif + // reset (the current value was copied to a local variable) + ap->underflow = 0; } else { // SDL playback not started yet, do not accumulate more than // max_initial_buffering samples, this would cause unnecessary delay @@ -230,6 +217,23 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink, SDL_UnlockAudioDevice(ap->device); if (played) { + // Number of samples added (or removed, if negative) for compensation + int32_t instant_compensation = + (int32_t) samples_written - frame->nb_samples; + int32_t inserted_silence = (int32_t) underflow; + + // The compensation must apply instantly, it must not be smoothed + ap->avg_buffering.avg += instant_compensation + inserted_silence; + + + // However, the buffering level must be smoothed + sc_average_push(&ap->avg_buffering, buffered_samples); + +#ifndef SC_AUDIO_PLAYER_NDEBUG + LOGD("[Audio] buffered_samples=%" PRIu32 " avg_buffering=%f", + buffered_samples, sc_average_get(&ap->avg_buffering)); +#endif + ap->samples_since_resync += samples_written; if (ap->samples_since_resync >= ap->sample_rate) { // Recompute compensation every second From 2380879376bd0aeeffb0262f98b9f168bfe198fb Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 11 Mar 2023 16:01:45 +0100 Subject: [PATCH 1478/2244] Remove unused IOException IOException may not be thrown from this method. --- server/src/main/java/com/genymobile/scrcpy/AudioCapture.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java b/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java index 3cef7801..9228e3d7 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java @@ -14,7 +14,6 @@ import android.media.MediaRecorder; import android.os.Build; import android.os.SystemClock; -import java.io.IOException; import java.nio.ByteBuffer; public final class AudioCapture { @@ -110,7 +109,7 @@ public final class AudioCapture { } @TargetApi(Build.VERSION_CODES.N) - public int read(ByteBuffer directBuffer, int size, MediaCodec.BufferInfo outBufferInfo) throws IOException { + public int read(ByteBuffer directBuffer, int size, MediaCodec.BufferInfo outBufferInfo) { int r = recorder.read(directBuffer, size); if (r < 0) { return r; From f5bb9e576dfae8444215e36e4fbd28bc2947f4c9 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 11 Mar 2023 16:11:57 +0100 Subject: [PATCH 1479/2244] Upgrade SDL (2.26.4) for Windows Include the latest version of SDL in Windows releases. --- app/prebuilt-deps/prepare-sdl.sh | 6 +++--- cross_win32.txt | 2 +- cross_win64.txt | 2 +- release.mk | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/prebuilt-deps/prepare-sdl.sh b/app/prebuilt-deps/prepare-sdl.sh index 644ed72d..60bffae9 100755 --- a/app/prebuilt-deps/prepare-sdl.sh +++ b/app/prebuilt-deps/prepare-sdl.sh @@ -6,10 +6,10 @@ cd "$DIR" mkdir -p "$PREBUILT_DATA_DIR" cd "$PREBUILT_DATA_DIR" -DEP_DIR=SDL2-2.26.1 +DEP_DIR=SDL2-2.26.4 -FILENAME=SDL2-devel-2.26.1-mingw.tar.gz -SHA256SUM=aa43e1531a89551f9f9e14b27953a81d4ac946a9e574b5813cd0f2b36e83cc1c +FILENAME=SDL2-devel-2.26.4-mingw.tar.gz +SHA256SUM=fe899c8642caac2f180b1ee6f786857ddcaa0adc1fa82474312b09dd47d74712 if [[ -d "$DEP_DIR" ]] then diff --git a/cross_win32.txt b/cross_win32.txt index a02e798a..18834af4 100644 --- a/cross_win32.txt +++ b/cross_win32.txt @@ -17,5 +17,5 @@ endian = 'little' [properties] prebuilt_ffmpeg = 'ffmpeg-6.0-scrcpy-2/win32' -prebuilt_sdl2 = 'SDL2-2.26.1/i686-w64-mingw32' +prebuilt_sdl2 = 'SDL2-2.26.4/i686-w64-mingw32' prebuilt_libusb = 'libusb-1.0.26/libusb-MinGW-Win32' diff --git a/cross_win64.txt b/cross_win64.txt index 126de36e..1c7c0875 100644 --- a/cross_win64.txt +++ b/cross_win64.txt @@ -17,5 +17,5 @@ endian = 'little' [properties] prebuilt_ffmpeg = 'ffmpeg-6.0-scrcpy-2/win64' -prebuilt_sdl2 = 'SDL2-2.26.1/x86_64-w64-mingw32' +prebuilt_sdl2 = 'SDL2-2.26.4/x86_64-w64-mingw32' prebuilt_libusb = 'libusb-1.0.26/libusb-MinGW-x64' diff --git a/release.mk b/release.mk index 75e5a9c0..3c60d80e 100644 --- a/release.mk +++ b/release.mk @@ -102,7 +102,7 @@ dist-win32: build-server build-win32 cp app/prebuilt-deps/data/platform-tools-33.0.3/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-33.0.3/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-33.0.3/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/SDL2-2.26.1/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/SDL2-2.26.4/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/libusb-1.0.26/libusb-MinGW-Win32/bin/msys-usb-1.0.dll "$(DIST)/$(WIN32_TARGET_DIR)/" dist-win64: build-server build-win64 @@ -121,7 +121,7 @@ dist-win64: build-server build-win64 cp app/prebuilt-deps/data/platform-tools-33.0.3/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-33.0.3/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-33.0.3/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/SDL2-2.26.1/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/SDL2-2.26.4/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/libusb-1.0.26/libusb-MinGW-x64/bin/msys-usb-1.0.dll "$(DIST)/$(WIN64_TARGET_DIR)/" zip-win32: dist-win32 From cc07f8dac4247c7d6b175b1f58e71d50e87f85db Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 11 Mar 2023 16:15:24 +0100 Subject: [PATCH 1480/2244] Upgrade platform-tools (34.0.1) for Windows Include the latest version of adb in Windows releases. --- app/prebuilt-deps/prepare-adb.sh | 6 +++--- release.mk | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/prebuilt-deps/prepare-adb.sh b/app/prebuilt-deps/prepare-adb.sh index 0e4e498c..cc139095 100755 --- a/app/prebuilt-deps/prepare-adb.sh +++ b/app/prebuilt-deps/prepare-adb.sh @@ -6,10 +6,10 @@ cd "$DIR" mkdir -p "$PREBUILT_DATA_DIR" cd "$PREBUILT_DATA_DIR" -DEP_DIR=platform-tools-33.0.3 +DEP_DIR=platform-tools-34.0.1 -FILENAME=platform-tools_r33.0.3-windows.zip -SHA256SUM=1e59afd40a74c5c0eab0a9fad3f0faf8a674267106e0b19921be9f67081808c2 +FILENAME=platform-tools_r34.0.1-windows.zip +SHA256SUM=5dd9c2be744c224fa3a7cbe30ba02d2cb378c763bd0f797a7e47e9f3156a5daa if [[ -d "$DEP_DIR" ]] then diff --git a/release.mk b/release.mk index 3c60d80e..0f5cbe24 100644 --- a/release.mk +++ b/release.mk @@ -99,9 +99,9 @@ dist-win32: build-server build-win32 cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win32/bin/avformat-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win32/bin/swresample-4.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win32/bin/zlib1.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/platform-tools-33.0.3/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/platform-tools-33.0.3/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/platform-tools-33.0.3/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/platform-tools-34.0.1/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/platform-tools-34.0.1/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/platform-tools-34.0.1/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/SDL2-2.26.4/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/libusb-1.0.26/libusb-MinGW-Win32/bin/msys-usb-1.0.dll "$(DIST)/$(WIN32_TARGET_DIR)/" @@ -118,9 +118,9 @@ dist-win64: build-server build-win64 cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win64/bin/avformat-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win64/bin/swresample-4.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win64/bin/zlib1.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/platform-tools-33.0.3/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/platform-tools-33.0.3/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/platform-tools-33.0.3/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/platform-tools-34.0.1/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/platform-tools-34.0.1/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/platform-tools-34.0.1/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/SDL2-2.26.4/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/libusb-1.0.26/libusb-MinGW-x64/bin/msys-usb-1.0.dll "$(DIST)/$(WIN64_TARGET_DIR)/" From 5512777404617ed34a2e0343985f949ae4b554c2 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 11 Mar 2023 16:29:56 +0100 Subject: [PATCH 1481/2244] Remove deprecated option --render-expired-frames This option did nothing since it was deprecated. Totally remove it. --- app/src/cli.c | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index 8a0b6aa4..fc5f0a2f 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -18,8 +18,7 @@ #define STR(x) STR_IMPL_(x) enum { - OPT_RENDER_EXPIRED_FRAMES = 1000, - OPT_BIT_RATE, + OPT_BIT_RATE = 1000, OPT_WINDOW_TITLE, OPT_PUSH_TARGET, OPT_ALWAYS_ON_TOP, @@ -471,11 +470,6 @@ static const struct sc_option options[] = { "\"opengles2\", \"opengles\", \"metal\" and \"software\".\n" "", }, - { - // deprecated - .longopt_id = OPT_RENDER_EXPIRED_FRAMES, - .longopt = "render-expired-frames", - }, { .longopt_id = OPT_REQUIRE_AUDIO, .longopt = "require-audio", @@ -1660,10 +1654,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], case 'w': opts->stay_awake = true; break; - case OPT_RENDER_EXPIRED_FRAMES: - LOGW("Option --render-expired-frames has been removed. This " - "flag has been ignored."); - break; case OPT_WINDOW_TITLE: opts->window_title = optarg; break; From 426dfbf21d9518fc8b325fc1ccf106f3b03e33c1 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 11 Mar 2023 16:32:53 +0100 Subject: [PATCH 1482/2244] Remove dead code about the deprecated -F option The -F option was already removed. --- app/src/cli.c | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index fc5f0a2f..016b07d7 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -1564,9 +1564,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], case 'f': opts->fullscreen = true; break; - case 'F': - LOGW("Deprecated option -F. Use --record-format instead."); - // fall through case OPT_RECORD_FORMAT: if (!parse_record_format(optarg, &opts->record_format)) { return false; From c22c87ededb2b97249a956b0d0a29b16a14bb5d8 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 11 Mar 2023 16:40:39 +0100 Subject: [PATCH 1483/2244] Fail on deprecated options Suggest the video and audio specific options instead. --- app/src/cli.c | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index 016b07d7..cb101a51 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -1535,8 +1535,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], while ((c = getopt_long(argc, argv, optstring, longopts, NULL)) != -1) { switch (c) { case OPT_BIT_RATE: - LOGW("--bit-rate is deprecated, use --video-bit-rate instead."); - // fall through + LOGE("--bit-rate has been removed, " + "use --video-bit-rate or --audio-bit-rate."); + return false; case 'b': if (!parse_bit_rate(optarg, &opts->video_bit_rate)) { return false; @@ -1709,9 +1710,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], opts->forward_key_repeat = false; break; case OPT_CODEC_OPTIONS: - LOGW("--codec-options is deprecated, use --video-codec-options " - "instead."); - // fall through + LOGE("--codec-options has been removed, " + "use --video-codec-options or --audio-codec-options."); + return false; case OPT_VIDEO_CODEC_OPTIONS: opts->video_codec_options = optarg; break; @@ -1719,8 +1720,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], opts->audio_codec_options = optarg; break; case OPT_ENCODER: - LOGW("--encoder is deprecated, use --video-encoder instead."); - // fall through + LOGE("--encoder has been removed, " + "use --video-encoder or --audio-encoder."); + return false; case OPT_VIDEO_ENCODER: opts->video_encoder = optarg; break; @@ -1775,8 +1777,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], opts->start_fps_counter = true; break; case OPT_CODEC: - LOGW("--codec is deprecated, use --video-codec instead."); - // fall through + LOGE("--codec has been removed, " + "use --video-codec or --audio-codec."); + return false; case OPT_VIDEO_CODEC: if (!parse_video_codec(optarg, &opts->video_codec)) { return false; From 73727e7fdfdd579f14f957dce680a3fc9e454437 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 11 Mar 2023 18:21:46 +0100 Subject: [PATCH 1484/2244] Disable clock drift compensation for tiny values For less than 1ms, the estimated drift is just noise. --- app/src/audio_player.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/audio_player.c b/app/src/audio_player.c index 1bff2b76..aa34c316 100644 --- a/app/src/audio_player.c +++ b/app/src/audio_player.c @@ -241,7 +241,10 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink, float avg = sc_average_get(&ap->avg_buffering); int diff = ap->target_buffering - avg; - if (diff < 0 && buffered_samples < ap->target_buffering) { + if (abs(diff) < ap->sample_rate / 1000) { + // Do not compensate for less than 1ms, the error is just noise + diff = 0; + } else if (diff < 0 && buffered_samples < ap->target_buffering) { // Do not accelerate if the instant buffering level is below // the average, this would increase underflow diff = 0; From 0bf866fa8d98e8d35c08de5c69e33b80ccda8c44 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 11 Mar 2023 23:00:48 +0100 Subject: [PATCH 1485/2244] Apply new compensation only if it changed If the compensation is the same (typically when it is 0), do not reapply it. --- app/src/audio_player.c | 14 ++++++++++---- app/src/audio_player.h | 3 +++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/app/src/audio_player.c b/app/src/audio_player.c index aa34c316..0511ec1f 100644 --- a/app/src/audio_player.c +++ b/app/src/audio_player.c @@ -258,10 +258,15 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink, LOGV("[Audio] Buffering: target=%" PRIu32 " avg=%f cur=%" PRIu32 " compensation=%d", ap->target_buffering, avg, buffered_samples, diff); - int ret = swr_set_compensation(swr_ctx, diff, distance); - if (ret < 0) { - LOGW("Resampling compensation failed: %d", ret); - // not fatal + + if (diff != ap->compensation) { + int ret = swr_set_compensation(swr_ctx, diff, distance); + if (ret < 0) { + LOGW("Resampling compensation failed: %d", ret); + // not fatal + } else { + ap->compensation = diff; + } } } } @@ -369,6 +374,7 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink, ap->received = false; ap->played = false; ap->underflow = 0; + ap->compensation = 0; // The thread calling open() is the thread calling push(), which fills the // audio buffer consumed by the SDL audio thread. diff --git a/app/src/audio_player.h b/app/src/audio_player.h index 3227f2fe..4dd9c4dc 100644 --- a/app/src/audio_player.h +++ b/app/src/audio_player.h @@ -60,6 +60,9 @@ struct sc_audio_player { // (protected by SDL_AudioDeviceLock()) uint32_t underflow; + // Current applied compensation value (only used by the receiver thread) + int compensation; + // Set to true the first time a sample is received (protected by // SDL_AudioDeviceLock()) bool received; From affda26bfa5678f358f834248179cbe98e38dcc2 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 11 Mar 2023 16:43:56 +0100 Subject: [PATCH 1486/2244] Document audio player Add some high-level documentation on the audio player implementation. --- app/src/audio_player.c | 46 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/app/src/audio_player.c b/app/src/audio_player.c index 0511ec1f..5abc9088 100644 --- a/app/src/audio_player.c +++ b/app/src/audio_player.c @@ -7,6 +7,52 @@ #define SC_AUDIO_PLAYER_NDEBUG // comment to debug +/** + * Real-time audio player with configurable latency + * + * As input, the player regularly receives AVFrames of decoded audio samples. + * As output, an SDL callback regularly requests audio samples to be played. + * In the middle, an audio buffer stores the samples produced but not consumed + * yet. + * + * The goal of the player is to feed the audio output with a latency as low as + * possible while avoiding buffer underrun (i.e. not being able to provide + * samples when requested). + * + * The player aims to feed the audio output with as little latency as possible + * while avoiding buffer underrun. To achieve this, it attempts to maintain the + * average buffering (the number of samples present in the buffer) around a + * target value. If this target buffering is too low, then buffer underrun will + * occur frequently. If it is too high, then latency will become unacceptable. + * This target value is configured using the scrcpy option --audio-buffer. + * + * The player cannot adjust the sample input rate (it receives samples produced + * in real-time) or the sample output rate (it must provide samples as + * requested by the audio output callback). Therefore, it may only apply + * compensation by resampling (converting _m_ input samples to _n_ output + * samples). + * + * The compensation itself is applied by libswresample (FFmpeg). It is + * configured using swr_set_compensation(). An important work for the player + * is to estimate the compensation value regularly and apply it. + * + * The estimated buffering level is the result of averaging the "natural" + * buffering (samples are produced and consumed by blocks, so it must be + * smoothed), and making instant adjustments resulting of its own actions + * (explicit compensation and silence insertion on underflow), which are not + * smoothed. + * + * Buffer underflow events can occur when packets arrive too late. In that case, + * the player inserts silence. Once the packets finally arrive (late), one + * strategy could be to drop the samples that were replaced by silence, in + * order to keep a minimal latency. However, dropping samples in case of buffer + * underflow is inadvisable, as it would temporarily increase the underflow + * even more and cause very noticeable audio glitches. + * + * Therefore, the player doesn't drop any sample on underflow. The compensation + * mechanism will absorb the delay introduced by the inserted silence. + */ + /** Downcast frame_sink to sc_audio_player */ #define DOWNCAST(SINK) container_of(SINK, struct sc_audio_player, frame_sink) From 05a55e36878b78754d0c031d5c81dc7de7ac1893 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 4 Mar 2023 09:08:42 +0100 Subject: [PATCH 1487/2244] Happy new year 2023! --- LICENSE | 2 +- README.md | 2 +- app/scrcpy.1 | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/LICENSE b/LICENSE index bea74a6b..55f96811 100644 --- a/LICENSE +++ b/LICENSE @@ -188,7 +188,7 @@ identification within third-party archives. Copyright (C) 2018 Genymobile - Copyright (C) 2018-2022 Romain Vimont + Copyright (C) 2018-2023 Romain Vimont Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index a2e275f9..765a6466 100644 --- a/README.md +++ b/README.md @@ -1185,7 +1185,7 @@ Read the [developers page]. ## Licence Copyright (C) 2018 Genymobile - Copyright (C) 2018-2022 Romain Vimont + Copyright (C) 2018-2023 Romain Vimont Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/app/scrcpy.1 b/app/scrcpy.1 index e8e36188..65357686 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -571,7 +571,7 @@ Copyright \(co 2018 Genymobile Genymobile .UE -Copyright \(co 2018\-2022 +Copyright \(co 2018\-2023 .MT rom@rom1v.com Romain Vimont .ME From f12590ed08b892b289fb382dd1e4f89fe2640a2d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 4 Mar 2023 08:56:35 +0100 Subject: [PATCH 1488/2244] Rework README and documentation The README.md page is HUGE. Split it up. Also document audio forwarding and improve installation instructions for each platform and user documentation. PR #3774 --- FAQ.md | 123 +--- README.md | 1205 ++-------------------------------- doc/audio.md | 90 +++ BUILD.md => doc/build.md | 60 +- doc/control.md | 149 +++++ DEVELOP.md => doc/develop.md | 0 doc/device.md | 228 +++++++ doc/hid-otg.md | 108 +++ doc/linux.md | 79 +++ doc/macos.md | 47 ++ doc/recording.md | 44 ++ doc/shortcuts.md | 68 ++ doc/tunnels.md | 123 ++++ doc/v4l2.md | 65 ++ doc/video.md | 175 +++++ doc/window.md | 55 ++ doc/windows.md | 84 +++ 17 files changed, 1387 insertions(+), 1316 deletions(-) create mode 100644 doc/audio.md rename BUILD.md => doc/build.md (85%) create mode 100644 doc/control.md rename DEVELOP.md => doc/develop.md (100%) create mode 100644 doc/device.md create mode 100644 doc/hid-otg.md create mode 100644 doc/linux.md create mode 100644 doc/macos.md create mode 100644 doc/recording.md create mode 100644 doc/shortcuts.md create mode 100644 doc/tunnels.md create mode 100644 doc/v4l2.md create mode 100644 doc/video.md create mode 100644 doc/window.md create mode 100644 doc/windows.md diff --git a/FAQ.md b/FAQ.md index e6c3c94d..9b22c447 100644 --- a/FAQ.md +++ b/FAQ.md @@ -164,32 +164,6 @@ keyboard][hid] (HID). ## Client issues -### The quality is low - -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 - -This problem should be fixed in scrcpy v1.22: **update to the latest version**. - -On older versions, you must 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 - -Also, 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 to enable mipmapping: - -``` -scrcpy --render-driver=opengl -``` - - ### Issue with Wayland By default, SDL uses x11 on Linux. The [video driver] can be changed via the @@ -224,102 +198,15 @@ As a workaround, [disable "Block compositing"][kwin]. ### 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 -> ``` - -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: +If you get any exception related to `MediaCodec`: ``` -scrcpy -m 1920 -scrcpy -m 1024 -scrcpy -m 800 +ERROR: Exception on thread Thread[main,5,main] +java.lang.IllegalStateException + at android.media.MediaCodec.native_dequeueOutputBuffer(Native Method) ``` -Since scrcpy v1.22, scrcpy automatically tries again with a lower definition -before failing. This behavior can be disabled with `--no-downsize-on-error`. - -You could also try another [encoder](README.md#encoder). - - -If you encounter this exception on Android 12, then just upgrade to scrcpy >= -1.18 (see [#2129]): - -``` -> ERROR: Exception on thread Thread[main,5,main] -java.lang.AssertionError: java.lang.reflect.InvocationTargetException - at com.genymobile.scrcpy.wrappers.SurfaceControl.setDisplaySurface(SurfaceControl.java:75) - ... -Caused by: java.lang.reflect.InvocationTargetException - at java.lang.reflect.Method.invoke(Native Method) - at com.genymobile.scrcpy.wrappers.SurfaceControl.setDisplaySurface(SurfaceControl.java:73) - ... 7 more -Caused by: java.lang.IllegalArgumentException: displayToken must not be null - at android.view.SurfaceControl$Transaction.setDisplaySurface(SurfaceControl.java:3067) - at android.view.SurfaceControl.setDisplaySurface(SurfaceControl.java:2147) - ... 9 more -``` - -[#2129]: https://github.com/Genymobile/scrcpy/issues/2129 - - -## Command line on Windows - -Since v1.22, a "shortcut" has been added to directly open a terminal in the -scrcpy directory. Double-click on `open_a_terminal_here.bat`, then type your -command. For example: - -``` -scrcpy --record file.mkv -``` - -You could also open a terminal and go to the scrcpy folder manually: - - 1. Press Windows+r, this opens a dialog box. - 2. Type `cmd` and press Enter, this opens a terminal. - 3. Go to your _scrcpy_ directory, by typing (adapt the path): - - ```bat - cd C:\Users\user\Downloads\scrcpy-win64-xxx - ``` - - and press Enter - 4. Type your command. For example: - - ```bat - scrcpy --record file.mkv - ``` - -If you plan to always use the same arguments, create a file `myscrcpy.bat` -(enable [show file extensions] to avoid confusion) in the `scrcpy` directory, -containing your command. For example: - -```bat -scrcpy --prefer-text --turn-screen-off --stay-awake -``` - -Then just double-click on that file. - -You could also edit (a copy of) `scrcpy-console.bat` or `scrcpy-noconsole.vbs` -to add some arguments. - -[show file extensions]: https://www.howtogeek.com/205086/beginner-how-to-make-windows-show-file-extensions/ +then try with another [encoder](doc/video.md#codec). ## Translations diff --git a/README.md b/README.md index 765a6466..bc6f3bd3 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,10 @@ _pronounced "**scr**een **c**o**py**"_ -[Read in another language](#translations) - -This application provides display and control of Android devices connected via -USB or [over TCP/IP](#tcpip-wireless). It does not require any _root_ access. -It works on _GNU/Linux_, _Windows_ and _macOS_. +This application mirrors Android devices (video and audio) connected via +USB or [over TCP/IP](doc/device.md#tcpip-wireless), and allows to control the +device with the keyboard and the mouse of the computer. It does not require any +_root_ access. It works on _Linux_, _Windows_ and _macOS_. ![screenshot](assets/screenshot-debian-600.jpg) @@ -26,1161 +25,96 @@ It focuses on: [lowlatency]: https://github.com/Genymobile/scrcpy/pull/646 Its features include: - - [recording](#recording) - - mirroring with [Android device screen off](#turn-screen-off) - - [copy-paste](#copy-paste) in both directions - - [configurable quality](#capture-configuration) - - Android device [as a webcam (V4L2)](#v4l2loopback) (Linux-only) - - [physical keyboard simulation (HID)](#physical-keyboard-simulation-hid) - - [physical mouse simulation (HID)](#physical-mouse-simulation-hid) - - [OTG mode](#otg) + - [audio forwarding](doc/audio.md) (Android >= 11) + - [recording](doc/recording.md) + - mirroring with [Android device screen off](doc/device.md#turn-screen-off) + - [copy-paste](doc/control.md#copy-paste) in both directions + - [configurable quality](doc/video.md) + - Android device [as a webcam (V4L2)](doc/v4l2.md) (Linux-only) + - [physical keyboard/mouse simulation (HID)](doc/hid-otg.md) + - [OTG mode](doc/hid-otg.md#otg) - and more… ## Requirements The Android device requires at least API 21 (Android 5.0). -Make sure you [enable adb debugging][enable-adb] on your device(s). +[Audio forwarding](doc/audio.md) is supported from API 30 (Android 11). + +Make sure you [enabled adb debugging][enable-adb] on your device(s). [enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling -On some devices, you also need to enable [an additional option][control] to -control it using a keyboard and mouse. +On some devices, you also need to enable [an additional option][control] `USB +debugging (Security Settings)` (this is an item different from `USB debugging`) +to control it using a keyboard and mouse. [control]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323 ## Get the app -Packaging status + - [Linux](doc/linux.md) + - [Windows](doc/windows.md) + - [macOS](doc/macos.md) -### Summary - - Linux: `apt install scrcpy` - - Windows: [download][direct-win64] - - macOS: `brew install scrcpy` +## User documentation -Build from sources: [BUILD] ([simplified process][BUILD_simple]) +The application provides a lot of features and configuration options. They are +documented in the following pages: -[BUILD]: BUILD.md -[BUILD_simple]: BUILD.md#simple + - [Device](doc/device.md) + - [Video](doc/video.md) + - [Audio](doc/audio.md) + - [Control](doc/control.md) + - [Window](doc/window.md) + - [Recording](doc/recording.md) + - [Tunnels](doc/tunnels.md) + - [HID/OTG](doc/hid-otg.md) + - [Video4Linux](doc/v4l2.md) + - [Shortcuts](doc/shortcuts.md) -### Linux +## Resources -On Debian and Ubuntu: + - [FAQ](FAQ.md) + - [Translations][wiki] (not necessarily up to date) + - [Build instructions](doc/build.md) + - [Developers](doc/develop.md) -``` -apt install scrcpy -``` +[wiki]: https://github.com/Genymobile/scrcpy/wiki -On Arch Linux: -``` -pacman -S scrcpy -``` +## Articles -A [Snap] package is available: [`scrcpy`][snap-link]. +- [Introducing scrcpy][article-intro] +- [Scrcpy now works wirelessly][article-tcpip] -[snap-link]: https://snapstats.org/snaps/scrcpy +[article-intro]: https://blog.rom1v.com/2018/03/introducing-scrcpy/ +[article-tcpip]: https://www.genymotion.com/blog/open-source-project-scrcpy-now-works-wirelessly/ -[snap]: https://en.wikipedia.org/wiki/Snappy_(package_manager) -For Fedora, a [COPR] package is available: [`scrcpy`][copr-link]. +## Contact -[COPR]: https://fedoraproject.org/wiki/Category:Copr -[copr-link]: https://copr.fedorainfracloud.org/coprs/zeno/scrcpy/ +If you encounter a bug, please read the [FAQ](FAQ.md) first, then open an [issue]. +[issue]: https://github.com/Genymobile/scrcpy/issues -For Gentoo, an [Ebuild] is available: [`scrcpy/`][ebuild-link]. +For general questions or discussions, you can also use: -[Ebuild]: https://wiki.gentoo.org/wiki/Ebuild -[ebuild-link]: https://github.com/maggu2810/maggu2810-overlay/tree/master/app-mobilephone/scrcpy + - Reddit: [`r/scrcpy`](https://www.reddit.com/r/scrcpy) + - Twitter: [`@scrcpy_app`](https://twitter.com/scrcpy_app) -You can also [build the app manually][BUILD] ([simplified -process][BUILD_simple]). +## Donate -### Windows +I'm [@rom1v](https://github.com/rom1v), the author and maintainer of _scrcpy_. -For Windows, a prebuilt archive with all the dependencies (including `adb`) is -available: - - - [`scrcpy-win64-v1.25.zip`][direct-win64] - SHA-256: `db65125e9c65acd00359efb7cea9c05f63cc7ccd5833000cd243cc92f5053028` - -[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.25/scrcpy-win64-v1.25.zip - -It is also available in [Chocolatey]: - -[Chocolatey]: https://chocolatey.org/ - -```bash -choco install scrcpy -choco install adb # if you don't have it yet -``` - -And in [Scoop]: - -```bash -scoop install scrcpy -scoop install adb # if you don't have it yet -``` - -[Scoop]: https://scoop.sh - -You can also [build the app manually][BUILD]. - - -### macOS - -The application is available in [Homebrew]. Just install it: - -[Homebrew]: https://brew.sh/ - -```bash -brew install scrcpy -``` - -You need `adb`, accessible from your `PATH`. If you don't have it yet: - -```bash -brew install android-platform-tools -``` - -It's also available in [MacPorts], which sets up `adb` for you: - -```bash -sudo port install scrcpy -``` - -[MacPorts]: https://www.macports.org/ - - -You can also [build the app manually][BUILD]. - - -## Run - -Plug an Android device into your computer, and execute: - -```bash -scrcpy -``` - -It accepts command-line arguments, listed by: - -```bash -scrcpy --help -``` - -## Features - -### Capture configuration - -#### Reduce size - -Sometimes, it is useful to mirror an Android device at a lower resolution to -increase performance. - -To limit both the width and height to some value (e.g. 1024): - -```bash -scrcpy --max-size=1024 -scrcpy -m 1024 # short version -``` - -The other dimension is computed so that the Android device aspect ratio is -preserved. That way, a device in 1920×1080 will be mirrored at 1024×576. - - -#### Change bit-rate - -The default bit-rate is 8 Mbps. To change the video bitrate (e.g. to 2 Mbps): - -```bash -scrcpy --video-bit-rate=2M -scrcpy -b 2M # short version -``` - -#### Limit frame rate - -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. - -The actual capture framerate may be printed to the console: - -``` -scrcpy --print-fps -``` - -It may also be enabled or disabled at any time with MOD+i. - - -#### Crop - -The device screen may be cropped to mirror only part of the screen. - -This is useful, for example, to mirror only one eye of the Oculus Go: - -```bash -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 # initial (current) orientation -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. - -The [window may also be rotated](#rotation) independently. - - -#### Codec - -The video codec can be selected. The possible values are `h264` (default), -`h265` and `av1`: - -```bash -scrcpy --video-codec=h264 # default -scrcpy --video-codec=h265 -scrcpy --video-codec=av1 -``` - - -##### Encoder - -Some devices have more than one encoder for a specific codec, and some of them -may cause issues or crash. It is possible to select a different encoder: - -```bash -scrcpy --video-encoder=OMX.qcom.video.encoder.avc -``` - -To list the available encoders: - -```bash -scrcpy --list-encoders -``` - -### Capture - -#### Recording - -It is possible to record the screen while mirroring: - -```bash -scrcpy --record=file.mp4 -scrcpy -r file.mkv -``` - -To disable mirroring while recording: - -```bash -scrcpy --no-display --record=file.mp4 -scrcpy -Nr file.mkv -# interrupt recording with 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. - -[packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation - - -#### v4l2loopback - -On Linux, it is possible to send the video stream to a v4l2 loopback device, so -that the Android device can be opened like a webcam by any v4l2-capable tool. - -The module `v4l2loopback` must be installed: - -```bash -sudo apt install v4l2loopback-dkms -``` - -To create a v4l2 device: - -```bash -sudo modprobe v4l2loopback -``` - -This will create a new video device in `/dev/videoN`, where `N` is an integer -(more [options](https://github.com/umlaeute/v4l2loopback#options) are available -to create several devices or devices with specific IDs). - -To list the enabled devices: - -```bash -# requires v4l-utils package -v4l2-ctl --list-devices - -# simple but might be sufficient -ls /dev/video* -``` - -To start `scrcpy` using a v4l2 sink: - -```bash -scrcpy --v4l2-sink=/dev/videoN -scrcpy --v4l2-sink=/dev/videoN --no-display # disable mirroring window -scrcpy --v4l2-sink=/dev/videoN -N # short version -``` - -(replace `N` with the device ID, check with `ls /dev/video*`) - -Once enabled, you can open your video stream with a v4l2-capable tool: - -```bash -ffplay -i /dev/videoN -vlc v4l2:///dev/videoN # VLC might add some buffering delay -``` - -For example, you could capture the video within [OBS]. - -[OBS]: https://obsproject.com/ - - -#### Buffering - -It is possible to add buffering. This increases latency, but reduces jitter (see -[#2464]). - -[#2464]: https://github.com/Genymobile/scrcpy/issues/2464 - -The option is available for display buffering: - -```bash -scrcpy --display-buffer=50 # add 50 ms buffering for display -``` - -and V4L2 sink: - -```bash -scrcpy --v4l2-buffer=500 # add 500 ms buffering for v4l2 sink -``` - - -### Connection - -#### TCP/IP (wireless) - -_Scrcpy_ uses `adb` to communicate with the device, and `adb` can [connect] to a -device over TCP/IP. The device must be connected on the same network as the -computer. - -##### Automatic - -An option `--tcpip` allows to configure the connection automatically. There are -two variants. - -If the device (accessible at 192.168.1.1 in this example) already listens on a -port (typically 5555) for incoming _adb_ connections, then run: - -```bash -scrcpy --tcpip=192.168.1.1 # default port is 5555 -scrcpy --tcpip=192.168.1.1:5555 -``` - -If _adb_ TCP/IP mode is disabled on the device (or if you don't know the IP -address), connect the device over USB, then run: - -```bash -scrcpy --tcpip # without arguments -``` - -It will automatically find the device IP address and adb port, enable TCP/IP -mode if necessary, then connect to the device before starting. - -##### Manual - -Alternatively, it is possible to enable the TCP/IP connection manually using -`adb`: - -1. Plug the device into a USB port on your computer. -2. Connect the device to the same Wi-Fi network as your computer. -3. Get your device IP address, in Settings → About phone → Status, or by - executing this command: - - ```bash - adb shell ip route | awk '{print $9}' - ``` - -4. Enable `adb` over TCP/IP on your device: `adb tcpip 5555`. -5. Unplug your device. -6. Connect to your device: `adb connect DEVICE_IP:5555` _(replace `DEVICE_IP` -with the device IP address you found)_. -7. Run `scrcpy` as usual. - -Since Android 11, a [Wireless debugging option][adb-wireless] allows to bypass -having to physically connect your device directly to your computer. - -[adb-wireless]: https://developer.android.com/studio/command-line/adb#connect-to-a-device-over-wi-fi-android-11+ - -If the connection randomly drops, run your `scrcpy` command to reconnect. If it -says there are no devices/emulators found, try running `adb connect -DEVICE_IP:5555` again, and then `scrcpy` as usual. If it still says there are -none found, try running `adb disconnect`, and then run those two commands again. - -It may be useful to decrease the bit-rate and the resolution: - -```bash -scrcpy --video-bit-rate=2M --max-size=800 -scrcpy -b2M -m800 # short version -``` - -[connect]: https://developer.android.com/studio/command-line/adb.html#wireless - - -#### Multi-devices - -If several devices are listed in `adb devices`, you can specify the _serial_: - -```bash -scrcpy --serial=0123456789abcdef -scrcpy -s 0123456789abcdef # short version -``` - -The serial may also be provided via the environment variable `ANDROID_SERIAL` -(also used by `adb`). - -If the device is connected over TCP/IP: - -```bash -scrcpy --serial=192.168.0.1:5555 -scrcpy -s 192.168.0.1:5555 # short version -``` - -If only one device is connected via either USB or TCP/IP, it is possible to -select it automatically: - -```bash -# Select the only device connected via USB -scrcpy -d # like adb -d -scrcpy --select-usb # long version - -# Select the only device connected via TCP/IP -scrcpy -e # like adb -e -scrcpy --select-tcpip # long 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/autoadb - -#### Tunnels - -To connect to a remote device, it is possible to connect a local `adb` client to -a remote `adb` server (provided they use the same version of the _adb_ -protocol). - -##### Remote ADB server - -To connect to a remote _adb server_, make the server listen on all interfaces: - -```bash -adb kill-server -adb -a nodaemon server start -# keep this open -``` - -**Warning: all communications between clients and the _adb server_ are -unencrypted.** - -Suppose that this server is accessible at 192.168.1.2. Then, from another -terminal, run `scrcpy`: - -```bash -# in bash -export ADB_SERVER_SOCKET=tcp:192.168.1.2:5037 -scrcpy --tunnel-host=192.168.1.2 -``` - -```cmd -:: in cmd -set ADB_SERVER_SOCKET=tcp:192.168.1.2:5037 -scrcpy --tunnel-host=192.168.1.2 -``` - -```powershell -# in PowerShell -$env:ADB_SERVER_SOCKET = 'tcp:192.168.1.2:5037' -scrcpy --tunnel-host=192.168.1.2 -``` - -By default, `scrcpy` uses the local port used for `adb forward` tunnel -establishment (typically `27183`, see `--port`). It is also possible to force a -different tunnel port (it may be useful in more complex situations, when more -redirections are involved): - -``` -scrcpy --tunnel-port=1234 -``` - - -##### SSH tunnel - -To communicate with a remote _adb server_ securely, it is preferable to use an -SSH tunnel. - -First, make sure the _adb server_ is running on the remote computer: - -```bash -adb start-server -``` - -Then, establish an SSH tunnel: - -```bash -# local 5038 --> remote 5037 -# local 27183 <-- remote 27183 -ssh -CN -L5038:localhost:5037 -R27183:localhost:27183 your_remote_computer -# keep this open -``` - -From another terminal, run `scrcpy`: - -```bash -# in bash -export ADB_SERVER_SOCKET=tcp:localhost:5038 -scrcpy -``` - -```cmd -:: in cmd -set ADB_SERVER_SOCKET=tcp:localhost:5038 -scrcpy -``` - -```powershell -# in PowerShell -$env:ADB_SERVER_SOCKET = 'tcp:localhost:5038' -scrcpy -``` - -To avoid enabling remote port forwarding, you could force a forward connection -instead (notice the `-L` instead of `-R`): - -```bash -# local 5038 --> remote 5037 -# local 27183 --> remote 27183 -ssh -CN -L5038:localhost:5037 -L27183:localhost:27183 your_remote_computer -# keep this open -``` - -From another terminal, run `scrcpy`: - -```bash -# in bash -export ADB_SERVER_SOCKET=tcp:localhost:5038 -scrcpy --force-adb-forward -``` - -```cmd -:: in cmd -set ADB_SERVER_SOCKET=tcp:localhost:5038 -scrcpy --force-adb-forward -``` - -```powershell -# in PowerShell -$env:ADB_SERVER_SOCKET = 'tcp:localhost:5038' -scrcpy --force-adb-forward -``` - - -Like for wireless connections, it may be useful to reduce quality: - -``` -scrcpy -b2M -m800 --max-fps=15 -``` - -### Window configuration - -#### Title - -By default, the window title is the device model. It can be changed: - -```bash -scrcpy --window-title='My device' -``` - -#### Position and size - -The initial window position and size may be specified: - -```bash -scrcpy --window-x=100 --window-y=100 --window-width=800 --window-height=600 -``` - -#### Borderless - -To disable window decorations: - -```bash -scrcpy --window-borderless -``` - -#### Always on top - -To keep the _scrcpy_ window always on top: - -```bash -scrcpy --always-on-top -``` - -#### Fullscreen - -The app may be started directly in fullscreen: - -```bash -scrcpy --fullscreen -scrcpy -f # short version -``` - -Fullscreen can then be toggled dynamically with MOD+f. - -#### Rotation - -The window may be rotated: - -```bash -scrcpy --rotation=1 -``` - -Possible values: - - `0`: no rotation - - `1`: 90 degrees counterclockwise - - `2`: 180 degrees - - `3`: 90 degrees clockwise - -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 not support the - requested orientation). - - [`--lock-video-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. - - -### Other mirroring options - -#### Read-only - -To disable controls (everything which can interact with the device: input keys, -mouse events, drag&drop files): - -```bash -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: - -```bash -scrcpy --list-displays -``` - -The secondary display may only be controlled if the device runs at least Android -10 (otherwise it is mirrored as read-only). - - -#### Stay awake - -To prevent the device from sleeping after a delay when the device is plugged in: - -```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 -command-line option: - -```bash -scrcpy --turn-screen-off -scrcpy -S -``` - -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 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. - -It can also be useful to prevent the device from sleeping: - -```bash -scrcpy --turn-screen-off --stay-awake -scrcpy -Sw -``` - -#### Power off on close - -To turn the device screen off when closing _scrcpy_: - -```bash -scrcpy --power-off-on-close -``` - -#### Power on on start - -By default, on start, the device is powered on. - -To prevent this behavior: - -```bash -scrcpy --no-power-on -``` - - -#### Show touches - -For presentations, it may be useful to show physical touches (on the physical -device). - -Android provides this feature in _Developers options_. - -_Scrcpy_ provides an option to enable this feature on start and restore the -initial value on exit: - -```bash -scrcpy --show-touches -scrcpy -t -``` - -Note that it only shows _physical_ touches (by a finger on the device). - - -#### Disable screensaver - -By default, _scrcpy_ does not prevent the screensaver from running on the -computer. - -To disable it: - -```bash -scrcpy --disable-screensaver -``` - - -### Input control - -#### Rotate device screen - -Press MOD+r to switch between portrait and landscape -modes. - -Note that it rotates only if the application in foreground supports the -requested orientation. - -#### Copy-paste - -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) - -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 injects 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 Android clipboard. As a consequence, any Android application could read -its content. You should avoid pasting sensitive content (like passwords) that -way. - -Some Android devices do not behave as expected when setting the device clipboard -programmatically. An option `--legacy-paste` is provided to change the behavior -of Ctrl+v and MOD+v so that they -also inject the computer clipboard text as a sequence of key events (the same -way as MOD+Shift+v). - -To disable automatic clipboard synchronization, use -`--no-clipboard-autosync`. - -#### Pinch-to-zoom - -To simulate "pinch-to-zoom": Ctrl+_click-and-move_. - -More precisely, hold down 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. - -Technically, _scrcpy_ generates additional touch events from a "virtual finger" -at a location inverted through the center of the screen. - -#### Physical keyboard simulation (HID) - -By default, _scrcpy_ uses Android key or text injection: it works everywhere, -but is limited to ASCII. - -Alternatively, `scrcpy` can simulate a physical USB keyboard on Android to -provide a better input experience (using [USB HID over AOAv2][hid-aoav2]): the -virtual keyboard is disabled and it works for all characters and IME. - -[hid-aoav2]: https://source.android.com/devices/accessories/aoa2#hid-support - -However, it only works if the device is connected via USB. - -Note: On Windows, it may only work in [OTG mode](#otg), not while mirroring (it -is not possible to open a USB device if it is already open by another process -like the _adb daemon_). - -To enable this mode: - -```bash -scrcpy --hid-keyboard -scrcpy -K # short version -``` - -If it fails for some reason (for example because the device is not connected via -USB), it automatically fallbacks to the default mode (with a log in the -console). This allows using the same command line options when connected over -USB and TCP/IP. - -In this mode, raw key events (scancodes) are sent to the device, independently -of the host key mapping. Therefore, if your keyboard layout does not match, it -must be configured on the Android device, in Settings → System → Languages and -input → [Physical keyboard]. - -This settings page can be started directly: - -```bash -adb shell am start -a android.settings.HARD_KEYBOARD_SETTINGS -``` - -However, the option is only available when the HID keyboard is enabled (or when -a physical keyboard is connected). - -[Physical keyboard]: https://github.com/Genymobile/scrcpy/pull/2632#issuecomment-923756915 - -#### Physical mouse simulation (HID) - -Similarly to the physical keyboard simulation, it is possible to simulate a -physical mouse. Likewise, it only works if the device is connected by USB. - -By default, _scrcpy_ uses Android mouse events injection with absolute -coordinates. By simulating a physical mouse, a mouse pointer appears on the -Android device, and relative mouse motion, clicks and scrolls are injected. - -To enable this mode: - -```bash -scrcpy --hid-mouse -scrcpy -M # short version -``` - -You can also add `--forward-all-clicks` to [forward all mouse -buttons][forward_all_clicks]. - -[forward_all_clicks]: #right-click-and-middle-click - -When this mode is enabled, the computer mouse is "captured" (the mouse pointer -disappears from the computer and appears on the Android device instead). - -Special capture keys, either Alt or Super, toggle -(disable or enable) the mouse capture. Use one of them to give the control of -the mouse back to the computer. - - -#### OTG - -It is possible to run _scrcpy_ with only physical keyboard and mouse simulation -(HID), as if the computer keyboard and mouse were plugged directly to the device -via an OTG cable. - -In this mode, `adb` (USB debugging) is not necessary, and mirroring is disabled. - -To enable OTG mode: - -```bash -scrcpy --otg -# Pass the serial if several USB devices are available -scrcpy --otg -s 0123456789abcdef -``` - -It is possible to enable only HID keyboard or HID mouse: - -```bash -scrcpy --otg --hid-keyboard # keyboard only -scrcpy --otg --hid-mouse # mouse only -scrcpy --otg --hid-keyboard --hid-mouse # keyboard and mouse -# for convenience, enable both by default -scrcpy --otg # keyboard and mouse -``` - -Like `--hid-keyboard` and `--hid-mouse`, it only works if the device is -connected by USB. - - -#### Text injection preference - -Two kinds of [events][textevents] are generated when typing text: - - _key events_, signaling that a key is pressed or released; - - _text events_, signaling that a text has been entered. - -By default, letters are injected using key events, so that the keyboard behaves -as expected in games (typically for WASD keys). - -But this may [cause issues][prefertext]. If you encounter such a problem, you -can avoid it by: - -```bash -scrcpy --prefer-text -``` - -(but this will break keyboard behavior in games) - -On the contrary, you could force to always inject raw key events: - -```bash -scrcpy --raw-key-events -``` - -These options have no effect on HID keyboard (all key events are sent as -scancodes in this mode). - -[textevents]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-text-input -[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 -``` - -This option has no effect on HID keyboard (key repeat is handled by Android -directly in this mode). - - -#### Right-click and middle-click - -By default, right-click triggers BACK (or POWER on) and middle-click triggers -HOME. To disable these shortcuts and forward the clicks to the device instead: - -```bash -scrcpy --forward-all-clicks -``` - - -### File drop - -#### Install APK - -To install an APK, drag & drop an APK file (ending with `.apk`) to the _scrcpy_ -window. - -There is no visual feedback, a log is printed to the console. - - -#### Push file to device - -To push a file to `/sdcard/Download/` on the device, drag & drop a (non-APK) -file to the _scrcpy_ window. - -There is no visual feedback, a log is printed to the console. - -The target directory can be changed on start: - -```bash -scrcpy --push-target=/sdcard/Movies/ -``` - - -### Audio forwarding - -Audio is not forwarded by _scrcpy_. Use [sndcpy]. - -Also see [issue #14]. - -[sndcpy]: https://github.com/rom1v/sndcpy -[issue #14]: https://github.com/Genymobile/scrcpy/issues/14 - - -## Shortcuts - -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: - -```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 | 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-left-click¹_ - | Click on `HOME` | MOD+h \| _Middle-click_ - | Click on `BACK` | MOD+b \| _Right-click²_ - | Click on `APP_SWITCH` | MOD+s \| _4th-click³_ - | Click on `MENU` (unlock screen)⁴ | 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 \| _5th-click³_ - | Expand settings panel | MOD+n+n \| _Double-5th-click³_ - | Collapse panels | 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 - | Pinch-to-zoom | Ctrl+_click-and-move_ - | Drag & drop APK file | Install APK from computer - | Drag & drop non-APK file | [Push file to device](#push-file-to-device) - -_¹Double-click on black borders to remove them._ -_²Right-click turns the screen on if it was off, presses BACK otherwise._ -_³4th and 5th mouse buttons, if your mouse has them._ -_⁴For react-native apps in development, `MENU` triggers development menu._ -_⁵Only on Android >= 7._ - -Shortcuts with repeated keys are executed by releasing and pressing the key a -second time. For example, to execute "Expand settings panel": - - 1. Press and keep pressing MOD. - 2. Then double-press n. - 3. Finally, release MOD. - -All Ctrl+_key_ shortcuts are forwarded to the device, so they are -handled by the active application. - - -## Custom paths - -To use a specific `adb` binary, configure its path in the environment variable -`ADB`: - -```bash -ADB=/path/to/adb scrcpy -``` - -To override the path of the `scrcpy-server` file, configure its path in -`SCRCPY_SERVER_PATH`. - -To override the icon, configure its path in `SCRCPY_ICON_PATH`. - - -## Why the name _scrcpy_? - -A colleague challenged me to find a name as unpronounceable as [gnirehtet]. - -[`strcpy`] copies a **str**ing; `scrcpy` copies a **scr**een. - -[gnirehtet]: https://github.com/Genymobile/gnirehtet -[`strcpy`]: http://man7.org/linux/man-pages/man3/strcpy.3.html - - -## How to build? - -See [BUILD]. - - -## Common issues - -See the [FAQ]. - -[FAQ]: FAQ.md - - -## Developers - -Read the [developers page]. - -[developers page]: DEVELOP.md +If you appreciate this application, you can [support my open source +work][donate]. +[donate]: https://blog.rom1v.com/about/#support-my-open-source-work ## Licence @@ -1198,30 +132,3 @@ Read the [developers page]. 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. - -## Articles - -- [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/ - -## Contact - -If you encounter a bug, please read the [FAQ] first, then open an [issue]. - -[issue]: https://github.com/Genymobile/scrcpy/issues - -For general questions or discussions, you can also use: - - - Reddit: [`r/scrcpy`](https://www.reddit.com/r/scrcpy) - - Twitter: [`@scrcpy_app`](https://twitter.com/scrcpy_app) - -## Translations - -Translations of this README in other languages are available in the [wiki]. - -[wiki]: https://github.com/Genymobile/scrcpy/wiki - -Only this README file is guaranteed to be up-to-date. diff --git a/doc/audio.md b/doc/audio.md new file mode 100644 index 00000000..3755fe37 --- /dev/null +++ b/doc/audio.md @@ -0,0 +1,90 @@ +# Audio + +Audio forwarding is supported for devices with Android 11 or higher, and it is +enabled by default: + + - For **Android 12 or newer**, it works out-of-the-box. + - For **Android 11**, you'll need to ensure that the device screen is unlocked + when starting scrcpy. A fake popup will briefly appear to make the system + think that the shell app is in the foreground. Without this, audio capture + will fail. + - For **Android 10 or earlier**, audio cannot be captured and is automatically + disabled. + +If audio capture fails, then mirroring continues with video only (since audio is +enabled by default, it is not acceptable to make scrcpy fail if it is not +available), unless `--require-audio` is set. + + +## No audio + +To disable audio: + +``` +scrcpy --no-audio +``` + +## Codec + +The audio codec can be selected. The possible values are `opus` (default), `aac` +and `raw` (uncompressed PCM 16-bit LE): + +```bash +scrcpy --audio-codec=opus # default +scrcpy --audio-codec=aac +scrcpy --audio-codec=raw +``` + +Several encoders may be available on the device. They can be listed by: + +```bash +scrcpy --list-encoders +``` + +To select a specific encoder: + +``` +scrcpy --audio-codec=opus --audio-encoder='c2.android.opus.encoder' +``` + +For advanced usage, to pass arbitrary parameters to the [`MediaFormat`], +check `--audio-codec-options` in the manpage or in `scrcpy --help`. + +[`MediaFormat`]: https://developer.android.com/reference/android/media/MediaFormat + + +## Bit rate + +The default video bit-rate is 128Kbps. To change it: + +```bash +scrcpy --audio-bit-rate=64K +scrcpy --audio-bit-rate=64000 # equivalent +``` + +_This parameter does not apply to RAW audio codec (`--audio-codec=raw`)._ + + +## Buffering + +Audio buffering is unavoidable. It must be kept small enough so that the latency +is acceptable, but large enough to minimize buffer underrun (causing audio +glitches). + +The default buffer size is set to 50ms. It can be adjusted: + +```bash +scrcpy --audio-buffer=40 # smaller than default +scrcpy --audio-buffer=100 # higher than default +``` + +Note that this option changes the _target_ buffering. It is possible that this +target buffering might not be reached (on frequent buffer underflow typically). + +If you don't interact with the device (to watch a video for example), a higher +latency (for both [video](video.md#buffering) and audio) might be preferable to +avoid glitches and smooth the playback: + +``` +scrcpy --display-buffer=200 --audio-buffer=200 +``` diff --git a/BUILD.md b/doc/build.md similarity index 85% rename from BUILD.md rename to doc/build.md index 51f8141e..31a04cfb 100644 --- a/BUILD.md +++ b/doc/build.md @@ -2,57 +2,16 @@ Here are the instructions to build _scrcpy_ (client and server). - -## Simple - -If you just want to install the latest release from `master`, follow this -simplified process. - -First, you need to install the required packages: - -```bash -# for Debian/Ubuntu -sudo apt install ffmpeg libsdl2-2.0-0 adb wget \ - gcc git pkg-config meson ninja-build libsdl2-dev \ - libavcodec-dev libavdevice-dev libavformat-dev libavutil-dev \ - libswresample-dev libusb-1.0-0 libusb-1.0-0-dev -``` - -Then clone the repo and execute the installation script -([source](install_release.sh)): - -```bash -git clone https://github.com/Genymobile/scrcpy -cd scrcpy -./install_release.sh -``` - -When a new release is out, update the repo and reinstall: - -```bash -git pull -./install_release.sh -``` - -To uninstall: - -```bash -sudo ninja -Cbuild-auto uninstall -``` - +If you just want to build and install the latest release, follow the simplified +process described in [doc/linux.md](linux.md). ## 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. +There are two main branches: + - `master`: contains the latest release. It is the home page of the project on + GitHub. + - `dev`: 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. @@ -69,6 +28,8 @@ the following files to a directory accessible from your `PATH`: - `AdbWinApi.dll` - `AdbWinUsbApi.dll` +It is also available in scrcpy releases. + The client requires [FFmpeg] and [LibSDL2]. Just follow the instructions. [adb]: https://developer.android.com/studio/command-line/adb.html @@ -314,7 +275,8 @@ This installs several files: - `/usr/local/share/zsh/site-functions/_scrcpy` (zsh completion) - `/usr/local/share/bash-completion/completions/scrcpy` (bash completion) -You can then [run](README.md#run) `scrcpy`. +You can then run `scrcpy`. + ### Uninstall diff --git a/doc/control.md b/doc/control.md new file mode 100644 index 00000000..0b060775 --- /dev/null +++ b/doc/control.md @@ -0,0 +1,149 @@ +# Control + +## Read-only + +To disable controls (everything which can interact with the device: input keys, +mouse events, drag&drop files): + +```bash +scrcpy --no-control +scrcpy -n # short version +``` + + +## Text injection preference + +Two kinds of [events][textevents] are generated when typing text: + - _key events_, signaling that a key is pressed or released; + - _text events_, signaling that a text has been entered. + +By default, letters are injected using key events, so that the keyboard behaves +as expected in games (typically for WASD keys). + +But this may [cause issues][prefertext]. If you encounter such a problem, you +can avoid it by: + +```bash +scrcpy --prefer-text +``` + +(but this will break keyboard behavior in games) + +On the contrary, you could force to always inject raw key events: + +```bash +scrcpy --raw-key-events +``` + +These options have no effect on HID keyboard (all key events are sent as +scancodes in this mode). + +[textevents]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-text-input +[prefertext]: https://github.com/Genymobile/scrcpy/issues/650#issuecomment-512945343 + + +## Copy-paste + +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) + +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 injects 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 Android clipboard. As a consequence, any Android application could read +its content. You should avoid pasting sensitive content (like passwords) that +way. + +Some Android devices do not behave as expected when setting the device clipboard +programmatically. An option `--legacy-paste` is provided to change the behavior +of Ctrl+v and MOD+v so that they +also inject the computer clipboard text as a sequence of key events (the same +way as MOD+Shift+v). + +To disable automatic clipboard synchronization, use +`--no-clipboard-autosync`. + +## Pinch-to-zoom + +To simulate "pinch-to-zoom": Ctrl+_click-and-move_. + +More precisely, hold down 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. + +Technically, _scrcpy_ generates additional touch events from a "virtual finger" +at a location inverted through the center of the screen. + + +## 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 +``` + +This option has no effect on HID keyboard (key repeat is handled by Android +directly in this mode). + + +## Right-click and middle-click + +By default, right-click triggers BACK (or POWER on) and middle-click triggers +HOME. To disable these shortcuts and forward the clicks to the device instead: + +```bash +scrcpy --forward-all-clicks +``` + +## File drop + +### Install APK + +To install an APK, drag & drop an APK file (ending with `.apk`) to the _scrcpy_ +window. + +There is no visual feedback, a log is printed to the console. + + +### Push file to device + +To push a file to `/sdcard/Download/` on the device, drag & drop a (non-APK) +file to the _scrcpy_ window. + +There is no visual feedback, a log is printed to the console. + +The target directory can be changed on start: + +```bash +scrcpy --push-target=/sdcard/Movies/ +``` + +## Physical keyboard and mouse simulation + +See the dedicated [HID/OTG](hid-otg.md) page. diff --git a/DEVELOP.md b/doc/develop.md similarity index 100% rename from DEVELOP.md rename to doc/develop.md diff --git a/doc/device.md b/doc/device.md new file mode 100644 index 00000000..c7e1ec04 --- /dev/null +++ b/doc/device.md @@ -0,0 +1,228 @@ +# Device + +## Selection + +If exactly one device is connected (i.e. listed by `adb devices`), then it is +automatically selected. + +However, if there are multiple devices connected, you must specify the one to +use in one of 4 ways: + - by its serial: + ```bash + scrcpy --serial=0123456789abcdef + scrcpy -s 0123456789abcdef # short version + + # the serial is the ip:port if connected over TCP/IP (same behavior as adb) + scrcpy --serial=192.168.1.1:5555 + ``` + - the one connected over USB (if there is exactly one): + ```bash + scrcpy --select-usb + scrcpy -d # short version + ``` + - the one connected over TCP/IP (if there is exactly one): + ```bash + scrcpy --select-tcpip + scrcpy -e # short version + ``` + - a device already listening on TCP/IP (see [below](#tcpip-wireless)): + ```bash + scrcpy --tcpip=192.168.1.1:5555 + scrcpy --tcpip=192.168.1.1 # default port is 5555 + ``` + +The serial may also be provided via the environment variable `ANDROID_SERIAL` +(also used by `adb`): + +```bash +# in bash +export ANDROID_SERIAL=0123456789abcdef +scrcpy +``` + +```cmd +:: in cmd +set ANDROID_SERIAL=0123456789abcdef +scrcpy +``` + +```powershell +# in PowerShell +$env:ANDROID_SERIAL = '0123456789abcdef' +scrcpy +``` + + +## TCP/IP (wireless) + +_Scrcpy_ uses `adb` to communicate with the device, and `adb` can [connect] to a +device over TCP/IP. The device must be connected on the same network as the +computer. + +[connect]: https://developer.android.com/studio/command-line/adb.html#wireless + + +### Automatic + +An option `--tcpip` allows to configure the connection automatically. There are +two variants. + +If the device (accessible at 192.168.1.1 in this example) already listens on a +port (typically 5555) for incoming _adb_ connections, then run: + +```bash +scrcpy --tcpip=192.168.1.1 # default port is 5555 +scrcpy --tcpip=192.168.1.1:5555 +``` + +If _adb_ TCP/IP mode is disabled on the device (or if you don't know the IP +address), connect the device over USB, then run: + +```bash +scrcpy --tcpip # without arguments +``` + +It will automatically find the device IP address and adb port, enable TCP/IP +mode if necessary, then connect to the device before starting. + + +### Manual + +Alternatively, it is possible to enable the TCP/IP connection manually using +`adb`: + +1. Plug the device into a USB port on your computer. +2. Connect the device to the same Wi-Fi network as your computer. +3. Get your device IP address, in Settings → About phone → Status, or by + executing this command: + + ```bash + adb shell ip route | awk '{print $9}' + ``` + +4. Enable `adb` over TCP/IP on your device: `adb tcpip 5555`. +5. Unplug your device. +6. Connect to your device: `adb connect DEVICE_IP:5555` _(replace `DEVICE_IP` +with the device IP address you found)_. +7. Run `scrcpy` as usual. +8. Run `adb disconnect` once you're done. + +Since Android 11, a [Wireless debugging option][adb-wireless] allows to bypass +having to physically connect your device directly to your computer. + +[adb-wireless]: https://developer.android.com/studio/command-line/adb#connect-to-a-device-over-wi-fi-android-11+ + + +## Autostart + +A small tool (by the scrcpy author) allows to run arbitrary commands whenever a +new Android device is connected: [AutoAdb]. It can be used to start scrcpy: + +```bash +autoadb scrcpy -s '{}' +``` + +[AutoAdb]: https://github.com/rom1v/autoadb + + +## Display + +If several displays are available on the Android device, it is possible to +select the display to mirror: + +```bash +scrcpy --display=1 +``` + +The list of display ids can be retrieved by: + +```bash +scrcpy --list-displays +``` + +A secondary display may only be controlled if the device runs at least Android +10 (otherwise it is mirrored as read-only). + + +## Actions + +Some command line arguments perform actions on the device itself while scrcpy is +running. + + +### Stay awake + +To prevent the device from sleeping after a delay **when the device is plugged +in**: + +```bash +scrcpy --stay-awake +scrcpy -w +``` + +The initial state is restored when _scrcpy_ is closed. + +If the device is not plugged in (i.e. only connected over TCP/IP), +`--stay-awake` has no effect (this is the Android behavior). + + +### Turn screen off + +It is possible to turn the device screen off while mirroring on start with a +command-line option: + +```bash +scrcpy --turn-screen-off +scrcpy -S # short version +``` + +Or by pressing MOD+o at any time (see +[shortcuts](shortcuts.md)). + +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 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. + +It can also be useful to prevent the device from sleeping: + +```bash +scrcpy --turn-screen-off --stay-awake +scrcpy -Sw # short version +``` + + +### Show touches + +For presentations, it may be useful to show physical touches (on the physical +device). Android exposes this feature in _Developers options_. + +_Scrcpy_ provides an option to enable this feature on start and restore the +initial value on exit: + +```bash +scrcpy --show-touches +scrcpy -t # short version +``` + +Note that it only shows _physical_ touches (by a finger on the device). + + +### Power off on close + +To turn the device screen off when closing _scrcpy_: + +```bash +scrcpy --power-off-on-close +``` + +### Power on on start + +By default, on start, the device is powered on. To prevent this behavior: + +```bash +scrcpy --no-power-on +``` + diff --git a/doc/hid-otg.md b/doc/hid-otg.md new file mode 100644 index 00000000..c64af752 --- /dev/null +++ b/doc/hid-otg.md @@ -0,0 +1,108 @@ +# HID/OTG + +By default, _scrcpy_ injects input events at the Android API level. As an +alternative, when connected over USB, it is possible to send HID events, so that +scrcpy behaves as if it was a physical keyboard and/or mouse connected to the +Android device. + +A special [OTG](#otg) mode allows to control the device without mirroring (and +without USB debugging). + + +## Physical keyboard simulation + +By default, _scrcpy_ uses Android key or text injection. It works everywhere, +but is limited to ASCII. + +Instead, it can simulate a physical USB keyboard on Android to provide a better +input experience (using [USB HID over AOAv2][hid-aoav2]): the virtual keyboard +is disabled and it works for all characters and IME. + +[hid-aoav2]: https://source.android.com/devices/accessories/aoa2#hid-support + +However, it only works if the device is connected via USB. + +Note: On Windows, it may only work in [OTG mode](#otg), not while mirroring (it +is not possible to open a USB device if it is already open by another process +like the _adb daemon_). + +To enable this mode: + +```bash +scrcpy --hid-keyboard +scrcpy -K # short version +``` + +If it fails for some reason (for example because the device is not connected via +USB), it automatically fallbacks to the default mode (with a log in the +console). This allows using the same command line options when connected over +USB and TCP/IP. + +In this mode, raw key events (scancodes) are sent to the device, independently +of the host key mapping. Therefore, if your keyboard layout does not match, it +must be configured on the Android device, in Settings → System → Languages and +input → [Physical keyboard]. + +This settings page can be started directly: + +```bash +adb shell am start -a android.settings.HARD_KEYBOARD_SETTINGS +``` + +However, the option is only available when the HID keyboard is enabled (or when +a physical keyboard is connected). + +[Physical keyboard]: https://github.com/Genymobile/scrcpy/pull/2632#issuecomment-923756915 + + +## Physical mouse simulation + +By default, _scrcpy_ uses Android mouse events injection with absolute +coordinates. By simulating a physical mouse, a mouse pointer appears on the +Android device, and relative mouse motion, clicks and scrolls are injected. + +To enable this mode: + +```bash +scrcpy --hid-mouse +scrcpy -M # short version +``` + +When this mode is enabled, the computer mouse is "captured" (the mouse pointer +disappears from the computer and appears on the Android device instead). + +Special capture keys, either Alt or Super, toggle +(disable or enable) the mouse capture. Use one of them to give the control of +the mouse back to the computer. + + +## OTG + +It is possible to run _scrcpy_ with only physical keyboard and mouse simulation +(HID), as if the computer keyboard and mouse were plugged directly to the device +via an OTG cable. + +In this mode, `adb` (USB debugging) is not necessary, and mirroring is disabled. + +This is similar to `--hid-keyboard --hid-mouse`, but without mirroring. + +To enable OTG mode: + +```bash +scrcpy --otg +# Pass the serial if several USB devices are available +scrcpy --otg -s 0123456789abcdef +``` + +It is possible to enable only HID keyboard or HID mouse: + +```bash +scrcpy --otg --hid-keyboard # keyboard only +scrcpy --otg --hid-mouse # mouse only +scrcpy --otg --hid-keyboard --hid-mouse # keyboard and mouse +# for convenience, enable both by default +scrcpy --otg # keyboard and mouse +``` + +Like `--hid-keyboard` and `--hid-mouse`, it only works if the device is +connected over USB. diff --git a/doc/linux.md b/doc/linux.md new file mode 100644 index 00000000..3b0c560d --- /dev/null +++ b/doc/linux.md @@ -0,0 +1,79 @@ +# On Linux + +## Install + +Packaging status + +Scrcpy is packaged in several distributions and package managers: + + - Debian/Ubuntu: `apt install scrcpy` + - Arch Linux: `pacman -S scrcpy` + - Fedora: `dnf copr enable zeno/scrcpy && dnf install scrcpy` + - Gentoo: [ebuild][ebuild-link] file + - Snap: `snap install scrcpy` + - … (see [repology](https://repology.org/project/scrcpy/versions)) + +[ebuild-link]: https://github.com/maggu2810/maggu2810-overlay/tree/master/app-mobilephone/scrcpy + +### Latest version + +However, the packaged version is not always the latest release. To install the +latest release from `master`, follow this simplified process. + +First, you need to install the required packages: + +```bash +# for Debian/Ubuntu +sudo apt install ffmpeg libsdl2-2.0-0 adb wget \ + gcc git pkg-config meson ninja-build libsdl2-dev \ + libavcodec-dev libavdevice-dev libavformat-dev libavutil-dev \ + libswresample-dev libusb-1.0-0 libusb-1.0-0-dev +``` + +Then clone the repo and execute the installation script +([source](/install_release.sh)): + +```bash +git clone https://github.com/Genymobile/scrcpy +cd scrcpy +./install_release.sh +``` + +When a new release is out, update the repo and reinstall: + +```bash +git pull +./install_release.sh +``` + +To uninstall: + +```bash +sudo ninja -Cbuild-auto uninstall +``` + +_Note that this simplified process only works for released versions (it +downloads a prebuilt server binary), so for example you can't use it for testing +the development branch (`dev`)._ + +_See [build.md](build.md) to build and install the app manually._ + + +## Run + +Once installed, run from a terminal: + +```bash +scrcpy +``` + +or with arguments (here to disable audio and record to `file.mkv`): + +```bash +scrcpy --no-audio --record=file.mkv +``` + +Documentation for command line arguments is available: + - `man scrcpy` + - `scrcpy --help` + - on [github](/README.md) diff --git a/doc/macos.md b/doc/macos.md new file mode 100644 index 00000000..3092dbdc --- /dev/null +++ b/doc/macos.md @@ -0,0 +1,47 @@ +# On macOS + +## Install + +Scrcpy is available in [Homebrew]: + +```bash +brew install scrcpy +``` + +[Homebrew]: https://brew.sh/ + +You need `adb`, accessible from your `PATH`. If you don't have it yet: + +```bash +brew install android-platform-tools +``` + +Alternatively, Scrcpy is also available in [MacPorts], which sets up `adb` for you: + +```bash +sudo port install scrcpy +``` + +[MacPorts]: https://www.macports.org/ + +_See [build.md](build.md) to build and install the app manually._ + + +## Run + +Once installed, run from a terminal: + +```bash +scrcpy +``` + +or with arguments (here to disable audio and record to `file.mkv`): + +```bash +scrcpy --no-audio --record=file.mkv +``` + +Documentation for command line arguments is available: + - `man scrcpy` + - `scrcpy --help` + - on [github](/README.md) diff --git a/doc/recording.md b/doc/recording.md new file mode 100644 index 00000000..4aad088c --- /dev/null +++ b/doc/recording.md @@ -0,0 +1,44 @@ +# Recording + +To record video and audio streams while mirroring: + +```bash +scrcpy --record=file.mp4 +scrcpy -r file.mkv +``` + +To record only the video: + +```bash +scrcpy --no-audio --record=file.mp4 +``` + +_It is currently not possible to record only the audio._ + +To disable mirroring while recording: + +```bash +scrcpy --no-display --record=file.mp4 +scrcpy -Nr file.mkv +# interrupt recording with Ctrl+C +``` + +Timestamps are captured on the device, so [packet delay variation] does not +impact the recorded file, which is always clean (only if you use `--record` of +course, not if you capture your scrcpy window and audio output on the computer). + +[packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation + +The video and audio streams are encoded on the device, but are muxed on the +client side. Two formats (containers) are supported: + - Matroska (`.mkv`) + - MP4 (`.mp4`) + +The container is automatically selected based on the filename. + +It is also possible to explicitly select a container (in that case the filename +needs not end with `.mkv` or `.mp4`): + +``` +scrcpy --record=file --record-format=mkv +``` diff --git a/doc/shortcuts.md b/doc/shortcuts.md new file mode 100644 index 00000000..06cae428 --- /dev/null +++ b/doc/shortcuts.md @@ -0,0 +1,68 @@ +# Shortcuts + +Actions can be performed on the scrcpy window using keyboard and mouse +shortcuts. + +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: + +```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 | 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-left-click¹_ + | Click on `HOME` | MOD+h \| _Middle-click_ + | Click on `BACK` | MOD+b \| _Right-click²_ + | Click on `APP_SWITCH` | MOD+s \| _4th-click³_ + | Click on `MENU` (unlock screen)⁴ | 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 \| _5th-click³_ + | Expand settings panel | MOD+n+n \| _Double-5th-click³_ + | Collapse panels | 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 + | Pinch-to-zoom | Ctrl+_click-and-move_ + | Drag & drop APK file | Install APK from computer + | Drag & drop non-APK file | [Push file to device](#push-file-to-device) + +_¹Double-click on black borders to remove them._ +_²Right-click turns the screen on if it was off, presses BACK otherwise._ +_³4th and 5th mouse buttons, if your mouse has them._ +_⁴For react-native apps in development, `MENU` triggers development menu._ +_⁵Only on Android >= 7._ + +Shortcuts with repeated keys are executed by releasing and pressing the key a +second time. For example, to execute "Expand settings panel": + + 1. Press and keep pressing MOD. + 2. Then double-press n. + 3. Finally, release MOD. + +All Ctrl+_key_ shortcuts are forwarded to the device, so they are +handled by the active application. diff --git a/doc/tunnels.md b/doc/tunnels.md new file mode 100644 index 00000000..987a0293 --- /dev/null +++ b/doc/tunnels.md @@ -0,0 +1,123 @@ +# Tunnels + +Scrcpy is designed to mirror local Android devices. Tunnels allow to connect to +a remote device (e.g. over the Internet). + +To connect to a remote device, it is possible to connect a local `adb` client to +a remote `adb` server (provided they use the same version of the _adb_ +protocol). + + +## Remote ADB server + +To connect to a remote _adb server_, make the server listen on all interfaces: + +```bash +adb kill-server +adb -a nodaemon server start +# keep this open +``` + +**Warning: all communications between clients and the _adb server_ are +unencrypted.** + +Suppose that this server is accessible at 192.168.1.2. Then, from another +terminal, run `scrcpy`: + +```bash +# in bash +export ADB_SERVER_SOCKET=tcp:192.168.1.2:5037 +scrcpy --tunnel-host=192.168.1.2 +``` + +```cmd +:: in cmd +set ADB_SERVER_SOCKET=tcp:192.168.1.2:5037 +scrcpy --tunnel-host=192.168.1.2 +``` + +```powershell +# in PowerShell +$env:ADB_SERVER_SOCKET = 'tcp:192.168.1.2:5037' +scrcpy --tunnel-host=192.168.1.2 +``` + +By default, `scrcpy` uses the local port used for `adb forward` tunnel +establishment (typically `27183`, see `--port`). It is also possible to force a +different tunnel port (it may be useful in more complex situations, when more +redirections are involved): + +``` +scrcpy --tunnel-port=1234 +``` + + +## SSH tunnel + +To communicate with a remote _adb server_ securely, it is preferable to use an +SSH tunnel. + +First, make sure the _adb server_ is running on the remote computer: + +```bash +adb start-server +``` + +Then, establish an SSH tunnel: + +```bash +# local 5038 --> remote 5037 +# local 27183 <-- remote 27183 +ssh -CN -L5038:localhost:5037 -R27183:localhost:27183 your_remote_computer +# keep this open +``` + +From another terminal, run `scrcpy`: + +```bash +# in bash +export ADB_SERVER_SOCKET=tcp:localhost:5038 +scrcpy +``` + +```cmd +:: in cmd +set ADB_SERVER_SOCKET=tcp:localhost:5038 +scrcpy +``` + +```powershell +# in PowerShell +$env:ADB_SERVER_SOCKET = 'tcp:localhost:5038' +scrcpy +``` + +To avoid enabling remote port forwarding, you could force a forward connection +instead (notice the `-L` instead of `-R`): + +```bash +# local 5038 --> remote 5037 +# local 27183 --> remote 27183 +ssh -CN -L5038:localhost:5037 -L27183:localhost:27183 your_remote_computer +# keep this open +``` + +From another terminal, run `scrcpy`: + +```bash +# in bash +export ADB_SERVER_SOCKET=tcp:localhost:5038 +scrcpy --force-adb-forward +``` + +```cmd +:: in cmd +set ADB_SERVER_SOCKET=tcp:localhost:5038 +scrcpy --force-adb-forward +``` + +```powershell +# in PowerShell +$env:ADB_SERVER_SOCKET = 'tcp:localhost:5038' +scrcpy --force-adb-forward +``` diff --git a/doc/v4l2.md b/doc/v4l2.md new file mode 100644 index 00000000..ea8c0eed --- /dev/null +++ b/doc/v4l2.md @@ -0,0 +1,65 @@ +# Video4Linux + +On Linux, it is possible to send the video stream to a [v4l2] loopback device, +so that the Android device can be opened like a webcam by any v4l2-capable tool. + +[v4l2]: https://en.wikipedia.org/wiki/Video4Linux + +The module `v4l2loopback` must be installed: + +```bash +sudo apt install v4l2loopback-dkms +``` + +To create a v4l2 device: + +```bash +sudo modprobe v4l2loopback +``` + +This will create a new video device in `/dev/videoN`, where `N` is an integer +(more [options](https://github.com/umlaeute/v4l2loopback#options) are available +to create several devices or devices with specific IDs). + +To list the enabled devices: + +```bash +# requires v4l-utils package +v4l2-ctl --list-devices + +# simple but might be sufficient +ls /dev/video* +``` + +To start `scrcpy` using a v4l2 sink: + +```bash +scrcpy --v4l2-sink=/dev/videoN +scrcpy --v4l2-sink=/dev/videoN --no-display # disable mirroring window +``` + +(replace `N` with the device ID, check with `ls /dev/video*`) + +Once enabled, you can open your video stream with a v4l2-capable tool: + +```bash +ffplay -i /dev/videoN +vlc v4l2:///dev/videoN # VLC might add some buffering delay +``` + +For example, you could capture the video within [OBS] or within your video +conference tool. + +[OBS]: https://obsproject.com/ + + +## Buffering + +By default, there is no video buffering, to get the lowest possible latency. + +As for the [video display](video.md#buffering), it is possible to add +buffering to delay the v4l2 stream: + +```bash +scrcpy --v4l2-buffer=300 # add 300ms buffering for v4l2 sink +``` diff --git a/doc/video.md b/doc/video.md new file mode 100644 index 00000000..a2e9d106 --- /dev/null +++ b/doc/video.md @@ -0,0 +1,175 @@ +# Video + +## Size + +By default, scrcpy attempts to mirror at the Android device resolution. + +It might be useful to mirror at a lower definition to increase performance. To +limit both width and height to some maximum value (here 1024): + +```bash +scrcpy --max-size=1024 +scrcpy -m 1024 # short version +``` + +The other dimension is computed so that the Android device aspect ratio is +preserved. That way, a device in 1920×1080 will be mirrored at 1024×576. + +If encoding fails, scrcpy automatically tries again with a lower definition +(unless `--no-downsize-on-error` is enabled). + + +## Bit rate + +The default video bit-rate is 8 Mbps. To change it: + +```bash +scrcpy --video-bit-rate=2M +scrcpy --video-bit-rate=2000000 # equivalent +scrcpy -b 2M # short version +``` + + +## Frame rate + +The capture frame rate can be limited: + +```bash +scrcpy --max-fps=15 +``` + +The actual capture frame rate may be printed to the console: + +``` +scrcpy --print-fps +``` + +It may also be enabled or disabled at anytime with MOD+i +(see [shortcuts](shortcuts.md)). + +The frame rate is intrinsically variable: a new frame is produced only when the +screen content changes. For example, if you play a fullscreen video at 24fps on +your device, you should not get more than 24 frames per second in scrcpy. + + +## Codec + +The video codec can be selected. The possible values are `h264` (default), +`h265` and `av1`: + +```bash +scrcpy --video-codec=h264 # default +scrcpy --video-codec=h265 +scrcpy --video-codec=av1 +``` + +H265 may provide better quality, but H264 should provide lower latency. +AV1 encoders are not common on current Android devices. + +Several encoders may be available on the device. They can be listed by: + +```bash +scrcpy --list-encoders +``` + +Sometimes, the default encoder may have issues or even crash, so it is useful to +try another one: + +```bash +scrcpy --video-codec=h264 --video-encoder='OMX.qcom.video.encoder.avc' +``` + +For advanced usage, to pass arbitrary parameters to the [`MediaFormat`], +check `--video-codec-options` in the manpage or in `scrcpy --help`. + +[`MediaFormat`]: https://developer.android.com/reference/android/media/MediaFormat + + +## Rotation + +The rotation may be applied at 3 different levels: + - The [shortcut](shortcuts.md) MOD+r requests the + device to switch between portrait and landscape (the current running app may + refuse, if it does not 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` rotates only the window content. This only affects the display, + not the recording. It may be changed dynamically at any time using the + [shortcuts](shortcuts.md) MOD+ and + MOD+. + +To lock the mirroring orientation: + +```bash +scrcpy --lock-video-orientation # initial (current) orientation +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 +``` + +To set an initial window rotation: + +```bash +scrcpy --rotation=0 # no rotation +scrcpy --rotation=1 # 90 degrees counterclockwise +scrcpy --rotation=2 # 180 degrees +scrcpy --rotation=3 # 90 degrees clockwise +``` + +## Crop + +The device screen may be cropped to mirror only part of the screen. + +This is useful, for example, to mirror only one eye of the Oculus Go: + +```bash +scrcpy --crop=1224:1440:0:0 # 1224x1440 at offset (0,0) +``` + +The values are expressed in the device natural orientation (portrait for a +phone, landscape for a tablet). + +If `--max-size` is also specified, resizing is applied after cropping. + + +## Buffering + +By default, there is no video buffering, to get the lowest possible latency. + +Buffering can be added to delay the video stream and compensate for jitter to +get a smoother playback (see [#2464]). + +[#2464]: https://github.com/Genymobile/scrcpy/issues/2464 + +The configuration is available independently for the display, +[v4l2 sinks](video.md#video4linux) and [audio](audio.md#buffering) playback. + +```bash +scrcpy --display-buffer=50 # add 50ms buffering for display +scrcpy --v4l2-buffer=300 # add 300ms buffering for v4l2 sink +scrcpy --audio-buffer=200 # set 200ms buffering for audio playback +``` + +They can be applied simultaneously: + +```bash +scrcpy --display-buffer=50 --v4l2-buffer=300 +``` + + +## No display + +It is possible to capture an Android device without displaying a mirroring +window. This option is available if either [recording](recording.md) or +[v4l2](#video4linux) is enabled: + +```bash +scrcpy --v4l2-sink=/dev/video2 --no-display +scrcpy --record=file.mkv --no-display +``` + +## Video4Linux + +See the dedicated [Video4Linux](v4l2.md) page. diff --git a/doc/window.md b/doc/window.md new file mode 100644 index 00000000..b5b73921 --- /dev/null +++ b/doc/window.md @@ -0,0 +1,55 @@ +# Window + +## Title + +By default, the window title is the device model. It can be changed: + +```bash +scrcpy --window-title='My device' +``` + +## Position and size + +The initial window position and size may be specified: + +```bash +scrcpy --window-x=100 --window-y=100 --window-width=800 --window-height=600 +``` + +## Borderless + +To disable window decorations: + +```bash +scrcpy --window-borderless +``` + +## Always on top + +To keep the window always on top: + +```bash +scrcpy --always-on-top +``` + +## Fullscreen + +The app may be started directly in fullscreen: + +```bash +scrcpy --fullscreen +scrcpy -f # short version +``` + +Fullscreen mode can then be toggled dynamically with MOD+f +(see [shortcuts](shortcuts.md)). + + +## Disable screensaver + +By default, _scrcpy_ does not prevent the screensaver from running on the +computer. To disable it: + +```bash +scrcpy --disable-screensaver +``` diff --git a/doc/windows.md b/doc/windows.md new file mode 100644 index 00000000..fd15a5a7 --- /dev/null +++ b/doc/windows.md @@ -0,0 +1,84 @@ +# On Windows + +## Install + +Download the [latest release]: + + - [`scrcpy-win64-v1.25.zip`][direct-win64] + SHA-256: `db65125e9c65acd00359efb7cea9c05f63cc7ccd5833000cd243cc92f5053028` + +[release]: https://github.com/Genymobile/scrcpy/releases/latest +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.25/scrcpy-win64-v1.25.zip + +and extract it. + +Alternatively, you could install it from packages manager, like [Chocolatey]: + +```bash +choco install scrcpy +choco install adb # if you don't have it yet +``` + +or [Scoop]: + + +```bash +scoop install scrcpy +scoop install adb # if you don't have it yet +``` + +[Chocolatey]: https://chocolatey.org/ +[Scoop]: https://scoop.sh + +_See [build.md](build.md) to build and install the app manually._ + + +## Run + +Scrcpy is a command line application: it is mainly intended to be executed from +a terminal with command line arguments. + +To open a terminal at the expected location, double-click on +`open_a_terminal_here.bat` in your scrcpy directory, then type your command. For +example, without arguments: + +```bash +scrcpy +``` + +or with arguments (here to disable audio and record to `file.mkv`): + +``` +scrcpy --no-audio --record=file.mkv +``` + +Documentation for command line arguments is available: + - `scrcpy --help` + - on [github](/README.md) + +To start scrcpy directly without opening a terminal, double-click on one of +these files: + - `scrcpy-console.bat`: start with a terminal open (it will close when scrcpy + terminates, unless an error occurs); + - `scrcpy-noconsole.vbs`: start without a terminal (but you won't see any error + message). + +_Avoid double-clicking on `scrcpy.exe` directly: on error, the terminal would +close immediately and you won't have time to read any error message (this +executable is intended to be run from the terminal). Use `scrcpy-console.bat` +instead._ + +If you plan to always use the same arguments, create a file `myscrcpy.bat` +(enable [show file extensions] to avoid confusion) containing your command, For +example: + +```bash +scrcpy --prefer-text --turn-screen-off --stay-awake +``` + +[show file extensions]: https://www.howtogeek.com/205086/beginner-how-to-make-windows-show-file-extensions/ + +Then just double-click on that file. + +You could also edit (a copy of) `scrcpy-console.bat` or `scrcpy-noconsole.vbs` +to add some arguments. From f1b2d6bbbb5afd1f95913a49b94eda7cfb85226b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 12 Mar 2023 01:45:49 +0100 Subject: [PATCH 1489/2244] Bump version to 2.0 --- app/scrcpy-windows.rc | 2 +- meson.build | 2 +- server/build.gradle | 4 ++-- server/build_without_gradle.sh | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/scrcpy-windows.rc b/app/scrcpy-windows.rc index e20453c1..e9eb1903 100644 --- a/app/scrcpy-windows.rc +++ b/app/scrcpy-windows.rc @@ -13,7 +13,7 @@ BEGIN VALUE "LegalCopyright", "Romain Vimont, Genymobile" VALUE "OriginalFilename", "scrcpy.exe" VALUE "ProductName", "scrcpy" - VALUE "ProductVersion", "1.25" + VALUE "ProductVersion", "2.0" END END BLOCK "VarFileInfo" diff --git a/meson.build b/meson.build index 0d25085a..ac16c23b 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('scrcpy', 'c', - version: '1.25', + version: '2.0', meson_version: '>= 0.48', default_options: [ 'c_std=c11', diff --git a/server/build.gradle b/server/build.gradle index 1d80d15f..ce234d10 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -7,8 +7,8 @@ android { applicationId "com.genymobile.scrcpy" minSdkVersion 21 targetSdkVersion 33 - versionCode 12500 - versionName "1.25" + versionCode 20000 + versionName "2.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index 6677844c..3201034f 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.25 +SCRCPY_VERSION_NAME=2.0 PLATFORM=${ANDROID_PLATFORM:-33} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-33.0.0} From abc1be4872ee1869b307405b091d736b7cd2e0ff Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 12 Mar 2023 02:37:49 +0100 Subject: [PATCH 1490/2244] Update links to v2.0 --- README.md | 2 +- doc/build.md | 6 +++--- doc/windows.md | 9 ++++++--- install_release.sh | 4 ++-- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index bc6f3bd3..a97822a5 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# scrcpy (v1.25) +# scrcpy (v2.0) scrcpy diff --git a/doc/build.md b/doc/build.md index 31a04cfb..86d9436a 100644 --- a/doc/build.md +++ b/doc/build.md @@ -233,10 +233,10 @@ install` must be run as root)._ #### Option 2: Use prebuilt server - - [`scrcpy-server-v1.25`][direct-scrcpy-server] - SHA-256: `ce0306c7bbd06ae72f6d06f7ec0ee33774995a65de71e0a83813ecb67aec9bdb` + - [`scrcpy-server-v2.0`][direct-scrcpy-server] + SHA-256: `9e241615f578cd690bb43311000debdecf6a9c50a7082b001952f18f6f21ddc2` -[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.25/scrcpy-server-v1.25 +[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v2.0/scrcpy-server-v2.0 Download the prebuilt server somewhere, and specify its path during the Meson configuration: diff --git a/doc/windows.md b/doc/windows.md index fd15a5a7..814cda9d 100644 --- a/doc/windows.md +++ b/doc/windows.md @@ -4,11 +4,14 @@ Download the [latest release]: - - [`scrcpy-win64-v1.25.zip`][direct-win64] - SHA-256: `db65125e9c65acd00359efb7cea9c05f63cc7ccd5833000cd243cc92f5053028` + - [`scrcpy-win64-v2.0.zip`][direct-win64] (64-bit) + SHA-256: `ae4c8d37a496b43f8974ba8f07f708e22a9570ba0cddc3dc3a36edbccd4d2a20` + - [`scrcpy-win32-v2.0.zip`][direct-win32] (32-bit) + SHA-256: `15d98c02cb0e0bbd84f8b5d54991e0f6925569b1286a86a40743944fcb1c2d8c` [release]: https://github.com/Genymobile/scrcpy/releases/latest -[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.25/scrcpy-win64-v1.25.zip +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v2.0/scrcpy-win64-v2.0.zip +[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v2.0/scrcpy-win32-v2.0.zip and extract it. diff --git a/install_release.sh b/install_release.sh index 319aaa4f..609c9556 100755 --- a/install_release.sh +++ b/install_release.sh @@ -2,8 +2,8 @@ set -e BUILDDIR=build-auto -PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v1.25/scrcpy-server-v1.25 -PREBUILT_SERVER_SHA256=ce0306c7bbd06ae72f6d06f7ec0ee33774995a65de71e0a83813ecb67aec9bdb +PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v2.0/scrcpy-server-v2.0 +PREBUILT_SERVER_SHA256=9e241615f578cd690bb43311000debdecf6a9c50a7082b001952f18f6f21ddc2 echo "[scrcpy] Downloading prebuilt server..." wget "$PREBUILT_SERVER_URL" -O scrcpy-server From cbc638c6baa31b989ea40ac22dc109ae6c1cce60 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 12 Mar 2023 02:44:42 +0100 Subject: [PATCH 1491/2244] Fix broken link in shortcuts documentation --- doc/shortcuts.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/shortcuts.md b/doc/shortcuts.md index 06cae428..6528e7b4 100644 --- a/doc/shortcuts.md +++ b/doc/shortcuts.md @@ -49,7 +49,7 @@ _[Super] is typically the Windows or Cmd key._ | Enable/disable FPS counter (on stdout) | MOD+i | Pinch-to-zoom | Ctrl+_click-and-move_ | Drag & drop APK file | Install APK from computer - | Drag & drop non-APK file | [Push file to device](#push-file-to-device) + | Drag & drop non-APK file | [Push file to device](control.md#push-file-to-device) _¹Double-click on black borders to remove them._ _²Right-click turns the screen on if it was off, presses BACK otherwise._ From e5aa2ce01f638ebe083c821fd204a45d9d8f317d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 12 Mar 2023 02:59:44 +0100 Subject: [PATCH 1492/2244] Fix broken link in Windows download page --- doc/windows.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/windows.md b/doc/windows.md index 814cda9d..1f0e3b81 100644 --- a/doc/windows.md +++ b/doc/windows.md @@ -9,7 +9,7 @@ Download the [latest release]: - [`scrcpy-win32-v2.0.zip`][direct-win32] (32-bit) SHA-256: `15d98c02cb0e0bbd84f8b5d54991e0f6925569b1286a86a40743944fcb1c2d8c` -[release]: https://github.com/Genymobile/scrcpy/releases/latest +[latest release]: https://github.com/Genymobile/scrcpy/releases/latest [direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v2.0/scrcpy-win64-v2.0.zip [direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v2.0/scrcpy-win32-v2.0.zip From 6b769675fa68e60c9765022e43c4d7b1e329353a Mon Sep 17 00:00:00 2001 From: Ruoyu Zhong Date: Sun, 12 Mar 2023 14:23:35 +0800 Subject: [PATCH 1493/2244] Fix an "expected expression" error In C, a label can only be followed by a statement, not a declaration. An error in `app/src/screen.c` violated this, and led to a build error with an error message similar to the one below: ../app/src/screen.c:821:13: error: expected expression bool ok = sc_screen_init_size(screen); ^ /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/13.0.0/include/stdbool.h:15:14: note: expanded from macro 'bool' #define bool _Bool ^ ../app/src/screen.c:822:18: error: use of undeclared identifier 'ok' if (!ok) { ^ 2 errors generated. This could be fixed by introducing a new block (or compound statement; as is already being done in the next `case`). That is a statement. Fixes #3785 PR #3787 Signed-off-by: Ruoyu Zhong Signed-off-by: Romain Vimont --- app/src/screen.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/screen.c b/app/src/screen.c index f74fd8a5..b00b0d05 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -816,7 +816,7 @@ sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event) { bool relative_mode = sc_screen_is_relative_mode(screen); switch (event->type) { - case SC_EVENT_SCREEN_INIT_SIZE: + case SC_EVENT_SCREEN_INIT_SIZE: { // The initial size is passed via screen->frame_size bool ok = sc_screen_init_size(screen); if (!ok) { @@ -824,6 +824,7 @@ sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event) { return false; } return true; + } case SC_EVENT_NEW_FRAME: { bool ok = sc_screen_update_frame(screen); if (!ok) { From 80a6fa7a01f019e59e95bfcca47de1a80fe17c6a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 12 Mar 2023 08:37:08 +0100 Subject: [PATCH 1494/2244] Fix comparison warning An int was compared with an unsigned: ../app/src/audio_player.c:290:27: warning: comparison of integers of different signs: 'int' and 'unsigned int' [-Wsign-compare] if (abs(diff) < ap->sample_rate / 1000) { ~~~~~~~~~ ^ ~~~~~~~~~~~~~~~~~~~~~~ --- app/src/audio_player.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/audio_player.c b/app/src/audio_player.c index 5abc9088..bba39acb 100644 --- a/app/src/audio_player.c +++ b/app/src/audio_player.c @@ -287,7 +287,7 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink, float avg = sc_average_get(&ap->avg_buffering); int diff = ap->target_buffering - avg; - if (abs(diff) < ap->sample_rate / 1000) { + if (abs(diff) < (int) ap->sample_rate / 1000) { // Do not compensate for less than 1ms, the error is just noise diff = 0; } else if (diff < 0 && buffered_samples < ap->target_buffering) { From 02586cf21f18d52160a982e9b55e19d8b0b9993f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 12 Mar 2023 08:54:42 +0100 Subject: [PATCH 1495/2244] Fix build issue on FFmpeg < 5.1 An include was missing. Fixes #3783 --- app/src/demuxer.c | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/demuxer.c b/app/src/demuxer.c index 96303155..5a613505 100644 --- a/app/src/demuxer.c +++ b/app/src/demuxer.c @@ -1,6 +1,7 @@ #include "demuxer.h" #include +#include #include #include From cbca79b95baa2b2cf39b72d0a0b2b3de0725959a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 12 Mar 2023 12:39:05 +0100 Subject: [PATCH 1496/2244] Fix v4l2 sink The codec id to write as codec parameters is the one from the v4l2 encoder, not from the decoder. Regression introduced by be985b8242e0626288674b84c5039725170f8f0c. Fixes #3795 --- app/src/v4l2_sink.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/v4l2_sink.c b/app/src/v4l2_sink.c index 717d2bd5..3b3eb8d0 100644 --- a/app/src/v4l2_sink.c +++ b/app/src/v4l2_sink.c @@ -210,6 +210,9 @@ sc_v4l2_sink_open(struct sc_v4l2_sink *vs, const AVCodecContext *ctx) { goto error_avformat_free_context; } + // The codec is from the v4l2 encoder, not from the decoder + ostream->codecpar->codec_id = encoder->id; + int ret = avio_open(&vs->format_ctx->pb, vs->device_name, AVIO_FLAG_WRITE); if (ret < 0) { LOGE("Failed to open output device: %s", vs->device_name); From 5899af6a2fda6c326912a7d49bae268c0c60b71c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 12 Mar 2023 21:08:30 +0100 Subject: [PATCH 1497/2244] Add blogpost link about scrcpy 2.0 --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a97822a5..858ba843 100644 --- a/README.md +++ b/README.md @@ -90,10 +90,11 @@ documented in the following pages: - [Introducing scrcpy][article-intro] - [Scrcpy now works wirelessly][article-tcpip] +- [Scrcpy 2.0, with audio][article-scrcpy2] [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-scrcpy2]: https://blog.rom1v.com/2023/03/scrcpy-2-0-with-audio/ ## Contact From fb61b779a6da299b1f93ec598b2332af9d387416 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 13 Mar 2023 08:40:31 +0100 Subject: [PATCH 1498/2244] Add references to prerequisites Users sometimes only read the OS-specific instructions, they must be aware of the prerequisites. --- README.md | 2 +- doc/linux.md | 2 ++ doc/macos.md | 2 ++ doc/windows.md | 2 ++ 4 files changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 858ba843..2dfb6686 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ Its features include: - [OTG mode](doc/hid-otg.md#otg) - and more… -## Requirements +## Prerequisites The Android device requires at least API 21 (Android 5.0). diff --git a/doc/linux.md b/doc/linux.md index 3b0c560d..bc354959 100644 --- a/doc/linux.md +++ b/doc/linux.md @@ -61,6 +61,8 @@ _See [build.md](build.md) to build and install the app manually._ ## Run +_Make sure that your device meets the [prerequisites](/README.md#prerequisites)._ + Once installed, run from a terminal: ```bash diff --git a/doc/macos.md b/doc/macos.md index 3092dbdc..35d90e9d 100644 --- a/doc/macos.md +++ b/doc/macos.md @@ -29,6 +29,8 @@ _See [build.md](build.md) to build and install the app manually._ ## Run +_Make sure that your device meets the [prerequisites](/README.md#prerequisites)._ + Once installed, run from a terminal: ```bash diff --git a/doc/windows.md b/doc/windows.md index 1f0e3b81..521ad45e 100644 --- a/doc/windows.md +++ b/doc/windows.md @@ -38,6 +38,8 @@ _See [build.md](build.md) to build and install the app manually._ ## Run +_Make sure that your device meets the [prerequisites](/README.md#prerequisites)._ + Scrcpy is a command line application: it is mainly intended to be executed from a terminal with command line arguments. From 1a80333747de602b03ca7ae6b8e58790eeefc3dc Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 13 Mar 2023 10:17:08 +0100 Subject: [PATCH 1499/2244] Replace link to enable USB debugging in README Link to a more relevant page in the official documentation to enable USB debugging. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2dfb6686..8b9e4d46 100644 --- a/README.md +++ b/README.md @@ -41,9 +41,9 @@ The Android device requires at least API 21 (Android 5.0). [Audio forwarding](doc/audio.md) is supported from API 30 (Android 11). -Make sure you [enabled adb debugging][enable-adb] on your device(s). +Make sure you [enabled USB debugging][enable-adb] on your device(s). -[enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling +[enable-adb]: https://developer.android.com/studio/debug/dev-options#enable On some devices, you also need to enable [an additional option][control] `USB debugging (Security Settings)` (this is an item different from `USB debugging`) From 2eced46a37e5d73c23f37e031aab4bfcc1f589b0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 14 Mar 2023 19:21:43 +0100 Subject: [PATCH 1500/2244] Update broken link in documentation The Android documentation has been updated. --- doc/device.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/device.md b/doc/device.md index c7e1ec04..b6ae2338 100644 --- a/doc/device.md +++ b/doc/device.md @@ -107,10 +107,10 @@ with the device IP address you found)_. 7. Run `scrcpy` as usual. 8. Run `adb disconnect` once you're done. -Since Android 11, a [Wireless debugging option][adb-wireless] allows to bypass +Since Android 11, a [wireless debugging option][adb-wireless] allows to bypass having to physically connect your device directly to your computer. -[adb-wireless]: https://developer.android.com/studio/command-line/adb#connect-to-a-device-over-wi-fi-android-11+ +[adb-wireless]: https://developer.android.com/studio/command-line/adb#wireless-android11-command-line ## Autostart From 337d6c2fd351226ede8b7b2b164ffd3d67a6953f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 14 Mar 2023 19:26:37 +0100 Subject: [PATCH 1501/2244] Fail on empty AudioRecord read() If read() returns 0, then there is no data. According to the documentation, it happens if the buffer is not a direct buffer: Refs #3812 --- server/src/main/java/com/genymobile/scrcpy/AudioCapture.java | 2 +- server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java b/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java index 9228e3d7..6bb3ce23 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java @@ -111,7 +111,7 @@ public final class AudioCapture { @TargetApi(Build.VERSION_CODES.N) public int read(ByteBuffer directBuffer, int size, MediaCodec.BufferInfo outBufferInfo) { int r = recorder.read(directBuffer, size); - if (r < 0) { + if (r <= 0) { return r; } diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java b/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java index 24d685c5..d3459831 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java @@ -92,7 +92,7 @@ public final class AudioEncoder implements AsyncProcessor { InputTask task = inputTasks.take(); ByteBuffer buffer = mediaCodec.getInputBuffer(task.index); int r = capture.read(buffer, READ_SIZE, bufferInfo); - if (r < 0) { + if (r <= 0) { throw new IOException("Could not read audio: " + r); } From 6ad037ea043debb1fb240912a8a4b639a5e2d96e Mon Sep 17 00:00:00 2001 From: Bernard Cafarelli Date: Tue, 14 Mar 2023 21:51:54 +0100 Subject: [PATCH 1502/2244] Update Gentoo instructions scrcpy is available directly in the distro, drop link to the overlay (which only contains older versions). PR #3816 Signed-off-by: Romain Vimont --- doc/linux.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/doc/linux.md b/doc/linux.md index bc354959..68b4ee10 100644 --- a/doc/linux.md +++ b/doc/linux.md @@ -9,12 +9,10 @@ Scrcpy is packaged in several distributions and package managers: - Debian/Ubuntu: `apt install scrcpy` - Arch Linux: `pacman -S scrcpy` - Fedora: `dnf copr enable zeno/scrcpy && dnf install scrcpy` - - Gentoo: [ebuild][ebuild-link] file + - Gentoo: `emerge scrcpy` - Snap: `snap install scrcpy` - … (see [repology](https://repology.org/project/scrcpy/versions)) -[ebuild-link]: https://github.com/maggu2810/maggu2810-overlay/tree/master/app-mobilephone/scrcpy - ### Latest version However, the packaged version is not always the latest release. To install the From d2b7315ba693e11bd9b9a0421a75b7df8a31c5cc Mon Sep 17 00:00:00 2001 From: Bernard Cafarelli Date: Tue, 14 Mar 2023 21:48:23 +0100 Subject: [PATCH 1503/2244] Fix linux desktop files validation Follow quoting rules from: PR #3817 Fixes #3633 Signed-off-by: Romain Vimont --- app/data/scrcpy-console.desktop | 2 +- app/data/scrcpy.desktop | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/data/scrcpy-console.desktop b/app/data/scrcpy-console.desktop index 47a63ec9..f9921782 100644 --- a/app/data/scrcpy-console.desktop +++ b/app/data/scrcpy-console.desktop @@ -5,7 +5,7 @@ Comment=Display and control your Android device # For some users, the PATH or ADB environment variables are set from the shell # startup file, like .bashrc or .zshrc… Run an interactive shell to get # environment correctly initialized. -Exec=/bin/bash --norc --noprofile -i -c '"$SHELL" -i -c scrcpy || read -p "Press any key to quit..."' +Exec=/bin/bash --norc --noprofile -i -c "\"\\$SHELL\" -i -c scrcpy || read -p 'Press any key to quit...'" Icon=scrcpy Terminal=true Type=Application diff --git a/app/data/scrcpy.desktop b/app/data/scrcpy.desktop index 082b75e0..1be86a2b 100644 --- a/app/data/scrcpy.desktop +++ b/app/data/scrcpy.desktop @@ -5,7 +5,7 @@ Comment=Display and control your Android device # For some users, the PATH or ADB environment variables are set from the shell # startup file, like .bashrc or .zshrc… Run an interactive shell to get # environment correctly initialized. -Exec=/bin/sh -c '"$SHELL" -i -c scrcpy' +Exec=/bin/sh -c "\"\\$SHELL\" -i -c scrcpy" Icon=scrcpy Terminal=false Type=Application From 6ba99a62ff51c0ce209db78a9ed17f1633170027 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 14 Mar 2023 22:51:31 +0100 Subject: [PATCH 1504/2244] Split workarounds to fix audio on some devices There were several workarounds applied in a single method. Some of them are specific to Meizu phones, but cause issues on other devices. Split the method to be able to only fill the app context for audio capture without applying the Meizu workarounds. Fixes #3801 --- .../java/com/genymobile/scrcpy/Server.java | 12 +++---- .../com/genymobile/scrcpy/Workarounds.java | 31 ++++++++++++++++--- 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 5800487d..244913cf 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -81,15 +81,15 @@ public final class Server { // But only apply when strictly necessary, since workarounds can cause other issues: // - // - - boolean mustFillAppInfo = Build.BRAND.equalsIgnoreCase("meizu"); + if (Build.BRAND.equalsIgnoreCase("meizu")) { + Workarounds.fillAppInfo(); + } // Before Android 11, audio is not supported. // Since Android 12, we can properly set a context on the AudioRecord. - // Only on Android 11 we must fill app info for the AudioRecord to work. - mustFillAppInfo |= audio && Build.VERSION.SDK_INT == Build.VERSION_CODES.R; - - if (mustFillAppInfo) { - Workarounds.fillAppInfo(); + // Only on Android 11 we must fill the application context for the AudioRecord to work. + if (audio && Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { + Workarounds.fillAppContext(); } List asyncProcessors = new ArrayList<>(); diff --git a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java index 64cc1272..89380ece 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java +++ b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java @@ -10,6 +10,10 @@ import java.lang.reflect.Constructor; import java.lang.reflect.Field; public final class Workarounds { + + private static Class activityThreadClass; + private static Object activityThread; + private Workarounds() { // not instantiable } @@ -28,18 +32,25 @@ public final class Workarounds { } @SuppressLint("PrivateApi,DiscouragedPrivateApi") - public static void fillAppInfo() { - try { + private static void fillActivityThread() throws Exception { + if (activityThread == null) { // ActivityThread activityThread = new ActivityThread(); - Class activityThreadClass = Class.forName("android.app.ActivityThread"); + activityThreadClass = Class.forName("android.app.ActivityThread"); Constructor activityThreadConstructor = activityThreadClass.getDeclaredConstructor(); activityThreadConstructor.setAccessible(true); - Object activityThread = activityThreadConstructor.newInstance(); + activityThread = activityThreadConstructor.newInstance(); // ActivityThread.sCurrentActivityThread = activityThread; Field sCurrentActivityThreadField = activityThreadClass.getDeclaredField("sCurrentActivityThread"); sCurrentActivityThreadField.setAccessible(true); sCurrentActivityThreadField.set(null, activityThread); + } + } + + @SuppressLint("PrivateApi,DiscouragedPrivateApi") + public static void fillAppInfo() { + try { + fillActivityThread(); // ActivityThread.AppBindData appBindData = new ActivityThread.AppBindData(); Class appBindDataClass = Class.forName("android.app.ActivityThread$AppBindData"); @@ -59,6 +70,16 @@ public final class Workarounds { Field mBoundApplicationField = activityThreadClass.getDeclaredField("mBoundApplication"); mBoundApplicationField.setAccessible(true); mBoundApplicationField.set(activityThread, appBindData); + } catch (Throwable throwable) { + // this is a workaround, so failing is not an error + Ln.d("Could not fill app info: " + throwable.getMessage()); + } + } + + @SuppressLint("PrivateApi,DiscouragedPrivateApi") + public static void fillAppContext() { + try { + fillActivityThread(); Application app = Application.class.newInstance(); Field baseField = ContextWrapper.class.getDeclaredField("mBase"); @@ -71,7 +92,7 @@ public final class Workarounds { mInitialApplicationField.set(activityThread, app); } catch (Throwable throwable) { // this is a workaround, so failing is not an error - Ln.d("Could not fill app info: " + throwable.getMessage()); + Ln.d("Could not fill app context: " + throwable.getMessage()); } } } From cba2501254209073d5bb09b6d43e973a4013f6b4 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 14 Mar 2023 23:40:48 +0100 Subject: [PATCH 1505/2244] Add missing auto-completion for --audio-buffer --- app/data/bash-completion/scrcpy | 1 + app/data/zsh-completion/_scrcpy | 1 + 2 files changed, 2 insertions(+) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index c3649364..dd9c9520 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -3,6 +3,7 @@ _scrcpy() { local opts=" --always-on-top --audio-bit-rate= + --audio-buffer= --audio-codec= --audio-codec-options= --audio-encoder= diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index d713761c..e6a3bc2a 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -10,6 +10,7 @@ local arguments arguments=( '--always-on-top[Make scrcpy window always on top \(above other windows\)]' '--audio-bit-rate=[Encode the audio at the given bit-rate]' + '--audio-buffer=[Configure the audio buffering delay (in milliseconds)]' '--audio-codec=[Select the audio codec]:codec:(opus aac raw)' '--audio-codec-options=[Set a list of comma-separated key\:type=value options for the device audio encoder]' '--audio-encoder=[Use a specific MediaCodec audio encoder]' From 4755b97908bdf8e7d5b9ac1f3557da7ae083f076 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 14 Mar 2023 23:45:08 +0100 Subject: [PATCH 1506/2244] Fix bash auto-completion handling Options having an argument impossible to auto-complete must be handled separately. --- app/data/bash-completion/scrcpy | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index dd9c9520..3aa991b8 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -116,20 +116,25 @@ _scrcpy() { COMPREPLY=($(compgen -W "$("${ADB:-adb}" devices | awk '$2 == "device" {print $1}')" -- ${cur})) return ;; - -b|--video-bit-rate \ - |--codec-options \ + --audio-bit-rate \ + |--audio-buffer \ + |-b|--video-bit-rate \ + |--audio-codec-options \ + |--audio-encoder \ |--crop \ |--display \ |--display-buffer \ - |--encoder \ |--max-fps \ |-m|--max-size \ |-p|--port \ |--push-target \ + |--rotation \ |--tunnel-host \ |--tunnel-port \ |--v4l2-buffer \ |--v4l2-sink \ + |--video-codec-options \ + |--video-encoder \ |--tcpip \ |--window-*) # Option accepting an argument, but nothing to auto-complete From 39544f34b4d63a6cdb91802a9c6136b3d6a6003a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 13 Mar 2023 09:23:02 +0100 Subject: [PATCH 1507/2244] Add --audio-output-buffer On some systems, the SDL audio callback is not called frequently enough (for example it requests 5ms of samples every 10ms), because the output buffer is too small. By default, we want to use a small value (5ms) to minimize latency and buffer underrun, but if it does not work well, users need a way to increase it. Refs #3793 --- app/data/bash-completion/scrcpy | 2 ++ app/data/zsh-completion/_scrcpy | 1 + app/scrcpy.1 | 8 ++++++ app/src/audio_player.c | 43 ++++++++++++++++++--------------- app/src/audio_player.h | 7 +++++- app/src/cli.c | 30 +++++++++++++++++++++++ app/src/options.c | 1 + app/src/options.h | 1 + app/src/scrcpy.c | 3 ++- doc/audio.md | 11 +++++++++ 10 files changed, 86 insertions(+), 21 deletions(-) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index 3aa991b8..ae516c34 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -7,6 +7,7 @@ _scrcpy() { --audio-codec= --audio-codec-options= --audio-encoder= + --audio-output-buffer= -b --video-bit-rate= --crop= -d --select-usb @@ -121,6 +122,7 @@ _scrcpy() { |-b|--video-bit-rate \ |--audio-codec-options \ |--audio-encoder \ + |--audio-output-buffer \ |--crop \ |--display \ |--display-buffer \ diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index e6a3bc2a..97bf4f3e 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -14,6 +14,7 @@ arguments=( '--audio-codec=[Select the audio codec]:codec:(opus aac raw)' '--audio-codec-options=[Set a list of comma-separated key\:type=value options for the device audio encoder]' '--audio-encoder=[Use a specific MediaCodec audio encoder]' + '--audio-output-buffer=[Configure the size of the SDL audio output buffer (in milliseconds)]' {-b,--video-bit-rate=}'[Encode the video at the given bit-rate]' '--crop=[\[width\:height\:x\:y\] Crop the device screen on the server]' {-d,--select-usb}'[Use USB device]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 65357686..97a15d1d 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -33,6 +33,14 @@ Lower values decrease the latency, but increase the likelyhood of buffer underru Default is 50. +.TP +.BI "\-\-audio\-output\-buffer ms +Configure the size of the SDL audio output buffer (in milliseconds). + +If you get "robotic" audio playback, you should test with a higher value (10). Do not change this setting otherwise. + +Default is 5. + .TP .BI "\-\-audio\-codec " name Select an audio codec (opus, aac or raw). diff --git a/app/src/audio_player.c b/app/src/audio_player.c index bba39acb..a0c52c62 100644 --- a/app/src/audio_player.c +++ b/app/src/audio_player.c @@ -59,8 +59,6 @@ #define SC_AV_SAMPLE_FMT AV_SAMPLE_FMT_FLT #define SC_SDL_SAMPLE_FMT AUDIO_F32 -#define SC_AUDIO_OUTPUT_BUFFER_MS 5 - #define TO_BYTES(SAMPLES) sc_audiobuf_to_bytes(&ap->buf, (SAMPLES)) #define TO_SAMPLES(BYTES) sc_audiobuf_to_samples(&ap->buf, (BYTES)) @@ -230,8 +228,8 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink, if (played) { uint32_t max_buffered_samples = ap->target_buffering - + 12 * SC_AUDIO_OUTPUT_BUFFER_MS * ap->sample_rate / 1000 - + ap->target_buffering / 10; + + 12 * ap->output_buffer + + ap->target_buffering / 10; if (buffered_samples > max_buffered_samples) { uint32_t skip_samples = buffered_samples - max_buffered_samples; sc_audiobuf_skip(&ap->buf, skip_samples); @@ -246,7 +244,7 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink, // max_initial_buffering samples, this would cause unnecessary delay // (and glitches to compensate) on start. uint32_t max_initial_buffering = ap->target_buffering - + 2 * SC_AUDIO_OUTPUT_BUFFER_MS * ap->sample_rate / 1000; + + 2 * ap->output_buffer; if (buffered_samples > max_initial_buffering) { uint32_t skip_samples = buffered_samples - max_initial_buffering; sc_audiobuf_skip(&ap->buf, skip_samples); @@ -333,11 +331,28 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink, unsigned nb_channels = tmp; #endif + assert(ctx->sample_rate > 0); + assert(!av_sample_fmt_is_planar(SC_AV_SAMPLE_FMT)); + int out_bytes_per_sample = av_get_bytes_per_sample(SC_AV_SAMPLE_FMT); + assert(out_bytes_per_sample > 0); + + ap->sample_rate = ctx->sample_rate; + ap->nb_channels = nb_channels; + ap->out_bytes_per_sample = out_bytes_per_sample; + + ap->target_buffering = ap->target_buffering_delay * ap->sample_rate + / SC_TICK_FREQ; + + uint64_t aout_samples = ap->output_buffer_duration * ap->sample_rate + / SC_TICK_FREQ; + assert(aout_samples <= 0xFFFF); + ap->output_buffer = (uint16_t) aout_samples; + SDL_AudioSpec desired = { .freq = ctx->sample_rate, .format = SC_SDL_SAMPLE_FMT, .channels = nb_channels, - .samples = SC_AUDIO_OUTPUT_BUFFER_MS * ctx->sample_rate / 1000, + .samples = aout_samples, .callback = sc_audio_player_sdl_callback, .userdata = ap, }; @@ -356,11 +371,6 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink, } ap->swr_ctx = swr_ctx; - assert(ctx->sample_rate > 0); - assert(!av_sample_fmt_is_planar(SC_AV_SAMPLE_FMT)); - int out_bytes_per_sample = av_get_bytes_per_sample(SC_AV_SAMPLE_FMT); - assert(out_bytes_per_sample > 0); - #ifdef SCRCPY_LAVU_HAS_CHLAYOUT av_opt_set_chlayout(swr_ctx, "in_chlayout", &ctx->ch_layout, 0); av_opt_set_chlayout(swr_ctx, "out_chlayout", &ctx->ch_layout, 0); @@ -383,13 +393,6 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink, goto error_free_swr_ctx; } - ap->sample_rate = ctx->sample_rate; - ap->nb_channels = nb_channels; - ap->out_bytes_per_sample = out_bytes_per_sample; - - ap->target_buffering = ap->target_buffering_delay * ap->sample_rate - / SC_TICK_FREQ; - // Use a ring-buffer of the target buffering size plus 1 second between the // producer and the consumer. It's too big on purpose, to guarantee that // the producer and the consumer will be able to access it in parallel @@ -458,8 +461,10 @@ sc_audio_player_frame_sink_close(struct sc_frame_sink *sink) { } void -sc_audio_player_init(struct sc_audio_player *ap, sc_tick target_buffering) { +sc_audio_player_init(struct sc_audio_player *ap, sc_tick target_buffering, + sc_tick output_buffer_duration) { ap->target_buffering_delay = target_buffering; + ap->output_buffer_duration = output_buffer_duration; static const struct sc_frame_sink_ops ops = { .open = sc_audio_player_frame_sink_open, diff --git a/app/src/audio_player.h b/app/src/audio_player.h index 4dd9c4dc..a03e9e35 100644 --- a/app/src/audio_player.h +++ b/app/src/audio_player.h @@ -27,6 +27,10 @@ struct sc_audio_player { sc_tick target_buffering_delay; uint32_t target_buffering; // in samples + // SDL audio output buffer size. + sc_tick output_buffer_duration; + uint16_t output_buffer; + // Audio buffer to communicate between the receiver and the SDL audio // callback (protected by SDL_AudioDeviceLock()) struct sc_audiobuf buf; @@ -80,6 +84,7 @@ struct sc_audio_player_callbacks { }; void -sc_audio_player_init(struct sc_audio_player *ap, sc_tick target_buffering); +sc_audio_player_init(struct sc_audio_player *ap, sc_tick target_buffering, + sc_tick audio_output_buffer); #endif diff --git a/app/src/cli.c b/app/src/cli.c index cb101a51..d6d9f41d 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -71,6 +71,7 @@ enum { OPT_LIST_DISPLAYS, OPT_REQUIRE_AUDIO, OPT_AUDIO_BUFFER, + OPT_AUDIO_OUTPUT_BUFFER, }; struct sc_option { @@ -129,6 +130,16 @@ static const struct sc_option options[] = { "likelyhood of buffer underrun (causing audio glitches).\n" "Default is 50.", }, + { + .longopt_id = OPT_AUDIO_OUTPUT_BUFFER, + .longopt = "audio-output-buffer", + .argdesc = "ms", + .text = "Configure the size of the SDL audio output buffer (in " + "milliseconds).\n" + "If you get \"robotic\" audio playback, you should test with " + "a higher value (10). Do not change this setting otherwise.\n" + "Default is 5.", + }, { .longopt_id = OPT_AUDIO_CODEC, .longopt = "audio-codec", @@ -1204,6 +1215,19 @@ parse_buffering_time(const char *s, sc_tick *tick) { return true; } +static bool +parse_audio_output_buffer(const char *s, sc_tick *tick) { + long value; + bool ok = parse_integer_arg(s, &value, false, 0, 1000, + "audio output buffer"); + if (!ok) { + return false; + } + + *tick = SC_TICK_FROM_MS(value); + return true; +} + static bool parse_lock_video_orientation(const char *s, enum sc_lock_video_orientation *lock_mode) { @@ -1831,6 +1855,12 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; } break; + case OPT_AUDIO_OUTPUT_BUFFER: + if (!parse_audio_output_buffer(optarg, + &opts->audio_output_buffer)) { + return false; + } + break; default: // getopt prints the error message on stderr return false; diff --git a/app/src/options.c b/app/src/options.c index 68c16d53..8b99f6f3 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -44,6 +44,7 @@ const struct scrcpy_options scrcpy_options_default = { .display_buffer = 0, .v4l2_buffer = 0, .audio_buffer = SC_TICK_FROM_MS(50), + .audio_output_buffer = SC_TICK_FROM_MS(5), #ifdef HAVE_USB .otg = false, #endif diff --git a/app/src/options.h b/app/src/options.h index 06b4ddfa..c41e2757 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -127,6 +127,7 @@ struct scrcpy_options { sc_tick display_buffer; sc_tick v4l2_buffer; sc_tick audio_buffer; + sc_tick audio_output_buffer; #ifdef HAVE_USB bool otg; #endif diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 9e5ec6f0..efa69d31 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -688,7 +688,8 @@ aoa_hid_end: sc_frame_source_add_sink(src, &s->screen.frame_sink); if (options->audio) { - sc_audio_player_init(&s->audio_player, options->audio_buffer); + sc_audio_player_init(&s->audio_player, options->audio_buffer, + options->audio_output_buffer); sc_frame_source_add_sink(&s->audio_decoder.frame_source, &s->audio_player.frame_sink); } diff --git a/doc/audio.md b/doc/audio.md index 3755fe37..6e97b103 100644 --- a/doc/audio.md +++ b/doc/audio.md @@ -88,3 +88,14 @@ avoid glitches and smooth the playback: ``` scrcpy --display-buffer=200 --audio-buffer=200 ``` + +It is also possible to configure another audio buffer (the audio output buffer), +by default set to 5ms. Don't change it, unless you get some [robotic and glitchy +sound][#3793]: + +```bash +# Only if absolutely necessary +scrcpy --audio-output-buffer=10 +``` + +[#3793]: https://github.com/Genymobile/scrcpy/issues/3793 From 45717733a10b8e73ae70b25722b2ddf5922c1c07 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 15 Mar 2023 00:36:13 +0100 Subject: [PATCH 1508/2244] Document missing Opus encoder error And how to solve it. --- doc/audio.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/doc/audio.md b/doc/audio.md index 3755fe37..c436eb56 100644 --- a/doc/audio.md +++ b/doc/audio.md @@ -35,6 +35,13 @@ scrcpy --audio-codec=aac scrcpy --audio-codec=raw ``` +In particular, if you get the following error: + +> Failed to initialize audio/opus, error 0xfffffffe + +then your device has no Opus encoder: try `scrcpy --audio-codec=aac`. + + Several encoders may be available on the device. They can be listed by: ```bash From d9a644df9c060ae844cee0bf451af608e5284c66 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 15 Mar 2023 10:36:07 +0100 Subject: [PATCH 1509/2244] Clarify V4L2 feature in README The previous formulation could suggest that the device camera could be used as a webcam. This is not the case (yet?). --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8b9e4d46..5bafbe2b 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Its features include: - mirroring with [Android device screen off](doc/device.md#turn-screen-off) - [copy-paste](doc/control.md#copy-paste) in both directions - [configurable quality](doc/video.md) - - Android device [as a webcam (V4L2)](doc/v4l2.md) (Linux-only) + - Android device screen [as a webcam (V4L2)](doc/v4l2.md) (Linux-only) - [physical keyboard/mouse simulation (HID)](doc/hid-otg.md) - [OTG mode](doc/hid-otg.md#otg) - and more… From d7841664f4569b24823df1d153665828a3138e05 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 15 Mar 2023 19:53:40 +0100 Subject: [PATCH 1510/2244] Simplify logic in setScreenPowerMode() Refs Suggested-by: brunoais --- server/src/main/java/com/genymobile/scrcpy/Device.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index b66474b7..3d83f73e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -288,10 +288,7 @@ public final class Device { boolean allOk = true; for (long physicalDisplayId : physicalDisplayIds) { IBinder binder = SurfaceControl.getPhysicalDisplayToken(physicalDisplayId); - boolean ok = SurfaceControl.setDisplayPowerMode(binder, mode); - if (!ok) { - allOk = false; - } + allOk &= SurfaceControl.setDisplayPowerMode(binder, mode); } return allOk; } From 53cb5635cfbe022061e44a39a3f9f6044b0da5e4 Mon Sep 17 00:00:00 2001 From: sixg0000d Date: Thu, 16 Mar 2023 18:22:53 +0800 Subject: [PATCH 1511/2244] Fix pause message The pause terminates only once the Enter key is pressed, not any key. PR #3826 Signed-off-by: Romain Vimont --- app/data/scrcpy-console.desktop | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/data/scrcpy-console.desktop b/app/data/scrcpy-console.desktop index f9921782..0e2f9ab0 100644 --- a/app/data/scrcpy-console.desktop +++ b/app/data/scrcpy-console.desktop @@ -5,7 +5,7 @@ Comment=Display and control your Android device # For some users, the PATH or ADB environment variables are set from the shell # startup file, like .bashrc or .zshrc… Run an interactive shell to get # environment correctly initialized. -Exec=/bin/bash --norc --noprofile -i -c "\"\\$SHELL\" -i -c scrcpy || read -p 'Press any key to quit...'" +Exec=/bin/bash --norc --noprofile -i -c "\"\\$SHELL\" -i -c scrcpy || read -p 'Press Enter to quit...'" Icon=scrcpy Terminal=true Type=Application From a3871130cc540e1391e6576ae55fd0a5902e4b07 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 16 Mar 2023 20:18:52 +0100 Subject: [PATCH 1512/2244] List available encoders on failure When the creation of an encoder fails, log an explicit error message with the list of available encoders. --- .../com/genymobile/scrcpy/AudioEncoder.java | 17 +++++++++++++---- .../com/genymobile/scrcpy/ScreenEncoder.java | 17 +++++++++++++---- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java b/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java index d3459831..f2bba772 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java @@ -271,13 +271,22 @@ public final class AudioEncoder implements AsyncProcessor { try { return MediaCodec.createByCodecName(encoderName); } catch (IllegalArgumentException e) { - Ln.e("Encoder '" + encoderName + "' for " + codec.getName() + " not found\n" + LogUtils.buildAudioEncoderListMessage()); + Ln.e("Audio encoder '" + encoderName + "' for " + codec.getName() + " not found\n" + LogUtils.buildAudioEncoderListMessage()); throw new ConfigurationException("Unknown encoder: " + encoderName); + } catch (IOException e) { + Ln.e("Could not create audio encoder '" + encoderName + "' for " + codec.getName() + "\n" + LogUtils.buildAudioEncoderListMessage()); + throw e; } } - MediaCodec mediaCodec = MediaCodec.createEncoderByType(codec.getMimeType()); - Ln.d("Using audio encoder: '" + mediaCodec.getName() + "'"); - return mediaCodec; + + try { + MediaCodec mediaCodec = MediaCodec.createEncoderByType(codec.getMimeType()); + Ln.d("Using audio encoder: '" + mediaCodec.getName() + "'"); + return mediaCodec; + } catch (IOException | IllegalArgumentException e) { + Ln.e("Could not create default audio encoder for " + codec.getName() + "\n" + LogUtils.buildAudioEncoderListMessage()); + throw e; + } } private class EncoderCallback extends MediaCodec.Callback { diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index 015cc993..528cd327 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -202,13 +202,22 @@ public class ScreenEncoder implements Device.RotationListener { try { return MediaCodec.createByCodecName(encoderName); } catch (IllegalArgumentException e) { - Ln.e("Encoder '" + encoderName + "' for " + codec.getName() + " not found\n" + LogUtils.buildVideoEncoderListMessage()); + Ln.e("Video encoder '" + encoderName + "' for " + codec.getName() + " not found\n" + LogUtils.buildVideoEncoderListMessage()); throw new ConfigurationException("Unknown encoder: " + encoderName); + } catch (IOException e) { + Ln.e("Could not create video encoder '" + encoderName + "' for " + codec.getName() + "\n" + LogUtils.buildVideoEncoderListMessage()); + throw e; } } - MediaCodec mediaCodec = MediaCodec.createEncoderByType(codec.getMimeType()); - Ln.d("Using encoder: '" + mediaCodec.getName() + "'"); - return mediaCodec; + + try { + MediaCodec mediaCodec = MediaCodec.createEncoderByType(codec.getMimeType()); + Ln.d("Using video encoder: '" + mediaCodec.getName() + "'"); + return mediaCodec; + } catch (IOException | IllegalArgumentException e) { + Ln.e("Could not create default video encoder for " + codec.getName() + "\n" + LogUtils.buildVideoEncoderListMessage()); + throw e; + } } private static MediaFormat createFormat(String videoMimeType, int bitRate, int maxFps, List codecOptions) { From 02f4ff7534649153d6f87b05a0757431a2d0ee5f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 16 Mar 2023 21:54:06 +0100 Subject: [PATCH 1513/2244] Make 3 attempts to start AudioRecord On Android 11, a fake popup must be briefly opened to make the system think that the shell app is in the foreground so that audio may be recorded. Making the shell app foreground may take some time depending on the device, so make 3 attempts, waiting 100ms before each. Fixes #3796 --- .../com/genymobile/scrcpy/AudioCapture.java | 37 +++++++++++++------ 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java b/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java index 6bb3ce23..e15c8285 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java @@ -72,8 +72,6 @@ public final class AudioCapture { intent.addCategory(Intent.CATEGORY_LAUNCHER); intent.setComponent(new ComponentName(FakeContext.PACKAGE_NAME, "com.android.shell.HeapDumpActivity")); ServiceManager.getActivityManager().startActivityAsUserWithFeature(intent); - // Wait for activity to start - SystemClock.sleep(150); } } } @@ -84,18 +82,35 @@ public final class AudioCapture { } } + private void tryStartRecording(int attempts, int delayMs) throws AudioCaptureForegroundException { + while (attempts-- > 0) { + // Wait for activity to start + SystemClock.sleep(delayMs); + try { + recorder = createAudioRecord(); + recorder.startRecording(); + return; // it worked + } catch (UnsupportedOperationException e) { + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { + if (attempts == 0) { + Ln.e("Failed to start audio capture"); + Ln.e("On Android 11, audio capture must be started in the foreground, make sure that the device is unlocked when starting " + + "scrcpy."); + throw new AudioCaptureForegroundException(); + } else { + Ln.d("Failed to start audio capture, retrying..."); + } + } else { + throw e; + } + } + } + } + public void start() throws AudioCaptureForegroundException { startWorkaroundAndroid11(); try { - recorder = createAudioRecord(); - recorder.startRecording(); - } catch (UnsupportedOperationException e) { - if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { - Ln.e("Failed to start audio capture"); - Ln.e("On Android 11, it is only possible to capture in foreground, make sure that the device is unlocked when starting scrcpy."); - throw new AudioCaptureForegroundException(); - } - throw e; + tryStartRecording(3, 100); } finally { stopWorkaroundAndroid11(); } From 3626d90004c9946320152564a375e56f9c5030f4 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 16 Mar 2023 22:19:21 +0100 Subject: [PATCH 1514/2244] Use separate audio capture code for Android 11 The code to start audio capture is more complicated for Android 11 (launch a fake popup, wait, make several attempts, close the shell package). Use a distinct code path specific to Android 11. --- .../com/genymobile/scrcpy/AudioCapture.java | 68 +++++++++---------- 1 file changed, 33 insertions(+), 35 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java b/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java index e15c8285..c940db16 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java @@ -59,27 +59,21 @@ public final class AudioCapture { } private static void startWorkaroundAndroid11() { - if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { - // Android 11 requires Apps to be at foreground to record audio. - // Normally, each App has its own user ID, so Android checks whether the requesting App has the user ID that's at the foreground. - // But scrcpy server is NOT an App, it's a Java application started from Android shell, so it has the same user ID (2000) with Android - // shell ("com.android.shell"). - // If there is an Activity from Android shell running at foreground, then the permission system will believe scrcpy is also in the - // foreground. - if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { - Intent intent = new Intent(Intent.ACTION_MAIN); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.addCategory(Intent.CATEGORY_LAUNCHER); - intent.setComponent(new ComponentName(FakeContext.PACKAGE_NAME, "com.android.shell.HeapDumpActivity")); - ServiceManager.getActivityManager().startActivityAsUserWithFeature(intent); - } - } + // Android 11 requires Apps to be at foreground to record audio. + // Normally, each App has its own user ID, so Android checks whether the requesting App has the user ID that's at the foreground. + // But scrcpy server is NOT an App, it's a Java application started from Android shell, so it has the same user ID (2000) with Android + // shell ("com.android.shell"). + // If there is an Activity from Android shell running at foreground, then the permission system will believe scrcpy is also in the + // foreground. + Intent intent = new Intent(Intent.ACTION_MAIN); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.addCategory(Intent.CATEGORY_LAUNCHER); + intent.setComponent(new ComponentName(FakeContext.PACKAGE_NAME, "com.android.shell.HeapDumpActivity")); + ServiceManager.getActivityManager().startActivityAsUserWithFeature(intent); } private static void stopWorkaroundAndroid11() { - if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { - ServiceManager.getActivityManager().forceStopPackage(FakeContext.PACKAGE_NAME); - } + ServiceManager.getActivityManager().forceStopPackage(FakeContext.PACKAGE_NAME); } private void tryStartRecording(int attempts, int delayMs) throws AudioCaptureForegroundException { @@ -87,32 +81,36 @@ public final class AudioCapture { // Wait for activity to start SystemClock.sleep(delayMs); try { - recorder = createAudioRecord(); - recorder.startRecording(); + startRecording(); return; // it worked } catch (UnsupportedOperationException e) { - if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { - if (attempts == 0) { - Ln.e("Failed to start audio capture"); - Ln.e("On Android 11, audio capture must be started in the foreground, make sure that the device is unlocked when starting " - + "scrcpy."); - throw new AudioCaptureForegroundException(); - } else { - Ln.d("Failed to start audio capture, retrying..."); - } + if (attempts == 0) { + Ln.e("Failed to start audio capture"); + Ln.e("On Android 11, audio capture must be started in the foreground, make sure that the device is unlocked when starting " + + "scrcpy."); + throw new AudioCaptureForegroundException(); } else { - throw e; + Ln.d("Failed to start audio capture, retrying..."); } } } } + private void startRecording() { + recorder = createAudioRecord(); + recorder.startRecording(); + } + public void start() throws AudioCaptureForegroundException { - startWorkaroundAndroid11(); - try { - tryStartRecording(3, 100); - } finally { - stopWorkaroundAndroid11(); + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { + startWorkaroundAndroid11(); + try { + tryStartRecording(3, 100); + } finally { + stopWorkaroundAndroid11(); + } + } else { + startRecording(); } } From 55899c091e0b4940516cd811a248bad1a9c8cff4 Mon Sep 17 00:00:00 2001 From: NextDev65 <12612637+NextDev65@users.noreply.github.com> Date: Sun, 19 Mar 2023 21:26:45 -0500 Subject: [PATCH 1515/2244] Fix typo in doc/audio.md The documentation is about audio bit rate, not video bit rate. PR #3839 Signed-off-by: Romain Vimont --- doc/audio.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/audio.md b/doc/audio.md index c436eb56..bd324465 100644 --- a/doc/audio.md +++ b/doc/audio.md @@ -62,7 +62,7 @@ check `--audio-codec-options` in the manpage or in `scrcpy --help`. ## Bit rate -The default video bit-rate is 128Kbps. To change it: +The default audio bit-rate is 128Kbps. To change it: ```bash scrcpy --audio-bit-rate=64K From 478aece68f2b24def8f9be198a733a106b25616f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 20 Mar 2023 08:35:13 +0100 Subject: [PATCH 1516/2244] Replace "bit-rate" with "bit rate" --- app/scrcpy.1 | 4 ++-- app/src/cli.c | 4 ++-- doc/audio.md | 2 +- doc/video.md | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 65357686..1ea65796 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -21,7 +21,7 @@ Make scrcpy window always on top (above other windows). .TP .BI "\-\-audio\-bit\-rate " value -Encode the audio at the given bit\-rate, expressed in bits/s. Unit suffixes are supported: '\fBK\fR' (x1000) and '\fBM\fR' (x1000000). +Encode the audio at the given bit rate, expressed in bits/s. Unit suffixes are supported: '\fBK\fR' (x1000) and '\fBM\fR' (x1000000). Default is 128K (128000). @@ -57,7 +57,7 @@ The available encoders can be listed by \-\-list\-encoders. .TP .BI "\-b, \-\-video\-bit\-rate " value -Encode the video at the given bit\-rate, expressed in bits/s. Unit suffixes are supported: '\fBK\fR' (x1000) and '\fBM\fR' (x1000000). +Encode the video at the given bit rate, expressed in bits/s. Unit suffixes are supported: '\fBK\fR' (x1000) and '\fBM\fR' (x1000000). Default is 8M (8000000). diff --git a/app/src/cli.c b/app/src/cli.c index cb101a51..69559afc 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -116,7 +116,7 @@ static const struct sc_option options[] = { .longopt_id = OPT_AUDIO_BIT_RATE, .longopt = "audio-bit-rate", .argdesc = "value", - .text = "Encode the audio at the given bit-rate, expressed in bits/s. " + .text = "Encode the audio at the given bit rate, expressed in bits/s. " "Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n" "Default is 128K (128000).", }, @@ -160,7 +160,7 @@ static const struct sc_option options[] = { .shortopt = 'b', .longopt = "video-bit-rate", .argdesc = "value", - .text = "Encode the video at the given bit-rate, expressed in bits/s. " + .text = "Encode the video at the given bit rate, expressed in bits/s. " "Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n" "Default is 8M (8000000).", }, diff --git a/doc/audio.md b/doc/audio.md index bd324465..bf286809 100644 --- a/doc/audio.md +++ b/doc/audio.md @@ -62,7 +62,7 @@ check `--audio-codec-options` in the manpage or in `scrcpy --help`. ## Bit rate -The default audio bit-rate is 128Kbps. To change it: +The default audio bit rate is 128Kbps. To change it: ```bash scrcpy --audio-bit-rate=64K diff --git a/doc/video.md b/doc/video.md index a2e9d106..a0dbf7dd 100644 --- a/doc/video.md +++ b/doc/video.md @@ -21,7 +21,7 @@ If encoding fails, scrcpy automatically tries again with a lower definition ## Bit rate -The default video bit-rate is 8 Mbps. To change it: +The default video bit rate is 8 Mbps. To change it: ```bash scrcpy --video-bit-rate=2M From 57f879d68a72c0d84f17d71ddf49c3b564bc614c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 17 Mar 2023 22:06:58 +0100 Subject: [PATCH 1517/2244] Adapt clipboard wrappers to Android 14 A new deviceId parameter has been added. Fixes #3784 --- .../scrcpy/wrappers/ClipboardManager.java | 105 +++++++++++++----- 1 file changed, 75 insertions(+), 30 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 0c1777ec..cb176cc3 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java @@ -16,9 +16,9 @@ public class ClipboardManager { private Method getPrimaryClipMethod; private Method setPrimaryClipMethod; private Method addPrimaryClipChangedListener; - private boolean alternativeGetMethod; - private boolean alternativeSetMethod; - private boolean alternativeAddListenerMethod; + private int getMethodVersion; + private int setMethodVersion; + private int addListenerMethodVersion; public ClipboardManager(IInterface manager) { this.manager = manager; @@ -31,9 +31,15 @@ public class ClipboardManager { } else { try { getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class); - } catch (NoSuchMethodException e) { - getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class); - alternativeGetMethod = true; + getMethodVersion = 0; + } catch (NoSuchMethodException e1) { + try { + getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class); + getMethodVersion = 1; + } catch (NoSuchMethodException e2) { + getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class, int.class); + getMethodVersion = 2; + } } } } @@ -47,41 +53,62 @@ public class ClipboardManager { } else { try { setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, int.class); - } catch (NoSuchMethodException e) { - setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class); - alternativeSetMethod = true; + setMethodVersion = 0; + } catch (NoSuchMethodException e1) { + try { + setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class); + setMethodVersion = 1; + } catch (NoSuchMethodException e2) { + setPrimaryClipMethod = manager.getClass() + .getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class, int.class); + setMethodVersion = 2; + } } } } return setPrimaryClipMethod; } - private static ClipData getPrimaryClip(Method method, boolean alternativeMethod, IInterface manager) + private static ClipData getPrimaryClip(Method method, int methodVersion, IInterface manager) throws InvocationTargetException, IllegalAccessException { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME); } - if (alternativeMethod) { - return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID); + + switch (methodVersion) { + case 0: + return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID); + case 1: + return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID); + default: + return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0); } - return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID); } - private static void setPrimaryClip(Method method, boolean alternativeMethod, IInterface manager, ClipData clipData) + private static void setPrimaryClip(Method method, int methodVersion, IInterface manager, ClipData clipData) throws InvocationTargetException, IllegalAccessException { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { method.invoke(manager, clipData, FakeContext.PACKAGE_NAME); - } else if (alternativeMethod) { - method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID); - } else { - method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID); + return; + } + + switch (methodVersion) { + case 0: + method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID); + break; + case 1: + method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID); + break; + default: + method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0); + break; } } public CharSequence getText() { try { Method method = getGetPrimaryClipMethod(); - ClipData clipData = getPrimaryClip(method, alternativeGetMethod, manager); + ClipData clipData = getPrimaryClip(method, getMethodVersion, manager); if (clipData == null || clipData.getItemCount() == 0) { return null; } @@ -96,7 +123,7 @@ public class ClipboardManager { try { Method method = getSetPrimaryClipMethod(); ClipData clipData = ClipData.newPlainText(null, text); - setPrimaryClip(method, alternativeSetMethod, manager, clipData); + setPrimaryClip(method, setMethodVersion, manager, clipData); return true; } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { Ln.e("Could not invoke method", e); @@ -104,14 +131,23 @@ public class ClipboardManager { } } - private static void addPrimaryClipChangedListener(Method method, boolean alternativeMethod, IInterface manager, + private static void addPrimaryClipChangedListener(Method method, int methodVersion, IInterface manager, IOnPrimaryClipChangedListener listener) throws InvocationTargetException, IllegalAccessException { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { method.invoke(manager, listener, FakeContext.PACKAGE_NAME); - } else if (alternativeMethod) { - method.invoke(manager, listener, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID); - } else { - method.invoke(manager, listener, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID); + return; + } + + switch (methodVersion) { + case 0: + method.invoke(manager, listener, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID); + break; + case 1: + method.invoke(manager, listener, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID); + break; + default: + method.invoke(manager, listener, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0); + break; } } @@ -124,10 +160,19 @@ public class ClipboardManager { try { addPrimaryClipChangedListener = manager.getClass() .getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, int.class); - } catch (NoSuchMethodException e) { - addPrimaryClipChangedListener = manager.getClass() - .getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, String.class, int.class); - alternativeAddListenerMethod = true; + addListenerMethodVersion = 0; + } catch (NoSuchMethodException e1) { + try { + addPrimaryClipChangedListener = manager.getClass() + .getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, String.class, + int.class); + addListenerMethodVersion = 1; + } catch (NoSuchMethodException e2) { + addPrimaryClipChangedListener = manager.getClass() + .getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, String.class, + int.class, int.class); + addListenerMethodVersion = 2; + } } } } @@ -137,7 +182,7 @@ public class ClipboardManager { public boolean addPrimaryClipChangedListener(IOnPrimaryClipChangedListener listener) { try { Method method = getAddPrimaryClipChangedListener(); - addPrimaryClipChangedListener(method, alternativeAddListenerMethod, manager, listener); + addPrimaryClipChangedListener(method, addListenerMethodVersion, manager, listener); return true; } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { Ln.e("Could not invoke method", e); From 2fff9b9edf749dd7a8ccf36fe2df3c3587f535ab Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 17 Mar 2023 22:10:38 +0100 Subject: [PATCH 1518/2244] Adapt FakeContext for Android 14 This fixes audio for Android 14 developer preview 2. Fixes #3784 Suggested-by: Namelesswonder --- server/src/main/java/com/genymobile/scrcpy/FakeContext.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/server/src/main/java/com/genymobile/scrcpy/FakeContext.java b/server/src/main/java/com/genymobile/scrcpy/FakeContext.java index 844d6bd8..738203de 100644 --- a/server/src/main/java/com/genymobile/scrcpy/FakeContext.java +++ b/server/src/main/java/com/genymobile/scrcpy/FakeContext.java @@ -38,4 +38,10 @@ public final class FakeContext extends ContextWrapper { builder.setPackageName(PACKAGE_NAME); return builder.build(); } + + // @Override to be added on SDK upgrade for Android 14 + @SuppressWarnings("unused") + public int getDeviceId() { + return 0; + } } From 2d3059e1ab687a18123636784f1e98ac653d3469 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 20 Mar 2023 19:40:35 +0100 Subject: [PATCH 1519/2244] Reference FAQ from HID/OTG documentation Reference the FAQ section about "HID/OTG issues on Windows" from the HID/OTG documentation. --- doc/hid-otg.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/hid-otg.md b/doc/hid-otg.md index c64af752..7dfc60fc 100644 --- a/doc/hid-otg.md +++ b/doc/hid-otg.md @@ -106,3 +106,7 @@ scrcpy --otg # keyboard and mouse Like `--hid-keyboard` and `--hid-mouse`, it only works if the device is connected over USB. + +## HID/OTG issues on Windows + +See [FAQ](/FAQ.md#hidotg-issues-on-windows). From 21df2c240e544b1c1eba7775e1474c1c772be04b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 23 Mar 2023 19:02:14 +0100 Subject: [PATCH 1520/2244] Mention necessary reboot After setting "USB debugging (security settings)", a reboot is necessary. --- FAQ.md | 2 ++ README.md | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/FAQ.md b/FAQ.md index 25bae5d5..484e9c50 100644 --- a/FAQ.md +++ b/FAQ.md @@ -159,6 +159,8 @@ In developer options, enable: > **USB debugging (Security settings)** > _Allow granting permissions and simulating input via USB debugging_ +Rebooting the device is necessary once this option is set. + [simulating input]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323 diff --git a/README.md b/README.md index 5bafbe2b..ddcc565a 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,8 @@ Make sure you [enabled USB debugging][enable-adb] on your device(s). On some devices, you also need to enable [an additional option][control] `USB debugging (Security Settings)` (this is an item different from `USB debugging`) -to control it using a keyboard and mouse. +to control it using a keyboard and mouse. Rebooting the device is necessary once +this option is set. [control]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323 From 0ebb3df69cb5e7c977c12a7be31abde5511e4d13 Mon Sep 17 00:00:00 2001 From: Yan Date: Mon, 27 Mar 2023 14:59:09 +0200 Subject: [PATCH 1521/2244] Fix debug build by adding compat.c to tests Linking of tests that needed something from compat.c failed. PR #3865 Signed-off-by: Romain Vimont --- app/meson.build | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/meson.build b/app/meson.build index 723274c9..b1cfb2b6 100644 --- a/app/meson.build +++ b/app/meson.build @@ -310,7 +310,8 @@ if get_option('buildtype') == 'debug' ] foreach t : tests - exe = executable(t[0], t[1], + sources = t[1] + ['src/compat.c'] + exe = executable(t[0], sources, include_directories: src_dir, dependencies: dependencies, c_args: ['-DSDL_MAIN_HANDLED', '-DSC_TEST']) From 00534b0b2d06a2b31a94e2bbbfe35961a9b8879a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 28 Mar 2023 08:30:43 +0200 Subject: [PATCH 1522/2244] Fix typo in FAQ --- FAQ.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FAQ.md b/FAQ.md index 484e9c50..30155808 100644 --- a/FAQ.md +++ b/FAQ.md @@ -231,4 +231,4 @@ Translations of this FAQ in other languages are available in the [wiki]. [wiki]: https://github.com/Genymobile/scrcpy/wiki -Only this README file is guaranteed to be up-to-date. +Only this FAQ file is guaranteed to be up-to-date. From a1e8a340016875cfea4b42b326026889d684f221 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 28 Mar 2023 08:29:41 +0200 Subject: [PATCH 1523/2244] Fix documentation link in FAQ --- FAQ.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/FAQ.md b/FAQ.md index 30155808..a6eaeefa 100644 --- a/FAQ.md +++ b/FAQ.md @@ -170,12 +170,12 @@ The default text injection method is [limited to ASCII characters][text-input]. A trick allows to also inject some [accented characters][accented-characters], but that's all. See [#37]. -Since scrcpy v1.20, it is possible to simulate a [physical keyboard][hid] (HID). +It is also possible to simulate a [physical keyboard][hid] (HID). [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 -[hid]: README.md#physical-keyboard-simulation-hid +[hid]: doc/hid-otg.md ## Client issues From 2f9396e24a5beb76f8754bca37a858aa95536abc Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 27 Mar 2023 02:12:59 +0200 Subject: [PATCH 1524/2244] Simplify clock estimation The slope encodes the drift between the device clock and the computer clock. Its real value is expected very close to 1. To estimate it, just assume it is exactly 1. Since the clock is used to estimate very close points in the future, the error caused by clock drift is totally negligible, and in practice it is way lower than the slope estimation error. Therefore, only estimate the offset. --- app/meson.build | 4 -- app/src/clock.c | 108 ++++++----------------------------------- app/src/clock.h | 43 +++------------- app/src/delay_buffer.c | 2 +- app/tests/test_clock.c | 79 ------------------------------ 5 files changed, 23 insertions(+), 213 deletions(-) delete mode 100644 app/tests/test_clock.c diff --git a/app/meson.build b/app/meson.build index b1cfb2b6..05c463e6 100644 --- a/app/meson.build +++ b/app/meson.build @@ -277,10 +277,6 @@ if get_option('buildtype') == 'debug' 'src/util/strbuf.c', 'src/util/term.c', ]], - ['test_clock', [ - 'tests/test_clock.c', - 'src/clock.c', - ]], ['test_control_msg_serialize', [ 'tests/test_control_msg_serialize.c', 'src/control_msg.c', diff --git a/app/src/clock.c b/app/src/clock.c index 3e1a794d..92989bfe 100644 --- a/app/src/clock.c +++ b/app/src/clock.c @@ -1,116 +1,36 @@ #include "clock.h" +#include + #include "util/log.h" #define SC_CLOCK_NDEBUG // comment to debug +#define SC_CLOCK_RANGE 32 + void sc_clock_init(struct sc_clock *clock) { - clock->count = 0; - clock->head = 0; - clock->left_sum.system = 0; - clock->left_sum.stream = 0; - clock->right_sum.system = 0; - clock->right_sum.stream = 0; -} - -// Estimate the affine function f(stream) = slope * stream + offset -static void -sc_clock_estimate(struct sc_clock *clock, - double *out_slope, sc_tick *out_offset) { - assert(clock->count); - - if (clock->count == 1) { - // If there is only 1 point, we can't compute a slope. Assume it is 1. - struct sc_clock_point *single_point = &clock->right_sum; - *out_slope = 1; - *out_offset = single_point->system - single_point->stream; - return; - } - - struct sc_clock_point left_avg = { - .system = clock->left_sum.system / (clock->count / 2), - .stream = clock->left_sum.stream / (clock->count / 2), - }; - struct sc_clock_point right_avg = { - .system = clock->right_sum.system / ((clock->count + 1) / 2), - .stream = clock->right_sum.stream / ((clock->count + 1) / 2), - }; - - double slope = (double) (right_avg.system - left_avg.system) - / (right_avg.stream - left_avg.stream); - - if (clock->count < SC_CLOCK_RANGE) { - /* The first frames are typically received and decoded with more delay - * than the others, causing a wrong slope estimation on start. To - * compensate, assume an initial slope of 1, then progressively use the - * estimated slope. */ - slope = (clock->count * slope + (SC_CLOCK_RANGE - clock->count)) - / SC_CLOCK_RANGE; - } - - struct sc_clock_point global_avg = { - .system = (clock->left_sum.system + clock->right_sum.system) - / clock->count, - .stream = (clock->left_sum.stream + clock->right_sum.stream) - / clock->count, - }; - - sc_tick offset = global_avg.system - (sc_tick) (global_avg.stream * slope); - - *out_slope = slope; - *out_offset = offset; + clock->range = 0; + clock->offset = 0; } void sc_clock_update(struct sc_clock *clock, sc_tick system, sc_tick stream) { - struct sc_clock_point *point = &clock->points[clock->head]; - - if (clock->count == SC_CLOCK_RANGE || clock->count & 1) { - // One point passes from the right sum to the left sum - - unsigned mid; - if (clock->count == SC_CLOCK_RANGE) { - mid = (clock->head + SC_CLOCK_RANGE / 2) % SC_CLOCK_RANGE; - } else { - // Only for the first frames - mid = clock->count / 2; - } - - struct sc_clock_point *mid_point = &clock->points[mid]; - clock->left_sum.system += mid_point->system; - clock->left_sum.stream += mid_point->stream; - clock->right_sum.system -= mid_point->system; - clock->right_sum.stream -= mid_point->stream; + if (clock->range < SC_CLOCK_RANGE) { + ++clock->range; } - if (clock->count == SC_CLOCK_RANGE) { - // The current point overwrites the previous value in the circular - // array, update the left sum accordingly - clock->left_sum.system -= point->system; - clock->left_sum.stream -= point->stream; - } else { - ++clock->count; - } - - point->system = system; - point->stream = stream; - - clock->right_sum.system += system; - clock->right_sum.stream += stream; - - clock->head = (clock->head + 1) % SC_CLOCK_RANGE; - - // Update estimation - sc_clock_estimate(clock, &clock->slope, &clock->offset); + sc_tick offset = system - stream; + clock->offset = ((clock->range - 1) * clock->offset + offset) + / clock->range; #ifndef SC_CLOCK_NDEBUG - LOGD("Clock estimation: %f * pts + %" PRItick, clock->slope, clock->offset); + LOGD("Clock estimation: pts + %" PRItick, clock->offset); #endif } sc_tick sc_clock_to_system_time(struct sc_clock *clock, sc_tick stream) { - assert(clock->count); // sc_clock_update() must have been called - return (sc_tick) (stream * clock->slope) + clock->offset; + assert(clock->range); // sc_clock_update() must have been called + return stream + clock->offset; } diff --git a/app/src/clock.h b/app/src/clock.h index 886d1f4d..0d34ab99 100644 --- a/app/src/clock.h +++ b/app/src/clock.h @@ -3,13 +3,8 @@ #include "common.h" -#include - #include "util/tick.h" -#define SC_CLOCK_RANGE 32 -static_assert(!(SC_CLOCK_RANGE & 1), "SC_CLOCK_RANGE must be even"); - struct sc_clock_point { sc_tick system; sc_tick stream; @@ -21,40 +16,18 @@ struct sc_clock_point { * * f(stream) = slope * stream + offset * - * To that end, it stores the SC_CLOCK_RANGE last clock points (the timestamps - * of a frame expressed both in stream time and system time) in a circular - * array. + * Theoretically, the slope encodes the drift between the device clock and the + * computer clock. It is expected to be very close to 1. * - * To estimate the slope, it splits the last SC_CLOCK_RANGE points into two - * sets of SC_CLOCK_RANGE/2 points, and computes their centroid ("average - * point"). The slope of the estimated affine function is that of the line - * passing through these two points. + * Since the clock is used to estimate very close points in the future (which + * are reestimated on every clock update, see delay_buffer), the error caused + * by clock drift is totally negligible, so it is better to assume that the + * slope is 1 than to estimate it (the estimation error would be larger). * - * To estimate the offset, it computes the centroid of all the SC_CLOCK_RANGE - * points. The resulting affine function passes by this centroid. - * - * With a circular array, the rolling sums (and average) are quick to compute. - * In practice, the estimation is stable and the evolution is smooth. + * Therefore, only the offset is estimated. */ struct sc_clock { - // Circular array - struct sc_clock_point points[SC_CLOCK_RANGE]; - - // Number of points in the array (count <= SC_CLOCK_RANGE) - unsigned count; - - // Index of the next point to write - unsigned head; - - // Sum of the first count/2 points - struct sc_clock_point left_sum; - - // Sum of the last (count+1)/2 points - struct sc_clock_point right_sum; - - // Estimated slope and offset - // (computed on sc_clock_update(), used by sc_clock_to_system_time()) - double slope; + unsigned range; sc_tick offset; }; diff --git a/app/src/delay_buffer.c b/app/src/delay_buffer.c index 9d4690a2..f6141b35 100644 --- a/app/src/delay_buffer.c +++ b/app/src/delay_buffer.c @@ -194,7 +194,7 @@ sc_delay_buffer_frame_sink_push(struct sc_frame_sink *sink, sc_clock_update(&db->clock, sc_tick_now(), pts); sc_cond_signal(&db->wait_cond); - if (db->first_frame_asap && db->clock.count == 1) { + if (db->first_frame_asap && db->clock.range == 1) { sc_mutex_unlock(&db->mutex); return sc_frame_source_sinks_push(&db->frame_source, frame); } diff --git a/app/tests/test_clock.c b/app/tests/test_clock.c deleted file mode 100644 index a88d5800..00000000 --- a/app/tests/test_clock.c +++ /dev/null @@ -1,79 +0,0 @@ -#include "common.h" - -#include - -#include "clock.h" - -void test_small_rolling_sum(void) { - struct sc_clock clock; - sc_clock_init(&clock); - - assert(clock.count == 0); - assert(clock.left_sum.system == 0); - assert(clock.left_sum.stream == 0); - assert(clock.right_sum.system == 0); - assert(clock.right_sum.stream == 0); - - sc_clock_update(&clock, 2, 3); - assert(clock.count == 1); - assert(clock.left_sum.system == 0); - assert(clock.left_sum.stream == 0); - assert(clock.right_sum.system == 2); - assert(clock.right_sum.stream == 3); - - sc_clock_update(&clock, 10, 20); - assert(clock.count == 2); - assert(clock.left_sum.system == 2); - assert(clock.left_sum.stream == 3); - assert(clock.right_sum.system == 10); - assert(clock.right_sum.stream == 20); - - sc_clock_update(&clock, 40, 80); - assert(clock.count == 3); - assert(clock.left_sum.system == 2); - assert(clock.left_sum.stream == 3); - assert(clock.right_sum.system == 50); - assert(clock.right_sum.stream == 100); - - sc_clock_update(&clock, 400, 800); - assert(clock.count == 4); - assert(clock.left_sum.system == 12); - assert(clock.left_sum.stream == 23); - assert(clock.right_sum.system == 440); - assert(clock.right_sum.stream == 880); -} - -void test_large_rolling_sum(void) { - const unsigned half_range = SC_CLOCK_RANGE / 2; - - struct sc_clock clock1; - sc_clock_init(&clock1); - for (unsigned i = 0; i < 5 * half_range; ++i) { - sc_clock_update(&clock1, i, 2 * i + 1); - } - - struct sc_clock clock2; - sc_clock_init(&clock2); - for (unsigned i = 3 * half_range; i < 5 * half_range; ++i) { - sc_clock_update(&clock2, i, 2 * i + 1); - } - - assert(clock1.count == SC_CLOCK_RANGE); - assert(clock2.count == SC_CLOCK_RANGE); - - // The values before the last SC_CLOCK_RANGE points in clock1 should have - // no impact - assert(clock1.left_sum.system == clock2.left_sum.system); - assert(clock1.left_sum.stream == clock2.left_sum.stream); - assert(clock1.right_sum.system == clock2.right_sum.system); - assert(clock1.right_sum.stream == clock2.right_sum.stream); -} - -int main(int argc, char *argv[]) { - (void) argc; - (void) argv; - - test_small_rolling_sum(); - test_large_rolling_sum(); - return 0; -}; From 8f0b38cc4f503ea16dd57c705ae0ede81efc8630 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 31 Mar 2023 07:51:34 +0200 Subject: [PATCH 1525/2244] Specify in README that OTG does not require adb --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index ddcc565a..9969dc51 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,9 @@ this option is set. [control]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323 +Note that USB debugging is not required to run scrcpy in [OTG +mode](doc/hid-otg.md#otg). + ## Get the app From f77e1c474edfafcd46f692452413f35a18c70949 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 1 Apr 2023 11:50:21 +0200 Subject: [PATCH 1526/2244] Fix copy-paste for some devices On Honor Magic 5 Pro, the method to get the clipboard content has been modified in the framework. Adapt the call to make it work also on this device. Fixes #3885 --- .../scrcpy/wrappers/ClipboardManager.java | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 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 cb176cc3..7b750975 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java @@ -37,8 +37,13 @@ public class ClipboardManager { getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class); getMethodVersion = 1; } catch (NoSuchMethodException e2) { - getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class, int.class); - getMethodVersion = 2; + try { + getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class, int.class); + getMethodVersion = 2; + } catch (NoSuchMethodException e3) { + getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class, String.class); + getMethodVersion = 3; + } } } } @@ -80,8 +85,10 @@ public class ClipboardManager { return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID); case 1: return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID); - default: + case 2: return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0); + default: + return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID, null); } } From 669e9a8d1ecf3bc94195c6d57c0fec59c51a8367 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 2 Apr 2023 17:45:46 +0200 Subject: [PATCH 1527/2244] Fix "ip route" parsing If a line did not end with '\r', then the final `\n' was replaced by '\0' for parsing the current line. This `\0` was then mistakenly considered as the end of the whole "ip route" output, so the remaining lines were not parsed, causing "scrcpy --tcpip" to fail in some cases. To fix the issue, read the final character of the current line before it is (possibly) overwritten by '\0'. --- app/src/adb/adb_parser.c | 11 ++++++----- app/tests/test_adb_parser.c | 13 +++++++++++++ 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/app/src/adb/adb_parser.c b/app/src/adb/adb_parser.c index ab121347..e7358403 100644 --- a/app/src/adb/adb_parser.c +++ b/app/src/adb/adb_parser.c @@ -204,6 +204,7 @@ sc_adb_parse_device_ip(char *str) { while (str[idx_line] != '\0') { char *line = &str[idx_line]; size_t len = strcspn(line, "\n"); + bool is_last_line = line[len] == '\0'; // The same, but without any trailing '\r' size_t line_len = sc_str_remove_trailing_cr(line, len); @@ -215,12 +216,12 @@ sc_adb_parse_device_ip(char *str) { return ip; } - idx_line += len; - - if (str[idx_line] != '\0') { - // The next line starts after the '\n' - ++idx_line; + if (is_last_line) { + break; } + + // The next line starts after the '\n' + idx_line += len + 1; } return NULL; diff --git a/app/tests/test_adb_parser.c b/app/tests/test_adb_parser.c index d95e7ef2..362b254f 100644 --- a/app/tests/test_adb_parser.c +++ b/app/tests/test_adb_parser.c @@ -217,6 +217,18 @@ static void test_get_ip_multiline_second_ok(void) { free(ip); } +static void test_get_ip_multiline_second_ok_without_cr(void) { + char ip_route[] = "10.0.0.0/24 dev rmnet proto kernel scope link src " + "10.0.0.3\n" + "192.168.1.0/24 dev wlan0 proto kernel scope link src " + "192.168.1.3\n"; + + char *ip = sc_adb_parse_device_ip(ip_route); + assert(ip); + assert(!strcmp(ip, "192.168.1.3")); + free(ip); +} + static void test_get_ip_no_wlan(void) { char ip_route[] = "192.168.1.0/24 dev rmnet proto kernel scope link src " "192.168.12.34\r\r\n"; @@ -259,6 +271,7 @@ int main(int argc, char *argv[]) { test_get_ip_single_line_with_trailing_space(); test_get_ip_multiline_first_ok(); test_get_ip_multiline_second_ok(); + test_get_ip_multiline_second_ok_without_cr(); test_get_ip_no_wlan(); test_get_ip_no_wlan_without_eol(); test_get_ip_truncated(); From fdf465851c786f153255d044c621c1ac4766e5fd Mon Sep 17 00:00:00 2001 From: Simon Chan <1330321+yume-chan@users.noreply.github.com> Date: Tue, 4 Apr 2023 22:33:39 +0800 Subject: [PATCH 1528/2244] Add Android version check in raw audio recorder Do not attempt to capture audio below Android 11, this may cause a segfault on the device. PR #3889 Signed-off-by: Romain Vimont --- .../main/java/com/genymobile/scrcpy/AudioRawRecorder.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java b/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java index 4b1b5bd0..f98440de 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java @@ -1,6 +1,7 @@ package com.genymobile.scrcpy; import android.media.MediaCodec; +import android.os.Build; import java.io.IOException; import java.nio.ByteBuffer; @@ -19,6 +20,12 @@ public final class AudioRawRecorder implements AsyncProcessor { } private void record() throws IOException, AudioCaptureForegroundException { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + Ln.w("Audio disabled: it is not supported before Android 11"); + streamer.writeDisableStream(false); + return; + } + final ByteBuffer buffer = ByteBuffer.allocateDirect(READ_SIZE); final MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); From 2e532afd2baf63096a156251220044409be42802 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 3 Apr 2023 21:41:54 +0200 Subject: [PATCH 1529/2244] Pass const pointers to events SDL_Events are only read. --- app/src/input_manager.c | 3 ++- app/src/input_manager.h | 3 ++- app/src/screen.c | 2 +- app/src/screen.h | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index c8098ee7..c9e83d48 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -797,7 +797,8 @@ sc_input_manager_process_file(struct sc_input_manager *im, } void -sc_input_manager_handle_event(struct sc_input_manager *im, SDL_Event *event) { +sc_input_manager_handle_event(struct sc_input_manager *im, + const SDL_Event *event) { bool control = im->controller; switch (event->type) { case SDL_TEXTINPUT: diff --git a/app/src/input_manager.h b/app/src/input_manager.h index 46b1160e..b5a762eb 100644 --- a/app/src/input_manager.h +++ b/app/src/input_manager.h @@ -61,6 +61,7 @@ sc_input_manager_init(struct sc_input_manager *im, const struct sc_input_manager_params *params); void -sc_input_manager_handle_event(struct sc_input_manager *im, SDL_Event *event); +sc_input_manager_handle_event(struct sc_input_manager *im, + const SDL_Event *event); #endif diff --git a/app/src/screen.c b/app/src/screen.c index b00b0d05..65a4047d 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -812,7 +812,7 @@ sc_screen_is_mouse_capture_key(SDL_Keycode key) { } bool -sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event) { +sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) { bool relative_mode = sc_screen_is_relative_mode(screen); switch (event->type) { diff --git a/app/src/screen.h b/app/src/screen.h index 4fca04d8..ffb896a7 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -137,7 +137,7 @@ sc_screen_set_rotation(struct sc_screen *screen, unsigned rotation); // react to SDL events // If this function returns false, scrcpy must exit with an error. bool -sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event); +sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event); // convert point from window coordinates to frame coordinates // x and y are expressed in pixels From 051b74c88338702508d98bd532057b343e0a177c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 31 Mar 2023 20:20:27 +0200 Subject: [PATCH 1530/2244] Extract sc_display from sc_screen Move the display code to a separate component. --- app/meson.build | 1 + app/src/display.c | 166 ++++++++++++++++++++++++++++++++++++++++++++++ app/src/display.h | 37 +++++++++++ app/src/screen.c | 160 +++++++------------------------------------- app/src/screen.h | 6 +- 5 files changed, 231 insertions(+), 139 deletions(-) create mode 100644 app/src/display.c create mode 100644 app/src/display.h diff --git a/app/meson.build b/app/meson.build index 05c463e6..061fdcab 100644 --- a/app/meson.build +++ b/app/meson.build @@ -14,6 +14,7 @@ src = [ 'src/delay_buffer.c', 'src/demuxer.c', 'src/device_msg.c', + 'src/display.c', 'src/icon.c', 'src/file_pusher.c', 'src/fps_counter.c', diff --git a/app/src/display.c b/app/src/display.c new file mode 100644 index 00000000..96ff9e06 --- /dev/null +++ b/app/src/display.c @@ -0,0 +1,166 @@ +#include "display.h" + +#include + +#include "util/log.h" + +bool +sc_display_init(struct sc_display *display, SDL_Window *window, bool mipmaps) { + display->renderer = + SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED); + if (!display->renderer) { + LOGE("Could not create renderer: %s", SDL_GetError()); + return false; + } + + SDL_RendererInfo renderer_info; + int r = SDL_GetRendererInfo(display->renderer, &renderer_info); + const char *renderer_name = r ? NULL : renderer_info.name; + LOGI("Renderer: %s", renderer_name ? renderer_name : "(unknown)"); + + display->mipmaps = false; + + // starts with "opengl" + bool use_opengl = renderer_name && !strncmp(renderer_name, "opengl", 6); + if (use_opengl) { + struct sc_opengl *gl = &display->gl; + sc_opengl_init(gl); + + LOGI("OpenGL version: %s", gl->version); + + 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"); + display->mipmaps = true; + } else { + LOGW("Trilinear filtering disabled " + "(OpenGL 3.0+ or ES 2.0+ required"); + } + } else { + LOGI("Trilinear filtering disabled"); + } + } else if (mipmaps) { + LOGD("Trilinear filtering disabled (not an OpenGL renderer"); + } + + return true; +} + +void +sc_display_destroy(struct sc_display *display) { + if (display->texture) { + SDL_DestroyTexture(display->texture); + } + SDL_DestroyRenderer(display->renderer); +} + +static SDL_Texture * +sc_display_create_texture(struct sc_display *display, + struct sc_size size) { + SDL_Renderer *renderer = display->renderer; + SDL_Texture *texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_YV12, + SDL_TEXTUREACCESS_STREAMING, + size.width, size.height); + if (!texture) { + LOGE("Could not create texture: %s", SDL_GetError()); + return NULL; + } + + if (display->mipmaps) { + struct sc_opengl *gl = &display->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, -1.f); + + SDL_GL_UnbindTexture(texture); + } + + return texture; +} + +bool +sc_display_set_texture_size(struct sc_display *display, struct sc_size size) { + if (display->texture) { + SDL_DestroyTexture(display->texture); + } + + display->texture = sc_display_create_texture(display, size); + if (!display->texture) { + return false; + } + + LOGI("Texture: %" PRIu16 "x%" PRIu16, size.width, size.height); + return true; +} + +bool +sc_display_update_texture(struct sc_display *display, const AVFrame *frame) { + int ret = SDL_UpdateYUVTexture(display->texture, NULL, + frame->data[0], frame->linesize[0], + frame->data[1], frame->linesize[1], + frame->data[2], frame->linesize[2]); + if (ret) { + LOGE("Could not update texture: %s", SDL_GetError()); + return false; + } + + if (display->mipmaps) { + SDL_GL_BindTexture(display->texture, NULL, NULL); + display->gl.GenerateMipmap(GL_TEXTURE_2D); + SDL_GL_UnbindTexture(display->texture); + } + + return true; +} + +bool +sc_display_render(struct sc_display *display, const SDL_Rect *geometry, + unsigned rotation) { + SDL_RenderClear(display->renderer); + + SDL_Renderer *renderer = display->renderer; + SDL_Texture *texture = display->texture; + + if (rotation == 0) { + int ret = SDL_RenderCopy(renderer, texture, NULL, geometry); + if (ret) { + LOGE("Could not render texture: %s", SDL_GetError()); + return false; + } + } else { + // rotation in RenderCopyEx() is clockwise, while screen->rotation is + // counterclockwise (to be consistent with --lock-video-orientation) + int cw_rotation = (4 - rotation) % 4; + double angle = 90 * cw_rotation; + + const SDL_Rect *dstrect = NULL; + SDL_Rect rect; + if (rotation & 1) { + rect.x = geometry->x + (geometry->w - geometry->h) / 2; + rect.y = geometry->y + (geometry->h - geometry->w) / 2; + rect.w = geometry->h; + rect.h = geometry->w; + dstrect = ▭ + } else { + assert(rotation == 2); + dstrect = geometry; + } + + int ret = SDL_RenderCopyEx(renderer, texture, NULL, dstrect, angle, + NULL, 0); + if (ret) { + LOGE("Could not render texture: %s", SDL_GetError()); + return false; + } + } + + SDL_RenderPresent(display->renderer); + return true; +} diff --git a/app/src/display.h b/app/src/display.h new file mode 100644 index 00000000..8856afcd --- /dev/null +++ b/app/src/display.h @@ -0,0 +1,37 @@ +#ifndef SC_DISPLAY_H +#define SC_DISPLAY_H + +#include "common.h" + +#include +#include +#include + +#include "coords.h" +#include "opengl.h" + +struct sc_display { + SDL_Renderer *renderer; + SDL_Texture *texture; + + struct sc_opengl gl; + bool mipmaps; +}; + +bool +sc_display_init(struct sc_display *display, SDL_Window *window, bool mipmaps); + +void +sc_display_destroy(struct sc_display *display); + +bool +sc_display_set_texture_size(struct sc_display *display, struct sc_size size); + +bool +sc_display_update_texture(struct sc_display *display, const AVFrame *frame); + +bool +sc_display_render(struct sc_display *display, const SDL_Rect *geometry, + unsigned rotation); + +#endif diff --git a/app/src/screen.c b/app/src/screen.c index 65a4047d..70665ed6 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -239,35 +239,6 @@ sc_screen_update_content_rect(struct sc_screen *screen) { } } -static bool -create_texture(struct sc_screen *screen) { - SDL_Renderer *renderer = screen->renderer; - struct sc_size size = screen->frame_size; - SDL_Texture *texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_YV12, - SDL_TEXTUREACCESS_STREAMING, - size.width, size.height); - if (!texture) { - LOGE("Could not create texture: %s", SDL_GetError()); - return false; - } - - 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, -1.f); - - SDL_GL_UnbindTexture(texture); - } - - screen->texture = texture; - return true; -} - // render the texture to the renderer // // Set the update_content_rect flag if the window or content size may have @@ -278,35 +249,11 @@ sc_screen_render(struct sc_screen *screen, bool update_content_rect) { sc_screen_update_content_rect(screen); } - SDL_RenderClear(screen->renderer); - if (screen->rotation == 0) { - 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) - int cw_rotation = (4 - screen->rotation) % 4; - double angle = 90 * cw_rotation; - - SDL_Rect *dstrect = NULL; - SDL_Rect rect; - if (screen->rotation & 1) { - 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, - angle, NULL, 0); - } - SDL_RenderPresent(screen->renderer); + bool ok = sc_display_render(&screen->display, &screen->rect, + screen->rotation); + (void) ok; // error already logged } - #if defined(__APPLE__) || defined(__WINDOWS__) # define CONTINUOUS_RESIZING_WORKAROUND #endif @@ -453,46 +400,11 @@ sc_screen_init(struct sc_screen *screen, goto error_destroy_fps_counter; } - screen->renderer = SDL_CreateRenderer(screen->window, -1, - SDL_RENDERER_ACCELERATED); - if (!screen->renderer) { - LOGE("Could not create renderer: %s", SDL_GetError()); + ok = sc_display_init(&screen->display, screen->window, params->mipmaps); + if (!ok) { goto error_destroy_window; } - 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)"); - - screen->mipmaps = false; - - // starts with "opengl" - bool use_opengl = renderer_name && !strncmp(renderer_name, "opengl", 6); - if (use_opengl) { - struct sc_opengl *gl = &screen->gl; - sc_opengl_init(gl); - - LOGI("OpenGL version: %s", gl->version); - - if (params->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 { - LOGI("Trilinear filtering disabled"); - } - } else if (params->mipmaps) { - LOGD("Trilinear filtering disabled (not an OpenGL renderer)"); - } - SDL_Surface *icon = scrcpy_icon_load(); if (icon) { SDL_SetWindowIcon(screen->window, icon); @@ -504,7 +416,7 @@ sc_screen_init(struct sc_screen *screen, screen->frame = av_frame_alloc(); if (!screen->frame) { LOG_OOM(); - goto error_destroy_renderer; + goto error_destroy_display; } struct sc_input_manager_params im_params = { @@ -539,8 +451,8 @@ sc_screen_init(struct sc_screen *screen, return true; -error_destroy_renderer: - SDL_DestroyRenderer(screen->renderer); +error_destroy_display: + sc_display_destroy(&screen->display); error_destroy_window: SDL_DestroyWindow(screen->window); error_destroy_fps_counter: @@ -596,11 +508,8 @@ sc_screen_destroy(struct sc_screen *screen) { #ifndef NDEBUG assert(!screen->open); #endif + sc_display_destroy(&screen->display); av_frame_free(&screen->frame); - if (screen->texture) { - SDL_DestroyTexture(screen->texture); - } - SDL_DestroyRenderer(screen->renderer); SDL_DestroyWindow(screen->window); sc_fps_counter_destroy(&screen->fps_counter); sc_frame_buffer_destroy(&screen->fb); @@ -667,7 +576,6 @@ static bool sc_screen_init_size(struct sc_screen *screen) { // Before first frame assert(!screen->has_frame); - assert(!screen->texture); // The requested size is passed via screen->frame_size @@ -675,48 +583,27 @@ sc_screen_init_size(struct sc_screen *screen) { get_rotated_size(screen->frame_size, screen->rotation); screen->content_size = content_size; - LOGI("Initial texture: %" PRIu16 "x%" PRIu16, - screen->frame_size.width, screen->frame_size.height); - return create_texture(screen); + return sc_display_set_texture_size(&screen->display, screen->frame_size); } // recreate the texture and resize the window if the frame size has changed static bool prepare_for_frame(struct sc_screen *screen, struct sc_size new_frame_size) { - if (screen->frame_size.width != new_frame_size.width - || screen->frame_size.height != new_frame_size.height) { - // frame dimension changed, destroy texture - SDL_DestroyTexture(screen->texture); - - screen->frame_size = new_frame_size; - - struct sc_size new_content_size = - get_rotated_size(new_frame_size, screen->rotation); - set_content_size(screen, new_content_size); - - sc_screen_update_content_rect(screen); - - LOGI("New texture: %" PRIu16 "x%" PRIu16, - screen->frame_size.width, screen->frame_size.height); - return create_texture(screen); + if (screen->frame_size.width == new_frame_size.width + && screen->frame_size.height == new_frame_size.height) { + return true; } - return true; -} + // frame dimension changed + screen->frame_size = new_frame_size; -// write the frame into the texture -static void -update_texture(struct sc_screen *screen, const AVFrame *frame) { - SDL_UpdateYUVTexture(screen->texture, NULL, - frame->data[0], frame->linesize[0], - frame->data[1], frame->linesize[1], - frame->data[2], frame->linesize[2]); + struct sc_size new_content_size = + get_rotated_size(new_frame_size, screen->rotation); + set_content_size(screen, new_content_size); - if (screen->mipmaps) { - SDL_GL_BindTexture(screen->texture, NULL, NULL); - screen->gl.GenerateMipmap(GL_TEXTURE_2D); - SDL_GL_UnbindTexture(screen->texture); - } + sc_screen_update_content_rect(screen); + + return sc_display_set_texture_size(&screen->display, screen->frame_size); } static bool @@ -731,7 +618,10 @@ sc_screen_update_frame(struct sc_screen *screen) { if (!prepare_for_frame(screen, new_frame_size)) { return false; } - update_texture(screen, frame); + + if (!sc_display_update_texture(&screen->display, frame)) { + return false; + } if (!screen->has_frame) { screen->has_frame = true; diff --git a/app/src/screen.h b/app/src/screen.h index ffb896a7..2c032119 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -9,6 +9,7 @@ #include "controller.h" #include "coords.h" +#include "display.h" #include "fps_counter.h" #include "frame_buffer.h" #include "input_manager.h" @@ -24,6 +25,7 @@ struct sc_screen { bool open; // track the open/close state to assert correct behavior #endif + struct sc_display display; struct sc_input_manager im; struct sc_frame_buffer fb; struct sc_fps_counter fps_counter; @@ -39,9 +41,6 @@ struct sc_screen { } req; SDL_Window *window; - SDL_Renderer *renderer; - SDL_Texture *texture; - struct sc_opengl gl; struct sc_size frame_size; struct sc_size content_size; // rotated frame_size @@ -57,7 +56,6 @@ struct sc_screen { bool has_frame; bool fullscreen; bool maximized; - bool mipmaps; // To enable/disable mouse capture, a mouse capture key (LALT, LGUI or // RGUI) must be pressed. This variable tracks the pressed capture key. From afcdfc7fd7440782a9924cd8ccb8a40933b4ba90 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 9 Apr 2023 20:01:58 +0200 Subject: [PATCH 1531/2244] Fix checkstyle violation Checkstyle reported this error: [ant:checkstyle] [ERROR] AudioCapture.java:89:145: '+' should be on a new line. [OperatorWrap] --- server/src/main/java/com/genymobile/scrcpy/AudioCapture.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java b/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java index c940db16..dbb38dd2 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java @@ -86,8 +86,8 @@ public final class AudioCapture { } catch (UnsupportedOperationException e) { if (attempts == 0) { Ln.e("Failed to start audio capture"); - Ln.e("On Android 11, audio capture must be started in the foreground, make sure that the device is unlocked when starting " + - "scrcpy."); + Ln.e("On Android 11, audio capture must be started in the foreground, make sure that the device is unlocked when starting " + + "scrcpy."); throw new AudioCaptureForegroundException(); } else { Ln.d("Failed to start audio capture, retrying..."); From ce064fb5e00f6999d9fe9f66e932d6dee71dfe7d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 9 Apr 2023 19:52:01 +0200 Subject: [PATCH 1532/2244] Move options parsing to Options class --- .../java/com/genymobile/scrcpy/Options.java | 192 +++++++++++++++++ .../java/com/genymobile/scrcpy/Server.java | 195 +----------------- 2 files changed, 193 insertions(+), 194 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 2a3de757..0ebb790f 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; import java.util.List; +import java.util.Locale; public class Options { @@ -289,4 +290,195 @@ public class Options { public void setSendCodecMeta(boolean sendCodecMeta) { this.sendCodecMeta = sendCodecMeta; } + + @SuppressWarnings("MethodLength") + public static Options parse(String... args) { + if (args.length < 1) { + throw new IllegalArgumentException("Missing client version"); + } + + String clientVersion = args[0]; + if (!clientVersion.equals(BuildConfig.VERSION_NAME)) { + throw new IllegalArgumentException( + "The server version (" + BuildConfig.VERSION_NAME + ") does not match the client " + "(" + clientVersion + ")"); + } + + Options options = new Options(); + + for (int i = 1; i < args.length; ++i) { + String arg = args[i]; + int equalIndex = arg.indexOf('='); + if (equalIndex == -1) { + throw new IllegalArgumentException("Invalid key=value pair: \"" + arg + "\""); + } + String key = arg.substring(0, equalIndex); + String value = arg.substring(equalIndex + 1); + switch (key) { + case "scid": + int scid = Integer.parseInt(value, 0x10); + if (scid < -1) { + throw new IllegalArgumentException("scid may not be negative (except -1 for 'none'): " + scid); + } + options.setScid(scid); + break; + case "log_level": + Ln.Level level = Ln.Level.valueOf(value.toUpperCase(Locale.ENGLISH)); + options.setLogLevel(level); + break; + case "audio": + boolean audio = Boolean.parseBoolean(value); + options.setAudio(audio); + break; + case "video_codec": + VideoCodec videoCodec = VideoCodec.findByName(value); + if (videoCodec == null) { + throw new IllegalArgumentException("Video codec " + value + " not supported"); + } + options.setVideoCodec(videoCodec); + break; + case "audio_codec": + AudioCodec audioCodec = AudioCodec.findByName(value); + if (audioCodec == null) { + throw new IllegalArgumentException("Audio codec " + value + " not supported"); + } + options.setAudioCodec(audioCodec); + break; + case "max_size": + int maxSize = Integer.parseInt(value) & ~7; // multiple of 8 + options.setMaxSize(maxSize); + break; + case "video_bit_rate": + int videoBitRate = Integer.parseInt(value); + options.setVideoBitRate(videoBitRate); + break; + case "audio_bit_rate": + int audioBitRate = Integer.parseInt(value); + options.setAudioBitRate(audioBitRate); + break; + case "max_fps": + int maxFps = Integer.parseInt(value); + options.setMaxFps(maxFps); + break; + case "lock_video_orientation": + int lockVideoOrientation = Integer.parseInt(value); + options.setLockVideoOrientation(lockVideoOrientation); + break; + case "tunnel_forward": + boolean tunnelForward = Boolean.parseBoolean(value); + options.setTunnelForward(tunnelForward); + break; + case "crop": + Rect crop = parseCrop(value); + options.setCrop(crop); + break; + case "control": + boolean control = Boolean.parseBoolean(value); + options.setControl(control); + break; + case "display_id": + int displayId = Integer.parseInt(value); + options.setDisplayId(displayId); + break; + case "show_touches": + boolean showTouches = Boolean.parseBoolean(value); + options.setShowTouches(showTouches); + break; + case "stay_awake": + boolean stayAwake = Boolean.parseBoolean(value); + options.setStayAwake(stayAwake); + break; + case "video_codec_options": + List videoCodecOptions = CodecOption.parse(value); + options.setVideoCodecOptions(videoCodecOptions); + break; + case "audio_codec_options": + List audioCodecOptions = CodecOption.parse(value); + options.setAudioCodecOptions(audioCodecOptions); + break; + case "video_encoder": + if (!value.isEmpty()) { + options.setVideoEncoder(value); + } + break; + case "audio_encoder": + if (!value.isEmpty()) { + options.setAudioEncoder(value); + } + case "power_off_on_close": + boolean powerOffScreenOnClose = Boolean.parseBoolean(value); + options.setPowerOffScreenOnClose(powerOffScreenOnClose); + break; + case "clipboard_autosync": + boolean clipboardAutosync = Boolean.parseBoolean(value); + options.setClipboardAutosync(clipboardAutosync); + break; + case "downsize_on_error": + boolean downsizeOnError = Boolean.parseBoolean(value); + options.setDownsizeOnError(downsizeOnError); + break; + case "cleanup": + boolean cleanup = Boolean.parseBoolean(value); + options.setCleanup(cleanup); + break; + case "power_on": + boolean powerOn = Boolean.parseBoolean(value); + options.setPowerOn(powerOn); + break; + case "list_encoders": + boolean listEncoders = Boolean.parseBoolean(value); + options.setListEncoders(listEncoders); + break; + case "list_displays": + boolean listDisplays = Boolean.parseBoolean(value); + options.setListDisplays(listDisplays); + break; + case "send_device_meta": + boolean sendDeviceMeta = Boolean.parseBoolean(value); + options.setSendDeviceMeta(sendDeviceMeta); + break; + case "send_frame_meta": + boolean sendFrameMeta = Boolean.parseBoolean(value); + options.setSendFrameMeta(sendFrameMeta); + break; + case "send_dummy_byte": + boolean sendDummyByte = Boolean.parseBoolean(value); + options.setSendDummyByte(sendDummyByte); + break; + case "send_codec_meta": + boolean sendCodecMeta = Boolean.parseBoolean(value); + options.setSendCodecMeta(sendCodecMeta); + break; + case "raw_video_stream": + boolean rawVideoStream = Boolean.parseBoolean(value); + if (rawVideoStream) { + options.setSendDeviceMeta(false); + options.setSendFrameMeta(false); + options.setSendDummyByte(false); + options.setSendCodecMeta(false); + } + break; + default: + Ln.w("Unknown server option: " + key); + break; + } + } + + return options; + } + + private static Rect parseCrop(String crop) { + if (crop.isEmpty()) { + return null; + } + // input format: "width:height:x:y" + String[] tokens = crop.split(":"); + if (tokens.length != 4) { + throw new IllegalArgumentException("Crop must contains 4 values separated by colons: \"" + crop + "\""); + } + int width = Integer.parseInt(tokens[0]); + int height = Integer.parseInt(tokens[1]); + int x = Integer.parseInt(tokens[2]); + int y = Integer.parseInt(tokens[3]); + return new Rect(x, y, x + width, y + height); + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 244913cf..067b1670 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -1,13 +1,11 @@ package com.genymobile.scrcpy; -import android.graphics.Rect; import android.os.BatteryManager; import android.os.Build; import java.io.IOException; import java.util.ArrayList; import java.util.List; -import java.util.Locale; public final class Server { @@ -161,203 +159,12 @@ public final class Server { return thread; } - @SuppressWarnings("MethodLength") - private static Options createOptions(String... args) { - if (args.length < 1) { - throw new IllegalArgumentException("Missing client version"); - } - - String clientVersion = args[0]; - if (!clientVersion.equals(BuildConfig.VERSION_NAME)) { - throw new IllegalArgumentException( - "The server version (" + BuildConfig.VERSION_NAME + ") does not match the client " + "(" + clientVersion + ")"); - } - - Options options = new Options(); - - for (int i = 1; i < args.length; ++i) { - String arg = args[i]; - int equalIndex = arg.indexOf('='); - if (equalIndex == -1) { - throw new IllegalArgumentException("Invalid key=value pair: \"" + arg + "\""); - } - String key = arg.substring(0, equalIndex); - String value = arg.substring(equalIndex + 1); - switch (key) { - case "scid": - int scid = Integer.parseInt(value, 0x10); - if (scid < -1) { - throw new IllegalArgumentException("scid may not be negative (except -1 for 'none'): " + scid); - } - options.setScid(scid); - break; - case "log_level": - Ln.Level level = Ln.Level.valueOf(value.toUpperCase(Locale.ENGLISH)); - options.setLogLevel(level); - break; - case "audio": - boolean audio = Boolean.parseBoolean(value); - options.setAudio(audio); - break; - case "video_codec": - VideoCodec videoCodec = VideoCodec.findByName(value); - if (videoCodec == null) { - throw new IllegalArgumentException("Video codec " + value + " not supported"); - } - options.setVideoCodec(videoCodec); - break; - case "audio_codec": - AudioCodec audioCodec = AudioCodec.findByName(value); - if (audioCodec == null) { - throw new IllegalArgumentException("Audio codec " + value + " not supported"); - } - options.setAudioCodec(audioCodec); - break; - case "max_size": - int maxSize = Integer.parseInt(value) & ~7; // multiple of 8 - options.setMaxSize(maxSize); - break; - case "video_bit_rate": - int videoBitRate = Integer.parseInt(value); - options.setVideoBitRate(videoBitRate); - break; - case "audio_bit_rate": - int audioBitRate = Integer.parseInt(value); - options.setAudioBitRate(audioBitRate); - break; - case "max_fps": - int maxFps = Integer.parseInt(value); - options.setMaxFps(maxFps); - break; - case "lock_video_orientation": - int lockVideoOrientation = Integer.parseInt(value); - options.setLockVideoOrientation(lockVideoOrientation); - break; - case "tunnel_forward": - boolean tunnelForward = Boolean.parseBoolean(value); - options.setTunnelForward(tunnelForward); - break; - case "crop": - Rect crop = parseCrop(value); - options.setCrop(crop); - break; - case "control": - boolean control = Boolean.parseBoolean(value); - options.setControl(control); - break; - case "display_id": - int displayId = Integer.parseInt(value); - options.setDisplayId(displayId); - break; - case "show_touches": - boolean showTouches = Boolean.parseBoolean(value); - options.setShowTouches(showTouches); - break; - case "stay_awake": - boolean stayAwake = Boolean.parseBoolean(value); - options.setStayAwake(stayAwake); - break; - case "video_codec_options": - List videoCodecOptions = CodecOption.parse(value); - options.setVideoCodecOptions(videoCodecOptions); - break; - case "audio_codec_options": - List audioCodecOptions = CodecOption.parse(value); - options.setAudioCodecOptions(audioCodecOptions); - break; - case "video_encoder": - if (!value.isEmpty()) { - options.setVideoEncoder(value); - } - break; - case "audio_encoder": - if (!value.isEmpty()) { - options.setAudioEncoder(value); - } - case "power_off_on_close": - boolean powerOffScreenOnClose = Boolean.parseBoolean(value); - options.setPowerOffScreenOnClose(powerOffScreenOnClose); - break; - case "clipboard_autosync": - boolean clipboardAutosync = Boolean.parseBoolean(value); - options.setClipboardAutosync(clipboardAutosync); - break; - case "downsize_on_error": - boolean downsizeOnError = Boolean.parseBoolean(value); - options.setDownsizeOnError(downsizeOnError); - break; - case "cleanup": - boolean cleanup = Boolean.parseBoolean(value); - options.setCleanup(cleanup); - break; - case "power_on": - boolean powerOn = Boolean.parseBoolean(value); - options.setPowerOn(powerOn); - break; - case "list_encoders": - boolean listEncoders = Boolean.parseBoolean(value); - options.setListEncoders(listEncoders); - break; - case "list_displays": - boolean listDisplays = Boolean.parseBoolean(value); - options.setListDisplays(listDisplays); - break; - case "send_device_meta": - boolean sendDeviceMeta = Boolean.parseBoolean(value); - options.setSendDeviceMeta(sendDeviceMeta); - break; - case "send_frame_meta": - boolean sendFrameMeta = Boolean.parseBoolean(value); - options.setSendFrameMeta(sendFrameMeta); - break; - case "send_dummy_byte": - boolean sendDummyByte = Boolean.parseBoolean(value); - options.setSendDummyByte(sendDummyByte); - break; - case "send_codec_meta": - boolean sendCodecMeta = Boolean.parseBoolean(value); - options.setSendCodecMeta(sendCodecMeta); - break; - case "raw_video_stream": - boolean rawVideoStream = Boolean.parseBoolean(value); - if (rawVideoStream) { - options.setSendDeviceMeta(false); - options.setSendFrameMeta(false); - options.setSendDummyByte(false); - options.setSendCodecMeta(false); - } - break; - default: - Ln.w("Unknown server option: " + key); - break; - } - } - - return options; - } - - private static Rect parseCrop(String crop) { - if (crop.isEmpty()) { - return null; - } - // input format: "width:height:x:y" - String[] tokens = crop.split(":"); - if (tokens.length != 4) { - throw new IllegalArgumentException("Crop must contains 4 values separated by colons: \"" + crop + "\""); - } - int width = Integer.parseInt(tokens[0]); - int height = Integer.parseInt(tokens[1]); - int x = Integer.parseInt(tokens[2]); - int y = Integer.parseInt(tokens[3]); - return new Rect(x, y, x + width, y + height); - } - public static void main(String... args) throws Exception { Thread.setDefaultUncaughtExceptionHandler((t, e) -> { Ln.e("Exception on thread " + t, e); }); - Options options = createOptions(args); + Options options = Options.parse(args); Ln.initLogLevel(options.getLogLevel()); From 9cfea347d0c4f460d2f06bb14f59363024f41c0a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 9 Apr 2023 19:58:34 +0200 Subject: [PATCH 1533/2244] Remove Options setters Now that options parsing is performed from the Options class, setters are not necessary anymore. --- .../java/com/genymobile/scrcpy/Options.java | 220 +++--------------- 1 file changed, 35 insertions(+), 185 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 0ebb790f..a34eb9b5 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -47,166 +47,82 @@ public class Options { return logLevel; } - public void setLogLevel(Ln.Level logLevel) { - this.logLevel = logLevel; - } - public int getScid() { return scid; } - public void setScid(int scid) { - this.scid = scid; - } - public boolean getAudio() { return audio; } - public void setAudio(boolean audio) { - this.audio = audio; - } - public int getMaxSize() { return maxSize; } - public void setMaxSize(int maxSize) { - this.maxSize = maxSize; - } - public VideoCodec getVideoCodec() { return videoCodec; } - public void setVideoCodec(VideoCodec videoCodec) { - this.videoCodec = videoCodec; - } - public AudioCodec getAudioCodec() { return audioCodec; } - public void setAudioCodec(AudioCodec audioCodec) { - this.audioCodec = audioCodec; - } - public int getVideoBitRate() { return videoBitRate; } - public void setVideoBitRate(int videoBitRate) { - this.videoBitRate = videoBitRate; - } - public int getAudioBitRate() { return audioBitRate; } - public void setAudioBitRate(int audioBitRate) { - this.audioBitRate = audioBitRate; - } - public int getMaxFps() { return maxFps; } - public void setMaxFps(int maxFps) { - this.maxFps = maxFps; - } - public int getLockVideoOrientation() { return lockVideoOrientation; } - public void setLockVideoOrientation(int lockVideoOrientation) { - this.lockVideoOrientation = lockVideoOrientation; - } - public boolean isTunnelForward() { return tunnelForward; } - public void setTunnelForward(boolean tunnelForward) { - this.tunnelForward = tunnelForward; - } - public Rect getCrop() { return crop; } - public void setCrop(Rect crop) { - this.crop = crop; - } - public boolean getControl() { return control; } - public void setControl(boolean control) { - this.control = control; - } - public int getDisplayId() { return displayId; } - public void setDisplayId(int displayId) { - this.displayId = displayId; - } - public boolean getShowTouches() { return showTouches; } - public void setShowTouches(boolean showTouches) { - this.showTouches = showTouches; - } - public boolean getStayAwake() { return stayAwake; } - public void setStayAwake(boolean stayAwake) { - this.stayAwake = stayAwake; - } - public List getVideoCodecOptions() { return videoCodecOptions; } - public void setVideoCodecOptions(List videoCodecOptions) { - this.videoCodecOptions = videoCodecOptions; - } - public List getAudioCodecOptions() { return audioCodecOptions; } - public void setAudioCodecOptions(List audioCodecOptions) { - this.audioCodecOptions = audioCodecOptions; - } - public String getVideoEncoder() { return videoEncoder; } - public void setVideoEncoder(String videoEncoder) { - this.videoEncoder = videoEncoder; - } - public String getAudioEncoder() { return audioEncoder; } - public void setAudioEncoder(String audioEncoder) { - this.audioEncoder = audioEncoder; - } - - public void setPowerOffScreenOnClose(boolean powerOffScreenOnClose) { - this.powerOffScreenOnClose = powerOffScreenOnClose; - } - public boolean getPowerOffScreenOnClose() { return this.powerOffScreenOnClose; } @@ -215,82 +131,42 @@ public class Options { return clipboardAutosync; } - public void setClipboardAutosync(boolean clipboardAutosync) { - this.clipboardAutosync = clipboardAutosync; - } - public boolean getDownsizeOnError() { return downsizeOnError; } - public void setDownsizeOnError(boolean downsizeOnError) { - this.downsizeOnError = downsizeOnError; - } - public boolean getCleanup() { return cleanup; } - public void setCleanup(boolean cleanup) { - this.cleanup = cleanup; - } - public boolean getPowerOn() { return powerOn; } - public void setPowerOn(boolean powerOn) { - this.powerOn = powerOn; - } - public boolean getListEncoders() { return listEncoders; } - public void setListEncoders(boolean listEncoders) { - this.listEncoders = listEncoders; - } - public boolean getListDisplays() { return listDisplays; } - public void setListDisplays(boolean listDisplays) { - this.listDisplays = listDisplays; - } - public boolean getSendDeviceMeta() { return sendDeviceMeta; } - public void setSendDeviceMeta(boolean sendDeviceMeta) { - this.sendDeviceMeta = sendDeviceMeta; - } - public boolean getSendFrameMeta() { return sendFrameMeta; } - public void setSendFrameMeta(boolean sendFrameMeta) { - this.sendFrameMeta = sendFrameMeta; - } - public boolean getSendDummyByte() { return sendDummyByte; } - public void setSendDummyByte(boolean sendDummyByte) { - this.sendDummyByte = sendDummyByte; - } - public boolean getSendCodecMeta() { return sendCodecMeta; } - public void setSendCodecMeta(boolean sendCodecMeta) { - this.sendCodecMeta = sendCodecMeta; - } - @SuppressWarnings("MethodLength") public static Options parse(String... args) { if (args.length < 1) { @@ -319,142 +195,116 @@ public class Options { if (scid < -1) { throw new IllegalArgumentException("scid may not be negative (except -1 for 'none'): " + scid); } - options.setScid(scid); + options.scid = scid; break; case "log_level": - Ln.Level level = Ln.Level.valueOf(value.toUpperCase(Locale.ENGLISH)); - options.setLogLevel(level); + options.logLevel = Ln.Level.valueOf(value.toUpperCase(Locale.ENGLISH)); break; case "audio": - boolean audio = Boolean.parseBoolean(value); - options.setAudio(audio); + options.audio = Boolean.parseBoolean(value); break; case "video_codec": VideoCodec videoCodec = VideoCodec.findByName(value); if (videoCodec == null) { throw new IllegalArgumentException("Video codec " + value + " not supported"); } - options.setVideoCodec(videoCodec); + options.videoCodec = videoCodec; break; case "audio_codec": AudioCodec audioCodec = AudioCodec.findByName(value); if (audioCodec == null) { throw new IllegalArgumentException("Audio codec " + value + " not supported"); } - options.setAudioCodec(audioCodec); + options.audioCodec = audioCodec; break; case "max_size": - int maxSize = Integer.parseInt(value) & ~7; // multiple of 8 - options.setMaxSize(maxSize); + options.maxSize = Integer.parseInt(value) & ~7; // multiple of 8 break; case "video_bit_rate": - int videoBitRate = Integer.parseInt(value); - options.setVideoBitRate(videoBitRate); + options.videoBitRate = Integer.parseInt(value); break; case "audio_bit_rate": - int audioBitRate = Integer.parseInt(value); - options.setAudioBitRate(audioBitRate); + options.audioBitRate = Integer.parseInt(value); break; case "max_fps": - int maxFps = Integer.parseInt(value); - options.setMaxFps(maxFps); + options.maxFps = Integer.parseInt(value); break; case "lock_video_orientation": - int lockVideoOrientation = Integer.parseInt(value); - options.setLockVideoOrientation(lockVideoOrientation); + options.lockVideoOrientation = Integer.parseInt(value); break; case "tunnel_forward": - boolean tunnelForward = Boolean.parseBoolean(value); - options.setTunnelForward(tunnelForward); + options.tunnelForward = Boolean.parseBoolean(value); break; case "crop": - Rect crop = parseCrop(value); - options.setCrop(crop); + options.crop = parseCrop(value); break; case "control": - boolean control = Boolean.parseBoolean(value); - options.setControl(control); + options.control = Boolean.parseBoolean(value); break; case "display_id": - int displayId = Integer.parseInt(value); - options.setDisplayId(displayId); + options.displayId = Integer.parseInt(value); break; case "show_touches": - boolean showTouches = Boolean.parseBoolean(value); - options.setShowTouches(showTouches); + options.showTouches = Boolean.parseBoolean(value); break; case "stay_awake": - boolean stayAwake = Boolean.parseBoolean(value); - options.setStayAwake(stayAwake); + options.stayAwake = Boolean.parseBoolean(value); break; case "video_codec_options": - List videoCodecOptions = CodecOption.parse(value); - options.setVideoCodecOptions(videoCodecOptions); + options.videoCodecOptions = CodecOption.parse(value); break; case "audio_codec_options": - List audioCodecOptions = CodecOption.parse(value); - options.setAudioCodecOptions(audioCodecOptions); + options.audioCodecOptions = CodecOption.parse(value); break; case "video_encoder": if (!value.isEmpty()) { - options.setVideoEncoder(value); + options.videoEncoder = value; } break; case "audio_encoder": if (!value.isEmpty()) { - options.setAudioEncoder(value); + options.audioEncoder = value; } case "power_off_on_close": - boolean powerOffScreenOnClose = Boolean.parseBoolean(value); - options.setPowerOffScreenOnClose(powerOffScreenOnClose); + options.powerOffScreenOnClose = Boolean.parseBoolean(value); break; case "clipboard_autosync": - boolean clipboardAutosync = Boolean.parseBoolean(value); - options.setClipboardAutosync(clipboardAutosync); + options.clipboardAutosync = Boolean.parseBoolean(value); break; case "downsize_on_error": - boolean downsizeOnError = Boolean.parseBoolean(value); - options.setDownsizeOnError(downsizeOnError); + options.downsizeOnError = Boolean.parseBoolean(value); break; case "cleanup": - boolean cleanup = Boolean.parseBoolean(value); - options.setCleanup(cleanup); + options.cleanup = Boolean.parseBoolean(value); break; case "power_on": - boolean powerOn = Boolean.parseBoolean(value); - options.setPowerOn(powerOn); + options.powerOn = Boolean.parseBoolean(value); break; case "list_encoders": - boolean listEncoders = Boolean.parseBoolean(value); - options.setListEncoders(listEncoders); + options.listEncoders = Boolean.parseBoolean(value); break; case "list_displays": - boolean listDisplays = Boolean.parseBoolean(value); - options.setListDisplays(listDisplays); + options.listDisplays = Boolean.parseBoolean(value); break; case "send_device_meta": - boolean sendDeviceMeta = Boolean.parseBoolean(value); - options.setSendDeviceMeta(sendDeviceMeta); + options.sendDeviceMeta = Boolean.parseBoolean(value); break; case "send_frame_meta": - boolean sendFrameMeta = Boolean.parseBoolean(value); - options.setSendFrameMeta(sendFrameMeta); + options.sendFrameMeta = Boolean.parseBoolean(value); break; case "send_dummy_byte": - boolean sendDummyByte = Boolean.parseBoolean(value); - options.setSendDummyByte(sendDummyByte); + options.sendDummyByte = Boolean.parseBoolean(value); break; case "send_codec_meta": - boolean sendCodecMeta = Boolean.parseBoolean(value); - options.setSendCodecMeta(sendCodecMeta); + options.sendCodecMeta = Boolean.parseBoolean(value); break; case "raw_video_stream": boolean rawVideoStream = Boolean.parseBoolean(value); if (rawVideoStream) { - options.setSendDeviceMeta(false); - options.setSendFrameMeta(false); - options.setSendDummyByte(false); - options.setSendCodecMeta(false); + options.sendDeviceMeta = false; + options.sendFrameMeta = false; + options.sendDummyByte = false; + options.sendCodecMeta = false; } break; default: From 9eb6591913bed3601df156a38bc6491f0360c8ce Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 10 Apr 2023 19:29:09 +0200 Subject: [PATCH 1534/2244] Add missing --no-audio option in manpage --- app/scrcpy.1 | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 97a15d1d..37497211 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -183,6 +183,10 @@ It may only work over USB. Also see \fB\-\-hid\-keyboard\fR. +.TP +.B \-\-no\-audio +Disable audio forwarding. + .TP .B \-\-no\-cleanup By default, scrcpy removes the server binary from the device and restores the device state (show touches, stay awake and power mode) on exit. From c083a7cc9015b52e12b0fb78e5a488ab1f5b68f6 Mon Sep 17 00:00:00 2001 From: Yan Date: Wed, 5 Apr 2023 16:04:03 +0200 Subject: [PATCH 1535/2244] Force OpenGL Core Profile context on macOS By default, SDL creates an OpenGL 2.1 context on macOS for an OpenGL renderer. As a consequence, mipmapping is not supported. Force to use a core profile context, to get a higher version. Before: INFO: Renderer: opengl INFO: OpenGL version: 2.1 NVIDIA-14.0.32 355.11.11.10.10.143 WARN: Trilinear filtering disabled (OpenGL 3.0+ or ES 2.0+ required) After: INFO: Renderer: opengl DEBUG: Creating OpenGL Core Profile context INFO: OpenGL version: 4.1 NVIDIA-14.0.32 355.11.11.10.10.143 INFO: Trilinear filtering enabled when running with: scrcpy --verbosity=debug --render-driver=opengl Note: Since SDL_CreateRenderer() causes a fallback to OpenGL 2.1, the profile and version attributes have to be set and the context created _after_. PR #3895 Signed-off-by: Romain Vimont --- app/src/display.c | 19 +++++++++++++++++++ app/src/display.h | 8 ++++++++ 2 files changed, 27 insertions(+) diff --git a/app/src/display.c b/app/src/display.c index 96ff9e06..4852952b 100644 --- a/app/src/display.c +++ b/app/src/display.c @@ -23,6 +23,22 @@ sc_display_init(struct sc_display *display, SDL_Window *window, bool mipmaps) { // starts with "opengl" bool use_opengl = renderer_name && !strncmp(renderer_name, "opengl", 6); if (use_opengl) { + +#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE + // Persuade macOS to give us something better than OpenGL 2.1. + // If we create a Core Profile context, we get the best OpenGL version. + SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, + SDL_GL_CONTEXT_PROFILE_CORE); + + LOGD("Creating OpenGL Core Profile context"); + display->gl_context = SDL_GL_CreateContext(window); + if (!display->gl_context) { + LOGE("Could not create OpenGL context: %s", SDL_GetError()); + SDL_DestroyRenderer(display->renderer); + return false; + } +#endif + struct sc_opengl *gl = &display->gl; sc_opengl_init(gl); @@ -51,6 +67,9 @@ sc_display_init(struct sc_display *display, SDL_Window *window, bool mipmaps) { void sc_display_destroy(struct sc_display *display) { +#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE + SDL_GL_DeleteContext(display->gl_context); +#endif if (display->texture) { SDL_DestroyTexture(display->texture); } diff --git a/app/src/display.h b/app/src/display.h index 8856afcd..e30b4822 100644 --- a/app/src/display.h +++ b/app/src/display.h @@ -10,11 +10,19 @@ #include "coords.h" #include "opengl.h" +#ifdef __APPLE__ +# define SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE +#endif + struct sc_display { SDL_Renderer *renderer; SDL_Texture *texture; struct sc_opengl gl; +#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE + SDL_GLContext *gl_context; +#endif + bool mipmaps; }; From 0f3af2d20b201a07ad999699e07e59cf8066e32f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 22 Apr 2023 19:46:45 +0200 Subject: [PATCH 1536/2244] Fix build for FFmpeg < 3.3 The constant AV_CODEC_ID_AV1 was introduced in FFmpeg 3.3. Add an ifdef to support older versions. Fixes #3939 --- app/src/compat.h | 6 ++++++ app/src/demuxer.c | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/app/src/compat.h b/app/src/compat.h index 00cb7204..e80a9dd2 100644 --- a/app/src/compat.h +++ b/app/src/compat.h @@ -25,6 +25,12 @@ # define SCRCPY_LAVF_REQUIRES_REGISTER_ALL #endif +// Not documented in ffmpeg/doc/APIchanges, but AV_CODEC_ID_AV1 has been added +// by FFmpeg commit d42809f9835a4e9e5c7c63210abb09ad0ef19cfb (included in tag +// n3.3). +#if LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(57, 89, 100) +# define SCRCPY_LAVC_HAS_AV1 +#endif // In ffmpeg/doc/APIchanges: // 2018-01-28 - ea3672b7d6 - lavf 58.7.100 - avformat.h diff --git a/app/src/demuxer.c b/app/src/demuxer.c index 5a613505..4fcdd9ad 100644 --- a/app/src/demuxer.c +++ b/app/src/demuxer.c @@ -33,7 +33,12 @@ sc_demuxer_to_avcodec_id(uint32_t codec_id) { case SC_CODEC_ID_H265: return AV_CODEC_ID_HEVC; case SC_CODEC_ID_AV1: +#ifdef SCRCPY_LAVC_HAS_AV1 return AV_CODEC_ID_AV1; +#else + LOGE("AV1 not supported by this FFmpeg version"); + return AV_CODEC_ID_NONE; +#endif case SC_CODEC_ID_OPUS: return AV_CODEC_ID_OPUS; case SC_CODEC_ID_AAC: From cb20bcb16f4ca191e237c8744a7c2e6d29701d60 Mon Sep 17 00:00:00 2001 From: parknich081 <45834520+parknich081@users.noreply.github.com> Date: Wed, 26 Apr 2023 09:49:26 -0400 Subject: [PATCH 1537/2244] Clarify API versions that support Audio Forwarding Reword the supported API versions for audio forwarding sentence to clarify that it supports API >= 30 PR #3949 Signed-off-by: Romain Vimont --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9969dc51..9002031d 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ Its features include: The Android device requires at least API 21 (Android 5.0). -[Audio forwarding](doc/audio.md) is supported from API 30 (Android 11). +[Audio forwarding](doc/audio.md) is supported for API >= 30 (Android 11+). Make sure you [enabled USB debugging][enable-adb] on your device(s). From 6928acdeac29eee404a7c7014654965ef5128b88 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 5 May 2023 23:43:14 +0200 Subject: [PATCH 1538/2244] Rename --no-display to --no-mirror The option impacts both video and audio playback, so "no display" is not an appropriate name. PR #3978 --- app/data/bash-completion/scrcpy | 2 +- app/data/zsh-completion/_scrcpy | 2 +- app/scrcpy.1 | 4 ++-- app/src/cli.c | 27 ++++++++++++++++++--------- app/src/options.c | 2 +- app/src/options.h | 2 +- app/src/scrcpy.c | 20 +++++++++----------- app/tests/test_cli.c | 8 ++++---- doc/recording.md | 2 +- doc/v4l2.md | 2 +- doc/video.md | 6 +++--- 11 files changed, 42 insertions(+), 35 deletions(-) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index ae516c34..a0fca23d 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -33,7 +33,7 @@ _scrcpy() { --no-clipboard-autosync --no-downsize-on-error -n --no-control - -N --no-display + -N --no-mirror --no-key-repeat --no-mipmaps --no-power-on diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 97bf4f3e..ccb51a2c 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -39,7 +39,7 @@ arguments=( '--no-clipboard-autosync[Disable automatic clipboard synchronization]' '--no-downsize-on-error[Disable lowering definition on MediaCodec error]' {-n,--no-control}'[Disable device control \(mirror the device in read only\)]' - {-N,--no-display}'[Do not display device \(during screen recording or when V4L2 sink is enabled\)]' + {-N,--no-mirror}'[Do not mirror device \(only when recording or V4L2 sink is enabled\)]' '--no-key-repeat[Do not forward repeated key events when a key is held down]' '--no-mipmaps[Disable the generation of mipmaps]' '--no-power-on[Do not power on the device on start]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 37497211..6ef01680 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -210,8 +210,8 @@ This option disables this behavior. Disable device control (mirror the device in read\-only). .TP -.B \-N, \-\-no\-display -Do not display device (only when screen recording is enabled). +.B \-N, \-\-no\-mirror +Do not mirror device video or audio on the computer (only when recording or V4L2 sink is enabled). .TP .B \-\-no\-key\-repeat diff --git a/app/src/cli.c b/app/src/cli.c index d6d9f41d..1ba7fe1f 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -72,6 +72,7 @@ enum { OPT_REQUIRE_AUDIO, OPT_AUDIO_BUFFER, OPT_AUDIO_OUTPUT_BUFFER, + OPT_NO_DISPLAY, }; struct sc_option { @@ -380,9 +381,14 @@ static const struct sc_option options[] = { }, { .shortopt = 'N', + .longopt = "no-mirror", + .text = "Do not mirror device video or audio on the computer (only " + "when recording or V4L2 sink is enabled).", + }, + { + // deprecated + .longopt_id = OPT_NO_DISPLAY, .longopt = "no-display", - .text = "Do not display device (only when screen recording or V4L2 " - "sink is enabled).", }, { .longopt_id = OPT_NO_KEY_REPEAT, @@ -1642,8 +1648,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], case 'n': opts->control = false; break; + case OPT_NO_DISPLAY: + LOGW("--no-display is deprecated, use --no-mirror instead."); + // fall through case 'N': - opts->display = false; + opts->mirror = false; break; case 'p': if (!parse_port_range(optarg, &opts->port_range)) { @@ -1890,8 +1899,8 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } #ifdef HAVE_V4L2 - if (!opts->display && !opts->record_filename && !opts->v4l2_device) { - LOGE("-N/--no-display requires either screen recording (-r/--record)" + if (!opts->mirror && !opts->record_filename && !opts->v4l2_device) { + LOGE("-N/--no-mirror requires either screen recording (-r/--record)" " or sink to v4l2loopback device (--v4l2-sink)"); return false; } @@ -1915,14 +1924,14 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; } #else - if (!opts->display && !opts->record_filename) { - LOGE("-N/--no-display requires screen recording (-r/--record)"); + if (!opts->mirror && !opts->record_filename) { + LOGE("-N/--no-mirror requires screen recording (-r/--record)"); return false; } #endif - if (opts->audio && !opts->display && !opts->record_filename) { - LOGI("No display and no recording: audio disabled"); + if (opts->audio && !opts->mirror && !opts->record_filename) { + LOGI("No mirror and no recording: audio disabled"); opts->audio = false; } diff --git a/app/src/options.c b/app/src/options.c index 8b99f6f3..eec81716 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -52,7 +52,7 @@ const struct scrcpy_options scrcpy_options_default = { .fullscreen = false, .always_on_top = false, .control = true, - .display = true, + .mirror = true, .turn_screen_off = false, .key_inject_mode = SC_KEY_INJECT_MODE_MIXED, .window_borderless = false, diff --git a/app/src/options.h b/app/src/options.h index c41e2757..3bb0c91e 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -135,7 +135,7 @@ struct scrcpy_options { bool fullscreen; bool always_on_top; bool control; - bool display; + bool mirror; bool turn_screen_off; enum sc_key_inject_mode key_inject_mode; bool window_borderless; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index efa69d31..2c7fbf30 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -137,7 +137,7 @@ sdl_set_hints(const char *render_driver) { } static void -sdl_configure(bool display, bool disable_screensaver) { +sdl_configure(bool mirror, bool disable_screensaver) { #ifdef _WIN32 // Clean up properly on Ctrl+C on Windows bool ok = SetConsoleCtrlHandler(windows_ctrl_handler, TRUE); @@ -146,7 +146,7 @@ sdl_configure(bool display, bool disable_screensaver) { } #endif // _WIN32 - if (!display) { + if (!mirror) { return; } @@ -385,12 +385,10 @@ scrcpy(struct scrcpy_options *options) { goto end; } - if (options->display) { + if (options->mirror) { sdl_set_hints(options->render_driver); - } - // Initialize SDL video in addition if display is enabled - if (options->display) { + // Initialize SDL video and audio in addition if mirroring is enabled if (SDL_Init(SDL_INIT_VIDEO)) { LOGE("Could not initialize SDL video: %s", SDL_GetError()); goto end; @@ -402,7 +400,7 @@ scrcpy(struct scrcpy_options *options) { } } - sdl_configure(options->display, options->disable_screensaver); + sdl_configure(options->mirror, options->disable_screensaver); // Await for server without blocking Ctrl+C handling bool connected; @@ -428,7 +426,7 @@ scrcpy(struct scrcpy_options *options) { struct sc_file_pusher *fp = NULL; - if (options->display && options->control) { + if (options->mirror && options->control) { if (!sc_file_pusher_init(&s->file_pusher, serial, options->push_target)) { goto end; @@ -451,8 +449,8 @@ scrcpy(struct scrcpy_options *options) { &audio_demuxer_cbs, options); } - bool needs_video_decoder = options->display; - bool needs_audio_decoder = options->audio && options->display; + bool needs_video_decoder = options->mirror; + bool needs_audio_decoder = options->mirror && options->audio; #ifdef HAVE_V4L2 needs_video_decoder |= !!options->v4l2_device; #endif @@ -646,7 +644,7 @@ aoa_hid_end: // There is a controller if and only if control is enabled assert(options->control == !!controller); - if (options->display) { + if (options->mirror) { const char *window_title = options->window_title ? options->window_title : info->device_name; diff --git a/app/tests/test_cli.c b/app/tests/test_cli.c index 3e9a248a..1f00cb90 100644 --- a/app/tests/test_cli.c +++ b/app/tests/test_cli.c @@ -53,7 +53,7 @@ static void test_options(void) { "--max-size", "1024", "--lock-video-orientation=2", // optional arguments require '=' // "--no-control" is not compatible with "--turn-screen-off" - // "--no-display" is not compatible with "--fulscreen" + // "--no-mirror" is not compatible with "--fulscreen" "--port", "1234:1236", "--push-target", "/sdcard/Movies", "--record", "file", @@ -108,8 +108,8 @@ static void test_options2(void) { char *argv[] = { "scrcpy", "--no-control", - "--no-display", - "--record", "file.mp4", // cannot enable --no-display without recording + "--no-mirror", + "--record", "file.mp4", // cannot enable --no-mirror without recording }; bool ok = scrcpy_parse_args(&args, ARRAY_LEN(argv), argv); @@ -117,7 +117,7 @@ static void test_options2(void) { const struct scrcpy_options *opts = &args.opts; assert(!opts->control); - assert(!opts->display); + assert(!opts->mirror); assert(!strcmp(opts->record_filename, "file.mp4")); assert(opts->record_format == SC_RECORD_FORMAT_MP4); } diff --git a/doc/recording.md b/doc/recording.md index 4aad088c..9455a6fc 100644 --- a/doc/recording.md +++ b/doc/recording.md @@ -18,7 +18,7 @@ _It is currently not possible to record only the audio._ To disable mirroring while recording: ```bash -scrcpy --no-display --record=file.mp4 +scrcpy --no-mirror --record=file.mp4 scrcpy -Nr file.mkv # interrupt recording with Ctrl+C ``` diff --git a/doc/v4l2.md b/doc/v4l2.md index ea8c0eed..2c6e2cfc 100644 --- a/doc/v4l2.md +++ b/doc/v4l2.md @@ -35,7 +35,7 @@ To start `scrcpy` using a v4l2 sink: ```bash scrcpy --v4l2-sink=/dev/videoN -scrcpy --v4l2-sink=/dev/videoN --no-display # disable mirroring window +scrcpy --v4l2-sink=/dev/videoN --no-mirror # disable mirroring window ``` (replace `N` with the device ID, check with `ls /dev/video*`) diff --git a/doc/video.md b/doc/video.md index a2e9d106..58aa5022 100644 --- a/doc/video.md +++ b/doc/video.md @@ -159,15 +159,15 @@ scrcpy --display-buffer=50 --v4l2-buffer=300 ``` -## No display +## No mirror It is possible to capture an Android device without displaying a mirroring window. This option is available if either [recording](recording.md) or [v4l2](#video4linux) is enabled: ```bash -scrcpy --v4l2-sink=/dev/video2 --no-display -scrcpy --record=file.mkv --no-display +scrcpy --v4l2-sink=/dev/video2 --no-mirror +scrcpy --record=file.mkv --no-mirror ``` ## Video4Linux From 92483fe11b6fd6bae5ef775ccaff78fefa92aad4 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 5 May 2023 23:45:55 +0200 Subject: [PATCH 1539/2244] Disable controls on --no-mirror If mirroring is disabled, control must also be disabled. PR #3978 --- app/src/cli.c | 9 +++++++++ app/src/scrcpy.c | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/app/src/cli.c b/app/src/cli.c index 1ba7fe1f..066f46fc 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -2041,6 +2041,15 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } #endif +#ifdef HAVE_USB + if (!opts->mirror && opts->control && !opts->otg) { +#else + if (!opts->mirror && opts->control) { +#endif + LOGD("Mirroring is disabled, force --no-control"); + opts->control = false; + } + return true; } diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 2c7fbf30..03b643f6 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -426,7 +426,8 @@ scrcpy(struct scrcpy_options *options) { struct sc_file_pusher *fp = NULL; - if (options->mirror && options->control) { + assert(!options->control || options->mirror); // control implies mirror + if (options->control) { if (!sc_file_pusher_init(&s->file_pusher, serial, options->push_target)) { goto end; From 9c08eb79cb7941848882cb908cefee9933450de5 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 9 Apr 2023 19:12:39 +0200 Subject: [PATCH 1540/2244] Close connection at the end of finally-block The async processors use the socket file descriptors from the connection. Therefore, the connection must not be closed before all async processor threads are joined. PR #3978 --- server/src/main/java/com/genymobile/scrcpy/Server.java | 5 ++++- 1 file changed, 4 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 067b1670..fade7214 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -92,7 +92,8 @@ public final class Server { List asyncProcessors = new ArrayList<>(); - try (DesktopConnection connection = DesktopConnection.open(scid, tunnelForward, audio, control, sendDummyByte)) { + DesktopConnection connection = DesktopConnection.open(scid, tunnelForward, audio, control, sendDummyByte); + try { if (options.getSendDeviceMeta()) { connection.sendDeviceMeta(Device.getDeviceName()); } @@ -150,6 +151,8 @@ public final class Server { } catch (InterruptedException e) { // ignore } + + connection.close(); } } From 751a3653a0ab36b98d29da689f26d315c8426701 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 9 Apr 2023 15:18:31 +0200 Subject: [PATCH 1541/2244] Add missing @Override annotations PR #3978 --- server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java | 3 +++ .../src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java | 3 +++ server/src/main/java/com/genymobile/scrcpy/Controller.java | 3 +++ 3 files changed, 9 insertions(+) diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java b/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java index f2bba772..ac2f0a31 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java @@ -114,6 +114,7 @@ public final class AudioEncoder implements AsyncProcessor { } } + @Override public void start() { thread = new Thread(() -> { try { @@ -129,6 +130,7 @@ public final class AudioEncoder implements AsyncProcessor { thread.start(); } + @Override public void stop() { if (thread != null) { // Just wake up the blocking wait from the thread, so that it properly releases all its resources and terminates @@ -136,6 +138,7 @@ public final class AudioEncoder implements AsyncProcessor { } } + @Override public void join() throws InterruptedException { if (thread != null) { thread.join(); diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java b/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java index f98440de..32efc354 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java @@ -53,6 +53,7 @@ public final class AudioRawRecorder implements AsyncProcessor { } } + @Override public void start() { thread = new Thread(() -> { try { @@ -68,12 +69,14 @@ public final class AudioRawRecorder implements AsyncProcessor { thread.start(); } + @Override public void stop() { if (thread != null) { thread.interrupt(); } } + @Override public void join() throws InterruptedException { if (thread != null) { thread.join(); diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index 59fae602..ab09c336 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -84,6 +84,7 @@ public class Controller implements AsyncProcessor { } } + @Override public void start() { thread = new Thread(() -> { try { @@ -98,6 +99,7 @@ public class Controller implements AsyncProcessor { sender.start(); } + @Override public void stop() { if (thread != null) { thread.interrupt(); @@ -105,6 +107,7 @@ public class Controller implements AsyncProcessor { sender.stop(); } + @Override public void join() throws InterruptedException { if (thread != null) { thread.join(); From feab87053abcceded41342d9d856763dedc09187 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 9 Apr 2023 15:17:54 +0200 Subject: [PATCH 1542/2244] Convert screen encoder to async processor Contrary to the other tasks (controller and audio capture/encoding), the screen encoder was executed synchronously. As a consequence, scrcpy-server could not terminate until the screen encoder returned. Convert it to an async processor. This allows to terminate on controller error, and this paves the way to disable video mirroring. PR #3978 --- .../com/genymobile/scrcpy/AsyncProcessor.java | 11 +++- .../com/genymobile/scrcpy/AudioEncoder.java | 10 +++- .../genymobile/scrcpy/AudioRawRecorder.java | 5 +- .../com/genymobile/scrcpy/Controller.java | 3 +- .../com/genymobile/scrcpy/ScreenEncoder.java | 50 +++++++++++++++++-- .../java/com/genymobile/scrcpy/Server.java | 46 +++++++++++++---- 6 files changed, 105 insertions(+), 20 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/AsyncProcessor.java b/server/src/main/java/com/genymobile/scrcpy/AsyncProcessor.java index cbc435b0..b9b6745c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AsyncProcessor.java +++ b/server/src/main/java/com/genymobile/scrcpy/AsyncProcessor.java @@ -1,7 +1,16 @@ package com.genymobile.scrcpy; public interface AsyncProcessor { - void start(); + interface TerminationListener { + /** + * Notify processor termination + * + * @param fatalError {@code true} if this must cause the termination of the whole scrcpy-server. + */ + void onTerminated(boolean fatalError); + } + + void start(TerminationListener listener); void stop(); void join() throws InterruptedException; } diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java b/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java index ac2f0a31..a1abd71b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java @@ -115,16 +115,22 @@ public final class AudioEncoder implements AsyncProcessor { } @Override - public void start() { + public void start(TerminationListener listener) { thread = new Thread(() -> { + boolean fatalError = false; try { encode(); - } catch (ConfigurationException | AudioCaptureForegroundException e) { + } catch (ConfigurationException e) { + // Do not print stack trace, a user-friendly error-message has already been logged + fatalError = true; + } catch (AudioCaptureForegroundException e) { // Do not print stack trace, a user-friendly error-message has already been logged } catch (IOException e) { Ln.e("Audio encoding error", e); + fatalError = true; } finally { Ln.d("Audio encoder stopped"); + listener.onTerminated(fatalError); } }); thread.start(); diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java b/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java index 32efc354..685ac3bd 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java @@ -54,16 +54,19 @@ public final class AudioRawRecorder implements AsyncProcessor { } @Override - public void start() { + public void start(TerminationListener listener) { thread = new Thread(() -> { + boolean fatalError = false; try { record(); } catch (AudioCaptureForegroundException e) { // Do not print stack trace, a user-friendly error-message has already been logged } catch (IOException e) { Ln.e("Audio recording error", e); + fatalError = true; } finally { Ln.d("Audio recorder stopped"); + listener.onTerminated(fatalError); } }); thread.start(); diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index ab09c336..9a4e275a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -85,7 +85,7 @@ public class Controller implements AsyncProcessor { } @Override - public void start() { + public void start(TerminationListener listener) { thread = new Thread(() -> { try { control(); @@ -93,6 +93,7 @@ public class Controller implements AsyncProcessor { // this is expected on close } finally { Ln.d("Controller stopped"); + listener.onTerminated(true); } }); thread.start(); diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index 528cd327..901ba94c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -16,7 +16,7 @@ import java.nio.ByteBuffer; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; -public class ScreenEncoder implements Device.RotationListener { +public class ScreenEncoder implements Device.RotationListener, AsyncProcessor { private static final int DEFAULT_I_FRAME_INTERVAL = 10; // seconds private static final int REPEAT_FRAME_DELAY_US = 100_000; // repeat after 100ms @@ -39,6 +39,9 @@ public class ScreenEncoder implements Device.RotationListener { private boolean firstFrameSent; private int consecutiveErrors; + private Thread thread; + private final AtomicBoolean stopped = new AtomicBoolean(); + public ScreenEncoder(Device device, Streamer streamer, int videoBitRate, int maxFps, List codecOptions, String encoderName, boolean downsizeOnError) { this.device = device; @@ -55,11 +58,11 @@ public class ScreenEncoder implements Device.RotationListener { rotationChanged.set(true); } - public boolean consumeRotationChange() { + private boolean consumeRotationChange() { return rotationChanged.getAndSet(false); } - public void streamScreen() throws IOException, ConfigurationException { + private void streamScreen() throws IOException, ConfigurationException { Codec codec = streamer.getCodec(); MediaCodec mediaCodec = createMediaCodec(codec, encoderName); MediaFormat format = createFormat(codec.getMimeType(), videoBitRate, maxFps, codecOptions); @@ -163,9 +166,14 @@ public class ScreenEncoder implements Device.RotationListener { private boolean encode(MediaCodec codec, Streamer streamer) throws IOException { boolean eof = false; + boolean alive = true; MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); while (!consumeRotationChange() && !eof) { + if (stopped.get()) { + alive = false; + break; + } int outputBufferId = codec.dequeueOutputBuffer(bufferInfo, -1); try { if (consumeRotationChange()) { @@ -193,7 +201,7 @@ public class ScreenEncoder implements Device.RotationListener { } } - return !eof; + return !eof && alive; } private static MediaCodec createMediaCodec(Codec codec, String encoderName) throws IOException, ConfigurationException { @@ -267,4 +275,38 @@ public class ScreenEncoder implements Device.RotationListener { SurfaceControl.closeTransaction(); } } + + @Override + public void start(TerminationListener listener) { + thread = new Thread(() -> { + try { + streamScreen(); + } catch (ConfigurationException e) { + // Do not print stack trace, a user-friendly error-message has already been logged + } catch (IOException e) { + // Broken pipe is expected on close, because the socket is closed by the client + if (!IO.isBrokenPipe(e)) { + Ln.e("Video encoding error", e); + } + } finally { + Ln.d("Screen streaming stopped"); + listener.onTerminated(true); + } + }); + thread.start(); + } + + @Override + public void stop() { + if (thread != null) { + stopped.set(true); + } + } + + @Override + public void join() throws InterruptedException { + if (thread != null) { + thread.join(); + } + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index fade7214..4d72d1e8 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -9,6 +9,35 @@ import java.util.List; public final class Server { + private static class Completion { + private int running; + private boolean fatalError; + + Completion(int running) { + this.running = running; + } + + synchronized void addCompleted(boolean fatalError) { + --running; + if (fatalError) { + this.fatalError = true; + } + if (running == 0 || this.fatalError) { + notify(); + } + } + + synchronized void await() { + try { + while (running > 0 && !fatalError) { + wait(); + } + } catch (InterruptedException e) { + // ignore + } + } + } + private Server() { // not instantiable } @@ -122,22 +151,17 @@ public final class Server { options.getSendFrameMeta()); ScreenEncoder screenEncoder = new ScreenEncoder(device, videoStreamer, options.getVideoBitRate(), options.getMaxFps(), options.getVideoCodecOptions(), options.getVideoEncoder(), options.getDownsizeOnError()); + asyncProcessors.add(screenEncoder); + Completion completion = new Completion(asyncProcessors.size()); for (AsyncProcessor asyncProcessor : asyncProcessors) { - asyncProcessor.start(); + asyncProcessor.start((fatalError) -> { + completion.addCompleted(fatalError); + }); } - try { - // synchronous - screenEncoder.streamScreen(); - } catch (IOException e) { - // Broken pipe is expected on close, because the socket is closed by the client - if (!IO.isBrokenPipe(e)) { - Ln.e("Video encoding error", e); - } - } + completion.await(); } finally { - Ln.d("Screen streaming stopped"); initThread.interrupt(); for (AsyncProcessor asyncProcessor : asyncProcessors) { asyncProcessor.stop(); From e89e772c7c3df65e33362a542cd900f46cf62baf Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 7 May 2023 12:02:45 +0200 Subject: [PATCH 1543/2244] Remove unnecessary 'else' Some server parameters may depend on one another. For example, audio_bit_rate is meaningless if audio is false. But it is inconsistent to disable some parameters based on these dependencies checks, but not others. Handling all dependencies between parameters would add too much complexity for no benefit. So just pass individual parameters independently. PR #3978 --- app/src/server.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/server.c b/app/src/server.c index 8c4e9a95..2b35c085 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -231,7 +231,8 @@ execute_server(struct sc_server *server, } if (!params->audio) { ADD_PARAM("audio=false"); - } else if (params->audio_bit_rate) { + } + if (params->audio_bit_rate) { ADD_PARAM("audio_bit_rate=%" PRIu32, params->audio_bit_rate); } if (params->video_codec != SC_CODEC_H264) { From 8c650e53cd37a53d5c3aa746c30a71c6b742a4e2 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 7 May 2023 12:08:50 +0200 Subject: [PATCH 1544/2244] Add --no-video Similar to --no-audio, add --no-video to play audio only. Fixes #3842 PR #3978 --- app/data/bash-completion/scrcpy | 1 + app/data/zsh-completion/_scrcpy | 1 + app/scrcpy.1 | 4 + app/src/cli.c | 21 ++++- app/src/options.c | 1 + app/src/options.h | 1 + app/src/recorder.c | 69 ++++++++++------ app/src/recorder.h | 3 +- app/src/scrcpy.c | 54 ++++++++----- app/src/server.c | 80 ++++++++++++------- app/src/server.h | 1 + doc/audio.md | 15 ++++ doc/recording.md | 6 +- doc/video.md | 10 +++ .../genymobile/scrcpy/DesktopConnection.java | 49 +++++++++--- .../java/com/genymobile/scrcpy/Options.java | 8 ++ .../java/com/genymobile/scrcpy/Server.java | 15 ++-- 17 files changed, 243 insertions(+), 96 deletions(-) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index a0fca23d..cdc9270f 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -37,6 +37,7 @@ _scrcpy() { --no-key-repeat --no-mipmaps --no-power-on + --no-video --otg -p --port= --power-off-on-close diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index ccb51a2c..5e40b2fb 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -43,6 +43,7 @@ arguments=( '--no-key-repeat[Do not forward repeated key events when a key is held down]' '--no-mipmaps[Disable the generation of mipmaps]' '--no-power-on[Do not power on the device on start]' + '--no-video[Disable video forwarding]' '--otg[Run in OTG mode \(simulating physical keyboard and mouse\)]' {-p,--port=}'[\[port\[\:port\]\] Set the TCP port \(range\) used by the client to listen]' '--power-off-on-close[Turn the device screen off when closing scrcpy]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 6ef01680..29d14b58 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -225,6 +225,10 @@ If the renderer is OpenGL 3.0+ or OpenGL ES 2.0+, then mipmaps are automatically .B \-\-no\-power\-on Do not power on the device on start. +.TP +.B \-\-no\-video +Disable video forwarding. + .TP .B \-\-otg Run in OTG mode: simulate physical keyboard and mouse, as if the computer keyboard and mouse were plugged directly to the device via an OTG cable. diff --git a/app/src/cli.c b/app/src/cli.c index 066f46fc..1558ef0c 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -73,6 +73,7 @@ enum { OPT_AUDIO_BUFFER, OPT_AUDIO_OUTPUT_BUFFER, OPT_NO_DISPLAY, + OPT_NO_VIDEO, }; struct sc_option { @@ -407,6 +408,11 @@ static const struct sc_option options[] = { .longopt = "no-power-on", .text = "Do not power on the device on start.", }, + { + .longopt_id = OPT_NO_VIDEO, + .longopt = "no-video", + .text = "Disable video forwarding.", + }, { .longopt_id = OPT_OTG, .longopt = "otg", @@ -1797,6 +1803,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], case OPT_NO_DOWNSIZE_ON_ERROR: opts->downsize_on_error = false; break; + case OPT_NO_VIDEO: + opts->video = false; + break; case OPT_NO_AUDIO: opts->audio = false; break; @@ -2042,14 +2051,20 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], #endif #ifdef HAVE_USB - if (!opts->mirror && opts->control && !opts->otg) { + if (!(opts->mirror && opts->video) && !opts->otg) { #else - if (!opts->mirror && opts->control) { + if (!(opts->mirror && opts->video)) { #endif - LOGD("Mirroring is disabled, force --no-control"); + // If video mirroring is disabled and OTG are disabled, then there is + // no way to control the device. opts->control = false; } + if (!opts->video) { + // If video is disabled, then scrcpy must exit on audio failure. + opts->require_audio = true; + } + return true; } diff --git a/app/src/options.c b/app/src/options.c index eec81716..12283952 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -73,6 +73,7 @@ const struct scrcpy_options scrcpy_options_default = { .cleanup = true, .start_fps_counter = false, .power_on = true, + .video = true, .audio = true, .require_audio = false, .list_encoders = false, diff --git a/app/src/options.h b/app/src/options.h index 3bb0c91e..4edf3f32 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -156,6 +156,7 @@ struct scrcpy_options { bool cleanup; bool start_fps_counter; bool power_on; + bool video; bool audio; bool require_audio; bool list_encoders; diff --git a/app/src/recorder.c b/app/src/recorder.c index 2fc95eca..5cbe6873 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -152,7 +152,7 @@ sc_recorder_close_output_file(struct sc_recorder *recorder) { static inline bool sc_recorder_has_empty_queues(struct sc_recorder *recorder) { - if (sc_vecdeque_is_empty(&recorder->video_queue)) { + if (recorder->video && sc_vecdeque_is_empty(&recorder->video_queue)) { // The video queue is empty return true; } @@ -176,7 +176,7 @@ sc_recorder_process_header(struct sc_recorder *recorder) { sc_cond_wait(&recorder->stream_cond, &recorder->mutex); } - if (sc_vecdeque_is_empty(&recorder->video_queue)) { + if (recorder->video && sc_vecdeque_is_empty(&recorder->video_queue)) { assert(recorder->stopped); // If the recorder is stopped, don't process anything if there are not // at least video packets @@ -184,7 +184,11 @@ sc_recorder_process_header(struct sc_recorder *recorder) { return false; } - AVPacket *video_pkt = sc_vecdeque_pop(&recorder->video_queue); + AVPacket *video_pkt = NULL; + if (!sc_vecdeque_is_empty(&recorder->video_queue)) { + assert(recorder->video); + video_pkt = sc_vecdeque_pop(&recorder->video_queue); + } AVPacket *audio_pkt = NULL; if (!sc_vecdeque_is_empty(&recorder->audio_queue)) { @@ -196,17 +200,19 @@ sc_recorder_process_header(struct sc_recorder *recorder) { int ret = false; - if (video_pkt->pts != AV_NOPTS_VALUE) { - LOGE("The first video packet is not a config packet"); - goto end; - } + if (video_pkt) { + if (video_pkt->pts != AV_NOPTS_VALUE) { + LOGE("The first video packet is not a config packet"); + goto end; + } - assert(recorder->video_stream_index >= 0); - AVStream *video_stream = - recorder->ctx->streams[recorder->video_stream_index]; - bool ok = sc_recorder_set_extradata(video_stream, video_pkt); - if (!ok) { - goto end; + assert(recorder->video_stream_index >= 0); + AVStream *video_stream = + recorder->ctx->streams[recorder->video_stream_index]; + bool ok = sc_recorder_set_extradata(video_stream, video_pkt); + if (!ok) { + goto end; + } } if (audio_pkt) { @@ -218,13 +224,13 @@ sc_recorder_process_header(struct sc_recorder *recorder) { assert(recorder->audio_stream_index >= 0); AVStream *audio_stream = recorder->ctx->streams[recorder->audio_stream_index]; - ok = sc_recorder_set_extradata(audio_stream, audio_pkt); + bool ok = sc_recorder_set_extradata(audio_stream, audio_pkt); if (!ok) { goto end; } } - ok = avformat_write_header(recorder->ctx, NULL) >= 0; + bool ok = avformat_write_header(recorder->ctx, NULL) >= 0; if (!ok) { LOGE("Failed to write header to %s", recorder->filename); goto end; @@ -233,7 +239,9 @@ sc_recorder_process_header(struct sc_recorder *recorder) { ret = true; end: - av_packet_free(&video_pkt); + if (video_pkt) { + av_packet_free(&video_pkt); + } if (audio_pkt) { av_packet_free(&audio_pkt); } @@ -263,7 +271,8 @@ sc_recorder_process_packets(struct sc_recorder *recorder) { sc_mutex_lock(&recorder->mutex); while (!recorder->stopped) { - if (!video_pkt && !sc_vecdeque_is_empty(&recorder->video_queue)) { + if (recorder->video && !video_pkt && + !sc_vecdeque_is_empty(&recorder->video_queue)) { // A new packet may be assigned to video_pkt and be processed break; } @@ -278,6 +287,11 @@ sc_recorder_process_packets(struct sc_recorder *recorder) { // If stopped is set, continue to process the remaining events (to // finish the recording) before actually stopping. + // If there is no video, then the video_queue will remain empty forever + // and video_pkt will always be NULL. + assert(recorder->video || (!video_pkt + && sc_vecdeque_is_empty(&recorder->video_queue))); + // If there is no audio, then the audio_queue will remain empty forever // and audio_pkt will always be NULL. assert(recorder->audio || (!audio_pkt @@ -319,6 +333,9 @@ sc_recorder_process_packets(struct sc_recorder *recorder) { if (!recorder->audio) { assert(video_pkt); pts_origin = video_pkt->pts; + } else if (!recorder->video) { + assert(audio_pkt); + pts_origin = audio_pkt->pts; } else if (video_pkt && audio_pkt) { pts_origin = MIN(video_pkt->pts, audio_pkt->pts); } else if (recorder->stopped) { @@ -639,7 +656,7 @@ sc_recorder_audio_packet_sink_disable(struct sc_packet_sink *sink) { bool sc_recorder_init(struct sc_recorder *recorder, const char *filename, - enum sc_record_format format, bool audio, + enum sc_record_format format, bool video, bool audio, const struct sc_recorder_callbacks *cbs, void *cbs_userdata) { recorder->filename = strdup(filename); if (!recorder->filename) { @@ -662,6 +679,8 @@ sc_recorder_init(struct sc_recorder *recorder, const char *filename, goto error_queue_cond_destroy; } + assert(video || audio); + recorder->video = video; recorder->audio = audio; sc_vecdeque_init(&recorder->video_queue); @@ -680,13 +699,15 @@ sc_recorder_init(struct sc_recorder *recorder, const char *filename, recorder->cbs = cbs; recorder->cbs_userdata = cbs_userdata; - static const struct sc_packet_sink_ops video_ops = { - .open = sc_recorder_video_packet_sink_open, - .close = sc_recorder_video_packet_sink_close, - .push = sc_recorder_video_packet_sink_push, - }; + if (video) { + static const struct sc_packet_sink_ops video_ops = { + .open = sc_recorder_video_packet_sink_open, + .close = sc_recorder_video_packet_sink_close, + .push = sc_recorder_video_packet_sink_push, + }; - recorder->video_packet_sink.ops = &video_ops; + recorder->video_packet_sink.ops = &video_ops; + } if (audio) { static const struct sc_packet_sink_ops audio_ops = { diff --git a/app/src/recorder.h b/app/src/recorder.h index 41b8db65..9c6cd4f9 100644 --- a/app/src/recorder.h +++ b/app/src/recorder.h @@ -27,6 +27,7 @@ struct sc_recorder { * may access it without data races. */ bool audio; + bool video; char *filename; enum sc_record_format format; @@ -59,7 +60,7 @@ struct sc_recorder_callbacks { bool sc_recorder_init(struct sc_recorder *recorder, const char *filename, - enum sc_record_format format, bool audio, + enum sc_record_format format, bool video, bool audio, const struct sc_recorder_callbacks *cbs, void *cbs_userdata); bool diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 03b643f6..39a2f1a4 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -345,6 +345,7 @@ scrcpy(struct scrcpy_options *options) { .lock_video_orientation = options->lock_video_orientation, .control = options->control, .display_id = options->display_id, + .video = options->video, .audio = options->audio, .show_touches = options->show_touches, .stay_awake = options->stay_awake, @@ -389,7 +390,7 @@ scrcpy(struct scrcpy_options *options) { sdl_set_hints(options->render_driver); // Initialize SDL video and audio in addition if mirroring is enabled - if (SDL_Init(SDL_INIT_VIDEO)) { + if (options->video && SDL_Init(SDL_INIT_VIDEO)) { LOGE("Could not initialize SDL video: %s", SDL_GetError()); goto end; } @@ -436,11 +437,13 @@ scrcpy(struct scrcpy_options *options) { file_pusher_initialized = true; } - static const struct sc_demuxer_callbacks video_demuxer_cbs = { - .on_ended = sc_video_demuxer_on_ended, - }; - sc_demuxer_init(&s->video_demuxer, "video", s->server.video_socket, - &video_demuxer_cbs, NULL); + if (options->video) { + static const struct sc_demuxer_callbacks video_demuxer_cbs = { + .on_ended = sc_video_demuxer_on_ended, + }; + sc_demuxer_init(&s->video_demuxer, "video", s->server.video_socket, + &video_demuxer_cbs, NULL); + } if (options->audio) { static const struct sc_demuxer_callbacks audio_demuxer_cbs = { @@ -450,7 +453,7 @@ scrcpy(struct scrcpy_options *options) { &audio_demuxer_cbs, options); } - bool needs_video_decoder = options->mirror; + bool needs_video_decoder = options->mirror && options->video; bool needs_audio_decoder = options->mirror && options->audio; #ifdef HAVE_V4L2 needs_video_decoder |= !!options->v4l2_device; @@ -471,8 +474,8 @@ scrcpy(struct scrcpy_options *options) { .on_ended = sc_recorder_on_ended, }; if (!sc_recorder_init(&s->recorder, options->record_filename, - options->record_format, options->audio, - &recorder_cbs, NULL)) { + options->record_format, options->video, + options->audio, &recorder_cbs, NULL)) { goto end; } recorder_initialized = true; @@ -482,8 +485,10 @@ scrcpy(struct scrcpy_options *options) { } recorder_started = true; - sc_packet_source_add_sink(&s->video_demuxer.packet_source, - &s->recorder.video_packet_sink); + if (options->video) { + sc_packet_source_add_sink(&s->video_demuxer.packet_source, + &s->recorder.video_packet_sink); + } if (options->audio) { sc_packet_source_add_sink(&s->audio_demuxer.packet_source, &s->recorder.audio_packet_sink); @@ -671,11 +676,6 @@ aoa_hid_end: .start_fps_counter = options->start_fps_counter, }; - if (!sc_screen_init(&s->screen, &screen_params)) { - goto end; - } - screen_initialized = true; - struct sc_frame_source *src = &s->video_decoder.frame_source; if (options->display_buffer) { sc_delay_buffer_init(&s->display_buffer, options->display_buffer, @@ -684,7 +684,14 @@ aoa_hid_end: src = &s->display_buffer.frame_source; } - sc_frame_source_add_sink(src, &s->screen.frame_sink); + if (options->video) { + if (!sc_screen_init(&s->screen, &screen_params)) { + goto end; + } + screen_initialized = true; + + sc_frame_source_add_sink(src, &s->screen.frame_sink); + } if (options->audio) { sc_audio_player_init(&s->audio_player, options->audio_buffer, @@ -713,12 +720,15 @@ aoa_hid_end: } #endif - // now we consumed the header values, the socket receives the video stream - // start the video demuxer - if (!sc_demuxer_start(&s->video_demuxer)) { - goto end; + // Now that the header values have been consumed, the socket(s) will + // receive the stream(s). Start the demuxer(s). + + if (options->video) { + if (!sc_demuxer_start(&s->video_demuxer)) { + goto end; + } + video_demuxer_started = true; } - video_demuxer_started = true; if (options->audio) { if (!sc_demuxer_start(&s->audio_demuxer)) { diff --git a/app/src/server.c b/app/src/server.c index 2b35c085..9c554760 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -226,6 +226,9 @@ execute_server(struct sc_server *server, ADD_PARAM("scid=%08x", params->scid); ADD_PARAM("log_level=%s", log_level_to_server_string(params->log_level)); + if (!params->video) { + ADD_PARAM("video=false"); + } if (params->video_bit_rate) { ADD_PARAM("video_bit_rate=%" PRIu32, params->video_bit_rate); } @@ -464,6 +467,7 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) { const char *serial = server->serial; assert(serial); + bool video = server->params.video; bool audio = server->params.audio; bool control = server->params.control; @@ -471,9 +475,12 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) { sc_socket audio_socket = SC_SOCKET_NONE; sc_socket control_socket = SC_SOCKET_NONE; if (!tunnel->forward) { - video_socket = net_accept_intr(&server->intr, tunnel->server_socket); - if (video_socket == SC_SOCKET_NONE) { - goto fail; + if (video) { + video_socket = + net_accept_intr(&server->intr, tunnel->server_socket); + if (video_socket == SC_SOCKET_NONE) { + goto fail; + } } if (audio) { @@ -504,35 +511,45 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) { unsigned attempts = 100; sc_tick delay = SC_TICK_FROM_MS(100); - video_socket = connect_to_server(server, attempts, delay, tunnel_host, - tunnel_port); - if (video_socket == SC_SOCKET_NONE) { + sc_socket first_socket = connect_to_server(server, attempts, delay, + tunnel_host, tunnel_port); + if (first_socket == SC_SOCKET_NONE) { goto fail; } + if (video) { + video_socket = first_socket; + } + if (audio) { - audio_socket = net_socket(); - if (audio_socket == SC_SOCKET_NONE) { - goto fail; - } - bool ok = net_connect_intr(&server->intr, audio_socket, tunnel_host, - tunnel_port); - if (!ok) { - goto fail; + if (!video) { + audio_socket = first_socket; + } else { + audio_socket = net_socket(); + if (audio_socket == SC_SOCKET_NONE) { + goto fail; + } + bool ok = net_connect_intr(&server->intr, audio_socket, tunnel_host, + tunnel_port); + if (!ok) { + goto fail; + } } } if (control) { - // we know that the device is listening, we don't need several - // attempts - control_socket = net_socket(); - if (control_socket == SC_SOCKET_NONE) { - goto fail; - } - bool ok = net_connect_intr(&server->intr, control_socket, - tunnel_host, tunnel_port); - if (!ok) { - goto fail; + if (!video && !audio) { + control_socket = first_socket; + } else { + control_socket = net_socket(); + if (control_socket == SC_SOCKET_NONE) { + goto fail; + } + bool ok = net_connect_intr(&server->intr, control_socket, + tunnel_host, tunnel_port); + if (!ok) { + goto fail; + } } } } @@ -541,13 +558,17 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) { sc_adb_tunnel_close(tunnel, &server->intr, serial, server->device_socket_name); + sc_socket first_socket = video ? video_socket + : audio ? audio_socket + : control_socket; + // The sockets will be closed on stop if device_read_info() fails - bool ok = device_read_info(&server->intr, video_socket, info); + bool ok = device_read_info(&server->intr, first_socket, info); if (!ok) { goto fail; } - assert(video_socket != SC_SOCKET_NONE); + assert(!video || video_socket != SC_SOCKET_NONE); assert(!audio || audio_socket != SC_SOCKET_NONE); assert(!control || control_socket != SC_SOCKET_NONE); @@ -931,8 +952,11 @@ run_server(void *data) { sc_mutex_unlock(&server->mutex); // Interrupt sockets to wake up socket blocking calls on the server - assert(server->video_socket != SC_SOCKET_NONE); - net_interrupt(server->video_socket); + + if (server->video_socket != SC_SOCKET_NONE) { + // There is no video_socket if --no-video is set + net_interrupt(server->video_socket); + } if (server->audio_socket != SC_SOCKET_NONE) { // There is no audio_socket if --no-audio is set diff --git a/app/src/server.h b/app/src/server.h index c425856b..31648445 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -41,6 +41,7 @@ struct sc_server_params { int8_t lock_video_orientation; bool control; uint32_t display_id; + bool video; bool audio; bool show_touches; bool stay_awake; diff --git a/doc/audio.md b/doc/audio.md index 6e97b103..08868eaf 100644 --- a/doc/audio.md +++ b/doc/audio.md @@ -24,6 +24,21 @@ To disable audio: scrcpy --no-audio ``` +## Audio only + +To play audio only, disable the video: + +``` +scrcpy --no-video +``` + +Without video, the audio latency is typically not criticial, so it might be +interesting to add [buffering](#buffering) to minimize glitches: + +``` +scrcpy --no-video --audio-buffer=200 +``` + ## Codec The audio codec can be selected. The possible values are `opus` (default), `aac` diff --git a/doc/recording.md b/doc/recording.md index 9455a6fc..e3e2cf68 100644 --- a/doc/recording.md +++ b/doc/recording.md @@ -13,7 +13,11 @@ To record only the video: scrcpy --no-audio --record=file.mp4 ``` -_It is currently not possible to record only the audio._ +To record only the audio: + +```bash +scrcpy --no-video --record=file.mp4 +``` To disable mirroring while recording: diff --git a/doc/video.md b/doc/video.md index 58aa5022..303d9048 100644 --- a/doc/video.md +++ b/doc/video.md @@ -170,6 +170,16 @@ scrcpy --v4l2-sink=/dev/video2 --no-mirror scrcpy --record=file.mkv --no-mirror ``` + +## No video + +To disable video forwarding completely, so that only audio is forwarded: + +``` +scrcpy --no-video +``` + + ## Video4Linux See the dedicated [Video4Linux](v4l2.md) page. diff --git a/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java b/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java index 4bfff726..20ab1f9c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java +++ b/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java @@ -41,7 +41,7 @@ public final class DesktopConnection implements Closeable { controlInputStream = null; controlOutputStream = null; } - videoFd = videoSocket.getFileDescriptor(); + videoFd = videoSocket != null ? videoSocket.getFileDescriptor() : null; audioFd = audioSocket != null ? audioSocket.getFileDescriptor() : null; } @@ -60,29 +60,43 @@ public final class DesktopConnection implements Closeable { return SOCKET_NAME_PREFIX + String.format("_%08x", scid); } - public static DesktopConnection open(int scid, boolean tunnelForward, boolean audio, boolean control, boolean sendDummyByte) throws IOException { + public static DesktopConnection open(int scid, boolean tunnelForward, boolean video, boolean audio, boolean control, boolean sendDummyByte) + throws IOException { String socketName = getSocketName(scid); + LocalSocket firstSocket = null; + LocalSocket videoSocket = null; LocalSocket audioSocket = null; LocalSocket controlSocket = null; try { if (tunnelForward) { try (LocalServerSocket localServerSocket = new LocalServerSocket(socketName)) { - videoSocket = localServerSocket.accept(); - if (sendDummyByte) { - // send one byte so the client may read() to detect a connection error - videoSocket.getOutputStream().write(0); + if (video) { + videoSocket = localServerSocket.accept(); + firstSocket = videoSocket; } if (audio) { audioSocket = localServerSocket.accept(); + if (firstSocket == null) { + firstSocket = audioSocket; + } } if (control) { controlSocket = localServerSocket.accept(); + if (firstSocket == null) { + firstSocket = controlSocket; + } + } + if (sendDummyByte) { + // send one byte so the client may read() to detect a connection error + firstSocket.getOutputStream().write(0); } } } else { - videoSocket = connect(socketName); + if (video) { + videoSocket = connect(socketName); + } if (audio) { audioSocket = connect(socketName); } @@ -106,10 +120,22 @@ public final class DesktopConnection implements Closeable { return new DesktopConnection(videoSocket, audioSocket, controlSocket); } + private LocalSocket getFirstSocket() { + if (videoSocket != null) { + return videoSocket; + } + if (audioSocket != null) { + return audioSocket; + } + return controlSocket; + } + public void close() throws IOException { - videoSocket.shutdownInput(); - videoSocket.shutdownOutput(); - videoSocket.close(); + if (videoSocket != null) { + videoSocket.shutdownInput(); + videoSocket.shutdownOutput(); + videoSocket.close(); + } if (audioSocket != null) { audioSocket.shutdownInput(); audioSocket.shutdownOutput(); @@ -130,7 +156,8 @@ public final class DesktopConnection implements Closeable { System.arraycopy(deviceNameBytes, 0, buffer, 0, len); // byte[] are always 0-initialized in java, no need to set '\0' explicitly - IO.writeFully(videoFd, buffer, 0, buffer.length); + FileDescriptor fd = getFirstSocket().getFileDescriptor(); + IO.writeFully(fd, buffer, 0, buffer.length); } public FileDescriptor getVideoFd() { diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index a34eb9b5..7bd94cb3 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -9,6 +9,7 @@ public class Options { private Ln.Level logLevel = Ln.Level.DEBUG; private int scid = -1; // 31-bit non-negative value, or -1 + private boolean video = true; private boolean audio = true; private int maxSize; private VideoCodec videoCodec = VideoCodec.H264; @@ -51,6 +52,10 @@ public class Options { return scid; } + public boolean getVideo() { + return video; + } + public boolean getAudio() { return audio; } @@ -200,6 +205,9 @@ public class Options { case "log_level": options.logLevel = Ln.Level.valueOf(value.toUpperCase(Locale.ENGLISH)); break; + case "video": + options.video = Boolean.parseBoolean(value); + break; case "audio": options.audio = Boolean.parseBoolean(value); break; diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 4d72d1e8..db993830 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -95,6 +95,7 @@ public final class Server { int scid = options.getScid(); boolean tunnelForward = options.isTunnelForward(); boolean control = options.getControl(); + boolean video = options.getVideo(); boolean audio = options.getAudio(); boolean sendDummyByte = options.getSendDummyByte(); @@ -121,7 +122,7 @@ public final class Server { List asyncProcessors = new ArrayList<>(); - DesktopConnection connection = DesktopConnection.open(scid, tunnelForward, audio, control, sendDummyByte); + DesktopConnection connection = DesktopConnection.open(scid, tunnelForward, video, audio, control, sendDummyByte); try { if (options.getSendDeviceMeta()) { connection.sendDeviceMeta(Device.getDeviceName()); @@ -147,11 +148,13 @@ public final class Server { asyncProcessors.add(audioRecorder); } - Streamer videoStreamer = new Streamer(connection.getVideoFd(), options.getVideoCodec(), options.getSendCodecMeta(), - options.getSendFrameMeta()); - ScreenEncoder screenEncoder = new ScreenEncoder(device, videoStreamer, options.getVideoBitRate(), options.getMaxFps(), - options.getVideoCodecOptions(), options.getVideoEncoder(), options.getDownsizeOnError()); - asyncProcessors.add(screenEncoder); + if (video) { + Streamer videoStreamer = new Streamer(connection.getVideoFd(), options.getVideoCodec(), options.getSendCodecMeta(), + options.getSendFrameMeta()); + ScreenEncoder screenEncoder = new ScreenEncoder(device, videoStreamer, options.getVideoBitRate(), options.getMaxFps(), + options.getVideoCodecOptions(), options.getVideoEncoder(), options.getDownsizeOnError()); + asyncProcessors.add(screenEncoder); + } Completion completion = new Completion(asyncProcessors.size()); for (AsyncProcessor asyncProcessor : asyncProcessors) { From be86e14e051a40c62837f690282f2214f467778f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 7 May 2023 12:18:27 +0200 Subject: [PATCH 1545/2244] Factorize record format parsing Convert either the filename extension or the explicit record format to a sc_record_format using the same function. PR #3978 --- app/src/cli.c | 42 +++++++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index 1558ef0c..619f372e 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -1479,18 +1479,27 @@ sc_parse_shortcut_mods(const char *s, struct sc_shortcut_mods *mods) { } #endif +static enum sc_record_format +get_record_format(const char *name) { + if (!strcmp(name, "mp4")) { + return SC_RECORD_FORMAT_MP4; + } + if (!strcmp(name, "mkv")) { + return SC_RECORD_FORMAT_MKV; + } + return 0; +} + static bool parse_record_format(const char *optarg, enum sc_record_format *format) { - if (!strcmp(optarg, "mp4")) { - *format = SC_RECORD_FORMAT_MP4; - return true; + enum sc_record_format fmt = get_record_format(optarg); + if (!fmt) { + LOGE("Unsupported format: %s (expected mp4 or mkv)", optarg); + return false; } - if (!strcmp(optarg, "mkv")) { - *format = SC_RECORD_FORMAT_MKV; - return true; - } - LOGE("Unsupported format: %s (expected mp4 or mkv)", optarg); - return false; + + *format = fmt; + return true; } static bool @@ -1510,18 +1519,13 @@ parse_port(const char *optarg, uint16_t *port) { static enum sc_record_format guess_record_format(const char *filename) { - size_t len = strlen(filename); - if (len < 4) { + const char *dot = strrchr(filename, '.'); + if (!dot) { return 0; } - const char *ext = &filename[len - 4]; - if (!strcmp(ext, ".mp4")) { - return SC_RECORD_FORMAT_MP4; - } - if (!strcmp(ext, ".mkv")) { - return SC_RECORD_FORMAT_MKV; - } - return 0; + + const char *ext = dot + 1; + return get_record_format(ext); } static bool From 98f4f4e68a21dd072d83b1e474d71e6f28d9eb91 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 7 May 2023 12:22:01 +0200 Subject: [PATCH 1546/2244] Refactor command line checks Several checks are performed when opts->record_filename is not NULL. Group them in a single block. PR #3978 --- app/src/cli.c | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index 619f372e..4b895cd2 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -1959,21 +1959,23 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; } - if (opts->record_filename && !opts->record_format) { - opts->record_format = guess_record_format(opts->record_filename); + if (opts->record_filename) { if (!opts->record_format) { - LOGE("No format specified for \"%s\" " - "(try with --record-format=mkv)", - opts->record_filename); + opts->record_format = guess_record_format(opts->record_filename); + if (!opts->record_format) { + LOGE("No format specified for \"%s\" " + "(try with --record-format=mkv)", + opts->record_filename); + return false; + } + } + + if (opts->audio_codec == SC_CODEC_RAW) { + LOGW("Recording does not support RAW audio codec"); return false; } } - if (opts->record_filename && opts->audio_codec == SC_CODEC_RAW) { - LOGW("Recording does not support RAW audio codec"); - return false; - } - if (opts->audio_codec == SC_CODEC_RAW) { if (opts->audio_bit_rate) { LOGW("--audio-bit-rate is ignored for raw audio codec"); From d6bcde565f8155b6a51ce4682ada367fe62d5a18 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 7 May 2023 12:23:51 +0200 Subject: [PATCH 1547/2244] Accept .m4a and .mka These are just aliases for mp4 and mkv when there is no video stream. PR #3978 --- app/src/cli.c | 12 ++++++++++++ app/src/options.h | 8 ++++++++ app/src/recorder.c | 11 ++++++++--- 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index 4b895cd2..40fe242e 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -1487,6 +1487,12 @@ get_record_format(const char *name) { if (!strcmp(name, "mkv")) { return SC_RECORD_FORMAT_MKV; } + if (!strcmp(name, "m4a")) { + return SC_RECORD_FORMAT_M4A; + } + if (!strcmp(name, "mka")) { + return SC_RECORD_FORMAT_MKA; + } return 0; } @@ -1974,6 +1980,12 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], LOGW("Recording does not support RAW audio codec"); return false; } + + if (opts->video + && sc_record_format_is_audio_only(opts->record_format)) { + LOGE("Audio container does not support video stream"); + return false; + } } if (opts->audio_codec == SC_CODEC_RAW) { diff --git a/app/src/options.h b/app/src/options.h index 4edf3f32..2424638f 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -21,8 +21,16 @@ enum sc_record_format { SC_RECORD_FORMAT_AUTO, SC_RECORD_FORMAT_MP4, SC_RECORD_FORMAT_MKV, + SC_RECORD_FORMAT_M4A, + SC_RECORD_FORMAT_MKA, }; +static inline bool +sc_record_format_is_audio_only(enum sc_record_format fmt) { + return fmt == SC_RECORD_FORMAT_M4A + || fmt == SC_RECORD_FORMAT_MKA; +} + enum sc_codec { SC_CODEC_H264, SC_CODEC_H265, diff --git a/app/src/recorder.c b/app/src/recorder.c index 5cbe6873..10102ec4 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -60,9 +60,14 @@ sc_recorder_queue_clear(struct sc_recorder_queue *queue) { static const char * sc_recorder_get_format_name(enum sc_record_format format) { switch (format) { - case SC_RECORD_FORMAT_MP4: return "mp4"; - case SC_RECORD_FORMAT_MKV: return "matroska"; - default: return NULL; + case SC_RECORD_FORMAT_MP4: + case SC_RECORD_FORMAT_M4A: + return "mp4"; + case SC_RECORD_FORMAT_MKV: + case SC_RECORD_FORMAT_MKA: + return "matroska"; + default: + return NULL; } } From 7321db6f28914df81e654cc4e2fbb3deec15fe2c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 7 May 2023 12:35:27 +0200 Subject: [PATCH 1548/2244] Add recording to opus file Use the FFmpeg opus muxer to record an opus file. PR #3978 --- app/src/cli.c | 10 ++++++++++ app/src/options.h | 4 +++- app/src/recorder.c | 2 ++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/app/src/cli.c b/app/src/cli.c index 40fe242e..ee0319ee 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -1493,6 +1493,9 @@ get_record_format(const char *name) { if (!strcmp(name, "mka")) { return SC_RECORD_FORMAT_MKA; } + if (!strcmp(name, "opus")) { + return SC_RECORD_FORMAT_OPUS; + } return 0; } @@ -1986,6 +1989,13 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], LOGE("Audio container does not support video stream"); return false; } + + if (opts->record_format == SC_RECORD_FORMAT_OPUS + && opts->audio_codec != SC_CODEC_OPUS) { + LOGE("Recording to OPUS file requires an OPUS audio stream " + "(try with --audio-codec=opus)"); + return false; + } } if (opts->audio_codec == SC_CODEC_RAW) { diff --git a/app/src/options.h b/app/src/options.h index 2424638f..0547b4ec 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -23,12 +23,14 @@ enum sc_record_format { SC_RECORD_FORMAT_MKV, SC_RECORD_FORMAT_M4A, SC_RECORD_FORMAT_MKA, + SC_RECORD_FORMAT_OPUS, }; static inline bool sc_record_format_is_audio_only(enum sc_record_format fmt) { return fmt == SC_RECORD_FORMAT_M4A - || fmt == SC_RECORD_FORMAT_MKA; + || fmt == SC_RECORD_FORMAT_MKA + || fmt == SC_RECORD_FORMAT_OPUS; } enum sc_codec { diff --git a/app/src/recorder.c b/app/src/recorder.c index 10102ec4..b0c41df0 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -66,6 +66,8 @@ sc_recorder_get_format_name(enum sc_record_format format) { case SC_RECORD_FORMAT_MKV: case SC_RECORD_FORMAT_MKA: return "matroska"; + case SC_RECORD_FORMAT_OPUS: + return "opus"; default: return NULL; } From b11b363e8ec1666927ca4931e24520daa8ef7ef3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 7 May 2023 12:38:32 +0200 Subject: [PATCH 1549/2244] Add recording to aac file It is just an alias for mp4. PR #3978 --- app/src/cli.c | 10 ++++++++++ app/src/options.h | 4 +++- app/src/recorder.c | 1 + 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/app/src/cli.c b/app/src/cli.c index ee0319ee..e5b18277 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -1496,6 +1496,9 @@ get_record_format(const char *name) { if (!strcmp(name, "opus")) { return SC_RECORD_FORMAT_OPUS; } + if (!strcmp(name, "aac")) { + return SC_RECORD_FORMAT_AAC; + } return 0; } @@ -1996,6 +1999,13 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], "(try with --audio-codec=opus)"); return false; } + + if (opts->record_format == SC_RECORD_FORMAT_AAC + && opts->audio_codec != SC_CODEC_AAC) { + LOGE("Recording to AAC file requires an AAC audio stream " + "(try with --audio-codec=aac)"); + return false; + } } if (opts->audio_codec == SC_CODEC_RAW) { diff --git a/app/src/options.h b/app/src/options.h index 0547b4ec..7c442149 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -24,13 +24,15 @@ enum sc_record_format { SC_RECORD_FORMAT_M4A, SC_RECORD_FORMAT_MKA, SC_RECORD_FORMAT_OPUS, + SC_RECORD_FORMAT_AAC, }; static inline bool sc_record_format_is_audio_only(enum sc_record_format fmt) { return fmt == SC_RECORD_FORMAT_M4A || fmt == SC_RECORD_FORMAT_MKA - || fmt == SC_RECORD_FORMAT_OPUS; + || fmt == SC_RECORD_FORMAT_OPUS + || fmt == SC_RECORD_FORMAT_AAC; } enum sc_codec { diff --git a/app/src/recorder.c b/app/src/recorder.c index b0c41df0..be1cbe71 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -62,6 +62,7 @@ sc_recorder_get_format_name(enum sc_record_format format) { switch (format) { case SC_RECORD_FORMAT_MP4: case SC_RECORD_FORMAT_M4A: + case SC_RECORD_FORMAT_AAC: return "mp4"; case SC_RECORD_FORMAT_MKV: case SC_RECORD_FORMAT_MKA: From a166eee909a66385dc5b00169f4f61b490e163dd Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 7 May 2023 12:46:44 +0200 Subject: [PATCH 1550/2244] Upgrade FFmpeg build to 6.0-scrcpy-3 Use a build which includes the opus muxer, to support recording to .opus files. Refs PR #3978 --- app/prebuilt-deps/prepare-ffmpeg.sh | 4 ++-- cross_win32.txt | 2 +- cross_win64.txt | 2 +- release.mk | 20 ++++++++++---------- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/app/prebuilt-deps/prepare-ffmpeg.sh b/app/prebuilt-deps/prepare-ffmpeg.sh index b156099a..3533ded4 100755 --- a/app/prebuilt-deps/prepare-ffmpeg.sh +++ b/app/prebuilt-deps/prepare-ffmpeg.sh @@ -6,11 +6,11 @@ cd "$DIR" mkdir -p "$PREBUILT_DATA_DIR" cd "$PREBUILT_DATA_DIR" -VERSION=6.0-scrcpy-2 +VERSION=6.0-scrcpy-3 DEP_DIR="ffmpeg-$VERSION" FILENAME="$DEP_DIR".7z -SHA256SUM=98ef97f8607c97a5c4f9c5a0a991b78f105d002a3619145011d16ffb92501b14 +SHA256SUM=36829d98ac4454d7092c72ddb92faa20b60450bc0fe8873076efb0858cdcbc2c if [[ -d "$DEP_DIR" ]] then diff --git a/cross_win32.txt b/cross_win32.txt index 18834af4..e8e8c7e8 100644 --- a/cross_win32.txt +++ b/cross_win32.txt @@ -16,6 +16,6 @@ cpu = 'i686' endian = 'little' [properties] -prebuilt_ffmpeg = 'ffmpeg-6.0-scrcpy-2/win32' +prebuilt_ffmpeg = 'ffmpeg-6.0-scrcpy-3/win32' prebuilt_sdl2 = 'SDL2-2.26.4/i686-w64-mingw32' prebuilt_libusb = 'libusb-1.0.26/libusb-MinGW-Win32' diff --git a/cross_win64.txt b/cross_win64.txt index 1c7c0875..d1de3c21 100644 --- a/cross_win64.txt +++ b/cross_win64.txt @@ -16,6 +16,6 @@ cpu = 'x86_64' endian = 'little' [properties] -prebuilt_ffmpeg = 'ffmpeg-6.0-scrcpy-2/win64' +prebuilt_ffmpeg = 'ffmpeg-6.0-scrcpy-3/win64' prebuilt_sdl2 = 'SDL2-2.26.4/x86_64-w64-mingw32' prebuilt_libusb = 'libusb-1.0.26/libusb-MinGW-x64' diff --git a/release.mk b/release.mk index 0f5cbe24..9fbd67c6 100644 --- a/release.mk +++ b/release.mk @@ -94,11 +94,11 @@ dist-win32: build-server build-win32 cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN32_TARGET_DIR)" cp app/data/icon.png "$(DIST)/$(WIN32_TARGET_DIR)" cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN32_TARGET_DIR)" - cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win32/bin/avutil-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win32/bin/avcodec-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win32/bin/avformat-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win32/bin/swresample-4.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win32/bin/zlib1.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-3/win32/bin/avutil-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-3/win32/bin/avcodec-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-3/win32/bin/avformat-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-3/win32/bin/swresample-4.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-3/win32/bin/zlib1.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-34.0.1/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-34.0.1/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-34.0.1/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" @@ -113,11 +113,11 @@ dist-win64: build-server build-win64 cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN64_TARGET_DIR)" cp app/data/icon.png "$(DIST)/$(WIN64_TARGET_DIR)" cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN64_TARGET_DIR)" - cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win64/bin/avutil-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win64/bin/avcodec-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win64/bin/avformat-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win64/bin/swresample-4.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win64/bin/zlib1.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-3/win64/bin/avutil-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-3/win64/bin/avcodec-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-3/win64/bin/avformat-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-3/win64/bin/swresample-4.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-3/win64/bin/zlib1.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-34.0.1/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-34.0.1/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-34.0.1/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" From d50055021284bf0da49a5ff7810b48fe0f7c492d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 7 May 2023 13:04:11 +0200 Subject: [PATCH 1551/2244] Update audio recording documentation Document how to record audio-only to .opus and .aac files. PR #3978 --- doc/recording.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/recording.md b/doc/recording.md index e3e2cf68..7d66f2ff 100644 --- a/doc/recording.md +++ b/doc/recording.md @@ -16,7 +16,9 @@ scrcpy --no-audio --record=file.mp4 To record only the audio: ```bash -scrcpy --no-video --record=file.mp4 +scrcpy --no-video --record=file.opus +scrcpy --no-video --audio-codec=aac --record-file=file.aac +# .m4a/.mp4 and .mka/.mkv are also supported for both opus and aac ``` To disable mirroring while recording: From 7d33798b40bf9c371d417c4911d67141d62365ec Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 15 May 2023 14:27:23 +0200 Subject: [PATCH 1552/2244] Upgrade FFmpeg build to 6.0-scrcpy-4 Use FFmpeg DLLs which do not depend on zlib1.dll. --- app/prebuilt-deps/prepare-ffmpeg.sh | 4 ++-- cross_win32.txt | 2 +- cross_win64.txt | 2 +- release.mk | 18 ++++++++---------- 4 files changed, 12 insertions(+), 14 deletions(-) diff --git a/app/prebuilt-deps/prepare-ffmpeg.sh b/app/prebuilt-deps/prepare-ffmpeg.sh index 3533ded4..9019cc2d 100755 --- a/app/prebuilt-deps/prepare-ffmpeg.sh +++ b/app/prebuilt-deps/prepare-ffmpeg.sh @@ -6,11 +6,11 @@ cd "$DIR" mkdir -p "$PREBUILT_DATA_DIR" cd "$PREBUILT_DATA_DIR" -VERSION=6.0-scrcpy-3 +VERSION=6.0-scrcpy-4 DEP_DIR="ffmpeg-$VERSION" FILENAME="$DEP_DIR".7z -SHA256SUM=36829d98ac4454d7092c72ddb92faa20b60450bc0fe8873076efb0858cdcbc2c +SHA256SUM=39274b321491ce83e76cab5d24e7cbe3f402d3ccf382f739b13be5651c146b60 if [[ -d "$DEP_DIR" ]] then diff --git a/cross_win32.txt b/cross_win32.txt index e8e8c7e8..f3fded40 100644 --- a/cross_win32.txt +++ b/cross_win32.txt @@ -16,6 +16,6 @@ cpu = 'i686' endian = 'little' [properties] -prebuilt_ffmpeg = 'ffmpeg-6.0-scrcpy-3/win32' +prebuilt_ffmpeg = 'ffmpeg-6.0-scrcpy-4/win32' prebuilt_sdl2 = 'SDL2-2.26.4/i686-w64-mingw32' prebuilt_libusb = 'libusb-1.0.26/libusb-MinGW-Win32' diff --git a/cross_win64.txt b/cross_win64.txt index d1de3c21..1b02b93f 100644 --- a/cross_win64.txt +++ b/cross_win64.txt @@ -16,6 +16,6 @@ cpu = 'x86_64' endian = 'little' [properties] -prebuilt_ffmpeg = 'ffmpeg-6.0-scrcpy-3/win64' +prebuilt_ffmpeg = 'ffmpeg-6.0-scrcpy-4/win64' prebuilt_sdl2 = 'SDL2-2.26.4/x86_64-w64-mingw32' prebuilt_libusb = 'libusb-1.0.26/libusb-MinGW-x64' diff --git a/release.mk b/release.mk index 9fbd67c6..e41a45ad 100644 --- a/release.mk +++ b/release.mk @@ -94,11 +94,10 @@ dist-win32: build-server build-win32 cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN32_TARGET_DIR)" cp app/data/icon.png "$(DIST)/$(WIN32_TARGET_DIR)" cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN32_TARGET_DIR)" - cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-3/win32/bin/avutil-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-3/win32/bin/avcodec-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-3/win32/bin/avformat-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-3/win32/bin/swresample-4.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-3/win32/bin/zlib1.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-4/win32/bin/avutil-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-4/win32/bin/avcodec-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-4/win32/bin/avformat-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-4/win32/bin/swresample-4.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-34.0.1/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-34.0.1/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-34.0.1/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" @@ -113,11 +112,10 @@ dist-win64: build-server build-win64 cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN64_TARGET_DIR)" cp app/data/icon.png "$(DIST)/$(WIN64_TARGET_DIR)" cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN64_TARGET_DIR)" - cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-3/win64/bin/avutil-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-3/win64/bin/avcodec-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-3/win64/bin/avformat-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-3/win64/bin/swresample-4.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-3/win64/bin/zlib1.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-4/win64/bin/avutil-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-4/win64/bin/avcodec-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-4/win64/bin/avformat-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-4/win64/bin/swresample-4.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-34.0.1/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-34.0.1/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-34.0.1/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" From 958f22490b3ab86677bd8b6126ddaa4de4082798 Mon Sep 17 00:00:00 2001 From: Marek Madejski Date: Tue, 16 May 2023 12:53:58 +0200 Subject: [PATCH 1553/2244] Document installation via winget on Windows PR #4005 Refs #1444 Refs #3932 Signed-off-by: Romain Vimont --- doc/windows.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/doc/windows.md b/doc/windows.md index 521ad45e..2cbd99b6 100644 --- a/doc/windows.md +++ b/doc/windows.md @@ -15,7 +15,13 @@ Download the [latest release]: and extract it. -Alternatively, you could install it from packages manager, like [Chocolatey]: +Alternatively, you could install it from packages manager, like [Winget]: + +```bash +winget install scrcpy +``` + +or [Chocolatey]: ```bash choco install scrcpy @@ -30,6 +36,7 @@ scoop install scrcpy scoop install adb # if you don't have it yet ``` +[Winget]: https://github.com/microsoft/winget-cli [Chocolatey]: https://chocolatey.org/ [Scoop]: https://scoop.sh From 6298ef095ffa9a2cdd2b4d245e71280743b5a59e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 8 May 2023 18:16:38 +0200 Subject: [PATCH 1554/2244] Accept texture failures When the scrcpy window is minimized on Windows with D3D9, texture creation and update fail. In that case, do not terminate scrcpy. Instead, store the pending size or frame to update, to attempt again during the next update or rendering. Fixes #3947 --- app/src/display.c | 120 ++++++++++++++++++++++++++++++++++++++++++---- app/src/display.h | 20 ++++++-- app/src/screen.c | 28 +++++++---- 3 files changed, 147 insertions(+), 21 deletions(-) diff --git a/app/src/display.c b/app/src/display.c index 4852952b..dabc2cb7 100644 --- a/app/src/display.c +++ b/app/src/display.c @@ -62,11 +62,17 @@ sc_display_init(struct sc_display *display, SDL_Window *window, bool mipmaps) { LOGD("Trilinear filtering disabled (not an OpenGL renderer"); } + display->pending.flags = 0; + display->pending.frame = NULL; + return true; } void sc_display_destroy(struct sc_display *display) { + if (display->pending.frame) { + av_frame_free(&display->pending.frame); + } #ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE SDL_GL_DeleteContext(display->gl_context); #endif @@ -84,7 +90,7 @@ sc_display_create_texture(struct sc_display *display, SDL_TEXTUREACCESS_STREAMING, size.width, size.height); if (!texture) { - LOGE("Could not create texture: %s", SDL_GetError()); + LOGD("Could not create texture: %s", SDL_GetError()); return NULL; } @@ -104,8 +110,66 @@ sc_display_create_texture(struct sc_display *display, return texture; } -bool -sc_display_set_texture_size(struct sc_display *display, struct sc_size size) { +static inline void +sc_display_set_pending_size(struct sc_display *display, struct sc_size size) { + assert(!display->texture); + display->pending.size = size; + display->pending.flags |= SC_DISPLAY_PENDING_FLAG_SIZE; +} + +static bool +sc_display_set_pending_frame(struct sc_display *display, const AVFrame *frame) { + if (!display->pending.frame) { + display->pending.frame = av_frame_alloc(); + if (!display->pending.frame) { + LOG_OOM(); + return false; + } + } + + int r = av_frame_ref(display->pending.frame, frame); + if (r) { + LOGE("Could not ref frame: %d", r); + return false; + } + + display->pending.flags |= SC_DISPLAY_PENDING_FLAG_FRAME; + + return true; +} + +static bool +sc_display_apply_pending(struct sc_display *display) { + if (display->pending.flags & SC_DISPLAY_PENDING_FLAG_SIZE) { + assert(!display->texture); + display->texture = + sc_display_create_texture(display, display->pending.size); + if (!display->texture) { + return false; + } + + display->pending.flags &= ~SC_DISPLAY_PENDING_FLAG_SIZE; + } + + if (display->pending.flags & SC_DISPLAY_PENDING_FLAG_FRAME) { + assert(display->pending.frame); + bool ok = sc_display_update_texture(display, display->pending.frame); + if (!ok) { + return false; + } + + av_frame_unref(display->pending.frame); + display->pending.flags &= ~SC_DISPLAY_PENDING_FLAG_FRAME; + } + + return true; +} + +static bool +sc_display_set_texture_size_internal(struct sc_display *display, + struct sc_size size) { + assert(size.width && size.height); + if (display->texture) { SDL_DestroyTexture(display->texture); } @@ -119,14 +183,27 @@ sc_display_set_texture_size(struct sc_display *display, struct sc_size size) { return true; } -bool -sc_display_update_texture(struct sc_display *display, const AVFrame *frame) { +enum sc_display_result +sc_display_set_texture_size(struct sc_display *display, struct sc_size size) { + bool ok = sc_display_set_texture_size_internal(display, size); + if (!ok) { + sc_display_set_pending_size(display, size); + return SC_DISPLAY_RESULT_PENDING; + + } + + return SC_DISPLAY_RESULT_OK; +} + +static bool +sc_display_update_texture_internal(struct sc_display *display, + const AVFrame *frame) { int ret = SDL_UpdateYUVTexture(display->texture, NULL, frame->data[0], frame->linesize[0], frame->data[1], frame->linesize[1], frame->data[2], frame->linesize[2]); if (ret) { - LOGE("Could not update texture: %s", SDL_GetError()); + LOGD("Could not update texture: %s", SDL_GetError()); return false; } @@ -139,11 +216,34 @@ sc_display_update_texture(struct sc_display *display, const AVFrame *frame) { return true; } -bool +enum sc_display_result +sc_display_update_texture(struct sc_display *display, const AVFrame *frame) { + bool ok = sc_display_update_texture_internal(display, frame); + if (!ok) { + ok = sc_display_set_pending_frame(display, frame); + if (!ok) { + LOGE("Could not set pending frame"); + return SC_DISPLAY_RESULT_ERROR; + } + + return SC_DISPLAY_RESULT_PENDING; + } + + return SC_DISPLAY_RESULT_OK; +} + +enum sc_display_result sc_display_render(struct sc_display *display, const SDL_Rect *geometry, unsigned rotation) { SDL_RenderClear(display->renderer); + if (display->pending.flags) { + bool ok = sc_display_apply_pending(display); + if (!ok) { + return SC_DISPLAY_RESULT_PENDING; + } + } + SDL_Renderer *renderer = display->renderer; SDL_Texture *texture = display->texture; @@ -151,7 +251,7 @@ sc_display_render(struct sc_display *display, const SDL_Rect *geometry, int ret = SDL_RenderCopy(renderer, texture, NULL, geometry); if (ret) { LOGE("Could not render texture: %s", SDL_GetError()); - return false; + return SC_DISPLAY_RESULT_ERROR; } } else { // rotation in RenderCopyEx() is clockwise, while screen->rotation is @@ -176,10 +276,10 @@ sc_display_render(struct sc_display *display, const SDL_Rect *geometry, NULL, 0); if (ret) { LOGE("Could not render texture: %s", SDL_GetError()); - return false; + return SC_DISPLAY_RESULT_ERROR; } } SDL_RenderPresent(display->renderer); - return true; + return SC_DISPLAY_RESULT_OK; } diff --git a/app/src/display.h b/app/src/display.h index e30b4822..6b83a5c9 100644 --- a/app/src/display.h +++ b/app/src/display.h @@ -24,6 +24,20 @@ struct sc_display { #endif bool mipmaps; + + struct { +#define SC_DISPLAY_PENDING_FLAG_SIZE 1 +#define SC_DISPLAY_PENDING_FLAG_FRAME 2 + int8_t flags; + struct sc_size size; + AVFrame *frame; + } pending; +}; + +enum sc_display_result { + SC_DISPLAY_RESULT_OK, + SC_DISPLAY_RESULT_PENDING, + SC_DISPLAY_RESULT_ERROR, }; bool @@ -32,13 +46,13 @@ sc_display_init(struct sc_display *display, SDL_Window *window, bool mipmaps); void sc_display_destroy(struct sc_display *display); -bool +enum sc_display_result sc_display_set_texture_size(struct sc_display *display, struct sc_size size); -bool +enum sc_display_result sc_display_update_texture(struct sc_display *display, const AVFrame *frame); -bool +enum sc_display_result sc_display_render(struct sc_display *display, const SDL_Rect *geometry, unsigned rotation); diff --git a/app/src/screen.c b/app/src/screen.c index 70665ed6..14fd2a00 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -249,9 +249,9 @@ sc_screen_render(struct sc_screen *screen, bool update_content_rect) { sc_screen_update_content_rect(screen); } - bool ok = sc_display_render(&screen->display, &screen->rect, - screen->rotation); - (void) ok; // error already logged + enum sc_display_result res = + sc_display_render(&screen->display, &screen->rect, screen->rotation); + (void) res; // any error already logged } #if defined(__APPLE__) || defined(__WINDOWS__) @@ -583,15 +583,17 @@ sc_screen_init_size(struct sc_screen *screen) { get_rotated_size(screen->frame_size, screen->rotation); screen->content_size = content_size; - return sc_display_set_texture_size(&screen->display, screen->frame_size); + enum sc_display_result res = + sc_display_set_texture_size(&screen->display, screen->frame_size); + return res != SC_DISPLAY_RESULT_ERROR; } // recreate the texture and resize the window if the frame size has changed -static bool +static enum sc_display_result prepare_for_frame(struct sc_screen *screen, struct sc_size new_frame_size) { if (screen->frame_size.width == new_frame_size.width && screen->frame_size.height == new_frame_size.height) { - return true; + return SC_DISPLAY_RESULT_OK; } // frame dimension changed @@ -615,13 +617,23 @@ sc_screen_update_frame(struct sc_screen *screen) { sc_fps_counter_add_rendered_frame(&screen->fps_counter); struct sc_size new_frame_size = {frame->width, frame->height}; - if (!prepare_for_frame(screen, new_frame_size)) { + enum sc_display_result res = prepare_for_frame(screen, new_frame_size); + if (res == SC_DISPLAY_RESULT_ERROR) { return false; } + if (res == SC_DISPLAY_RESULT_PENDING) { + // Not an error, but do not continue + return true; + } - if (!sc_display_update_texture(&screen->display, frame)) { + res = sc_display_update_texture(&screen->display, frame); + if (res == SC_DISPLAY_RESULT_ERROR) { return false; } + if (res == SC_DISPLAY_RESULT_PENDING) { + // Not an error, but do not continue + return true; + } if (!screen->has_frame) { screen->has_frame = true; From e926bf1fe8546dc3df129d1ecab555539af5c19e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 8 May 2023 21:02:01 +0200 Subject: [PATCH 1555/2244] Delay window resize when minimized On some window managers (e.g. on Windows), performing a resize while the window is minimized does nothing (the restored window keeps its old size). Therefore, like for maximized and fullscreen states, wait for the window to be restored to apply a resize. Refs #3947 --- app/src/screen.c | 17 ++++++++++++----- app/src/screen.h | 1 + 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/app/src/screen.c b/app/src/screen.c index 14fd2a00..2724a266 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -56,6 +56,7 @@ static void set_window_size(struct sc_screen *screen, struct sc_size new_size) { assert(!screen->fullscreen); assert(!screen->maximized); + assert(!screen->minimized); SDL_SetWindowSize(screen->window, new_size.width, new_size.height); } @@ -359,6 +360,7 @@ sc_screen_init(struct sc_screen *screen, screen->has_frame = false; screen->fullscreen = false; screen->maximized = false; + screen->minimized = false; screen->mouse_capture_key_pressed = 0; screen->req.x = params->window_x; @@ -531,11 +533,11 @@ resize_for_content(struct sc_screen *screen, struct sc_size old_content_size, static void set_content_size(struct sc_screen *screen, struct sc_size new_content_size) { - if (!screen->fullscreen && !screen->maximized) { + if (!screen->fullscreen && !screen->maximized && !screen->minimized) { 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 + // fullscreen/maximized/minimized are disabled screen->windowed_content_size = screen->content_size; screen->resize_pending = true; } @@ -547,6 +549,7 @@ static void apply_pending_resize(struct sc_screen *screen) { assert(!screen->fullscreen); assert(!screen->maximized); + assert(!screen->minimized); if (screen->resize_pending) { resize_for_content(screen, screen->windowed_content_size, screen->content_size); @@ -659,7 +662,7 @@ sc_screen_switch_fullscreen(struct sc_screen *screen) { } screen->fullscreen = !screen->fullscreen; - if (!screen->fullscreen && !screen->maximized) { + if (!screen->fullscreen && !screen->maximized && !screen->minimized) { apply_pending_resize(screen); } @@ -669,7 +672,7 @@ sc_screen_switch_fullscreen(struct sc_screen *screen) { void sc_screen_resize_to_fit(struct sc_screen *screen) { - if (screen->fullscreen || screen->maximized) { + if (screen->fullscreen || screen->maximized || screen->minimized) { return; } @@ -693,7 +696,7 @@ sc_screen_resize_to_fit(struct sc_screen *screen) { void sc_screen_resize_to_pixel_perfect(struct sc_screen *screen) { - if (screen->fullscreen) { + if (screen->fullscreen || screen->minimized) { return; } @@ -750,6 +753,9 @@ sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) { case SDL_WINDOWEVENT_MAXIMIZED: screen->maximized = true; break; + case SDL_WINDOWEVENT_MINIMIZED: + screen->minimized = true; + break; case SDL_WINDOWEVENT_RESTORED: if (screen->fullscreen) { // On Windows, in maximized+fullscreen, disabling @@ -760,6 +766,7 @@ sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) { break; } screen->maximized = false; + screen->minimized = false; apply_pending_resize(screen); sc_screen_render(screen, true); break; diff --git a/app/src/screen.h b/app/src/screen.h index 2c032119..acbaab4b 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -56,6 +56,7 @@ struct sc_screen { bool has_frame; bool fullscreen; bool maximized; + bool minimized; // To enable/disable mouse capture, a mouse capture key (LALT, LGUI or // RGUI) must be pressed. This variable tracks the pressed capture key. From 38900d77307d24690168ad05e4f95fe238d9e83a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 24 May 2023 21:31:47 +0200 Subject: [PATCH 1556/2244] Extract audio source to a static constant For consistency with the other parameters. --- server/src/main/java/com/genymobile/scrcpy/AudioCapture.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java b/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java index dbb38dd2..ef9ef30d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java @@ -18,6 +18,7 @@ import java.nio.ByteBuffer; public final class AudioCapture { + public static final int SOURCE = MediaRecorder.AudioSource.REMOTE_SUBMIX; public static final int SAMPLE_RATE = 48000; public static final int CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_STEREO; public static final int CHANNELS = 2; @@ -50,7 +51,7 @@ public final class AudioCapture { // On older APIs, Workarounds.fillAppInfo() must be called beforehand builder.setContext(FakeContext.get()); } - builder.setAudioSource(MediaRecorder.AudioSource.REMOTE_SUBMIX); + builder.setAudioSource(SOURCE); builder.setAudioFormat(createAudioFormat()); int minBufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, FORMAT); // This buffer size does not impact latency From 597d2ccc01b6fe32fe21d315182eac4a523e014d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 24 May 2023 22:13:02 +0200 Subject: [PATCH 1557/2244] Rename FORMAT to ENCODING The AudioFormat contains several properties. This specific value is named "encoding". --- .../src/main/java/com/genymobile/scrcpy/AudioCapture.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java b/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java index ef9ef30d..92ecd839 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java @@ -22,7 +22,7 @@ public final class AudioCapture { public static final int SAMPLE_RATE = 48000; public static final int CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_STEREO; public static final int CHANNELS = 2; - public static final int FORMAT = AudioFormat.ENCODING_PCM_16BIT; + public static final int ENCODING = AudioFormat.ENCODING_PCM_16BIT; public static final int BYTES_PER_SAMPLE = 2; private AudioRecord recorder; @@ -37,7 +37,7 @@ public final class AudioCapture { private static AudioFormat createAudioFormat() { AudioFormat.Builder builder = new AudioFormat.Builder(); - builder.setEncoding(FORMAT); + builder.setEncoding(ENCODING); builder.setSampleRate(SAMPLE_RATE); builder.setChannelMask(CHANNEL_CONFIG); return builder.build(); @@ -53,7 +53,7 @@ public final class AudioCapture { } builder.setAudioSource(SOURCE); builder.setAudioFormat(createAudioFormat()); - int minBufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, FORMAT); + int minBufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, ENCODING); // This buffer size does not impact latency builder.setBufferSizeInBytes(8 * minBufferSize); return builder.build(); From cab354102d722c27660e83817ecff6fd3cb9cfc7 Mon Sep 17 00:00:00 2001 From: Simon Chan <1330321+yume-chan@users.noreply.github.com> Date: Mon, 27 Mar 2023 01:09:07 +0800 Subject: [PATCH 1558/2244] Create AudioRecord by reflection as a fallback Some devices (Vivo phones) fail to create an AudioRecord from an AudioRecord.Builder (which throws a NullPointerException). In that case, create an AudioRecord instance directly by reflection. The AOSP version of AudioRecord constructor code can be found at: - Android 11 (R): - Android 12 (S): - Android 13 (T, functionally identical to Android 12): - Android 14 (U): Not released, but expected to change PR #3862 Fixes #3805 Signed-off-by: Romain Vimont --- .../com/genymobile/scrcpy/AudioCapture.java | 10 +- .../com/genymobile/scrcpy/Workarounds.java | 145 ++++++++++++++++++ 2 files changed, 154 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java b/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java index 92ecd839..b8fc076b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java @@ -22,6 +22,7 @@ public final class AudioCapture { public static final int SAMPLE_RATE = 48000; public static final int CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_STEREO; public static final int CHANNELS = 2; + public static final int CHANNEL_MASK = AudioFormat.CHANNEL_IN_LEFT | AudioFormat.CHANNEL_IN_RIGHT; public static final int ENCODING = AudioFormat.ENCODING_PCM_16BIT; public static final int BYTES_PER_SAMPLE = 2; @@ -98,7 +99,14 @@ public final class AudioCapture { } private void startRecording() { - recorder = createAudioRecord(); + try { + recorder = createAudioRecord(); + } catch (NullPointerException e) { + // Creating an AudioRecord using an AudioRecord.Builder does not work on Vivo phones: + // - + // - + recorder = Workarounds.createAudioRecord(SOURCE, SAMPLE_RATE, CHANNEL_CONFIG, CHANNELS, CHANNEL_MASK, ENCODING); + } recorder.startRecording(); } diff --git a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java index 89380ece..b343a344 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java +++ b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java @@ -1,13 +1,22 @@ package com.genymobile.scrcpy; import android.annotation.SuppressLint; +import android.annotation.TargetApi; import android.app.Application; +import android.content.AttributionSource; import android.content.ContextWrapper; import android.content.pm.ApplicationInfo; +import android.media.AudioAttributes; +import android.media.AudioManager; +import android.media.AudioRecord; +import android.os.Build; import android.os.Looper; +import android.os.Parcel; +import java.lang.ref.WeakReference; import java.lang.reflect.Constructor; import java.lang.reflect.Field; +import java.lang.reflect.Method; public final class Workarounds { @@ -95,4 +104,140 @@ public final class Workarounds { Ln.d("Could not fill app context: " + throwable.getMessage()); } } + + @TargetApi(Build.VERSION_CODES.R) + @SuppressLint({"WrongConstant", "MissingPermission", "BlockedPrivateApi", "SoonBlockedPrivateApi"}) + public static AudioRecord createAudioRecord(int source, int sampleRate, int channelConfig, int channels, int channelMask, int encoding) { + // Vivo (and maybe some other third-party ROMs) modified `AudioRecord`'s constructor, requiring `Context`s from real App environment. + // + // This method invokes the `AudioRecord(long nativeRecordInJavaObj)` constructor to create an empty `AudioRecord` instance, then uses + // reflections to initialize it like the normal constructor do (or the `AudioRecord.Builder.build()` method do). + // As a result, the modified code was not executed. + try { + // AudioRecord audioRecord = new AudioRecord(0L); + Constructor audioRecordConstructor = AudioRecord.class.getDeclaredConstructor(long.class); + audioRecordConstructor.setAccessible(true); + AudioRecord audioRecord = audioRecordConstructor.newInstance(0L); + + // audioRecord.mRecordingState = RECORDSTATE_STOPPED; + Field mRecordingStateField = AudioRecord.class.getDeclaredField("mRecordingState"); + mRecordingStateField.setAccessible(true); + mRecordingStateField.set(audioRecord, AudioRecord.RECORDSTATE_STOPPED); + + Looper looper = Looper.myLooper(); + if (looper == null) { + looper = Looper.getMainLooper(); + } + + // audioRecord.mInitializationLooper = looper; + Field mInitializationLooperField = AudioRecord.class.getDeclaredField("mInitializationLooper"); + mInitializationLooperField.setAccessible(true); + mInitializationLooperField.set(audioRecord, looper); + + // Create `AudioAttributes` with fixed capture preset + int capturePreset = source; + AudioAttributes.Builder audioAttributesBuilder = new AudioAttributes.Builder(); + Method setInternalCapturePresetMethod = AudioAttributes.Builder.class.getMethod("setInternalCapturePreset", int.class); + setInternalCapturePresetMethod.invoke(audioAttributesBuilder, capturePreset); + AudioAttributes attributes = audioAttributesBuilder.build(); + + // audioRecord.mAudioAttributes = attributes; + Field mAudioAttributesField = AudioRecord.class.getDeclaredField("mAudioAttributes"); + mAudioAttributesField.setAccessible(true); + mAudioAttributesField.set(audioRecord, attributes); + + // audioRecord.audioParamCheck(capturePreset, sampleRate, encoding); + Method audioParamCheckMethod = AudioRecord.class.getDeclaredMethod("audioParamCheck", int.class, int.class, int.class); + audioParamCheckMethod.setAccessible(true); + audioParamCheckMethod.invoke(audioRecord, capturePreset, sampleRate, encoding); + + // audioRecord.mChannelCount = channels + Field mChannelCountField = AudioRecord.class.getDeclaredField("mChannelCount"); + mChannelCountField.setAccessible(true); + mChannelCountField.set(audioRecord, channels); + + // audioRecord.mChannelMask = channelMask + Field mChannelMaskField = AudioRecord.class.getDeclaredField("mChannelMask"); + mChannelMaskField.setAccessible(true); + mChannelMaskField.set(audioRecord, channelMask); + + int minBufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, encoding); + int bufferSizeInBytes = minBufferSize * 8; + + // audioRecord.audioBuffSizeCheck(bufferSizeInBytes) + Method audioBuffSizeCheckMethod = AudioRecord.class.getDeclaredMethod("audioBuffSizeCheck", int.class); + audioBuffSizeCheckMethod.setAccessible(true); + audioBuffSizeCheckMethod.invoke(audioRecord, bufferSizeInBytes); + + final int channelIndexMask = 0; + + int[] sampleRateArray = new int[]{sampleRate}; + int[] session = new int[]{AudioManager.AUDIO_SESSION_ID_GENERATE}; + + int initResult; + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { + // private native final int native_setup(Object audiorecord_this, + // Object /*AudioAttributes*/ attributes, + // int[] sampleRate, int channelMask, int channelIndexMask, int audioFormat, + // int buffSizeInBytes, int[] sessionId, String opPackageName, + // long nativeRecordInJavaObj); + Method nativeSetupMethod = AudioRecord.class.getDeclaredMethod("native_setup", Object.class, Object.class, int[].class, int.class, + int.class, int.class, int.class, int[].class, String.class, long.class); + nativeSetupMethod.setAccessible(true); + initResult = (int) nativeSetupMethod.invoke(audioRecord, new WeakReference(audioRecord), attributes, sampleRateArray, + channelMask, channelIndexMask, audioRecord.getAudioFormat(), bufferSizeInBytes, session, FakeContext.get().getOpPackageName(), + 0L); + } else { + // Assume `context` is never `null` + AttributionSource attributionSource = FakeContext.get().getAttributionSource(); + + // Assume `attributionSource.getPackageName()` is never null + + // ScopedParcelState attributionSourceState = attributionSource.asScopedParcelState() + Method asScopedParcelStateMethod = AttributionSource.class.getDeclaredMethod("asScopedParcelState"); + asScopedParcelStateMethod.setAccessible(true); + + try (AutoCloseable attributionSourceState = (AutoCloseable) asScopedParcelStateMethod.invoke(attributionSource)) { + Method getParcelMethod = attributionSourceState.getClass().getDeclaredMethod("getParcel"); + Parcel attributionSourceParcel = (Parcel) getParcelMethod.invoke(attributionSourceState); + + // private native int native_setup(Object audiorecordThis, + // Object /*AudioAttributes*/ attributes, + // int[] sampleRate, int channelMask, int channelIndexMask, int audioFormat, + // int buffSizeInBytes, int[] sessionId, @NonNull Parcel attributionSource, + // long nativeRecordInJavaObj, int maxSharedAudioHistoryMs); + Method nativeSetupMethod = AudioRecord.class.getDeclaredMethod("native_setup", Object.class, Object.class, int[].class, int.class, + int.class, int.class, int.class, int[].class, Parcel.class, long.class, int.class); + nativeSetupMethod.setAccessible(true); + initResult = (int) nativeSetupMethod.invoke(audioRecord, new WeakReference(audioRecord), attributes, sampleRateArray, + channelMask, channelIndexMask, audioRecord.getAudioFormat(), bufferSizeInBytes, session, attributionSourceParcel, 0L, 0); + } + } + + if (initResult != AudioRecord.SUCCESS) { + Ln.e("Error code " + initResult + " when initializing native AudioRecord object."); + throw new RuntimeException("Cannot create AudioRecord"); + } + + // mSampleRate = sampleRate[0] + Field mSampleRateField = AudioRecord.class.getDeclaredField("mSampleRate"); + mSampleRateField.setAccessible(true); + mSampleRateField.set(audioRecord, sampleRateArray[0]); + + // audioRecord.mSessionId = session[0] + Field mSessionIdField = AudioRecord.class.getDeclaredField("mSessionId"); + mSessionIdField.setAccessible(true); + mSessionIdField.set(audioRecord, session[0]); + + // audioRecord.mState = AudioRecord.STATE_INITIALIZED + Field mStateField = AudioRecord.class.getDeclaredField("mState"); + mStateField.setAccessible(true); + mStateField.set(audioRecord, AudioRecord.STATE_INITIALIZED); + + return audioRecord; + } catch (Exception e) { + Ln.e("Failed to invoke AudioRecord..", e); + throw new RuntimeException("Cannot create AudioRecord"); + } + } } From a2c89100065f684dcf8859be4ea5274a7e6d5b26 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 24 May 2023 20:22:35 +0200 Subject: [PATCH 1559/2244] Rename --no-mirror to --no-playback This option impacts video and audio _playback_. For example, if we use V4L2, the device is still "mirrored" (via V4L2), even if playback is disabled. Therefore, "playback" is more approriate than "mirror". The initial option --no-display option was renamed to --no-mirror by commit 6928acdeac29eee404a7c7014654965ef5128b88, but this has never been released, so it is ok to rename it one more time. Refs #3978 PR #4033 --- app/data/bash-completion/scrcpy | 2 +- app/data/zsh-completion/_scrcpy | 2 +- app/scrcpy.1 | 4 ++-- app/src/cli.c | 27 +++++++++++++-------------- app/src/options.c | 2 +- app/src/options.h | 2 +- app/src/scrcpy.c | 18 +++++++++--------- app/tests/test_cli.c | 8 ++++---- doc/recording.md | 4 ++-- doc/v4l2.md | 2 +- doc/video.md | 8 ++++---- 11 files changed, 39 insertions(+), 40 deletions(-) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index cdc9270f..dccb68e5 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -33,7 +33,7 @@ _scrcpy() { --no-clipboard-autosync --no-downsize-on-error -n --no-control - -N --no-mirror + -N --no-playback --no-key-repeat --no-mipmaps --no-power-on diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 5e40b2fb..afc6b1e6 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -39,7 +39,7 @@ arguments=( '--no-clipboard-autosync[Disable automatic clipboard synchronization]' '--no-downsize-on-error[Disable lowering definition on MediaCodec error]' {-n,--no-control}'[Disable device control \(mirror the device in read only\)]' - {-N,--no-mirror}'[Do not mirror device \(only when recording or V4L2 sink is enabled\)]' + {-N,--no-playback}'[Disable video and audio playback]' '--no-key-repeat[Do not forward repeated key events when a key is held down]' '--no-mipmaps[Disable the generation of mipmaps]' '--no-power-on[Do not power on the device on start]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 29d14b58..a622d22b 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -210,8 +210,8 @@ This option disables this behavior. Disable device control (mirror the device in read\-only). .TP -.B \-N, \-\-no\-mirror -Do not mirror device video or audio on the computer (only when recording or V4L2 sink is enabled). +.B \-N, \-\-no\-playback +Disable video and audio playback on the computer. .TP .B \-\-no\-key\-repeat diff --git a/app/src/cli.c b/app/src/cli.c index e5b18277..dbf774a8 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -382,9 +382,8 @@ static const struct sc_option options[] = { }, { .shortopt = 'N', - .longopt = "no-mirror", - .text = "Do not mirror device video or audio on the computer (only " - "when recording or V4L2 sink is enabled).", + .longopt = "no-playback", + .text = "Disable video and audio playback on the computer.", }, { // deprecated @@ -1671,10 +1670,10 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], opts->control = false; break; case OPT_NO_DISPLAY: - LOGW("--no-display is deprecated, use --no-mirror instead."); + LOGW("--no-display is deprecated, use --no-playback instead."); // fall through case 'N': - opts->mirror = false; + opts->playback = false; break; case 'p': if (!parse_port_range(optarg, &opts->port_range)) { @@ -1924,8 +1923,8 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } #ifdef HAVE_V4L2 - if (!opts->mirror && !opts->record_filename && !opts->v4l2_device) { - LOGE("-N/--no-mirror requires either screen recording (-r/--record)" + if (!opts->playback && !opts->record_filename && !opts->v4l2_device) { + LOGE("-N/--no-playback requires either screen recording (-r/--record)" " or sink to v4l2loopback device (--v4l2-sink)"); return false; } @@ -1949,14 +1948,14 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; } #else - if (!opts->mirror && !opts->record_filename) { - LOGE("-N/--no-mirror requires screen recording (-r/--record)"); + if (!opts->playback && !opts->record_filename) { + LOGE("-N/--no-playback requires screen recording (-r/--record)"); return false; } #endif - if (opts->audio && !opts->mirror && !opts->record_filename) { - LOGI("No mirror and no recording: audio disabled"); + if (opts->audio && !opts->playback && !opts->record_filename) { + LOGI("No playback and no recording: audio disabled"); opts->audio = false; } @@ -2089,11 +2088,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], #endif #ifdef HAVE_USB - if (!(opts->mirror && opts->video) && !opts->otg) { + if (!(opts->playback && opts->video) && !opts->otg) { #else - if (!(opts->mirror && opts->video)) { + if (!(opts->playback && opts->video)) { #endif - // If video mirroring is disabled and OTG are disabled, then there is + // If video playback is disabled and OTG are disabled, then there is // no way to control the device. opts->control = false; } diff --git a/app/src/options.c b/app/src/options.c index 12283952..b021921f 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -52,7 +52,7 @@ const struct scrcpy_options scrcpy_options_default = { .fullscreen = false, .always_on_top = false, .control = true, - .mirror = true, + .playback = true, .turn_screen_off = false, .key_inject_mode = SC_KEY_INJECT_MODE_MIXED, .window_borderless = false, diff --git a/app/src/options.h b/app/src/options.h index 7c442149..0c886de0 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -147,7 +147,7 @@ struct scrcpy_options { bool fullscreen; bool always_on_top; bool control; - bool mirror; + bool playback; bool turn_screen_off; enum sc_key_inject_mode key_inject_mode; bool window_borderless; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 39a2f1a4..3334f57d 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -137,7 +137,7 @@ sdl_set_hints(const char *render_driver) { } static void -sdl_configure(bool mirror, bool disable_screensaver) { +sdl_configure(bool playback, bool disable_screensaver) { #ifdef _WIN32 // Clean up properly on Ctrl+C on Windows bool ok = SetConsoleCtrlHandler(windows_ctrl_handler, TRUE); @@ -146,7 +146,7 @@ sdl_configure(bool mirror, bool disable_screensaver) { } #endif // _WIN32 - if (!mirror) { + if (!playback) { return; } @@ -386,10 +386,10 @@ scrcpy(struct scrcpy_options *options) { goto end; } - if (options->mirror) { + if (options->playback) { sdl_set_hints(options->render_driver); - // Initialize SDL video and audio in addition if mirroring is enabled + // Initialize SDL video and audio in addition if playback is enabled if (options->video && SDL_Init(SDL_INIT_VIDEO)) { LOGE("Could not initialize SDL video: %s", SDL_GetError()); goto end; @@ -401,7 +401,7 @@ scrcpy(struct scrcpy_options *options) { } } - sdl_configure(options->mirror, options->disable_screensaver); + sdl_configure(options->playback, options->disable_screensaver); // Await for server without blocking Ctrl+C handling bool connected; @@ -427,7 +427,7 @@ scrcpy(struct scrcpy_options *options) { struct sc_file_pusher *fp = NULL; - assert(!options->control || options->mirror); // control implies mirror + assert(!options->control || options->playback); // control implies playback if (options->control) { if (!sc_file_pusher_init(&s->file_pusher, serial, options->push_target)) { @@ -453,8 +453,8 @@ scrcpy(struct scrcpy_options *options) { &audio_demuxer_cbs, options); } - bool needs_video_decoder = options->mirror && options->video; - bool needs_audio_decoder = options->mirror && options->audio; + bool needs_video_decoder = options->playback && options->video; + bool needs_audio_decoder = options->playback && options->audio; #ifdef HAVE_V4L2 needs_video_decoder |= !!options->v4l2_device; #endif @@ -650,7 +650,7 @@ aoa_hid_end: // There is a controller if and only if control is enabled assert(options->control == !!controller); - if (options->mirror) { + if (options->playback) { const char *window_title = options->window_title ? options->window_title : info->device_name; diff --git a/app/tests/test_cli.c b/app/tests/test_cli.c index 1f00cb90..ec1a9531 100644 --- a/app/tests/test_cli.c +++ b/app/tests/test_cli.c @@ -53,7 +53,7 @@ static void test_options(void) { "--max-size", "1024", "--lock-video-orientation=2", // optional arguments require '=' // "--no-control" is not compatible with "--turn-screen-off" - // "--no-mirror" is not compatible with "--fulscreen" + // "--no-playback" is not compatible with "--fulscreen" "--port", "1234:1236", "--push-target", "/sdcard/Movies", "--record", "file", @@ -108,8 +108,8 @@ static void test_options2(void) { char *argv[] = { "scrcpy", "--no-control", - "--no-mirror", - "--record", "file.mp4", // cannot enable --no-mirror without recording + "--no-playback", + "--record", "file.mp4", // cannot enable --no-playback without recording }; bool ok = scrcpy_parse_args(&args, ARRAY_LEN(argv), argv); @@ -117,7 +117,7 @@ static void test_options2(void) { const struct scrcpy_options *opts = &args.opts; assert(!opts->control); - assert(!opts->mirror); + assert(!opts->playback); assert(!strcmp(opts->record_filename, "file.mp4")); assert(opts->record_format == SC_RECORD_FORMAT_MP4); } diff --git a/doc/recording.md b/doc/recording.md index 7d66f2ff..6f721062 100644 --- a/doc/recording.md +++ b/doc/recording.md @@ -21,10 +21,10 @@ scrcpy --no-video --audio-codec=aac --record-file=file.aac # .m4a/.mp4 and .mka/.mkv are also supported for both opus and aac ``` -To disable mirroring while recording: +To disable playback while recording: ```bash -scrcpy --no-mirror --record=file.mp4 +scrcpy --no-playback --record=file.mp4 scrcpy -Nr file.mkv # interrupt recording with Ctrl+C ``` diff --git a/doc/v4l2.md b/doc/v4l2.md index 2c6e2cfc..62480478 100644 --- a/doc/v4l2.md +++ b/doc/v4l2.md @@ -35,7 +35,7 @@ To start `scrcpy` using a v4l2 sink: ```bash scrcpy --v4l2-sink=/dev/videoN -scrcpy --v4l2-sink=/dev/videoN --no-mirror # disable mirroring window +scrcpy --v4l2-sink=/dev/videoN --no-playback # disable playback window ``` (replace `N` with the device ID, check with `ls /dev/video*`) diff --git a/doc/video.md b/doc/video.md index 303d9048..757d9324 100644 --- a/doc/video.md +++ b/doc/video.md @@ -159,15 +159,15 @@ scrcpy --display-buffer=50 --v4l2-buffer=300 ``` -## No mirror +## No playback -It is possible to capture an Android device without displaying a mirroring +It is possible to capture an Android device without displaying a playback window. This option is available if either [recording](recording.md) or [v4l2](#video4linux) is enabled: ```bash -scrcpy --v4l2-sink=/dev/video2 --no-mirror -scrcpy --record=file.mkv --no-mirror +scrcpy --v4l2-sink=/dev/video2 --no-playback +scrcpy --record=file.mkv --no-playback ``` From e71f5358b3ebf44569b1f3798c13ed65554b69b9 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 24 May 2023 20:25:01 +0200 Subject: [PATCH 1560/2244] Reorder command line options checks Perform checks that impact the options first. --- app/src/cli.c | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index dbf774a8..60a007e9 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -1922,6 +1922,21 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; } +#ifdef HAVE_USB + if (!(opts->playback && opts->video) && !opts->otg) { +#else + if (!(opts->playback && opts->video)) { +#endif + // If video playback is disabled and OTG are disabled, then there is + // no way to control the device. + opts->control = false; + } + + if (!opts->video) { + // If video is disabled, then scrcpy must exit on audio failure. + opts->require_audio = true; + } + #ifdef HAVE_V4L2 if (!opts->playback && !opts->record_filename && !opts->v4l2_device) { LOGE("-N/--no-playback requires either screen recording (-r/--record)" @@ -2087,21 +2102,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } #endif -#ifdef HAVE_USB - if (!(opts->playback && opts->video) && !opts->otg) { -#else - if (!(opts->playback && opts->video)) { -#endif - // If video playback is disabled and OTG are disabled, then there is - // no way to control the device. - opts->control = false; - } - - if (!opts->video) { - // If video is disabled, then scrcpy must exit on audio failure. - opts->require_audio = true; - } - return true; } From f46758d1c5ccef668435d712958a6c5acfb1c44c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 24 May 2023 20:36:27 +0200 Subject: [PATCH 1561/2244] Fix V4L2 error message when disabled For consistency, use the same error message for --v4l2-sink and --v4l2-buffer. --- app/src/cli.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/cli.c b/app/src/cli.c index 60a007e9..10565da9 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -1871,7 +1871,8 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } break; #else - LOGE("V4L2 (--v4l2-buffer) is only available on Linux."); + LOGE("V4L2 (--v4l2-buffer) is disabled (or unsupported on this " + "platform)."); return false; #endif case OPT_LIST_ENCODERS: From 6ad46d70b8190a8f409cdd62870f51a6e24baa50 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 24 May 2023 20:39:13 +0200 Subject: [PATCH 1562/2244] Define v4l2_buffer only if HAVE_V4L2 If V4L2 support is disabled, there is no v4l2 buffer option. --- app/src/options.c | 8 ++++---- app/src/options.h | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/src/options.c b/app/src/options.c index b021921f..fc3b1790 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -11,9 +11,6 @@ const struct scrcpy_options scrcpy_options_default = { .audio_codec_options = NULL, .video_encoder = NULL, .audio_encoder = NULL, -#ifdef HAVE_V4L2 - .v4l2_device = NULL, -#endif .log_level = SC_LOG_LEVEL_INFO, .video_codec = SC_CODEC_H264, .audio_codec = SC_CODEC_OPUS, @@ -42,9 +39,12 @@ const struct scrcpy_options scrcpy_options_default = { .window_height = 0, .display_id = 0, .display_buffer = 0, - .v4l2_buffer = 0, .audio_buffer = SC_TICK_FROM_MS(50), .audio_output_buffer = SC_TICK_FROM_MS(5), +#ifdef HAVE_V4L2 + .v4l2_device = NULL, + .v4l2_buffer = 0, +#endif #ifdef HAVE_USB .otg = false, #endif diff --git a/app/src/options.h b/app/src/options.h index 0c886de0..e3cc8064 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -112,9 +112,6 @@ struct scrcpy_options { const char *audio_codec_options; const char *video_encoder; const char *audio_encoder; -#ifdef HAVE_V4L2 - const char *v4l2_device; -#endif enum sc_log_level log_level; enum sc_codec video_codec; enum sc_codec audio_codec; @@ -137,9 +134,12 @@ struct scrcpy_options { uint16_t window_height; uint32_t display_id; sc_tick display_buffer; - sc_tick v4l2_buffer; sc_tick audio_buffer; sc_tick audio_output_buffer; +#ifdef HAVE_V4L2 + const char *v4l2_device; + sc_tick v4l2_buffer; +#endif #ifdef HAVE_USB bool otg; #endif From 751c09f47a025f0affbd44db2e9a1f343d827cb5 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 24 May 2023 20:44:17 +0200 Subject: [PATCH 1563/2244] Simplify V4L2/USB ifdefs Define local variables whose value depends on ifdefs, to avoid cluttering all conditions with ifdefs. --- app/src/cli.c | 35 +++++++++++++++-------------------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index 10565da9..e96a3b85 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -1923,11 +1923,16 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; } + bool otg = false; + bool v4l2 = false; #ifdef HAVE_USB - if (!(opts->playback && opts->video) && !opts->otg) { -#else - if (!(opts->playback && opts->video)) { + otg = opts->otg; #endif +#ifdef HAVE_V4L2 + v4l2 = !!opts->v4l2_device; +#endif + + if (!(opts->playback && opts->video) && !otg) { // If video playback is disabled and OTG are disabled, then there is // no way to control the device. opts->control = false; @@ -1938,14 +1943,14 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], opts->require_audio = true; } -#ifdef HAVE_V4L2 - if (!opts->playback && !opts->record_filename && !opts->v4l2_device) { + if (!opts->playback && !opts->record_filename && !v4l2) { LOGE("-N/--no-playback requires either screen recording (-r/--record)" " or sink to v4l2loopback device (--v4l2-sink)"); return false; } - if (opts->v4l2_device) { +#ifdef HAVE_V4L2 + if (v4l2) { if (opts->lock_video_orientation == SC_LOCK_VIDEO_ORIENTATION_UNLOCKED) { LOGI("Video orientation is locked for v4l2 sink. " @@ -1963,11 +1968,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], LOGE("V4L2 buffer value without V4L2 sink\n"); return false; } -#else - if (!opts->playback && !opts->record_filename) { - LOGE("-N/--no-playback requires screen recording (-r/--record)"); - return false; - } #endif if (opts->audio && !opts->playback && !opts->record_filename) { @@ -2054,11 +2054,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } } -#ifdef HAVE_USB - # ifdef _WIN32 - if (!opts->otg && (opts->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_HID - || opts->mouse_input_mode == SC_MOUSE_INPUT_MODE_HID)) { + if (!otg && (opts->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_HID + || opts->mouse_input_mode == SC_MOUSE_INPUT_MODE_HID)) { LOGE("On Windows, it is not possible to open a USB device already open " "by another process (like adb)."); LOGE("Therefore, -K/--hid-keyboard and -M/--hid-mouse may only work in " @@ -2067,7 +2065,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } # endif - if (opts->otg) { + if (otg) { // OTG mode is compatible with only very few options. // Only report obvious errors. if (opts->record_filename) { @@ -2094,14 +2092,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], LOGE("OTG mode: could not select display"); return false; } -# ifdef HAVE_V4L2 - if (opts->v4l2_device) { + if (v4l2) { LOGE("OTG mode: could not sink to V4L2 device"); return false; } -# endif } -#endif return true; } From 1efbfe1175b023880b99f55df054f53ba3b30aa7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 24 May 2023 21:22:31 +0200 Subject: [PATCH 1564/2244] Add separate video and audio playback options Add --no-video-playback and --no-audio-playback. The option --no-playback is now an alias for both. PR #4033 --- app/data/bash-completion/scrcpy | 2 ++ app/data/zsh-completion/_scrcpy | 2 ++ app/scrcpy.1 | 10 +++++- app/src/cli.c | 61 +++++++++++++++++++++++++-------- app/src/options.c | 3 +- app/src/options.h | 3 +- app/src/scrcpy.c | 53 ++++++++++++++-------------- app/tests/test_cli.c | 3 +- 8 files changed, 94 insertions(+), 43 deletions(-) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index dccb68e5..010125fb 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -29,6 +29,7 @@ _scrcpy() { -M --hid-mouse -m --max-size= --no-audio + --no-audio-playback --no-cleanup --no-clipboard-autosync --no-downsize-on-error @@ -38,6 +39,7 @@ _scrcpy() { --no-mipmaps --no-power-on --no-video + --no-video-playback --otg -p --port= --power-off-on-close diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index afc6b1e6..4f1f16b9 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -35,6 +35,7 @@ arguments=( {-M,--hid-mouse}'[Simulate a physical mouse by using HID over AOAv2]' {-m,--max-size=}'[Limit both the width and height of the video to value]' '--no-audio[Disable audio forwarding]' + '--no-audio-playback[Disable audio playback]' '--no-cleanup[Disable device cleanup actions on exit]' '--no-clipboard-autosync[Disable automatic clipboard synchronization]' '--no-downsize-on-error[Disable lowering definition on MediaCodec error]' @@ -44,6 +45,7 @@ arguments=( '--no-mipmaps[Disable the generation of mipmaps]' '--no-power-on[Do not power on the device on start]' '--no-video[Disable video forwarding]' + '--no-video-playback[Disable video playback]' '--otg[Run in OTG mode \(simulating physical keyboard and mouse\)]' {-p,--port=}'[\[port\[\:port\]\] Set the TCP port \(range\) used by the client to listen]' '--power-off-on-close[Turn the device screen off when closing scrcpy]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index a622d22b..d506d9e7 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -187,6 +187,10 @@ Also see \fB\-\-hid\-keyboard\fR. .B \-\-no\-audio Disable audio forwarding. +.TP +.B \-\-no\-audio\-playback +Disable audio playback on the computer. + .TP .B \-\-no\-cleanup By default, scrcpy removes the server binary from the device and restores the device state (show touches, stay awake and power mode) on exit. @@ -211,7 +215,7 @@ Disable device control (mirror the device in read\-only). .TP .B \-N, \-\-no\-playback -Disable video and audio playback on the computer. +Disable video and audio playback on the computer (equivalent to --no-video-playback --no-audio-playback). .TP .B \-\-no\-key\-repeat @@ -229,6 +233,10 @@ Do not power on the device on start. .B \-\-no\-video Disable video forwarding. +.TP +.B \-\-no\-video\-playback +Disable video playback on the computer. + .TP .B \-\-otg Run in OTG mode: simulate physical keyboard and mouse, as if the computer keyboard and mouse were plugged directly to the device via an OTG cable. diff --git a/app/src/cli.c b/app/src/cli.c index e96a3b85..1b42f75c 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -74,6 +74,8 @@ enum { OPT_AUDIO_OUTPUT_BUFFER, OPT_NO_DISPLAY, OPT_NO_VIDEO, + OPT_NO_AUDIO_PLAYBACK, + OPT_NO_VIDEO_PLAYBACK, }; struct sc_option { @@ -351,6 +353,11 @@ static const struct sc_option options[] = { .longopt = "no-audio", .text = "Disable audio forwarding.", }, + { + .longopt_id = OPT_NO_AUDIO_PLAYBACK, + .longopt = "no-audio-playback", + .text = "Disable audio playback on the computer.", + }, { .longopt_id = OPT_NO_CLEANUP, .longopt = "no-cleanup", @@ -383,7 +390,8 @@ static const struct sc_option options[] = { { .shortopt = 'N', .longopt = "no-playback", - .text = "Disable video and audio playback on the computer.", + .text = "Disable video and audio playback on the computer (equivalent " + "to --no-video-playback --no-audio-playback).", }, { // deprecated @@ -412,6 +420,11 @@ static const struct sc_option options[] = { .longopt = "no-video", .text = "Disable video forwarding.", }, + { + .longopt_id = OPT_NO_VIDEO_PLAYBACK, + .longopt = "no-video-playback", + .text = "Disable video playback on the computer.", + }, { .longopt_id = OPT_OTG, .longopt = "otg", @@ -1673,7 +1686,14 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], LOGW("--no-display is deprecated, use --no-playback instead."); // fall through case 'N': - opts->playback = false; + opts->video_playback = false; + opts->audio_playback = false; + break; + case OPT_NO_VIDEO_PLAYBACK: + opts->video_playback = false; + break; + case OPT_NO_AUDIO_PLAYBACK: + opts->audio_playback = false; break; case 'p': if (!parse_port_range(optarg, &opts->port_range)) { @@ -1932,23 +1952,41 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], v4l2 = !!opts->v4l2_device; #endif - if (!(opts->playback && opts->video) && !otg) { + if (!opts->video) { + opts->video_playback = false; + } + + if (!opts->audio) { + opts->audio_playback = false; + } + + if (!opts->video_playback && !otg) { // If video playback is disabled and OTG are disabled, then there is // no way to control the device. opts->control = false; } - if (!opts->video) { - // If video is disabled, then scrcpy must exit on audio failure. - opts->require_audio = true; + if (opts->video && !opts->video_playback && !opts->record_filename + && !v4l2) { + LOGI("No video playback, no recording, no V4L2 sink: video disabled"); + opts->video = false; } - if (!opts->playback && !opts->record_filename && !v4l2) { - LOGE("-N/--no-playback requires either screen recording (-r/--record)" - " or sink to v4l2loopback device (--v4l2-sink)"); + if (opts->audio && !opts->audio_playback && !opts->record_filename) { + LOGI("No audio playback, no recording: audio disabled"); + opts->audio = false; + } + + if (!opts->video && !opts->audio && !otg) { + LOGE("No video, no audio, no OTG: nothing to do"); return false; } + if (!opts->video && !otg) { + // If video is disabled, then scrcpy must exit on audio failure. + opts->require_audio = true; + } + #ifdef HAVE_V4L2 if (v4l2) { if (opts->lock_video_orientation == @@ -1970,11 +2008,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } #endif - if (opts->audio && !opts->playback && !opts->record_filename) { - LOGI("No playback and no recording: audio disabled"); - opts->audio = false; - } - if ((opts->tunnel_host || opts->tunnel_port) && !opts->force_adb_forward) { LOGI("Tunnel host/port is set, " "--force-adb-forward automatically enabled."); diff --git a/app/src/options.c b/app/src/options.c index fc3b1790..49cca969 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -52,7 +52,8 @@ const struct scrcpy_options scrcpy_options_default = { .fullscreen = false, .always_on_top = false, .control = true, - .playback = true, + .video_playback = true, + .audio_playback = true, .turn_screen_off = false, .key_inject_mode = SC_KEY_INJECT_MODE_MIXED, .window_borderless = false, diff --git a/app/src/options.h b/app/src/options.h index e3cc8064..dd5d0dad 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -147,7 +147,8 @@ struct scrcpy_options { bool fullscreen; bool always_on_top; bool control; - bool playback; + bool video_playback; + bool audio_playback; bool turn_screen_off; enum sc_key_inject_mode key_inject_mode; bool window_borderless; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 3334f57d..27a1c6be 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -137,7 +137,7 @@ sdl_set_hints(const char *render_driver) { } static void -sdl_configure(bool playback, bool disable_screensaver) { +sdl_configure(bool video_playback, bool disable_screensaver) { #ifdef _WIN32 // Clean up properly on Ctrl+C on Windows bool ok = SetConsoleCtrlHandler(windows_ctrl_handler, TRUE); @@ -146,7 +146,7 @@ sdl_configure(bool playback, bool disable_screensaver) { } #endif // _WIN32 - if (!playback) { + if (!video_playback) { return; } @@ -386,22 +386,26 @@ scrcpy(struct scrcpy_options *options) { goto end; } - if (options->playback) { - sdl_set_hints(options->render_driver); + // playback implies capture + assert(!options->video_playback || options->video); + assert(!options->audio_playback || options->audio); - // Initialize SDL video and audio in addition if playback is enabled - if (options->video && SDL_Init(SDL_INIT_VIDEO)) { + if (options->video_playback) { + sdl_set_hints(options->render_driver); + if (SDL_Init(SDL_INIT_VIDEO)) { LOGE("Could not initialize SDL video: %s", SDL_GetError()); goto end; } + } - if (options->audio && SDL_Init(SDL_INIT_AUDIO)) { + if (options->audio_playback) { + if (SDL_Init(SDL_INIT_AUDIO)) { LOGE("Could not initialize SDL audio: %s", SDL_GetError()); goto end; } } - sdl_configure(options->playback, options->disable_screensaver); + sdl_configure(options->video_playback, options->disable_screensaver); // Await for server without blocking Ctrl+C handling bool connected; @@ -427,7 +431,8 @@ scrcpy(struct scrcpy_options *options) { struct sc_file_pusher *fp = NULL; - assert(!options->control || options->playback); // control implies playback + // control implies video playback + assert(!options->control || options->video_playback); if (options->control) { if (!sc_file_pusher_init(&s->file_pusher, serial, options->push_target)) { @@ -453,8 +458,8 @@ scrcpy(struct scrcpy_options *options) { &audio_demuxer_cbs, options); } - bool needs_video_decoder = options->playback && options->video; - bool needs_audio_decoder = options->playback && options->audio; + bool needs_video_decoder = options->video_playback; + bool needs_audio_decoder = options->audio_playback; #ifdef HAVE_V4L2 needs_video_decoder |= !!options->v4l2_device; #endif @@ -650,7 +655,7 @@ aoa_hid_end: // There is a controller if and only if control is enabled assert(options->control == !!controller); - if (options->playback) { + if (options->video_playback) { const char *window_title = options->window_title ? options->window_title : info->device_name; @@ -684,21 +689,19 @@ aoa_hid_end: src = &s->display_buffer.frame_source; } - if (options->video) { - if (!sc_screen_init(&s->screen, &screen_params)) { - goto end; - } - screen_initialized = true; - - sc_frame_source_add_sink(src, &s->screen.frame_sink); + if (!sc_screen_init(&s->screen, &screen_params)) { + goto end; } + screen_initialized = true; - if (options->audio) { - sc_audio_player_init(&s->audio_player, options->audio_buffer, - options->audio_output_buffer); - sc_frame_source_add_sink(&s->audio_decoder.frame_source, - &s->audio_player.frame_sink); - } + sc_frame_source_add_sink(src, &s->screen.frame_sink); + } + + if (options->audio_playback) { + sc_audio_player_init(&s->audio_player, options->audio_buffer, + options->audio_output_buffer); + sc_frame_source_add_sink(&s->audio_decoder.frame_source, + &s->audio_player.frame_sink); } #ifdef HAVE_V4L2 diff --git a/app/tests/test_cli.c b/app/tests/test_cli.c index ec1a9531..f2a17272 100644 --- a/app/tests/test_cli.c +++ b/app/tests/test_cli.c @@ -117,7 +117,8 @@ static void test_options2(void) { const struct scrcpy_options *opts = &args.opts; assert(!opts->control); - assert(!opts->playback); + assert(!opts->video_playback); + assert(!opts->audio_playback); assert(!strcmp(opts->record_filename, "file.mp4")); assert(opts->record_format == SC_RECORD_FORMAT_MP4); } From c4caa6b81d86cb82fa648612b1b4e4a405d8cab0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 24 May 2023 21:28:40 +0200 Subject: [PATCH 1565/2244] Document --no-{video,audio}-playback PR #4033 --- doc/audio.md | 2 ++ doc/v4l2.md | 2 +- doc/video.md | 14 ++++++++++++-- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/doc/audio.md b/doc/audio.md index 08868eaf..a437005c 100644 --- a/doc/audio.md +++ b/doc/audio.md @@ -24,6 +24,8 @@ To disable audio: scrcpy --no-audio ``` +To disable only the audio playback, see [no playback](video.md#no-playback). + ## Audio only To play audio only, disable the video: diff --git a/doc/v4l2.md b/doc/v4l2.md index 62480478..23c99912 100644 --- a/doc/v4l2.md +++ b/doc/v4l2.md @@ -35,7 +35,7 @@ To start `scrcpy` using a v4l2 sink: ```bash scrcpy --v4l2-sink=/dev/videoN -scrcpy --v4l2-sink=/dev/videoN --no-playback # disable playback window +scrcpy --v4l2-sink=/dev/videoN --no-video-playback # disable playback window ``` (replace `N` with the device ID, check with `ls /dev/video*`) diff --git a/doc/video.md b/doc/video.md index 757d9324..e411cf0d 100644 --- a/doc/video.md +++ b/doc/video.md @@ -161,8 +161,8 @@ scrcpy --display-buffer=50 --v4l2-buffer=300 ## No playback -It is possible to capture an Android device without displaying a playback -window. This option is available if either [recording](recording.md) or +It is possible to capture an Android device without playing video or audio on +the computer. This option is useful when [recording](recording.md) or when [v4l2](#video4linux) is enabled: ```bash @@ -170,6 +170,16 @@ scrcpy --v4l2-sink=/dev/video2 --no-playback scrcpy --record=file.mkv --no-playback ``` +It is also possible to disable video and audio playback separately: + +```bash +# Send video to V4L2 sink without playing it, but keep audio playback +scrcpy --v4l2-sink=/dev/video2 --no-video-playback + +# Record both video and audio, but only play video +scrcpy --record=file.mkv --no-audio-playback +``` + ## No video From 798dfd240e53e964094c656f4e365f0cef33b943 Mon Sep 17 00:00:00 2001 From: Yan Date: Sat, 8 Apr 2023 10:11:47 +0200 Subject: [PATCH 1566/2244] Turn device screen off after set up Sometimes it can take quite a while for everything to get set up and the screen to appear. PR #3902 Signed-off-by: Romain Vimont --- app/src/scrcpy.c | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 27a1c6be..4bbc8dca 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -639,17 +639,6 @@ aoa_hid_end: } controller_started = true; controller = &s->controller; - - if (options->turn_screen_off) { - struct sc_control_msg msg; - msg.type = SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE; - msg.set_screen_power_mode.mode = SC_SCREEN_POWER_MODE_OFF; - - if (!sc_controller_push_msg(&s->controller, &msg)) { - LOGW("Could not request 'set screen power mode'"); - } - } - } // There is a controller if and only if control is enabled @@ -740,6 +729,18 @@ aoa_hid_end: audio_demuxer_started = true; } + // If the device screen is to be turned off, send the control message after + // everything is set up + if (options->control && options->turn_screen_off) { + struct sc_control_msg msg; + msg.type = SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE; + msg.set_screen_power_mode.mode = SC_SCREEN_POWER_MODE_OFF; + + if (!sc_controller_push_msg(&s->controller, &msg)) { + LOGW("Could not request 'set screen power mode'"); + } + } + ret = event_loop(s); LOGD("quit..."); From 4c4a03ebe16749aec8a83d27cd28e3d686de171c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 30 May 2023 19:31:48 +0200 Subject: [PATCH 1567/2244] Reorder options to maintain alphabetical order --- app/scrcpy.1 | 16 ++++++++-------- app/src/cli.c | 20 ++++++++++---------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index d506d9e7..a30e7db0 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -33,14 +33,6 @@ Lower values decrease the latency, but increase the likelyhood of buffer underru Default is 50. -.TP -.BI "\-\-audio\-output\-buffer ms -Configure the size of the SDL audio output buffer (in milliseconds). - -If you get "robotic" audio playback, you should test with a higher value (10). Do not change this setting otherwise. - -Default is 5. - .TP .BI "\-\-audio\-codec " name Select an audio codec (opus, aac or raw). @@ -63,6 +55,14 @@ Use a specific MediaCodec audio encoder (depending on the codec provided by \fB\ The available encoders can be listed by \-\-list\-encoders. +.TP +.BI "\-\-audio\-output\-buffer ms +Configure the size of the SDL audio output buffer (in milliseconds). + +If you get "robotic" audio playback, you should test with a higher value (10). Do not change this setting otherwise. + +Default is 5. + .TP .BI "\-b, \-\-video\-bit\-rate " value Encode the video at the given bit\-rate, expressed in bits/s. Unit suffixes are supported: '\fBK\fR' (x1000) and '\fBM\fR' (x1000000). diff --git a/app/src/cli.c b/app/src/cli.c index 1b42f75c..01b55406 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -134,16 +134,6 @@ static const struct sc_option options[] = { "likelyhood of buffer underrun (causing audio glitches).\n" "Default is 50.", }, - { - .longopt_id = OPT_AUDIO_OUTPUT_BUFFER, - .longopt = "audio-output-buffer", - .argdesc = "ms", - .text = "Configure the size of the SDL audio output buffer (in " - "milliseconds).\n" - "If you get \"robotic\" audio playback, you should test with " - "a higher value (10). Do not change this setting otherwise.\n" - "Default is 5.", - }, { .longopt_id = OPT_AUDIO_CODEC, .longopt = "audio-codec", @@ -171,6 +161,16 @@ static const struct sc_option options[] = { "codec provided by --audio-codec).\n" "The available encoders can be listed by --list-encoders.", }, + { + .longopt_id = OPT_AUDIO_OUTPUT_BUFFER, + .longopt = "audio-output-buffer", + .argdesc = "ms", + .text = "Configure the size of the SDL audio output buffer (in " + "milliseconds).\n" + "If you get \"robotic\" audio playback, you should test with " + "a higher value (10). Do not change this setting otherwise.\n" + "Default is 5.", + }, { .shortopt = 'b', .longopt = "video-bit-rate", From b2d860382fd6ccefe85c45343da622ffbed8b3fa Mon Sep 17 00:00:00 2001 From: shuax <6940583+shuax@users.noreply.github.com> Date: Wed, 31 May 2023 03:26:33 +0000 Subject: [PATCH 1568/2244] Fix stream offset on audio buffer underflow The `read` variable is in number of samples, while the offset must be in bytes. PR #4045 Signed-off-by: Romain Vimont --- app/src/audio_player.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/audio_player.c b/app/src/audio_player.c index a0c52c62..8f0ad7fb 100644 --- a/app/src/audio_player.c +++ b/app/src/audio_player.c @@ -107,7 +107,7 @@ sc_audio_player_sdl_callback(void *userdata, uint8_t *stream, int len_int) { // latency. LOGD("[Audio] Buffer underflow, inserting silence: %" PRIu32 " samples", silence); - memset(stream + read, 0, TO_BYTES(silence)); + memset(stream + TO_BYTES(read), 0, TO_BYTES(silence)); if (ap->received) { // Inserting additional samples immediately increases buffering From 9a2abba09827b162f0dc5e2d48badde897c3f7dc Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 1 Jun 2023 09:01:52 +0200 Subject: [PATCH 1569/2244] Update demuxer comment The comment was outdated: - the "meta" header is now always present (not only when recording is enabled); - it is not only used for the video stream, but also for the audio stream. --- app/src/demuxer.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/src/demuxer.c b/app/src/demuxer.c index 4fcdd9ad..943f72b6 100644 --- a/app/src/demuxer.c +++ b/app/src/demuxer.c @@ -79,9 +79,8 @@ sc_demuxer_recv_video_size(struct sc_demuxer *demuxer, uint32_t *width, static bool sc_demuxer_recv_packet(struct sc_demuxer *demuxer, AVPacket *packet) { - // The video stream contains raw packets, without time information. When we - // record, we retrieve the timestamps separately, from a "meta" header - // added by the server before each raw packet. + // The video and audio streams contain a sequence of raw packets (as + // provided by MediaCodec), each prefixed with a "meta" header. // // The "meta" header length is 12 bytes: // [. . . . . . . .|. . . .]. . . . . . . . . . . . . . . ... From 8e2c0d64079672d802b52c03862492671a0351d6 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 29 May 2023 19:23:51 +0200 Subject: [PATCH 1570/2244] Rename rotationChanged to resetCapture The flag is used to reset the capture (restart the encoding) on rotation change. It will also be used for other events (on folding change), so rename it. PR #3979 --- .../java/com/genymobile/scrcpy/ScreenEncoder.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index 901ba94c..d45ca853 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -26,7 +26,7 @@ public class ScreenEncoder implements Device.RotationListener, AsyncProcessor { private static final int[] MAX_SIZE_FALLBACK = {2560, 1920, 1600, 1280, 1024, 800}; private static final int MAX_CONSECUTIVE_ERRORS = 3; - private final AtomicBoolean rotationChanged = new AtomicBoolean(); + private final AtomicBoolean resetCapture = new AtomicBoolean(); private final Device device; private final Streamer streamer; @@ -55,11 +55,11 @@ public class ScreenEncoder implements Device.RotationListener, AsyncProcessor { @Override public void onRotationChanged(int rotation) { - rotationChanged.set(true); + resetCapture.set(true); } - private boolean consumeRotationChange() { - return rotationChanged.getAndSet(false); + private boolean consumeResetCapture() { + return resetCapture.getAndSet(false); } private void streamScreen() throws IOException, ConfigurationException { @@ -169,14 +169,14 @@ public class ScreenEncoder implements Device.RotationListener, AsyncProcessor { boolean alive = true; MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); - while (!consumeRotationChange() && !eof) { + while (!consumeResetCapture() && !eof) { if (stopped.get()) { alive = false; break; } int outputBufferId = codec.dequeueOutputBuffer(bufferInfo, -1); try { - if (consumeRotationChange()) { + if (consumeResetCapture()) { // must restart encoding with new size break; } From 24999d0d32daf50246168dde212f01cf109569f9 Mon Sep 17 00:00:00 2001 From: Adonis Najimi Date: Sun, 7 May 2023 22:09:21 +0200 Subject: [PATCH 1571/2244] Reset video capture on folding event Handle folding event the same way as rotation events. Fixes #3960 PR #3979 Signed-off-by: Romain Vimont --- .../android/view/IDisplayFoldListener.aidl | 26 ++++++++++++++++ .../java/com/genymobile/scrcpy/Device.java | 30 +++++++++++++++++++ .../com/genymobile/scrcpy/ScreenEncoder.java | 9 +++++- .../scrcpy/wrappers/WindowManager.java | 10 +++++++ 4 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 server/src/main/aidl/android/view/IDisplayFoldListener.aidl diff --git a/server/src/main/aidl/android/view/IDisplayFoldListener.aidl b/server/src/main/aidl/android/view/IDisplayFoldListener.aidl new file mode 100644 index 00000000..2c91149d --- /dev/null +++ b/server/src/main/aidl/android/view/IDisplayFoldListener.aidl @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2019 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.view; + +/** + * {@hide} + */ +oneway interface IDisplayFoldListener +{ + /** Called when the foldedness of a display changes */ + void onDisplayFoldChanged(int displayId, boolean folded); +} diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index 3d83f73e..a3b6a270 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -12,6 +12,7 @@ import android.os.Build; import android.os.IBinder; import android.os.SystemClock; import android.view.IRotationWatcher; +import android.view.IDisplayFoldListener; import android.view.InputDevice; import android.view.InputEvent; import android.view.KeyCharacterMap; @@ -35,6 +36,10 @@ public final class Device { void onRotationChanged(int rotation); } + public interface FoldListener { + void onFoldChanged(int displayId, boolean folded); + } + public interface ClipboardListener { void onClipboardTextChanged(String text); } @@ -46,6 +51,7 @@ public final class Device { private ScreenInfo screenInfo; private RotationListener rotationListener; + private FoldListener foldListener; private ClipboardListener clipboardListener; private final AtomicBoolean isSettingClipboard = new AtomicBoolean(); @@ -93,6 +99,26 @@ public final class Device { } }, displayId); + ServiceManager.getWindowManager().registerDisplayFoldListener(new IDisplayFoldListener.Stub() { + @Override + public void onDisplayFoldChanged(int displayId, boolean folded) { + synchronized (Device.this) { + DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(displayId); + if (displayInfo == null) { + Ln.e("Display " + displayId + " not found\n" + LogUtils.buildDisplayListMessage()); + return; + } + + screenInfo = ScreenInfo.computeScreenInfo(displayInfo.getRotation(), displayInfo.getSize(), options.getCrop(), + options.getMaxSize(), options.getLockVideoOrientation()); + // notify + if (foldListener != null) { + foldListener.onFoldChanged(displayId, folded); + } + } + } + }); + if (options.getControl() && options.getClipboardAutosync()) { // If control and autosync are enabled, synchronize Android clipboard to the computer automatically ClipboardManager clipboardManager = ServiceManager.getClipboardManager(); @@ -224,6 +250,10 @@ public final class Device { this.rotationListener = rotationListener; } + public synchronized void setFoldListener(FoldListener foldlistener) { + this.foldListener = foldlistener; + } + public synchronized void setClipboardListener(ClipboardListener clipboardListener) { this.clipboardListener = clipboardListener; } diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index d45ca853..d56e5d27 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -16,7 +16,7 @@ import java.nio.ByteBuffer; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; -public class ScreenEncoder implements Device.RotationListener, AsyncProcessor { +public class ScreenEncoder implements Device.RotationListener, Device.FoldListener, AsyncProcessor { private static final int DEFAULT_I_FRAME_INTERVAL = 10; // seconds private static final int REPEAT_FRAME_DELAY_US = 100_000; // repeat after 100ms @@ -53,6 +53,11 @@ public class ScreenEncoder implements Device.RotationListener, AsyncProcessor { this.downsizeOnError = downsizeOnError; } + @Override + public void onFoldChanged(int displayId, boolean folded) { + resetCapture.set(true); + } + @Override public void onRotationChanged(int rotation) { resetCapture.set(true); @@ -68,6 +73,7 @@ public class ScreenEncoder implements Device.RotationListener, AsyncProcessor { MediaFormat format = createFormat(codec.getMimeType(), videoBitRate, maxFps, codecOptions); IBinder display = createDisplay(); device.setRotationListener(this); + device.setFoldListener(this); streamer.writeVideoHeader(device.getScreenInfo().getVideoSize()); @@ -115,6 +121,7 @@ public class ScreenEncoder implements Device.RotationListener, AsyncProcessor { } finally { mediaCodec.release(); device.setRotationListener(null); + device.setFoldListener(null); SurfaceControl.destroyDisplay(display); } } 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 faa366a5..d9fd9825 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java @@ -4,6 +4,7 @@ import com.genymobile.scrcpy.Ln; import android.os.IInterface; import android.view.IRotationWatcher; +import android.view.IDisplayFoldListener; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -108,4 +109,13 @@ public final class WindowManager { throw new AssertionError(e); } } + + public void registerDisplayFoldListener(IDisplayFoldListener foldListener) { + try { + Class cls = manager.getClass(); + cls.getMethod("registerDisplayFoldListener", IDisplayFoldListener.class).invoke(manager, foldListener); + } catch (Exception e) { + throw new AssertionError(e); + } + } } From 360f2fea1e66a34fb4a6739887d2efc926dee856 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 30 May 2023 21:25:47 +0200 Subject: [PATCH 1572/2244] Extract AudioCapture creation This will allow to pass capture options without code duplication. --- .../src/main/java/com/genymobile/scrcpy/AudioEncoder.java | 8 ++++---- .../main/java/com/genymobile/scrcpy/AudioRawRecorder.java | 5 +++-- server/src/main/java/com/genymobile/scrcpy/Server.java | 5 +++-- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java b/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java index a1abd71b..108bbaa1 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java @@ -40,6 +40,7 @@ public final class AudioEncoder implements AsyncProcessor { private static final int READ_MS = 5; // milliseconds private static final int READ_SIZE = AudioCapture.millisToBytes(READ_MS); + private final AudioCapture capture; private final Streamer streamer; private final int bitRate; private final List codecOptions; @@ -58,7 +59,8 @@ public final class AudioEncoder implements AsyncProcessor { private boolean ended; - public AudioEncoder(Streamer streamer, int bitRate, List codecOptions, String encoderName) { + public AudioEncoder(AudioCapture capture, Streamer streamer, int bitRate, List codecOptions, String encoderName) { + this.capture = capture; this.streamer = streamer; this.bitRate = bitRate; this.codecOptions = codecOptions; @@ -175,7 +177,6 @@ public final class AudioEncoder implements AsyncProcessor { } MediaCodec mediaCodec = null; - AudioCapture capture = new AudioCapture(); boolean mediaCodecStarted = false; try { @@ -192,10 +193,9 @@ public final class AudioEncoder implements AsyncProcessor { capture.start(); final MediaCodec mediaCodecRef = mediaCodec; - final AudioCapture captureRef = capture; inputThread = new Thread(() -> { try { - inputThread(mediaCodecRef, captureRef); + inputThread(mediaCodecRef, capture); } catch (IOException | InterruptedException e) { Ln.e("Audio capture error", e); } finally { diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java b/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java index 685ac3bd..2fc8c887 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java @@ -8,6 +8,7 @@ import java.nio.ByteBuffer; public final class AudioRawRecorder implements AsyncProcessor { + private final AudioCapture capture; private final Streamer streamer; private Thread thread; @@ -15,7 +16,8 @@ public final class AudioRawRecorder implements AsyncProcessor { private static final int READ_MS = 5; // milliseconds private static final int READ_SIZE = AudioCapture.millisToBytes(READ_MS); - public AudioRawRecorder(Streamer streamer) { + public AudioRawRecorder(AudioCapture capture, Streamer streamer) { + this.capture = capture; this.streamer = streamer; } @@ -29,7 +31,6 @@ public final class AudioRawRecorder implements AsyncProcessor { final ByteBuffer buffer = ByteBuffer.allocateDirect(READ_SIZE); final MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); - AudioCapture capture = new AudioCapture(); try { capture.start(); diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index db993830..616b771e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -136,13 +136,14 @@ public final class Server { if (audio) { AudioCodec audioCodec = options.getAudioCodec(); + AudioCapture audioCapture = new AudioCapture(); Streamer audioStreamer = new Streamer(connection.getAudioFd(), audioCodec, options.getSendCodecMeta(), options.getSendFrameMeta()); AsyncProcessor audioRecorder; if (audioCodec == AudioCodec.RAW) { - audioRecorder = new AudioRawRecorder(audioStreamer); + audioRecorder = new AudioRawRecorder(audioCapture, audioStreamer); } else { - audioRecorder = new AudioEncoder(audioStreamer, options.getAudioBitRate(), options.getAudioCodecOptions(), + audioRecorder = new AudioEncoder(audioCapture, audioStreamer, options.getAudioBitRate(), options.getAudioCodecOptions(), options.getAudioEncoder()); } asyncProcessors.add(audioRecorder); From ff5ffc892fa50993e7b5f9ea9d9790c7e4f29693 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 30 May 2023 21:29:05 +0200 Subject: [PATCH 1573/2244] Add option to select audio source Pass --audio-source=mic to capture the microphone instead of the device audio output. --- app/data/bash-completion/scrcpy | 5 ++++ app/data/zsh-completion/_scrcpy | 1 + app/scrcpy.1 | 6 ++++ app/src/cli.c | 29 ++++++++++++++++++ app/src/options.c | 1 + app/src/options.h | 6 ++++ app/src/scrcpy.c | 1 + app/src/server.c | 4 +++ app/src/server.h | 1 + doc/audio.md | 18 +++++++++++ .../com/genymobile/scrcpy/AudioCapture.java | 16 ++++++---- .../com/genymobile/scrcpy/AudioSource.java | 30 +++++++++++++++++++ .../java/com/genymobile/scrcpy/Options.java | 12 ++++++++ .../java/com/genymobile/scrcpy/Server.java | 2 +- 14 files changed, 125 insertions(+), 7 deletions(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/AudioSource.java diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index 010125fb..8d1f1e13 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -7,6 +7,7 @@ _scrcpy() { --audio-codec= --audio-codec-options= --audio-encoder= + --audio-source= --audio-output-buffer= -b --video-bit-rate= --crop= @@ -86,6 +87,10 @@ _scrcpy() { COMPREPLY=($(compgen -W 'opus aac raw' -- "$cur")) return ;; + --audio-source) + COMPREPLY=($(compgen -W 'output mic' -- "$cur")) + return + ;; --lock-video-orientation) COMPREPLY=($(compgen -W 'unlocked initial 0 1 2 3' -- "$cur")) return diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 4f1f16b9..6e742fd7 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -14,6 +14,7 @@ arguments=( '--audio-codec=[Select the audio codec]:codec:(opus aac raw)' '--audio-codec-options=[Set a list of comma-separated key\:type=value options for the device audio encoder]' '--audio-encoder=[Use a specific MediaCodec audio encoder]' + '--audio-source=[Select the audio source]:source:(output mic)' '--audio-output-buffer=[Configure the size of the SDL audio output buffer (in milliseconds)]' {-b,--video-bit-rate=}'[Encode the video at the given bit-rate]' '--crop=[\[width\:height\:x\:y\] Crop the device screen on the server]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index a30e7db0..04098b9c 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -55,6 +55,12 @@ Use a specific MediaCodec audio encoder (depending on the codec provided by \fB\ The available encoders can be listed by \-\-list\-encoders. +.TP +.BI "\-\-audio\-source " source +Select the audio source (output or mic). + +Default is output. + .TP .BI "\-\-audio\-output\-buffer ms Configure the size of the SDL audio output buffer (in milliseconds). diff --git a/app/src/cli.c b/app/src/cli.c index 01b55406..533e63ca 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -76,6 +76,7 @@ enum { OPT_NO_VIDEO, OPT_NO_AUDIO_PLAYBACK, OPT_NO_VIDEO_PLAYBACK, + OPT_AUDIO_SOURCE, }; struct sc_option { @@ -161,6 +162,13 @@ static const struct sc_option options[] = { "codec provided by --audio-codec).\n" "The available encoders can be listed by --list-encoders.", }, + { + .longopt_id = OPT_AUDIO_SOURCE, + .longopt = "audio-source", + .argdesc = "source", + .text = "Select the audio source (output or mic).\n" + "Default is output.", + }, { .longopt_id = OPT_AUDIO_OUTPUT_BUFFER, .longopt = "audio-output-buffer", @@ -1588,6 +1596,22 @@ parse_audio_codec(const char *optarg, enum sc_codec *codec) { return false; } +static bool +parse_audio_source(const char *optarg, enum sc_audio_source *source) { + if (!strcmp(optarg, "mic")) { + *source = SC_AUDIO_SOURCE_MIC; + return true; + } + + if (!strcmp(optarg, "output")) { + *source = SC_AUDIO_SOURCE_OUTPUT; + return true; + } + + LOGE("Unsupported audio source: %s (expected output or mic)", optarg); + return false; +} + static bool parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], const char *optstring, const struct option *longopts) { @@ -1915,6 +1939,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; } break; + case OPT_AUDIO_SOURCE: + if (!parse_audio_source(optarg, &opts->audio_source)) { + return false; + } + break; default: // getopt prints the error message on stderr return false; diff --git a/app/src/options.c b/app/src/options.c index 49cca969..e1373753 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -14,6 +14,7 @@ const struct scrcpy_options scrcpy_options_default = { .log_level = SC_LOG_LEVEL_INFO, .video_codec = SC_CODEC_H264, .audio_codec = SC_CODEC_OPUS, + .audio_source = SC_AUDIO_SOURCE_OUTPUT, .record_format = SC_RECORD_FORMAT_AUTO, .keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_INJECT, .mouse_input_mode = SC_MOUSE_INPUT_MODE_INJECT, diff --git a/app/src/options.h b/app/src/options.h index dd5d0dad..c33fafef 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -44,6 +44,11 @@ enum sc_codec { SC_CODEC_RAW, }; +enum sc_audio_source { + SC_AUDIO_SOURCE_OUTPUT, + SC_AUDIO_SOURCE_MIC, +}; + enum sc_lock_video_orientation { SC_LOCK_VIDEO_ORIENTATION_UNLOCKED = -1, // lock the current orientation when scrcpy starts @@ -115,6 +120,7 @@ struct scrcpy_options { enum sc_log_level log_level; enum sc_codec video_codec; enum sc_codec audio_codec; + enum sc_audio_source audio_source; enum sc_record_format record_format; enum sc_keyboard_input_mode keyboard_input_mode; enum sc_mouse_input_mode mouse_input_mode; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 4bbc8dca..79007bf2 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -334,6 +334,7 @@ scrcpy(struct scrcpy_options *options) { .log_level = options->log_level, .video_codec = options->video_codec, .audio_codec = options->audio_codec, + .audio_source = options->audio_source, .crop = options->crop, .port_range = options->port_range, .tunnel_host = options->tunnel_host, diff --git a/app/src/server.c b/app/src/server.c index 9c554760..4e70c4d9 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -246,6 +246,10 @@ execute_server(struct sc_server *server, ADD_PARAM("audio_codec=%s", sc_server_get_codec_name(params->audio_codec)); } + if (params->audio_source != SC_AUDIO_SOURCE_OUTPUT) { + assert(params->audio_source == SC_AUDIO_SOURCE_MIC); + ADD_PARAM("audio_source=mic"); + } if (params->max_size) { ADD_PARAM("max_size=%" PRIu16, params->max_size); } diff --git a/app/src/server.h b/app/src/server.h index 31648445..fad44e66 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -26,6 +26,7 @@ struct sc_server_params { enum sc_log_level log_level; enum sc_codec video_codec; enum sc_codec audio_codec; + enum sc_audio_source audio_source; const char *crop; const char *video_codec_options; const char *audio_codec_options; diff --git a/doc/audio.md b/doc/audio.md index a437005c..40dd6036 100644 --- a/doc/audio.md +++ b/doc/audio.md @@ -41,6 +41,24 @@ interesting to add [buffering](#buffering) to minimize glitches: scrcpy --no-video --audio-buffer=200 ``` +## Source + +By default, the device audio output is forwarded. + +It is possible to capture the device microphone instead: + +``` +scrcpy --audio-source=mic +``` + +For example, to use the device as a dictaphone and record a capture directly on +the computer: + +``` +scrcpy --audio-source=mic --no-video --no-audio-playback --record=file.opus +``` + + ## Codec The audio codec can be selected. The possible values are `opus` (default), `aac` diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java b/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java index b8fc076b..7b20cce4 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java @@ -10,7 +10,6 @@ import android.media.AudioFormat; import android.media.AudioRecord; import android.media.AudioTimestamp; import android.media.MediaCodec; -import android.media.MediaRecorder; import android.os.Build; import android.os.SystemClock; @@ -18,7 +17,6 @@ import java.nio.ByteBuffer; public final class AudioCapture { - public static final int SOURCE = MediaRecorder.AudioSource.REMOTE_SUBMIX; public static final int SAMPLE_RATE = 48000; public static final int CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_STEREO; public static final int CHANNELS = 2; @@ -26,12 +24,18 @@ public final class AudioCapture { public static final int ENCODING = AudioFormat.ENCODING_PCM_16BIT; public static final int BYTES_PER_SAMPLE = 2; + private final int audioSource; + private AudioRecord recorder; private final AudioTimestamp timestamp = new AudioTimestamp(); private long previousPts = 0; private long nextPts = 0; + public AudioCapture(AudioSource audioSource) { + this.audioSource = audioSource.value(); + } + public static int millisToBytes(int millis) { return SAMPLE_RATE * CHANNELS * BYTES_PER_SAMPLE * millis / 1000; } @@ -46,13 +50,13 @@ public final class AudioCapture { @TargetApi(Build.VERSION_CODES.M) @SuppressLint({"WrongConstant", "MissingPermission"}) - private static AudioRecord createAudioRecord() { + private static AudioRecord createAudioRecord(int audioSource) { AudioRecord.Builder builder = new AudioRecord.Builder(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { // On older APIs, Workarounds.fillAppInfo() must be called beforehand builder.setContext(FakeContext.get()); } - builder.setAudioSource(SOURCE); + builder.setAudioSource(audioSource); builder.setAudioFormat(createAudioFormat()); int minBufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, ENCODING); // This buffer size does not impact latency @@ -100,12 +104,12 @@ public final class AudioCapture { private void startRecording() { try { - recorder = createAudioRecord(); + recorder = createAudioRecord(audioSource); } catch (NullPointerException e) { // Creating an AudioRecord using an AudioRecord.Builder does not work on Vivo phones: // - // - - recorder = Workarounds.createAudioRecord(SOURCE, SAMPLE_RATE, CHANNEL_CONFIG, CHANNELS, CHANNEL_MASK, ENCODING); + recorder = Workarounds.createAudioRecord(audioSource, SAMPLE_RATE, CHANNEL_CONFIG, CHANNELS, CHANNEL_MASK, ENCODING); } recorder.startRecording(); } diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioSource.java b/server/src/main/java/com/genymobile/scrcpy/AudioSource.java new file mode 100644 index 00000000..466ea297 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/AudioSource.java @@ -0,0 +1,30 @@ +package com.genymobile.scrcpy; + +import android.media.MediaRecorder; + +public enum AudioSource { + OUTPUT("output", MediaRecorder.AudioSource.REMOTE_SUBMIX), + MIC("mic", MediaRecorder.AudioSource.MIC); + + private final String name; + private final int value; + + AudioSource(String name, int value) { + this.name = name; + this.value = value; + } + + int value() { + return value; + } + + static AudioSource findByName(String name) { + for (AudioSource audioSource : AudioSource.values()) { + if (name.equals(audioSource.name)) { + return audioSource; + } + } + + return null; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 7bd94cb3..23d4e383 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 maxSize; private VideoCodec videoCodec = VideoCodec.H264; private AudioCodec audioCodec = AudioCodec.OPUS; + private AudioSource audioSource = AudioSource.OUTPUT; private int videoBitRate = 8000000; private int audioBitRate = 128000; private int maxFps; @@ -72,6 +73,10 @@ public class Options { return audioCodec; } + public AudioSource getAudioSource() { + return audioSource; + } + public int getVideoBitRate() { return videoBitRate; } @@ -225,6 +230,13 @@ public class Options { } options.audioCodec = audioCodec; break; + case "audio_source": + AudioSource audioSource = AudioSource.findByName(value); + if (audioSource == null) { + throw new IllegalArgumentException("Audio source " + value + " not supported"); + } + options.audioSource = audioSource; + break; case "max_size": options.maxSize = Integer.parseInt(value) & ~7; // multiple of 8 break; diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 616b771e..214ac27d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -136,7 +136,7 @@ public final class Server { if (audio) { AudioCodec audioCodec = options.getAudioCodec(); - AudioCapture audioCapture = new AudioCapture(); + AudioCapture audioCapture = new AudioCapture(options.getAudioSource()); Streamer audioStreamer = new Streamer(connection.getAudioFd(), audioCodec, options.getSendCodecMeta(), options.getSendFrameMeta()); AsyncProcessor audioRecorder; From fc52b2450333beb94c0dc2f94d01d4de13afeec5 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 1 Jun 2023 09:52:35 +0200 Subject: [PATCH 1574/2244] Reorder options in alphabetical order Fix the options order, using the short option as key first (if any) in all cases for consistency. --- app/data/bash-completion/scrcpy | 16 ++--- app/data/zsh-completion/_scrcpy | 16 ++--- app/scrcpy.1 | 80 ++++++++++++------------ app/src/cli.c | 104 ++++++++++++++++---------------- 4 files changed, 108 insertions(+), 108 deletions(-) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index 8d1f1e13..de298056 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -16,9 +16,9 @@ _scrcpy() { --display= --display-buffer= -e --select-tcpip + -f --fullscreen --force-adb-forward --forward-all-clicks - -f --fullscreen -K --hid-keyboard -h --help --legacy-paste @@ -26,16 +26,16 @@ _scrcpy() { --list-encoders --lock-video-orientation --lock-video-orientation= - --max-fps= - -M --hid-mouse -m --max-size= + -M --hid-mouse + --max-fps= + -n --no-control + -N --no-playback --no-audio --no-audio-playback --no-cleanup --no-clipboard-autosync --no-downsize-on-error - -n --no-control - -N --no-playback --no-key-repeat --no-mipmaps --no-power-on @@ -47,15 +47,15 @@ _scrcpy() { --prefer-text --print-fps --push-target= - --raw-key-events -r --record= + --raw-key-events --record-format= --render-driver= --require-audio --rotation= -s --serial= - --shortcut-mod= -S --turn-screen-off + --shortcut-mod= -t --show-touches --tcpip --tcpip= @@ -63,8 +63,8 @@ _scrcpy() { --tunnel-port= --v4l2-buffer= --v4l2-sink= - -V --verbosity= -v --version + -V --verbosity= --video-codec= --video-codec-options= --video-encoder= diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 6e742fd7..17e30dc5 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -23,25 +23,25 @@ arguments=( '--display=[Specify the display id to mirror]' '--display-buffer=[Add a buffering delay \(in milliseconds\) before displaying]' {-e,--select-tcpip}'[Use TCP/IP device]' + {-f,--fullscreen}'[Start in fullscreen]' '--force-adb-forward[Do not attempt to use \"adb reverse\" to connect to the device]' '--forward-all-clicks[Forward clicks to device]' - {-f,--fullscreen}'[Start in fullscreen]' {-K,--hid-keyboard}'[Simulate a physical keyboard by using HID over AOAv2]' {-h,--help}'[Print the help]' '--legacy-paste[Inject computer clipboard text as a sequence of key events on Ctrl+v]' '--list-displays[List displays available on the device]' '--list-encoders[List video and audio encoders available on the device]' '--lock-video-orientation=[Lock video orientation]:orientation:(unlocked initial 0 1 2 3)' - '--max-fps=[Limit the frame rate of screen capture]' - {-M,--hid-mouse}'[Simulate a physical mouse by using HID over AOAv2]' {-m,--max-size=}'[Limit both the width and height of the video to value]' + {-M,--hid-mouse}'[Simulate a physical mouse by using HID over AOAv2]' + '--max-fps=[Limit the frame rate of screen capture]' + {-n,--no-control}'[Disable device control \(mirror the device in read only\)]' + {-N,--no-playback}'[Disable video and audio playback]' '--no-audio[Disable audio forwarding]' '--no-audio-playback[Disable audio playback]' '--no-cleanup[Disable device cleanup actions on exit]' '--no-clipboard-autosync[Disable automatic clipboard synchronization]' '--no-downsize-on-error[Disable lowering definition on MediaCodec error]' - {-n,--no-control}'[Disable device control \(mirror the device in read only\)]' - {-N,--no-playback}'[Disable video and audio playback]' '--no-key-repeat[Do not forward repeated key events when a key is held down]' '--no-mipmaps[Disable the generation of mipmaps]' '--no-power-on[Do not power on the device on start]' @@ -53,23 +53,23 @@ arguments=( '--prefer-text[Inject alpha characters and space as text events instead of key events]' '--print-fps[Start FPS counter, to print frame logs to the console]' '--push-target=[Set the target directory for pushing files to the device by drag and drop]' - '--raw-key-events[Inject key events for all input keys, and ignore text events]' {-r,--record=}'[Record screen to file]:record file:_files' + '--raw-key-events[Inject key events for all input keys, and ignore text events]' '--record-format=[Force recording format]:format:(mp4 mkv)' '--render-driver=[Request SDL to use the given render driver]:driver name:(direct3d opengl opengles2 opengles metal software)' '--require-audio=[Make scrcpy fail if audio is enabled but does not work]' '--rotation=[Set the initial display rotation]:rotation values:(0 1 2 3)' {-s,--serial=}'[The device serial number \(mandatory for multiple devices only\)]:serial:($("${ADB-adb}" devices | awk '\''$2 == "device" {print $1}'\''))' - '--shortcut-mod=[\[key1,key2+key3,...\] Specify the modifiers to use for scrcpy shortcuts]:shortcut mod:(lctrl rctrl lalt ralt lsuper rsuper)' {-S,--turn-screen-off}'[Turn the device screen off immediately]' + '--shortcut-mod=[\[key1,key2+key3,...\] Specify the modifiers to use for scrcpy shortcuts]:shortcut mod:(lctrl rctrl lalt ralt lsuper rsuper)' {-t,--show-touches}'[Show physical touches]' '--tcpip[\(optional \[ip\:port\]\) Configure and connect the device over TCP/IP]' '--tunnel-host=[Set the IP address of the adb tunnel to reach the scrcpy server]' '--tunnel-port=[Set the TCP port of the adb tunnel to reach the scrcpy server]' '--v4l2-buffer=[Add a buffering delay \(in milliseconds\) before pushing frames]' '--v4l2-sink=[\[\/dev\/videoN\] Output to v4l2loopback device]' - {-V,--verbosity=}'[Set the log level]:verbosity:(verbose debug info warn error)' {-v,--version}'[Print the version of scrcpy]' + {-V,--verbosity=}'[Set the log level]:verbosity:(verbose debug info warn error)' '--video-codec=[Select the video codec]:codec:(h264 h265 av1)' '--video-codec-options=[Set a list of comma-separated key\:type=value options for the device video encoder]' '--video-encoder=[Use a specific MediaCodec video encoder]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 04098b9c..9b08f182 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -113,6 +113,10 @@ Use TCP/IP device (if there is exactly one, like adb -e). Also see \fB\-d\fR (\fB\-\-select\-usb\fR). +.TP +.B \-f, \-\-fullscreen +Start in fullscreen. + .TP .B \-\-force\-adb\-forward Do not attempt to use "adb reverse" to connect to the device. @@ -121,10 +125,6 @@ Do not attempt to use "adb reverse" to connect to the device. .B \-\-forward\-all\-clicks By default, right-click triggers BACK (or POWER on) and middle-click triggers HOME. This option disables these shortcuts and forward the clicks to the device instead. -.TP -.B \-f, \-\-fullscreen -Start in fullscreen. - .TP .B \-h, \-\-help Print this help. @@ -167,10 +167,6 @@ Default is "unlocked". Passing the option without argument is equivalent to passing "initial". -.TP -.BI "\-\-max\-fps " value -Limit the framerate of screen capture (officially supported since Android 10, but may work on earlier versions). - .TP .BI "\-m, \-\-max\-size " value Limit both the width and height of the video to \fIvalue\fR. The other dimension is computed so that the device aspect\-ratio is preserved. @@ -189,6 +185,18 @@ It may only work over USB. Also see \fB\-\-hid\-keyboard\fR. +.TP +.BI "\-\-max\-fps " value +Limit the framerate of screen capture (officially supported since Android 10, but may work on earlier versions). + +.TP +.B \-n, \-\-no\-control +Disable device control (mirror the device in read\-only). + +.TP +.B \-N, \-\-no\-playback +Disable video and audio playback on the computer (equivalent to --no-video-playback --no-audio-playback). + .TP .B \-\-no\-audio Disable audio forwarding. @@ -215,14 +223,6 @@ By default, on MediaCodec error, scrcpy automatically tries again with a lower d This option disables this behavior. -.TP -.B \-n, \-\-no\-control -Disable device control (mirror the device in read\-only). - -.TP -.B \-N, \-\-no\-playback -Disable video and audio playback on the computer (equivalent to --no-video-playback --no-audio-playback). - .TP .B \-\-no\-key\-repeat Do not forward repeated key events when a key is held down. @@ -284,10 +284,6 @@ Set the target directory for pushing files to the device by drag & drop. It is p Default is "/sdcard/Download/". -.TP -.B \-\-raw\-key\-events -Inject key events for all input keys, and ignore text events. - .TP .BI "\-r, \-\-record " file Record screen to @@ -297,6 +293,10 @@ The format is determined by the .B \-\-record\-format option if set, or by the file extension (.mp4 or .mkv). +.TP +.B \-\-raw\-key\-events +Inject key events for all input keys, and ignore text events. + .TP .BI "\-\-record\-format " format Force recording format (either mp4 or mkv). @@ -322,6 +322,10 @@ 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 +.B \-S, \-\-turn\-screen\-off +Turn the device screen off immediately. + .TP .BI "\-\-shortcut\-mod " key\fR[+...]][,...] Specify the modifiers to use for scrcpy shortcuts. Possible keys are "lctrl", "rctrl", "lalt", "ralt", "lsuper" and "rsuper". @@ -332,6 +336,12 @@ For example, to use either LCtrl+LAlt or LSuper for scrcpy shortcuts, pass "lctr Default is "lalt,lsuper" (left-Alt or left-Super). +.TP +.B \-t, \-\-show\-touches +Enable "show touches" on start, restore the initial value on exit. + +It only shows physical touches (not clicks from scrcpy). + .TP .BI "\-\-tcpip\fR[=\fIip\fR[:\fIport\fR]] Configure and reconnect the device over TCP/IP. @@ -340,16 +350,6 @@ If a destination address is provided, then scrcpy connects to this address befor If no destination address is provided, then scrcpy attempts to find the IP address and adb port of the current device (typically connected over USB), enables TCP/IP mode if necessary, then connects to this address before starting. -.TP -.B \-S, \-\-turn\-screen\-off -Turn the device screen off immediately. - -.TP -.B \-t, \-\-show\-touches -Enable "show touches" on start, restore the initial value on exit. - -It only shows physical touches (not clicks from scrcpy). - .TP .BI "\-\-tunnel\-host " ip Set the IP address of the adb tunnel to reach the scrcpy server. This option automatically enables --force-adb-forward. @@ -362,6 +362,16 @@ Set the TCP port of the adb tunnel to reach the scrcpy server. This option autom Default is 0 (not forced): the local port used for establishing the tunnel will be used. +.TP +.B \-v, \-\-version +Print the version of scrcpy. + +.TP +.BI "\-V, \-\-verbosity " value +Set the log level ("verbose", "debug", "info", "warn" or "error"). + +Default is "info" for release builds, "debug" for debug builds. + .TP .BI "\-\-v4l2-sink " /dev/videoN Output to v4l2loopback device. @@ -376,16 +386,6 @@ This option is similar to \fB\-\-display\-buffer\fR, but specific to V4L2 sink. Default is 0 (no buffering). -.TP -.BI "\-V, \-\-verbosity " value -Set the log level ("verbose", "debug", "info", "warn" or "error"). - -Default is "info" for release builds, "debug" for debug builds. - -.TP -.B \-v, \-\-version -Print the version of scrcpy. - .TP .BI "\-\-video\-codec " name Select a video codec (h264, h265 or av1). diff --git a/app/src/cli.c b/app/src/cli.c index 533e63ca..eb784ebc 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -257,6 +257,11 @@ static const struct sc_option options[] = { .longopt = "encoder", .argdesc = "name", }, + { + .shortopt = 'f', + .longopt = "fullscreen", + .text = "Start in fullscreen.", + }, { .longopt_id = OPT_FORCE_ADB_FORWARD, .longopt = "force-adb-forward", @@ -270,11 +275,6 @@ static const struct sc_option options[] = { "middle-click triggers HOME. This option disables these " "shortcuts and forwards the clicks to the device instead.", }, - { - .shortopt = 'f', - .longopt = "fullscreen", - .text = "Start in fullscreen.", - }, { .shortopt = 'K', .longopt = "hid-keyboard", @@ -330,11 +330,13 @@ static const struct sc_option options[] = { "\"initial\".", }, { - .longopt_id = OPT_MAX_FPS, - .longopt = "max-fps", + .shortopt = 'm', + .longopt = "max-size", .argdesc = "value", - .text = "Limit the frame rate of screen capture (officially supported " - "since Android 10, but may work on earlier versions).", + .text = "Limit both the width and height of the video to value. The " + "other dimension is computed so that the device aspect-ratio " + "is preserved.\n" + "Default is 0 (unlimited).", }, { .shortopt = 'M', @@ -348,13 +350,22 @@ static const struct sc_option options[] = { "Also see --hid-keyboard.", }, { - .shortopt = 'm', - .longopt = "max-size", + .longopt_id = OPT_MAX_FPS, + .longopt = "max-fps", .argdesc = "value", - .text = "Limit both the width and height of the video to value. The " - "other dimension is computed so that the device aspect-ratio " - "is preserved.\n" - "Default is 0 (unlimited).", + .text = "Limit the frame rate of screen capture (officially supported " + "since Android 10, but may work on earlier versions).", + }, + { + .shortopt = 'n', + .longopt = "no-control", + .text = "Disable device control (mirror the device in read-only).", + }, + { + .shortopt = 'N', + .longopt = "no-playback", + .text = "Disable video and audio playback on the computer (equivalent " + "to --no-video-playback --no-audio-playback).", }, { .longopt_id = OPT_NO_AUDIO, @@ -390,17 +401,6 @@ static const struct sc_option options[] = { "again with a lower definition.\n" "This option disables this behavior.", }, - { - .shortopt = 'n', - .longopt = "no-control", - .text = "Disable device control (mirror the device in read-only).", - }, - { - .shortopt = 'N', - .longopt = "no-playback", - .text = "Disable video and audio playback on the computer (equivalent " - "to --no-video-playback --no-audio-playback).", - }, { // deprecated .longopt_id = OPT_NO_DISPLAY, @@ -484,11 +484,6 @@ static const struct sc_option options[] = { "drag & drop. It is passed as is to \"adb push\".\n" "Default is \"/sdcard/Download/\".", }, - { - .longopt_id = OPT_RAW_KEY_EVENTS, - .longopt = "raw-key-events", - .text = "Inject key events for all input keys, and ignore text events." - }, { .shortopt = 'r', .longopt = "record", @@ -497,6 +492,11 @@ static const struct sc_option options[] = { "The format is determined by the --record-format option if " "set, or by the file extension (.mp4 or .mkv).", }, + { + .longopt_id = OPT_RAW_KEY_EVENTS, + .longopt = "raw-key-events", + .text = "Inject key events for all input keys, and ignore text events." + }, { .longopt_id = OPT_RECORD_FORMAT, .longopt = "record-format", @@ -535,6 +535,11 @@ static const struct sc_option options[] = { .text = "The device serial number. Mandatory only if several devices " "are connected to adb.", }, + { + .shortopt = 'S', + .longopt = "turn-screen-off", + .text = "Turn the device screen off immediately.", + }, { .longopt_id = OPT_SHORTCUT_MOD, .longopt = "shortcut-mod", @@ -548,11 +553,6 @@ static const struct sc_option options[] = { "shortcuts, pass \"lctrl+lalt,lsuper\".\n" "Default is \"lalt,lsuper\" (left-Alt or left-Super).", }, - { - .shortopt = 'S', - .longopt = "turn-screen-off", - .text = "Turn the device screen off immediately.", - }, { .shortopt = 't', .longopt = "show-touches", @@ -593,6 +593,22 @@ static const struct sc_option options[] = { "Default is 0 (not forced): the local port used for " "establishing the tunnel will be used.", }, + { + .shortopt = 'v', + .longopt = "version", + .text = "Print the version of scrcpy.", + }, + { + .shortopt = 'V', + .longopt = "verbosity", + .argdesc = "value", + .text = "Set the log level (verbose, debug, info, warn or error).\n" +#ifndef NDEBUG + "Default is debug.", +#else + "Default is info.", +#endif + }, { .longopt_id = OPT_V4L2_SINK, .longopt = "v4l2-sink", @@ -613,22 +629,6 @@ static const struct sc_option options[] = { "Default is 0 (no buffering).\n" "This option is only available on Linux.", }, - { - .shortopt = 'V', - .longopt = "verbosity", - .argdesc = "value", - .text = "Set the log level (verbose, debug, info, warn or error).\n" -#ifndef NDEBUG - "Default is debug.", -#else - "Default is info.", -#endif - }, - { - .shortopt = 'v', - .longopt = "version", - .text = "Print the version of scrcpy.", - }, { .longopt_id = OPT_VIDEO_CODEC, .longopt = "video-codec", From 2aec7b4c9d8637f16790a435e14ee020e49b5636 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 2 Jun 2023 08:59:12 +0200 Subject: [PATCH 1575/2244] Mention how to interrupt scrcpy without video There is no window to close if video playback is disabled. --- doc/audio.md | 3 ++- doc/video.md | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/audio.md b/doc/audio.md index 40dd6036..c16afd7f 100644 --- a/doc/audio.md +++ b/doc/audio.md @@ -30,8 +30,9 @@ To disable only the audio playback, see [no playback](video.md#no-playback). To play audio only, disable the video: -``` +```bash scrcpy --no-video +# interrupt with Ctrl+C ``` Without video, the audio latency is typically not criticial, so it might be diff --git a/doc/video.md b/doc/video.md index e411cf0d..5e7344d9 100644 --- a/doc/video.md +++ b/doc/video.md @@ -168,6 +168,7 @@ the computer. This option is useful when [recording](recording.md) or when ```bash scrcpy --v4l2-sink=/dev/video2 --no-playback scrcpy --record=file.mkv --no-playback +# interrupt with Ctrl+C ``` It is also possible to disable video and audio playback separately: From 379caf8551cdf1f2603decd9b44a8771c3b78475 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 3 Jun 2023 15:05:23 +0200 Subject: [PATCH 1576/2244] Use a single condvar in recorder The sc_cond_wait() in sc_recorder_process_header() needs to be notified of changes to video_init/audio_init (protected by stream_cond) and video_queue/audio_queue (protected by queue_cond). Use only one condition variable to simplify. --- app/src/recorder.c | 33 ++++++++++++--------------------- app/src/recorder.h | 3 +-- 2 files changed, 13 insertions(+), 23 deletions(-) diff --git a/app/src/recorder.c b/app/src/recorder.c index be1cbe71..adb1059d 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -181,7 +181,7 @@ sc_recorder_process_header(struct sc_recorder *recorder) { while (!recorder->stopped && (!recorder->video_init || !recorder->audio_init || sc_recorder_has_empty_queues(recorder))) { - sc_cond_wait(&recorder->stream_cond, &recorder->mutex); + sc_cond_wait(&recorder->cond, &recorder->mutex); } if (recorder->video && sc_vecdeque_is_empty(&recorder->video_queue)) { @@ -289,7 +289,7 @@ sc_recorder_process_packets(struct sc_recorder *recorder) { // A new packet may be assigned to audio_pkt and be processed break; } - sc_cond_wait(&recorder->queue_cond, &recorder->mutex); + sc_cond_wait(&recorder->cond, &recorder->mutex); } // If stopped is set, continue to process the remaining events (to @@ -507,7 +507,7 @@ sc_recorder_video_packet_sink_open(struct sc_packet_sink *sink, recorder->video_stream_index = stream->index; recorder->video_init = true; - sc_cond_signal(&recorder->stream_cond); + sc_cond_signal(&recorder->cond); sc_mutex_unlock(&recorder->mutex); return true; @@ -522,7 +522,7 @@ sc_recorder_video_packet_sink_close(struct sc_packet_sink *sink) { sc_mutex_lock(&recorder->mutex); // EOS also stops the recorder recorder->stopped = true; - sc_cond_signal(&recorder->queue_cond); + sc_cond_signal(&recorder->cond); sc_mutex_unlock(&recorder->mutex); } @@ -557,7 +557,7 @@ sc_recorder_video_packet_sink_push(struct sc_packet_sink *sink, return false; } - sc_cond_signal(&recorder->queue_cond); + sc_cond_signal(&recorder->cond); sc_mutex_unlock(&recorder->mutex); return true; @@ -588,7 +588,7 @@ sc_recorder_audio_packet_sink_open(struct sc_packet_sink *sink, recorder->audio_stream_index = stream->index; recorder->audio_init = true; - sc_cond_signal(&recorder->stream_cond); + sc_cond_signal(&recorder->cond); sc_mutex_unlock(&recorder->mutex); return true; @@ -604,7 +604,7 @@ sc_recorder_audio_packet_sink_close(struct sc_packet_sink *sink) { sc_mutex_lock(&recorder->mutex); // EOS also stops the recorder recorder->stopped = true; - sc_cond_signal(&recorder->queue_cond); + sc_cond_signal(&recorder->cond); sc_mutex_unlock(&recorder->mutex); } @@ -640,7 +640,7 @@ sc_recorder_audio_packet_sink_push(struct sc_packet_sink *sink, return false; } - sc_cond_signal(&recorder->queue_cond); + sc_cond_signal(&recorder->cond); sc_mutex_unlock(&recorder->mutex); return true; @@ -658,7 +658,7 @@ sc_recorder_audio_packet_sink_disable(struct sc_packet_sink *sink) { sc_mutex_lock(&recorder->mutex); recorder->audio = false; recorder->audio_init = true; - sc_cond_signal(&recorder->stream_cond); + sc_cond_signal(&recorder->cond); sc_mutex_unlock(&recorder->mutex); } @@ -677,16 +677,11 @@ sc_recorder_init(struct sc_recorder *recorder, const char *filename, goto error_free_filename; } - ok = sc_cond_init(&recorder->queue_cond); + ok = sc_cond_init(&recorder->cond); if (!ok) { goto error_mutex_destroy; } - ok = sc_cond_init(&recorder->stream_cond); - if (!ok) { - goto error_queue_cond_destroy; - } - assert(video || audio); recorder->video = video; recorder->audio = audio; @@ -730,8 +725,6 @@ sc_recorder_init(struct sc_recorder *recorder, const char *filename, return true; -error_queue_cond_destroy: - sc_cond_destroy(&recorder->queue_cond); error_mutex_destroy: sc_mutex_destroy(&recorder->mutex); error_free_filename: @@ -756,8 +749,7 @@ void sc_recorder_stop(struct sc_recorder *recorder) { sc_mutex_lock(&recorder->mutex); recorder->stopped = true; - sc_cond_signal(&recorder->queue_cond); - sc_cond_signal(&recorder->stream_cond); + sc_cond_signal(&recorder->cond); sc_mutex_unlock(&recorder->mutex); } @@ -768,8 +760,7 @@ sc_recorder_join(struct sc_recorder *recorder) { void sc_recorder_destroy(struct sc_recorder *recorder) { - sc_cond_destroy(&recorder->stream_cond); - sc_cond_destroy(&recorder->queue_cond); + sc_cond_destroy(&recorder->cond); sc_mutex_destroy(&recorder->mutex); free(recorder->filename); } diff --git a/app/src/recorder.h b/app/src/recorder.h index 9c6cd4f9..24f0070f 100644 --- a/app/src/recorder.h +++ b/app/src/recorder.h @@ -35,14 +35,13 @@ struct sc_recorder { sc_thread thread; sc_mutex mutex; - sc_cond queue_cond; + sc_cond cond; // set on sc_recorder_stop(), packet_sink close or recording failure bool stopped; struct sc_recorder_queue video_queue; struct sc_recorder_queue audio_queue; // wake up the recorder thread once the video or audio codec is known - sc_cond stream_cond; bool video_init; bool audio_init; From 9d3c656414ac096ae30e42c22671c72a6bd491af Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 3 Jun 2023 15:09:46 +0200 Subject: [PATCH 1577/2244] Fix recorder waiting when stream disabled In the recorder, if the video or audio stream is disabled, do not wait for its initialization (it will never happen) to process the header. In that case (scrcpy --no-audio --record=file.mp4), this caused the whole content to be buffered in memory, and written only on exit. --- app/src/recorder.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/recorder.c b/app/src/recorder.c index adb1059d..204bf835 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -178,9 +178,10 @@ static bool sc_recorder_process_header(struct sc_recorder *recorder) { sc_mutex_lock(&recorder->mutex); - while (!recorder->stopped && (!recorder->video_init - || !recorder->audio_init - || sc_recorder_has_empty_queues(recorder))) { + while (!recorder->stopped && + ((recorder->video && !recorder->video_init) + || (recorder->audio && !recorder->audio_init) + || sc_recorder_has_empty_queues(recorder))) { sc_cond_wait(&recorder->cond, &recorder->mutex); } From 9ca554ca417b3f92f8dc1603ed2100706f9ba578 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 3 Jun 2023 14:41:40 +0200 Subject: [PATCH 1578/2244] Extract stream-specific structure in recorder For now, it only contains the stream index, but more fields will be added. --- app/src/recorder.c | 37 ++++++++++++++++++++----------------- app/src/recorder.h | 8 ++++++-- 2 files changed, 26 insertions(+), 19 deletions(-) diff --git a/app/src/recorder.c b/app/src/recorder.c index 204bf835..df1b1799 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -96,23 +96,21 @@ sc_recorder_rescale_packet(AVStream *stream, AVPacket *packet) { } static bool -sc_recorder_write_stream(struct sc_recorder *recorder, int stream_index, - AVPacket *packet) { - AVStream *stream = recorder->ctx->streams[stream_index]; +sc_recorder_write_stream(struct sc_recorder *recorder, + struct sc_recorder_stream *st, AVPacket *packet) { + AVStream *stream = recorder->ctx->streams[st->index]; sc_recorder_rescale_packet(stream, packet); return av_interleaved_write_frame(recorder->ctx, packet) >= 0; } static inline bool sc_recorder_write_video(struct sc_recorder *recorder, AVPacket *packet) { - return sc_recorder_write_stream(recorder, recorder->video_stream_index, - packet); + return sc_recorder_write_stream(recorder, &recorder->video_stream, packet); } static inline bool sc_recorder_write_audio(struct sc_recorder *recorder, AVPacket *packet) { - return sc_recorder_write_stream(recorder, recorder->audio_stream_index, - packet); + return sc_recorder_write_stream(recorder, &recorder->audio_stream, packet); } static bool @@ -215,9 +213,9 @@ sc_recorder_process_header(struct sc_recorder *recorder) { goto end; } - assert(recorder->video_stream_index >= 0); + assert(recorder->video_stream.index >= 0); AVStream *video_stream = - recorder->ctx->streams[recorder->video_stream_index]; + recorder->ctx->streams[recorder->video_stream.index]; bool ok = sc_recorder_set_extradata(video_stream, video_pkt); if (!ok) { goto end; @@ -230,9 +228,9 @@ sc_recorder_process_header(struct sc_recorder *recorder) { goto end; } - assert(recorder->audio_stream_index >= 0); + assert(recorder->audio_stream.index >= 0); AVStream *audio_stream = - recorder->ctx->streams[recorder->audio_stream_index]; + recorder->ctx->streams[recorder->audio_stream.index]; bool ok = sc_recorder_set_extradata(audio_stream, audio_pkt); if (!ok) { goto end; @@ -505,7 +503,7 @@ sc_recorder_video_packet_sink_open(struct sc_packet_sink *sink, return false; } - recorder->video_stream_index = stream->index; + recorder->video_stream.index = stream->index; recorder->video_init = true; sc_cond_signal(&recorder->cond); @@ -549,7 +547,7 @@ sc_recorder_video_packet_sink_push(struct sc_packet_sink *sink, return false; } - rec->stream_index = recorder->video_stream_index; + rec->stream_index = recorder->video_stream.index; bool ok = sc_vecdeque_push(&recorder->video_queue, rec); if (!ok) { @@ -586,7 +584,7 @@ sc_recorder_audio_packet_sink_open(struct sc_packet_sink *sink, return false; } - recorder->audio_stream_index = stream->index; + recorder->audio_stream.index = stream->index; recorder->audio_init = true; sc_cond_signal(&recorder->cond); @@ -632,7 +630,7 @@ sc_recorder_audio_packet_sink_push(struct sc_packet_sink *sink, return false; } - rec->stream_index = recorder->audio_stream_index; + rec->stream_index = recorder->audio_stream.index; bool ok = sc_vecdeque_push(&recorder->audio_queue, rec); if (!ok) { @@ -663,6 +661,11 @@ sc_recorder_audio_packet_sink_disable(struct sc_packet_sink *sink) { sc_mutex_unlock(&recorder->mutex); } +static void +sc_recorder_stream_init(struct sc_recorder_stream *stream) { + stream->index = -1; +} + bool sc_recorder_init(struct sc_recorder *recorder, const char *filename, enum sc_record_format format, bool video, bool audio, @@ -694,8 +697,8 @@ sc_recorder_init(struct sc_recorder *recorder, const char *filename, recorder->video_init = false; recorder->audio_init = false; - recorder->video_stream_index = -1; - recorder->audio_stream_index = -1; + sc_recorder_stream_init(&recorder->video_stream); + sc_recorder_stream_init(&recorder->audio_stream); recorder->format = format; diff --git a/app/src/recorder.h b/app/src/recorder.h index 24f0070f..a40b4984 100644 --- a/app/src/recorder.h +++ b/app/src/recorder.h @@ -14,6 +14,10 @@ struct sc_recorder_queue SC_VECDEQUE(AVPacket *); +struct sc_recorder_stream { + int index; +}; + struct sc_recorder { struct sc_packet_sink video_packet_sink; struct sc_packet_sink audio_packet_sink; @@ -45,8 +49,8 @@ struct sc_recorder { bool video_init; bool audio_init; - int video_stream_index; - int audio_stream_index; + struct sc_recorder_stream video_stream; + struct sc_recorder_stream audio_stream; const struct sc_recorder_callbacks *cbs; void *cbs_userdata; From 323ea2f1d947f6e8917b66db797380a739cd261d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 3 Jun 2023 14:52:53 +0200 Subject: [PATCH 1579/2244] Fix PTS when not monotonically increasing Some decoders fail to guarantee that PTS is strictly monotonically increasing. Fix the (rescaled) PTS when it does not respect this constraint. Fixes #4054 --- app/src/recorder.c | 10 ++++++++++ app/src/recorder.h | 1 + 2 files changed, 11 insertions(+) diff --git a/app/src/recorder.c b/app/src/recorder.c index df1b1799..23c8b497 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -100,6 +100,15 @@ sc_recorder_write_stream(struct sc_recorder *recorder, struct sc_recorder_stream *st, AVPacket *packet) { AVStream *stream = recorder->ctx->streams[st->index]; sc_recorder_rescale_packet(stream, packet); + if (st->last_pts != AV_NOPTS_VALUE && packet->pts <= st->last_pts) { + LOGW("Fixing PTS non monotonically increasing in stream %d " + "(%" PRIi64 " >= %" PRIi64 ")", + st->index, st->last_pts, packet->pts); + packet->pts = ++st->last_pts; + packet->dts = packet->pts; + } else { + st->last_pts = packet->pts; + } return av_interleaved_write_frame(recorder->ctx, packet) >= 0; } @@ -664,6 +673,7 @@ sc_recorder_audio_packet_sink_disable(struct sc_packet_sink *sink) { static void sc_recorder_stream_init(struct sc_recorder_stream *stream) { stream->index = -1; + stream->last_pts = AV_NOPTS_VALUE; } bool diff --git a/app/src/recorder.h b/app/src/recorder.h index a40b4984..47fd3f21 100644 --- a/app/src/recorder.h +++ b/app/src/recorder.h @@ -16,6 +16,7 @@ struct sc_recorder_queue SC_VECDEQUE(AVPacket *); struct sc_recorder_stream { int index; + int64_t last_pts; }; struct sc_recorder { From 888a5aae7d90e6aba969baabb4c951b4ab9f4a7f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 4 Jun 2023 18:40:55 +0200 Subject: [PATCH 1580/2244] Fix typo in recording documentation The option is --record, not --record-file. --- doc/recording.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/recording.md b/doc/recording.md index 6f721062..02e3cfc8 100644 --- a/doc/recording.md +++ b/doc/recording.md @@ -17,7 +17,7 @@ To record only the audio: ```bash scrcpy --no-video --record=file.opus -scrcpy --no-video --audio-codec=aac --record-file=file.aac +scrcpy --no-video --audio-codec=aac --record=file.aac # .m4a/.mp4 and .mka/.mkv are also supported for both opus and aac ``` From 2d79aeb117a4532761ffd26c4093f6cbf4a8d337 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 4 Jun 2023 18:43:35 +0200 Subject: [PATCH 1581/2244] Simplify command in documentation If --no-video is passed, --no-playback is equivalent to --no-audio-playback. --- doc/audio.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/audio.md b/doc/audio.md index c16afd7f..fd17931e 100644 --- a/doc/audio.md +++ b/doc/audio.md @@ -56,7 +56,7 @@ For example, to use the device as a dictaphone and record a capture directly on the computer: ``` -scrcpy --audio-source=mic --no-video --no-audio-playback --record=file.opus +scrcpy --audio-source=mic --no-video --no-playback --record=file.opus ``` From b8d43866d24495c543edc63d48d180975713e93c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 5 Jun 2023 19:44:15 +0200 Subject: [PATCH 1582/2244] Fix options alphabetical order Commit fc52b2450333beb94c0dc2f94d01d4de13afeec5 missed this one. --- app/data/bash-completion/scrcpy | 2 +- app/data/zsh-completion/_scrcpy | 2 +- app/src/cli.c | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index de298056..a44d38f5 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -19,8 +19,8 @@ _scrcpy() { -f --fullscreen --force-adb-forward --forward-all-clicks - -K --hid-keyboard -h --help + -K --hid-keyboard --legacy-paste --list-displays --list-encoders diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 17e30dc5..8d3426dc 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -26,8 +26,8 @@ arguments=( {-f,--fullscreen}'[Start in fullscreen]' '--force-adb-forward[Do not attempt to use \"adb reverse\" to connect to the device]' '--forward-all-clicks[Forward clicks to device]' - {-K,--hid-keyboard}'[Simulate a physical keyboard by using HID over AOAv2]' {-h,--help}'[Print the help]' + {-K,--hid-keyboard}'[Simulate a physical keyboard by using HID over AOAv2]' '--legacy-paste[Inject computer clipboard text as a sequence of key events on Ctrl+v]' '--list-displays[List displays available on the device]' '--list-encoders[List video and audio encoders available on the device]' diff --git a/app/src/cli.c b/app/src/cli.c index eb784ebc..318a4230 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -275,6 +275,11 @@ static const struct sc_option options[] = { "middle-click triggers HOME. This option disables these " "shortcuts and forwards the clicks to the device instead.", }, + { + .shortopt = 'h', + .longopt = "help", + .text = "Print this help.", + }, { .shortopt = 'K', .longopt = "hid-keyboard", @@ -292,11 +297,6 @@ static const struct sc_option options[] = { "is enabled (or a physical keyboard is connected).\n" "Also see --hid-mouse.", }, - { - .shortopt = 'h', - .longopt = "help", - .text = "Print this help.", - }, { .longopt_id = OPT_LEGACY_PASTE, .longopt = "legacy-paste", From b16d4d18359cced40c4218a353c16b0093ee0608 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 5 Jun 2023 19:45:20 +0200 Subject: [PATCH 1583/2244] Fix adb server vs adb daemon confusion The adb daemon runs on the device, the adb server runs as a background process on the computer. --- app/src/server.c | 2 +- app/src/usb/scrcpy_otg.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/server.c b/app/src/server.c index 4e70c4d9..2c0779d9 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -805,7 +805,7 @@ run_server(void *data) { // is parsed, so it is not output) bool ok = sc_adb_start_server(&server->intr, 0); if (!ok) { - LOGE("Could not start adb daemon"); + LOGE("Could not start adb server"); goto error_connection_failed; } diff --git a/app/src/usb/scrcpy_otg.c b/app/src/usb/scrcpy_otg.c index f469de1a..35d8d4cc 100644 --- a/app/src/usb/scrcpy_otg.c +++ b/app/src/usb/scrcpy_otg.c @@ -83,7 +83,7 @@ scrcpy_otg(struct scrcpy_options *options) { #ifdef _WIN32 // On Windows, only one process could open a USB device // - LOGI("Killing adb daemon (if any)..."); + LOGI("Killing adb server (if any)..."); unsigned flags = SC_ADB_NO_STDOUT | SC_ADB_NO_STDERR | SC_ADB_NO_LOGERR; // uninterruptible (intr == NULL), but in practice it's very quick sc_adb_kill_server(NULL, flags); From a3cdf1a6b86ea22786e1f7d09b9c202feabc6949 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 5 Jun 2023 19:48:21 +0200 Subject: [PATCH 1584/2244] Add option to kill adb on close Killing adb on close by default would be incorrect, since it would break any other usage of adb in parallel. It could be easily done manually by calling "adb kill-server" once scrcpy terminates, but add an option --kill-adb-on-close for convenience. Fixes #205 Fixes #2580 Fixes #4049 --- app/data/bash-completion/scrcpy | 1 + app/data/zsh-completion/_scrcpy | 1 + app/scrcpy.1 | 4 ++++ app/src/cli.c | 9 +++++++++ app/src/options.c | 1 + app/src/options.h | 1 + app/src/scrcpy.c | 1 + app/src/server.c | 12 ++++++++++++ app/src/server.h | 1 + 9 files changed, 31 insertions(+) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index a44d38f5..a34a1a44 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -20,6 +20,7 @@ _scrcpy() { --force-adb-forward --forward-all-clicks -h --help + --kill-adb-on-close -K --hid-keyboard --legacy-paste --list-displays diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 8d3426dc..325ccb76 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -27,6 +27,7 @@ arguments=( '--force-adb-forward[Do not attempt to use \"adb reverse\" to connect to the device]' '--forward-all-clicks[Forward clicks to device]' {-h,--help}'[Print the help]' + '--kill-adb-on-close[Kill adb when scrcpy terminates]' {-K,--hid-keyboard}'[Simulate a physical keyboard by using HID over AOAv2]' '--legacy-paste[Inject computer clipboard text as a sequence of key events on Ctrl+v]' '--list-displays[List displays available on the device]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 9b08f182..21c3ac8f 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -129,6 +129,10 @@ By default, right-click triggers BACK (or POWER on) and middle-click triggers HO .B \-h, \-\-help Print this help. +.TP +.B \-\-kill\-adb\-on\-close +Kill adb when scrcpy terminates. + .TP .B \-K, \-\-hid\-keyboard Simulate a physical keyboard by using HID over AOAv2. diff --git a/app/src/cli.c b/app/src/cli.c index 318a4230..c9b818a1 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -77,6 +77,7 @@ enum { OPT_NO_AUDIO_PLAYBACK, OPT_NO_VIDEO_PLAYBACK, OPT_AUDIO_SOURCE, + OPT_KILL_ADB_ON_CLOSE, }; struct sc_option { @@ -280,6 +281,11 @@ static const struct sc_option options[] = { .longopt = "help", .text = "Print this help.", }, + { + .longopt_id = OPT_KILL_ADB_ON_CLOSE, + .longopt = "kill-adb-on-close", + .text = "Kill adb when scrcpy terminates.", + }, { .shortopt = 'K', .longopt = "hid-keyboard", @@ -1944,6 +1950,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; } break; + case OPT_KILL_ADB_ON_CLOSE: + opts->kill_adb_on_close = true; + break; default: // getopt prints the error message on stderr return false; diff --git a/app/src/options.c b/app/src/options.c index e1373753..30b5cb56 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -80,4 +80,5 @@ const struct scrcpy_options scrcpy_options_default = { .require_audio = false, .list_encoders = false, .list_displays = false, + .kill_adb_on_close = false, }; diff --git a/app/src/options.h b/app/src/options.h index c33fafef..75f193b3 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -180,6 +180,7 @@ struct scrcpy_options { bool require_audio; bool list_encoders; bool list_displays; + bool kill_adb_on_close; }; extern const struct scrcpy_options scrcpy_options_default; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 79007bf2..f9679ac1 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -364,6 +364,7 @@ scrcpy(struct scrcpy_options *options) { .power_on = options->power_on, .list_encoders = options->list_encoders, .list_displays = options->list_displays, + .kill_adb_on_close = options->kill_adb_on_close, }; static const struct sc_server_callbacks cbs = { diff --git a/app/src/server.c b/app/src/server.c index 2c0779d9..360e7e7c 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -794,6 +794,15 @@ sc_server_configure_tcpip_unknown_address(struct sc_server *server, return sc_server_connect_to_tcpip(server, ip_port); } +static void +sc_server_kill_adb_if_requested(struct sc_server *server) { + if (server->params.kill_adb_on_close) { + LOGI("Killing adb server..."); + unsigned flags = SC_ADB_NO_STDOUT | SC_ADB_NO_STDERR | SC_ADB_NO_LOGERR; + sc_adb_kill_server(&server->intr, flags); + } +} + static int run_server(void *data) { struct sc_server *server = data; @@ -993,9 +1002,12 @@ run_server(void *data) { sc_process_close(pid); + sc_server_kill_adb_if_requested(server); + return 0; error_connection_failed: + sc_server_kill_adb_if_requested(server); server->cbs->on_connection_failed(server, server->cbs_userdata); return -1; } diff --git a/app/src/server.h b/app/src/server.h index fad44e66..adba2652 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -58,6 +58,7 @@ struct sc_server_params { bool power_on; bool list_encoders; bool list_displays; + bool kill_adb_on_close; }; struct sc_server { From 4ad74794255b7669a89c023afc7ae7a2c0da4dc3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 8 Jun 2023 08:52:21 +0200 Subject: [PATCH 1585/2244] Add missing shortcut in documentation MOD+Backspace also triggers BACK. --- doc/shortcuts.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/shortcuts.md b/doc/shortcuts.md index 6528e7b4..5e706402 100644 --- a/doc/shortcuts.md +++ b/doc/shortcuts.md @@ -29,7 +29,7 @@ _[Super] is typically the Windows or Cmd key._ | Resize window to 1:1 (pixel-perfect) | MOD+g | Resize window to remove black borders | MOD+w \| _Double-left-click¹_ | Click on `HOME` | MOD+h \| _Middle-click_ - | Click on `BACK` | MOD+b \| _Right-click²_ + | Click on `BACK` | MOD+b \| MOD+Backspace \| _Right-click²_ | Click on `APP_SWITCH` | MOD+s \| _4th-click³_ | Click on `MENU` (unlock screen)⁴ | MOD+m | Click on `VOLUME_UP` | MOD+ _(up)_ From fdbc9397a7cb93eeac29481effb71eacbb74d24b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 9 Jun 2023 22:24:32 +0200 Subject: [PATCH 1586/2244] Name Java threads Give a user-friendly name to Java threads created by the server. --- .../src/main/java/com/genymobile/scrcpy/AudioEncoder.java | 8 ++++---- .../main/java/com/genymobile/scrcpy/AudioRawRecorder.java | 2 +- .../src/main/java/com/genymobile/scrcpy/Controller.java | 2 +- .../java/com/genymobile/scrcpy/DeviceMessageSender.java | 2 +- .../main/java/com/genymobile/scrcpy/ScreenEncoder.java | 2 +- server/src/main/java/com/genymobile/scrcpy/Server.java | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java b/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java index 108bbaa1..bec79b05 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java @@ -134,7 +134,7 @@ public final class AudioEncoder implements AsyncProcessor { Ln.d("Audio encoder stopped"); listener.onTerminated(fatalError); } - }); + }, "audio-encoder"); thread.start(); } @@ -183,7 +183,7 @@ public final class AudioEncoder implements AsyncProcessor { Codec codec = streamer.getCodec(); mediaCodec = createMediaCodec(codec, encoderName); - mediaCodecThread = new HandlerThread("AudioEncoder"); + mediaCodecThread = new HandlerThread("media-codec"); mediaCodecThread.start(); MediaFormat format = createFormat(codec.getMimeType(), bitRate, codecOptions); @@ -201,7 +201,7 @@ public final class AudioEncoder implements AsyncProcessor { } finally { end(); } - }); + }, "audio-in"); outputThread = new Thread(() -> { try { @@ -216,7 +216,7 @@ public final class AudioEncoder implements AsyncProcessor { } finally { end(); } - }); + }, "audio-out"); mediaCodec.start(); mediaCodecStarted = true; diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java b/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java index 2fc8c887..7d2adade 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java @@ -69,7 +69,7 @@ public final class AudioRawRecorder implements AsyncProcessor { Ln.d("Audio recorder stopped"); listener.onTerminated(fatalError); } - }); + }, "audio-raw"); thread.start(); } diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index 9a4e275a..733a2032 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -95,7 +95,7 @@ public class Controller implements AsyncProcessor { Ln.d("Controller stopped"); listener.onTerminated(true); } - }); + }, "control-recv"); thread.start(); sender.start(); } diff --git a/server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java b/server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java index 0ef2a9ee..628c1d3c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java +++ b/server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java @@ -60,7 +60,7 @@ public final class DeviceMessageSender { } finally { Ln.d("Device message sender stopped"); } - }); + }, "control-send"); thread.start(); } diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index d56e5d27..ce7c2838 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -299,7 +299,7 @@ public class ScreenEncoder implements Device.RotationListener, Device.FoldListen Ln.d("Screen streaming stopped"); listener.onTerminated(true); } - }); + }, "video"); thread.start(); } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 214ac27d..91e6f40a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -185,7 +185,7 @@ public final class Server { } private static Thread startInitThread(final Options options) { - Thread thread = new Thread(() -> initAndCleanUp(options)); + Thread thread = new Thread(() -> initAndCleanUp(options), "init-cleanup"); thread.start(); return thread; } From 28313631e549444795d9282574c1526c5d4008ce Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 9 Jun 2023 22:28:01 +0200 Subject: [PATCH 1587/2244] Reformat Java code Fix code style. --- server/src/main/java/com/genymobile/scrcpy/Server.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 91e6f40a..b2ee2fa9 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -137,8 +137,7 @@ public final class Server { if (audio) { AudioCodec audioCodec = options.getAudioCodec(); AudioCapture audioCapture = new AudioCapture(options.getAudioSource()); - Streamer audioStreamer = new Streamer(connection.getAudioFd(), audioCodec, options.getSendCodecMeta(), - options.getSendFrameMeta()); + Streamer audioStreamer = new Streamer(connection.getAudioFd(), audioCodec, options.getSendCodecMeta(), options.getSendFrameMeta()); AsyncProcessor audioRecorder; if (audioCodec == AudioCodec.RAW) { audioRecorder = new AudioRawRecorder(audioCapture, audioStreamer); From 6832e8d629305b8ad90b125258ebf612aae88af0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 10 Jun 2023 12:07:35 +0200 Subject: [PATCH 1588/2244] Remove spurious empty line --- .../java/com/genymobile/scrcpy/ControlMessageReaderTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java index 8405905a..47097c78 100644 --- a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java @@ -12,7 +12,6 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.Arrays; - public class ControlMessageReaderTest { @Test From 7536f95d1c1a8b48f12a7a4556d571b9a2f72667 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 10 Jun 2023 12:09:23 +0200 Subject: [PATCH 1589/2244] Rename raw_video_stream to raw_stream This server-specific option impacts both the video and audio streams. --- server/src/main/java/com/genymobile/scrcpy/Options.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 23d4e383..aab6fce8 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -318,9 +318,9 @@ public class Options { case "send_codec_meta": options.sendCodecMeta = Boolean.parseBoolean(value); break; - case "raw_video_stream": - boolean rawVideoStream = Boolean.parseBoolean(value); - if (rawVideoStream) { + case "raw_stream": + boolean rawStream = Boolean.parseBoolean(value); + if (rawStream) { options.sendDeviceMeta = false; options.sendFrameMeta = false; options.sendDummyByte = false; From 5042f8de933faecbeea270fc78713c294d93ec06 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 1 Jun 2023 18:44:53 +0200 Subject: [PATCH 1590/2244] Improve recording documentation --- doc/recording.md | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/doc/recording.md b/doc/recording.md index 02e3cfc8..d0c33181 100644 --- a/doc/recording.md +++ b/doc/recording.md @@ -21,20 +21,15 @@ scrcpy --no-video --audio-codec=aac --record=file.aac # .m4a/.mp4 and .mka/.mkv are also supported for both opus and aac ``` -To disable playback while recording: - -```bash -scrcpy --no-playback --record=file.mp4 -scrcpy -Nr file.mkv -# interrupt recording with Ctrl+C -``` - Timestamps are captured on the device, so [packet delay variation] does not impact the recorded file, which is always clean (only if you use `--record` of course, not if you capture your scrcpy window and audio output on the computer). [packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation + +## Format + The video and audio streams are encoded on the device, but are muxed on the client side. Two formats (containers) are supported: - Matroska (`.mkv`) @@ -48,3 +43,21 @@ needs not end with `.mkv` or `.mp4`): ``` scrcpy --record=file --record-format=mkv ``` + + +## No playback + +To disable playback while recording: + +```bash +scrcpy --no-playback --record=file.mp4 +scrcpy -Nr file.mkv +# interrupt recording with Ctrl+C +``` + +It is also possible to disable video and audio playback separately: + +```bash +# Record both video and audio, but only play video +scrcpy --record=file.mkv --no-audio-playback +``` From d3c2955fb9e46949694ce91cbe6cc15ca068207b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 1 Jun 2023 18:46:50 +0200 Subject: [PATCH 1591/2244] Add --time-limit Add an option to stop scrcpy automatically after a given delay. PR #4052 Fixes #3752 --- app/data/bash-completion/scrcpy | 1 + app/data/zsh-completion/_scrcpy | 1 + app/meson.build | 1 + app/scrcpy.1 | 4 ++ app/src/cli.c | 24 ++++++++++ app/src/events.h | 1 + app/src/options.c | 1 + app/src/options.h | 1 + app/src/scrcpy.c | 47 ++++++++++++++++++++ app/src/util/timeout.c | 77 +++++++++++++++++++++++++++++++++ app/src/util/timeout.h | 43 ++++++++++++++++++ doc/recording.md | 15 +++++++ 12 files changed, 216 insertions(+) create mode 100644 app/src/util/timeout.c create mode 100644 app/src/util/timeout.h diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index a34a1a44..003f9d73 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -60,6 +60,7 @@ _scrcpy() { -t --show-touches --tcpip --tcpip= + --time-limit= --tunnel-host= --tunnel-port= --v4l2-buffer= diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 325ccb76..81142851 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -65,6 +65,7 @@ arguments=( '--shortcut-mod=[\[key1,key2+key3,...\] Specify the modifiers to use for scrcpy shortcuts]:shortcut mod:(lctrl rctrl lalt ralt lsuper rsuper)' {-t,--show-touches}'[Show physical touches]' '--tcpip[\(optional \[ip\:port\]\) Configure and connect the device over TCP/IP]' + '--time-limit=[Set the maximum mirroring time, in seconds]' '--tunnel-host=[Set the IP address of the adb tunnel to reach the scrcpy server]' '--tunnel-port=[Set the TCP port of the adb tunnel to reach the scrcpy server]' '--v4l2-buffer=[Add a buffering delay \(in milliseconds\) before pushing frames]' diff --git a/app/meson.build b/app/meson.build index 061fdcab..e0d92050 100644 --- a/app/meson.build +++ b/app/meson.build @@ -51,6 +51,7 @@ src = [ 'src/util/term.c', 'src/util/thread.c', 'src/util/tick.c', + 'src/util/timeout.c', ] conf = configuration_data() diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 21c3ac8f..0c91701f 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -354,6 +354,10 @@ If a destination address is provided, then scrcpy connects to this address befor If no destination address is provided, then scrcpy attempts to find the IP address and adb port of the current device (typically connected over USB), enables TCP/IP mode if necessary, then connects to this address before starting. +.TP +.BI "\-\-time\-limit " seconds +Set the maximum mirroring time, in seconds. + .TP .BI "\-\-tunnel\-host " ip Set the IP address of the adb tunnel to reach the scrcpy server. This option automatically enables --force-adb-forward. diff --git a/app/src/cli.c b/app/src/cli.c index c9b818a1..72f4bea1 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -78,6 +78,7 @@ enum { OPT_NO_VIDEO_PLAYBACK, OPT_AUDIO_SOURCE, OPT_KILL_ADB_ON_CLOSE, + OPT_TIME_LIMIT, }; struct sc_option { @@ -580,6 +581,12 @@ static const struct sc_option options[] = { "connected over USB), enables TCP/IP mode, then connects to " "this address before starting.", }, + { + .longopt_id = OPT_TIME_LIMIT, + .longopt = "time-limit", + .argdesc = "seconds", + .text = "Set the maximum mirroring time, in seconds.", + }, { .longopt_id = OPT_TUNNEL_HOST, .longopt = "tunnel-host", @@ -1618,6 +1625,18 @@ parse_audio_source(const char *optarg, enum sc_audio_source *source) { return false; } +static bool +parse_time_limit(const char *s, sc_tick *tick) { + long value; + bool ok = parse_integer_arg(s, &value, false, 0, 0x7FFFFFFF, "time limit"); + if (!ok) { + return false; + } + + *tick = SC_TICK_FROM_SEC(value); + return true; +} + static bool parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], const char *optstring, const struct option *longopts) { @@ -1953,6 +1972,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], case OPT_KILL_ADB_ON_CLOSE: opts->kill_adb_on_close = true; break; + case OPT_TIME_LIMIT: + if (!parse_time_limit(optarg, &opts->time_limit)) { + return false; + } + break; default: // getopt prints the error message on stderr return false; diff --git a/app/src/events.h b/app/src/events.h index 609e3198..8bfa2582 100644 --- a/app/src/events.h +++ b/app/src/events.h @@ -6,3 +6,4 @@ #define SC_EVENT_DEMUXER_ERROR (SDL_USEREVENT + 5) #define SC_EVENT_RECORDER_ERROR (SDL_USEREVENT + 6) #define SC_EVENT_SCREEN_INIT_SIZE (SDL_USEREVENT + 7) +#define SC_EVENT_TIME_LIMIT_REACHED (SDL_USEREVENT + 8) diff --git a/app/src/options.c b/app/src/options.c index 30b5cb56..530e003b 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -42,6 +42,7 @@ const struct scrcpy_options scrcpy_options_default = { .display_buffer = 0, .audio_buffer = SC_TICK_FROM_MS(50), .audio_output_buffer = SC_TICK_FROM_MS(5), + .time_limit = 0, #ifdef HAVE_V4L2 .v4l2_device = NULL, .v4l2_buffer = 0, diff --git a/app/src/options.h b/app/src/options.h index 75f193b3..1f36ad7f 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -142,6 +142,7 @@ struct scrcpy_options { sc_tick display_buffer; sc_tick audio_buffer; sc_tick audio_output_buffer; + sc_tick time_limit; #ifdef HAVE_V4L2 const char *v4l2_device; sc_tick v4l2_buffer; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index f9679ac1..fd310c46 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -35,6 +35,7 @@ #include "util/log.h" #include "util/net.h" #include "util/rand.h" +#include "util/timeout.h" #ifdef HAVE_V4L2 # include "v4l2_sink.h" #endif @@ -73,6 +74,7 @@ struct scrcpy { struct sc_hid_mouse mouse_hid; #endif }; + struct sc_timeout timeout; }; static inline void @@ -171,6 +173,9 @@ event_loop(struct scrcpy *s) { case SC_EVENT_RECORDER_ERROR: LOGE("Recorder error"); return SCRCPY_EXIT_FAILURE; + case SC_EVENT_TIME_LIMIT_REACHED: + LOGI("Time limit reached"); + return SCRCPY_EXIT_SUCCESS; case SDL_QUIT: LOGD("User requested to quit"); return SCRCPY_EXIT_SUCCESS; @@ -280,6 +285,14 @@ sc_server_on_disconnected(struct sc_server *server, void *userdata) { // event } +static void +sc_timeout_on_timeout(struct sc_timeout *timeout, void *userdata) { + (void) timeout; + (void) userdata; + + PUSH_EVENT(SC_EVENT_TIME_LIMIT_REACHED); +} + // Generate a scrcpy id to differentiate multiple running scrcpy instances static uint32_t scrcpy_generate_scid() { @@ -321,6 +334,8 @@ scrcpy(struct scrcpy_options *options) { bool controller_initialized = false; bool controller_started = false; bool screen_initialized = false; + bool timeout_initialized = false; + bool timeout_started = false; struct sc_acksync *acksync = NULL; @@ -743,6 +758,27 @@ aoa_hid_end: } } + if (options->time_limit) { + bool ok = sc_timeout_init(&s->timeout); + if (!ok) { + goto end; + } + + timeout_initialized = true; + + sc_tick deadline = sc_tick_now() + options->time_limit; + static const struct sc_timeout_callbacks cbs = { + .on_timeout = sc_timeout_on_timeout, + }; + + ok = sc_timeout_start(&s->timeout, deadline, &cbs, NULL); + if (!ok) { + goto end; + } + + timeout_started = true; + } + ret = event_loop(s); LOGD("quit..."); @@ -751,6 +787,10 @@ aoa_hid_end: sc_screen_hide_window(&s->screen); end: + if (timeout_started) { + sc_timeout_stop(&s->timeout); + } + // The demuxer is not stopped explicitly, because it will stop by itself on // end-of-stream #ifdef HAVE_USB @@ -786,6 +826,13 @@ end: sc_server_stop(&s->server); } + if (timeout_started) { + sc_timeout_join(&s->timeout); + } + if (timeout_initialized) { + sc_timeout_destroy(&s->timeout); + } + // now that the sockets are shutdown, the demuxer and controller are // interrupted, we can join them if (video_demuxer_started) { diff --git a/app/src/util/timeout.c b/app/src/util/timeout.c new file mode 100644 index 00000000..a1665373 --- /dev/null +++ b/app/src/util/timeout.c @@ -0,0 +1,77 @@ +#include "timeout.h" + +#include + +#include "log.h" + +bool +sc_timeout_init(struct sc_timeout *timeout) { + bool ok = sc_mutex_init(&timeout->mutex); + if (!ok) { + return false; + } + + ok = sc_cond_init(&timeout->cond); + if (!ok) { + return false; + } + + timeout->stopped = false; + + return true; +} + +static int +run_timeout(void *data) { + struct sc_timeout *timeout = data; + sc_tick deadline = timeout->deadline; + + sc_mutex_lock(&timeout->mutex); + bool timed_out = false; + while (!timeout->stopped && !timed_out) { + timed_out = !sc_cond_timedwait(&timeout->cond, &timeout->mutex, + deadline); + } + sc_mutex_unlock(&timeout->mutex); + + timeout->cbs->on_timeout(timeout, timeout->cbs_userdata); + + return 0; +} + +bool +sc_timeout_start(struct sc_timeout *timeout, sc_tick deadline, + const struct sc_timeout_callbacks *cbs, void *cbs_userdata) { + bool ok = sc_thread_create(&timeout->thread, run_timeout, "scrcpy-timeout", + timeout); + if (!ok) { + LOGE("Timeout: could not start thread"); + return false; + } + + timeout->deadline = deadline; + + assert(cbs && cbs->on_timeout); + timeout->cbs = cbs; + timeout->cbs_userdata = cbs_userdata; + + return true; +} + +void +sc_timeout_stop(struct sc_timeout *timeout) { + sc_mutex_lock(&timeout->mutex); + timeout->stopped = true; + sc_mutex_unlock(&timeout->mutex); +} + +void +sc_timeout_join(struct sc_timeout *timeout) { + sc_thread_join(&timeout->thread, NULL); +} + +void +sc_timeout_destroy(struct sc_timeout *timeout) { + sc_mutex_destroy(&timeout->mutex); + sc_cond_destroy(&timeout->cond); +} diff --git a/app/src/util/timeout.h b/app/src/util/timeout.h new file mode 100644 index 00000000..ae171b86 --- /dev/null +++ b/app/src/util/timeout.h @@ -0,0 +1,43 @@ +#ifndef SC_TIMEOUT_H +#define SC_TIMEOUT_H + +#include "common.h" + +#include + +#include "thread.h" +#include "tick.h" + +struct sc_timeout { + sc_thread thread; + sc_tick deadline; + + sc_mutex mutex; + sc_cond cond; + bool stopped; + + const struct sc_timeout_callbacks *cbs; + void *cbs_userdata; +}; + +struct sc_timeout_callbacks { + void (*on_timeout)(struct sc_timeout *timeout, void *userdata); +}; + +bool +sc_timeout_init(struct sc_timeout *timeout); + +void +sc_timeout_destroy(struct sc_timeout *timeout); + +bool +sc_timeout_start(struct sc_timeout *timeout, sc_tick deadline, + const struct sc_timeout_callbacks *cbs, void *cbs_userdata); + +void +sc_timeout_stop(struct sc_timeout *timeout); + +void +sc_timeout_join(struct sc_timeout *timeout); + +#endif diff --git a/doc/recording.md b/doc/recording.md index d0c33181..76a7efd6 100644 --- a/doc/recording.md +++ b/doc/recording.md @@ -61,3 +61,18 @@ It is also possible to disable video and audio playback separately: # Record both video and audio, but only play video scrcpy --record=file.mkv --no-audio-playback ``` + +## Time limit + +To limit the recording time: + +```bash +scrcpy --record=file.mkv --time-limit=20 # in seconds +``` + +The `--time-limit` option is not limited to recording, it also impacts simple +mirroring: + +``` +scrcpy --time-limit=20 +``` From 5bd75148716496755dc32daab21a322f51dc58ca Mon Sep 17 00:00:00 2001 From: wuderek Date: Fri, 9 Jun 2023 06:03:09 +0000 Subject: [PATCH 1592/2244] Add InputManagerGlobal for Android 14 beta 3 Parts of the InputManager class have been moved to a new InputManagerGlobal class in Android 14 preview. Fixes #4074 PR #4075 Signed-off-by: Romain Vimont --- .../genymobile/scrcpy/wrappers/InputManager.java | 4 ++-- .../genymobile/scrcpy/wrappers/ServiceManager.java | 14 ++++++++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) 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 32bf4252..ef0a4f50 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java @@ -14,13 +14,13 @@ public final class InputManager { public static final int INJECT_INPUT_EVENT_MODE_WAIT_FOR_RESULT = 1; public static final int INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH = 2; - private final android.hardware.input.InputManager manager; + private final Object manager; private Method injectInputEventMethod; private static Method setDisplayIdMethod; private static Method setActionButtonMethod; - public InputManager(android.hardware.input.InputManager manager) { + public InputManager(Object manager) { this.manager = manager; } 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 ee2f0fa9..69803971 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java @@ -62,11 +62,21 @@ public final class ServiceManager { return displayManager; } + public static Class getInputManagerClass() { + try { + // Parts of the InputManager class have been moved to a new InputManagerGlobal class in Android 14 preview + return Class.forName("android.hardware.input.InputManagerGlobal"); + } catch (ClassNotFoundException e) { + return android.hardware.input.InputManager.class; + } + } + public static InputManager getInputManager() { if (inputManager == null) { try { - Method getInstanceMethod = android.hardware.input.InputManager.class.getDeclaredMethod("getInstance"); - android.hardware.input.InputManager im = (android.hardware.input.InputManager) getInstanceMethod.invoke(null); + Class inputManagerClass = getInputManagerClass(); + Method getInstanceMethod = inputManagerClass.getDeclaredMethod("getInstance"); + Object im = getInstanceMethod.invoke(null); inputManager = new InputManager(im); } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { throw new AssertionError(e); From 3b7e2ca9c8c648b3dadbeec25cdb087772cc6c99 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 16 Jun 2023 23:24:08 +0200 Subject: [PATCH 1593/2244] Fix lint warning Suppress lint "DiscouragedPrivateApi" in Workarounds.java. --- 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 b343a344..9ae7983f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java +++ b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java @@ -106,7 +106,7 @@ public final class Workarounds { } @TargetApi(Build.VERSION_CODES.R) - @SuppressLint({"WrongConstant", "MissingPermission", "BlockedPrivateApi", "SoonBlockedPrivateApi"}) + @SuppressLint("WrongConstant,MissingPermission,BlockedPrivateApi,SoonBlockedPrivateApi,DiscouragedPrivateApi") public static AudioRecord createAudioRecord(int source, int sampleRate, int channelConfig, int channels, int channelMask, int encoding) { // Vivo (and maybe some other third-party ROMs) modified `AudioRecord`'s constructor, requiring `Context`s from real App environment. // From 48a00fb481cec1d60cb41c1d7a6aa9eebf87878e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 17 Jun 2023 00:12:42 +0200 Subject: [PATCH 1594/2244] Log device BRAND The BRAND value is not always the same as the MANUFACTURER value. --- 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 b2ee2fa9..13802275 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -87,7 +87,7 @@ public final class Server { } private static void scrcpy(Options options) throws IOException, ConfigurationException { - Ln.i("Device: " + Build.MANUFACTURER + " " + Build.MODEL + " (Android " + Build.VERSION.RELEASE + ")"); + Ln.i("Device: [" + Build.MANUFACTURER + "] " + Build.BRAND + " " + Build.MODEL + " (Android " + Build.VERSION.RELEASE + ")"); final Device device = new Device(options); Thread initThread = startInitThread(options); From 0f1afff7a62e3fcc51a93025ed61a09408147413 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 19 Jun 2023 13:30:36 +0200 Subject: [PATCH 1595/2244] Move workarounds execution Expose a single public static method in the Workarounds class to apply all necessary workarounds. --- .../java/com/genymobile/scrcpy/Server.java | 21 +------------- .../com/genymobile/scrcpy/Workarounds.java | 29 +++++++++++++++++-- 2 files changed, 27 insertions(+), 23 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 13802275..2e6e1d4a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -99,26 +99,7 @@ public final class Server { boolean audio = options.getAudio(); boolean sendDummyByte = options.getSendDummyByte(); - Workarounds.prepareMainLooper(); - - // Workarounds must be applied for Meizu phones: - // - - // - - // - - // - // But only apply when strictly necessary, since workarounds can cause other issues: - // - - // - - if (Build.BRAND.equalsIgnoreCase("meizu")) { - Workarounds.fillAppInfo(); - } - - // Before Android 11, audio is not supported. - // Since Android 12, we can properly set a context on the AudioRecord. - // Only on Android 11 we must fill the application context for the AudioRecord to work. - if (audio && Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { - Workarounds.fillAppContext(); - } + Workarounds.apply(audio); List asyncProcessors = new ArrayList<>(); diff --git a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java index 9ae7983f..ded1d9cb 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java +++ b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java @@ -27,8 +27,31 @@ public final class Workarounds { // not instantiable } + public static void apply(boolean audio) { + Workarounds.prepareMainLooper(); + + // Workarounds must be applied for Meizu phones: + // - + // - + // - + // + // But only apply when strictly necessary, since workarounds can cause other issues: + // - + // - + if (Build.BRAND.equalsIgnoreCase("meizu")) { + Workarounds.fillAppInfo(); + } + + // Before Android 11, audio is not supported. + // Since Android 12, we can properly set a context on the AudioRecord. + // Only on Android 11 we must fill the application context for the AudioRecord to work. + if (audio && Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { + Workarounds.fillAppContext(); + } + } + @SuppressWarnings("deprecation") - public static void prepareMainLooper() { + private static void prepareMainLooper() { // Some devices internally create a Handler when creating an input Surface, causing an exception: // "Can't create handler inside thread that has not called Looper.prepare()" // @@ -57,7 +80,7 @@ public final class Workarounds { } @SuppressLint("PrivateApi,DiscouragedPrivateApi") - public static void fillAppInfo() { + private static void fillAppInfo() { try { fillActivityThread(); @@ -86,7 +109,7 @@ public final class Workarounds { } @SuppressLint("PrivateApi,DiscouragedPrivateApi") - public static void fillAppContext() { + private static void fillAppContext() { try { fillActivityThread(); From fb21bbf763691407bc069d84b8e6248667a80e6f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 18 Jun 2023 17:52:43 +0200 Subject: [PATCH 1596/2244] Add workarounds for Honor devices Audio did not work on Honor devices. To make it work, a system context must be set as a base context of FakeContext (so that a PackageManager is available), and a current Application and ActivityThread must be set. These workarounds must not be applied for all devices, because they might cause other issues. Fixes #4015 Refs #3085 Co-authored-by: Simon Chan <1330321+yume-chan@users.noreply.github.com> --- .../com/genymobile/scrcpy/FakeContext.java | 4 +- .../com/genymobile/scrcpy/Workarounds.java | 63 +++++++++++++++---- 2 files changed, 53 insertions(+), 14 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/FakeContext.java b/server/src/main/java/com/genymobile/scrcpy/FakeContext.java index 738203de..6501d4cf 100644 --- a/server/src/main/java/com/genymobile/scrcpy/FakeContext.java +++ b/server/src/main/java/com/genymobile/scrcpy/FakeContext.java @@ -2,11 +2,11 @@ package com.genymobile.scrcpy; import android.annotation.TargetApi; import android.content.AttributionSource; -import android.content.ContextWrapper; +import android.content.MutableContextWrapper; import android.os.Build; import android.os.Process; -public final class FakeContext extends ContextWrapper { +public final class FakeContext extends MutableContextWrapper { public static final String PACKAGE_NAME = "com.android.shell"; public static final int ROOT_UID = 0; // Like android.os.Process.ROOT_UID, but before API 29 diff --git a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java index ded1d9cb..b8294a87 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java +++ b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java @@ -4,6 +4,7 @@ import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.app.Application; import android.content.AttributionSource; +import android.content.Context; import android.content.ContextWrapper; import android.content.pm.ApplicationInfo; import android.media.AudioAttributes; @@ -30,22 +31,47 @@ public final class Workarounds { public static void apply(boolean audio) { Workarounds.prepareMainLooper(); - // Workarounds must be applied for Meizu phones: - // - - // - - // - - // - // But only apply when strictly necessary, since workarounds can cause other issues: - // - - // - + boolean mustFillAppInfo = false; + boolean mustFillBaseContext = false; + boolean mustFillAppContext = false; + + if (Build.BRAND.equalsIgnoreCase("meizu")) { - Workarounds.fillAppInfo(); + // Workarounds must be applied for Meizu phones: + // - + // - + // - + // + // But only apply when strictly necessary, since workarounds can cause other issues: + // - + // - + mustFillAppInfo = true; + } else if (Build.BRAND.equalsIgnoreCase("honor")) { + // More workarounds must be applied for Honor devices: + // - + // + // The system context must not be set for all devices, because it would cause other problems: + // - + // - + mustFillAppInfo = true; + mustFillBaseContext = true; + mustFillAppContext = true; } - // Before Android 11, audio is not supported. - // Since Android 12, we can properly set a context on the AudioRecord. - // Only on Android 11 we must fill the application context for the AudioRecord to work. if (audio && Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { + // Before Android 11, audio is not supported. + // Since Android 12, we can properly set a context on the AudioRecord. + // Only on Android 11 we must fill the application context for the AudioRecord to work. + mustFillAppContext = true; + } + + if (mustFillAppInfo) { + Workarounds.fillAppInfo(); + } + if (mustFillBaseContext) { + Workarounds.fillBaseContext(); + } + if (mustFillAppContext) { Workarounds.fillAppContext(); } } @@ -128,6 +154,19 @@ public final class Workarounds { } } + public static void fillBaseContext() { + try { + fillActivityThread(); + + Method getSystemContextMethod = activityThreadClass.getDeclaredMethod("getSystemContext"); + Context context = (Context) getSystemContextMethod.invoke(activityThread); + FakeContext.get().setBaseContext(context); + } catch (Throwable throwable) { + // this is a workaround, so failing is not an error + Ln.d("Could not fill base context: " + throwable.getMessage()); + } + } + @TargetApi(Build.VERSION_CODES.R) @SuppressLint("WrongConstant,MissingPermission,BlockedPrivateApi,SoonBlockedPrivateApi,DiscouragedPrivateApi") public static AudioRecord createAudioRecord(int source, int sampleRate, int channelConfig, int channels, int channelMask, int encoding) { From 09009c2aa71e1f748abd32b74c0f2014ad606f4e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 20 Jun 2023 21:45:14 +0200 Subject: [PATCH 1597/2244] Upgrade SDL (2.28.0) for Windows Include the latest version of SDL in Windows releases. Fixes #3825 Refs libsdl/#7478 --- app/prebuilt-deps/prepare-sdl.sh | 6 +++--- cross_win32.txt | 2 +- cross_win64.txt | 2 +- release.mk | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/prebuilt-deps/prepare-sdl.sh b/app/prebuilt-deps/prepare-sdl.sh index 60bffae9..b691aac5 100755 --- a/app/prebuilt-deps/prepare-sdl.sh +++ b/app/prebuilt-deps/prepare-sdl.sh @@ -6,10 +6,10 @@ cd "$DIR" mkdir -p "$PREBUILT_DATA_DIR" cd "$PREBUILT_DATA_DIR" -DEP_DIR=SDL2-2.26.4 +DEP_DIR=SDL2-2.28.0 -FILENAME=SDL2-devel-2.26.4-mingw.tar.gz -SHA256SUM=fe899c8642caac2f180b1ee6f786857ddcaa0adc1fa82474312b09dd47d74712 +FILENAME=SDL2-devel-2.28.0-mingw.tar.gz +SHA256SUM=b91ce59eeacd4a9db403f976fd2337d9360b21ada374124417d716065c380e20 if [[ -d "$DEP_DIR" ]] then diff --git a/cross_win32.txt b/cross_win32.txt index f3fded40..c3f72540 100644 --- a/cross_win32.txt +++ b/cross_win32.txt @@ -17,5 +17,5 @@ endian = 'little' [properties] prebuilt_ffmpeg = 'ffmpeg-6.0-scrcpy-4/win32' -prebuilt_sdl2 = 'SDL2-2.26.4/i686-w64-mingw32' +prebuilt_sdl2 = 'SDL2-2.28.0/i686-w64-mingw32' prebuilt_libusb = 'libusb-1.0.26/libusb-MinGW-Win32' diff --git a/cross_win64.txt b/cross_win64.txt index 1b02b93f..66cddf83 100644 --- a/cross_win64.txt +++ b/cross_win64.txt @@ -17,5 +17,5 @@ endian = 'little' [properties] prebuilt_ffmpeg = 'ffmpeg-6.0-scrcpy-4/win64' -prebuilt_sdl2 = 'SDL2-2.26.4/x86_64-w64-mingw32' +prebuilt_sdl2 = 'SDL2-2.28.0/x86_64-w64-mingw32' prebuilt_libusb = 'libusb-1.0.26/libusb-MinGW-x64' diff --git a/release.mk b/release.mk index e41a45ad..a467352c 100644 --- a/release.mk +++ b/release.mk @@ -101,7 +101,7 @@ dist-win32: build-server build-win32 cp app/prebuilt-deps/data/platform-tools-34.0.1/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-34.0.1/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-34.0.1/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/SDL2-2.26.4/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/SDL2-2.28.0/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/libusb-1.0.26/libusb-MinGW-Win32/bin/msys-usb-1.0.dll "$(DIST)/$(WIN32_TARGET_DIR)/" dist-win64: build-server build-win64 @@ -119,7 +119,7 @@ dist-win64: build-server build-win64 cp app/prebuilt-deps/data/platform-tools-34.0.1/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-34.0.1/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-34.0.1/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/SDL2-2.26.4/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/SDL2-2.28.0/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/libusb-1.0.26/libusb-MinGW-x64/bin/msys-usb-1.0.dll "$(DIST)/$(WIN64_TARGET_DIR)/" zip-win32: dist-win32 From 5061b7e02c3aa932f245adc98686bee57d6abfaf Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 21 Jun 2023 23:56:06 +0200 Subject: [PATCH 1598/2244] Fix build without gradle Add missing class generation from IDisplayFoldListener.aidl. Refs 24999d0d32daf50246168dde212f01cf109569f9 --- server/build_without_gradle.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index 3201034f..f13fd0dc 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -48,6 +48,7 @@ cd "$SERVER_DIR/src/main/aidl" "$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" android/view/IRotationWatcher.aidl "$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" \ android/content/IOnPrimaryClipChangedListener.aidl +"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" android/view/IDisplayFoldListener.aidl echo "Compiling java sources..." cd ../java From fae3fbc934b6b58f877439a1bffa6aaf9793f46a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 10 Jun 2023 12:26:04 +0200 Subject: [PATCH 1599/2244] Update developer documentation --- doc/develop.md | 445 ++++++++++++++++++++++++++++++++++--------------- 1 file changed, 312 insertions(+), 133 deletions(-) diff --git a/doc/develop.md b/doc/develop.md index bd409fff..67d7f9b0 100644 --- a/doc/develop.md +++ b/doc/develop.md @@ -9,16 +9,52 @@ This application is composed of two parts: The client is responsible to push the server to the device and start its execution. -Once the client and the server are connected to each other, the server initially -sends device information (name and initial screen dimensions), then starts to -send a raw H.264 video stream of the device screen. The client decodes the video -frames, and display them as soon as possible, without buffering, to minimize -latency. The client is not aware of the device rotation (which is handled by the -server), it just knows the dimensions of the video frames. +The client and the server establish communication using separate sockets for +video, audio and controls. Any of them may be disabled (but not all), so +there are 1, 2 or 3 socket(s). -The client captures relevant keyboard and mouse events, that it transmits to the -server, which injects them to the device. +The server initially sends the device name on the first socket (it is used for +the scrcpy window title), then each socket is used for its own purpose. All +reads and writes are performed from a dedicated thread for each socket, both on +the client and on the server. +If video is enabled, then the server sends a raw video stream (H.264 by default) +of the device screen, with some additional headers for each packet. The client +decodes the video frames, and displays them as soon as possible, without +buffering (unless `--display-buffer=delay` is specified) to minimize latency. +The client is not aware of the device rotation (which is handled by the server), +it just knows the dimensions of the video frames it receives. + +Similarly, if audio is enabled, then the server sends a raw audio stream (OPUS +by default) of the device audio output (or the microphone if +`--audio-source=mic` is specified), with some additional headers for each +packet. The client decodes the stream, attempts to keep a minimal latency by +maintaining an average buffering. The [blog post][scrcpy2] of the scrcpy v2.0 +release gives more details about the audio feature. + +If control is enabled, then the client captures relevant keyboard and mouse +events, that it transmits to the server, which injects them to the device. This +is the only socket which is used in both direction: input events are sent from +the client to the device, and when the device clipboard changes, the new content +is sent from the device to the client to support seamless copy-paste. + +[scrcpy2]: https://blog.rom1v.com/2023/03/scrcpy-2-0-with-audio/ + +Note that the client-server roles are expressed at the application level: + + - the server _serves_ video and audio streams, and handle requests from the + client, + - the client _controls_ the device through the server. + +However, by default (when `--force-adb-forward` is not set), the roles are +reversed at the network level: + + - the client opens a server socket and listen on a port before starting the + server, + - the server connects to the client. + +This role inversion guarantees that the connection will not fail due to race +conditions without polling. ## Server @@ -32,15 +68,14 @@ The server is a Java application (with a [`public static void main(String... args)`][main] method), compiled against the Android framework, and executed as `shell` on the Android device. -[main]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/Server.java#L123 +[main]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/server/src/main/java/com/genymobile/scrcpy/Server.java#L193 To run such a Java application, the classes must be [_dexed_][dex] (typically, to `classes.dex`). If `my.package.MainClass` is the main class, compiled to `classes.dex`, pushed to the device in `/data/local/tmp`, then it can be run with: - adb shell CLASSPATH=/data/local/tmp/classes.dex \ - app_process / my.package.MainClass + adb shell CLASSPATH=/data/local/tmp/classes.dex app_process / my.package.MainClass _The path `/data/local/tmp` is a good candidate to push the server, since it's readable and writable by `shell`, but not world-writable, so a malicious @@ -49,7 +84,7 @@ application may not replace the server just before the client executes it._ Instead of a raw _dex_ file, `app_process` accepts a _jar_ containing `classes.dex` (e.g. an [APK]). For simplicity, and to benefit from the gradle build system, the server is built to an (unsigned) APK (renamed to -`scrcpy-server`). +`scrcpy-server.jar`). [dex]: https://en.wikipedia.org/wiki/Dalvik_(software) [apk]: https://en.wikipedia.org/wiki/Android_application_package @@ -65,42 +100,77 @@ They can be called using reflection though. The communication with hidden components is provided by [_wrappers_ classes][wrappers] and [aidl]. [hidden]: https://stackoverflow.com/a/31908373/1987178 -[wrappers]: https://github.com/Genymobile/scrcpy/tree/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/wrappers -[aidl]: https://github.com/Genymobile/scrcpy/tree/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/aidl/android/view +[wrappers]: https://github.com/Genymobile/scrcpy/tree/master/server/src/main/java/com/genymobile/scrcpy/wrappers +[aidl]: https://github.com/Genymobile/scrcpy/tree/master/server/src/main/aidl -### Threading -The server uses 3 threads: +### Execution - - the **main** thread, encoding and streaming the video to the client; - - the **controller** thread, listening for _control messages_ (typically, - keyboard and mouse events) from the client; - - the **receiver** thread (managed by the controller), sending _device messages_ - to the clients (currently, it is only used to send the device clipboard - content). +The server is started by the client basically by executing the following +commands: -Since the video encoding is typically hardware, there would be no benefit in -encoding and streaming in two different threads. +```bash +adb push scrcpy-server /data/local/tmp/scrcpy-server.jar +adb forward tcp:27183 localabstract:scrcpy +adb shell CLASSPATH=/data/local/tmp/scrcpy-server.jar app_process / com.genymobile.scrcpy.Server 2.1 +``` + +The first argument (`2.1` in the example) is the client scrcpy version. The +server fails if the client and the server do not have the exact same version. +The protocol between the client and the server may change from version to +version (see [protocol](#protocol) below), and there is no backward or forward +compatibility (there is no point to use different client and server versions). +This check allows to detect misconfiguration (running an older or newer server +by mistake). + +It is followed by any number of arguments, in the form of `key=value` pairs. +Their order is irrelevant. The possible keys and associated value types can be +found in the [server][server-options] and [client][client-options] code. + +[server-options]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/server/src/main/java/com/genymobile/scrcpy/Options.java#L181 +[client-options]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/app/src/server.c#L226 + +For example, if we execute `scrcpy -m1920 --no-audio`, then the server +execution will look like this: + +```bash +# scid is a random number to identify different clients running on the same device +adb shell CLASSPATH=/data/local/tmp/scrcpy-server.jar app_process / com.genymobile.scrcpy.Server 2.1 scid=12345678 log_level=info audio=false max_size=1920 +``` + +### Components + +When executed, its [`main()`][main] method is executed (on the "main" thread). +It parses the arguments, establishes the connection with the client and starts +the other "components": + - the **video** streamer: it captures the video screen and send encoded video + packets on the _video_ socket (from the _video_ thread). + - the **audio** streamer: it uses several threads to capture raw packets, + submits them to encoding and retrieve encoded packets, which it sends on the + _audio_ socket. + - the **controller**: it receives _control messages_ (typically input events) + on the _control_ socket from one thread, and sends _device messages_ (e.g. to + transmit the device clipboard content to the client) on the same _control + socket_ from another thread. Thus, the _control_ socket is used in both + directions (contrary to the _video_ and _audio_ sockets). ### Screen video encoding The encoding is managed by [`ScreenEncoder`]. -The video is encoded using the [`MediaCodec`] API. The codec takes its input -from a [surface] associated to the display, and writes the resulting H.264 -stream to the provided output stream (the socket connected to the client). +The video is encoded using the [`MediaCodec`] API. The codec encodes the content +of a `Surface` associated to the display, and writes the encoding packets to the +client (on the _video_ socket). -[`ScreenEncoder`]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +[`ScreenEncoder`]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java [`MediaCodec`]: https://developer.android.com/reference/android/media/MediaCodec.html -[surface]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L68-L69 -On device [rotation], the codec, surface and display are reinitialized, and a -new video stream is produced. +On device rotation (or folding), the encoding session is [reset] and restarted. -New frames are produced only when changes occur on the surface. This is good -because it avoids to send unnecessary frames, but there are drawbacks: +New frames are produced only when changes occur on the surface. This avoids to +send unnecessary frames, but by default there might be drawbacks: - it does not send any frame on start if the device screen does not change, - after fast motion changes, the last frame may have poor quality. @@ -108,11 +178,24 @@ because it avoids to send unnecessary frames, but there are drawbacks: Both problems are [solved][repeat] by the flag [`KEY_REPEAT_PREVIOUS_FRAME_AFTER`][repeat-flag]. +[reset]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L179 [rotation]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L90 -[repeat]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L147-L148 +[repeat]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L246-L247 [repeat-flag]: https://developer.android.com/reference/android/media/MediaFormat.html#KEY_REPEAT_PREVIOUS_FRAME_AFTER +### Audio encoding + +Similarly, the audio is [captured] using an [`AudioRecord`], and [encoded] using +the [`MediaCodec`] asynchronous API. + +More details are available on the [blog post][scrcpy2] introducing the audio feature. + +[captured]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java +[encoded]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java +[`AudioRecord`]: https://developer.android.com/reference/android/media/AudioRecord + + ### Input events injection _Control messages_ are received from the client by the [`Controller`] (run in a @@ -124,13 +207,13 @@ separate thread). There are several types of input events: - other commands (e.g. to switch the screen on or to copy the clipboard). Some of them need to inject input events to the system. To do so, they use the -_hidden_ method [`InputManager.injectInputEvent`] (exposed by our +_hidden_ method [`InputManager.injectInputEvent()`] (exposed by the [`InputManager` wrapper][inject-wrapper]). -[`Controller`]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/Controller.java#L81 +[`Controller`]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/server/src/main/java/com/genymobile/scrcpy/Controller.java [`KeyEvent`]: https://developer.android.com/reference/android/view/KeyEvent.html [`MotionEvent`]: https://developer.android.com/reference/android/view/MotionEvent.html -[`InputManager.injectInputEvent`]: https://android.googlesource.com/platform/frameworks/base/+/oreo-release/core/java/android/hardware/input/InputManager.java#857 +[`InputManager.injectInputEvent()`]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java#L34 [inject-wrapper]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java#L27 @@ -140,126 +223,222 @@ _hidden_ method [`InputManager.injectInputEvent`] (exposed by our The client relies on [SDL], which provides cross-platform API for UI, input events, threading, etc. -The video stream is decoded by [libav] (FFmpeg). +The video and audio streams are decoded by [FFmpeg]. [SDL]: https://www.libsdl.org -[libav]: https://www.libav.org/ +[ffmpeg]: https://ffmpeg.org/ + ### Initialization -On startup, in addition to _libav_ and _SDL_ initialization, the client must -push and start the server on the device, and open two sockets (one for the video -stream, one for control) so that they may communicate. +The client parses the command line arguments, then [runs one of two code +paths][run]: + - scrcpy in "normal" mode ([`scrcpy.c`]) + - scrcpy in [OTG mode](hid-otg.md) ([`scrcpy_otg.c`]) -Note that the client-server roles are expressed at the application level: +[run]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/app/src/main.c#L81-L82 +[`scrcpy.c`]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/app/src/scrcpy.c#L292-L293 +[`scrcpy_otg.c`]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/app/src/usb/scrcpy_otg.c#L51-L52 - - the server _serves_ video stream and handle requests from the client, - - the client _controls_ the device through the server. +In the remaining of this document, we assume that the "normal" mode is used +(read the code for the OTG mode). -However, the roles are reversed at the network level: - - - the client opens a server socket and listen on a port before starting the - server, - - the server connects to the client. - -This role inversion guarantees that the connection will not fail due to race -conditions, and avoids polling. - -_(Note that over TCP/IP, the roles are not reversed, due to a bug in `adb -reverse`. See commit [1038bad] and [issue #5].)_ - -Once the server is connected, it sends the device information (name and initial -screen dimensions). Thus, the client may init the window and renderer, before -the first frame is available. - -To minimize startup time, SDL initialization is performed while listening for -the connection from the server (see commit [90a46b4]). - -[1038bad]: https://github.com/Genymobile/scrcpy/commit/1038bad3850f18717a048a4d5c0f8110e54ee172 -[issue #5]: https://github.com/Genymobile/scrcpy/issues/5 -[90a46b4]: https://github.com/Genymobile/scrcpy/commit/90a46b4c45637d083e877020d85ade52a9a5fa8e +On startup, the client: + - opens the _video_, _audio_ and _control_ sockets; + - pushes and starts the server on the device; + - initializes its components (demuxers, decoders, recorder…). -### Threading +### Video and audio streams -The client uses 4 threads: - - - the **main** thread, executing the SDL event loop, - - the **stream** thread, receiving the video and used for decoding and - recording, - - the **controller** thread, sending _control messages_ to the server, - - the **receiver** thread (managed by the controller), receiving _device - messages_ from the server. - -In addition, another thread can be started if necessary to handle APK -installation or file push requests (via drag&drop on the main window) or to -print the framerate regularly in the console. - - - -### Stream - -The video [stream] is received from the socket (connected to the server on the -device) in a separate thread. - -If a [decoder] is present (i.e. `--no-display` is not set), then it uses _libav_ -to decode the H.264 stream from the socket, and notifies the main thread when a -new frame is available. - -There are two [frames][video_buffer] simultaneously in memory: - - the **decoding** frame, written by the decoder from the decoder thread, - - the **rendering** frame, rendered in a texture from the main thread. - -When a new decoded frame is available, the decoder _swaps_ the decoding and -rendering frame (with proper synchronization). Thus, it immediately starts -to decode a new frame while the main thread renders the last one. - -If a [recorder] is present (i.e. `--record` is enabled), then it muxes the raw -H.264 packet to the output video file. - -[stream]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/stream.h -[decoder]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/decoder.h -[video_buffer]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/video_buffer.h -[recorder]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/recorder.h +Depending on the arguments passed to `scrcpy`, several components may be used. +Here is an overview of the video and audio components: ``` - +----------+ +----------+ - ---> | decoder | ---> | screen | - +---------+ / +----------+ +----------+ - socket ---> | stream | ---- - +---------+ \ +----------+ - ---> | recorder | - +----------+ + V4L2 sink + / + decoder + / \ + VIDEO -------------> demuxer display + \ + recorder + / + AUDIO -------------> demuxer + \ + decoder --- audio player ``` +The _demuxer_ is responsible to extract video and audio packets (read some +header, split the video stream into packets at correct boundaries, etc.). + +The demuxed packets may be sent to a _decoder_ (one per stream, to produce +frames) and to a recorder (receiving both video and audio stream to record a +single file). The packets are encoded on the device (by `MediaCodec`), but when +recording, they are _muxed_ (asynchronously) into a container (MKV or MP4) on +the client side. + +Video frames are sent to the screen/display to be rendered in the scrcpy window. +They may also be sent to a [V4L2 sink](v4l2.md). + +Audio "frames" (an array of decoded samples) are sent to the audio player. + + ### Controller -The [controller] is responsible to send _control messages_ to the device. It +The _controller_ is responsible to send _control messages_ to the device. It runs in a separate thread, to avoid I/O on the main thread. -On SDL event, received on the main thread, the [input manager][inputmanager] -creates appropriate [_control messages_][controlmsg]. It is responsible to -convert SDL events to Android events (using [convert]). It pushes the _control -messages_ to a queue hold by the controller. On its own thread, the controller -takes messages from the queue, that it serializes and sends to the client. - -[controller]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/controller.h -[controlmsg]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/control_msg.h -[inputmanager]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/input_manager.h -[convert]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/convert.h +On SDL event, received on the main thread, the _input manager_ creates +appropriate _control messages_. It is responsible to convert SDL events to +Android events. It then pushes the _control messages_ to a queue hold by the +controller. On its own thread, the controller takes messages from the queue, +that it serializes and sends to the client. -### UI and event loop +## Protocol -Initialization, input events and rendering are all [managed][scrcpy] in the main -thread. +The protocol between the client and the server must be considered _internal_: it +may (and will) change at any time for any reason. Everything may change (the +number of sockets, the order in which the sockets must be opened, the data +format on the wire…) from version to version. A client must always be run with a +matching server version. -Events are handled in the [event loop], which either updates the [screen] or -delegates to the [input manager][inputmanager]. +This section documents the current protocol in scrcpy v2.1. -[scrcpy]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/scrcpy.c -[event loop]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/scrcpy.c#L201 -[screen]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/screen.h +### Connection + +Firstly, the client sets up an adb tunnel: + +```bash +# By default, a reverse redirection: the computer listens, the device connects +adb reverse localabstract:scrcpy_ tcp:27183 + +# As a fallback (or if --force-adb forward is set), a forward redirection: +# the device listens, the computer connects +adb forward tcp:27183 localabstract:scrcpy_ +``` + +(`` is a 31-bit random number, so that it does not fail when several +scrcpy instances start "at the same time" for the same device.) + +Then, up to 3 sockets are opened, in that order: + - a _video_ socket + - an _audio_ socket + - a _control_ socket + +Each one may be disabled (respectively by `--no-video`, `--no-audio` and +`--no-control`, directly or indirectly). For example, if `--no-audio` is set, +then the _video_ socket is opened first, then the _control_ socket. + +On the _first_ socket opened (whichever it is), if the tunnel is _forward_, then +a [dummy byte] is sent from the device to the client. This allows to detect a +connection error (the client connection does not fail as long as there is an adb +forward redirection, even if nothing is listening on the device side). + +Still on this _first_ socket, the device sends some [metadata][device meta] to +the client (currently only the device name, used as the window title, but there +might be other fields in the future). + +[dummy byte]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java#L93 +[device meta]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java#L151 + +You can read the [client][client-connection] and [server][server-connection] +code for more details. + +[client-connection]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/app/src/server.c#L465-L466 +[server-connection]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java#L63 + +Then each socket is used for its intended purpose. + +### Video and audio + +On the _video_ and _audio_ sockets, the device first sends some [codec +metadata]: + - On the _video_ socket, 12 bytes: + - the codec id (`u32`) (H264, H265 or AV1) + - the initial video width (`u32`) + - the initial video height (`u32`) + - On the _audio_ socket, 4 bytes: + - the codec id (`u32`) (OPUS, AAC or RAW) + +[codec metadata]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/server/src/main/java/com/genymobile/scrcpy/Streamer.java#L33-L51 + +Then each packet produced by `MediaCodec` is sent, prefixed by a 12-byte [frame +header]: + - config packet flag (`u1`) + - key frame flag (`u1`) + - PTS (`u62`) + - packet size (`u32`) + +Here is a schema describing the frame header: + +``` + [. . . . . . . .|. . . .]. . . . . . . . . . . . . . . ... + <-------------> <-----> <-----------------------------... + PTS packet raw packet + size + <---------------------> + frame header + +The most significant bits of the PTS are used for packet flags: + + byte 7 byte 6 byte 5 byte 4 byte 3 byte 2 byte 1 byte 0 + CK...... ........ ........ ........ ........ ........ ........ ........ + ^^<-------------------------------------------------------------------> + || PTS + | `- key frame + `-- config packet +``` + +[frame header]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/server/src/main/java/com/genymobile/scrcpy/Streamer.java#L83 + + +### Controls + +Controls messages are sent via a custom binary protocol. + +The only documentation for this protocol is the set of unit tests on both sides: + - `ControlMessage` (from client to device): [serialization](https://github.com/Genymobile/scrcpy/blob/master/app/tests/test_control_msg_serialize.c) | [deserialization](https://github.com/Genymobile/scrcpy/blob/master/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java) + - `DeviceMessage` (from device to client) [serialization](https://github.com/Genymobile/scrcpy/blob/master/server/src/test/java/com/genymobile/scrcpy/DeviceMessageWriterTest.java) | [deserialization](https://github.com/Genymobile/scrcpy/blob/master/app/tests/test_device_msg_deserialize.c) + + +## Standalone server + +Although the server is designed to work for the scrcpy client, it can be used +with any client which uses the same protocol. + +For simplicity, some [server-specific options] have been added to produce raw +streams easily: + - `send_device_meta=false`: disable the device metata (in practice, the device + name) sent on the _first_ socket + - `send_frame_meta=false`: disable the 12-byte header for each packet + - `send_dummy_byte`: disable the dummy byte sent on forward connections + - `send_codec_meta`: disable the codec information (and initial device size for + video) + - `raw_stream`: disable all the above + +[server-specific options]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/server/src/main/java/com/genymobile/scrcpy/Options.java#L309-L329 + +Concretely, here is how to expose a raw H.264 stream on a TCP socket: + +```bash +adb push scrcpy-server-v2.1 /data/local/tmp/scrcpy-server-manual.jar +adb forward tcp:1234 localabstract:scrcpy +adb shell CLASSPATH=/data/local/tmp/scrcpy-server-manual.jar \ + app_process / com.genymobile.scrcpy.Server 2.1 \ + tunnel_forward=true audio=false control=false cleanup=false \ + raw_stream=true max_size=1920 +``` + +As soon as a client connects over TCP on port 1234, the device will start +streaming the video. For example, VLC can play the video (although you will +experience a very high latency, more details [here][vlc-0latency]): + +``` +vlc -Idummy --demux=h264 --network-caching=0 tcp://localhost:1234 +``` + +[vlc-0latency]: https://code.videolan.org/rom1v/vlc/-/merge_requests/20 ## Hack From d046678f85c0730570e807a2d25267280beab086 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 22 Jun 2023 00:10:37 +0200 Subject: [PATCH 1600/2244] Upgrade platform-tools (34.0.3) for Windows Include the latest version of adb in Windows releases. --- app/prebuilt-deps/prepare-adb.sh | 6 +++--- release.mk | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/prebuilt-deps/prepare-adb.sh b/app/prebuilt-deps/prepare-adb.sh index cc139095..f22873c0 100755 --- a/app/prebuilt-deps/prepare-adb.sh +++ b/app/prebuilt-deps/prepare-adb.sh @@ -6,10 +6,10 @@ cd "$DIR" mkdir -p "$PREBUILT_DATA_DIR" cd "$PREBUILT_DATA_DIR" -DEP_DIR=platform-tools-34.0.1 +DEP_DIR=platform-tools-34.0.3 -FILENAME=platform-tools_r34.0.1-windows.zip -SHA256SUM=5dd9c2be744c224fa3a7cbe30ba02d2cb378c763bd0f797a7e47e9f3156a5daa +FILENAME=platform-tools_r34.0.3-windows.zip +SHA256SUM=fce992e93eb786fc9f47df93d83a7b912c46742d45c39d712c02e06d05b72e2b if [[ -d "$DEP_DIR" ]] then diff --git a/release.mk b/release.mk index a467352c..4fe99c89 100644 --- a/release.mk +++ b/release.mk @@ -98,9 +98,9 @@ dist-win32: build-server build-win32 cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-4/win32/bin/avcodec-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-4/win32/bin/avformat-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-4/win32/bin/swresample-4.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/platform-tools-34.0.1/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/platform-tools-34.0.1/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/platform-tools-34.0.1/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/platform-tools-34.0.3/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/platform-tools-34.0.3/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/platform-tools-34.0.3/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/SDL2-2.28.0/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/libusb-1.0.26/libusb-MinGW-Win32/bin/msys-usb-1.0.dll "$(DIST)/$(WIN32_TARGET_DIR)/" @@ -116,9 +116,9 @@ dist-win64: build-server build-win64 cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-4/win64/bin/avcodec-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-4/win64/bin/avformat-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-4/win64/bin/swresample-4.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/platform-tools-34.0.1/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/platform-tools-34.0.1/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/platform-tools-34.0.1/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/platform-tools-34.0.3/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/platform-tools-34.0.3/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/platform-tools-34.0.3/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/SDL2-2.28.0/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/libusb-1.0.26/libusb-MinGW-x64/bin/msys-usb-1.0.dll "$(DIST)/$(WIN64_TARGET_DIR)/" From c0f3c080b63eb70c0941c300e461fc2ea1246cec Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 22 Jun 2023 00:49:11 +0200 Subject: [PATCH 1601/2244] Register DisplayFoldListener only for Android 10+ This listener does not exist on Android < 10, and it makes scrcpy fail. --- .../java/com/genymobile/scrcpy/Device.java | 34 ++++++++++--------- .../scrcpy/wrappers/WindowManager.java | 2 ++ 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index a3b6a270..f817a3ce 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -99,25 +99,27 @@ public final class Device { } }, displayId); - ServiceManager.getWindowManager().registerDisplayFoldListener(new IDisplayFoldListener.Stub() { - @Override - public void onDisplayFoldChanged(int displayId, boolean folded) { - synchronized (Device.this) { - DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(displayId); - if (displayInfo == null) { - Ln.e("Display " + displayId + " not found\n" + LogUtils.buildDisplayListMessage()); - return; - } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + ServiceManager.getWindowManager().registerDisplayFoldListener(new IDisplayFoldListener.Stub() { + @Override + public void onDisplayFoldChanged(int displayId, boolean folded) { + synchronized (Device.this) { + DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(displayId); + if (displayInfo == null) { + Ln.e("Display " + displayId + " not found\n" + LogUtils.buildDisplayListMessage()); + return; + } - screenInfo = ScreenInfo.computeScreenInfo(displayInfo.getRotation(), displayInfo.getSize(), options.getCrop(), - options.getMaxSize(), options.getLockVideoOrientation()); - // notify - if (foldListener != null) { - foldListener.onFoldChanged(displayId, folded); + screenInfo = ScreenInfo.computeScreenInfo(displayInfo.getRotation(), displayInfo.getSize(), options.getCrop(), + options.getMaxSize(), options.getLockVideoOrientation()); + // notify + if (foldListener != null) { + foldListener.onFoldChanged(displayId, folded); + } } } - } - }); + }); + } if (options.getControl() && options.getClipboardAutosync()) { // If control and autosync are enabled, synchronize Android clipboard to the computer automatically 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 d9fd9825..dde26e82 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java @@ -2,6 +2,7 @@ package com.genymobile.scrcpy.wrappers; import com.genymobile.scrcpy.Ln; +import android.annotation.TargetApi; import android.os.IInterface; import android.view.IRotationWatcher; import android.view.IDisplayFoldListener; @@ -110,6 +111,7 @@ public final class WindowManager { } } + @TargetApi(29) public void registerDisplayFoldListener(IDisplayFoldListener foldListener) { try { Class cls = manager.getClass(); From 0ffcfa0f5c444e2ebc919bfcd44a090c8511f02a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 22 Jun 2023 00:51:14 +0200 Subject: [PATCH 1602/2244] Accept failure in rotation or fold registration Do not make scrcpy fail if rotation or display fold listeners could not be registered. --- .../java/com/genymobile/scrcpy/wrappers/WindowManager.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 dde26e82..ce748855 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java @@ -107,7 +107,7 @@ public final class WindowManager { cls.getMethod("watchRotation", IRotationWatcher.class).invoke(manager, rotationWatcher); } } catch (Exception e) { - throw new AssertionError(e); + Ln.e("Could not register rotation watcher", e); } } @@ -117,7 +117,7 @@ public final class WindowManager { Class cls = manager.getClass(); cls.getMethod("registerDisplayFoldListener", IDisplayFoldListener.class).invoke(manager, foldListener); } catch (Exception e) { - throw new AssertionError(e); + Ln.e("Could not register display fold listener", e); } } } From ea59d525bd6910583fdbf901182ff0e102223c62 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 22 Jun 2023 01:07:09 +0200 Subject: [PATCH 1603/2244] Fix code style The code should fit in 80 columns. --- app/src/server.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/server.c b/app/src/server.c index 360e7e7c..4d787ea9 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -533,8 +533,8 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) { if (audio_socket == SC_SOCKET_NONE) { goto fail; } - bool ok = net_connect_intr(&server->intr, audio_socket, tunnel_host, - tunnel_port); + bool ok = net_connect_intr(&server->intr, audio_socket, + tunnel_host, tunnel_port); if (!ok) { goto fail; } From b9315620e2dfae363ba4ddb88d14badc88bd5dee Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 22 Jun 2023 01:13:53 +0200 Subject: [PATCH 1604/2244] Fix adb forward initialization In forward mode, the dummy byte must be written immediately after the first accept(), otherwise the client will wait indefinitely, causing a deadlock (or a timeout). Regression introduced by 8c650e53cd37a53d5c3aa746c30a71c6b742a4e2. --- .../genymobile/scrcpy/DesktopConnection.java | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java b/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java index 20ab1f9c..c3408fff 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java +++ b/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java @@ -64,8 +64,6 @@ public final class DesktopConnection implements Closeable { throws IOException { String socketName = getSocketName(scid); - LocalSocket firstSocket = null; - LocalSocket videoSocket = null; LocalSocket audioSocket = null; LocalSocket controlSocket = null; @@ -74,24 +72,28 @@ public final class DesktopConnection implements Closeable { try (LocalServerSocket localServerSocket = new LocalServerSocket(socketName)) { if (video) { videoSocket = localServerSocket.accept(); - firstSocket = videoSocket; + if (sendDummyByte) { + // send one byte so the client may read() to detect a connection error + videoSocket.getOutputStream().write(0); + sendDummyByte = false; + } } if (audio) { audioSocket = localServerSocket.accept(); - if (firstSocket == null) { - firstSocket = audioSocket; + if (sendDummyByte) { + // send one byte so the client may read() to detect a connection error + audioSocket.getOutputStream().write(0); + sendDummyByte = false; } } if (control) { controlSocket = localServerSocket.accept(); - if (firstSocket == null) { - firstSocket = controlSocket; + if (sendDummyByte) { + // send one byte so the client may read() to detect a connection error + controlSocket.getOutputStream().write(0); + sendDummyByte = false; } } - if (sendDummyByte) { - // send one byte so the client may read() to detect a connection error - firstSocket.getOutputStream().write(0); - } } } else { if (video) { From 2dab1f7024dd7edbd3b630a1a435c14b98f368c1 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 22 Jun 2023 00:16:46 +0200 Subject: [PATCH 1605/2244] Bump version to 2.1 --- app/scrcpy-windows.rc | 2 +- meson.build | 2 +- server/build.gradle | 4 ++-- server/build_without_gradle.sh | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/scrcpy-windows.rc b/app/scrcpy-windows.rc index e9eb1903..12551439 100644 --- a/app/scrcpy-windows.rc +++ b/app/scrcpy-windows.rc @@ -13,7 +13,7 @@ BEGIN VALUE "LegalCopyright", "Romain Vimont, Genymobile" VALUE "OriginalFilename", "scrcpy.exe" VALUE "ProductName", "scrcpy" - VALUE "ProductVersion", "2.0" + VALUE "ProductVersion", "2.1" END END BLOCK "VarFileInfo" diff --git a/meson.build b/meson.build index ac16c23b..983ea4ad 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('scrcpy', 'c', - version: '2.0', + version: '2.1', meson_version: '>= 0.48', default_options: [ 'c_std=c11', diff --git a/server/build.gradle b/server/build.gradle index ce234d10..02e8e381 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -7,8 +7,8 @@ android { applicationId "com.genymobile.scrcpy" minSdkVersion 21 targetSdkVersion 33 - versionCode 20000 - versionName "2.0" + versionCode 20100 + versionName "2.1" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index f13fd0dc..86b95895 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -12,7 +12,7 @@ set -e SCRCPY_DEBUG=false -SCRCPY_VERSION_NAME=2.0 +SCRCPY_VERSION_NAME=2.1 PLATFORM=${ANDROID_PLATFORM:-33} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-33.0.0} From 5764f47fee0f54f0b7e9270576588a67e7b7955f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 22 Jun 2023 00:26:36 +0200 Subject: [PATCH 1606/2244] Update links to v2.1 --- README.md | 2 +- doc/build.md | 6 +++--- doc/windows.md | 12 ++++++------ install_release.sh | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 9002031d..62aa7eac 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# scrcpy (v2.0) +# scrcpy (v2.1) scrcpy diff --git a/doc/build.md b/doc/build.md index 86d9436a..2e8137dd 100644 --- a/doc/build.md +++ b/doc/build.md @@ -233,10 +233,10 @@ install` must be run as root)._ #### Option 2: Use prebuilt server - - [`scrcpy-server-v2.0`][direct-scrcpy-server] - SHA-256: `9e241615f578cd690bb43311000debdecf6a9c50a7082b001952f18f6f21ddc2` + - [`scrcpy-server-v2.1`][direct-scrcpy-server] + SHA-256: `5b8bf1940264b930c71a1c614c57da2247f52b2d4240bca865cc6d366dff6688` -[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v2.0/scrcpy-server-v2.0 +[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v2.1/scrcpy-server-v2.1 Download the prebuilt server somewhere, and specify its path during the Meson configuration: diff --git a/doc/windows.md b/doc/windows.md index 2cbd99b6..8b74c263 100644 --- a/doc/windows.md +++ b/doc/windows.md @@ -4,14 +4,14 @@ Download the [latest release]: - - [`scrcpy-win64-v2.0.zip`][direct-win64] (64-bit) - SHA-256: `ae4c8d37a496b43f8974ba8f07f708e22a9570ba0cddc3dc3a36edbccd4d2a20` - - [`scrcpy-win32-v2.0.zip`][direct-win32] (32-bit) - SHA-256: `15d98c02cb0e0bbd84f8b5d54991e0f6925569b1286a86a40743944fcb1c2d8c` + - [`scrcpy-win64-v2.1.zip`][direct-win64] (64-bit) + SHA-256: `57b98813322c8b5b560ada68714a2cd7b7efe64086fa61d03e389c23212c803d` + - [`scrcpy-win32-v2.1.zip`][direct-win32] (32-bit) + SHA-256: `4d261d391a60ea975440d83cdc22f8250b3c8985f2ece8c7e53d6fb26c0d74ed` [latest release]: https://github.com/Genymobile/scrcpy/releases/latest -[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v2.0/scrcpy-win64-v2.0.zip -[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v2.0/scrcpy-win32-v2.0.zip +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v2.1/scrcpy-win64-v2.1.zip +[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v2.1/scrcpy-win32-v2.1.zip and extract it. diff --git a/install_release.sh b/install_release.sh index 609c9556..f21560fd 100755 --- a/install_release.sh +++ b/install_release.sh @@ -2,8 +2,8 @@ set -e BUILDDIR=build-auto -PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v2.0/scrcpy-server-v2.0 -PREBUILT_SERVER_SHA256=9e241615f578cd690bb43311000debdecf6a9c50a7082b001952f18f6f21ddc2 +PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v2.1/scrcpy-server-v2.1 +PREBUILT_SERVER_SHA256=5b8bf1940264b930c71a1c614c57da2247f52b2d4240bca865cc6d366dff6688 echo "[scrcpy] Downloading prebuilt server..." wget "$PREBUILT_SERVER_URL" -O scrcpy-server From 0049b3ce078d4abaad3cc94a462502951dabc243 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 23 Jun 2023 08:23:22 +0200 Subject: [PATCH 1607/2244] Remove superfluous log This line was committed by error in commit a52053421abee7c96b1071a06cb68bbeac2fd464. --- 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 ce7c2838..db15d5f3 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -147,7 +147,6 @@ public class ScreenEncoder implements Device.RotationListener, Device.FoldListen // Downsizing on error is only enabled if an encoding failure occurs before the first frame (downsizing later could be surprising) int newMaxSize = chooseMaxSizeFallback(screenInfo.getVideoSize()); - Ln.i("newMaxSize = " + newMaxSize); if (newMaxSize == 0) { // Must definitively fail return false; From 808bd14e301351ce63b4fc45ac54d3bf34423e94 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 27 Jun 2023 18:43:22 +0200 Subject: [PATCH 1608/2244] Ignore fold change events for other display ids Scrcpy mirrors a specific display id, it must ignore events for other display ids. Fixes #4120 --- server/src/main/java/com/genymobile/scrcpy/Device.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index f817a3ce..4ab689b0 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -103,6 +103,11 @@ public final class Device { ServiceManager.getWindowManager().registerDisplayFoldListener(new IDisplayFoldListener.Stub() { @Override public void onDisplayFoldChanged(int displayId, boolean folded) { + if (Device.this.displayId != displayId) { + // Ignore events related to other display ids + return; + } + synchronized (Device.this) { DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(displayId); if (displayInfo == null) { From 7b7076ef85684707e314b9ca0ef5066a7c9f8727 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 27 Jun 2023 21:23:42 +0200 Subject: [PATCH 1609/2244] Add direct links to donations --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 62aa7eac..ec8276d8 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,10 @@ For general questions or discussions, you can also use: I'm [@rom1v](https://github.com/rom1v), the author and maintainer of _scrcpy_. If you appreciate this application, you can [support my open source -work][donate]. +work][donate]: + - [GitHub Sponsors](https://github.com/sponsors/rom1v) + - [Liberapay](https://liberapay.com/rom1v/) + - [PayPal](https://paypal.me/rom2v) [donate]: https://blog.rom1v.com/about/#support-my-open-source-work From 85b55b3c4e3b05c155225e035c4a9544f8864268 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 27 Jun 2023 18:38:10 +0200 Subject: [PATCH 1610/2244] Fix possible division by zero On sway (a window manager), SDL_WINDOWEVENT_EXPOSED and SDL_WINDOWEVENT_SIZE_CHANGED might not be called before a mouse event is triggered. As a consequence, the "content rectangle" might not be initialized when the mouse event is processed, causing a division by zero. To avoid the problem, initialize the content rect immediately when the window is shown. Fixes #4115 --- app/src/screen.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/screen.c b/app/src/screen.c index 2724a266..5b7a8808 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -488,6 +488,7 @@ sc_screen_show_initial_window(struct sc_screen *screen) { } SDL_ShowWindow(screen->window); + sc_screen_update_content_rect(screen); } void @@ -848,6 +849,8 @@ sc_screen_convert_drawable_to_frame_coords(struct sc_screen *screen, int32_t w = screen->content_size.width; int32_t h = screen->content_size.height; + // screen->rect must be initialized to avoid a division by zero + assert(screen->rect.w && screen->rect.h); x = (int64_t) (x - screen->rect.x) * w / screen->rect.w; y = (int64_t) (y - screen->rect.y) * h / screen->rect.h; From 625934fb1b078a5d66bc4864e8082840d57d4557 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Skwar?= <64905406+benni347@users.noreply.github.com> Date: Fri, 30 Jun 2023 13:49:46 +0000 Subject: [PATCH 1611/2244] Fix fedora package in build instructions In Fedora, the package is libusb1-devel. Fixes #4131 PR #4132 Signed-off-by: Romain Vimont --- doc/build.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/build.md b/doc/build.md index 2e8137dd..ba200cf1 100644 --- a/doc/build.md +++ b/doc/build.md @@ -77,7 +77,7 @@ pip3 install meson sudo dnf install https://download1.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm # client build dependencies -sudo dnf install SDL2-devel ffms2-devel libusb-devel meson gcc make +sudo dnf install SDL2-devel ffms2-devel libusb1-devel meson gcc make # server build dependencies sudo dnf install java-devel From fe6e9acb36298a19f2e667ace724b249a04a7f30 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 4 Jul 2023 18:20:23 +0200 Subject: [PATCH 1612/2244] Log device selection at INFO level The selected device should be logged by default. --- app/src/adb/adb.c | 4 ++-- app/src/usb/scrcpy_otg.c | 4 ---- app/src/usb/usb.c | 4 ++-- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/app/src/adb/adb.c b/app/src/adb/adb.c index e8775a01..6bc6dd62 100644 --- a/app/src/adb/adb.c +++ b/app/src/adb/adb.c @@ -628,8 +628,8 @@ sc_adb_select_device(struct sc_intr *intr, return false; } - LOGD("ADB device found:"); - sc_adb_devices_log(SC_LOG_LEVEL_DEBUG, vec.data, vec.size); + LOGI("ADB device found:"); + sc_adb_devices_log(SC_LOG_LEVEL_INFO, vec.data, vec.size); // Move devics into out_device (do not destroy device) sc_adb_device_move(out_device, device); diff --git a/app/src/usb/scrcpy_otg.c b/app/src/usb/scrcpy_otg.c index 35d8d4cc..6a7fd79b 100644 --- a/app/src/usb/scrcpy_otg.c +++ b/app/src/usb/scrcpy_otg.c @@ -105,10 +105,6 @@ scrcpy_otg(struct scrcpy_options *options) { usb_device_initialized = true; - LOGI("USB device: %s (%04x:%04x) %s %s", usb_device.serial, - (unsigned) usb_device.vid, (unsigned) usb_device.pid, - usb_device.manufacturer, usb_device.product); - ok = sc_usb_connect(&s->usb, usb_device.device, &cbs, NULL); if (!ok) { goto end; diff --git a/app/src/usb/usb.c b/app/src/usb/usb.c index 190a4108..310ed5d9 100644 --- a/app/src/usb/usb.c +++ b/app/src/usb/usb.c @@ -213,8 +213,8 @@ sc_usb_select_device(struct sc_usb *usb, const char *serial, assert(sel_count == 1); // sel_idx is valid only if sel_count == 1 struct sc_usb_device *device = &vec.data[sel_idx]; - LOGD("USB device found:"); - sc_usb_devices_log(SC_LOG_LEVEL_DEBUG, vec.data, vec.size); + LOGI("USB device found:"); + sc_usb_devices_log(SC_LOG_LEVEL_INFO, vec.data, vec.size); // Move device into out_device (do not destroy device) sc_usb_device_move(out_device, device); From 01d785d9a3407dc0389abee688f1a58a5e4f8923 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 7 Jul 2023 18:21:17 +0200 Subject: [PATCH 1613/2244] Increase attempts to start AudioRecord Making the shell app foreground (specific for Android 11) may take more than 300ms on some devices, so increase the number of attempts from 3 to 5 (separated by 100ms). Fixes #4147 Refs #3796 Refs 02f4ff7534649153d6f87b05a0757431a2d0ee5f --- server/src/main/java/com/genymobile/scrcpy/AudioCapture.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java b/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java index 7b20cce4..5575ffb6 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java @@ -118,7 +118,7 @@ public final class AudioCapture { if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { startWorkaroundAndroid11(); try { - tryStartRecording(3, 100); + tryStartRecording(5, 100); } finally { stopWorkaroundAndroid11(); } From 7e936fa879d9ee37608d9d0ce36f66ea06a6317d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 13 Jul 2023 21:43:52 +0200 Subject: [PATCH 1614/2244] Fix meizu deadlock Some devices (Meizu) assume that the video encoding thread has a Looper. By moving video encoding to a separate thread, commit feab87053abcceded41342d9d856763dedc09187 broke this assumption. Call Looper.prepare() from this thread to fix the problem. Fixes #4143 --- .../src/main/java/com/genymobile/scrcpy/ScreenEncoder.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index db15d5f3..5a9db10d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -8,6 +8,7 @@ import android.media.MediaCodecInfo; import android.media.MediaFormat; import android.os.Build; import android.os.IBinder; +import android.os.Looper; import android.os.SystemClock; import android.view.Surface; @@ -285,6 +286,10 @@ public class ScreenEncoder implements Device.RotationListener, Device.FoldListen @Override public void start(TerminationListener listener) { thread = new Thread(() -> { + // Some devices (Meizu) deadlock if the video encoding thread has no Looper + // + Looper.prepare(); + try { streamScreen(); } catch (ConfigurationException e) { From d391fc3b695679f4770ed2e3950b135ebbda2f27 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 14 Jul 2023 18:58:58 +0200 Subject: [PATCH 1615/2244] Bump version to 2.1.1 --- app/scrcpy-windows.rc | 2 +- meson.build | 2 +- server/build.gradle | 4 ++-- server/build_without_gradle.sh | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/scrcpy-windows.rc b/app/scrcpy-windows.rc index 12551439..e3929119 100644 --- a/app/scrcpy-windows.rc +++ b/app/scrcpy-windows.rc @@ -13,7 +13,7 @@ BEGIN VALUE "LegalCopyright", "Romain Vimont, Genymobile" VALUE "OriginalFilename", "scrcpy.exe" VALUE "ProductName", "scrcpy" - VALUE "ProductVersion", "2.1" + VALUE "ProductVersion", "2.1.1" END END BLOCK "VarFileInfo" diff --git a/meson.build b/meson.build index 983ea4ad..847e33a1 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('scrcpy', 'c', - version: '2.1', + version: '2.1.1', meson_version: '>= 0.48', default_options: [ 'c_std=c11', diff --git a/server/build.gradle b/server/build.gradle index 02e8e381..4a05d2a5 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -7,8 +7,8 @@ android { applicationId "com.genymobile.scrcpy" minSdkVersion 21 targetSdkVersion 33 - versionCode 20100 - versionName "2.1" + versionCode 20101 + versionName "2.1.1" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index 86b95895..543f12ab 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -12,7 +12,7 @@ set -e SCRCPY_DEBUG=false -SCRCPY_VERSION_NAME=2.1 +SCRCPY_VERSION_NAME=2.1.1 PLATFORM=${ANDROID_PLATFORM:-33} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-33.0.0} From 637f48f360d0dce1b0927a9df24236ad48ab2130 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 14 Jul 2023 23:09:44 +0200 Subject: [PATCH 1616/2244] Update links to v2.1.1 --- README.md | 2 +- doc/build.md | 6 +++--- doc/windows.md | 12 ++++++------ install_release.sh | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index ec8276d8..9e49a15d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# scrcpy (v2.1) +# scrcpy (v2.1.1) scrcpy diff --git a/doc/build.md b/doc/build.md index ba200cf1..d65cdc93 100644 --- a/doc/build.md +++ b/doc/build.md @@ -233,10 +233,10 @@ install` must be run as root)._ #### Option 2: Use prebuilt server - - [`scrcpy-server-v2.1`][direct-scrcpy-server] - SHA-256: `5b8bf1940264b930c71a1c614c57da2247f52b2d4240bca865cc6d366dff6688` + - [`scrcpy-server-v2.1.1`][direct-scrcpy-server] + SHA-256: `9558db6c56743a1dc03b38f59801fb40e91cc891f8fc0c89e5b0b067761f148e` -[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v2.1/scrcpy-server-v2.1 +[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v2.1.1/scrcpy-server-v2.1.1 Download the prebuilt server somewhere, and specify its path during the Meson configuration: diff --git a/doc/windows.md b/doc/windows.md index 8b74c263..7525334d 100644 --- a/doc/windows.md +++ b/doc/windows.md @@ -4,14 +4,14 @@ Download the [latest release]: - - [`scrcpy-win64-v2.1.zip`][direct-win64] (64-bit) - SHA-256: `57b98813322c8b5b560ada68714a2cd7b7efe64086fa61d03e389c23212c803d` - - [`scrcpy-win32-v2.1.zip`][direct-win32] (32-bit) - SHA-256: `4d261d391a60ea975440d83cdc22f8250b3c8985f2ece8c7e53d6fb26c0d74ed` + - [`scrcpy-win64-v2.1.1.zip`][direct-win64] (64-bit) + SHA-256: `f77281e1bce2f9934617699c581f063d5b327f012eff602ee98fb2ef550c25c2` + - [`scrcpy-win32-v2.1.1.zip`][direct-win32] (32-bit) + SHA-256: `ef7ae7fbe9449f2643febdc2244fb186d1a746a3c736394150cfd14f06d3c943` [latest release]: https://github.com/Genymobile/scrcpy/releases/latest -[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v2.1/scrcpy-win64-v2.1.zip -[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v2.1/scrcpy-win32-v2.1.zip +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v2.1.1/scrcpy-win64-v2.1.1.zip +[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v2.1.1/scrcpy-win32-v2.1.1.zip and extract it. diff --git a/install_release.sh b/install_release.sh index f21560fd..24a3197b 100755 --- a/install_release.sh +++ b/install_release.sh @@ -2,8 +2,8 @@ set -e BUILDDIR=build-auto -PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v2.1/scrcpy-server-v2.1 -PREBUILT_SERVER_SHA256=5b8bf1940264b930c71a1c614c57da2247f52b2d4240bca865cc6d366dff6688 +PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v2.1.1/scrcpy-server-v2.1.1 +PREBUILT_SERVER_SHA256=9558db6c56743a1dc03b38f59801fb40e91cc891f8fc0c89e5b0b067761f148e echo "[scrcpy] Downloading prebuilt server..." wget "$PREBUILT_SERVER_URL" -O scrcpy-server From c14668b17779d9fe8f3e209bbe18669c443ddf8b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 14 Jul 2023 23:26:52 +0200 Subject: [PATCH 1617/2244] Move display section to video documentation --- doc/device.md | 19 ------------------- doc/video.md | 19 +++++++++++++++++++ 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/doc/device.md b/doc/device.md index b6ae2338..32b5ebc1 100644 --- a/doc/device.md +++ b/doc/device.md @@ -125,25 +125,6 @@ autoadb scrcpy -s '{}' [AutoAdb]: https://github.com/rom1v/autoadb -## Display - -If several displays are available on the Android device, it is possible to -select the display to mirror: - -```bash -scrcpy --display=1 -``` - -The list of display ids can be retrieved by: - -```bash -scrcpy --list-displays -``` - -A secondary display may only be controlled if the device runs at least Android -10 (otherwise it is mirrored as read-only). - - ## Actions Some command line arguments perform actions on the device itself while scrcpy is diff --git a/doc/video.md b/doc/video.md index 67372a5c..060e2778 100644 --- a/doc/video.md +++ b/doc/video.md @@ -134,6 +134,25 @@ phone, landscape for a tablet). If `--max-size` is also specified, resizing is applied after cropping. +## Display + +If several displays are available on the Android device, it is possible to +select the display to mirror: + +```bash +scrcpy --display=1 +``` + +The list of display ids can be retrieved by: + +```bash +scrcpy --list-displays +``` + +A secondary display may only be controlled if the device runs at least Android +10 (otherwise it is mirrored as read-only). + + ## Buffering By default, there is no video buffering, to get the lowest possible latency. From 328ed3650dd8abbdb138f629b4e5aa622f172cae Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 14 Jul 2023 23:29:09 +0200 Subject: [PATCH 1618/2244] Extract device connection to a separate doc page Create a new "Connection" documentation page. --- README.md | 3 +- doc/connection.md | 125 +++++++++++++++++++++++++++++++++++++++++ doc/device.md | 139 ++-------------------------------------------- 3 files changed, 132 insertions(+), 135 deletions(-) create mode 100644 doc/connection.md diff --git a/README.md b/README.md index 9e49a15d..412c186b 100644 --- a/README.md +++ b/README.md @@ -68,10 +68,11 @@ mode](doc/hid-otg.md#otg). The application provides a lot of features and configuration options. They are documented in the following pages: - - [Device](doc/device.md) + - [Connection](doc/connection.md) - [Video](doc/video.md) - [Audio](doc/audio.md) - [Control](doc/control.md) + - [Device](doc/device.md) - [Window](doc/window.md) - [Recording](doc/recording.md) - [Tunnels](doc/tunnels.md) diff --git a/doc/connection.md b/doc/connection.md new file mode 100644 index 00000000..90ced010 --- /dev/null +++ b/doc/connection.md @@ -0,0 +1,125 @@ +# Connection + +## Selection + +If exactly one device is connected (i.e. listed by `adb devices`), then it is +automatically selected. + +However, if there are multiple devices connected, you must specify the one to +use in one of 4 ways: + - by its serial: + ```bash + scrcpy --serial=0123456789abcdef + scrcpy -s 0123456789abcdef # short version + + # the serial is the ip:port if connected over TCP/IP (same behavior as adb) + scrcpy --serial=192.168.1.1:5555 + ``` + - the one connected over USB (if there is exactly one): + ```bash + scrcpy --select-usb + scrcpy -d # short version + ``` + - the one connected over TCP/IP (if there is exactly one): + ```bash + scrcpy --select-tcpip + scrcpy -e # short version + ``` + - a device already listening on TCP/IP (see [below](#tcpip-wireless)): + ```bash + scrcpy --tcpip=192.168.1.1:5555 + scrcpy --tcpip=192.168.1.1 # default port is 5555 + ``` + +The serial may also be provided via the environment variable `ANDROID_SERIAL` +(also used by `adb`): + +```bash +# in bash +export ANDROID_SERIAL=0123456789abcdef +scrcpy +``` + +```cmd +:: in cmd +set ANDROID_SERIAL=0123456789abcdef +scrcpy +``` + +```powershell +# in PowerShell +$env:ANDROID_SERIAL = '0123456789abcdef' +scrcpy +``` + + +## TCP/IP (wireless) + +_Scrcpy_ uses `adb` to communicate with the device, and `adb` can [connect] to a +device over TCP/IP. The device must be connected on the same network as the +computer. + +[connect]: https://developer.android.com/studio/command-line/adb.html#wireless + + +### Automatic + +An option `--tcpip` allows to configure the connection automatically. There are +two variants. + +If the device (accessible at 192.168.1.1 in this example) already listens on a +port (typically 5555) for incoming _adb_ connections, then run: + +```bash +scrcpy --tcpip=192.168.1.1 # default port is 5555 +scrcpy --tcpip=192.168.1.1:5555 +``` + +If _adb_ TCP/IP mode is disabled on the device (or if you don't know the IP +address), connect the device over USB, then run: + +```bash +scrcpy --tcpip # without arguments +``` + +It will automatically find the device IP address and adb port, enable TCP/IP +mode if necessary, then connect to the device before starting. + + +### Manual + +Alternatively, it is possible to enable the TCP/IP connection manually using +`adb`: + +1. Plug the device into a USB port on your computer. +2. Connect the device to the same Wi-Fi network as your computer. +3. Get your device IP address, in Settings → About phone → Status, or by + executing this command: + + ```bash + adb shell ip route | awk '{print $9}' + ``` + +4. Enable `adb` over TCP/IP on your device: `adb tcpip 5555`. +5. Unplug your device. +6. Connect to your device: `adb connect DEVICE_IP:5555` _(replace `DEVICE_IP` +with the device IP address you found)_. +7. Run `scrcpy` as usual. +8. Run `adb disconnect` once you're done. + +Since Android 11, a [wireless debugging option][adb-wireless] allows to bypass +having to physically connect your device directly to your computer. + +[adb-wireless]: https://developer.android.com/studio/command-line/adb#wireless-android11-command-line + + +## Autostart + +A small tool (by the scrcpy author) allows to run arbitrary commands whenever a +new Android device is connected: [AutoAdb]. It can be used to start scrcpy: + +```bash +autoadb scrcpy -s '{}' +``` + +[AutoAdb]: https://github.com/rom1v/autoadb diff --git a/doc/device.md b/doc/device.md index 32b5ebc1..988ad417 100644 --- a/doc/device.md +++ b/doc/device.md @@ -1,137 +1,9 @@ # Device -## Selection - -If exactly one device is connected (i.e. listed by `adb devices`), then it is -automatically selected. - -However, if there are multiple devices connected, you must specify the one to -use in one of 4 ways: - - by its serial: - ```bash - scrcpy --serial=0123456789abcdef - scrcpy -s 0123456789abcdef # short version - - # the serial is the ip:port if connected over TCP/IP (same behavior as adb) - scrcpy --serial=192.168.1.1:5555 - ``` - - the one connected over USB (if there is exactly one): - ```bash - scrcpy --select-usb - scrcpy -d # short version - ``` - - the one connected over TCP/IP (if there is exactly one): - ```bash - scrcpy --select-tcpip - scrcpy -e # short version - ``` - - a device already listening on TCP/IP (see [below](#tcpip-wireless)): - ```bash - scrcpy --tcpip=192.168.1.1:5555 - scrcpy --tcpip=192.168.1.1 # default port is 5555 - ``` - -The serial may also be provided via the environment variable `ANDROID_SERIAL` -(also used by `adb`): - -```bash -# in bash -export ANDROID_SERIAL=0123456789abcdef -scrcpy -``` - -```cmd -:: in cmd -set ANDROID_SERIAL=0123456789abcdef -scrcpy -``` - -```powershell -# in PowerShell -$env:ANDROID_SERIAL = '0123456789abcdef' -scrcpy -``` - - -## TCP/IP (wireless) - -_Scrcpy_ uses `adb` to communicate with the device, and `adb` can [connect] to a -device over TCP/IP. The device must be connected on the same network as the -computer. - -[connect]: https://developer.android.com/studio/command-line/adb.html#wireless - - -### Automatic - -An option `--tcpip` allows to configure the connection automatically. There are -two variants. - -If the device (accessible at 192.168.1.1 in this example) already listens on a -port (typically 5555) for incoming _adb_ connections, then run: - -```bash -scrcpy --tcpip=192.168.1.1 # default port is 5555 -scrcpy --tcpip=192.168.1.1:5555 -``` - -If _adb_ TCP/IP mode is disabled on the device (or if you don't know the IP -address), connect the device over USB, then run: - -```bash -scrcpy --tcpip # without arguments -``` - -It will automatically find the device IP address and adb port, enable TCP/IP -mode if necessary, then connect to the device before starting. - - -### Manual - -Alternatively, it is possible to enable the TCP/IP connection manually using -`adb`: - -1. Plug the device into a USB port on your computer. -2. Connect the device to the same Wi-Fi network as your computer. -3. Get your device IP address, in Settings → About phone → Status, or by - executing this command: - - ```bash - adb shell ip route | awk '{print $9}' - ``` - -4. Enable `adb` over TCP/IP on your device: `adb tcpip 5555`. -5. Unplug your device. -6. Connect to your device: `adb connect DEVICE_IP:5555` _(replace `DEVICE_IP` -with the device IP address you found)_. -7. Run `scrcpy` as usual. -8. Run `adb disconnect` once you're done. - -Since Android 11, a [wireless debugging option][adb-wireless] allows to bypass -having to physically connect your device directly to your computer. - -[adb-wireless]: https://developer.android.com/studio/command-line/adb#wireless-android11-command-line - - -## Autostart - -A small tool (by the scrcpy author) allows to run arbitrary commands whenever a -new Android device is connected: [AutoAdb]. It can be used to start scrcpy: - -```bash -autoadb scrcpy -s '{}' -``` - -[AutoAdb]: https://github.com/rom1v/autoadb - - -## Actions - Some command line arguments perform actions on the device itself while scrcpy is running. - -### Stay awake +## Stay awake To prevent the device from sleeping after a delay **when the device is plugged in**: @@ -147,7 +19,7 @@ If the device is not plugged in (i.e. only connected over TCP/IP), `--stay-awake` has no effect (this is the Android behavior). -### Turn screen off +## Turn screen off It is possible to turn the device screen off while mirroring on start with a command-line option: @@ -175,7 +47,7 @@ scrcpy -Sw # short version ``` -### Show touches +## Show touches For presentations, it may be useful to show physical touches (on the physical device). Android exposes this feature in _Developers options_. @@ -191,7 +63,7 @@ scrcpy -t # short version Note that it only shows _physical_ touches (by a finger on the device). -### Power off on close +## Power off on close To turn the device screen off when closing _scrcpy_: @@ -199,11 +71,10 @@ To turn the device screen off when closing _scrcpy_: scrcpy --power-off-on-close ``` -### Power on on start +## Power on on start By default, on start, the device is powered on. To prevent this behavior: ```bash scrcpy --no-power-on ``` - From ad05a018003a66b0a5f8afefb0d2f16a392d3077 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 14 Jul 2023 23:33:04 +0200 Subject: [PATCH 1619/2244] Add Encoder section This will allow to reference the encoder section directly in issues. --- doc/audio.md | 12 +++++++----- doc/video.md | 13 ++++++++----- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/doc/audio.md b/doc/audio.md index 6bb17a87..357cd4ea 100644 --- a/doc/audio.md +++ b/doc/audio.md @@ -77,6 +77,13 @@ In particular, if you get the following error: then your device has no Opus encoder: try `scrcpy --audio-codec=aac`. +For advanced usage, to pass arbitrary parameters to the [`MediaFormat`], +check `--audio-codec-options` in the manpage or in `scrcpy --help`. + +[`MediaFormat`]: https://developer.android.com/reference/android/media/MediaFormat + + +## Encoder Several encoders may be available on the device. They can be listed by: @@ -90,11 +97,6 @@ To select a specific encoder: scrcpy --audio-codec=opus --audio-encoder='c2.android.opus.encoder' ``` -For advanced usage, to pass arbitrary parameters to the [`MediaFormat`], -check `--audio-codec-options` in the manpage or in `scrcpy --help`. - -[`MediaFormat`]: https://developer.android.com/reference/android/media/MediaFormat - ## Bit rate diff --git a/doc/video.md b/doc/video.md index 060e2778..57af5c9f 100644 --- a/doc/video.md +++ b/doc/video.md @@ -66,6 +66,14 @@ scrcpy --video-codec=av1 H265 may provide better quality, but H264 should provide lower latency. AV1 encoders are not common on current Android devices. +For advanced usage, to pass arbitrary parameters to the [`MediaFormat`], +check `--video-codec-options` in the manpage or in `scrcpy --help`. + +[`MediaFormat`]: https://developer.android.com/reference/android/media/MediaFormat + + +## Encoder + Several encoders may be available on the device. They can be listed by: ```bash @@ -79,11 +87,6 @@ try another one: scrcpy --video-codec=h264 --video-encoder='OMX.qcom.video.encoder.avc' ``` -For advanced usage, to pass arbitrary parameters to the [`MediaFormat`], -check `--video-codec-options` in the manpage or in `scrcpy --help`. - -[`MediaFormat`]: https://developer.android.com/reference/android/media/MediaFormat - ## Rotation From fcdf847dd39daf61b936ec7fe7d34f41705ce0ae Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 14 Jul 2023 23:37:19 +0200 Subject: [PATCH 1620/2244] Add missing syntax highlighting in audio doc --- doc/audio.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/audio.md b/doc/audio.md index 357cd4ea..cb6cde95 100644 --- a/doc/audio.md +++ b/doc/audio.md @@ -93,7 +93,7 @@ scrcpy --list-encoders To select a specific encoder: -``` +```bash scrcpy --audio-codec=opus --audio-encoder='c2.android.opus.encoder' ``` From 1ee46970e373ea3c34c3d9b632fef34982d7a52b Mon Sep 17 00:00:00 2001 From: Aritz T <59333567+eltrevii@users.noreply.github.com> Date: Tue, 18 Jul 2023 18:09:54 +0200 Subject: [PATCH 1621/2244] Fix TCP/IP link in README Refs 328ed3650dd8abbdb138f629b4e5aa622f172cae PR #4173 Signed-off-by: Romain Vimont --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 412c186b..9f1ba6b4 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ _pronounced "**scr**een **c**o**py**"_ This application mirrors Android devices (video and audio) connected via -USB or [over TCP/IP](doc/device.md#tcpip-wireless), and allows to control the +USB or [over TCP/IP](doc/connection.md#tcpip-wireless), and allows to control the device with the keyboard and the mouse of the computer. It does not require any _root_ access. It works on _Linux_, _Windows_ and _macOS_. From 110b3a16f6d02124a4567d2ab79fcb74d78f949f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 28 Jul 2023 14:45:33 +0200 Subject: [PATCH 1622/2244] Do not disable controls without video playback Some control messages can still be used even when video playback is disabled (i.e. there is no window), for example to turn the screen off. This reverts commit 92483fe11b6fd6bae5ef775ccaff78fefa92aad4 (semantically). Fixes #4175 --- app/src/cli.c | 6 ------ app/src/scrcpy.c | 4 +--- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index 37a98426..09f853f5 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -2022,12 +2022,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], opts->audio_playback = false; } - if (!opts->video_playback && !otg) { - // If video playback is disabled and OTG are disabled, then there is - // no way to control the device. - opts->control = false; - } - if (opts->video && !opts->video_playback && !opts->record_filename && !v4l2) { LOGI("No video playback, no recording, no V4L2 sink: video disabled"); diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index fd310c46..d68a2424 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -448,9 +448,7 @@ scrcpy(struct scrcpy_options *options) { struct sc_file_pusher *fp = NULL; - // control implies video playback - assert(!options->control || options->video_playback); - if (options->control) { + if (options->video_playback && options->control) { if (!sc_file_pusher_init(&s->file_pusher, serial, options->push_target)) { goto end; From 0983f0a194db154823842a4728f3c2bb5a3b1cc3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 1 Aug 2023 12:05:16 +0200 Subject: [PATCH 1623/2244] Report device disconnection on audio EOS If --no-video was set, then device disconnection was not reported. To avoid the problem, report device disconnection also on audio end-of-stream (EOS). If both video and audio are enabled, then a device disconnection event will be sent twice, but only the first one will be handled (since it makes scrcpy exit). Fixes #4207 --- app/src/scrcpy.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index d68a2424..aabb7c5a 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -252,7 +252,9 @@ sc_audio_demuxer_on_ended(struct sc_demuxer *demuxer, // Contrary to the video demuxer, keep mirroring if only the audio fails // (unless --require-audio is set). - if (status == SC_DEMUXER_STATUS_ERROR + if (status == SC_DEMUXER_STATUS_EOS) { + PUSH_EVENT(SC_EVENT_DEVICE_DISCONNECTED); + } else if (status == SC_DEMUXER_STATUS_ERROR || (status == SC_DEMUXER_STATUS_DISABLED && options->require_audio)) { PUSH_EVENT(SC_EVENT_DEMUXER_ERROR); From 36670dda40c405f7fbcf622a4553651284f8aa96 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 7 Aug 2023 20:22:17 +0200 Subject: [PATCH 1624/2244] Fix warning typo A parenthesis was missing. --- app/src/display.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/display.c b/app/src/display.c index dabc2cb7..cf26e776 100644 --- a/app/src/display.c +++ b/app/src/display.c @@ -53,7 +53,7 @@ sc_display_init(struct sc_display *display, SDL_Window *window, bool mipmaps) { display->mipmaps = true; } else { LOGW("Trilinear filtering disabled " - "(OpenGL 3.0+ or ES 2.0+ required"); + "(OpenGL 3.0+ or ES 2.0+ required)"); } } else { LOGI("Trilinear filtering disabled"); From 111d02fca46c4cff50adbbafa946eb8d451f004a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 22 Aug 2023 18:52:03 +0200 Subject: [PATCH 1625/2244] Add missing 'final' in Java classes For consistency. --- .../java/com/genymobile/scrcpy/wrappers/ActivityManager.java | 2 +- .../java/com/genymobile/scrcpy/wrappers/ClipboardManager.java | 2 +- .../java/com/genymobile/scrcpy/wrappers/ContentProvider.java | 2 +- .../java/com/genymobile/scrcpy/wrappers/StatusBarManager.java | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java index aaf83d66..75115618 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java @@ -17,7 +17,7 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @SuppressLint("PrivateApi,DiscouragedPrivateApi") -public class ActivityManager { +public final class ActivityManager { private final IInterface manager; private Method getContentProviderExternalMethod; 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 7b750975..eae66858 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java @@ -11,7 +11,7 @@ import android.os.IInterface; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -public class ClipboardManager { +public final class ClipboardManager { private final IInterface manager; private Method getPrimaryClipMethod; private Method setPrimaryClipMethod; diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java index 4917f5eb..8171988e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java @@ -14,7 +14,7 @@ import java.io.Closeable; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -public class ContentProvider implements Closeable { +public final class ContentProvider implements Closeable { public static final String TABLE_SYSTEM = "system"; public static final String TABLE_SECURE = "secure"; diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java index 7a19e6e5..9126d5ed 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java @@ -7,7 +7,7 @@ import android.os.IInterface; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -public class StatusBarManager { +public final class StatusBarManager { private final IInterface manager; private Method expandNotificationsPanelMethod; From a7c3c9a54c928124e00fe127833b655bf3ea40c6 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 22 Aug 2023 20:08:50 +0200 Subject: [PATCH 1626/2244] Make fillBaseContext() method private This is consistent with fillAppInfo() and fillAppContext(), which are also private. --- 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 b8294a87..74c0202a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java +++ b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java @@ -154,7 +154,7 @@ public final class Workarounds { } } - public static void fillBaseContext() { + private static void fillBaseContext() { try { fillActivityThread(); From 1650b7c0581df65e9ab5df92f2d81cca4c150ac7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 29 Jun 2023 21:36:52 +0200 Subject: [PATCH 1627/2244] Add --pause-on-exit Add an option to make scrcpy pause on exit. Three behaviors are possible: - always pause on exit: --pause-on-exit --pause-on-exit=true - never pause on exit: (no option) --pause-on-exit=false - pause when scrcpy returns with an error (a non-zero exit code): --pause-on-exit=if-error This is useful to prevent the terminal window from automatically closing, so that error messages can be read. Refs #3817 Refs #3822 PR #4130 --- app/data/bash-completion/scrcpy | 6 +++ app/data/zsh-completion/_scrcpy | 1 + app/scrcpy.1 | 10 +++++ app/src/cli.c | 80 +++++++++++++++++++++++++++++++++ app/src/cli.h | 7 +++ app/src/main.c | 28 +++++++++--- 6 files changed, 125 insertions(+), 7 deletions(-) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index 003f9d73..a44ff6e5 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -44,6 +44,8 @@ _scrcpy() { --no-video-playback --otg -p --port= + --pause-on-exit + --pause-on-exit= --power-off-on-close --prefer-text --print-fps @@ -97,6 +99,10 @@ _scrcpy() { COMPREPLY=($(compgen -W 'unlocked initial 0 1 2 3' -- "$cur")) return ;; + --pause-on-exit) + COMPREPLY=($(compgen -W 'true false if-error' -- "$cur")) + return + ;; -r|--record) COMPREPLY=($(compgen -f -- "$cur")) return diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 81142851..ac40e903 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -50,6 +50,7 @@ arguments=( '--no-video-playback[Disable video playback]' '--otg[Run in OTG mode \(simulating physical keyboard and mouse\)]' {-p,--port=}'[\[port\[\:port\]\] Set the TCP port \(range\) used by the client to listen]' + '--pause-on-exit=[Make scrcpy pause before exiting]:mode:(true false if-error)' '--power-off-on-close[Turn the device screen off when closing scrcpy]' '--prefer-text[Inject alpha characters and space as text events instead of key events]' '--print-fps[Start FPS counter, to print frame logs to the console]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index c1378d8b..7ae56a27 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -267,6 +267,16 @@ Set the TCP port (range) used by the client to listen. Default is 27183:27199. +.TP +\fB\-\-pause\-on\-exit\fR[=\fImode\fR] +Configure pause on exit. Possible values are "true" (always pause on exit), "false" (never pause on exit) and "if-error" (pause only if an error occured). + +This is useful to prevent the terminal window from automatically closing, so that error messages can be read. + +Default is "false". + +Passing the option without argument is equivalent to passing "true". + .TP .B \-\-power\-off\-on\-close Turn the device screen off when closing scrcpy. diff --git a/app/src/cli.c b/app/src/cli.c index 09f853f5..86b720fc 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -79,6 +79,7 @@ enum { OPT_AUDIO_SOURCE, OPT_KILL_ADB_ON_CLOSE, OPT_TIME_LIMIT, + OPT_PAUSE_ON_EXIT, }; struct sc_option { @@ -463,6 +464,20 @@ static const struct sc_option options[] = { "Default is " STR(DEFAULT_LOCAL_PORT_RANGE_FIRST) ":" STR(DEFAULT_LOCAL_PORT_RANGE_LAST) ".", }, + { + .longopt_id = OPT_PAUSE_ON_EXIT, + .longopt = "pause-on-exit", + .argdesc = "mode", + .optional_arg = true, + .text = "Configure pause on exit. Possible values are \"true\" (always " + "pause on exit), \"false\" (never pause on exit) and " + "\"if-error\" (pause only if an error occured).\n" + "This is useful to prevent the terminal window from " + "automatically closing, so that error messages can be read.\n" + "Default is \"false\".\n" + "Passing the option without argument is equivalent to passing " + "\"true\".", + }, { .longopt_id = OPT_POWER_OFF_ON_CLOSE, .longopt = "power-off-on-close", @@ -1637,6 +1652,29 @@ parse_time_limit(const char *s, sc_tick *tick) { return true; } +static bool +parse_pause_on_exit(const char *s, enum sc_pause_on_exit *pause_on_exit) { + if (!s || !strcmp(s, "true")) { + *pause_on_exit = SC_PAUSE_ON_EXIT_TRUE; + return true; + } + + if (!strcmp(s, "false")) { + *pause_on_exit = SC_PAUSE_ON_EXIT_FALSE; + return true; + } + + if (!strcmp(s, "if-error")) { + *pause_on_exit = SC_PAUSE_ON_EXIT_IF_ERROR; + return true; + } + + LOGE("Unsupported pause on exit mode: %s " + "(expected true, false or if-error)", optarg); + return false; + +} + static bool parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], const char *optstring, const struct option *longopts) { @@ -1977,6 +2015,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; } break; + case OPT_PAUSE_ON_EXIT: + if (!parse_pause_on_exit(optarg, &args->pause_on_exit)) { + return false; + } + break; default: // getopt prints the error message on stderr return false; @@ -2190,6 +2233,37 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return true; } +static enum sc_pause_on_exit +sc_get_pause_on_exit(int argc, char *argv[]) { + // Read arguments backwards so that the last --pause-on-exit is considered + // (same behavior as getopt()) + for (int i = argc - 1; i >= 1; --i) { + const char *arg = argv[i]; + // Starts with "--pause-on-exit" + if (!strncmp("--pause-on-exit", arg, 15)) { + if (arg[15] == '\0') { + // No argument + return SC_PAUSE_ON_EXIT_TRUE; + } + if (arg[15] != '=') { + // Invalid parameter, ignore + return SC_PAUSE_ON_EXIT_FALSE; + } + const char *value = &arg[16]; + if (!strcmp(value, "true")) { + return SC_PAUSE_ON_EXIT_TRUE; + } + if (!strcmp(value, "if-error")) { + return SC_PAUSE_ON_EXIT_IF_ERROR; + } + // Set to false, inclusing when the value is invalid + return SC_PAUSE_ON_EXIT_FALSE; + } + } + + return false; +} + bool scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { struct sc_getopt_adapter adapter; @@ -2203,5 +2277,11 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { sc_getopt_adapter_destroy(&adapter); + if (!ret && args->pause_on_exit == SC_PAUSE_ON_EXIT_FALSE) { + // Check if "--pause-on-exit" is present in the arguments list, because + // it must be taken into account even if command line parsing failed + args->pause_on_exit = sc_get_pause_on_exit(argc, argv); + } + return ret; } diff --git a/app/src/cli.h b/app/src/cli.h index b9361a9c..23d34fcd 100644 --- a/app/src/cli.h +++ b/app/src/cli.h @@ -7,10 +7,17 @@ #include "options.h" +enum sc_pause_on_exit { + SC_PAUSE_ON_EXIT_TRUE, + SC_PAUSE_ON_EXIT_FALSE, + SC_PAUSE_ON_EXIT_IF_ERROR, +}; + struct scrcpy_cli_args { struct scrcpy_options opts; bool help; bool version; + enum sc_pause_on_exit pause_on_exit; }; void diff --git a/app/src/main.c b/app/src/main.c index cc3a85a7..d582c5ae 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -39,26 +39,32 @@ main_scrcpy(int argc, char *argv[]) { .opts = scrcpy_options_default, .help = false, .version = false, + .pause_on_exit = SC_PAUSE_ON_EXIT_FALSE, }; #ifndef NDEBUG args.opts.log_level = SC_LOG_LEVEL_DEBUG; #endif + enum scrcpy_exit_code ret; + if (!scrcpy_parse_args(&args, argc, argv)) { - return SCRCPY_EXIT_FAILURE; + ret = SCRCPY_EXIT_FAILURE; + goto end; } sc_set_log_level(args.opts.log_level); if (args.help) { scrcpy_print_usage(argv[0]); - return SCRCPY_EXIT_SUCCESS; + ret = SCRCPY_EXIT_SUCCESS; + goto end; } if (args.version) { scrcpy_print_version(); - return SCRCPY_EXIT_SUCCESS; + ret = SCRCPY_EXIT_SUCCESS; + goto end; } #ifdef SCRCPY_LAVF_REQUIRES_REGISTER_ALL @@ -72,18 +78,26 @@ main_scrcpy(int argc, char *argv[]) { #endif if (!net_init()) { - return SCRCPY_EXIT_FAILURE; + ret = SCRCPY_EXIT_FAILURE; + goto end; } sc_log_configure(); #ifdef HAVE_USB - enum scrcpy_exit_code ret = args.opts.otg ? scrcpy_otg(&args.opts) - : scrcpy(&args.opts); + ret = args.opts.otg ? scrcpy_otg(&args.opts) : scrcpy(&args.opts); #else - enum scrcpy_exit_code ret = scrcpy(&args.opts); + ret = scrcpy(&args.opts); #endif +end: + if (args.pause_on_exit == SC_PAUSE_ON_EXIT_TRUE || + (args.pause_on_exit == SC_PAUSE_ON_EXIT_IF_ERROR && + ret != SCRCPY_EXIT_SUCCESS)) { + printf("Press Enter to continue...\n"); + getchar(); + } + return ret; } From 1c864a88ebf113553c00928e7a7e5de94889a6e2 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 29 Jun 2023 21:40:12 +0200 Subject: [PATCH 1628/2244] Use --pause-on-exit from launchers The terminal opened by scrcpy-console (.bat or .desktop) must not close if scrcpy terminates with an error, so that error messages can be read. Refs #3817 Refs #3822 PR #4130 --- app/data/scrcpy-console.bat | 4 +--- app/data/scrcpy-console.desktop | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/app/data/scrcpy-console.bat b/app/data/scrcpy-console.bat index b90be29a..0ea7619f 100644 --- a/app/data/scrcpy-console.bat +++ b/app/data/scrcpy-console.bat @@ -1,4 +1,2 @@ @echo off -scrcpy.exe %* -:: if the exit code is >= 1, then pause -if errorlevel 1 pause +scrcpy.exe --pause-on-exit=if-error %* diff --git a/app/data/scrcpy-console.desktop b/app/data/scrcpy-console.desktop index 0e2f9ab0..6ca1e36a 100644 --- a/app/data/scrcpy-console.desktop +++ b/app/data/scrcpy-console.desktop @@ -5,7 +5,7 @@ Comment=Display and control your Android device # For some users, the PATH or ADB environment variables are set from the shell # startup file, like .bashrc or .zshrc… Run an interactive shell to get # environment correctly initialized. -Exec=/bin/bash --norc --noprofile -i -c "\"\\$SHELL\" -i -c scrcpy || read -p 'Press Enter to quit...'" +Exec=/bin/sh -c "\"\\$SHELL\" -i -c scrcpy --pause-on-exit=if-error" Icon=scrcpy Terminal=true Type=Application From 90ca46ee415514513da9dff8c558d726c8e34318 Mon Sep 17 00:00:00 2001 From: AmirSina Mashayekh Date: Fri, 20 Oct 2023 13:34:19 +0330 Subject: [PATCH 1629/2244] Add scrcpy-server to .gitignore The script install_release.sh downloads a file named scrcpy-server to the repo root directory. Add it to .gitignore so that it is ignored. PR #4364 Signed-off-by: Romain Vimont --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 2829d835..26d977ac 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ build/ .gradle/ /x/ local.properties +/scrcpy-server From 7adf98e9d491fc0cf218c661c3580827e8c89eb9 Mon Sep 17 00:00:00 2001 From: Avinash Sonawane Date: Mon, 16 Oct 2023 07:41:46 +0530 Subject: [PATCH 1630/2244] Use `void` for empty function parameter list PR #4371 Signed-off-by: Romain Vimont --- app/src/util/log.c | 2 +- app/src/util/log.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/util/log.c b/app/src/util/log.c index 0975e54a..8a347c84 100644 --- a/app/src/util/log.c +++ b/app/src/util/log.c @@ -147,7 +147,7 @@ sc_sdl_log_print(void *userdata, int category, SDL_LogPriority priority, } void -sc_log_configure() { +sc_log_configure(void) { SDL_LogSetOutputFunction(sc_sdl_log_print, NULL); // Redirect FFmpeg logs to SDL logs av_log_set_callback(sc_av_log_callback); diff --git a/app/src/util/log.h b/app/src/util/log.h index 8e1b73a2..0d79c9a4 100644 --- a/app/src/util/log.h +++ b/app/src/util/log.h @@ -36,6 +36,6 @@ sc_log_windows_error(const char *prefix, int error); #endif void -sc_log_configure(); +sc_log_configure(void); #endif From 90ba885547e74568a69b7f9cd8e917fc5ce8d0c1 Mon Sep 17 00:00:00 2001 From: Avinash Sonawane Date: Mon, 16 Oct 2023 07:53:38 +0530 Subject: [PATCH 1631/2244] Remove redundant `;` PR #4371 Signed-off-by: Romain Vimont --- 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 86b720fc..d334107a 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -1085,7 +1085,7 @@ print_shortcut(const struct sc_shortcut *shortcut, unsigned cols) { while (shortcut->shortcuts[i]) { printf(" %s\n", shortcut->shortcuts[i]); ++i; - }; + } char *text = sc_str_wrap_lines(shortcut->text, cols, 8); if (!text) { From 9ade389069a6898196dd8648461f30ea1e27aee1 Mon Sep 17 00:00:00 2001 From: Avinash Sonawane Date: Mon, 16 Oct 2023 09:08:06 +0530 Subject: [PATCH 1632/2244] Make sc_usb_devices_destroy() static It is only called from the implementation file. PR #4371 Signed-off-by: Romain Vimont --- app/src/usb/usb.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/usb/usb.c b/app/src/usb/usb.c index 310ed5d9..4f750581 100644 --- a/app/src/usb/usb.c +++ b/app/src/usb/usb.c @@ -93,7 +93,7 @@ sc_usb_device_move(struct sc_usb_device *dst, struct sc_usb_device *src) { src->product = NULL; } -void +static void sc_usb_devices_destroy(struct sc_vec_usb_devices *usb_devices) { for (size_t i = 0; i < usb_devices->size; ++i) { sc_usb_device_destroy(&usb_devices->data[i]); From 8e7b041f3596e119ee74fcc1a4dece2f1c1e1230 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 23 Oct 2023 21:49:42 +0200 Subject: [PATCH 1633/2244] Add missing `void`s for empty parameter list --- app/src/adb/adb.c | 2 +- app/src/icon.c | 2 +- app/src/scrcpy.c | 2 +- app/tests/test_str.c | 2 +- app/tests/test_vecdeque.c | 4 ++-- app/tests/test_vector.c | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/src/adb/adb.c b/app/src/adb/adb.c index 6bc6dd62..b248b8ed 100644 --- a/app/src/adb/adb.c +++ b/app/src/adb/adb.c @@ -70,7 +70,7 @@ argv_to_string(const char *const *argv, char *buf, size_t bufsize) { } static void -show_adb_installation_msg() { +show_adb_installation_msg(void) { #ifndef __WINDOWS__ static const struct { const char *binary; diff --git a/app/src/icon.c b/app/src/icon.c index a8588dd8..a9aad875 100644 --- a/app/src/icon.c +++ b/app/src/icon.c @@ -271,7 +271,7 @@ error: } SDL_Surface * -scrcpy_icon_load() { +scrcpy_icon_load(void) { char *icon_path = get_icon_path(); if (!icon_path) { return NULL; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index aabb7c5a..968629a2 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -297,7 +297,7 @@ sc_timeout_on_timeout(struct sc_timeout *timeout, void *userdata) { // Generate a scrcpy id to differentiate multiple running scrcpy instances static uint32_t -scrcpy_generate_scid() { +scrcpy_generate_scid(void) { struct sc_rand rand; sc_rand_init(&rand); // Only use 31 bits to avoid issues with signed values on the Java-side diff --git a/app/tests/test_str.c b/app/tests/test_str.c index 4fe8a1df..f719bc98 100644 --- a/app/tests/test_str.c +++ b/app/tests/test_str.c @@ -358,7 +358,7 @@ static void test_index_of_column(void) { assert(sc_str_index_of_column(" a bc d", 1, " ") == 2); } -static void test_remove_trailing_cr() { +static void test_remove_trailing_cr(void) { char s[] = "abc\r"; sc_str_remove_trailing_cr(s, sizeof(s) - 1); assert(!strcmp(s, "abc")); diff --git a/app/tests/test_vecdeque.c b/app/tests/test_vecdeque.c index fa3ba963..44d33560 100644 --- a/app/tests/test_vecdeque.c +++ b/app/tests/test_vecdeque.c @@ -102,7 +102,7 @@ static void test_vecdeque_reserve(void) { sc_vecdeque_destroy(&vdq); } -static void test_vecdeque_grow() { +static void test_vecdeque_grow(void) { struct SC_VECDEQUE(int) vdq = SC_VECDEQUE_INITIALIZER; bool ok = sc_vecdeque_reserve(&vdq, 20); @@ -142,7 +142,7 @@ static void test_vecdeque_grow() { sc_vecdeque_destroy(&vdq); } -static void test_vecdeque_push_hole() { +static void test_vecdeque_push_hole(void) { struct SC_VECDEQUE(int) vdq = SC_VECDEQUE_INITIALIZER; bool ok = sc_vecdeque_reserve(&vdq, 20); diff --git a/app/tests/test_vector.c b/app/tests/test_vector.c index 7ca09989..459b4e0f 100644 --- a/app/tests/test_vector.c +++ b/app/tests/test_vector.c @@ -187,7 +187,7 @@ static void test_vector_index_of(void) { sc_vector_destroy(&vec); } -static void test_vector_grow() { +static void test_vector_grow(void) { struct SC_VECTOR(int) vec = SC_VECTOR_INITIALIZER; bool ok; From 9fdb882509ad7c7b67f8d8ad68ce702ea7746f28 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 24 Oct 2023 22:53:41 +0200 Subject: [PATCH 1634/2244] Fix --pause-on-exit parsing The function incorrectly returned `false` instead of a valid (and expected) enum value. --- 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 d334107a..b78c84fd 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -2261,7 +2261,7 @@ sc_get_pause_on_exit(int argc, char *argv[]) { } } - return false; + return SC_PAUSE_ON_EXIT_FALSE; } bool From 0bbe8a700708ca6cf2b5112e5d4eed9a8800a8c6 Mon Sep 17 00:00:00 2001 From: Avinash Sonawane Date: Mon, 16 Oct 2023 07:56:14 +0530 Subject: [PATCH 1635/2244] Wrap macros in do-while(0) To fix the warnings of stray `;`. PR #4374 Signed-off-by: Romain Vimont --- app/src/server.c | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/app/src/server.c b/app/src/server.c index 4d787ea9..e49831fd 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -86,14 +86,15 @@ sc_server_params_copy(struct sc_server_params *dst, // The params reference user-allocated memory, so we must copy them to // handle them from another thread -#define COPY(FIELD) \ +#define COPY(FIELD) do { \ dst->FIELD = NULL; \ if (src->FIELD) { \ dst->FIELD = strdup(src->FIELD); \ if (!dst->FIELD) { \ goto error; \ } \ - } + } \ +} while(0) COPY(req_serial); COPY(crop); @@ -215,13 +216,13 @@ execute_server(struct sc_server *server, cmd[count++] = SCRCPY_VERSION; unsigned dyn_idx = count; // from there, the strings are allocated -#define ADD_PARAM(fmt, ...) { \ +#define ADD_PARAM(fmt, ...) do { \ char *p; \ if (asprintf(&p, fmt, ## __VA_ARGS__) == -1) { \ goto end; \ } \ cmd[count++] = p; \ - } + } while(0) ADD_PARAM("scid=%08x", params->scid); ADD_PARAM("log_level=%s", log_level_to_server_string(params->log_level)); From 8cef8bac947418435f23635711f463f9ca7e9ea6 Mon Sep 17 00:00:00 2001 From: Avinash Sonawane Date: Mon, 16 Oct 2023 09:08:35 +0530 Subject: [PATCH 1636/2244] Declare local functions as static PR #4374 Co-authored-by: Romain Vimont Signed-off-by: Romain Vimont --- app/src/adb/adb_parser.c | 2 +- app/src/main.c | 2 +- app/src/scrcpy.c | 2 +- app/tests/test_bytebuf.c | 6 +++--- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/src/adb/adb_parser.c b/app/src/adb/adb_parser.c index e7358403..66bb1854 100644 --- a/app/src/adb/adb_parser.c +++ b/app/src/adb/adb_parser.c @@ -7,7 +7,7 @@ #include "util/log.h" #include "util/str.h" -bool +static bool sc_adb_parse_device(char *line, struct sc_adb_device *device) { // One device line looks like: // "0123456789abcdef device usb:2-1 product:MyProduct model:MyModel " diff --git a/app/src/main.c b/app/src/main.c index d582c5ae..6050de11 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -23,7 +23,7 @@ #include "util/str.h" #endif -int +static int main_scrcpy(int argc, char *argv[]) { #ifdef _WIN32 // disable buffering, we want logs immediately diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 968629a2..f2893230 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -90,7 +90,7 @@ push_event(uint32_t type, const char *name) { #define PUSH_EVENT(TYPE) push_event(TYPE, # TYPE) #ifdef _WIN32 -BOOL WINAPI windows_ctrl_handler(DWORD ctrl_type) { +static BOOL WINAPI windows_ctrl_handler(DWORD ctrl_type) { if (ctrl_type == CTRL_C_EVENT) { PUSH_EVENT(SDL_QUIT); return TRUE; diff --git a/app/tests/test_bytebuf.c b/app/tests/test_bytebuf.c index c85e79ec..8e9d7c57 100644 --- a/app/tests/test_bytebuf.c +++ b/app/tests/test_bytebuf.c @@ -5,7 +5,7 @@ #include "util/bytebuf.h" -void test_bytebuf_simple(void) { +static void test_bytebuf_simple(void) { struct sc_bytebuf buf; uint8_t data[20]; @@ -34,7 +34,7 @@ void test_bytebuf_simple(void) { sc_bytebuf_destroy(&buf); } -void test_bytebuf_boundaries(void) { +static void test_bytebuf_boundaries(void) { struct sc_bytebuf buf; uint8_t data[20]; @@ -71,7 +71,7 @@ void test_bytebuf_boundaries(void) { sc_bytebuf_destroy(&buf); } -void test_bytebuf_two_steps_write(void) { +static void test_bytebuf_two_steps_write(void) { struct sc_bytebuf buf; uint8_t data[20]; From 3c2013de10bad9e18ac81ff46e954078a1effd2d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 24 Oct 2023 23:03:10 +0200 Subject: [PATCH 1637/2244] Enable missing-prototypes warning Warn if a global function is defined without a previous prototype declaration. It is not enabled by default at warning_level=2. --- meson.build | 2 ++ 1 file changed, 2 insertions(+) diff --git a/meson.build b/meson.build index 847e33a1..ed3f42bf 100644 --- a/meson.build +++ b/meson.build @@ -7,6 +7,8 @@ project('scrcpy', 'c', 'b_ndebug=if-release', ]) +add_project_arguments('-Wmissing-prototypes', language: 'c') + if get_option('compile_app') subdir('app') endif From bc8913e12bf775c050a655a4d44feab5ab5a0181 Mon Sep 17 00:00:00 2001 From: Avinash Sonawane Date: Mon, 16 Oct 2023 09:29:41 +0530 Subject: [PATCH 1638/2244] Use `char *` for pointer arithmetic PR #4374 Signed-off-by: Romain Vimont --- app/src/util/vecdeque.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/util/vecdeque.h b/app/src/util/vecdeque.h index e5372e02..ce559ee9 100644 --- a/app/src/util/vecdeque.h +++ b/app/src/util/vecdeque.h @@ -190,10 +190,10 @@ sc_vecdeque_reallocdata_(void *ptr, size_t newcap, size_t item_size, size_t right_len = MIN(size, oldcap - oldorigin); assert(right_len); - memcpy(newptr, ptr + (oldorigin * item_size), right_len * item_size); + memcpy(newptr, (char *) ptr + (oldorigin * item_size), right_len * item_size); if (size > right_len) { - memcpy(newptr + (right_len * item_size), ptr, + memcpy((char *) newptr + (right_len * item_size), ptr, (size - right_len) * item_size); } From 68b55ef2fed371f1be55f8373c14a4328cd026bf Mon Sep 17 00:00:00 2001 From: Avinash Sonawane Date: Tue, 24 Oct 2023 06:26:40 +0530 Subject: [PATCH 1639/2244] Replace sprintf() with safer snprintf() PR #4373 Co-authored-by: Romain Vimont Signed-off-by: Romain Vimont --- app/src/adb/adb.c | 39 ++++++++++++++++++++++++++++++++------- app/tests/test_str.c | 12 ++++++++---- 2 files changed, 40 insertions(+), 11 deletions(-) diff --git a/app/src/adb/adb.c b/app/src/adb/adb.c index b248b8ed..54375451 100644 --- a/app/src/adb/adb.c +++ b/app/src/adb/adb.c @@ -218,8 +218,16 @@ sc_adb_forward(struct sc_intr *intr, const char *serial, uint16_t local_port, const char *device_socket_name, unsigned flags) { char local[4 + 5 + 1]; // tcp:PORT char remote[108 + 14 + 1]; // localabstract:NAME - sprintf(local, "tcp:%" PRIu16, local_port); - snprintf(remote, sizeof(remote), "localabstract:%s", device_socket_name); + + int r = snprintf(local, sizeof(local), "tcp:%" PRIu16, local_port); + assert(r >= 0 && (size_t) r < sizeof(local)); + + r = snprintf(remote, sizeof(remote), "localabstract:%s", + device_socket_name); + if (r < 0 || (size_t) r >= sizeof(remote)) { + LOGE("Could not write socket name"); + return false; + } assert(serial); const char *const argv[] = @@ -233,7 +241,9 @@ bool sc_adb_forward_remove(struct sc_intr *intr, const char *serial, uint16_t local_port, unsigned flags) { char local[4 + 5 + 1]; // tcp:PORT - sprintf(local, "tcp:%" PRIu16, local_port); + int r = snprintf(local, sizeof(local), "tcp:%" PRIu16, local_port); + assert(r >= 0 && (size_t) r < sizeof(local)); + (void) r; assert(serial); const char *const argv[] = @@ -249,8 +259,16 @@ sc_adb_reverse(struct sc_intr *intr, const char *serial, unsigned flags) { char local[4 + 5 + 1]; // tcp:PORT char remote[108 + 14 + 1]; // localabstract:NAME - sprintf(local, "tcp:%" PRIu16, local_port); - snprintf(remote, sizeof(remote), "localabstract:%s", device_socket_name); + int r = snprintf(local, sizeof(local), "tcp:%" PRIu16, local_port); + assert(r >= 0 && (size_t) r < sizeof(local)); + + r = snprintf(remote, sizeof(remote), "localabstract:%s", + device_socket_name); + if (r < 0 || (size_t) r >= sizeof(remote)) { + LOGE("Could not write socket name"); + return false; + } + assert(serial); const char *const argv[] = SC_ADB_COMMAND("-s", serial, "reverse", remote, local); @@ -263,7 +281,12 @@ bool sc_adb_reverse_remove(struct sc_intr *intr, const char *serial, const char *device_socket_name, unsigned flags) { char remote[108 + 14 + 1]; // localabstract:NAME - snprintf(remote, sizeof(remote), "localabstract:%s", device_socket_name); + int r = snprintf(remote, sizeof(remote), "localabstract:%s", + device_socket_name); + if (r < 0 || (size_t) r >= sizeof(remote)) { + LOGE("Device socket name too long"); + return false; + } assert(serial); const char *const argv[] = @@ -333,7 +356,9 @@ bool sc_adb_tcpip(struct sc_intr *intr, const char *serial, uint16_t port, unsigned flags) { char port_string[5 + 1]; - sprintf(port_string, "%" PRIu16, port); + int r = snprintf(port_string, sizeof(port_string), "%" PRIu16, port); + assert(r >= 0 && (size_t) r < sizeof(port_string)); + (void) r; assert(serial); const char *const argv[] = diff --git a/app/tests/test_str.c b/app/tests/test_str.c index f719bc98..5d365ef5 100644 --- a/app/tests/test_str.c +++ b/app/tests/test_str.c @@ -269,21 +269,25 @@ static void test_parse_integer_with_suffix(void) { char buf[32]; - sprintf(buf, "%ldk", LONG_MAX / 2000); + int r = snprintf(buf, sizeof(buf), "%ldk", LONG_MAX / 2000); + assert(r >= 0 && (size_t) r < sizeof(buf)); ok = sc_str_parse_integer_with_suffix(buf, &value); assert(ok); assert(value == LONG_MAX / 2000 * 1000); - sprintf(buf, "%ldm", LONG_MAX / 2000); + r = snprintf(buf, sizeof(buf), "%ldm", LONG_MAX / 2000); + assert(r >= 0 && (size_t) r < sizeof(buf)); ok = sc_str_parse_integer_with_suffix(buf, &value); assert(!ok); - sprintf(buf, "%ldk", LONG_MIN / 2000); + r = snprintf(buf, sizeof(buf), "%ldk", LONG_MIN / 2000); + assert(r >= 0 && (size_t) r < sizeof(buf)); ok = sc_str_parse_integer_with_suffix(buf, &value); assert(ok); assert(value == LONG_MIN / 2000 * 1000); - sprintf(buf, "%ldm", LONG_MIN / 2000); + r = snprintf(buf, sizeof(buf), "%ldm", LONG_MIN / 2000); + assert(r >= 0 && (size_t) r < sizeof(buf)); ok = sc_str_parse_integer_with_suffix(buf, &value); assert(!ok); } From 76a99a7fcdb0a03c2e6185486090ad9123a4ce81 Mon Sep 17 00:00:00 2001 From: Avinash Sonawane Date: Tue, 24 Oct 2023 12:24:43 +0530 Subject: [PATCH 1640/2244] Replace raw number by its name PR #4373 Signed-off-by: Romain Vimont --- app/src/usb/hid_keyboard.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/usb/hid_keyboard.c b/app/src/usb/hid_keyboard.c index a12fbf3b..e717006a 100644 --- a/app/src/usb/hid_keyboard.c +++ b/app/src/usb/hid_keyboard.c @@ -27,7 +27,8 @@ // keyboard support, though OS could support more keys via modifying the report // desc. 6 should be enough for scrcpy. #define HID_KEYBOARD_MAX_KEYS 6 -#define HID_KEYBOARD_EVENT_SIZE (2 + HID_KEYBOARD_MAX_KEYS) +#define HID_KEYBOARD_EVENT_SIZE \ + (HID_KEYBOARD_INDEX_KEYS + HID_KEYBOARD_MAX_KEYS) #define HID_RESERVED 0x00 #define HID_ERROR_ROLL_OVER 0x01 From b7ad652a755fc3b4cfd42de47c71b1d67d8705e9 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 26 Oct 2023 22:42:46 +0200 Subject: [PATCH 1641/2244] Move empty string test for crop option parsing For consistency with other options. --- server/src/main/java/com/genymobile/scrcpy/Options.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index aab6fce8..49de0567 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -256,7 +256,9 @@ public class Options { options.tunnelForward = Boolean.parseBoolean(value); break; case "crop": - options.crop = parseCrop(value); + if (!value.isEmpty()) { + options.crop = parseCrop(value); + } break; case "control": options.control = Boolean.parseBoolean(value); @@ -337,9 +339,6 @@ public class Options { } private static Rect parseCrop(String crop) { - if (crop.isEmpty()) { - return null; - } // input format: "width:height:x:y" String[] tokens = crop.split(":"); if (tokens.length != 4) { From 7a2b756f1ed2c0ba582f51493043fe148f897e1b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 31 Oct 2023 12:35:04 +0100 Subject: [PATCH 1642/2244] Fix incorrect comment about AV1 constant MediaFormat.MIMETYPE_VIDEO_AV1 has been added in API 29, not 21. --- server/src/main/java/com/genymobile/scrcpy/VideoCodec.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/VideoCodec.java b/server/src/main/java/com/genymobile/scrcpy/VideoCodec.java index 43531f1e..fa787a99 100644 --- a/server/src/main/java/com/genymobile/scrcpy/VideoCodec.java +++ b/server/src/main/java/com/genymobile/scrcpy/VideoCodec.java @@ -6,7 +6,7 @@ import android.media.MediaFormat; public enum VideoCodec implements Codec { H264(0x68_32_36_34, "h264", MediaFormat.MIMETYPE_VIDEO_AVC), H265(0x68_32_36_35, "h265", MediaFormat.MIMETYPE_VIDEO_HEVC), - @SuppressLint("InlinedApi") // introduced in API 21 + @SuppressLint("InlinedApi") // introduced in API 29 AV1(0x00_61_76_31, "av1", MediaFormat.MIMETYPE_VIDEO_AV1); private final int id; // 4-byte ASCII representation of the name From 3432029a3dd9a910f7735b216b41839e3afdcafa Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 23 Aug 2023 20:22:39 +0200 Subject: [PATCH 1643/2244] Make separator configurable for parsing integers The separator was hardcoded to ':'. This will allow to reuse the function to parse sizes as WIDTHxHEIGHT. PR #4213 --- app/src/cli.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index b78c84fd..bc1aa2b5 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -1204,9 +1204,9 @@ parse_integer_arg(const char *s, long *out, bool accept_suffix, long min, } 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 = sc_str_parse_integers(s, ':', max_items, out); +parse_integers_arg(const char *s, const char sep, size_t max_items, long *out, + long min, long max, const char *name) { + size_t count = sc_str_parse_integers(s, sep, max_items, out); if (!count) { LOGE("Could not parse %s: %s", name, s); return 0; @@ -1362,7 +1362,7 @@ parse_window_dimension(const char *s, uint16_t *dimension) { static bool 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"); + size_t count = parse_integers_arg(s, ':', 2, values, 0, 0xFFFF, "port"); if (!count) { return false; } From 23e116064dc97f2af843e764f13eebd54fab486d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 26 Oct 2023 22:57:00 +0200 Subject: [PATCH 1644/2244] Rename --display to --display-id The option is named "display id" everywhere. This will be consistent with --camera-id (there will be many camera options, so an option --camera would be confusing). PR #4213 --- app/data/bash-completion/scrcpy | 4 ++-- app/data/zsh-completion/_scrcpy | 2 +- app/scrcpy.1 | 2 +- app/src/cli.c | 12 +++++++++++- doc/video.md | 2 +- .../main/java/com/genymobile/scrcpy/LogUtils.java | 2 +- 6 files changed, 17 insertions(+), 7 deletions(-) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index a44ff6e5..fe118289 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -13,7 +13,7 @@ _scrcpy() { --crop= -d --select-usb --disable-screensaver - --display= + --display-id= --display-buffer= -e --select-tcpip -f --fullscreen @@ -140,7 +140,7 @@ _scrcpy() { |--audio-encoder \ |--audio-output-buffer \ |--crop \ - |--display \ + |--display-id \ |--display-buffer \ |--max-fps \ |-m|--max-size \ diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index ac40e903..b688c0aa 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -20,7 +20,7 @@ arguments=( '--crop=[\[width\:height\:x\:y\] Crop the device screen on the server]' {-d,--select-usb}'[Use USB device]' '--disable-screensaver[Disable screensaver while scrcpy is running]' - '--display=[Specify the display id to mirror]' + '--display-id=[Specify the display id to mirror]' '--display-buffer=[Add a buffering delay \(in milliseconds\) before displaying]' {-e,--select-tcpip}'[Use TCP/IP device]' {-f,--fullscreen}'[Start in fullscreen]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 7ae56a27..80613aed 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -94,7 +94,7 @@ Also see \fB\-e\fR (\fB\-\-select\-tcpip\fR). Disable screensaver while scrcpy is running. .TP -.BI "\-\-display " id +.BI "\-\-display\-id " id Specify the device display id to mirror. The available display ids can be listed by \-\-list\-displays. diff --git a/app/src/cli.c b/app/src/cli.c index bc1aa2b5..2ae0ced2 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -32,6 +32,7 @@ enum { OPT_WINDOW_BORDERLESS, OPT_MAX_FPS, OPT_LOCK_VIDEO_ORIENTATION, + OPT_DISPLAY, OPT_DISPLAY_ID, OPT_ROTATION, OPT_RENDER_DRIVER, @@ -232,9 +233,15 @@ static const struct sc_option options[] = { .text = "Disable screensaver while scrcpy is running.", }, { - .longopt_id = OPT_DISPLAY_ID, + // deprecated + .longopt_id = OPT_DISPLAY, .longopt = "display", .argdesc = "id", + }, + { + .longopt_id = OPT_DISPLAY_ID, + .longopt = "display-id", + .argdesc = "id", .text = "Specify the device display id to mirror.\n" "The available display ids can be listed by:\n" " scrcpy --list-displays\n" @@ -1702,6 +1709,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], case OPT_CROP: opts->crop = optarg; break; + case OPT_DISPLAY: + LOGW("--display is deprecated, use --display-id instead."); + // fall through case OPT_DISPLAY_ID: if (!parse_display_id(optarg, &opts->display_id)) { return false; diff --git a/doc/video.md b/doc/video.md index 57af5c9f..5ce749f9 100644 --- a/doc/video.md +++ b/doc/video.md @@ -143,7 +143,7 @@ If several displays are available on the Android device, it is possible to select the display to mirror: ```bash -scrcpy --display=1 +scrcpy --display-id=1 ``` The list of display ids can be retrieved by: diff --git a/server/src/main/java/com/genymobile/scrcpy/LogUtils.java b/server/src/main/java/com/genymobile/scrcpy/LogUtils.java index 243a156b..ce1617e4 100644 --- a/server/src/main/java/com/genymobile/scrcpy/LogUtils.java +++ b/server/src/main/java/com/genymobile/scrcpy/LogUtils.java @@ -47,7 +47,7 @@ public final class LogUtils { builder.append("\n (none)"); } else { for (int id : displayIds) { - builder.append("\n --display=").append(id).append(" ("); + builder.append("\n --display-id=").append(id).append(" ("); DisplayInfo displayInfo = displayManager.getDisplayInfo(id); if (displayInfo != null) { Size size = displayInfo.getSize(); From 41ccb5883ed3538b3f0a99c583beec16bfc893ce Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 28 Oct 2023 14:21:15 +0200 Subject: [PATCH 1645/2244] Force server exit at the end of main() By default, the Java process exits when all non-daemon threads are terminated. The Android SDK might start some non-daemon threads internally, preventing the scrcpy server to exit in some cases. So force the process to exit explicitly. PR #4213 --- .../main/java/com/genymobile/scrcpy/Server.java | 17 ++++++++++++++++- 1 file changed, 16 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 2e6e1d4a..dc85c965 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -170,7 +170,22 @@ public final class Server { return thread; } - public static void main(String... args) throws Exception { + public static void main(String... args) { + int status = 0; + try { + internalMain(args); + } catch (Throwable t) { + t.printStackTrace(); + status = 1; + } finally { + // By default, the Java process exits when all non-daemon threads are terminated. + // The Android SDK might start some non-daemon threads internally, preventing the scrcpy server to exit. + // So force the process to exit explicitly. + System.exit(status); + } + } + + private static void internalMain(String... args) throws Exception { Thread.setDefaultUncaughtExceptionHandler((t, e) -> { Ln.e("Exception on thread " + t, e); }); From a2fb1b40f690a1e4556a2ac058d7c069513b7402 Mon Sep 17 00:00:00 2001 From: Simon Chan <1330321+yume-chan@users.noreply.github.com> Date: Sun, 16 Jul 2023 17:07:19 +0800 Subject: [PATCH 1646/2244] Extract SurfaceCapture from ScreenEncoder Extract an interface SurfaceCapture from ScreenEncoder, representing a video source which can be rendered to a Surface for encoding. Split ScreenEncoder into: - ScreenCapture, implementing SurfaceCapture to capture the device screen, - SurfaceEncoder, to encode any SurfaceCapture. This separation prepares the introduction of another SurfaceCapture implementation to capture the camera instead of the device screen. PR #4213 Co-authored-by: Romain Vimont Signed-off-by: Romain Vimont --- .../com/genymobile/scrcpy/ScreenCapture.java | 83 +++++++++++++++++ .../java/com/genymobile/scrcpy/Server.java | 3 +- .../com/genymobile/scrcpy/SurfaceCapture.java | 61 +++++++++++++ ...ScreenEncoder.java => SurfaceEncoder.java} | 90 +++++-------------- 4 files changed, 166 insertions(+), 71 deletions(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/ScreenCapture.java create mode 100644 server/src/main/java/com/genymobile/scrcpy/SurfaceCapture.java rename server/src/main/java/com/genymobile/scrcpy/{ScreenEncoder.java => SurfaceEncoder.java} (74%) diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenCapture.java b/server/src/main/java/com/genymobile/scrcpy/ScreenCapture.java new file mode 100644 index 00000000..f9ac66b8 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenCapture.java @@ -0,0 +1,83 @@ +package com.genymobile.scrcpy; + +import com.genymobile.scrcpy.wrappers.SurfaceControl; + +import android.graphics.Rect; +import android.os.Build; +import android.os.IBinder; +import android.view.Surface; + +public class ScreenCapture extends SurfaceCapture implements Device.RotationListener, Device.FoldListener { + + private final Device device; + private IBinder display; + + public ScreenCapture(Device device) { + this.device = device; + } + + @Override + public void init() { + display = createDisplay(); + device.setRotationListener(this); + device.setFoldListener(this); + } + + @Override + public void start(Surface surface) { + ScreenInfo screenInfo = device.getScreenInfo(); + Rect contentRect = screenInfo.getContentRect(); + + // does not include the locked video orientation + Rect unlockedVideoRect = screenInfo.getUnlockedVideoSize().toRect(); + int videoRotation = screenInfo.getVideoRotation(); + int layerStack = device.getLayerStack(); + setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack); + } + + @Override + public void release() { + device.setRotationListener(null); + device.setFoldListener(null); + SurfaceControl.destroyDisplay(display); + } + + @Override + public Size getSize() { + return device.getScreenInfo().getVideoSize(); + } + + @Override + public void setMaxSize(int maxSize) { + device.setMaxSize(maxSize); + } + + @Override + public void onFoldChanged(int displayId, boolean folded) { + requestReset(); + } + + @Override + public void onRotationChanged(int rotation) { + requestReset(); + } + + private static IBinder createDisplay() { + // Since Android 12 (preview), secure displays could not be created with shell permissions anymore. + // On Android 12 preview, SDK_INT is still R (not S), but CODENAME is "S". + boolean secure = Build.VERSION.SDK_INT < Build.VERSION_CODES.R || (Build.VERSION.SDK_INT == Build.VERSION_CODES.R && !"S".equals( + Build.VERSION.CODENAME)); + return SurfaceControl.createDisplay("scrcpy", secure); + } + + 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, 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 dc85c965..0f73a19b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -132,7 +132,8 @@ public final class Server { if (video) { Streamer videoStreamer = new Streamer(connection.getVideoFd(), options.getVideoCodec(), options.getSendCodecMeta(), options.getSendFrameMeta()); - ScreenEncoder screenEncoder = new ScreenEncoder(device, videoStreamer, options.getVideoBitRate(), options.getMaxFps(), + ScreenCapture screenCapture = new ScreenCapture(device); + SurfaceEncoder screenEncoder = new SurfaceEncoder(screenCapture, videoStreamer, options.getVideoBitRate(), options.getMaxFps(), options.getVideoCodecOptions(), options.getVideoEncoder(), options.getDownsizeOnError()); asyncProcessors.add(screenEncoder); } diff --git a/server/src/main/java/com/genymobile/scrcpy/SurfaceCapture.java b/server/src/main/java/com/genymobile/scrcpy/SurfaceCapture.java new file mode 100644 index 00000000..45a0fd2f --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/SurfaceCapture.java @@ -0,0 +1,61 @@ +package com.genymobile.scrcpy; + +import android.view.Surface; + +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * A video source which can be rendered on a Surface for encoding. + */ +public abstract class SurfaceCapture { + + private final AtomicBoolean resetCapture = new AtomicBoolean(); + + /** + * Request the encoding session to be restarted, for example if the capture implementation detects that the video source size has changed (on + * device rotation for example). + */ + protected void requestReset() { + resetCapture.set(true); + } + + /** + * Consume the reset request (intended to be called by the encoder). + * + * @return {@code true} if a reset request was pending, {@code false} otherwise. + */ + public boolean consumeReset() { + return resetCapture.getAndSet(false); + } + + /** + * Called once before the capture starts. + */ + public abstract void init(); + + /** + * Called after the capture ends (if and only if {@link #init()} has been called). + */ + public abstract void release(); + + /** + * Start the capture to the target surface. + * + * @param surface the surface which will be encoded + */ + public abstract void start(Surface surface); + + /** + * Return the video size + * + * @return the video size + */ + public abstract Size getSize(); + + /** + * Set the maximum capture size (set by the encoder if it does not support the current size). + * + * @param maxSize Maximum size + */ + public abstract void setMaxSize(int maxSize); +} diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/SurfaceEncoder.java similarity index 74% rename from server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java rename to server/src/main/java/com/genymobile/scrcpy/SurfaceEncoder.java index 5a9db10d..4af31e89 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/SurfaceEncoder.java @@ -1,13 +1,8 @@ package com.genymobile.scrcpy; -import com.genymobile.scrcpy.wrappers.SurfaceControl; - -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.os.Looper; import android.os.SystemClock; import android.view.Surface; @@ -17,7 +12,7 @@ import java.nio.ByteBuffer; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; -public class ScreenEncoder implements Device.RotationListener, Device.FoldListener, AsyncProcessor { +public class SurfaceEncoder implements AsyncProcessor { private static final int DEFAULT_I_FRAME_INTERVAL = 10; // seconds private static final int REPEAT_FRAME_DELAY_US = 100_000; // repeat after 100ms @@ -27,9 +22,7 @@ public class ScreenEncoder implements Device.RotationListener, Device.FoldListen private static final int[] MAX_SIZE_FALLBACK = {2560, 1920, 1600, 1280, 1024, 800}; private static final int MAX_CONSECUTIVE_ERRORS = 3; - private final AtomicBoolean resetCapture = new AtomicBoolean(); - - private final Device device; + private final SurfaceCapture capture; private final Streamer streamer; private final String encoderName; private final List codecOptions; @@ -43,9 +36,9 @@ public class ScreenEncoder implements Device.RotationListener, Device.FoldListen private Thread thread; private final AtomicBoolean stopped = new AtomicBoolean(); - public ScreenEncoder(Device device, Streamer streamer, int videoBitRate, int maxFps, List codecOptions, String encoderName, + public SurfaceEncoder(SurfaceCapture capture, Streamer streamer, int videoBitRate, int maxFps, List codecOptions, String encoderName, boolean downsizeOnError) { - this.device = device; + this.capture = capture; this.streamer = streamer; this.videoBitRate = videoBitRate; this.maxFps = maxFps; @@ -54,51 +47,29 @@ public class ScreenEncoder implements Device.RotationListener, Device.FoldListen this.downsizeOnError = downsizeOnError; } - @Override - public void onFoldChanged(int displayId, boolean folded) { - resetCapture.set(true); - } - - @Override - public void onRotationChanged(int rotation) { - resetCapture.set(true); - } - - private boolean consumeResetCapture() { - return resetCapture.getAndSet(false); - } - private void streamScreen() throws IOException, ConfigurationException { Codec codec = streamer.getCodec(); MediaCodec mediaCodec = createMediaCodec(codec, encoderName); MediaFormat format = createFormat(codec.getMimeType(), videoBitRate, maxFps, codecOptions); - IBinder display = createDisplay(); - device.setRotationListener(this); - device.setFoldListener(this); - streamer.writeVideoHeader(device.getScreenInfo().getVideoSize()); + capture.init(); - boolean alive; try { - do { - ScreenInfo screenInfo = device.getScreenInfo(); - Rect contentRect = screenInfo.getContentRect(); + streamer.writeVideoHeader(capture.getSize()); - // include the locked video orientation - Rect videoRect = screenInfo.getVideoSize().toRect(); - format.setInteger(MediaFormat.KEY_WIDTH, videoRect.width()); - format.setInteger(MediaFormat.KEY_HEIGHT, videoRect.height()); + boolean alive; + + do { + Size size = capture.getSize(); + format.setInteger(MediaFormat.KEY_WIDTH, size.getWidth()); + format.setInteger(MediaFormat.KEY_HEIGHT, size.getHeight()); Surface surface = null; try { mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); surface = mediaCodec.createInputSurface(); - // does not include the locked video orientation - Rect unlockedVideoRect = screenInfo.getUnlockedVideoSize().toRect(); - int videoRotation = screenInfo.getVideoRotation(); - int layerStack = device.getLayerStack(); - setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack); + capture.start(surface); mediaCodec.start(); @@ -107,7 +78,7 @@ public class ScreenEncoder implements Device.RotationListener, Device.FoldListen mediaCodec.stop(); } catch (IllegalStateException | IllegalArgumentException e) { Ln.e("Encoding error: " + e.getClass().getName() + ": " + e.getMessage()); - if (!prepareRetry(device, screenInfo)) { + if (!prepareRetry(size)) { throw e; } Ln.i("Retrying..."); @@ -121,13 +92,11 @@ public class ScreenEncoder implements Device.RotationListener, Device.FoldListen } while (alive); } finally { mediaCodec.release(); - device.setRotationListener(null); - device.setFoldListener(null); - SurfaceControl.destroyDisplay(display); + capture.release(); } } - private boolean prepareRetry(Device device, ScreenInfo screenInfo) { + private boolean prepareRetry(Size currentSize) { if (firstFrameSent) { ++consecutiveErrors; if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) { @@ -147,7 +116,7 @@ public class ScreenEncoder implements Device.RotationListener, Device.FoldListen // Downsizing on error is only enabled if an encoding failure occurs before the first frame (downsizing later could be surprising) - int newMaxSize = chooseMaxSizeFallback(screenInfo.getVideoSize()); + int newMaxSize = chooseMaxSizeFallback(currentSize); if (newMaxSize == 0) { // Must definitively fail return false; @@ -155,7 +124,7 @@ public class ScreenEncoder implements Device.RotationListener, Device.FoldListen // Retry with a smaller device size Ln.i("Retrying with -m" + newMaxSize + "..."); - device.setMaxSize(newMaxSize); + capture.setMaxSize(newMaxSize); return true; } @@ -176,14 +145,14 @@ public class ScreenEncoder implements Device.RotationListener, Device.FoldListen boolean alive = true; MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); - while (!consumeResetCapture() && !eof) { + while (!capture.consumeReset() && !eof) { if (stopped.get()) { alive = false; break; } int outputBufferId = codec.dequeueOutputBuffer(bufferInfo, -1); try { - if (consumeResetCapture()) { + if (capture.consumeReset()) { // must restart encoding with new size break; } @@ -264,25 +233,6 @@ public class ScreenEncoder implements Device.RotationListener, Device.FoldListen return format; } - private static IBinder createDisplay() { - // Since Android 12 (preview), secure displays could not be created with shell permissions anymore. - // On Android 12 preview, SDK_INT is still R (not S), but CODENAME is "S". - boolean secure = Build.VERSION.SDK_INT < Build.VERSION_CODES.R || (Build.VERSION.SDK_INT == Build.VERSION_CODES.R && !"S" - .equals(Build.VERSION.CODENAME)); - return SurfaceControl.createDisplay("scrcpy", secure); - } - - 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, layerStack); - } finally { - SurfaceControl.closeTransaction(); - } - } - @Override public void start(TerminationListener listener) { thread = new Thread(() -> { From f085765e04a22508b237fc83b077c18d23dd26a1 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 24 Oct 2023 23:54:56 +0200 Subject: [PATCH 1647/2244] Factorize --list- options handling This will limit code duplication as more list options will be added. PR #4213 --- app/src/cli.c | 4 ++-- app/src/options.c | 3 +-- app/src/options.h | 5 +++-- app/src/scrcpy.c | 5 ++--- app/src/server.c | 6 +++--- app/src/server.h | 3 +-- server/src/main/java/com/genymobile/scrcpy/Options.java | 4 ++++ server/src/main/java/com/genymobile/scrcpy/Server.java | 2 +- 8 files changed, 17 insertions(+), 15 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index 2ae0ced2..c7eebb0a 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -1993,10 +1993,10 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; #endif case OPT_LIST_ENCODERS: - opts->list_encoders = true; + opts->list |= SC_OPTION_LIST_ENCODERS; break; case OPT_LIST_DISPLAYS: - opts->list_displays = true; + opts->list |= SC_OPTION_LIST_DISPLAYS; break; case OPT_REQUIRE_AUDIO: opts->require_audio = true; diff --git a/app/src/options.c b/app/src/options.c index 530e003b..b633d762 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -79,7 +79,6 @@ const struct scrcpy_options scrcpy_options_default = { .video = true, .audio = true, .require_audio = false, - .list_encoders = false, - .list_displays = false, .kill_adb_on_close = false, + .list = 0, }; diff --git a/app/src/options.h b/app/src/options.h index 1f36ad7f..e1f693bc 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -179,9 +179,10 @@ struct scrcpy_options { bool video; bool audio; bool require_audio; - bool list_encoders; - bool list_displays; bool kill_adb_on_close; +#define SC_OPTION_LIST_ENCODERS 0x1 +#define SC_OPTION_LIST_DISPLAYS 0x2 + uint8_t list; }; extern const struct scrcpy_options scrcpy_options_default; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index f2893230..5f0158f1 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -379,9 +379,8 @@ scrcpy(struct scrcpy_options *options) { .tcpip_dst = options->tcpip_dst, .cleanup = options->cleanup, .power_on = options->power_on, - .list_encoders = options->list_encoders, - .list_displays = options->list_displays, .kill_adb_on_close = options->kill_adb_on_close, + .list = options->list, }; static const struct sc_server_callbacks cbs = { @@ -399,7 +398,7 @@ scrcpy(struct scrcpy_options *options) { server_started = true; - if (options->list_encoders || options->list_displays) { + if (options->list) { bool ok = await_for_server(NULL); ret = ok ? SCRCPY_EXIT_SUCCESS : SCRCPY_EXIT_FAILURE; goto end; diff --git a/app/src/server.c b/app/src/server.c index e49831fd..d52b164b 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -311,10 +311,10 @@ execute_server(struct sc_server *server, // By default, power_on is true ADD_PARAM("power_on=false"); } - if (params->list_encoders) { + if (params->list & SC_OPTION_LIST_ENCODERS) { ADD_PARAM("list_encoders=true"); } - if (params->list_displays) { + if (params->list & SC_OPTION_LIST_DISPLAYS) { ADD_PARAM("list_displays=true"); } @@ -896,7 +896,7 @@ run_server(void *data) { // If --list-* is passed, then the server just prints the requested data // then exits. - if (params->list_encoders || params->list_displays) { + if (params->list) { sc_pid pid = execute_server(server, params); if (pid == SC_PROCESS_NONE) { goto error_connection_failed; diff --git a/app/src/server.h b/app/src/server.h index adba2652..04955974 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -56,9 +56,8 @@ struct sc_server_params { bool select_tcpip; bool cleanup; bool power_on; - bool list_encoders; - bool list_displays; bool kill_adb_on_close; + uint8_t list; }; struct sc_server { diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 49de0567..59820ea7 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -153,6 +153,10 @@ public class Options { return powerOn; } + public boolean getList() { + return listEncoders || listDisplays; + } + public boolean getListEncoders() { return listEncoders; } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 0f73a19b..1b56b859 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -195,7 +195,7 @@ public final class Server { Ln.initLogLevel(options.getLogLevel()); - if (options.getListEncoders() || options.getListDisplays()) { + if (options.getList()) { if (options.getCleanup()) { CleanUp.unlinkSelf(); } From cd63896d63c6a504441397706729648d48e45d96 Mon Sep 17 00:00:00 2001 From: Simon Chan <1330321+yume-chan@users.noreply.github.com> Date: Sun, 16 Jul 2023 17:07:19 +0800 Subject: [PATCH 1648/2244] Add --list-cameras Add an option to list the device cameras. PR #4213 Co-authored-by: Romain Vimont Signed-off-by: Romain Vimont --- app/data/bash-completion/scrcpy | 1 + app/data/zsh-completion/_scrcpy | 1 + app/scrcpy.1 | 4 ++ app/src/cli.c | 9 ++++ app/src/options.h | 1 + app/src/server.c | 3 ++ .../java/com/genymobile/scrcpy/LogUtils.java | 43 +++++++++++++++++++ .../java/com/genymobile/scrcpy/Options.java | 10 ++++- .../java/com/genymobile/scrcpy/Server.java | 7 ++- .../com/genymobile/scrcpy/Workarounds.java | 8 +++- .../scrcpy/wrappers/ServiceManager.java | 18 ++++++++ 11 files changed, 101 insertions(+), 4 deletions(-) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index fe118289..b6f550c5 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -23,6 +23,7 @@ _scrcpy() { --kill-adb-on-close -K --hid-keyboard --legacy-paste + --list-cameras --list-displays --list-encoders --lock-video-orientation diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index b688c0aa..3ba2c4b8 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -30,6 +30,7 @@ arguments=( '--kill-adb-on-close[Kill adb when scrcpy terminates]' {-K,--hid-keyboard}'[Simulate a physical keyboard by using HID over AOAv2]' '--legacy-paste[Inject computer clipboard text as a sequence of key events on Ctrl+v]' + '--list-cameras[List cameras available on the device]' '--list-displays[List displays available on the device]' '--list-encoders[List video and audio encoders available on the device]' '--lock-video-orientation=[Lock video orientation]:orientation:(unlocked initial 0 1 2 3)' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 80613aed..30d15b4d 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -155,6 +155,10 @@ Inject computer clipboard text as a sequence of key events on Ctrl+v (like MOD+S This is a workaround for some devices not behaving as expected when setting the device clipboard programmatically. +.TP +.B \-\-list\-cameras +List cameras available on the device. + .TP .B \-\-list\-encoders List video and audio encoders available on the device. diff --git a/app/src/cli.c b/app/src/cli.c index c7eebb0a..c9382c5b 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -81,6 +81,7 @@ enum { OPT_KILL_ADB_ON_CLOSE, OPT_TIME_LIMIT, OPT_PAUSE_ON_EXIT, + OPT_LIST_CAMERAS, }; struct sc_option { @@ -320,6 +321,11 @@ static const struct sc_option options[] = { "This is a workaround for some devices not behaving as " "expected when setting the device clipboard programmatically.", }, + { + .longopt_id = OPT_LIST_CAMERAS, + .longopt = "list-cameras", + .text = "List device cameras.", + }, { .longopt_id = OPT_LIST_DISPLAYS, .longopt = "list-displays", @@ -1998,6 +2004,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], case OPT_LIST_DISPLAYS: opts->list |= SC_OPTION_LIST_DISPLAYS; break; + case OPT_LIST_CAMERAS: + opts->list |= SC_OPTION_LIST_CAMERAS; + break; case OPT_REQUIRE_AUDIO: opts->require_audio = true; break; diff --git a/app/src/options.h b/app/src/options.h index e1f693bc..e960968a 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -182,6 +182,7 @@ struct scrcpy_options { bool kill_adb_on_close; #define SC_OPTION_LIST_ENCODERS 0x1 #define SC_OPTION_LIST_DISPLAYS 0x2 +#define SC_OPTION_LIST_CAMERAS 0x4 uint8_t list; }; diff --git a/app/src/server.c b/app/src/server.c index d52b164b..571b813c 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -317,6 +317,9 @@ execute_server(struct sc_server *server, if (params->list & SC_OPTION_LIST_DISPLAYS) { ADD_PARAM("list_displays=true"); } + if (params->list & SC_OPTION_LIST_CAMERAS) { + ADD_PARAM("list_cameras=true"); + } #undef ADD_PARAM diff --git a/server/src/main/java/com/genymobile/scrcpy/LogUtils.java b/server/src/main/java/com/genymobile/scrcpy/LogUtils.java index ce1617e4..d609adcd 100644 --- a/server/src/main/java/com/genymobile/scrcpy/LogUtils.java +++ b/server/src/main/java/com/genymobile/scrcpy/LogUtils.java @@ -3,6 +3,11 @@ package com.genymobile.scrcpy; import com.genymobile.scrcpy.wrappers.DisplayManager; import com.genymobile.scrcpy.wrappers.ServiceManager; +import android.graphics.Rect; +import android.hardware.camera2.CameraAccessException; +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CameraManager; + import java.util.List; public final class LogUtils { @@ -60,4 +65,42 @@ public final class LogUtils { } return builder.toString(); } + + private static String getCameraFacingName(int facing) { + switch (facing) { + case CameraCharacteristics.LENS_FACING_FRONT: + return "front"; + case CameraCharacteristics.LENS_FACING_BACK: + return "back"; + case CameraCharacteristics.LENS_FACING_EXTERNAL: + return "external"; + default: + return "unknown"; + } + } + + public static String buildCameraListMessage() { + StringBuilder builder = new StringBuilder("List of cameras:"); + CameraManager cameraManager = ServiceManager.getCameraManager(); + try { + String[] cameraIds = cameraManager.getCameraIdList(); + if (cameraIds == null || cameraIds.length == 0) { + builder.append("\n (none)"); + } else { + for (String id : cameraIds) { + builder.append("\n --video-source=camera --camera-id=").append(id); + CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(id); + + int facing = characteristics.get(CameraCharacteristics.LENS_FACING); + builder.append(" (").append(getCameraFacingName(facing)).append(", "); + + Rect activeSize = characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); + builder.append(activeSize.width()).append("x").append(activeSize.height()).append(')'); + } + } + } catch (CameraAccessException e) { + builder.append("\n (access denied)"); + } + return builder.toString(); + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 59820ea7..0a3032cf 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -38,6 +38,7 @@ public class Options { private boolean listEncoders; private boolean listDisplays; + private boolean listCameras; // Options not used by the scrcpy client, but useful to use scrcpy-server directly private boolean sendDeviceMeta = true; // send device name and size @@ -154,7 +155,7 @@ public class Options { } public boolean getList() { - return listEncoders || listDisplays; + return listEncoders || listDisplays || listCameras; } public boolean getListEncoders() { @@ -165,6 +166,10 @@ public class Options { return listDisplays; } + public boolean getListCameras() { + return listCameras; + } + public boolean getSendDeviceMeta() { return sendDeviceMeta; } @@ -312,6 +317,9 @@ public class Options { case "list_displays": options.listDisplays = Boolean.parseBoolean(value); break; + case "list_cameras": + options.listCameras = Boolean.parseBoolean(value); + break; case "send_device_meta": options.sendDeviceMeta = Boolean.parseBoolean(value); break; diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 1b56b859..2129dbc0 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -98,8 +98,9 @@ public final class Server { boolean video = options.getVideo(); boolean audio = options.getAudio(); boolean sendDummyByte = options.getSendDummyByte(); + boolean camera = false; - Workarounds.apply(audio); + Workarounds.apply(audio, camera); List asyncProcessors = new ArrayList<>(); @@ -207,6 +208,10 @@ public final class Server { if (options.getListDisplays()) { Ln.i(LogUtils.buildDisplayListMessage()); } + if (options.getListCameras()) { + Workarounds.apply(false, true); + Ln.i(LogUtils.buildCameraListMessage()); + } // Just print the requested data, do not mirror return; } diff --git a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java index 74c0202a..b8ee68ca 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java +++ b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java @@ -28,14 +28,13 @@ public final class Workarounds { // not instantiable } - public static void apply(boolean audio) { + public static void apply(boolean audio, boolean camera) { Workarounds.prepareMainLooper(); boolean mustFillAppInfo = false; boolean mustFillBaseContext = false; boolean mustFillAppContext = false; - if (Build.BRAND.equalsIgnoreCase("meizu")) { // Workarounds must be applied for Meizu phones: // - @@ -65,6 +64,11 @@ public final class Workarounds { mustFillAppContext = true; } + if (camera) { + mustFillAppInfo = true; + mustFillBaseContext = true; + } + if (mustFillAppInfo) { Workarounds.fillAppInfo(); } 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 69803971..ae04a6d2 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java @@ -1,9 +1,14 @@ package com.genymobile.scrcpy.wrappers; +import com.genymobile.scrcpy.FakeContext; + import android.annotation.SuppressLint; +import android.content.Context; +import android.hardware.camera2.CameraManager; import android.os.IBinder; import android.os.IInterface; +import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -26,6 +31,7 @@ public final class ServiceManager { private static StatusBarManager statusBarManager; private static ClipboardManager clipboardManager; private static ActivityManager activityManager; + private static CameraManager cameraManager; private ServiceManager() { /* not instantiable */ @@ -129,4 +135,16 @@ public final class ServiceManager { return activityManager; } + + public static CameraManager getCameraManager() { + if (cameraManager == null) { + try { + Constructor ctor = CameraManager.class.getDeclaredConstructor(Context.class); + cameraManager = ctor.newInstance(FakeContext.get()); + } catch (Exception e) { + throw new AssertionError(e); + } + } + return cameraManager; + } } From f032262cd76b27621813f9fe13f4e18981471d4c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 24 Oct 2023 23:46:56 +0200 Subject: [PATCH 1649/2244] Add --list-camera-sizes Add an option to list the device camera declared sizes. PR #4213 --- app/data/bash-completion/scrcpy | 1 + app/data/zsh-completion/_scrcpy | 1 + app/scrcpy.1 | 4 ++++ app/src/cli.c | 9 +++++++++ app/src/options.h | 1 + app/src/server.c | 3 +++ .../main/java/com/genymobile/scrcpy/LogUtils.java | 12 +++++++++++- .../src/main/java/com/genymobile/scrcpy/Options.java | 10 +++++++++- .../src/main/java/com/genymobile/scrcpy/Server.java | 4 ++-- 9 files changed, 41 insertions(+), 4 deletions(-) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index b6f550c5..1cf750ac 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -23,6 +23,7 @@ _scrcpy() { --kill-adb-on-close -K --hid-keyboard --legacy-paste + --list-camera-sizes --list-cameras --list-displays --list-encoders diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 3ba2c4b8..926bc0a1 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -30,6 +30,7 @@ arguments=( '--kill-adb-on-close[Kill adb when scrcpy terminates]' {-K,--hid-keyboard}'[Simulate a physical keyboard by using HID over AOAv2]' '--legacy-paste[Inject computer clipboard text as a sequence of key events on Ctrl+v]' + '--list-camera-sizes[List the valid camera capture sizes]' '--list-cameras[List cameras available on the device]' '--list-displays[List displays available on the device]' '--list-encoders[List video and audio encoders available on the device]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 30d15b4d..428254dd 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -155,6 +155,10 @@ Inject computer clipboard text as a sequence of key events on Ctrl+v (like MOD+S This is a workaround for some devices not behaving as expected when setting the device clipboard programmatically. +.TP +.B \-\-list\-camera\-sizes +List the valid camera capture sizes. + .TP .B \-\-list\-cameras List cameras available on the device. diff --git a/app/src/cli.c b/app/src/cli.c index c9382c5b..788a629b 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -82,6 +82,7 @@ enum { OPT_TIME_LIMIT, OPT_PAUSE_ON_EXIT, OPT_LIST_CAMERAS, + OPT_LIST_CAMERA_SIZES, }; struct sc_option { @@ -326,6 +327,11 @@ static const struct sc_option options[] = { .longopt = "list-cameras", .text = "List device cameras.", }, + { + .longopt_id = OPT_LIST_CAMERA_SIZES, + .longopt = "list-camera-sizes", + .text = "List the valid camera capture sizes.", + }, { .longopt_id = OPT_LIST_DISPLAYS, .longopt = "list-displays", @@ -2007,6 +2013,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], case OPT_LIST_CAMERAS: opts->list |= SC_OPTION_LIST_CAMERAS; break; + case OPT_LIST_CAMERA_SIZES: + opts->list |= SC_OPTION_LIST_CAMERA_SIZES; + break; case OPT_REQUIRE_AUDIO: opts->require_audio = true; break; diff --git a/app/src/options.h b/app/src/options.h index e960968a..070a2b00 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -183,6 +183,7 @@ struct scrcpy_options { #define SC_OPTION_LIST_ENCODERS 0x1 #define SC_OPTION_LIST_DISPLAYS 0x2 #define SC_OPTION_LIST_CAMERAS 0x4 +#define SC_OPTION_LIST_CAMERA_SIZES 0x8 uint8_t list; }; diff --git a/app/src/server.c b/app/src/server.c index 571b813c..424c67e9 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -320,6 +320,9 @@ execute_server(struct sc_server *server, if (params->list & SC_OPTION_LIST_CAMERAS) { ADD_PARAM("list_cameras=true"); } + if (params->list & SC_OPTION_LIST_CAMERA_SIZES) { + ADD_PARAM("list_camera_sizes=true"); + } #undef ADD_PARAM diff --git a/server/src/main/java/com/genymobile/scrcpy/LogUtils.java b/server/src/main/java/com/genymobile/scrcpy/LogUtils.java index d609adcd..7806cf51 100644 --- a/server/src/main/java/com/genymobile/scrcpy/LogUtils.java +++ b/server/src/main/java/com/genymobile/scrcpy/LogUtils.java @@ -7,6 +7,8 @@ import android.graphics.Rect; import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraCharacteristics; import android.hardware.camera2.CameraManager; +import android.hardware.camera2.params.StreamConfigurationMap; +import android.media.MediaCodec; import java.util.List; @@ -79,7 +81,7 @@ public final class LogUtils { } } - public static String buildCameraListMessage() { + public static String buildCameraListMessage(boolean includeSizes) { StringBuilder builder = new StringBuilder("List of cameras:"); CameraManager cameraManager = ServiceManager.getCameraManager(); try { @@ -96,6 +98,14 @@ public final class LogUtils { Rect activeSize = characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); builder.append(activeSize.width()).append("x").append(activeSize.height()).append(')'); + + if (includeSizes) { + StreamConfigurationMap configs = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); + android.util.Size[] sizes = configs.getOutputSizes(MediaCodec.class); + for (android.util.Size size : sizes) { + builder.append("\n - ").append(size.getWidth()).append('x').append(size.getHeight()); + } + } } } } catch (CameraAccessException e) { diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 0a3032cf..c9600404 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -39,6 +39,7 @@ public class Options { private boolean listEncoders; private boolean listDisplays; private boolean listCameras; + private boolean listCameraSizes; // Options not used by the scrcpy client, but useful to use scrcpy-server directly private boolean sendDeviceMeta = true; // send device name and size @@ -155,7 +156,7 @@ public class Options { } public boolean getList() { - return listEncoders || listDisplays || listCameras; + return listEncoders || listDisplays || listCameras || listCameraSizes; } public boolean getListEncoders() { @@ -170,6 +171,10 @@ public class Options { return listCameras; } + public boolean getListCameraSizes() { + return listCameraSizes; + } + public boolean getSendDeviceMeta() { return sendDeviceMeta; } @@ -320,6 +325,9 @@ public class Options { case "list_cameras": options.listCameras = Boolean.parseBoolean(value); break; + case "list_camera_sizes": + options.listCameraSizes = Boolean.parseBoolean(value); + break; case "send_device_meta": options.sendDeviceMeta = Boolean.parseBoolean(value); break; diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 2129dbc0..4dbc00fe 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -208,9 +208,9 @@ public final class Server { if (options.getListDisplays()) { Ln.i(LogUtils.buildDisplayListMessage()); } - if (options.getListCameras()) { + if (options.getListCameras() || options.getListCameraSizes()) { Workarounds.apply(false, true); - Ln.i(LogUtils.buildCameraListMessage()); + Ln.i(LogUtils.buildCameraListMessage(options.getListCameraSizes())); } // Just print the requested data, do not mirror return; From bfeecc01316cb04b992f56c2b5d0ea9f21a45b7d Mon Sep 17 00:00:00 2001 From: Simon Chan <1330321+yume-chan@users.noreply.github.com> Date: Sun, 16 Jul 2023 17:07:19 +0800 Subject: [PATCH 1650/2244] Add camera mirroring Add --video-source=camera, and related options: - --camera-id=: select the camera by its id (see --list-cameras); - --camera-size=x: select the capture size. Fixed #241 PR #4213 Co-authored-by: Romain Vimont Signed-off-by: Romain Vimont --- app/data/bash-completion/scrcpy | 9 + app/data/zsh-completion/_scrcpy | 3 + app/scrcpy.1 | 18 ++ app/src/cli.c | 78 ++++++++ app/src/options.c | 3 + app/src/options.h | 8 + app/src/scrcpy.c | 3 + app/src/server.c | 12 ++ app/src/server.h | 3 + .../com/genymobile/scrcpy/CameraCapture.java | 180 ++++++++++++++++++ .../genymobile/scrcpy/HandlerExecutor.java | 23 +++ .../java/com/genymobile/scrcpy/Options.java | 43 +++++ .../com/genymobile/scrcpy/ScreenCapture.java | 3 +- .../java/com/genymobile/scrcpy/Server.java | 13 +- .../com/genymobile/scrcpy/SurfaceCapture.java | 7 +- .../com/genymobile/scrcpy/SurfaceEncoder.java | 8 +- .../com/genymobile/scrcpy/VideoSource.java | 22 +++ 17 files changed, 426 insertions(+), 10 deletions(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/CameraCapture.java create mode 100644 server/src/main/java/com/genymobile/scrcpy/HandlerExecutor.java create mode 100644 server/src/main/java/com/genymobile/scrcpy/VideoSource.java diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index 1cf750ac..27448baf 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -10,6 +10,8 @@ _scrcpy() { --audio-source= --audio-output-buffer= -b --video-bit-rate= + --camera-id= + --camera-size= --crop= -d --select-usb --disable-screensaver @@ -74,6 +76,7 @@ _scrcpy() { --video-codec= --video-codec-options= --video-encoder= + --video-source= -w --stay-awake --window-borderless --window-title= @@ -93,6 +96,10 @@ _scrcpy() { COMPREPLY=($(compgen -W 'opus aac raw' -- "$cur")) return ;; + --video-source) + COMPREPLY=($(compgen -W 'display camera' -- "$cur")) + return + ;; --audio-source) COMPREPLY=($(compgen -W 'output mic' -- "$cur")) return @@ -141,6 +148,8 @@ _scrcpy() { |--audio-codec-options \ |--audio-encoder \ |--audio-output-buffer \ + |--camera-id \ + |--camera-size \ |--crop \ |--display-id \ |--display-buffer \ diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 926bc0a1..58c3cccc 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -17,6 +17,8 @@ arguments=( '--audio-source=[Select the audio source]:source:(output mic)' '--audio-output-buffer=[Configure the size of the SDL audio output buffer (in milliseconds)]' {-b,--video-bit-rate=}'[Encode the video at the given bit-rate]' + '--camera-id=[Specify the camera id to mirror]' + '--camera-size=[Specify an explicit camera capture size]' '--crop=[\[width\:height\:x\:y\] Crop the device screen on the server]' {-d,--select-usb}'[Use USB device]' '--disable-screensaver[Disable screensaver while scrcpy is running]' @@ -78,6 +80,7 @@ arguments=( '--video-codec=[Select the video codec]:codec:(h264 h265 av1)' '--video-codec-options=[Set a list of comma-separated key\:type=value options for the device video encoder]' '--video-encoder=[Use a specific MediaCodec video encoder]' + '--video-source=[Select the video source]:source:(display camera)' {-w,--stay-awake}'[Keep the device on while scrcpy is running, when the device is plugged in]' '--window-borderless[Disable window decorations \(display borderless window\)]' '--window-title=[Set a custom window title]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 428254dd..b108d675 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -75,6 +75,16 @@ Encode the video at the given bit rate, expressed in bits/s. Unit suffixes are s Default is 8M (8000000). +.TP +.BI "\-\-camera\-id " id +Specify the device camera id to mirror. + +The available camera ids can be listed by \-\-list\-cameras. + +.TP +.BI "\-\-camera\-size " width\fRx\fIheight +Specify an explicit camera capture size. + .TP .BI "\-\-crop " width\fR:\fIheight\fR:\fIx\fR:\fIy Crop the device screen on the server. @@ -434,6 +444,14 @@ Use a specific MediaCodec video encoder (depending on the codec provided by \fB\ The available encoders can be listed by \-\-list\-encoders. +.TP +.BI "\-\-video\-source " source +Select the video source (display or camera). + +Camera mirroring requires Android 12+. + +Default is display. + .TP .B \-w, \-\-stay-awake Keep the device on while scrcpy is running, when the device is plugged in. diff --git a/app/src/cli.c b/app/src/cli.c index 788a629b..4b54b401 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -77,12 +77,15 @@ enum { OPT_NO_VIDEO, OPT_NO_AUDIO_PLAYBACK, OPT_NO_VIDEO_PLAYBACK, + OPT_VIDEO_SOURCE, OPT_AUDIO_SOURCE, OPT_KILL_ADB_ON_CLOSE, OPT_TIME_LIMIT, OPT_PAUSE_ON_EXIT, OPT_LIST_CAMERAS, OPT_LIST_CAMERA_SIZES, + OPT_CAMERA_ID, + OPT_CAMERA_SIZE, }; struct sc_option { @@ -199,6 +202,20 @@ static const struct sc_option options[] = { .longopt = "bit-rate", .argdesc = "value", }, + { + .longopt_id = OPT_CAMERA_ID, + .longopt = "camera-id", + .argdesc = "id", + .text = "Specify the device camera id to mirror.\n" + "The available camera ids can be listed by:\n" + " scrcpy --list-cameras", + }, + { + .longopt_id = OPT_CAMERA_SIZE, + .longopt = "camera-size", + .argdesc = "x", + .text = "Specify an explicit camera capture size.", + }, { // Not really deprecated (--codec has never been released), but without // declaring an explicit --codec option, getopt_long() partial matching @@ -703,6 +720,14 @@ static const struct sc_option options[] = { "codec provided by --video-codec).\n" "The available encoders can be listed by --list-encoders.", }, + { + .longopt_id = OPT_VIDEO_SOURCE, + .longopt = "video-source", + .argdesc = "source", + .text = "Select the video source (display or camera).\n" + "Camera mirroring requires Android 12+.\n" + "Default is display.", + }, { .shortopt = 'w', .longopt = "stay-awake", @@ -1643,6 +1668,22 @@ parse_audio_codec(const char *optarg, enum sc_codec *codec) { return false; } +static bool +parse_video_source(const char *optarg, enum sc_video_source *source) { + if (!strcmp(optarg, "display")) { + *source = SC_VIDEO_SOURCE_DISPLAY; + return true; + } + + if (!strcmp(optarg, "camera")) { + *source = SC_VIDEO_SOURCE_CAMERA; + return true; + } + + LOGE("Unsupported video source: %s (expected display or camera)", optarg); + return false; +} + static bool parse_audio_source(const char *optarg, enum sc_audio_source *source) { if (!strcmp(optarg, "mic")) { @@ -2030,6 +2071,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; } break; + case OPT_VIDEO_SOURCE: + if (!parse_video_source(optarg, &opts->video_source)) { + return false; + } + break; case OPT_AUDIO_SOURCE: if (!parse_audio_source(optarg, &opts->audio_source)) { return false; @@ -2048,6 +2094,12 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; } break; + case OPT_CAMERA_ID: + opts->camera_id = optarg; + break; + case OPT_CAMERA_SIZE: + opts->camera_size = optarg; + break; default: // getopt prints the error message on stderr return false; @@ -2141,6 +2193,32 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], opts->force_adb_forward = true; } + if (opts->video_source == SC_VIDEO_SOURCE_CAMERA) { + if (opts->display_id) { + LOGE("--display-id is only available with --video-source=display"); + return false; + } + + if (!opts->camera_id) { + LOGE("Camera id must be specified by --camera-id " + "(list the available ids with --list-cameras)"); + return false; + } + + if (!opts->camera_size) { + LOGE("Camera size must be specified by --camera-size"); + return false; + } + + if (opts->control) { + LOGI("Camera video source: control disabled"); + opts->control = false; + } + } else if (opts->camera_id || opts->camera_size) { + LOGE("Camera options are only available with --video-source=camera"); + return false; + } + if (opts->record_format && !opts->record_filename) { LOGE("Record format specified without recording"); return false; diff --git a/app/src/options.c b/app/src/options.c index b633d762..22be9f36 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -11,9 +11,12 @@ const struct scrcpy_options scrcpy_options_default = { .audio_codec_options = NULL, .video_encoder = NULL, .audio_encoder = NULL, + .camera_id = NULL, + .camera_size = NULL, .log_level = SC_LOG_LEVEL_INFO, .video_codec = SC_CODEC_H264, .audio_codec = SC_CODEC_OPUS, + .video_source = SC_VIDEO_SOURCE_DISPLAY, .audio_source = SC_AUDIO_SOURCE_OUTPUT, .record_format = SC_RECORD_FORMAT_AUTO, .keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_INJECT, diff --git a/app/src/options.h b/app/src/options.h index 070a2b00..af195793 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -44,6 +44,11 @@ enum sc_codec { SC_CODEC_RAW, }; +enum sc_video_source { + SC_VIDEO_SOURCE_DISPLAY, + SC_VIDEO_SOURCE_CAMERA, +}; + enum sc_audio_source { SC_AUDIO_SOURCE_OUTPUT, SC_AUDIO_SOURCE_MIC, @@ -117,9 +122,12 @@ struct scrcpy_options { const char *audio_codec_options; const char *video_encoder; const char *audio_encoder; + const char *camera_id; + const char *camera_size; enum sc_log_level log_level; enum sc_codec video_codec; enum sc_codec audio_codec; + enum sc_video_source video_source; enum sc_audio_source audio_source; enum sc_record_format record_format; enum sc_keyboard_input_mode keyboard_input_mode; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 5f0158f1..d51d573b 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -351,6 +351,7 @@ scrcpy(struct scrcpy_options *options) { .log_level = options->log_level, .video_codec = options->video_codec, .audio_codec = options->audio_codec, + .video_source = options->video_source, .audio_source = options->audio_source, .crop = options->crop, .port_range = options->port_range, @@ -371,6 +372,8 @@ scrcpy(struct scrcpy_options *options) { .audio_codec_options = options->audio_codec_options, .video_encoder = options->video_encoder, .audio_encoder = options->audio_encoder, + .camera_id = options->camera_id, + .camera_size = options->camera_size, .force_adb_forward = options->force_adb_forward, .power_off_on_close = options->power_off_on_close, .clipboard_autosync = options->clipboard_autosync, diff --git a/app/src/server.c b/app/src/server.c index 424c67e9..413103ef 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -76,6 +76,7 @@ sc_server_params_destroy(struct sc_server_params *params) { free((char *) params->video_encoder); free((char *) params->audio_encoder); free((char *) params->tcpip_dst); + free((char *) params->camera_id); } static bool @@ -103,6 +104,7 @@ sc_server_params_copy(struct sc_server_params *dst, COPY(video_encoder); COPY(audio_encoder); COPY(tcpip_dst); + COPY(camera_id); #undef COPY return true; @@ -247,6 +249,10 @@ execute_server(struct sc_server *server, ADD_PARAM("audio_codec=%s", sc_server_get_codec_name(params->audio_codec)); } + if (params->video_source != SC_VIDEO_SOURCE_DISPLAY) { + assert(params->video_source == SC_VIDEO_SOURCE_CAMERA); + ADD_PARAM("video_source=camera"); + } if (params->audio_source != SC_AUDIO_SOURCE_OUTPUT) { assert(params->audio_source == SC_AUDIO_SOURCE_MIC); ADD_PARAM("audio_source=mic"); @@ -274,6 +280,12 @@ execute_server(struct sc_server *server, if (params->display_id) { ADD_PARAM("display_id=%" PRIu32, params->display_id); } + if (params->camera_id) { + ADD_PARAM("camera_id=%s", params->camera_id); + } + if (params->camera_size) { + ADD_PARAM("camera_size=%s", params->camera_size); + } if (params->show_touches) { ADD_PARAM("show_touches=true"); } diff --git a/app/src/server.h b/app/src/server.h index 04955974..92c5f22e 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -26,12 +26,15 @@ struct sc_server_params { enum sc_log_level log_level; enum sc_codec video_codec; enum sc_codec audio_codec; + enum sc_video_source video_source; enum sc_audio_source audio_source; const char *crop; const char *video_codec_options; const char *audio_codec_options; const char *video_encoder; const char *audio_encoder; + const char *camera_id; + const char *camera_size; struct sc_port_range port_range; uint32_t tunnel_host; uint16_t tunnel_port; diff --git a/server/src/main/java/com/genymobile/scrcpy/CameraCapture.java b/server/src/main/java/com/genymobile/scrcpy/CameraCapture.java new file mode 100644 index 00000000..3efd4cb2 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/CameraCapture.java @@ -0,0 +1,180 @@ +package com.genymobile.scrcpy; + +import com.genymobile.scrcpy.wrappers.ServiceManager; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.hardware.camera2.CameraAccessException; +import android.hardware.camera2.CameraCaptureSession; +import android.hardware.camera2.CameraDevice; +import android.hardware.camera2.CaptureFailure; +import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.params.OutputConfiguration; +import android.hardware.camera2.params.SessionConfiguration; +import android.os.Build; +import android.os.Handler; +import android.os.HandlerThread; +import android.view.Surface; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; + +public class CameraCapture extends SurfaceCapture { + + private final String explicitCameraId; + private final Size explicitSize; + + private HandlerThread cameraThread; + private Handler cameraHandler; + private CameraDevice cameraDevice; + private Executor cameraExecutor; + + public CameraCapture(String explicitCameraId, Size explicitSize) { + this.explicitCameraId = explicitCameraId; + this.explicitSize = explicitSize; + } + + @Override + public void init() throws IOException { + cameraThread = new HandlerThread("camera"); + cameraThread.start(); + cameraHandler = new Handler(cameraThread.getLooper()); + cameraExecutor = new HandlerExecutor(cameraHandler); + + try { + cameraDevice = openCamera(explicitCameraId); + } catch (CameraAccessException | InterruptedException e) { + throw new IOException(e); + } + } + + @Override + public void start(Surface surface) throws IOException { + try { + CameraCaptureSession session = createCaptureSession(cameraDevice, surface); + CaptureRequest request = createCaptureRequest(surface); + setRepeatingRequest(session, request); + } catch (CameraAccessException | InterruptedException e) { + throw new IOException(e); + } + } + + @Override + public void release() { + if (cameraDevice != null) { + cameraDevice.close(); + } + if (cameraThread != null) { + cameraThread.quitSafely(); + } + } + + @Override + public Size getSize() { + return explicitSize; + } + + @Override + public boolean setMaxSize(int maxSize) { + return false; + } + + @SuppressLint("MissingPermission") + @TargetApi(Build.VERSION_CODES.S) + private CameraDevice openCamera(String id) throws CameraAccessException, InterruptedException { + CompletableFuture future = new CompletableFuture<>(); + ServiceManager.getCameraManager().openCamera(id, new CameraDevice.StateCallback() { + @Override + public void onOpened(CameraDevice camera) { + Ln.d("Camera opened successfully"); + future.complete(camera); + } + + @Override + public void onDisconnected(CameraDevice camera) { + Ln.w("Camera disconnected"); + // TODO + } + + @Override + public void onError(CameraDevice camera, int error) { + int cameraAccessExceptionErrorCode; + switch (error) { + case CameraDevice.StateCallback.ERROR_CAMERA_IN_USE: + cameraAccessExceptionErrorCode = CameraAccessException.CAMERA_IN_USE; + break; + case CameraDevice.StateCallback.ERROR_MAX_CAMERAS_IN_USE: + cameraAccessExceptionErrorCode = CameraAccessException.MAX_CAMERAS_IN_USE; + break; + case CameraDevice.StateCallback.ERROR_CAMERA_DISABLED: + cameraAccessExceptionErrorCode = CameraAccessException.CAMERA_DISABLED; + break; + case CameraDevice.StateCallback.ERROR_CAMERA_DEVICE: + case CameraDevice.StateCallback.ERROR_CAMERA_SERVICE: + default: + cameraAccessExceptionErrorCode = CameraAccessException.CAMERA_ERROR; + break; + } + future.completeExceptionally(new CameraAccessException(cameraAccessExceptionErrorCode)); + } + }, cameraHandler); + + try { + return future.get(); + } catch (ExecutionException e) { + throw (CameraAccessException) e.getCause(); + } + } + + @TargetApi(Build.VERSION_CODES.S) + private CameraCaptureSession createCaptureSession(CameraDevice camera, Surface surface) throws CameraAccessException, InterruptedException { + CompletableFuture future = new CompletableFuture<>(); + OutputConfiguration outputConfig = new OutputConfiguration(surface); + List outputs = Arrays.asList(outputConfig); + SessionConfiguration sessionConfig = new SessionConfiguration(SessionConfiguration.SESSION_REGULAR, outputs, cameraExecutor, + new CameraCaptureSession.StateCallback() { + @Override + public void onConfigured(CameraCaptureSession session) { + future.complete(session); + } + + @Override + public void onConfigureFailed(CameraCaptureSession session) { + future.completeExceptionally(new CameraAccessException(CameraAccessException.CAMERA_ERROR)); + } + }); + + camera.createCaptureSession(sessionConfig); + + try { + return future.get(); + } catch (ExecutionException e) { + throw (CameraAccessException) e.getCause(); + } + } + + private CaptureRequest createCaptureRequest(Surface surface) throws CameraAccessException { + CaptureRequest.Builder requestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD); + requestBuilder.addTarget(surface); + return requestBuilder.build(); + } + + @TargetApi(Build.VERSION_CODES.S) + private void setRepeatingRequest(CameraCaptureSession session, CaptureRequest request) throws CameraAccessException, InterruptedException { + session.setRepeatingRequest(request, new CameraCaptureSession.CaptureCallback() { + @Override + public void onCaptureStarted(CameraCaptureSession session, CaptureRequest request, long timestamp, long frameNumber) { + // Called for each frame captured, do nothing + } + + @Override + public void onCaptureFailed(CameraCaptureSession session, CaptureRequest request, CaptureFailure failure) { + Ln.w("Camera capture failed: frame " + failure.getFrameNumber()); + } + }, cameraHandler); + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/HandlerExecutor.java b/server/src/main/java/com/genymobile/scrcpy/HandlerExecutor.java new file mode 100644 index 00000000..1f5f0a4f --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/HandlerExecutor.java @@ -0,0 +1,23 @@ +package com.genymobile.scrcpy; + +import android.os.Handler; + +import java.util.concurrent.Executor; +import java.util.concurrent.RejectedExecutionException; + +// Inspired from hidden android.os.HandlerExecutor + +public class HandlerExecutor implements Executor { + private final Handler handler; + + public HandlerExecutor(Handler handler) { + this.handler = handler; + } + + @Override + public void execute(Runnable command) { + if (!handler.post(command)) { + throw new RejectedExecutionException(handler + " is shutting down"); + } + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index c9600404..2366f9c8 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 maxSize; private VideoCodec videoCodec = VideoCodec.H264; private AudioCodec audioCodec = AudioCodec.OPUS; + private VideoSource videoSource = VideoSource.DISPLAY; private AudioSource audioSource = AudioSource.OUTPUT; private int videoBitRate = 8000000; private int audioBitRate = 128000; @@ -23,6 +24,8 @@ public class Options { private Rect crop; private boolean control = true; private int displayId; + private String cameraId; + private Size cameraSize; private boolean showTouches; private boolean stayAwake; private List videoCodecOptions; @@ -75,6 +78,10 @@ public class Options { return audioCodec; } + public VideoSource getVideoSource() { + return videoSource; + } + public AudioSource getAudioSource() { return audioSource; } @@ -111,6 +118,14 @@ public class Options { return displayId; } + public String getCameraId() { + return cameraId; + } + + public Size getCameraSize() { + return cameraSize; + } + public boolean getShowTouches() { return showTouches; } @@ -244,6 +259,13 @@ public class Options { } options.audioCodec = audioCodec; break; + case "video_source": + VideoSource videoSource = VideoSource.findByName(value); + if (videoSource == null) { + throw new IllegalArgumentException("Video source " + value + " not supported"); + } + options.videoSource = videoSource; + break; case "audio_source": AudioSource audioSource = AudioSource.findByName(value); if (audioSource == null) { @@ -328,6 +350,16 @@ public class Options { case "list_camera_sizes": options.listCameraSizes = Boolean.parseBoolean(value); break; + case "camera_id": + if (!value.isEmpty()) { + options.cameraId = value; + } + break; + case "camera_size": + if (!value.isEmpty()) { + options.cameraSize = parseSize(value); + } + break; case "send_device_meta": options.sendDeviceMeta = Boolean.parseBoolean(value); break; @@ -370,4 +402,15 @@ public class Options { int y = Integer.parseInt(tokens[3]); return new Rect(x, y, x + width, y + height); } + + private static Size parseSize(String size) { + // input format: "x" + String[] tokens = size.split("x"); + if (tokens.length != 2) { + throw new IllegalArgumentException("Invalid size format (expected x): \"" + size + "\""); + } + int width = Integer.parseInt(tokens[0]); + int height = Integer.parseInt(tokens[1]); + return new Size(width, height); + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenCapture.java b/server/src/main/java/com/genymobile/scrcpy/ScreenCapture.java index f9ac66b8..f81332f5 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenCapture.java @@ -48,8 +48,9 @@ public class ScreenCapture extends SurfaceCapture implements Device.RotationList } @Override - public void setMaxSize(int maxSize) { + public boolean setMaxSize(int maxSize) { device.setMaxSize(maxSize); + return true; } @Override diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 4dbc00fe..e43b9f0a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -98,7 +98,7 @@ public final class Server { boolean video = options.getVideo(); boolean audio = options.getAudio(); boolean sendDummyByte = options.getSendDummyByte(); - boolean camera = false; + boolean camera = options.getVideoSource() == VideoSource.CAMERA; Workarounds.apply(audio, camera); @@ -133,10 +133,15 @@ public final class Server { if (video) { Streamer videoStreamer = new Streamer(connection.getVideoFd(), options.getVideoCodec(), options.getSendCodecMeta(), options.getSendFrameMeta()); - ScreenCapture screenCapture = new ScreenCapture(device); - SurfaceEncoder screenEncoder = new SurfaceEncoder(screenCapture, videoStreamer, options.getVideoBitRate(), options.getMaxFps(), + SurfaceCapture surfaceCapture; + if (options.getVideoSource() == VideoSource.DISPLAY) { + surfaceCapture = new ScreenCapture(device); + } else { + surfaceCapture = new CameraCapture(options.getCameraId(), options.getCameraSize()); + } + SurfaceEncoder surfaceEncoder = new SurfaceEncoder(surfaceCapture, videoStreamer, options.getVideoBitRate(), options.getMaxFps(), options.getVideoCodecOptions(), options.getVideoEncoder(), options.getDownsizeOnError()); - asyncProcessors.add(screenEncoder); + asyncProcessors.add(surfaceEncoder); } Completion completion = new Completion(asyncProcessors.size()); diff --git a/server/src/main/java/com/genymobile/scrcpy/SurfaceCapture.java b/server/src/main/java/com/genymobile/scrcpy/SurfaceCapture.java index 45a0fd2f..207cfad8 100644 --- a/server/src/main/java/com/genymobile/scrcpy/SurfaceCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/SurfaceCapture.java @@ -2,6 +2,7 @@ package com.genymobile.scrcpy; import android.view.Surface; +import java.io.IOException; import java.util.concurrent.atomic.AtomicBoolean; /** @@ -31,7 +32,7 @@ public abstract class SurfaceCapture { /** * Called once before the capture starts. */ - public abstract void init(); + public abstract void init() throws IOException; /** * Called after the capture ends (if and only if {@link #init()} has been called). @@ -43,7 +44,7 @@ public abstract class SurfaceCapture { * * @param surface the surface which will be encoded */ - public abstract void start(Surface surface); + public abstract void start(Surface surface) throws IOException; /** * Return the video size @@ -57,5 +58,5 @@ public abstract class SurfaceCapture { * * @param maxSize Maximum size */ - public abstract void setMaxSize(int maxSize); + public abstract boolean setMaxSize(int maxSize); } diff --git a/server/src/main/java/com/genymobile/scrcpy/SurfaceEncoder.java b/server/src/main/java/com/genymobile/scrcpy/SurfaceEncoder.java index 4af31e89..9f90115a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/SurfaceEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/SurfaceEncoder.java @@ -122,9 +122,13 @@ public class SurfaceEncoder implements AsyncProcessor { return false; } - // Retry with a smaller device size + boolean accepted = capture.setMaxSize(newMaxSize); + if (!accepted) { + return false; + } + + // Retry with a smaller size Ln.i("Retrying with -m" + newMaxSize + "..."); - capture.setMaxSize(newMaxSize); return true; } diff --git a/server/src/main/java/com/genymobile/scrcpy/VideoSource.java b/server/src/main/java/com/genymobile/scrcpy/VideoSource.java new file mode 100644 index 00000000..b5a74fbe --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/VideoSource.java @@ -0,0 +1,22 @@ +package com.genymobile.scrcpy; + +public enum VideoSource { + DISPLAY("display"), + CAMERA("camera"); + + private final String name; + + VideoSource(String name) { + this.name = name; + } + + static VideoSource findByName(String name) { + for (VideoSource videoSource : VideoSource.values()) { + if (name.equals(videoSource.name)) { + return videoSource; + } + } + + return null; + } +} From d544e577c086efc89443c388466b14ae0f25af59 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 25 Oct 2023 00:09:54 +0200 Subject: [PATCH 1651/2244] Automatically select audio source If --audio-source is not specified, select the default value according to the video source: - for display mirroring, use device audio by default; - for camera mirroring, use microphone by default. PR #4213 --- app/src/cli.c | 10 ++++++++++ app/src/options.c | 2 +- app/src/options.h | 1 + app/src/server.c | 3 +-- 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index 4b54b401..7f2b3d49 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -2219,6 +2219,16 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; } + if (opts->audio && opts->audio_source == SC_AUDIO_SOURCE_AUTO) { + // Select the audio source according to the video source + if (opts->video_source == SC_VIDEO_SOURCE_DISPLAY) { + opts->audio_source = SC_AUDIO_SOURCE_OUTPUT; + } else { + opts->audio_source = SC_AUDIO_SOURCE_MIC; + LOGI("Camera video source: microphone audio source selected"); + } + } + if (opts->record_format && !opts->record_filename) { LOGE("Record format specified without recording"); return false; diff --git a/app/src/options.c b/app/src/options.c index 22be9f36..96741a7d 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -17,7 +17,7 @@ const struct scrcpy_options scrcpy_options_default = { .video_codec = SC_CODEC_H264, .audio_codec = SC_CODEC_OPUS, .video_source = SC_VIDEO_SOURCE_DISPLAY, - .audio_source = SC_AUDIO_SOURCE_OUTPUT, + .audio_source = SC_AUDIO_SOURCE_AUTO, .record_format = SC_RECORD_FORMAT_AUTO, .keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_INJECT, .mouse_input_mode = SC_MOUSE_INPUT_MODE_INJECT, diff --git a/app/src/options.h b/app/src/options.h index af195793..afc6aa49 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -50,6 +50,7 @@ enum sc_video_source { }; enum sc_audio_source { + SC_AUDIO_SOURCE_AUTO, // OUTPUT for video DISPLAY, MIC for video CAMERA SC_AUDIO_SOURCE_OUTPUT, SC_AUDIO_SOURCE_MIC, }; diff --git a/app/src/server.c b/app/src/server.c index 413103ef..81a371ae 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -253,8 +253,7 @@ execute_server(struct sc_server *server, assert(params->video_source == SC_VIDEO_SOURCE_CAMERA); ADD_PARAM("video_source=camera"); } - if (params->audio_source != SC_AUDIO_SOURCE_OUTPUT) { - assert(params->audio_source == SC_AUDIO_SOURCE_MIC); + if (params->audio_source == SC_AUDIO_SOURCE_MIC) { ADD_PARAM("audio_source=mic"); } if (params->max_size) { From 64930e71b9636544468646e61dac3d7a4a62896c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 25 Oct 2023 00:18:05 +0200 Subject: [PATCH 1652/2244] Handle camera disconnection Stop mirroring on camera disconnection. PR #4213 --- .../java/com/genymobile/scrcpy/CameraCapture.java | 11 ++++++++++- .../java/com/genymobile/scrcpy/SurfaceCapture.java | 9 +++++++++ .../java/com/genymobile/scrcpy/SurfaceEncoder.java | 5 +++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/CameraCapture.java b/server/src/main/java/com/genymobile/scrcpy/CameraCapture.java index 3efd4cb2..78ad0e09 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CameraCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/CameraCapture.java @@ -22,6 +22,7 @@ import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicBoolean; public class CameraCapture extends SurfaceCapture { @@ -33,6 +34,8 @@ public class CameraCapture extends SurfaceCapture { private CameraDevice cameraDevice; private Executor cameraExecutor; + private final AtomicBoolean disconnected = new AtomicBoolean(); + public CameraCapture(String explicitCameraId, Size explicitSize) { this.explicitCameraId = explicitCameraId; this.explicitSize = explicitSize; @@ -97,7 +100,8 @@ public class CameraCapture extends SurfaceCapture { @Override public void onDisconnected(CameraDevice camera) { Ln.w("Camera disconnected"); - // TODO + disconnected.set(true); + requestReset(); } @Override @@ -177,4 +181,9 @@ public class CameraCapture extends SurfaceCapture { } }, cameraHandler); } + + @Override + public boolean isClosed() { + return disconnected.get(); + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/SurfaceCapture.java b/server/src/main/java/com/genymobile/scrcpy/SurfaceCapture.java index 207cfad8..e300e4d6 100644 --- a/server/src/main/java/com/genymobile/scrcpy/SurfaceCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/SurfaceCapture.java @@ -59,4 +59,13 @@ public abstract class SurfaceCapture { * @param maxSize Maximum size */ public abstract boolean setMaxSize(int maxSize); + + /** + * Indicate if the capture has been closed internally. + * + * @return {@code true} is the capture is closed, {@code false} otherwise. + */ + public boolean isClosed() { + return false; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/SurfaceEncoder.java b/server/src/main/java/com/genymobile/scrcpy/SurfaceEncoder.java index 9f90115a..28435c09 100644 --- a/server/src/main/java/com/genymobile/scrcpy/SurfaceEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/SurfaceEncoder.java @@ -181,6 +181,11 @@ public class SurfaceEncoder implements AsyncProcessor { } } + if (capture.isClosed()) { + // The capture might have been closed internally (for example if the camera is disconnected) + alive = false; + } + return !eof && alive; } From 7f8d079c8c5dd28942992361d2f521a554d18416 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 26 Oct 2023 23:07:56 +0200 Subject: [PATCH 1653/2244] Make camera id optional If no camera id is provided, use the first camera available. PR #4213 --- app/src/cli.c | 6 ------ .../com/genymobile/scrcpy/CameraCapture.java | 21 ++++++++++++++++++- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index 7f2b3d49..cac54730 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -2199,12 +2199,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; } - if (!opts->camera_id) { - LOGE("Camera id must be specified by --camera-id " - "(list the available ids with --list-cameras)"); - return false; - } - if (!opts->camera_size) { LOGE("Camera size must be specified by --camera-size"); return false; diff --git a/server/src/main/java/com/genymobile/scrcpy/CameraCapture.java b/server/src/main/java/com/genymobile/scrcpy/CameraCapture.java index 78ad0e09..5aadbae7 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CameraCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/CameraCapture.java @@ -7,6 +7,7 @@ import android.annotation.TargetApi; import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraCaptureSession; import android.hardware.camera2.CameraDevice; +import android.hardware.camera2.CameraManager; import android.hardware.camera2.CaptureFailure; import android.hardware.camera2.CaptureRequest; import android.hardware.camera2.params.OutputConfiguration; @@ -49,12 +50,30 @@ public class CameraCapture extends SurfaceCapture { cameraExecutor = new HandlerExecutor(cameraHandler); try { - cameraDevice = openCamera(explicitCameraId); + String cameraId = selectCamera(explicitCameraId); + if (cameraId == null) { + throw new IOException("No matching camera found"); + } + + Ln.i("Using camera '" + cameraId + "'"); + cameraDevice = openCamera(cameraId); } catch (CameraAccessException | InterruptedException e) { throw new IOException(e); } } + private static String selectCamera(String explicitCameraId) throws CameraAccessException { + if (explicitCameraId != null) { + return explicitCameraId; + } + + CameraManager cameraManager = ServiceManager.getCameraManager(); + + String[] cameraIds = cameraManager.getCameraIdList(); + // Use the first one + return cameraIds.length > 0 ? cameraIds[0] : null; + } + @Override public void start(Surface surface) throws IOException { try { From faebb7d70ab4076fcbe84f00a1e2d07d871e41d9 Mon Sep 17 00:00:00 2001 From: Simon Chan <1330321+yume-chan@users.noreply.github.com> Date: Sat, 22 Jul 2023 17:17:05 +0800 Subject: [PATCH 1654/2244] Add --camera-facing Add an option to select the camera by its lens facing (front, back or external). PR #4213 Co-authored-by: Romain Vimont Signed-off-by: Romain Vimont --- app/data/bash-completion/scrcpy | 5 ++ app/data/zsh-completion/_scrcpy | 1 + app/scrcpy.1 | 6 +++ app/src/cli.c | 50 ++++++++++++++++++- app/src/options.c | 1 + app/src/options.h | 8 +++ app/src/scrcpy.c | 1 + app/src/server.c | 18 +++++++ app/src/server.h | 1 + .../com/genymobile/scrcpy/CameraCapture.java | 27 ++++++++-- .../com/genymobile/scrcpy/CameraFacing.java | 33 ++++++++++++ .../java/com/genymobile/scrcpy/Options.java | 14 ++++++ .../java/com/genymobile/scrcpy/Server.java | 2 +- 13 files changed, 160 insertions(+), 7 deletions(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/CameraFacing.java diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index 27448baf..339a819a 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -11,6 +11,7 @@ _scrcpy() { --audio-output-buffer= -b --video-bit-rate= --camera-id= + --camera-facing= --camera-size= --crop= -d --select-usb @@ -104,6 +105,10 @@ _scrcpy() { COMPREPLY=($(compgen -W 'output mic' -- "$cur")) return ;; + --camera-facing) + COMPREPLY=($(compgen -W 'front back external' -- "$cur")) + return + ;; --lock-video-orientation) COMPREPLY=($(compgen -W 'unlocked initial 0 1 2 3' -- "$cur")) return diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 58c3cccc..c92f0ac1 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -18,6 +18,7 @@ arguments=( '--audio-output-buffer=[Configure the size of the SDL audio output buffer (in milliseconds)]' {-b,--video-bit-rate=}'[Encode the video at the given bit-rate]' '--camera-id=[Specify the camera id to mirror]' + '--camera-facing=[Select the device camera by its facing direction]:facing:(front back external)' '--camera-size=[Specify an explicit camera capture size]' '--crop=[\[width\:height\:x\:y\] Crop the device screen on the server]' {-d,--select-usb}'[Use USB device]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index b108d675..d2fb3ad5 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -81,6 +81,12 @@ Specify the device camera id to mirror. The available camera ids can be listed by \-\-list\-cameras. +.TP +.BI "\-\-camera\-facing " facing +Select the device camera by its facing direction. + +Possible values are "front", "back" and "external". + .TP .BI "\-\-camera\-size " width\fRx\fIheight Specify an explicit camera capture size. diff --git a/app/src/cli.c b/app/src/cli.c index cac54730..96b8c26d 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -86,6 +86,7 @@ enum { OPT_LIST_CAMERA_SIZES, OPT_CAMERA_ID, OPT_CAMERA_SIZE, + OPT_CAMERA_FACING, }; struct sc_option { @@ -210,6 +211,13 @@ static const struct sc_option options[] = { "The available camera ids can be listed by:\n" " scrcpy --list-cameras", }, + { + .longopt_id = OPT_CAMERA_FACING, + .longopt = "camera-facing", + .argdesc = "facing", + .text = "Select the device camera by its facing direction.\n" + "Possible values are \"front\", \"back\" and \"external\".", + }, { .longopt_id = OPT_CAMERA_SIZE, .longopt = "camera-size", @@ -1700,6 +1708,34 @@ parse_audio_source(const char *optarg, enum sc_audio_source *source) { return false; } +static bool +parse_camera_facing(const char *optarg, enum sc_camera_facing *facing) { + if (!strcmp(optarg, "front")) { + *facing = SC_CAMERA_FACING_FRONT; + return true; + } + + if (!strcmp(optarg, "back")) { + *facing = SC_CAMERA_FACING_BACK; + return true; + } + + if (!strcmp(optarg, "external")) { + *facing = SC_CAMERA_FACING_EXTERNAL; + return true; + } + + if (*optarg == '\0') { + // Empty string is a valid value (equivalent to not passing the option) + *facing = SC_CAMERA_FACING_ANY; + return true; + } + + LOGE("Unsupported camera facing: %s (expected front, back or external)", + optarg); + return false; +} + static bool parse_time_limit(const char *s, sc_tick *tick) { long value; @@ -2100,6 +2136,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], case OPT_CAMERA_SIZE: opts->camera_size = optarg; break; + case OPT_CAMERA_FACING: + if (!parse_camera_facing(optarg, &opts->camera_facing)) { + return false; + } + break; default: // getopt prints the error message on stderr return false; @@ -2199,6 +2240,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; } + if (opts->camera_id && opts->camera_facing != SC_CAMERA_FACING_ANY) { + LOGE("Could not specify both --camera-id and --camera-facing"); + return false; + } + if (!opts->camera_size) { LOGE("Camera size must be specified by --camera-size"); return false; @@ -2208,7 +2254,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], LOGI("Camera video source: control disabled"); opts->control = false; } - } else if (opts->camera_id || opts->camera_size) { + } else if (opts->camera_id + || opts->camera_facing != SC_CAMERA_FACING_ANY + || opts->camera_size) { LOGE("Camera options are only available with --video-source=camera"); return false; } diff --git a/app/src/options.c b/app/src/options.c index 96741a7d..2adb4323 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -21,6 +21,7 @@ const struct scrcpy_options scrcpy_options_default = { .record_format = SC_RECORD_FORMAT_AUTO, .keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_INJECT, .mouse_input_mode = SC_MOUSE_INPUT_MODE_INJECT, + .camera_facing = SC_CAMERA_FACING_ANY, .port_range = { .first = DEFAULT_LOCAL_PORT_RANGE_FIRST, .last = DEFAULT_LOCAL_PORT_RANGE_LAST, diff --git a/app/src/options.h b/app/src/options.h index afc6aa49..1e783bae 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -55,6 +55,13 @@ enum sc_audio_source { SC_AUDIO_SOURCE_MIC, }; +enum sc_camera_facing { + SC_CAMERA_FACING_ANY, + SC_CAMERA_FACING_FRONT, + SC_CAMERA_FACING_BACK, + SC_CAMERA_FACING_EXTERNAL, +}; + enum sc_lock_video_orientation { SC_LOCK_VIDEO_ORIENTATION_UNLOCKED = -1, // lock the current orientation when scrcpy starts @@ -133,6 +140,7 @@ struct scrcpy_options { enum sc_record_format record_format; enum sc_keyboard_input_mode keyboard_input_mode; enum sc_mouse_input_mode mouse_input_mode; + enum sc_camera_facing camera_facing; struct sc_port_range port_range; uint32_t tunnel_host; uint16_t tunnel_port; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index d51d573b..54e794e0 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -353,6 +353,7 @@ scrcpy(struct scrcpy_options *options) { .audio_codec = options->audio_codec, .video_source = options->video_source, .audio_source = options->audio_source, + .camera_facing = options->camera_facing, .crop = options->crop, .port_range = options->port_range, .tunnel_host = options->tunnel_host, diff --git a/app/src/server.c b/app/src/server.c index 81a371ae..7f8a4926 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -183,6 +183,20 @@ sc_server_get_codec_name(enum sc_codec codec) { } } +static const char * +sc_server_get_camera_facing_name(enum sc_camera_facing camera_facing) { + switch (camera_facing) { + case SC_CAMERA_FACING_FRONT: + return "front"; + case SC_CAMERA_FACING_BACK: + return "back"; + case SC_CAMERA_FACING_EXTERNAL: + return "external"; + default: + return NULL; + } +} + static sc_pid execute_server(struct sc_server *server, const struct sc_server_params *params) { @@ -285,6 +299,10 @@ execute_server(struct sc_server *server, if (params->camera_size) { ADD_PARAM("camera_size=%s", params->camera_size); } + if (params->camera_facing != SC_CAMERA_FACING_ANY) { + ADD_PARAM("camera_facing=%s", + sc_server_get_camera_facing_name(params->camera_facing)); + } if (params->show_touches) { ADD_PARAM("show_touches=true"); } diff --git a/app/src/server.h b/app/src/server.h index 92c5f22e..786aea5c 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -28,6 +28,7 @@ struct sc_server_params { enum sc_codec audio_codec; enum sc_video_source video_source; enum sc_audio_source audio_source; + enum sc_camera_facing camera_facing; const char *crop; const char *video_codec_options; const char *audio_codec_options; diff --git a/server/src/main/java/com/genymobile/scrcpy/CameraCapture.java b/server/src/main/java/com/genymobile/scrcpy/CameraCapture.java index 5aadbae7..949eb343 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CameraCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/CameraCapture.java @@ -6,6 +6,7 @@ import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraCaptureSession; +import android.hardware.camera2.CameraCharacteristics; import android.hardware.camera2.CameraDevice; import android.hardware.camera2.CameraManager; import android.hardware.camera2.CaptureFailure; @@ -28,6 +29,7 @@ import java.util.concurrent.atomic.AtomicBoolean; public class CameraCapture extends SurfaceCapture { private final String explicitCameraId; + private final CameraFacing cameraFacing; private final Size explicitSize; private HandlerThread cameraThread; @@ -37,8 +39,9 @@ public class CameraCapture extends SurfaceCapture { private final AtomicBoolean disconnected = new AtomicBoolean(); - public CameraCapture(String explicitCameraId, Size explicitSize) { + public CameraCapture(String explicitCameraId, CameraFacing cameraFacing, Size explicitSize) { this.explicitCameraId = explicitCameraId; + this.cameraFacing = cameraFacing; this.explicitSize = explicitSize; } @@ -50,7 +53,7 @@ public class CameraCapture extends SurfaceCapture { cameraExecutor = new HandlerExecutor(cameraHandler); try { - String cameraId = selectCamera(explicitCameraId); + String cameraId = selectCamera(explicitCameraId, cameraFacing); if (cameraId == null) { throw new IOException("No matching camera found"); } @@ -62,7 +65,7 @@ public class CameraCapture extends SurfaceCapture { } } - private static String selectCamera(String explicitCameraId) throws CameraAccessException { + private static String selectCamera(String explicitCameraId, CameraFacing cameraFacing) throws CameraAccessException { if (explicitCameraId != null) { return explicitCameraId; } @@ -70,8 +73,22 @@ public class CameraCapture extends SurfaceCapture { CameraManager cameraManager = ServiceManager.getCameraManager(); String[] cameraIds = cameraManager.getCameraIdList(); - // Use the first one - return cameraIds.length > 0 ? cameraIds[0] : null; + if (cameraFacing == null) { + // Use the first one + return cameraIds.length > 0 ? cameraIds[0] : null; + } + + for (String cameraId : cameraIds) { + CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraId); + + int facing = characteristics.get(CameraCharacteristics.LENS_FACING); + if (cameraFacing.value() == facing) { + return cameraId; + } + } + + // Not found + return null; } @Override diff --git a/server/src/main/java/com/genymobile/scrcpy/CameraFacing.java b/server/src/main/java/com/genymobile/scrcpy/CameraFacing.java new file mode 100644 index 00000000..b7e8daa5 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/CameraFacing.java @@ -0,0 +1,33 @@ +package com.genymobile.scrcpy; + +import android.annotation.SuppressLint; +import android.hardware.camera2.CameraCharacteristics; + +public enum CameraFacing { + FRONT("front", CameraCharacteristics.LENS_FACING_FRONT), + BACK("back", CameraCharacteristics.LENS_FACING_BACK), + @SuppressLint("InlinedApi") // introduced in API 23 + EXTERNAL("external", CameraCharacteristics.LENS_FACING_EXTERNAL); + + private final String name; + private final int value; + + CameraFacing(String name, int value) { + this.name = name; + this.value = value; + } + + int value() { + return value; + } + + static CameraFacing findByName(String name) { + for (CameraFacing facing : CameraFacing.values()) { + if (name.equals(facing.name)) { + return facing; + } + } + + return null; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 2366f9c8..11af3cca 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -26,6 +26,7 @@ public class Options { private int displayId; private String cameraId; private Size cameraSize; + private CameraFacing cameraFacing; private boolean showTouches; private boolean stayAwake; private List videoCodecOptions; @@ -126,6 +127,10 @@ public class Options { return cameraSize; } + public CameraFacing getCameraFacing() { + return cameraFacing; + } + public boolean getShowTouches() { return showTouches; } @@ -360,6 +365,15 @@ public class Options { options.cameraSize = parseSize(value); } break; + case "camera_facing": + if (!value.isEmpty()) { + CameraFacing facing = CameraFacing.findByName(value); + if (facing == null) { + throw new IllegalArgumentException("Camera facing " + value + " not supported"); + } + options.cameraFacing = facing; + } + break; case "send_device_meta": options.sendDeviceMeta = Boolean.parseBoolean(value); break; diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index e43b9f0a..1a93323a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -137,7 +137,7 @@ public final class Server { if (options.getVideoSource() == VideoSource.DISPLAY) { surfaceCapture = new ScreenCapture(device); } else { - surfaceCapture = new CameraCapture(options.getCameraId(), options.getCameraSize()); + surfaceCapture = new CameraCapture(options.getCameraId(), options.getCameraFacing(), options.getCameraSize()); } SurfaceEncoder surfaceEncoder = new SurfaceEncoder(surfaceCapture, videoStreamer, options.getVideoBitRate(), options.getMaxFps(), options.getVideoCodecOptions(), options.getVideoEncoder(), options.getDownsizeOnError()); From dd36d6135fb08f8d3a99f9e9eb4ec8e19af0a660 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 26 Oct 2023 23:54:34 +0200 Subject: [PATCH 1655/2244] Support camera size selection using -m/--camera-ar In addition to --camera-size to specify an explicit size, make it possible to select the camera size automatically, respecting the maximum size (already used for display mirroring) and an aspect ratio. For example, "scrcpy --video-source=camera" followed by: - (no additional arguments) : mirrors at the maximum size, any a-r - -m1920 : only consider valid sizes having both dimensions not above 1920 - --camera-ar=4:3 : only consider valid sizes having an aspect ratio of 4:3 (+/- 10%) - -m2048 --camera-ar=1.6 : only consider valid sizes having both dimensions not above 2048 and an aspect ratio of 1.6 (+/- 10%) PR #4213 Co-authored-by: Simon Chan <1330321+yume-chan@users.noreply.github.com> --- app/data/bash-completion/scrcpy | 2 + app/data/zsh-completion/_scrcpy | 1 + app/scrcpy.1 | 6 + app/src/cli.c | 27 ++++- app/src/options.c | 1 + app/src/options.h | 1 + app/src/scrcpy.c | 1 + app/src/server.c | 5 + app/src/server.h | 1 + .../genymobile/scrcpy/CameraAspectRatio.java | 37 ++++++ .../com/genymobile/scrcpy/CameraCapture.java | 112 +++++++++++++++++- .../java/com/genymobile/scrcpy/Options.java | 26 ++++ .../java/com/genymobile/scrcpy/Server.java | 3 +- 13 files changed, 215 insertions(+), 8 deletions(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/CameraAspectRatio.java diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index 339a819a..c8b6609e 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -10,6 +10,7 @@ _scrcpy() { --audio-source= --audio-output-buffer= -b --video-bit-rate= + --camera-ar= --camera-id= --camera-facing= --camera-size= @@ -153,6 +154,7 @@ _scrcpy() { |--audio-codec-options \ |--audio-encoder \ |--audio-output-buffer \ + |--camera-ar \ |--camera-id \ |--camera-size \ |--crop \ diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index c92f0ac1..823e6b9e 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -17,6 +17,7 @@ arguments=( '--audio-source=[Select the audio source]:source:(output mic)' '--audio-output-buffer=[Configure the size of the SDL audio output buffer (in milliseconds)]' {-b,--video-bit-rate=}'[Encode the video at the given bit-rate]' + '--camera-ar=[Select the camera size by its aspect ratio]' '--camera-id=[Specify the camera id to mirror]' '--camera-facing=[Select the device camera by its facing direction]:facing:(front back external)' '--camera-size=[Specify an explicit camera capture size]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index d2fb3ad5..c473adb5 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -75,6 +75,12 @@ Encode the video at the given bit rate, expressed in bits/s. Unit suffixes are s Default is 8M (8000000). +.TP +.BI "\-\-camera\-ar " ar +Select the camera size by its aspect ratio (+/- 10%). + +Possible values are "sensor" (use the camera sensor aspect ratio), ":" (e.g. "4:3") and "" (e.g. "1.6"). + .TP .BI "\-\-camera\-id " id Specify the device camera id to mirror. diff --git a/app/src/cli.c b/app/src/cli.c index 96b8c26d..69a918c3 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -87,6 +87,7 @@ enum { OPT_CAMERA_ID, OPT_CAMERA_SIZE, OPT_CAMERA_FACING, + OPT_CAMERA_AR, }; struct sc_option { @@ -203,6 +204,15 @@ static const struct sc_option options[] = { .longopt = "bit-rate", .argdesc = "value", }, + { + .longopt_id = OPT_CAMERA_AR, + .longopt = "camera-ar", + .argdesc = "ar", + .text = "Select the camera size by its aspect ratio (+/- 10%).\n" + "Possible values are \"sensor\" (use the camera sensor aspect " + "ratio), \":\" (e.g. \"4:3\") or \"\" (e.g. " + "\"1.6\")." + }, { .longopt_id = OPT_CAMERA_ID, .longopt = "camera-id", @@ -2130,6 +2140,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; } break; + case OPT_CAMERA_AR: + opts->camera_ar = optarg; + break; case OPT_CAMERA_ID: opts->camera_id = optarg; break; @@ -2245,9 +2258,16 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; } - if (!opts->camera_size) { - LOGE("Camera size must be specified by --camera-size"); - return false; + if (opts->camera_size) { + if (opts->max_size) { + LOGE("Could not specify both --camera-size and -m/--max-size"); + return false; + } + + if (opts->camera_ar) { + LOGE("Could not specify both --camera-size and --camera-ar"); + return false; + } } if (opts->control) { @@ -2255,6 +2275,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], opts->control = false; } } else if (opts->camera_id + || opts->camera_ar || opts->camera_facing != SC_CAMERA_FACING_ANY || opts->camera_size) { LOGE("Camera options are only available with --video-source=camera"); diff --git a/app/src/options.c b/app/src/options.c index 2adb4323..589a5a22 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -13,6 +13,7 @@ const struct scrcpy_options scrcpy_options_default = { .audio_encoder = NULL, .camera_id = NULL, .camera_size = NULL, + .camera_ar = NULL, .log_level = SC_LOG_LEVEL_INFO, .video_codec = SC_CODEC_H264, .audio_codec = SC_CODEC_OPUS, diff --git a/app/src/options.h b/app/src/options.h index 1e783bae..40f04670 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -132,6 +132,7 @@ struct scrcpy_options { const char *audio_encoder; const char *camera_id; const char *camera_size; + const char *camera_ar; enum sc_log_level log_level; enum sc_codec video_codec; enum sc_codec audio_codec; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 54e794e0..1f4c2a7c 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -375,6 +375,7 @@ scrcpy(struct scrcpy_options *options) { .audio_encoder = options->audio_encoder, .camera_id = options->camera_id, .camera_size = options->camera_size, + .camera_ar = options->camera_ar, .force_adb_forward = options->force_adb_forward, .power_off_on_close = options->power_off_on_close, .clipboard_autosync = options->clipboard_autosync, diff --git a/app/src/server.c b/app/src/server.c index 7f8a4926..0c40bccb 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -77,6 +77,7 @@ sc_server_params_destroy(struct sc_server_params *params) { free((char *) params->audio_encoder); free((char *) params->tcpip_dst); free((char *) params->camera_id); + free((char *) params->camera_ar); } static bool @@ -105,6 +106,7 @@ sc_server_params_copy(struct sc_server_params *dst, COPY(audio_encoder); COPY(tcpip_dst); COPY(camera_id); + COPY(camera_ar); #undef COPY return true; @@ -303,6 +305,9 @@ execute_server(struct sc_server *server, ADD_PARAM("camera_facing=%s", sc_server_get_camera_facing_name(params->camera_facing)); } + if (params->camera_ar) { + ADD_PARAM("camera_ar=%s", params->camera_ar); + } if (params->show_touches) { ADD_PARAM("show_touches=true"); } diff --git a/app/src/server.h b/app/src/server.h index 786aea5c..71d22fe8 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -36,6 +36,7 @@ struct sc_server_params { const char *audio_encoder; const char *camera_id; const char *camera_size; + const char *camera_ar; struct sc_port_range port_range; uint32_t tunnel_host; uint16_t tunnel_port; diff --git a/server/src/main/java/com/genymobile/scrcpy/CameraAspectRatio.java b/server/src/main/java/com/genymobile/scrcpy/CameraAspectRatio.java new file mode 100644 index 00000000..4fdf4c74 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/CameraAspectRatio.java @@ -0,0 +1,37 @@ +package com.genymobile.scrcpy; + +public final class CameraAspectRatio { + private static final float SENSOR = -1; + + private float ar; + + private CameraAspectRatio(float ar) { + this.ar = ar; + } + + public static CameraAspectRatio fromFloat(float ar) { + if (ar < 0) { + throw new IllegalArgumentException("Invalid aspect ratio: " + ar); + } + return new CameraAspectRatio(ar); + } + + public static CameraAspectRatio fromFraction(int w, int h) { + if (w <= 0 || h <= 0) { + throw new IllegalArgumentException("Invalid aspect ratio: " + w + ":" + h); + } + return new CameraAspectRatio((float) w / h); + } + + public static CameraAspectRatio sensorAspectRatio() { + return new CameraAspectRatio(SENSOR); + } + + public boolean isSensor() { + return ar == SENSOR; + } + + public float getAspectRatio() { + return ar; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/CameraCapture.java b/server/src/main/java/com/genymobile/scrcpy/CameraCapture.java index 949eb343..e4aba872 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CameraCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/CameraCapture.java @@ -4,6 +4,7 @@ import com.genymobile.scrcpy.wrappers.ServiceManager; import android.annotation.SuppressLint; import android.annotation.TargetApi; +import android.graphics.Rect; import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraCaptureSession; import android.hardware.camera2.CameraCharacteristics; @@ -13,6 +14,8 @@ import android.hardware.camera2.CaptureFailure; import android.hardware.camera2.CaptureRequest; import android.hardware.camera2.params.OutputConfiguration; import android.hardware.camera2.params.SessionConfiguration; +import android.hardware.camera2.params.StreamConfigurationMap; +import android.media.MediaCodec; import android.os.Build; import android.os.Handler; import android.os.HandlerThread; @@ -21,16 +24,23 @@ import android.view.Surface; import java.io.IOException; import java.util.Arrays; import java.util.List; +import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Stream; public class CameraCapture extends SurfaceCapture { private final String explicitCameraId; private final CameraFacing cameraFacing; private final Size explicitSize; + private int maxSize; + private final CameraAspectRatio aspectRatio; + + private String cameraId; + private Size size; private HandlerThread cameraThread; private Handler cameraHandler; @@ -39,10 +49,12 @@ public class CameraCapture extends SurfaceCapture { private final AtomicBoolean disconnected = new AtomicBoolean(); - public CameraCapture(String explicitCameraId, CameraFacing cameraFacing, Size explicitSize) { + public CameraCapture(String explicitCameraId, CameraFacing cameraFacing, Size explicitSize, int maxSize, CameraAspectRatio aspectRatio) { this.explicitCameraId = explicitCameraId; this.cameraFacing = cameraFacing; this.explicitSize = explicitSize; + this.maxSize = maxSize; + this.aspectRatio = aspectRatio; } @Override @@ -53,11 +65,16 @@ public class CameraCapture extends SurfaceCapture { cameraExecutor = new HandlerExecutor(cameraHandler); try { - String cameraId = selectCamera(explicitCameraId, cameraFacing); + cameraId = selectCamera(explicitCameraId, cameraFacing); if (cameraId == null) { throw new IOException("No matching camera found"); } + size = selectSize(cameraId, explicitSize, maxSize, aspectRatio); + if (size == null) { + throw new IOException("Could not select camera size"); + } + Ln.i("Using camera '" + cameraId + "'"); cameraDevice = openCamera(cameraId); } catch (CameraAccessException | InterruptedException e) { @@ -91,6 +108,82 @@ public class CameraCapture extends SurfaceCapture { return null; } + @TargetApi(Build.VERSION_CODES.N) + private static Size selectSize(String cameraId, Size explicitSize, int maxSize, CameraAspectRatio aspectRatio) throws CameraAccessException { + if (explicitSize != null) { + return explicitSize; + } + + CameraManager cameraManager = ServiceManager.getCameraManager(); + CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraId); + + StreamConfigurationMap configs = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); + android.util.Size[] sizes = configs.getOutputSizes(MediaCodec.class); + Stream stream = Arrays.stream(sizes); + if (maxSize > 0) { + stream = stream.filter(it -> it.getWidth() <= maxSize && it.getHeight() <= maxSize); + } + + Float targetAspectRatio = resolveAspectRatio(aspectRatio, characteristics); + if (targetAspectRatio != null) { + stream = stream.filter(it -> { + float ar = ((float) it.getWidth() / it.getHeight()); + float arRatio = ar / targetAspectRatio; + // Accept if the aspect ratio is the target aspect ratio + or - 10% + return arRatio >= 0.9f && arRatio <= 1.1f; + }); + } + + Optional selected = stream.max((s1, s2) -> { + // Greater width is better + int cmp = Integer.compare(s1.getWidth(), s2.getWidth()); + if (cmp != 0) { + return cmp; + } + + if (targetAspectRatio != null) { + // Closer to the target aspect ratio is better + float ar1 = ((float) s1.getWidth() / s1.getHeight()); + float arRatio1 = ar1 / targetAspectRatio; + float distance1 = Math.abs(1 - arRatio1); + + float ar2 = ((float) s2.getWidth() / s2.getHeight()); + float arRatio2 = ar2 / targetAspectRatio; + float distance2 = Math.abs(1 - arRatio2); + + // Reverse the order because lower distance is better + cmp = Float.compare(distance2, distance1); + if (cmp != 0) { + return cmp; + } + } + + // Greater height is better + return Integer.compare(s1.getHeight(), s2.getHeight()); + }); + + if (selected.isPresent()) { + android.util.Size size = selected.get(); + return new Size(size.getWidth(), size.getHeight()); + } + + // Not found + return null; + } + + private static Float resolveAspectRatio(CameraAspectRatio ratio, CameraCharacteristics characteristics) { + if (ratio == null) { + return null; + } + + if (ratio.isSensor()) { + Rect activeSize = characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); + return (float) activeSize.width() / activeSize.height(); + } + + return ratio.getAspectRatio(); + } + @Override public void start(Surface surface) throws IOException { try { @@ -114,12 +207,23 @@ public class CameraCapture extends SurfaceCapture { @Override public Size getSize() { - return explicitSize; + return size; } @Override public boolean setMaxSize(int maxSize) { - return false; + if (explicitSize != null) { + return false; + } + + this.maxSize = maxSize; + try { + size = selectSize(cameraId, null, maxSize, aspectRatio); + return size != null; + } catch (CameraAccessException e) { + Ln.w("Could not select camera size", e); + return false; + } } @SuppressLint("MissingPermission") diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 11af3cca..eec19e52 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -27,6 +27,7 @@ public class Options { private String cameraId; private Size cameraSize; private CameraFacing cameraFacing; + private CameraAspectRatio cameraAspectRatio; private boolean showTouches; private boolean stayAwake; private List videoCodecOptions; @@ -131,6 +132,10 @@ public class Options { return cameraFacing; } + public CameraAspectRatio getCameraAspectRatio() { + return cameraAspectRatio; + } + public boolean getShowTouches() { return showTouches; } @@ -374,6 +379,11 @@ public class Options { options.cameraFacing = facing; } break; + case "camera_ar": + if (!value.isEmpty()) { + options.cameraAspectRatio = parseCameraAspectRatio(value); + } + break; case "send_device_meta": options.sendDeviceMeta = Boolean.parseBoolean(value); break; @@ -427,4 +437,20 @@ public class Options { int height = Integer.parseInt(tokens[1]); return new Size(width, height); } + + private static CameraAspectRatio parseCameraAspectRatio(String ar) { + if ("sensor".equals(ar)) { + return CameraAspectRatio.sensorAspectRatio(); + } + + String[] tokens = ar.split(":"); + if (tokens.length == 2) { + int w = Integer.parseInt(tokens[0]); + int h = Integer.parseInt(tokens[1]); + return CameraAspectRatio.fromFraction(w, h); + } + + float floatAr = Float.parseFloat(tokens[0]); + return CameraAspectRatio.fromFloat(floatAr); + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 1a93323a..4505a523 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -137,7 +137,8 @@ public final class Server { if (options.getVideoSource() == VideoSource.DISPLAY) { surfaceCapture = new ScreenCapture(device); } else { - surfaceCapture = new CameraCapture(options.getCameraId(), options.getCameraFacing(), options.getCameraSize()); + surfaceCapture = new CameraCapture(options.getCameraId(), options.getCameraFacing(), options.getCameraSize(), + options.getMaxSize(), options.getCameraAspectRatio()); } SurfaceEncoder surfaceEncoder = new SurfaceEncoder(surfaceCapture, videoStreamer, options.getVideoBitRate(), options.getMaxFps(), options.getVideoCodecOptions(), options.getVideoEncoder(), options.getDownsizeOnError()); From 9fc583548535ceb2d517ca522189d9814e9d2c13 Mon Sep 17 00:00:00 2001 From: Simon Chan <1330321+yume-chan@users.noreply.github.com> Date: Mon, 14 Aug 2023 00:28:25 +0800 Subject: [PATCH 1656/2244] Fail-fast camera mirroring on Android 11 and older PR #4213 Co-authored-by: Romain Vimont Signed-off-by: Romain Vimont --- server/src/main/java/com/genymobile/scrcpy/Server.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 4505a523..9789f7f2 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -88,6 +88,12 @@ public final class Server { private static void scrcpy(Options options) throws IOException, ConfigurationException { Ln.i("Device: [" + Build.MANUFACTURER + "] " + Build.BRAND + " " + Build.MODEL + " (Android " + Build.VERSION.RELEASE + ")"); + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S && options.getVideoSource() == VideoSource.CAMERA) { + Ln.e("Camera mirroring is not supported before Android 12"); + throw new ConfigurationException("Camera mirroring is not supported"); + } + final Device device = new Device(options); Thread initThread = startInitThread(options); From 928f8b8eb39597141c12d248e74cb6cac672ff07 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 28 Oct 2023 15:19:04 +0200 Subject: [PATCH 1657/2244] Do not arbitrary limit --max-fps to 1000 Limit to the variable type size, for consistency. PR #4213 --- 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 69a918c3..df73edac 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -1315,7 +1315,7 @@ parse_max_size(const char *s, uint16_t *max_size) { static bool parse_max_fps(const char *s, uint16_t *max_fps) { long value; - bool ok = parse_integer_arg(s, &value, false, 0, 1000, "max fps"); + bool ok = parse_integer_arg(s, &value, false, 0, 0xFFFF, "max fps"); if (!ok) { return false; } From 4722bff42316c0ecef1d3ee69d1a327c3405d5b5 Mon Sep 17 00:00:00 2001 From: Andrew Gunnerson Date: Fri, 27 Oct 2023 20:20:19 -0400 Subject: [PATCH 1658/2244] Add --camera-fps Add a new option for specifying the camera frame rate. By default, Android's default frame rate (30 fps) is used. PR #4213 Signed-off-by: Andrew Gunnerson Signed-off-by: Romain Vimont --- app/data/bash-completion/scrcpy | 2 ++ app/data/zsh-completion/_scrcpy | 1 + app/scrcpy.1 | 6 +++++ app/src/cli.c | 27 +++++++++++++++++++ app/src/options.c | 1 + app/src/options.h | 1 + app/src/scrcpy.c | 1 + app/src/server.c | 3 +++ app/src/server.h | 1 + .../com/genymobile/scrcpy/CameraCapture.java | 10 ++++++- .../java/com/genymobile/scrcpy/LogUtils.java | 12 ++++++++- .../java/com/genymobile/scrcpy/Options.java | 8 ++++++ .../java/com/genymobile/scrcpy/Server.java | 2 +- 13 files changed, 72 insertions(+), 3 deletions(-) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index c8b6609e..9743e44a 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -13,6 +13,7 @@ _scrcpy() { --camera-ar= --camera-id= --camera-facing= + --camera-fps= --camera-size= --crop= -d --select-usb @@ -156,6 +157,7 @@ _scrcpy() { |--audio-output-buffer \ |--camera-ar \ |--camera-id \ + |--camera-fps \ |--camera-size \ |--crop \ |--display-id \ diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 823e6b9e..1ad96ad5 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -20,6 +20,7 @@ arguments=( '--camera-ar=[Select the camera size by its aspect ratio]' '--camera-id=[Specify the camera id to mirror]' '--camera-facing=[Select the device camera by its facing direction]:facing:(front back external)' + '--camera-fps=[Specify the camera capture frame rate]' '--camera-size=[Specify an explicit camera capture size]' '--crop=[\[width\:height\:x\:y\] Crop the device screen on the server]' {-d,--select-usb}'[Use USB device]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index c473adb5..e3b1b6f0 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -93,6 +93,12 @@ Select the device camera by its facing direction. Possible values are "front", "back" and "external". +.TP +.BI "\-\-camera\-fps " fps +Specify the camera capture frame rate. + +If not specified, Android's default frame rate (30 fps) is used. + .TP .BI "\-\-camera\-size " width\fRx\fIheight Specify an explicit camera capture size. diff --git a/app/src/cli.c b/app/src/cli.c index df73edac..b82d332d 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -88,6 +88,7 @@ enum { OPT_CAMERA_SIZE, OPT_CAMERA_FACING, OPT_CAMERA_AR, + OPT_CAMERA_FPS, }; struct sc_option { @@ -234,6 +235,14 @@ static const struct sc_option options[] = { .argdesc = "x", .text = "Specify an explicit camera capture size.", }, + { + .longopt_id = OPT_CAMERA_FPS, + .longopt = "camera-fps", + .argdesc = "value", + .text = "Specify the camera capture frame rate.\n" + "If not specified, Android's default frame rate (30 fps) is " + "used.", + }, { // Not really deprecated (--codec has never been released), but without // declaring an explicit --codec option, getopt_long() partial matching @@ -1746,6 +1755,18 @@ parse_camera_facing(const char *optarg, enum sc_camera_facing *facing) { return false; } +static bool +parse_camera_fps(const char *s, uint16_t *camera_fps) { + long value; + bool ok = parse_integer_arg(s, &value, false, 0, 0xFFFF, "camera fps"); + if (!ok) { + return false; + } + + *camera_fps = (uint16_t) value; + return true; +} + static bool parse_time_limit(const char *s, sc_tick *tick) { long value; @@ -2154,6 +2175,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; } break; + case OPT_CAMERA_FPS: + if (!parse_camera_fps(optarg, &opts->camera_fps)) { + return false; + } + break; default: // getopt prints the error message on stderr return false; @@ -2277,6 +2303,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } else if (opts->camera_id || opts->camera_ar || opts->camera_facing != SC_CAMERA_FACING_ANY + || opts->camera_fps || opts->camera_size) { LOGE("Camera options are only available with --video-source=camera"); return false; diff --git a/app/src/options.c b/app/src/options.c index 589a5a22..8601678b 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -14,6 +14,7 @@ const struct scrcpy_options scrcpy_options_default = { .camera_id = NULL, .camera_size = NULL, .camera_ar = NULL, + .camera_fps = 0, .log_level = SC_LOG_LEVEL_INFO, .video_codec = SC_CODEC_H264, .audio_codec = SC_CODEC_OPUS, diff --git a/app/src/options.h b/app/src/options.h index 40f04670..a712f443 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -133,6 +133,7 @@ struct scrcpy_options { const char *camera_id; const char *camera_size; const char *camera_ar; + uint16_t camera_fps; enum sc_log_level log_level; enum sc_codec video_codec; enum sc_codec audio_codec; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 1f4c2a7c..64067cf6 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -376,6 +376,7 @@ scrcpy(struct scrcpy_options *options) { .camera_id = options->camera_id, .camera_size = options->camera_size, .camera_ar = options->camera_ar, + .camera_fps = options->camera_fps, .force_adb_forward = options->force_adb_forward, .power_off_on_close = options->power_off_on_close, .clipboard_autosync = options->clipboard_autosync, diff --git a/app/src/server.c b/app/src/server.c index 0c40bccb..8a91952a 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -308,6 +308,9 @@ execute_server(struct sc_server *server, if (params->camera_ar) { ADD_PARAM("camera_ar=%s", params->camera_ar); } + if (params->camera_fps) { + ADD_PARAM("camera_fps=%" PRIu16, params->camera_fps); + } if (params->show_touches) { ADD_PARAM("show_touches=true"); } diff --git a/app/src/server.h b/app/src/server.h index 71d22fe8..ed1f307e 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -37,6 +37,7 @@ struct sc_server_params { const char *camera_id; const char *camera_size; const char *camera_ar; + uint16_t camera_fps; struct sc_port_range port_range; uint32_t tunnel_host; uint16_t tunnel_port; diff --git a/server/src/main/java/com/genymobile/scrcpy/CameraCapture.java b/server/src/main/java/com/genymobile/scrcpy/CameraCapture.java index e4aba872..9edc600c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CameraCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/CameraCapture.java @@ -19,6 +19,7 @@ import android.media.MediaCodec; import android.os.Build; import android.os.Handler; import android.os.HandlerThread; +import android.util.Range; import android.view.Surface; import java.io.IOException; @@ -38,6 +39,7 @@ public class CameraCapture extends SurfaceCapture { private final Size explicitSize; private int maxSize; private final CameraAspectRatio aspectRatio; + private final int fps; private String cameraId; private Size size; @@ -49,12 +51,13 @@ public class CameraCapture extends SurfaceCapture { private final AtomicBoolean disconnected = new AtomicBoolean(); - public CameraCapture(String explicitCameraId, CameraFacing cameraFacing, Size explicitSize, int maxSize, CameraAspectRatio aspectRatio) { + public CameraCapture(String explicitCameraId, CameraFacing cameraFacing, Size explicitSize, int maxSize, CameraAspectRatio aspectRatio, int fps) { this.explicitCameraId = explicitCameraId; this.cameraFacing = cameraFacing; this.explicitSize = explicitSize; this.maxSize = maxSize; this.aspectRatio = aspectRatio; + this.fps = fps; } @Override @@ -304,6 +307,11 @@ public class CameraCapture extends SurfaceCapture { private CaptureRequest createCaptureRequest(Surface surface) throws CameraAccessException { CaptureRequest.Builder requestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD); requestBuilder.addTarget(surface); + + if (fps > 0) { + requestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, new Range<>(fps, fps)); + } + return requestBuilder.build(); } diff --git a/server/src/main/java/com/genymobile/scrcpy/LogUtils.java b/server/src/main/java/com/genymobile/scrcpy/LogUtils.java index 7806cf51..329f2570 100644 --- a/server/src/main/java/com/genymobile/scrcpy/LogUtils.java +++ b/server/src/main/java/com/genymobile/scrcpy/LogUtils.java @@ -9,8 +9,10 @@ import android.hardware.camera2.CameraCharacteristics; import android.hardware.camera2.CameraManager; import android.hardware.camera2.params.StreamConfigurationMap; import android.media.MediaCodec; +import android.util.Range; import java.util.List; +import java.util.TreeSet; public final class LogUtils { @@ -97,7 +99,15 @@ public final class LogUtils { builder.append(" (").append(getCameraFacingName(facing)).append(", "); Rect activeSize = characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); - builder.append(activeSize.width()).append("x").append(activeSize.height()).append(')'); + builder.append(activeSize.width()).append("x").append(activeSize.height()).append(", "); + + // Capture frame rates for low-FPS mode are the same for every resolution + Range[] lowFpsRanges = characteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES); + TreeSet uniqueLowFps = new TreeSet<>(); + for (Range range : lowFpsRanges) { + uniqueLowFps.add(range.getUpper()); + } + builder.append("fps=").append(uniqueLowFps).append(')'); if (includeSizes) { StreamConfigurationMap configs = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index eec19e52..843fe9f1 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -28,6 +28,7 @@ public class Options { private Size cameraSize; private CameraFacing cameraFacing; private CameraAspectRatio cameraAspectRatio; + private int cameraFps; private boolean showTouches; private boolean stayAwake; private List videoCodecOptions; @@ -136,6 +137,10 @@ public class Options { return cameraAspectRatio; } + public int getCameraFps() { + return cameraFps; + } + public boolean getShowTouches() { return showTouches; } @@ -384,6 +389,9 @@ public class Options { options.cameraAspectRatio = parseCameraAspectRatio(value); } break; + case "camera_fps": + options.cameraFps = Integer.parseInt(value); + break; case "send_device_meta": options.sendDeviceMeta = Boolean.parseBoolean(value); break; diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 9789f7f2..ca72d584 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -144,7 +144,7 @@ public final class Server { surfaceCapture = new ScreenCapture(device); } else { surfaceCapture = new CameraCapture(options.getCameraId(), options.getCameraFacing(), options.getCameraSize(), - options.getMaxSize(), options.getCameraAspectRatio()); + options.getMaxSize(), options.getCameraAspectRatio(), options.getCameraFps()); } SurfaceEncoder surfaceEncoder = new SurfaceEncoder(surfaceCapture, videoStreamer, options.getVideoBitRate(), options.getMaxFps(), options.getVideoCodecOptions(), options.getVideoEncoder(), options.getDownsizeOnError()); From 6af4bd601f028d4e3d96eb90326a2330f03bc960 Mon Sep 17 00:00:00 2001 From: Andrew Gunnerson Date: Fri, 27 Oct 2023 20:20:19 -0400 Subject: [PATCH 1659/2244] Add support for high frame rate camera capture Add --camera-high-speed to enable high frame rate camera capture. If the option is enabled, then --camera-fps is mandatory. PR #4213 Co-authored-by: Romain Vimont Signed-off-by: Andrew Gunnerson Signed-off-by: Romain Vimont --- app/data/bash-completion/scrcpy | 1 + app/data/zsh-completion/_scrcpy | 1 + app/scrcpy.1 | 6 ++++ app/src/cli.c | 17 ++++++++++ app/src/options.c | 1 + app/src/options.h | 1 + app/src/scrcpy.c | 1 + app/src/server.c | 3 ++ app/src/server.h | 1 + .../com/genymobile/scrcpy/CameraCapture.java | 31 ++++++++++++++----- .../java/com/genymobile/scrcpy/LogUtils.java | 26 +++++++++++++--- .../java/com/genymobile/scrcpy/Options.java | 8 +++++ .../java/com/genymobile/scrcpy/Server.java | 2 +- 13 files changed, 86 insertions(+), 13 deletions(-) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index 9743e44a..eaed88b7 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -14,6 +14,7 @@ _scrcpy() { --camera-id= --camera-facing= --camera-fps= + --camera-high-speed --camera-size= --crop= -d --select-usb diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 1ad96ad5..4b1e5868 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -18,6 +18,7 @@ arguments=( '--audio-output-buffer=[Configure the size of the SDL audio output buffer (in milliseconds)]' {-b,--video-bit-rate=}'[Encode the video at the given bit-rate]' '--camera-ar=[Select the camera size by its aspect ratio]' + '--camera-high-speed=[Enable high-speed camera capture mode]' '--camera-id=[Specify the camera id to mirror]' '--camera-facing=[Select the device camera by its facing direction]:facing:(front back external)' '--camera-fps=[Specify the camera capture frame rate]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index e3b1b6f0..2e5cbc60 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -81,6 +81,12 @@ Select the camera size by its aspect ratio (+/- 10%). Possible values are "sensor" (use the camera sensor aspect ratio), ":" (e.g. "4:3") and "" (e.g. "1.6"). +.TP +.B \-\-camera\-high\-speed +Enable high-speed camera capture mode. + +This mode is restricted to specific resolutions and frame rates, listed by --list-camera-sizes. + .TP .BI "\-\-camera\-id " id Specify the device camera id to mirror. diff --git a/app/src/cli.c b/app/src/cli.c index b82d332d..56b5cfb2 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -89,6 +89,7 @@ enum { OPT_CAMERA_FACING, OPT_CAMERA_AR, OPT_CAMERA_FPS, + OPT_CAMERA_HIGH_SPEED, }; struct sc_option { @@ -229,6 +230,13 @@ static const struct sc_option options[] = { .text = "Select the device camera by its facing direction.\n" "Possible values are \"front\", \"back\" and \"external\".", }, + { + .longopt_id = OPT_CAMERA_HIGH_SPEED, + .longopt = "camera-high-speed", + .text = "Enable high-speed camera capture mode.\n" + "This mode is restricted to specific resolutions and frame " + "rates, listed by --list-camera-sizes.", + }, { .longopt_id = OPT_CAMERA_SIZE, .longopt = "camera-size", @@ -2180,6 +2188,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; } break; + case OPT_CAMERA_HIGH_SPEED: + opts->camera_high_speed = true; + break; default: // getopt prints the error message on stderr return false; @@ -2296,6 +2307,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } } + if (opts->camera_high_speed && !opts->camera_fps) { + LOGE("--camera-high-speed requires an explicit --camera-fps value"); + return false; + } + if (opts->control) { LOGI("Camera video source: control disabled"); opts->control = false; @@ -2304,6 +2320,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], || opts->camera_ar || opts->camera_facing != SC_CAMERA_FACING_ANY || opts->camera_fps + || opts->camera_high_speed || opts->camera_size) { LOGE("Camera options are only available with --video-source=camera"); return false; diff --git a/app/src/options.c b/app/src/options.c index 8601678b..6c72d767 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -86,5 +86,6 @@ const struct scrcpy_options scrcpy_options_default = { .audio = true, .require_audio = false, .kill_adb_on_close = false, + .camera_high_speed = false, .list = 0, }; diff --git a/app/src/options.h b/app/src/options.h index a712f443..18b437d8 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -199,6 +199,7 @@ struct scrcpy_options { bool audio; bool require_audio; bool kill_adb_on_close; + bool camera_high_speed; #define SC_OPTION_LIST_ENCODERS 0x1 #define SC_OPTION_LIST_DISPLAYS 0x2 #define SC_OPTION_LIST_CAMERAS 0x4 diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 64067cf6..1d0e90c1 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -386,6 +386,7 @@ scrcpy(struct scrcpy_options *options) { .cleanup = options->cleanup, .power_on = options->power_on, .kill_adb_on_close = options->kill_adb_on_close, + .camera_high_speed = options->camera_high_speed, .list = options->list, }; diff --git a/app/src/server.c b/app/src/server.c index 8a91952a..2b3439da 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -311,6 +311,9 @@ execute_server(struct sc_server *server, if (params->camera_fps) { ADD_PARAM("camera_fps=%" PRIu16, params->camera_fps); } + if (params->camera_high_speed) { + ADD_PARAM("camera_high_speed=true"); + } if (params->show_touches) { ADD_PARAM("show_touches=true"); } diff --git a/app/src/server.h b/app/src/server.h index ed1f307e..062af0a9 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -63,6 +63,7 @@ struct sc_server_params { bool cleanup; bool power_on; bool kill_adb_on_close; + bool camera_high_speed; uint8_t list; }; diff --git a/server/src/main/java/com/genymobile/scrcpy/CameraCapture.java b/server/src/main/java/com/genymobile/scrcpy/CameraCapture.java index 9edc600c..b9da3658 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CameraCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/CameraCapture.java @@ -8,6 +8,7 @@ import android.graphics.Rect; import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraCaptureSession; import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CameraConstrainedHighSpeedCaptureSession; import android.hardware.camera2.CameraDevice; import android.hardware.camera2.CameraManager; import android.hardware.camera2.CaptureFailure; @@ -40,6 +41,7 @@ public class CameraCapture extends SurfaceCapture { private int maxSize; private final CameraAspectRatio aspectRatio; private final int fps; + private final boolean highSpeed; private String cameraId; private Size size; @@ -51,13 +53,15 @@ public class CameraCapture extends SurfaceCapture { private final AtomicBoolean disconnected = new AtomicBoolean(); - public CameraCapture(String explicitCameraId, CameraFacing cameraFacing, Size explicitSize, int maxSize, CameraAspectRatio aspectRatio, int fps) { + public CameraCapture(String explicitCameraId, CameraFacing cameraFacing, Size explicitSize, int maxSize, CameraAspectRatio aspectRatio, int fps, + boolean highSpeed) { this.explicitCameraId = explicitCameraId; this.cameraFacing = cameraFacing; this.explicitSize = explicitSize; this.maxSize = maxSize; this.aspectRatio = aspectRatio; this.fps = fps; + this.highSpeed = highSpeed; } @Override @@ -73,7 +77,7 @@ public class CameraCapture extends SurfaceCapture { throw new IOException("No matching camera found"); } - size = selectSize(cameraId, explicitSize, maxSize, aspectRatio); + size = selectSize(cameraId, explicitSize, maxSize, aspectRatio, highSpeed); if (size == null) { throw new IOException("Could not select camera size"); } @@ -112,7 +116,8 @@ public class CameraCapture extends SurfaceCapture { } @TargetApi(Build.VERSION_CODES.N) - private static Size selectSize(String cameraId, Size explicitSize, int maxSize, CameraAspectRatio aspectRatio) throws CameraAccessException { + private static Size selectSize(String cameraId, Size explicitSize, int maxSize, CameraAspectRatio aspectRatio, boolean highSpeed) + throws CameraAccessException { if (explicitSize != null) { return explicitSize; } @@ -121,7 +126,7 @@ public class CameraCapture extends SurfaceCapture { CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraId); StreamConfigurationMap configs = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); - android.util.Size[] sizes = configs.getOutputSizes(MediaCodec.class); + android.util.Size[] sizes = highSpeed ? configs.getHighSpeedVideoSizes() : configs.getOutputSizes(MediaCodec.class); Stream stream = Arrays.stream(sizes); if (maxSize > 0) { stream = stream.filter(it -> it.getWidth() <= maxSize && it.getHeight() <= maxSize); @@ -221,7 +226,7 @@ public class CameraCapture extends SurfaceCapture { this.maxSize = maxSize; try { - size = selectSize(cameraId, null, maxSize, aspectRatio); + size = selectSize(cameraId, null, maxSize, aspectRatio, highSpeed); return size != null; } catch (CameraAccessException e) { Ln.w("Could not select camera size", e); @@ -282,7 +287,9 @@ public class CameraCapture extends SurfaceCapture { CompletableFuture future = new CompletableFuture<>(); OutputConfiguration outputConfig = new OutputConfiguration(surface); List outputs = Arrays.asList(outputConfig); - SessionConfiguration sessionConfig = new SessionConfiguration(SessionConfiguration.SESSION_REGULAR, outputs, cameraExecutor, + + int sessionType = highSpeed ? SessionConfiguration.SESSION_HIGH_SPEED : SessionConfiguration.SESSION_REGULAR; + SessionConfiguration sessionConfig = new SessionConfiguration(sessionType, outputs, cameraExecutor, new CameraCaptureSession.StateCallback() { @Override public void onConfigured(CameraCaptureSession session) { @@ -317,7 +324,7 @@ public class CameraCapture extends SurfaceCapture { @TargetApi(Build.VERSION_CODES.S) private void setRepeatingRequest(CameraCaptureSession session, CaptureRequest request) throws CameraAccessException, InterruptedException { - session.setRepeatingRequest(request, new CameraCaptureSession.CaptureCallback() { + CameraCaptureSession.CaptureCallback callback = new CameraCaptureSession.CaptureCallback() { @Override public void onCaptureStarted(CameraCaptureSession session, CaptureRequest request, long timestamp, long frameNumber) { // Called for each frame captured, do nothing @@ -327,7 +334,15 @@ public class CameraCapture extends SurfaceCapture { public void onCaptureFailed(CameraCaptureSession session, CaptureRequest request, CaptureFailure failure) { Ln.w("Camera capture failed: frame " + failure.getFrameNumber()); } - }, cameraHandler); + }; + + if (highSpeed) { + CameraConstrainedHighSpeedCaptureSession highSpeedSession = (CameraConstrainedHighSpeedCaptureSession) session; + List requests = highSpeedSession.createHighSpeedRequestList(request); + highSpeedSession.setRepeatingBurst(requests, callback, cameraHandler); + } else { + session.setRepeatingRequest(request, callback, cameraHandler); + } } @Override diff --git a/server/src/main/java/com/genymobile/scrcpy/LogUtils.java b/server/src/main/java/com/genymobile/scrcpy/LogUtils.java index 329f2570..8dc54629 100644 --- a/server/src/main/java/com/genymobile/scrcpy/LogUtils.java +++ b/server/src/main/java/com/genymobile/scrcpy/LogUtils.java @@ -12,6 +12,7 @@ import android.media.MediaCodec; import android.util.Range; import java.util.List; +import java.util.SortedSet; import java.util.TreeSet; public final class LogUtils { @@ -103,18 +104,27 @@ public final class LogUtils { // Capture frame rates for low-FPS mode are the same for every resolution Range[] lowFpsRanges = characteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES); - TreeSet uniqueLowFps = new TreeSet<>(); - for (Range range : lowFpsRanges) { - uniqueLowFps.add(range.getUpper()); - } + SortedSet uniqueLowFps = getUniqueSet(lowFpsRanges); builder.append("fps=").append(uniqueLowFps).append(')'); if (includeSizes) { StreamConfigurationMap configs = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); + android.util.Size[] sizes = configs.getOutputSizes(MediaCodec.class); for (android.util.Size size : sizes) { builder.append("\n - ").append(size.getWidth()).append('x').append(size.getHeight()); } + + android.util.Size[] highSpeedSizes = configs.getHighSpeedVideoSizes(); + if (highSpeedSizes.length > 0) { + builder.append("\n High speed capture (--camera-high-speed):"); + for (android.util.Size size : highSpeedSizes) { + Range[] highFpsRanges = configs.getHighSpeedVideoFpsRanges(); + SortedSet uniqueHighFps = getUniqueSet(highFpsRanges); + builder.append("\n - ").append(size.getWidth()).append("x").append(size.getHeight()); + builder.append(" (fps=").append(uniqueHighFps).append(')'); + } + } } } } @@ -123,4 +133,12 @@ public final class LogUtils { } return builder.toString(); } + + private static SortedSet getUniqueSet(Range[] ranges) { + SortedSet set = new TreeSet<>(); + for (Range range : ranges) { + set.add(range.getUpper()); + } + return set; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 843fe9f1..9b1d8d8d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -29,6 +29,7 @@ public class Options { private CameraFacing cameraFacing; private CameraAspectRatio cameraAspectRatio; private int cameraFps; + private boolean cameraHighSpeed; private boolean showTouches; private boolean stayAwake; private List videoCodecOptions; @@ -141,6 +142,10 @@ public class Options { return cameraFps; } + public boolean getCameraHighSpeed() { + return cameraHighSpeed; + } + public boolean getShowTouches() { return showTouches; } @@ -392,6 +397,9 @@ public class Options { case "camera_fps": options.cameraFps = Integer.parseInt(value); break; + case "camera_high_speed": + options.cameraHighSpeed = Boolean.parseBoolean(value); + break; case "send_device_meta": options.sendDeviceMeta = Boolean.parseBoolean(value); break; diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index ca72d584..a86b8130 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -144,7 +144,7 @@ public final class Server { surfaceCapture = new ScreenCapture(device); } else { surfaceCapture = new CameraCapture(options.getCameraId(), options.getCameraFacing(), options.getCameraSize(), - options.getMaxSize(), options.getCameraAspectRatio(), options.getCameraFps()); + options.getMaxSize(), options.getCameraAspectRatio(), options.getCameraFps(), options.getCameraHighSpeed()); } SurfaceEncoder surfaceEncoder = new SurfaceEncoder(surfaceCapture, videoStreamer, options.getVideoBitRate(), options.getMaxFps(), options.getVideoCodecOptions(), options.getVideoEncoder(), options.getDownsizeOnError()); From 9bfc749803d79a7de5bcc46b3db376a12ef66e31 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 30 Oct 2023 23:27:11 +0100 Subject: [PATCH 1660/2244] Add camera documentation PR #4213 --- README.md | 6 +- doc/camera.md | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++ doc/video.md | 9 +++ 3 files changed, 163 insertions(+), 2 deletions(-) create mode 100644 doc/camera.md diff --git a/README.md b/README.md index 9f1ba6b4..10437b79 100644 --- a/README.md +++ b/README.md @@ -25,12 +25,13 @@ It focuses on: [lowlatency]: https://github.com/Genymobile/scrcpy/pull/646 Its features include: - - [audio forwarding](doc/audio.md) (Android >= 11) + - [audio forwarding](doc/audio.md) (Android 11+) - [recording](doc/recording.md) - mirroring with [Android device screen off](doc/device.md#turn-screen-off) - [copy-paste](doc/control.md#copy-paste) in both directions - [configurable quality](doc/video.md) - - Android device screen [as a webcam (V4L2)](doc/v4l2.md) (Linux-only) + - [camera mirroring](doc/camera.md) (Android 12+) + - [mirroring as a webcam (V4L2)](doc/v4l2.md) (Linux-only) - [physical keyboard/mouse simulation (HID)](doc/hid-otg.md) - [OTG mode](doc/hid-otg.md#otg) - and more… @@ -77,6 +78,7 @@ documented in the following pages: - [Recording](doc/recording.md) - [Tunnels](doc/tunnels.md) - [HID/OTG](doc/hid-otg.md) + - [Camera](doc/camera.md) - [Video4Linux](doc/v4l2.md) - [Shortcuts](doc/shortcuts.md) diff --git a/doc/camera.md b/doc/camera.md new file mode 100644 index 00000000..d1008bda --- /dev/null +++ b/doc/camera.md @@ -0,0 +1,150 @@ +# Camera + +Camera mirroring is supported for devices with Android 12 or higher. + +To capture the camera instead of the device screen: + +``` +scrcpy --video-source=camera +``` + +By default, it automatically switches [audio source](audio.md#source) to +microphone (as if `--audio-source=mic` were also passed). + +```bash +scrcpy --video-source=display # default is --audio-source=output +scrcpy --video-source=camera # default is --audio-source=mic +scrcpy --video-source=display --audio-source=mic # force display AND microphone +scrcpy --video-source=camera --audio-source=output # force camera AND device audio output +``` + + +## List + +To list the cameras available (with their declared valid sizes and frame rates): + +``` +scrcpy --list-cameras +scrcpy --list-camera-sizes +``` + +_Note that the sizes and frame rates are declarative. They are not accurate on +all devices: some of them are declared but not supported, while some others are +not declared but supported._ + + +## Selection + +It is possible to pass an explicit camera id (as listed by `--list-cameras`): + +``` +scrcpy --video-source=camera --camera-id=0 +``` + +Alternatively, the camera may be selected automatically: + +```bash +scrcpy --video-source=camera # use the first camera +scrcpy --video-source=camera --camera-facing=front # use the first front camera +scrcpy --video-source=camera --camera-facing=back # use the first back camera +scrcpy --video-source=camera --camera-facing=external # use the first external camera +``` + +If `--camera-id` is specified, then `--camera-facing` is forbidden (the id +already determines the camera): + +```bash +scrcpy --video-source=camera --camera-id=0 --camera-facing=front # error +``` + + +### Size selection + +It is possible to pass an explicit camera size: + +``` +scrcpy --video-source=camera --camera-size=1920x1080 +``` + +The given size may be listed among the declared valid sizes +(`--list-camera-sizes`), but may also be anything else (some devices support +arbitrary sizes): + +``` +scrcpy --video-source=camera --camera-size=1840x444 +``` + +Alternatively, a declared valid size (among the ones listed by +`list-camera-sizes`) may be selected automatically. + +Two constraints are supported: + - `-m`/`--max-size` (already used for display mirroring), for example `-m1920`; + - `--camera-ar` to specify an aspect ratio (`:`, `` or + `sensor`). + +Some examples: + +```bash +scrcpy --video-source=camera # use the greatest width and the greatest associated height +scrcpy --video-source=camera -m1920 # use the greatest width not above 1920 and the greatest associated height +scrcpy --video-source=camera --camera-ar=4:3 # use the greatest size with an aspect ratio of 4:3 (+/- 10%) +scrcpy --video-source=camera --camera-ar=1.6 # use the greatest size with an aspect ratio of 1.6 (+/- 10%) +scrcpy --video-source=camera --camera-ar=sensor # use the greatest size with the aspect ratio of the camera sensor (+/- 10%) +scrcpy --video-source=camera -m1920 --camera-ar=16:9 # use the greatest width not above 1920 and the closest to 16:9 aspect ratio +``` + +If `--camera-size` is specified, then `-m`/`--max-size` and `--camera-ar` are +forbidden (the size is determined by the value given explicitly): + +```bash +scrcpy --video-source=camera --camera-size=1920x1080 -m3000 # error +``` + + +## Frame rate + +By default, camera is captured at Android's default frame rate (30 fps). + +To configure a different frame rate: + +``` +scrcpy --video-source=camera --camera-fps=60 +``` + + +## High speed capture + +The Android camera API also supports a [high speed capture mode][high speed]. + +This mode is restricted to specific resolutions and frame rates, listed by +`--list-camera-sizes`. + +``` +scrcpy --video-source=camera --camera-size=1920x1080 --camera-fps=240 +``` + +[high speed]: https://developer.android.com/reference/android/hardware/camera2/CameraConstrainedHighSpeedCaptureSession + + +## Brace expansion tip + +All camera options start with `--camera-`, so if your shell supports it, you can +benefit from [brace expansion] (for example, it is supported _bash_ and _zsh_): + +```bash +scrcpy --video-source=camera --camera-{facing=back,ar=16:9,high-speed,fps=120} +``` + +This will be expanded as: + +```bash +scrcpy --video-source=camera --camera-facing=back --camera-ar=16:9 --camera-high-speed --camera-fps=120 +``` + +[brace expansion]: https://www.gnu.org/software/bash/manual/html_node/Brace-Expansion.html + + +## Webcam + +Combined with the [V4L2](v4l2.md) feature on Linux, the Android device camera +may be used as a webcam on the computer. diff --git a/doc/video.md b/doc/video.md index 5ce749f9..512e0aba 100644 --- a/doc/video.md +++ b/doc/video.md @@ -1,5 +1,14 @@ # Video +## Source + +By default, scrcpy mirrors the device screen. + +It is possible to capture the device camera instead. + +See the dedicated [camera](camera.md) page. + + ## Size By default, scrcpy attempts to mirror at the Android device resolution. From 55808034066b51f8e3c96f3a7e054898fb1f590e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 31 Oct 2023 19:13:17 +0100 Subject: [PATCH 1661/2244] Always print device model and version Print the log before checking for --list-* options so that it is printed in all cases. --- server/src/main/java/com/genymobile/scrcpy/Server.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index a86b8130..a8e8e36a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -87,8 +87,6 @@ public final class Server { } private static void scrcpy(Options options) throws IOException, ConfigurationException { - Ln.i("Device: [" + Build.MANUFACTURER + "] " + Build.BRAND + " " + Build.MODEL + " (Android " + Build.VERSION.RELEASE + ")"); - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S && options.getVideoSource() == VideoSource.CAMERA) { Ln.e("Camera mirroring is not supported before Android 12"); throw new ConfigurationException("Camera mirroring is not supported"); @@ -208,6 +206,8 @@ public final class Server { Ln.initLogLevel(options.getLogLevel()); + Ln.i("Device: [" + Build.MANUFACTURER + "] " + Build.BRAND + " " + Build.MODEL + " (Android " + Build.VERSION.RELEASE + ")"); + if (options.getList()) { if (options.getCleanup()) { CleanUp.unlinkSelf(); From 8350a619261d0349a7aea291cb9fe7f6b20078c8 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 31 Oct 2023 19:18:57 +0100 Subject: [PATCH 1662/2244] Simplify URLs in manpage The .UR-formatted URLs are not always rendered correctly. Use simple brackets instead. --- app/scrcpy.1 | 32 +++++++++++--------------------- 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 2e5cbc60..9af41528 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -45,9 +45,9 @@ Set a list of comma-separated key:type=value options for the device audio encode 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 . +The list of possible codec options is available in the Android documentation: + + .TP .BI "\-\-audio\-encoder " name @@ -363,8 +363,7 @@ 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 \-\-require\-audio @@ -458,9 +457,9 @@ Set a list of comma-separated key:type=value options for the device video encode 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 . +The list of possible codec options is available in the Android documentation: + + .TP .BI "\-\-video\-encoder " name @@ -659,23 +658,14 @@ for the Debian Project (and may be used by others). .SH "REPORTING BUGS" -Report bugs to -.UR https://github.com/Genymobile/scrcpy/issues -.UE . +Report bugs to . .SH COPYRIGHT -Copyright \(co 2018 Genymobile -.UR https://www.genymobile.com -Genymobile -.UE +Copyright \(co 2018 Genymobile -Copyright \(co 2018\-2023 -.MT rom@rom1v.com -Romain Vimont -.ME +Copyright \(co 2018\-2023 Romain Vimont Licensed under the Apache License, Version 2.0. .SH WWW -.UR https://github.com/Genymobile/scrcpy -.UE + From c64d1502024e3adaaf10761bd8d50149a113dcc3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 31 Oct 2023 19:20:59 +0100 Subject: [PATCH 1663/2244] Improve manpage formatting --- app/scrcpy.1 | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 9af41528..2901d014 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -53,7 +53,7 @@ The list of possible codec options is available in the Android documentation: .BI "\-\-audio\-encoder " name Use a specific MediaCodec audio encoder (depending on the codec provided by \fB\-\-audio\-codec\fR). -The available encoders can be listed by \-\-list\-encoders. +The available encoders can be listed by \fB\-\-list\-encoders\fR. .TP .BI "\-\-audio\-source " source @@ -79,19 +79,19 @@ Default is 8M (8000000). .BI "\-\-camera\-ar " ar Select the camera size by its aspect ratio (+/- 10%). -Possible values are "sensor" (use the camera sensor aspect ratio), ":" (e.g. "4:3") and "" (e.g. "1.6"). +Possible values are "sensor" (use the camera sensor aspect ratio), "\fInum\fR:\fIden\fR" (e.g. "4:3") and "\fIvalue\fR" (e.g. "1.6"). .TP .B \-\-camera\-high\-speed Enable high-speed camera capture mode. -This mode is restricted to specific resolutions and frame rates, listed by --list-camera-sizes. +This mode is restricted to specific resolutions and frame rates, listed by \fB\-\-list\-camera\-sizes\fR. .TP .BI "\-\-camera\-id " id Specify the device camera id to mirror. -The available camera ids can be listed by \-\-list\-cameras. +The available camera ids can be listed by \fB\-\-list\-cameras\fR. .TP .BI "\-\-camera\-facing " facing @@ -131,7 +131,7 @@ Disable screensaver while scrcpy is running. .BI "\-\-display\-id " id Specify the device display id to mirror. -The available display ids can be listed by \-\-list\-displays. +The available display ids can be listed by \fB\-\-list\-displays\fR. Default is 0. @@ -241,7 +241,7 @@ Disable device control (mirror the device in read\-only). .TP .B \-N, \-\-no\-playback -Disable video and audio playback on the computer (equivalent to --no-video-playback --no-audio-playback). +Disable video and audio playback on the computer (equivalent to \fB\-\-no\-video\-playback \-\-no\-audio\-playback\fR). .TP .B \-\-no\-audio @@ -411,13 +411,13 @@ Set the maximum mirroring time, in seconds. .TP .BI "\-\-tunnel\-host " ip -Set the IP address of the adb tunnel to reach the scrcpy server. This option automatically enables --force-adb-forward. +Set the IP address of the adb tunnel to reach the scrcpy server. This option automatically enables \fB\-\-force\-adb\-forward\fR. Default is localhost. .TP .BI "\-\-tunnel\-port " port -Set the TCP port of the adb tunnel to reach the scrcpy server. This option automatically enables --force-adb-forward. +Set the TCP port of the adb tunnel to reach the scrcpy server. This option automatically enables \fB\-\-force\-adb\-forward\fR. Default is 0 (not forced): the local port used for establishing the tunnel will be used. @@ -465,7 +465,7 @@ The list of possible codec options is available in the Android documentation: .BI "\-\-video\-encoder " name Use a specific MediaCodec video encoder (depending on the codec provided by \fB\-\-video\-codec\fR). -The available encoders can be listed by \-\-list\-encoders. +The available encoders can be listed by \fB\-\-list\-encoders\fR. .TP .BI "\-\-video\-source " source @@ -635,7 +635,7 @@ Path to adb. .TP .B ANDROID_SERIAL -Device serial to use if no selector (-s, -d, -e or --tcpip=) is specified. +Device serial to use if no selector (\fB-s\fR, \fB-d\fR, \fB-e\fR or \fB\-\-tcpip=\fIaddr\fR) is specified. .TP .B SCRCPY_ICON_PATH From b8c5853aa6ac9cfbe3fb4e46bf10978b3fa212e3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 1 Nov 2023 10:36:28 +0100 Subject: [PATCH 1664/2244] Disable default stdout/stderr Some devices (mostly Xiaomi) print internal errors using e.printStackTrace(), flooding the console with irrelevant errors. Disable system streams used via System.out and System.err streams, to print only the logs from scrcpy. Refs #994 Refs #4213 --- .../main/java/com/genymobile/scrcpy/Ln.java | 45 ++++++++++++++++--- .../java/com/genymobile/scrcpy/Server.java | 3 +- 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Ln.java b/server/src/main/java/com/genymobile/scrcpy/Ln.java index 199c29be..cdd57b9f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Ln.java +++ b/server/src/main/java/com/genymobile/scrcpy/Ln.java @@ -2,6 +2,11 @@ package com.genymobile.scrcpy; import android.util.Log; +import java.io.FileDescriptor; +import java.io.FileOutputStream; +import java.io.OutputStream; +import java.io.PrintStream; + /** * Log both to Android logger (so that logs are visible in "adb logcat") and standard output/error (so that they are visible in the terminal * directly). @@ -11,6 +16,9 @@ public final class Ln { private static final String TAG = "scrcpy"; private static final String PREFIX = "[server] "; + private static final PrintStream CONSOLE_OUT = new PrintStream(new FileOutputStream(FileDescriptor.out)); + private static final PrintStream CONSOLE_ERR = new PrintStream(new FileOutputStream(FileDescriptor.err)); + enum Level { VERBOSE, DEBUG, INFO, WARN, ERROR } @@ -21,6 +29,12 @@ public final class Ln { // not instantiable } + public static void disableSystemStreams() { + PrintStream nullStream = new PrintStream(new NullOutputStream()); + System.setOut(nullStream); + System.setErr(nullStream); + } + /** * Initialize the log level. *

@@ -39,30 +53,30 @@ public final class Ln { public static void v(String message) { if (isEnabled(Level.VERBOSE)) { Log.v(TAG, message); - System.out.print(PREFIX + "VERBOSE: " + message + '\n'); + CONSOLE_OUT.print(PREFIX + "VERBOSE: " + message + '\n'); } } public static void d(String message) { if (isEnabled(Level.DEBUG)) { Log.d(TAG, message); - System.out.print(PREFIX + "DEBUG: " + message + '\n'); + CONSOLE_OUT.print(PREFIX + "DEBUG: " + message + '\n'); } } public static void i(String message) { if (isEnabled(Level.INFO)) { Log.i(TAG, message); - System.out.print(PREFIX + "INFO: " + message + '\n'); + CONSOLE_OUT.print(PREFIX + "INFO: " + message + '\n'); } } public static void w(String message, Throwable throwable) { if (isEnabled(Level.WARN)) { Log.w(TAG, message, throwable); - System.err.print(PREFIX + "WARN: " + message + '\n'); + CONSOLE_ERR.print(PREFIX + "WARN: " + message + '\n'); if (throwable != null) { - throwable.printStackTrace(); + throwable.printStackTrace(CONSOLE_ERR); } } } @@ -74,9 +88,9 @@ public final class Ln { public static void e(String message, Throwable throwable) { if (isEnabled(Level.ERROR)) { Log.e(TAG, message, throwable); - System.err.print(PREFIX + "ERROR: " + message + "\n"); + CONSOLE_ERR.print(PREFIX + "ERROR: " + message + '\n'); if (throwable != null) { - throwable.printStackTrace(); + throwable.printStackTrace(CONSOLE_ERR); } } } @@ -84,4 +98,21 @@ public final class Ln { public static void e(String message) { e(message, null); } + + static class NullOutputStream extends OutputStream { + @Override + public void write(byte[] b) { + // ignore + } + + @Override + public void write(byte[] b, int off, int len) { + // ignore + } + + @Override + public void write(int b) { + // ignore + } + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index a8e8e36a..0126f396 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -187,7 +187,7 @@ public final class Server { try { internalMain(args); } catch (Throwable t) { - t.printStackTrace(); + Ln.e(t.getMessage(), t); status = 1; } finally { // By default, the Java process exits when all non-daemon threads are terminated. @@ -204,6 +204,7 @@ public final class Server { Options options = Options.parse(args); + Ln.disableSystemStreams(); Ln.initLogLevel(options.getLogLevel()); Ln.i("Device: [" + Build.MANUFACTURER + "] " + Build.BRAND + " " + Build.MODEL + " (Android " + Build.VERSION.RELEASE + ")"); From ff579990c21d73653aa74a3d8f2ad75b90504657 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 1 Nov 2023 14:52:38 +0100 Subject: [PATCH 1665/2244] Shutdown connection before joining threads Interrupting async processors may require to shutdown the connection to wake up blocking calls. Therefore, shutdown the connection first, then join the threads, then close the connection. Refs commit 9c08eb79cb7941848882cb908cefee9933450de5 --- .../com/genymobile/scrcpy/DesktopConnection.java | 15 ++++++++++++--- .../main/java/com/genymobile/scrcpy/Server.java | 2 ++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java b/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java index c3408fff..8bc743f8 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java +++ b/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java @@ -132,20 +132,29 @@ public final class DesktopConnection implements Closeable { return controlSocket; } - public void close() throws IOException { + public void shutdown() throws IOException { if (videoSocket != null) { videoSocket.shutdownInput(); videoSocket.shutdownOutput(); - videoSocket.close(); } if (audioSocket != null) { audioSocket.shutdownInput(); audioSocket.shutdownOutput(); - audioSocket.close(); } if (controlSocket != null) { controlSocket.shutdownInput(); controlSocket.shutdownOutput(); + } + } + + public void close() throws IOException { + if (videoSocket != null) { + videoSocket.close(); + } + if (audioSocket != null) { + audioSocket.close(); + } + if (controlSocket != null) { controlSocket.close(); } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 0126f396..a1c6090b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -163,6 +163,8 @@ public final class Server { asyncProcessor.stop(); } + connection.shutdown(); + try { initThread.join(); for (AsyncProcessor asyncProcessor : asyncProcessors) { From a8db3ec9e2ed3540bf33c2d0ad173a37613b5d8c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 29 Oct 2023 00:03:28 +0200 Subject: [PATCH 1666/2244] Upgrade platform-tools (34.0.5) for Windows Include the latest version of adb in Windows releases. --- app/prebuilt-deps/prepare-adb.sh | 6 +++--- release.mk | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/prebuilt-deps/prepare-adb.sh b/app/prebuilt-deps/prepare-adb.sh index f22873c0..4fb6fd7d 100755 --- a/app/prebuilt-deps/prepare-adb.sh +++ b/app/prebuilt-deps/prepare-adb.sh @@ -6,10 +6,10 @@ cd "$DIR" mkdir -p "$PREBUILT_DATA_DIR" cd "$PREBUILT_DATA_DIR" -DEP_DIR=platform-tools-34.0.3 +DEP_DIR=platform-tools-34.0.5 -FILENAME=platform-tools_r34.0.3-windows.zip -SHA256SUM=fce992e93eb786fc9f47df93d83a7b912c46742d45c39d712c02e06d05b72e2b +FILENAME=platform-tools_r34.0.5-windows.zip +SHA256SUM=3f8320152704377de150418a3c4c9d07d16d80a6c0d0d8f7289c22c499e33571 if [[ -d "$DEP_DIR" ]] then diff --git a/release.mk b/release.mk index 4fe99c89..8da4e528 100644 --- a/release.mk +++ b/release.mk @@ -98,9 +98,9 @@ dist-win32: build-server build-win32 cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-4/win32/bin/avcodec-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-4/win32/bin/avformat-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-4/win32/bin/swresample-4.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/platform-tools-34.0.3/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/platform-tools-34.0.3/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/platform-tools-34.0.3/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/platform-tools-34.0.5/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/platform-tools-34.0.5/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/platform-tools-34.0.5/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/SDL2-2.28.0/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/libusb-1.0.26/libusb-MinGW-Win32/bin/msys-usb-1.0.dll "$(DIST)/$(WIN32_TARGET_DIR)/" @@ -116,9 +116,9 @@ dist-win64: build-server build-win64 cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-4/win64/bin/avcodec-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-4/win64/bin/avformat-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-4/win64/bin/swresample-4.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/platform-tools-34.0.3/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/platform-tools-34.0.3/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/platform-tools-34.0.3/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/platform-tools-34.0.5/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/platform-tools-34.0.5/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/platform-tools-34.0.5/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/SDL2-2.28.0/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/libusb-1.0.26/libusb-MinGW-x64/bin/msys-usb-1.0.dll "$(DIST)/$(WIN64_TARGET_DIR)/" From 855ae4adb1a55c23aca2ce9cb1a17cd6bf1ef03d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 29 Oct 2023 00:05:59 +0200 Subject: [PATCH 1667/2244] Upgrade SDL (2.28.4) for Windows Include the latest version of SDL in Windows releases. --- app/prebuilt-deps/prepare-sdl.sh | 6 +++--- cross_win32.txt | 2 +- cross_win64.txt | 2 +- release.mk | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/prebuilt-deps/prepare-sdl.sh b/app/prebuilt-deps/prepare-sdl.sh index b691aac5..645646de 100755 --- a/app/prebuilt-deps/prepare-sdl.sh +++ b/app/prebuilt-deps/prepare-sdl.sh @@ -6,10 +6,10 @@ cd "$DIR" mkdir -p "$PREBUILT_DATA_DIR" cd "$PREBUILT_DATA_DIR" -DEP_DIR=SDL2-2.28.0 +DEP_DIR=SDL2-2.28.4 -FILENAME=SDL2-devel-2.28.0-mingw.tar.gz -SHA256SUM=b91ce59eeacd4a9db403f976fd2337d9360b21ada374124417d716065c380e20 +FILENAME=SDL2-devel-2.28.4-mingw.tar.gz +SHA256SUM=779d091072cf97291f80030f5232d97aa3d48ab0f2c14fe0b9d9a33c593cdc35 if [[ -d "$DEP_DIR" ]] then diff --git a/cross_win32.txt b/cross_win32.txt index c3f72540..109bdd27 100644 --- a/cross_win32.txt +++ b/cross_win32.txt @@ -17,5 +17,5 @@ endian = 'little' [properties] prebuilt_ffmpeg = 'ffmpeg-6.0-scrcpy-4/win32' -prebuilt_sdl2 = 'SDL2-2.28.0/i686-w64-mingw32' +prebuilt_sdl2 = 'SDL2-2.28.4/i686-w64-mingw32' prebuilt_libusb = 'libusb-1.0.26/libusb-MinGW-Win32' diff --git a/cross_win64.txt b/cross_win64.txt index 66cddf83..70e105ab 100644 --- a/cross_win64.txt +++ b/cross_win64.txt @@ -17,5 +17,5 @@ endian = 'little' [properties] prebuilt_ffmpeg = 'ffmpeg-6.0-scrcpy-4/win64' -prebuilt_sdl2 = 'SDL2-2.28.0/x86_64-w64-mingw32' +prebuilt_sdl2 = 'SDL2-2.28.4/x86_64-w64-mingw32' prebuilt_libusb = 'libusb-1.0.26/libusb-MinGW-x64' diff --git a/release.mk b/release.mk index 8da4e528..258017bc 100644 --- a/release.mk +++ b/release.mk @@ -101,7 +101,7 @@ dist-win32: build-server build-win32 cp app/prebuilt-deps/data/platform-tools-34.0.5/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-34.0.5/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-34.0.5/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/SDL2-2.28.0/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/SDL2-2.28.4/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/libusb-1.0.26/libusb-MinGW-Win32/bin/msys-usb-1.0.dll "$(DIST)/$(WIN32_TARGET_DIR)/" dist-win64: build-server build-win64 @@ -119,7 +119,7 @@ dist-win64: build-server build-win64 cp app/prebuilt-deps/data/platform-tools-34.0.5/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-34.0.5/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-34.0.5/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/SDL2-2.28.0/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/SDL2-2.28.4/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/libusb-1.0.26/libusb-MinGW-x64/bin/msys-usb-1.0.dll "$(DIST)/$(WIN64_TARGET_DIR)/" zip-win32: dist-win32 From c3c7bf7af329c8a5fd9f0310eae3b4e0a5f86207 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 1 Nov 2023 18:36:33 +0100 Subject: [PATCH 1668/2244] Bump version to v2.2 --- app/scrcpy-windows.rc | 2 +- meson.build | 2 +- server/build.gradle | 4 ++-- server/build_without_gradle.sh | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/scrcpy-windows.rc b/app/scrcpy-windows.rc index e3929119..832817d8 100644 --- a/app/scrcpy-windows.rc +++ b/app/scrcpy-windows.rc @@ -13,7 +13,7 @@ BEGIN VALUE "LegalCopyright", "Romain Vimont, Genymobile" VALUE "OriginalFilename", "scrcpy.exe" VALUE "ProductName", "scrcpy" - VALUE "ProductVersion", "2.1.1" + VALUE "ProductVersion", "v2.2" END END BLOCK "VarFileInfo" diff --git a/meson.build b/meson.build index ed3f42bf..d1f67e38 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('scrcpy', 'c', - version: '2.1.1', + version: 'v2.2', meson_version: '>= 0.48', default_options: [ 'c_std=c11', diff --git a/server/build.gradle b/server/build.gradle index 4a05d2a5..bee6509b 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -7,8 +7,8 @@ android { applicationId "com.genymobile.scrcpy" minSdkVersion 21 targetSdkVersion 33 - versionCode 20101 - versionName "2.1.1" + versionCode 200 + versionName "v2.2" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index 543f12ab..6e755272 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -12,7 +12,7 @@ set -e SCRCPY_DEBUG=false -SCRCPY_VERSION_NAME=2.1.1 +SCRCPY_VERSION_NAME=v2.2 PLATFORM=${ANDROID_PLATFORM:-33} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-33.0.0} From 446ea818a44eb75f03f21d08d307801b20ca2871 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 1 Nov 2023 18:47:58 +0100 Subject: [PATCH 1669/2244] Update links to v2.2 --- README.md | 2 +- doc/build.md | 6 +++--- doc/windows.md | 12 ++++++------ install_release.sh | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 10437b79..b8ef9df8 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# scrcpy (v2.1.1) +# scrcpy (v2.2) scrcpy diff --git a/doc/build.md b/doc/build.md index d65cdc93..54b7410b 100644 --- a/doc/build.md +++ b/doc/build.md @@ -233,10 +233,10 @@ install` must be run as root)._ #### Option 2: Use prebuilt server - - [`scrcpy-server-v2.1.1`][direct-scrcpy-server] - SHA-256: `9558db6c56743a1dc03b38f59801fb40e91cc891f8fc0c89e5b0b067761f148e` + - [`scrcpy-server-v2.2`][direct-scrcpy-server] + SHA-256: `c85c4aa84305efb69115cd497a120ebdd10258993b4cf123a8245b3d99d49874` -[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v2.1.1/scrcpy-server-v2.1.1 +[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v2.2/scrcpy-server-v2.2 Download the prebuilt server somewhere, and specify its path during the Meson configuration: diff --git a/doc/windows.md b/doc/windows.md index 7525334d..bd4a69f7 100644 --- a/doc/windows.md +++ b/doc/windows.md @@ -4,14 +4,14 @@ Download the [latest release]: - - [`scrcpy-win64-v2.1.1.zip`][direct-win64] (64-bit) - SHA-256: `f77281e1bce2f9934617699c581f063d5b327f012eff602ee98fb2ef550c25c2` - - [`scrcpy-win32-v2.1.1.zip`][direct-win32] (32-bit) - SHA-256: `ef7ae7fbe9449f2643febdc2244fb186d1a746a3c736394150cfd14f06d3c943` + - [`scrcpy-win64-v2.2.zip`][direct-win64] (64-bit) + SHA-256: `9f9da88ac4c8319dcb9bf852f2d9bba942bac663413383419cddf64eaa5685bd` + - [`scrcpy-win32-v2.2.zip`][direct-win32] (32-bit) + SHA-256: `cb84269fc847b8b880e320879492a1ae6c017b42175f03e199530f7a53be9d74` [latest release]: https://github.com/Genymobile/scrcpy/releases/latest -[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v2.1.1/scrcpy-win64-v2.1.1.zip -[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v2.1.1/scrcpy-win32-v2.1.1.zip +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v2.2/scrcpy-win64-v2.2.zip +[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v2.2/scrcpy-win32-v2.2.zip and extract it. diff --git a/install_release.sh b/install_release.sh index 24a3197b..adad85f7 100755 --- a/install_release.sh +++ b/install_release.sh @@ -2,8 +2,8 @@ set -e BUILDDIR=build-auto -PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v2.1.1/scrcpy-server-v2.1.1 -PREBUILT_SERVER_SHA256=9558db6c56743a1dc03b38f59801fb40e91cc891f8fc0c89e5b0b067761f148e +PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v2.2/scrcpy-server-v2.2 +PREBUILT_SERVER_SHA256=c85c4aa84305efb69115cd497a120ebdd10258993b4cf123a8245b3d99d49874 echo "[scrcpy] Downloading prebuilt server..." wget "$PREBUILT_SERVER_URL" -O scrcpy-server From 8c3e2bae7be47a0f17ddec8da25b89e0aea2617e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 3 Nov 2023 19:02:58 +0100 Subject: [PATCH 1670/2244] Simplify Application instantiation The constructor is public. --- 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 b8ee68ca..e8da9540 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java +++ b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java @@ -143,7 +143,7 @@ public final class Workarounds { try { fillActivityThread(); - Application app = Application.class.newInstance(); + Application app = new Application(); Field baseField = ContextWrapper.class.getDeclaredField("mBase"); baseField.setAccessible(true); baseField.set(app, FakeContext.get()); From 85a0b935c9d70a7f082eb3df5f3c9f61ea48009a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 3 Nov 2023 19:01:08 +0100 Subject: [PATCH 1671/2244] Always assign a system context as base context FakeContext used ActivityThread.getSystemContext() as base context only in some cases, because it caused problems on some devices: - warnings on Xiaomi devices [1], which are now fixed by b8c5853aa6ac9cfbe3fb4e46bf10978b3fa212e3 - issues related to Looper [2], which are solved by just calling Looper.prepare*() Therefore, we can now always assign a base context, which simplifies and helps to solve camera issues on some devices (#4392). [1] [2] Fixes #4392 --- .../com/genymobile/scrcpy/FakeContext.java | 6 +- .../com/genymobile/scrcpy/Workarounds.java | 71 ++++++++----------- 2 files changed, 33 insertions(+), 44 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/FakeContext.java b/server/src/main/java/com/genymobile/scrcpy/FakeContext.java index 6501d4cf..520e0378 100644 --- a/server/src/main/java/com/genymobile/scrcpy/FakeContext.java +++ b/server/src/main/java/com/genymobile/scrcpy/FakeContext.java @@ -2,11 +2,11 @@ package com.genymobile.scrcpy; import android.annotation.TargetApi; import android.content.AttributionSource; -import android.content.MutableContextWrapper; +import android.content.ContextWrapper; import android.os.Build; import android.os.Process; -public final class FakeContext extends MutableContextWrapper { +public final class FakeContext extends ContextWrapper { public static final String PACKAGE_NAME = "com.android.shell"; public static final int ROOT_UID = 0; // Like android.os.Process.ROOT_UID, but before API 29 @@ -18,7 +18,7 @@ public final class FakeContext extends MutableContextWrapper { } private FakeContext() { - super(null); + super(Workarounds.getSystemContext()); } @Override diff --git a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java index e8da9540..5b3a5c8c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java +++ b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java @@ -21,18 +21,34 @@ import java.lang.reflect.Method; public final class Workarounds { - private static Class activityThreadClass; - private static Object activityThread; + private static final Class ACTIVITY_THREAD_CLASS; + private static final Object ACTIVITY_THREAD; + + static { + prepareMainLooper(); + + try { + // ActivityThread activityThread = new ActivityThread(); + ACTIVITY_THREAD_CLASS = Class.forName("android.app.ActivityThread"); + Constructor activityThreadConstructor = ACTIVITY_THREAD_CLASS.getDeclaredConstructor(); + activityThreadConstructor.setAccessible(true); + ACTIVITY_THREAD = activityThreadConstructor.newInstance(); + + // ActivityThread.sCurrentActivityThread = activityThread; + Field sCurrentActivityThreadField = ACTIVITY_THREAD_CLASS.getDeclaredField("sCurrentActivityThread"); + sCurrentActivityThreadField.setAccessible(true); + sCurrentActivityThreadField.set(null, ACTIVITY_THREAD); + } catch (Exception e) { + throw new AssertionError(e); + } + } private Workarounds() { // not instantiable } public static void apply(boolean audio, boolean camera) { - Workarounds.prepareMainLooper(); - boolean mustFillAppInfo = false; - boolean mustFillBaseContext = false; boolean mustFillAppContext = false; if (Build.BRAND.equalsIgnoreCase("meizu")) { @@ -53,7 +69,6 @@ public final class Workarounds { // - // - mustFillAppInfo = true; - mustFillBaseContext = true; mustFillAppContext = true; } @@ -66,15 +81,11 @@ public final class Workarounds { if (camera) { mustFillAppInfo = true; - mustFillBaseContext = true; } if (mustFillAppInfo) { Workarounds.fillAppInfo(); } - if (mustFillBaseContext) { - Workarounds.fillBaseContext(); - } if (mustFillAppContext) { Workarounds.fillAppContext(); } @@ -93,27 +104,9 @@ public final class Workarounds { Looper.prepareMainLooper(); } - @SuppressLint("PrivateApi,DiscouragedPrivateApi") - private static void fillActivityThread() throws Exception { - if (activityThread == null) { - // ActivityThread activityThread = new ActivityThread(); - activityThreadClass = Class.forName("android.app.ActivityThread"); - Constructor activityThreadConstructor = activityThreadClass.getDeclaredConstructor(); - activityThreadConstructor.setAccessible(true); - activityThread = activityThreadConstructor.newInstance(); - - // ActivityThread.sCurrentActivityThread = activityThread; - Field sCurrentActivityThreadField = activityThreadClass.getDeclaredField("sCurrentActivityThread"); - sCurrentActivityThreadField.setAccessible(true); - sCurrentActivityThreadField.set(null, activityThread); - } - } - @SuppressLint("PrivateApi,DiscouragedPrivateApi") private static void fillAppInfo() { try { - fillActivityThread(); - // ActivityThread.AppBindData appBindData = new ActivityThread.AppBindData(); Class appBindDataClass = Class.forName("android.app.ActivityThread$AppBindData"); Constructor appBindDataConstructor = appBindDataClass.getDeclaredConstructor(); @@ -129,9 +122,9 @@ public final class Workarounds { appInfoField.set(appBindData, applicationInfo); // activityThread.mBoundApplication = appBindData; - Field mBoundApplicationField = activityThreadClass.getDeclaredField("mBoundApplication"); + Field mBoundApplicationField = ACTIVITY_THREAD_CLASS.getDeclaredField("mBoundApplication"); mBoundApplicationField.setAccessible(true); - mBoundApplicationField.set(activityThread, appBindData); + mBoundApplicationField.set(ACTIVITY_THREAD, appBindData); } catch (Throwable throwable) { // this is a workaround, so failing is not an error Ln.d("Could not fill app info: " + throwable.getMessage()); @@ -141,33 +134,29 @@ public final class Workarounds { @SuppressLint("PrivateApi,DiscouragedPrivateApi") private static void fillAppContext() { try { - fillActivityThread(); - Application app = new Application(); Field baseField = ContextWrapper.class.getDeclaredField("mBase"); baseField.setAccessible(true); baseField.set(app, FakeContext.get()); // activityThread.mInitialApplication = app; - Field mInitialApplicationField = activityThreadClass.getDeclaredField("mInitialApplication"); + Field mInitialApplicationField = ACTIVITY_THREAD_CLASS.getDeclaredField("mInitialApplication"); mInitialApplicationField.setAccessible(true); - mInitialApplicationField.set(activityThread, app); + mInitialApplicationField.set(ACTIVITY_THREAD, app); } catch (Throwable throwable) { // this is a workaround, so failing is not an error Ln.d("Could not fill app context: " + throwable.getMessage()); } } - private static void fillBaseContext() { + static Context getSystemContext() { try { - fillActivityThread(); - - Method getSystemContextMethod = activityThreadClass.getDeclaredMethod("getSystemContext"); - Context context = (Context) getSystemContextMethod.invoke(activityThread); - FakeContext.get().setBaseContext(context); + Method getSystemContextMethod = ACTIVITY_THREAD_CLASS.getDeclaredMethod("getSystemContext"); + return (Context) getSystemContextMethod.invoke(ACTIVITY_THREAD); } catch (Throwable throwable) { // this is a workaround, so failing is not an error - Ln.d("Could not fill base context: " + throwable.getMessage()); + Ln.d("Could not get system context: " + throwable.getMessage()); + return null; } } From 8d76b3e06dcd4b996ef20779feaf4cd8494a4a5c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 3 Nov 2023 19:07:08 +0100 Subject: [PATCH 1672/2244] Fill application context for camera Using the camera fails on some devices without a proper application context. Fixes #4392 --- server/src/main/java/com/genymobile/scrcpy/Workarounds.java | 1 + 1 file changed, 1 insertion(+) diff --git a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java index 5b3a5c8c..77827c47 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java +++ b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java @@ -81,6 +81,7 @@ public final class Workarounds { if (camera) { mustFillAppInfo = true; + mustFillAppContext = true; } if (mustFillAppInfo) { From 4e4ddc499fcf571109126fbe3722eb0cc60fe1b0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 3 Nov 2023 19:07:15 +0100 Subject: [PATCH 1673/2244] Return the FakeContext as application context This avoids getApplicationContext() to return null and cause NullPointerException. Fixes #4392 --- server/src/main/java/com/genymobile/scrcpy/FakeContext.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/server/src/main/java/com/genymobile/scrcpy/FakeContext.java b/server/src/main/java/com/genymobile/scrcpy/FakeContext.java index 520e0378..2ea7bf4a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/FakeContext.java +++ b/server/src/main/java/com/genymobile/scrcpy/FakeContext.java @@ -2,6 +2,7 @@ package com.genymobile.scrcpy; import android.annotation.TargetApi; import android.content.AttributionSource; +import android.content.Context; import android.content.ContextWrapper; import android.os.Build; import android.os.Process; @@ -44,4 +45,9 @@ public final class FakeContext extends ContextWrapper { public int getDeviceId() { return 0; } + + @Override + public Context getApplicationContext() { + return this; + } } From ccaa832f48d0454986777d9521e1028ec0d3eb35 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 5 Nov 2023 11:51:32 +0100 Subject: [PATCH 1674/2244] Simplify --list-cameras output Remove --video-source=camera from the output of --list-cameras (this is implicit). --- server/src/main/java/com/genymobile/scrcpy/LogUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/LogUtils.java b/server/src/main/java/com/genymobile/scrcpy/LogUtils.java index 8dc54629..70140525 100644 --- a/server/src/main/java/com/genymobile/scrcpy/LogUtils.java +++ b/server/src/main/java/com/genymobile/scrcpy/LogUtils.java @@ -93,7 +93,7 @@ public final class LogUtils { builder.append("\n (none)"); } else { for (String id : cameraIds) { - builder.append("\n --video-source=camera --camera-id=").append(id); + builder.append("\n --camera-id=").append(id); CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(id); int facing = characteristics.get(CameraCharacteristics.LENS_FACING); From 11d738321f8661a46c5f211ec4285047657177cb Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 5 Nov 2023 15:12:54 +0100 Subject: [PATCH 1675/2244] Recover on invalid camera FPS ranges Some devices may provide invalid ranges, causing an IllegalArgumentException "lower must be less than or equal to upper". Catch the exception to list the cameras anyway. Refs #4403 --- .../java/com/genymobile/scrcpy/LogUtils.java | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/LogUtils.java b/server/src/main/java/com/genymobile/scrcpy/LogUtils.java index 70140525..efa0672b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/LogUtils.java +++ b/server/src/main/java/com/genymobile/scrcpy/LogUtils.java @@ -100,12 +100,19 @@ public final class LogUtils { builder.append(" (").append(getCameraFacingName(facing)).append(", "); Rect activeSize = characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); - builder.append(activeSize.width()).append("x").append(activeSize.height()).append(", "); + builder.append(activeSize.width()).append("x").append(activeSize.height()); - // Capture frame rates for low-FPS mode are the same for every resolution - Range[] lowFpsRanges = characteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES); - SortedSet uniqueLowFps = getUniqueSet(lowFpsRanges); - builder.append("fps=").append(uniqueLowFps).append(')'); + try { + // Capture frame rates for low-FPS mode are the same for every resolution + Range[] lowFpsRanges = characteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES); + SortedSet uniqueLowFps = getUniqueSet(lowFpsRanges); + builder.append(", fps=").append(uniqueLowFps); + } catch (Exception e) { + // Some devices may provide invalid ranges, causing an IllegalArgumentException "lower must be less than or equal to upper" + Ln.w("Could not get available frame rates for camera " + id, e); + } + + builder.append(')'); if (includeSizes) { StreamConfigurationMap configs = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); From 3c456253242ed96796f71b2ab3dc66e2bedf6ea3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 11 Nov 2023 10:54:13 +0100 Subject: [PATCH 1676/2244] Log recording RAW audio codec as error It is not possible to record with a RAW audio codec, so the log before exiting should be an error rather than a warning. --- 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 56b5cfb2..462465fa 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -2353,7 +2353,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } if (opts->audio_codec == SC_CODEC_RAW) { - LOGW("Recording does not support RAW audio codec"); + LOGE("Recording does not support RAW audio codec"); return false; } From 9d5f53caa76151e0983700e4ae6ccb3a445e1379 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 11 Nov 2023 11:04:21 +0100 Subject: [PATCH 1677/2244] Stop capture on any RAW audio error The server was stopped only if an IOException occurred during RAW audio capture, but it did not catch RuntimeExceptions. --- .../src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java b/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java index 7d2adade..6108c54b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java @@ -62,8 +62,8 @@ public final class AudioRawRecorder implements AsyncProcessor { record(); } catch (AudioCaptureForegroundException e) { // Do not print stack trace, a user-friendly error-message has already been logged - } catch (IOException e) { - Ln.e("Audio recording error", e); + } catch (Throwable t) { + Ln.e("Audio recording error", t); fatalError = true; } finally { Ln.d("Audio recorder stopped"); From 420d3a40ddea61be80ef1e7026fa2edb22f66a41 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 11 Nov 2023 11:09:47 +0100 Subject: [PATCH 1678/2244] Fix error handling in raw audio recorder It is incorret to ever call: streamer.writeDisableStream(...); after: streamer.writeAudioHeader(); Move the try-catch block so that it can never happen. --- .../java/com/genymobile/scrcpy/AudioRawRecorder.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java b/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java index 6108c54b..fdac8b3a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java @@ -32,7 +32,13 @@ public final class AudioRawRecorder implements AsyncProcessor { final MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); try { - capture.start(); + try { + capture.start(); + } catch (Throwable t) { + // Notify the client that the audio could not be captured + streamer.writeDisableStream(false); + throw t; + } streamer.writeAudioHeader(); while (!Thread.currentThread().isInterrupted()) { @@ -45,10 +51,6 @@ public final class AudioRawRecorder implements AsyncProcessor { streamer.writePacket(buffer, bufferInfo); } - } catch (Throwable e) { - // Notify the client that the audio could not be captured - streamer.writeDisableStream(false); - throw e; } finally { capture.stop(); } From 4eb33054cda35043983b57eb37395cbdac8724eb Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 11 Nov 2023 11:14:01 +0100 Subject: [PATCH 1679/2244] Do not log EPIPE on close for raw audio Handle EPIPE the same way in AudioRawRecorder as in AudioEncoder. This prevents useless errors on close. --- .../main/java/com/genymobile/scrcpy/AudioRawRecorder.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java b/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java index fdac8b3a..ce33ae85 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java @@ -51,6 +51,11 @@ public final class AudioRawRecorder implements AsyncProcessor { streamer.writePacket(buffer, bufferInfo); } + } catch (IOException e) { + // Broken pipe is expected on close, because the socket is closed by the client + if (!IO.isBrokenPipe(e)) { + Ln.e("Audio capture error", e); + } } finally { capture.stop(); } From 5e59ed31352251791679e5931d7e5abf0c2d18f6 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 11 Nov 2023 11:34:31 +0100 Subject: [PATCH 1680/2244] Always initialize SDL with the video subsystem Clipboard synchronization requires SDL_INIT_VIDEO, so always initialize the video subsystem, even if --no-video or --no-video-playback is passed. Refs caf594c90ef1b71ed844b2a9b42c3b3371215d6f Fixes #4418 --- app/src/scrcpy.c | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 1d0e90c1..ac2b8e33 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -417,10 +417,14 @@ scrcpy(struct scrcpy_options *options) { if (options->video_playback) { sdl_set_hints(options->render_driver); - if (SDL_Init(SDL_INIT_VIDEO)) { - LOGE("Could not initialize SDL video: %s", SDL_GetError()); - goto end; - } + } + + // Initialize the video subsystem even if --no-video or --no-video-playback + // is passed so that clipboard synchronization still works. + // + if (SDL_Init(SDL_INIT_VIDEO)) { + LOGE("Could not initialize SDL video: %s", SDL_GetError()); + goto end; } if (options->audio_playback) { From e637feba51c2eac6de27ebb318a2f7a1aa54a62d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 14 Nov 2023 09:08:24 +0100 Subject: [PATCH 1681/2244] Update muxers documentation Recording now supports formats other than mp4 and mkv. --- app/data/bash-completion/scrcpy | 2 +- app/data/zsh-completion/_scrcpy | 2 +- app/scrcpy.1 | 4 ++-- app/src/cli.c | 4 ++-- doc/recording.md | 9 +++++---- 5 files changed, 11 insertions(+), 10 deletions(-) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index eaed88b7..08ca29db 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -125,7 +125,7 @@ _scrcpy() { return ;; --record-format) - COMPREPLY=($(compgen -W 'mkv mp4' -- "$cur")) + COMPREPLY=($(compgen -W 'mp4 mkv m4a mka opus aac' -- "$cur")) return ;; --render-driver) diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 4b1e5868..31706224 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -65,7 +65,7 @@ arguments=( '--push-target=[Set the target directory for pushing files to the device by drag and drop]' {-r,--record=}'[Record screen to file]:record file:_files' '--raw-key-events[Inject key events for all input keys, and ignore text events]' - '--record-format=[Force recording format]:format:(mp4 mkv)' + '--record-format=[Force recording format]:format:(mp4 mkv m4a mka opus aac)' '--render-driver=[Request SDL to use the given render driver]:driver name:(direct3d opengl opengles2 opengles metal software)' '--require-audio=[Make scrcpy fail if audio is enabled but does not work]' '--rotation=[Set the initial display rotation]:rotation values:(0 1 2 3)' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 2901d014..e72cf617 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -347,7 +347,7 @@ Record screen to The format is determined by the .B \-\-record\-format -option if set, or by the file extension (.mp4 or .mkv). +option if set, or by the file extension. .TP .B \-\-raw\-key\-events @@ -355,7 +355,7 @@ Inject key events for all input keys, and ignore text events. .TP .BI "\-\-record\-format " format -Force recording format (either mp4 or mkv). +Force recording format (mp4, mkv, m4a, mka, opus or aac). .TP .BI "\-\-render\-driver " name diff --git a/app/src/cli.c b/app/src/cli.c index 462465fa..078ce315 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -583,7 +583,7 @@ static const struct sc_option options[] = { .argdesc = "file.mp4", .text = "Record screen to file.\n" "The format is determined by the --record-format option if " - "set, or by the file extension (.mp4 or .mkv).", + "set, or by the file extension.", }, { .longopt_id = OPT_RAW_KEY_EVENTS, @@ -594,7 +594,7 @@ static const struct sc_option options[] = { .longopt_id = OPT_RECORD_FORMAT, .longopt = "record-format", .argdesc = "format", - .text = "Force recording format (either mp4 or mkv).", + .text = "Force recording format (mp4, mkv, m4a, mka, opus or aac).", }, { .longopt_id = OPT_RENDER_DRIVER, diff --git a/doc/recording.md b/doc/recording.md index 76a7efd6..d844b368 100644 --- a/doc/recording.md +++ b/doc/recording.md @@ -31,14 +31,15 @@ course, not if you capture your scrcpy window and audio output on the computer). ## Format The video and audio streams are encoded on the device, but are muxed on the -client side. Two formats (containers) are supported: - - Matroska (`.mkv`) - - MP4 (`.mp4`) +client side. Several formats (containers) are supported: + - MP4 (`.mp4`, `.m4a`, `.aac`) + - Matroska (`.mkv`, `.mka`) + - OPUS (`.opus`) The container is automatically selected based on the filename. It is also possible to explicitly select a container (in that case the filename -needs not end with `.mkv` or `.mp4`): +needs not end with a known extension): ``` scrcpy --record=file --record-format=mkv From 80defdd8aa29a89bc656df0f2cdc8a1474f95741 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 15 Nov 2023 12:01:10 +0100 Subject: [PATCH 1682/2244] Suppress private APIs lints to Workarounds class The whole class need them (including the static block). --- server/src/main/java/com/genymobile/scrcpy/Workarounds.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java index 77827c47..db9c9629 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java +++ b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java @@ -19,6 +19,7 @@ import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; +@SuppressLint("PrivateApi,BlockedPrivateApi,SoonBlockedPrivateApi,DiscouragedPrivateApi") public final class Workarounds { private static final Class ACTIVITY_THREAD_CLASS; @@ -105,7 +106,6 @@ public final class Workarounds { Looper.prepareMainLooper(); } - @SuppressLint("PrivateApi,DiscouragedPrivateApi") private static void fillAppInfo() { try { // ActivityThread.AppBindData appBindData = new ActivityThread.AppBindData(); @@ -132,7 +132,6 @@ public final class Workarounds { } } - @SuppressLint("PrivateApi,DiscouragedPrivateApi") private static void fillAppContext() { try { Application app = new Application(); @@ -162,7 +161,7 @@ public final class Workarounds { } @TargetApi(Build.VERSION_CODES.R) - @SuppressLint("WrongConstant,MissingPermission,BlockedPrivateApi,SoonBlockedPrivateApi,DiscouragedPrivateApi") + @SuppressLint("WrongConstant,MissingPermission") public static AudioRecord createAudioRecord(int source, int sampleRate, int channelConfig, int channels, int channelMask, int encoding) { // Vivo (and maybe some other third-party ROMs) modified `AudioRecord`'s constructor, requiring `Context`s from real App environment. // From 783719c72e6659e20c48fd57171ac957df5a148b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 12 Nov 2023 12:48:56 +0100 Subject: [PATCH 1683/2244] Fix OPUS packet in an endian-independent way Reading the header id as an int assumed that the current endianness was little endian. Read to a byte array to remove this assumption. --- .../src/main/java/com/genymobile/scrcpy/Streamer.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Streamer.java b/server/src/main/java/com/genymobile/scrcpy/Streamer.java index 39f74fb6..c3f1c6ee 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Streamer.java +++ b/server/src/main/java/com/genymobile/scrcpy/Streamer.java @@ -5,14 +5,13 @@ import android.media.MediaCodec; import java.io.FileDescriptor; import java.io.IOException; import java.nio.ByteBuffer; +import java.util.Arrays; public final class Streamer { private static final long PACKET_FLAG_CONFIG = 1L << 63; private static final long PACKET_FLAG_KEY_FRAME = 1L << 62; - private static final long AOPUSHDR = 0x5244485355504F41L; // "AOPUSHDR" in ASCII (little-endian) - private final FileDescriptor fd; private final Codec codec; private final boolean sendCodecMeta; @@ -120,11 +119,14 @@ public final class Streamer { throw new IOException("Not enough data in OPUS config packet"); } - long id = buffer.getLong(); - if (id != AOPUSHDR) { + final byte[] opusHeaderId = {'A', 'O', 'P', 'U', 'S', 'H', 'D', 'R'}; + byte[] idBuffer = new byte[8]; + buffer.get(idBuffer); + if (!Arrays.equals(idBuffer, opusHeaderId)) { throw new IOException("OPUS header not found"); } + // The size is in native byte-order long sizeLong = buffer.getLong(); if (sizeLong < 0 || sizeLong >= 0x7FFFFFFF) { throw new IOException("Invalid block size in OPUS header: " + sizeLong); From f23be823fded5090792deccdb2b892074208e9d3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 12 Nov 2023 17:45:11 +0100 Subject: [PATCH 1684/2244] Upgrade FFmpeg build to 6.1-scrcpy Upgrade to FFmpeg 6.1, and with FLAC support enabled. --- app/prebuilt-deps/prepare-ffmpeg.sh | 4 ++-- cross_win32.txt | 2 +- cross_win64.txt | 2 +- release.mk | 16 ++++++++-------- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/app/prebuilt-deps/prepare-ffmpeg.sh b/app/prebuilt-deps/prepare-ffmpeg.sh index 9019cc2d..12accb40 100755 --- a/app/prebuilt-deps/prepare-ffmpeg.sh +++ b/app/prebuilt-deps/prepare-ffmpeg.sh @@ -6,11 +6,11 @@ cd "$DIR" mkdir -p "$PREBUILT_DATA_DIR" cd "$PREBUILT_DATA_DIR" -VERSION=6.0-scrcpy-4 +VERSION=6.1-scrcpy DEP_DIR="ffmpeg-$VERSION" FILENAME="$DEP_DIR".7z -SHA256SUM=39274b321491ce83e76cab5d24e7cbe3f402d3ccf382f739b13be5651c146b60 +SHA256SUM=b41726e603f4624bb9ed7d2836e3e59d9d20b000e22a9ebd27055f4e99e48219 if [[ -d "$DEP_DIR" ]] then diff --git a/cross_win32.txt b/cross_win32.txt index 109bdd27..ef8d52ab 100644 --- a/cross_win32.txt +++ b/cross_win32.txt @@ -16,6 +16,6 @@ cpu = 'i686' endian = 'little' [properties] -prebuilt_ffmpeg = 'ffmpeg-6.0-scrcpy-4/win32' +prebuilt_ffmpeg = 'ffmpeg-6.1-scrcpy/win32' prebuilt_sdl2 = 'SDL2-2.28.4/i686-w64-mingw32' prebuilt_libusb = 'libusb-1.0.26/libusb-MinGW-Win32' diff --git a/cross_win64.txt b/cross_win64.txt index 70e105ab..4e39773d 100644 --- a/cross_win64.txt +++ b/cross_win64.txt @@ -16,6 +16,6 @@ cpu = 'x86_64' endian = 'little' [properties] -prebuilt_ffmpeg = 'ffmpeg-6.0-scrcpy-4/win64' +prebuilt_ffmpeg = 'ffmpeg-6.1-scrcpy/win64' prebuilt_sdl2 = 'SDL2-2.28.4/x86_64-w64-mingw32' prebuilt_libusb = 'libusb-1.0.26/libusb-MinGW-x64' diff --git a/release.mk b/release.mk index 258017bc..00498418 100644 --- a/release.mk +++ b/release.mk @@ -94,10 +94,10 @@ dist-win32: build-server build-win32 cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN32_TARGET_DIR)" cp app/data/icon.png "$(DIST)/$(WIN32_TARGET_DIR)" cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN32_TARGET_DIR)" - cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-4/win32/bin/avutil-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-4/win32/bin/avcodec-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-4/win32/bin/avformat-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-4/win32/bin/swresample-4.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy/win32/bin/avutil-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy/win32/bin/avcodec-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy/win32/bin/avformat-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy/win32/bin/swresample-4.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-34.0.5/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-34.0.5/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-34.0.5/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" @@ -112,10 +112,10 @@ dist-win64: build-server build-win64 cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN64_TARGET_DIR)" cp app/data/icon.png "$(DIST)/$(WIN64_TARGET_DIR)" cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN64_TARGET_DIR)" - cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-4/win64/bin/avutil-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-4/win64/bin/avcodec-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-4/win64/bin/avformat-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-4/win64/bin/swresample-4.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy/win64/bin/avutil-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy/win64/bin/avcodec-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy/win64/bin/avformat-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy/win64/bin/swresample-4.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-34.0.5/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-34.0.5/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-34.0.5/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" From 4857c5dd5964eccd2c8f772ae570332d12f4f825 Mon Sep 17 00:00:00 2001 From: megapro17 Date: Tue, 7 Nov 2023 15:09:47 +0300 Subject: [PATCH 1685/2244] Add support for FLAC audio codec PR #4410 <#https://github.com/Genymobile/scrcpy/pull/4410> Co-authored-by: Romain Vimont Signed-off-by: Romain Vimont --- app/data/bash-completion/scrcpy | 4 +- app/data/zsh-completion/_scrcpy | 4 +- app/scrcpy.1 | 4 +- app/src/cli.c | 24 ++++++++-- app/src/demuxer.c | 10 +++- app/src/options.h | 5 +- app/src/recorder.c | 2 + app/src/server.c | 2 + doc/audio.md | 12 ++++- doc/recording.md | 4 +- .../com/genymobile/scrcpy/AudioCodec.java | 1 + .../java/com/genymobile/scrcpy/Streamer.java | 47 ++++++++++++++++++- 12 files changed, 103 insertions(+), 16 deletions(-) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index 08ca29db..9d51fb18 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -97,7 +97,7 @@ _scrcpy() { return ;; --audio-codec) - COMPREPLY=($(compgen -W 'opus aac raw' -- "$cur")) + COMPREPLY=($(compgen -W 'opus aac flac raw' -- "$cur")) return ;; --video-source) @@ -125,7 +125,7 @@ _scrcpy() { return ;; --record-format) - COMPREPLY=($(compgen -W 'mp4 mkv m4a mka opus aac' -- "$cur")) + COMPREPLY=($(compgen -W 'mp4 mkv m4a mka opus aac flac' -- "$cur")) return ;; --render-driver) diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 31706224..c59ac669 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -11,7 +11,7 @@ arguments=( '--always-on-top[Make scrcpy window always on top \(above other windows\)]' '--audio-bit-rate=[Encode the audio at the given bit-rate]' '--audio-buffer=[Configure the audio buffering delay (in milliseconds)]' - '--audio-codec=[Select the audio codec]:codec:(opus aac raw)' + '--audio-codec=[Select the audio codec]:codec:(opus aac flac raw)' '--audio-codec-options=[Set a list of comma-separated key\:type=value options for the device audio encoder]' '--audio-encoder=[Use a specific MediaCodec audio encoder]' '--audio-source=[Select the audio source]:source:(output mic)' @@ -65,7 +65,7 @@ arguments=( '--push-target=[Set the target directory for pushing files to the device by drag and drop]' {-r,--record=}'[Record screen to file]:record file:_files' '--raw-key-events[Inject key events for all input keys, and ignore text events]' - '--record-format=[Force recording format]:format:(mp4 mkv m4a mka opus aac)' + '--record-format=[Force recording format]:format:(mp4 mkv m4a mka opus aac flac)' '--render-driver=[Request SDL to use the given render driver]:driver name:(direct3d opengl opengles2 opengles metal software)' '--require-audio=[Make scrcpy fail if audio is enabled but does not work]' '--rotation=[Set the initial display rotation]:rotation values:(0 1 2 3)' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index e72cf617..cfcfb227 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -35,7 +35,7 @@ Default is 50. .TP .BI "\-\-audio\-codec " name -Select an audio codec (opus, aac or raw). +Select an audio codec (opus, aac, flac or raw). Default is opus. @@ -355,7 +355,7 @@ Inject key events for all input keys, and ignore text events. .TP .BI "\-\-record\-format " format -Force recording format (mp4, mkv, m4a, mka, opus or aac). +Force recording format (mp4, mkv, m4a, mka, opus, aac or flac). .TP .BI "\-\-render\-driver " name diff --git a/app/src/cli.c b/app/src/cli.c index 078ce315..edb546fa 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -152,7 +152,7 @@ static const struct sc_option options[] = { .longopt_id = OPT_AUDIO_CODEC, .longopt = "audio-codec", .argdesc = "name", - .text = "Select an audio codec (opus, aac or raw).\n" + .text = "Select an audio codec (opus, aac, flac or raw).\n" "Default is opus.", }, { @@ -594,7 +594,8 @@ static const struct sc_option options[] = { .longopt_id = OPT_RECORD_FORMAT, .longopt = "record-format", .argdesc = "format", - .text = "Force recording format (mp4, mkv, m4a, mka, opus or aac).", + .text = "Force recording format (mp4, mkv, m4a, mka, opus, aac or " + "flac).", }, { .longopt_id = OPT_RENDER_DRIVER, @@ -1626,6 +1627,9 @@ get_record_format(const char *name) { if (!strcmp(name, "aac")) { return SC_RECORD_FORMAT_AAC; } + if (!strcmp(name, "flac")) { + return SC_RECORD_FORMAT_FLAC; + } return 0; } @@ -1695,11 +1699,15 @@ parse_audio_codec(const char *optarg, enum sc_codec *codec) { *codec = SC_CODEC_AAC; return true; } + if (!strcmp(optarg, "flac")) { + *codec = SC_CODEC_FLAC; + return true; + } if (!strcmp(optarg, "raw")) { *codec = SC_CODEC_RAW; return true; } - LOGE("Unsupported audio codec: %s (expected opus, aac or raw)", optarg); + LOGE("Unsupported audio codec: %s (expected opus, aac, flac or raw)", optarg); return false; } @@ -2376,6 +2384,16 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], "(try with --audio-codec=aac)"); return false; } + if (opts->record_format == SC_RECORD_FORMAT_FLAC + && opts->audio_codec != SC_CODEC_FLAC) { + LOGE("Recording to FLAC file requires a FLAC audio stream " + "(try with --audio-codec=flac)"); + return false; + } + } + + if (opts->audio_codec == SC_CODEC_FLAC && opts->audio_bit_rate) { + LOGW("--audio-bit-rate is ignored for FLAC audio codec"); } if (opts->audio_codec == SC_CODEC_RAW) { diff --git a/app/src/demuxer.c b/app/src/demuxer.c index 943f72b6..c9ee8f3c 100644 --- a/app/src/demuxer.c +++ b/app/src/demuxer.c @@ -25,7 +25,8 @@ sc_demuxer_to_avcodec_id(uint32_t codec_id) { #define SC_CODEC_ID_H265 UINT32_C(0x68323635) // "h265" in ASCII #define SC_CODEC_ID_AV1 UINT32_C(0x00617631) // "av1" in ASCII #define SC_CODEC_ID_OPUS UINT32_C(0x6f707573) // "opus" in ASCII -#define SC_CODEC_ID_AAC UINT32_C(0x00616163) // "aac in ASCII" +#define SC_CODEC_ID_AAC UINT32_C(0x00616163) // "aac" in ASCII +#define SC_CODEC_ID_FLAC UINT32_C(0x666c6163) // "flac" in ASCII #define SC_CODEC_ID_RAW UINT32_C(0x00726177) // "raw" in ASCII switch (codec_id) { case SC_CODEC_ID_H264: @@ -43,6 +44,8 @@ sc_demuxer_to_avcodec_id(uint32_t codec_id) { return AV_CODEC_ID_OPUS; case SC_CODEC_ID_AAC: return AV_CODEC_ID_AAC; + case SC_CODEC_ID_FLAC: + return AV_CODEC_ID_FLAC; case SC_CODEC_ID_RAW: return AV_CODEC_ID_PCM_S16LE; default: @@ -207,6 +210,11 @@ run_demuxer(void *data) { codec_ctx->channels = 2; #endif codec_ctx->sample_rate = 48000; + + if (raw_codec_id == SC_CODEC_ID_FLAC) { + // The sample_fmt is not set by the FLAC decoder + codec_ctx->sample_fmt = AV_SAMPLE_FMT_S16; + } } if (avcodec_open2(codec_ctx, codec, NULL) < 0) { diff --git a/app/src/options.h b/app/src/options.h index 18b437d8..91433894 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -25,6 +25,7 @@ enum sc_record_format { SC_RECORD_FORMAT_MKA, SC_RECORD_FORMAT_OPUS, SC_RECORD_FORMAT_AAC, + SC_RECORD_FORMAT_FLAC, }; static inline bool @@ -32,7 +33,8 @@ sc_record_format_is_audio_only(enum sc_record_format fmt) { return fmt == SC_RECORD_FORMAT_M4A || fmt == SC_RECORD_FORMAT_MKA || fmt == SC_RECORD_FORMAT_OPUS - || fmt == SC_RECORD_FORMAT_AAC; + || fmt == SC_RECORD_FORMAT_AAC + || fmt == SC_RECORD_FORMAT_FLAC; } enum sc_codec { @@ -41,6 +43,7 @@ enum sc_codec { SC_CODEC_AV1, SC_CODEC_OPUS, SC_CODEC_AAC, + SC_CODEC_FLAC, SC_CODEC_RAW, }; diff --git a/app/src/recorder.c b/app/src/recorder.c index 23c8b497..d13b122a 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -69,6 +69,8 @@ sc_recorder_get_format_name(enum sc_record_format format) { return "matroska"; case SC_RECORD_FORMAT_OPUS: return "opus"; + case SC_RECORD_FORMAT_FLAC: + return "flac"; default: return NULL; } diff --git a/app/src/server.c b/app/src/server.c index 2b3439da..d4726c2a 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -178,6 +178,8 @@ sc_server_get_codec_name(enum sc_codec codec) { return "opus"; case SC_CODEC_AAC: return "aac"; + case SC_CODEC_FLAC: + return "flac"; case SC_CODEC_RAW: return "raw"; default: diff --git a/doc/audio.md b/doc/audio.md index cb6cde95..ecae4468 100644 --- a/doc/audio.md +++ b/doc/audio.md @@ -62,12 +62,13 @@ scrcpy --audio-source=mic --no-video --no-playback --record=file.opus ## Codec -The audio codec can be selected. The possible values are `opus` (default), `aac` -and `raw` (uncompressed PCM 16-bit LE): +The audio codec can be selected. The possible values are `opus` (default), +`aac`, `flac` and `raw` (uncompressed PCM 16-bit LE): ```bash scrcpy --audio-codec=opus # default scrcpy --audio-codec=aac +scrcpy --audio-codec=flac scrcpy --audio-codec=raw ``` @@ -80,7 +81,14 @@ then your device has no Opus encoder: try `scrcpy --audio-codec=aac`. For advanced usage, to pass arbitrary parameters to the [`MediaFormat`], check `--audio-codec-options` in the manpage or in `scrcpy --help`. +For example, to change the [FLAC compression level]: + +```bash +scrcpy --audio-codec=flac --audio-codec-options=flac-compression-level=8 +``` + [`MediaFormat`]: https://developer.android.com/reference/android/media/MediaFormat +[FLAC compression level]: https://developer.android.com/reference/android/media/MediaFormat#KEY_FLAC_COMPRESSION_LEVEL ## Encoder diff --git a/doc/recording.md b/doc/recording.md index d844b368..466cf542 100644 --- a/doc/recording.md +++ b/doc/recording.md @@ -18,7 +18,8 @@ To record only the audio: ```bash scrcpy --no-video --record=file.opus scrcpy --no-video --audio-codec=aac --record=file.aac -# .m4a/.mp4 and .mka/.mkv are also supported for both opus and aac +scrcpy --no-video --audio-codec=flac --record=file.flac +# .m4a/.mp4 and .mka/.mkv are also supported for opus, aac and flac ``` Timestamps are captured on the device, so [packet delay variation] does not @@ -35,6 +36,7 @@ client side. Several formats (containers) are supported: - MP4 (`.mp4`, `.m4a`, `.aac`) - Matroska (`.mkv`, `.mka`) - OPUS (`.opus`) + - FLAC (`.flac`) The container is automatically selected based on the filename. diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioCodec.java b/server/src/main/java/com/genymobile/scrcpy/AudioCodec.java index 1f3b07a0..b4ea3680 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioCodec.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioCodec.java @@ -5,6 +5,7 @@ import android.media.MediaFormat; public enum AudioCodec implements Codec { OPUS(0x6f_70_75_73, "opus", MediaFormat.MIMETYPE_AUDIO_OPUS), AAC(0x00_61_61_63, "aac", MediaFormat.MIMETYPE_AUDIO_AAC), + FLAC(0x66_6c_61_63, "flac", MediaFormat.MIMETYPE_AUDIO_FLAC), RAW(0x00_72_61_77, "raw", MediaFormat.MIMETYPE_AUDIO_RAW); private final int id; // 4-byte ASCII representation of the name diff --git a/server/src/main/java/com/genymobile/scrcpy/Streamer.java b/server/src/main/java/com/genymobile/scrcpy/Streamer.java index c3f1c6ee..8b6c9dcc 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Streamer.java +++ b/server/src/main/java/com/genymobile/scrcpy/Streamer.java @@ -5,6 +5,7 @@ import android.media.MediaCodec; import java.io.FileDescriptor; import java.io.IOException; import java.nio.ByteBuffer; +import java.nio.ByteOrder; import java.util.Arrays; public final class Streamer { @@ -29,6 +30,7 @@ public final class Streamer { public Codec getCodec() { return codec; } + public void writeAudioHeader() throws IOException { if (sendCodecMeta) { ByteBuffer buffer = ByteBuffer.allocate(4); @@ -61,8 +63,12 @@ public final class Streamer { } public void writePacket(ByteBuffer buffer, long pts, boolean config, boolean keyFrame) throws IOException { - if (config && codec == AudioCodec.OPUS) { - fixOpusConfigPacket(buffer); + if (config) { + if (codec == AudioCodec.OPUS) { + fixOpusConfigPacket(buffer); + } else if (codec == AudioCodec.FLAC) { + fixFlacConfigPacket(buffer); + } } if (sendFrameMeta) { @@ -140,4 +146,41 @@ public final class Streamer { // Set the buffer to point to the OPUS header slice buffer.limit(buffer.position() + size); } + + private static void fixFlacConfigPacket(ByteBuffer buffer) throws IOException { + // 00000000 66 4c 61 43 00 00 00 22 |fLaC..." | + // -------------- BELOW IS THE PART WE MUST PUT AS EXTRADATA ------------------- + // 00000000 10 00 10 00 00 00 00 00 | ........| + // 00000010 00 00 0b b8 02 f0 00 00 00 00 00 00 00 00 00 00 |................| + // 00000020 00 00 00 00 00 00 00 00 00 00 |.......... | + // ------------------------------------------------------------------------------ + // 00000020 84 00 00 28 20 00 | ...( .| + // 00000030 00 00 72 65 66 65 72 65 6e 63 65 20 6c 69 62 46 |..reference libF| + // 00000040 4c 41 43 20 31 2e 33 2e 32 20 32 30 32 32 31 30 |LAC 1.3.2 202210| + // 00000050 32 32 00 00 00 00 |22....| + // + // + + if (buffer.remaining() < 8) { + throw new IOException("Not enough data in FLAC config packet"); + } + + final byte[] flacHeaderId = {'f', 'L', 'a', 'C'}; + byte[] idBuffer = new byte[4]; + buffer.get(idBuffer); + if (!Arrays.equals(idBuffer, flacHeaderId)) { + throw new IOException("FLAC header not found"); + } + + // The size is in big-endian + buffer.order(ByteOrder.BIG_ENDIAN); + + int size = buffer.getInt(); + if (buffer.remaining() < size) { + throw new IOException("Not enough data in FLAC header (invalid size: " + size + ")"); + } + + // Set the buffer to point to the FLAC header slice + buffer.limit(buffer.position() + size); + } } From 258eaaae2af9c1e41611f8f570bd46fe10165859 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 12 Nov 2023 18:34:04 +0100 Subject: [PATCH 1686/2244] Increase default audio buffer for FLAC FLAC is not low latency: the default encoder produces blocks of 4096 samples, which represent ~85.333ms. Increase the audio buffer by default so that audio playback works. --- app/src/cli.c | 13 +++++++++++++ app/src/options.c | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/app/src/cli.c b/app/src/cli.c index edb546fa..0482b233 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -2265,6 +2265,19 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], opts->require_audio = true; } + if (opts->audio_playback && opts->audio_buffer == -1) { + if (opts->audio_codec == SC_CODEC_FLAC) { + // Use 50 ms audio buffer by default, but use a higher value for FLAC, + // which is not low latency (the default encoder produces blocks of + // 4096 samples, which represent ~85.333ms). + LOGI("FLAC audio: audio buffer increased to 120 ms (use " + "--audio-buffer to set a custom value)"); + opts->audio_buffer = SC_TICK_FROM_MS(120); + } else { + opts->audio_buffer = SC_TICK_FROM_MS(50); + } + } + #ifdef HAVE_V4L2 if (v4l2) { if (opts->lock_video_orientation == diff --git a/app/src/options.c b/app/src/options.c index 6c72d767..092fbd56 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -46,7 +46,7 @@ const struct scrcpy_options scrcpy_options_default = { .window_height = 0, .display_id = 0, .display_buffer = 0, - .audio_buffer = SC_TICK_FROM_MS(50), + .audio_buffer = -1, // depends on the audio format, .audio_output_buffer = SC_TICK_FROM_MS(5), .time_limit = 0, #ifdef HAVE_V4L2 From 3bb6b0cb9f4a67046bd96b53bef46e34818ae0f3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 14 Nov 2023 08:49:10 +0100 Subject: [PATCH 1687/2244] Read audio by blocks of 1024 samples In practice, the system captures audio samples by blocks of 1024 samples. Remplace the hardcoded value of 5 milliseconds (240 samples), and let AudioRecord fill the input buffer provided by MediaCodec (or by AudioRawRecorder), with a maximum size of 1024 samples (just in case). --- .../java/com/genymobile/scrcpy/AudioCapture.java | 13 +++++++------ .../java/com/genymobile/scrcpy/AudioEncoder.java | 5 +---- .../com/genymobile/scrcpy/AudioRawRecorder.java | 7 ++----- 3 files changed, 10 insertions(+), 15 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java b/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java index 5575ffb6..e94b49ed 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java @@ -24,6 +24,11 @@ public final class AudioCapture { public static final int ENCODING = AudioFormat.ENCODING_PCM_16BIT; public static final int BYTES_PER_SAMPLE = 2; + // Never read more than 1024 samples, even if the buffer is bigger (that would increase latency). + // A lower value is useless, since the system captures audio samples by blocks of 1024 (so for example if we read by blocks of 256 samples, we + // receive 4 successive blocks without waiting, then we wait for the 4 next ones). + public static final int MAX_READ_SIZE = 1024 * CHANNELS * BYTES_PER_SAMPLE; + private final int audioSource; private AudioRecord recorder; @@ -36,10 +41,6 @@ public final class AudioCapture { this.audioSource = audioSource.value(); } - public static int millisToBytes(int millis) { - return SAMPLE_RATE * CHANNELS * BYTES_PER_SAMPLE * millis / 1000; - } - private static AudioFormat createAudioFormat() { AudioFormat.Builder builder = new AudioFormat.Builder(); builder.setEncoding(ENCODING); @@ -135,8 +136,8 @@ public final class AudioCapture { } @TargetApi(Build.VERSION_CODES.N) - public int read(ByteBuffer directBuffer, int size, MediaCodec.BufferInfo outBufferInfo) { - int r = recorder.read(directBuffer, size); + public int read(ByteBuffer directBuffer, MediaCodec.BufferInfo outBufferInfo) { + int r = recorder.read(directBuffer, MAX_READ_SIZE); if (r <= 0) { return r; } diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java b/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java index bec79b05..ad8d0422 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java @@ -37,9 +37,6 @@ public final class AudioEncoder implements AsyncProcessor { private static final int SAMPLE_RATE = AudioCapture.SAMPLE_RATE; private static final int CHANNELS = AudioCapture.CHANNELS; - private static final int READ_MS = 5; // milliseconds - private static final int READ_SIZE = AudioCapture.millisToBytes(READ_MS); - private final AudioCapture capture; private final Streamer streamer; private final int bitRate; @@ -93,7 +90,7 @@ public final class AudioEncoder implements AsyncProcessor { while (!Thread.currentThread().isInterrupted()) { InputTask task = inputTasks.take(); ByteBuffer buffer = mediaCodec.getInputBuffer(task.index); - int r = capture.read(buffer, READ_SIZE, bufferInfo); + int r = capture.read(buffer, bufferInfo); if (r <= 0) { throw new IOException("Could not read audio: " + r); } diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java b/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java index ce33ae85..7e052f32 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java @@ -13,9 +13,6 @@ public final class AudioRawRecorder implements AsyncProcessor { private Thread thread; - private static final int READ_MS = 5; // milliseconds - private static final int READ_SIZE = AudioCapture.millisToBytes(READ_MS); - public AudioRawRecorder(AudioCapture capture, Streamer streamer) { this.capture = capture; this.streamer = streamer; @@ -28,7 +25,7 @@ public final class AudioRawRecorder implements AsyncProcessor { return; } - final ByteBuffer buffer = ByteBuffer.allocateDirect(READ_SIZE); + final ByteBuffer buffer = ByteBuffer.allocateDirect(AudioCapture.MAX_READ_SIZE); final MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); try { @@ -43,7 +40,7 @@ public final class AudioRawRecorder implements AsyncProcessor { streamer.writeAudioHeader(); while (!Thread.currentThread().isInterrupted()) { buffer.position(0); - int r = capture.read(buffer, READ_SIZE, bufferInfo); + int r = capture.read(buffer, bufferInfo); if (r < 0) { throw new IOException("Could not read audio: " + r); } From a402eac7f293b1f63ce1c0a9449cf3a997dc061e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 14 Nov 2023 09:15:34 +0100 Subject: [PATCH 1688/2244] Compute PTS of intermediate blocks If several reads are performed for a single captured audio block (e.g. if the read size is smaller than the captured block), then the provided timestamp was the same for all packets. Recompute the timestamp for each of them. --- server/src/main/java/com/genymobile/scrcpy/AudioCapture.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java b/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java index e94b49ed..c05bb41d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java @@ -34,6 +34,7 @@ public final class AudioCapture { private AudioRecord recorder; private final AudioTimestamp timestamp = new AudioTimestamp(); + private long previousRecorderTimestamp = -1; private long previousPts = 0; private long nextPts = 0; @@ -145,8 +146,9 @@ public final class AudioCapture { long pts; int ret = recorder.getTimestamp(timestamp, AudioTimestamp.TIMEBASE_MONOTONIC); - if (ret == AudioRecord.SUCCESS) { + if (ret == AudioRecord.SUCCESS && timestamp.nanoTime != previousRecorderTimestamp) { pts = timestamp.nanoTime / 1000; + previousRecorderTimestamp = timestamp.nanoTime; } else { if (nextPts == 0) { Ln.w("Could not get any audio timestamp"); From 4b4f045e196fe037a841f7004eaabb09cf571942 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 14 Nov 2023 09:40:42 +0100 Subject: [PATCH 1689/2244] Fix audio PTS by the duration of 1 sample If the difference of PTS between two consecutive blocks of audio is less than 1 sample, then it will be considered as non-increasing by FFmpeg muxers having a time_base of 1/sample_rate. Increase the PTS by 1 sample instead. --- .../src/main/java/com/genymobile/scrcpy/AudioCapture.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java b/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java index c05bb41d..e3de50e6 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java @@ -29,6 +29,8 @@ public final class AudioCapture { // receive 4 successive blocks without waiting, then we wait for the 4 next ones). public static final int MAX_READ_SIZE = 1024 * CHANNELS * BYTES_PER_SAMPLE; + private static final long ONE_SAMPLE_US = (1000000 + SAMPLE_RATE - 1) / SAMPLE_RATE; // 1 sample in microseconds (used for fixing PTS) + private final int audioSource; private AudioRecord recorder; @@ -160,13 +162,13 @@ public final class AudioCapture { long durationUs = r * 1000000 / (CHANNELS * BYTES_PER_SAMPLE * SAMPLE_RATE); nextPts = pts + durationUs; - if (previousPts != 0 && pts < previousPts) { + if (previousPts != 0 && pts < previousPts + ONE_SAMPLE_US) { // Audio PTS may come from two sources: // - recorder.getTimestamp() if the call works; // - an estimation from the previous PTS and the packet size as a fallback. // // Therefore, the property that PTS are monotonically increasing is no guaranteed in corner cases, so enforce it. - pts = previousPts + 1; + pts = previousPts + ONE_SAMPLE_US; } previousPts = pts; From 1713422c13265946a15b25e9de5762727a33edfb Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 14 Nov 2023 09:30:42 +0100 Subject: [PATCH 1690/2244] Upgrade FFmpeg build to 6.1-scrcpy-2 Use a build with WAV muxer. --- app/prebuilt-deps/prepare-ffmpeg.sh | 4 ++-- cross_win32.txt | 2 +- cross_win64.txt | 2 +- release.mk | 16 ++++++++-------- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/app/prebuilt-deps/prepare-ffmpeg.sh b/app/prebuilt-deps/prepare-ffmpeg.sh index 12accb40..96ea3ee7 100755 --- a/app/prebuilt-deps/prepare-ffmpeg.sh +++ b/app/prebuilt-deps/prepare-ffmpeg.sh @@ -6,11 +6,11 @@ cd "$DIR" mkdir -p "$PREBUILT_DATA_DIR" cd "$PREBUILT_DATA_DIR" -VERSION=6.1-scrcpy +VERSION=6.1-scrcpy-2 DEP_DIR="ffmpeg-$VERSION" FILENAME="$DEP_DIR".7z -SHA256SUM=b41726e603f4624bb9ed7d2836e3e59d9d20b000e22a9ebd27055f4e99e48219 +SHA256SUM=7f25f638dc24a0f5d4af07a088b6a604cf33548900bbfd2f6ce0bae050b7664d if [[ -d "$DEP_DIR" ]] then diff --git a/cross_win32.txt b/cross_win32.txt index ef8d52ab..e24f3722 100644 --- a/cross_win32.txt +++ b/cross_win32.txt @@ -16,6 +16,6 @@ cpu = 'i686' endian = 'little' [properties] -prebuilt_ffmpeg = 'ffmpeg-6.1-scrcpy/win32' +prebuilt_ffmpeg = 'ffmpeg-6.1-scrcpy-2/win32' prebuilt_sdl2 = 'SDL2-2.28.4/i686-w64-mingw32' prebuilt_libusb = 'libusb-1.0.26/libusb-MinGW-Win32' diff --git a/cross_win64.txt b/cross_win64.txt index 4e39773d..39e79944 100644 --- a/cross_win64.txt +++ b/cross_win64.txt @@ -16,6 +16,6 @@ cpu = 'x86_64' endian = 'little' [properties] -prebuilt_ffmpeg = 'ffmpeg-6.1-scrcpy/win64' +prebuilt_ffmpeg = 'ffmpeg-6.1-scrcpy-2/win64' prebuilt_sdl2 = 'SDL2-2.28.4/x86_64-w64-mingw32' prebuilt_libusb = 'libusb-1.0.26/libusb-MinGW-x64' diff --git a/release.mk b/release.mk index 00498418..57fa994e 100644 --- a/release.mk +++ b/release.mk @@ -94,10 +94,10 @@ dist-win32: build-server build-win32 cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN32_TARGET_DIR)" cp app/data/icon.png "$(DIST)/$(WIN32_TARGET_DIR)" cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN32_TARGET_DIR)" - cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy/win32/bin/avutil-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy/win32/bin/avcodec-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy/win32/bin/avformat-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy/win32/bin/swresample-4.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-2/win32/bin/avutil-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-2/win32/bin/avcodec-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-2/win32/bin/avformat-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-2/win32/bin/swresample-4.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-34.0.5/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-34.0.5/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-34.0.5/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" @@ -112,10 +112,10 @@ dist-win64: build-server build-win64 cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN64_TARGET_DIR)" cp app/data/icon.png "$(DIST)/$(WIN64_TARGET_DIR)" cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN64_TARGET_DIR)" - cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy/win64/bin/avutil-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy/win64/bin/avcodec-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy/win64/bin/avformat-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy/win64/bin/swresample-4.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-2/win64/bin/avutil-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-2/win64/bin/avcodec-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-2/win64/bin/avformat-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-2/win64/bin/swresample-4.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-34.0.5/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-34.0.5/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-34.0.5/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" From 200488111e9f54585c67f915265094f7f22e8888 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 13 Nov 2023 09:35:18 +0100 Subject: [PATCH 1691/2244] Add support for RAW audio (WAV) recording RAW audio forwarding was supported but not for recording. Add support for recording a raw audio stream to a `.wav` file (and `.mkv`). --- app/data/bash-completion/scrcpy | 2 +- app/data/zsh-completion/_scrcpy | 2 +- app/scrcpy.1 | 2 +- app/src/cli.c | 26 +++++++++++++++++++------- app/src/options.h | 4 +++- app/src/recorder.c | 18 ++++++++++++++---- app/src/recorder.h | 2 ++ doc/recording.md | 2 ++ 8 files changed, 43 insertions(+), 15 deletions(-) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index 9d51fb18..97dbfe70 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -125,7 +125,7 @@ _scrcpy() { return ;; --record-format) - COMPREPLY=($(compgen -W 'mp4 mkv m4a mka opus aac flac' -- "$cur")) + COMPREPLY=($(compgen -W 'mp4 mkv m4a mka opus aac flac wav' -- "$cur")) return ;; --render-driver) diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index c59ac669..4b8a7737 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -65,7 +65,7 @@ arguments=( '--push-target=[Set the target directory for pushing files to the device by drag and drop]' {-r,--record=}'[Record screen to file]:record file:_files' '--raw-key-events[Inject key events for all input keys, and ignore text events]' - '--record-format=[Force recording format]:format:(mp4 mkv m4a mka opus aac flac)' + '--record-format=[Force recording format]:format:(mp4 mkv m4a mka opus aac flac wav)' '--render-driver=[Request SDL to use the given render driver]:driver name:(direct3d opengl opengles2 opengles metal software)' '--require-audio=[Make scrcpy fail if audio is enabled but does not work]' '--rotation=[Set the initial display rotation]:rotation values:(0 1 2 3)' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index cfcfb227..26f53ba4 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -355,7 +355,7 @@ Inject key events for all input keys, and ignore text events. .TP .BI "\-\-record\-format " format -Force recording format (mp4, mkv, m4a, mka, opus, aac or flac). +Force recording format (mp4, mkv, m4a, mka, opus, aac, flac or wav). .TP .BI "\-\-render\-driver " name diff --git a/app/src/cli.c b/app/src/cli.c index 0482b233..19097f47 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -594,8 +594,8 @@ static const struct sc_option options[] = { .longopt_id = OPT_RECORD_FORMAT, .longopt = "record-format", .argdesc = "format", - .text = "Force recording format (mp4, mkv, m4a, mka, opus, aac or " - "flac).", + .text = "Force recording format (mp4, mkv, m4a, mka, opus, aac, flac " + "or wav).", }, { .longopt_id = OPT_RENDER_DRIVER, @@ -1630,6 +1630,9 @@ get_record_format(const char *name) { if (!strcmp(name, "flac")) { return SC_RECORD_FORMAT_FLAC; } + if (!strcmp(name, "wav")) { + return SC_RECORD_FORMAT_WAV; + } return 0; } @@ -2373,11 +2376,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } } - if (opts->audio_codec == SC_CODEC_RAW) { - LOGE("Recording does not support RAW audio codec"); - return false; - } - if (opts->video && sc_record_format_is_audio_only(opts->record_format)) { LOGE("Audio container does not support video stream"); @@ -2403,6 +2401,20 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], "(try with --audio-codec=flac)"); return false; } + + if (opts->record_format == SC_RECORD_FORMAT_WAV + && opts->audio_codec != SC_CODEC_RAW) { + LOGE("Recording to WAV file requires a RAW audio stream " + "(try with --audio-codec=raw)"); + return false; + } + + if ((opts->record_format == SC_RECORD_FORMAT_MP4 || + opts->record_format == SC_RECORD_FORMAT_M4A) + && opts->audio_codec == SC_CODEC_RAW) { + LOGE("Recording to MP4 container does not support RAW audio"); + return false; + } } if (opts->audio_codec == SC_CODEC_FLAC && opts->audio_bit_rate) { diff --git a/app/src/options.h b/app/src/options.h index 91433894..c702ceeb 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -26,6 +26,7 @@ enum sc_record_format { SC_RECORD_FORMAT_OPUS, SC_RECORD_FORMAT_AAC, SC_RECORD_FORMAT_FLAC, + SC_RECORD_FORMAT_WAV, }; static inline bool @@ -34,7 +35,8 @@ sc_record_format_is_audio_only(enum sc_record_format fmt) { || fmt == SC_RECORD_FORMAT_MKA || fmt == SC_RECORD_FORMAT_OPUS || fmt == SC_RECORD_FORMAT_AAC - || fmt == SC_RECORD_FORMAT_FLAC; + || fmt == SC_RECORD_FORMAT_FLAC + || fmt == SC_RECORD_FORMAT_WAV; } enum sc_codec { diff --git a/app/src/recorder.c b/app/src/recorder.c index d13b122a..8794442b 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -71,6 +71,8 @@ sc_recorder_get_format_name(enum sc_record_format format) { return "opus"; case SC_RECORD_FORMAT_FLAC: return "flac"; + case SC_RECORD_FORMAT_WAV: + return "wav"; default: return NULL; } @@ -168,13 +170,14 @@ sc_recorder_close_output_file(struct sc_recorder *recorder) { } static inline bool -sc_recorder_has_empty_queues(struct sc_recorder *recorder) { +sc_recorder_must_wait_for_config_packets(struct sc_recorder *recorder) { if (recorder->video && sc_vecdeque_is_empty(&recorder->video_queue)) { // The video queue is empty return true; } - if (recorder->audio && sc_vecdeque_is_empty(&recorder->audio_queue)) { + if (recorder->audio && recorder->audio_expects_config_packet + && sc_vecdeque_is_empty(&recorder->audio_queue)) { // The audio queue is empty (when audio is enabled) return true; } @@ -190,7 +193,7 @@ sc_recorder_process_header(struct sc_recorder *recorder) { while (!recorder->stopped && ((recorder->video && !recorder->video_init) || (recorder->audio && !recorder->audio_init) - || sc_recorder_has_empty_queues(recorder))) { + || sc_recorder_must_wait_for_config_packets(recorder))) { sc_cond_wait(&recorder->cond, &recorder->mutex); } @@ -209,7 +212,8 @@ sc_recorder_process_header(struct sc_recorder *recorder) { } AVPacket *audio_pkt = NULL; - if (!sc_vecdeque_is_empty(&recorder->audio_queue)) { + if (recorder->audio_expects_config_packet && + !sc_vecdeque_is_empty(&recorder->audio_queue)) { assert(recorder->audio); audio_pkt = sc_vecdeque_pop(&recorder->audio_queue); } @@ -597,6 +601,10 @@ sc_recorder_audio_packet_sink_open(struct sc_packet_sink *sink, recorder->audio_stream.index = stream->index; + // A config packet is provided for all supported formats except raw audio + recorder->audio_expects_config_packet = + ctx->codec_id != AV_CODEC_ID_PCM_S16LE; + recorder->audio_init = true; sc_cond_signal(&recorder->cond); sc_mutex_unlock(&recorder->mutex); @@ -709,6 +717,8 @@ sc_recorder_init(struct sc_recorder *recorder, const char *filename, recorder->video_init = false; recorder->audio_init = false; + recorder->audio_expects_config_packet = false; + sc_recorder_stream_init(&recorder->video_stream); sc_recorder_stream_init(&recorder->audio_stream); diff --git a/app/src/recorder.h b/app/src/recorder.h index 47fd3f21..16327584 100644 --- a/app/src/recorder.h +++ b/app/src/recorder.h @@ -50,6 +50,8 @@ struct sc_recorder { bool video_init; bool audio_init; + bool audio_expects_config_packet; + struct sc_recorder_stream video_stream; struct sc_recorder_stream audio_stream; diff --git a/doc/recording.md b/doc/recording.md index 466cf542..c1a8445e 100644 --- a/doc/recording.md +++ b/doc/recording.md @@ -19,6 +19,7 @@ To record only the audio: scrcpy --no-video --record=file.opus scrcpy --no-video --audio-codec=aac --record=file.aac scrcpy --no-video --audio-codec=flac --record=file.flac +scrcpy --no-video --audio-codec=raw --record=file.wav # .m4a/.mp4 and .mka/.mkv are also supported for opus, aac and flac ``` @@ -37,6 +38,7 @@ client side. Several formats (containers) are supported: - Matroska (`.mkv`, `.mka`) - OPUS (`.opus`) - FLAC (`.flac`) + - WAV (`.wav`) The container is automatically selected based on the filename. From 15a3bad4abca691d6459821bbcaf4c4f52a52f28 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 14 Nov 2023 09:33:59 +0100 Subject: [PATCH 1692/2244] Log PTS fixing at debug level Audio PTS are retrieved by AudioRecord.getTimestamp(), so they do not necessarily exactly match the number of samples (this allows to take drift and lag into account). As a consequence, two consecutive timestamps in microseconds may sometimes end up within the same millisecond, causing the warning. This is particularly true for the Matroska muxer which uses a timebase of 1/1000 (1 ms precision). Since this is "expected", lower the log level from warning to debug. --- app/src/recorder.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/recorder.c b/app/src/recorder.c index 8794442b..c9d5f131 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -105,7 +105,7 @@ sc_recorder_write_stream(struct sc_recorder *recorder, AVStream *stream = recorder->ctx->streams[st->index]; sc_recorder_rescale_packet(stream, packet); if (st->last_pts != AV_NOPTS_VALUE && packet->pts <= st->last_pts) { - LOGW("Fixing PTS non monotonically increasing in stream %d " + LOGD("Fixing PTS non monotonically increasing in stream %d " "(%" PRIi64 " >= %" PRIi64 ")", st->index, st->last_pts, packet->pts); packet->pts = ++st->last_pts; From 86808e811424560c7ed188b9fd4213c3838b0724 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 15 Nov 2023 20:59:02 +0100 Subject: [PATCH 1693/2244] Upgrade Android checkstyle to 10.12.5 Upgrade to the latest version. --- config/android-checkstyle.gradle | 2 +- server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config/android-checkstyle.gradle b/config/android-checkstyle.gradle index 29c67b19..1e5ce3ba 100644 --- a/config/android-checkstyle.gradle +++ b/config/android-checkstyle.gradle @@ -2,7 +2,7 @@ apply plugin: 'checkstyle' check.dependsOn 'checkstyle' checkstyle { - toolVersion = '9.0.1' + toolVersion = '10.12.5' } task checkstyle(type: Checkstyle) { diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java b/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java index ad8d0422..0b59369b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java @@ -295,7 +295,7 @@ public final class AudioEncoder implements AsyncProcessor { } } - private class EncoderCallback extends MediaCodec.Callback { + private final class EncoderCallback extends MediaCodec.Callback { @TargetApi(Build.VERSION_CODES.N) @Override public void onInputBufferAvailable(MediaCodec codec, int index) { From e8801cc3c0493f72ece976f2b1d3a3bdef8237ac Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 15 Nov 2023 21:05:47 +0100 Subject: [PATCH 1694/2244] Upgrade AGP (8.1.3) and Gradle to 8.4 Android Gradle Plugin 8.1.3. Gradle 8.4. From now on, Java 17 is required. --- build.gradle | 6 +----- doc/build.md | 10 +++++----- gradle/wrapper/gradle-wrapper.properties | 2 +- server/build.gradle | 6 +++++- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/build.gradle b/build.gradle index f7e29b22..b27befb6 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.4.0' + classpath 'com.android.tools.build:gradle:8.1.3' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files @@ -23,7 +23,3 @@ allprojects { options.compilerArgs << "-Xlint:deprecation" } } - -task clean(type: Delete) { - delete rootProject.buildDir -} diff --git a/doc/build.md b/doc/build.md index 54b7410b..15c567b5 100644 --- a/doc/build.md +++ b/doc/build.md @@ -58,7 +58,7 @@ sudo apt install gcc git pkg-config meson ninja-build libsdl2-dev \ libswresample-dev libusb-1.0-0-dev # server build dependencies -sudo apt install openjdk-11-jdk +sudo apt install openjdk-17-jdk ``` On old versions (like Ubuntu 16.04), `meson` is too old. In that case, install @@ -100,7 +100,7 @@ sudo apt install mingw-w64 mingw-w64-tools You also need the JDK to build the server: ```bash -sudo apt install openjdk-11-jdk +sudo apt install openjdk-17-jdk ``` Then generate the releases: @@ -168,13 +168,13 @@ brew install sdl2 ffmpeg libusb brew install pkg-config meson ``` -Additionally, if you want to build the server, install Java 8 from Caskroom, and +Additionally, if you want to build the server, install Java 17 from Caskroom, and make it available from the `PATH`: ```bash brew tap homebrew/cask-versions -brew install adoptopenjdk/openjdk/adoptopenjdk11 -export JAVA_HOME="$(/usr/libexec/java_home --version 1.11)" +brew install adoptopenjdk/openjdk/adoptopenjdk17 +export JAVA_HOME="$(/usr/libexec/java_home --version 1.17)" export PATH="$JAVA_HOME/bin:$PATH" ``` diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2ec77e51..e411586a 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-7.5-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/server/build.gradle b/server/build.gradle index bee6509b..927906fc 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -2,7 +2,7 @@ apply plugin: 'com.android.application' android { namespace 'com.genymobile.scrcpy' - compileSdkVersion 33 + compileSdk 33 defaultConfig { applicationId "com.genymobile.scrcpy" minSdkVersion 21 @@ -17,6 +17,10 @@ android { proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } + buildFeatures { + buildConfig true + aidl true + } } dependencies { From abcb10059749d9536c6e344f5c61466305afd2ee Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 15 Nov 2023 21:08:15 +0100 Subject: [PATCH 1695/2244] Upgrade Android SDK to 34 --- server/build.gradle | 4 ++-- server/build_without_gradle.sh | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/server/build.gradle b/server/build.gradle index 927906fc..1bb31360 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -2,11 +2,11 @@ apply plugin: 'com.android.application' android { namespace 'com.genymobile.scrcpy' - compileSdk 33 + compileSdk 34 defaultConfig { applicationId "com.genymobile.scrcpy" minSdkVersion 21 - targetSdkVersion 33 + targetSdkVersion 34 versionCode 200 versionName "v2.2" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index 6e755272..5ab90a0a 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -14,8 +14,8 @@ set -e SCRCPY_DEBUG=false SCRCPY_VERSION_NAME=v2.2 -PLATFORM=${ANDROID_PLATFORM:-33} -BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-33.0.0} +PLATFORM=${ANDROID_PLATFORM:-34} +BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-34.0.0} BUILD_TOOLS_DIR="$ANDROID_HOME/build-tools/$BUILD_TOOLS" BUILD_DIR="$(realpath ${BUILD_DIR:-build_manual})" From 7e3b9359322fff65bd350febfdc02a76186981cd Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 16 Nov 2023 08:56:04 +0100 Subject: [PATCH 1696/2244] Recreate the display on rotation On Android 14 (Pixel 8), a device rotation while the camera app was running resulted in an incorrect capture. Destroying and recreating the display fixes the issue. --- .../main/java/com/genymobile/scrcpy/ScreenCapture.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenCapture.java b/server/src/main/java/com/genymobile/scrcpy/ScreenCapture.java index f81332f5..e048354a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenCapture.java @@ -18,7 +18,6 @@ public class ScreenCapture extends SurfaceCapture implements Device.RotationList @Override public void init() { - display = createDisplay(); device.setRotationListener(this); device.setFoldListener(this); } @@ -32,6 +31,11 @@ public class ScreenCapture extends SurfaceCapture implements Device.RotationList Rect unlockedVideoRect = screenInfo.getUnlockedVideoSize().toRect(); int videoRotation = screenInfo.getVideoRotation(); int layerStack = device.getLayerStack(); + + if (display != null) { + SurfaceControl.destroyDisplay(display); + } + display = createDisplay(); setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack); } @@ -39,7 +43,9 @@ public class ScreenCapture extends SurfaceCapture implements Device.RotationList public void release() { device.setRotationListener(null); device.setFoldListener(null); - SurfaceControl.destroyDisplay(display); + if (display != null) { + SurfaceControl.destroyDisplay(display); + } } @Override From 45a073a333564a3c596cfe4067de51bb339e8e2b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 16 Nov 2023 09:02:52 +0100 Subject: [PATCH 1697/2244] Do not create Device instance for camera The device instance manages the display and the injection of input events. It is not necessary for camera capture. --- server/src/main/java/com/genymobile/scrcpy/Server.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index a1c6090b..61d3497b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -92,8 +92,6 @@ public final class Server { throw new ConfigurationException("Camera mirroring is not supported"); } - final Device device = new Device(options); - Thread initThread = startInitThread(options); int scid = options.getScid(); @@ -102,7 +100,9 @@ public final class Server { boolean video = options.getVideo(); boolean audio = options.getAudio(); boolean sendDummyByte = options.getSendDummyByte(); - boolean camera = options.getVideoSource() == VideoSource.CAMERA; + boolean camera = video && options.getVideoSource() == VideoSource.CAMERA; + + final Device device = camera ? null : new Device(options); Workarounds.apply(audio, camera); From 4658c0e5d223d3d9aff19a37622e701261b09aef Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 16 Nov 2023 09:59:37 +0100 Subject: [PATCH 1698/2244] Update record format error message Recording now supports formats other than mp4 and mkv. Refs e637feba51c2eac6de27ebb318a2f7a1aa54a62d --- app/src/cli.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index 19097f47..b402f6c5 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -1640,7 +1640,8 @@ static bool parse_record_format(const char *optarg, enum sc_record_format *format) { enum sc_record_format fmt = get_record_format(optarg); if (!fmt) { - LOGE("Unsupported format: %s (expected mp4 or mkv)", optarg); + LOGE("Unsupported record format: %s (expected mp4, mkv, m4a, mka, " + "opus, aac, flac or wav)", optarg); return false; } @@ -1710,7 +1711,8 @@ parse_audio_codec(const char *optarg, enum sc_codec *codec) { *codec = SC_CODEC_RAW; return true; } - LOGE("Unsupported audio codec: %s (expected opus, aac, flac or raw)", optarg); + LOGE("Unsupported audio codec: %s (expected opus, aac, flac or raw)", + optarg); return false; } From 0801cf062722064506f2353c2c9bcd3504c59281 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 20 Nov 2023 13:09:46 +0100 Subject: [PATCH 1699/2244] Fix options alphabetical order Renaming --display to --display-id broke the alphabetical order. Refs 23e116064dc97f2af843e764f13eebd54fab486d --- app/data/bash-completion/scrcpy | 2 +- app/data/zsh-completion/_scrcpy | 2 +- app/scrcpy.1 | 12 ++++++------ app/src/cli.c | 16 ++++++++-------- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index 97dbfe70..f94a70a6 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -19,8 +19,8 @@ _scrcpy() { --crop= -d --select-usb --disable-screensaver - --display-id= --display-buffer= + --display-id= -e --select-tcpip -f --fullscreen --force-adb-forward diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 4b8a7737..cc58b866 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -26,8 +26,8 @@ arguments=( '--crop=[\[width\:height\:x\:y\] Crop the device screen on the server]' {-d,--select-usb}'[Use USB device]' '--disable-screensaver[Disable screensaver while scrcpy is running]' - '--display-id=[Specify the display id to mirror]' '--display-buffer=[Add a buffering delay \(in milliseconds\) before displaying]' + '--display-id=[Specify the display id to mirror]' {-e,--select-tcpip}'[Use TCP/IP device]' {-f,--fullscreen}'[Start in fullscreen]' '--force-adb-forward[Do not attempt to use \"adb reverse\" to connect to the device]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 26f53ba4..10c32ca1 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -127,6 +127,12 @@ Also see \fB\-e\fR (\fB\-\-select\-tcpip\fR). .BI "\-\-disable-screensaver" Disable screensaver while scrcpy is running. +.TP +.BI "\-\-display\-buffer ms +Add a buffering delay (in milliseconds) before displaying. This increases latency to compensate for jitter. + +Default is 0 (no buffering). + .TP .BI "\-\-display\-id " id Specify the device display id to mirror. @@ -135,12 +141,6 @@ The available display ids can be listed by \fB\-\-list\-displays\fR. Default is 0. -.TP -.BI "\-\-display\-buffer ms -Add a buffering delay (in milliseconds) before displaying. This increases latency to compensate for jitter. - -Default is 0 (no buffering). - .TP .B \-e, \-\-select\-tcpip Use TCP/IP device (if there is exactly one, like adb -e). diff --git a/app/src/cli.c b/app/src/cli.c index b402f6c5..12ed8111 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -292,6 +292,14 @@ static const struct sc_option options[] = { .longopt = "display", .argdesc = "id", }, + { + .longopt_id = OPT_DISPLAY_BUFFER, + .longopt = "display-buffer", + .argdesc = "ms", + .text = "Add a buffering delay (in milliseconds) before displaying. " + "This increases latency to compensate for jitter.\n" + "Default is 0 (no buffering).", + }, { .longopt_id = OPT_DISPLAY_ID, .longopt = "display-id", @@ -301,14 +309,6 @@ static const struct sc_option options[] = { " scrcpy --list-displays\n" "Default is 0.", }, - { - .longopt_id = OPT_DISPLAY_BUFFER, - .longopt = "display-buffer", - .argdesc = "ms", - .text = "Add a buffering delay (in milliseconds) before displaying. " - "This increases latency to compensate for jitter.\n" - "Default is 0 (no buffering).", - }, { .shortopt = 'e', .longopt = "select-tcpip", From 9df92ebe3765d2515f8e561896a239cdf82110bb Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 20 Nov 2023 14:05:21 +0100 Subject: [PATCH 1700/2244] Fix manpage style syntax --- app/scrcpy.1 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 10c32ca1..18941190 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -26,7 +26,7 @@ Encode the audio at the given bit rate, expressed in bits/s. Unit suffixes are s Default is 128K (128000). .TP -.BI "\-\-audio\-buffer ms +.BI "\-\-audio\-buffer " ms Configure the audio buffering delay (in milliseconds). Lower values decrease the latency, but increase the likelyhood of buffer underrun (causing audio glitches). @@ -62,7 +62,7 @@ Select the audio source (output or mic). Default is output. .TP -.BI "\-\-audio\-output\-buffer ms +.BI "\-\-audio\-output\-buffer " ms Configure the size of the SDL audio output buffer (in milliseconds). If you get "robotic" audio playback, you should test with a higher value (10). Do not change this setting otherwise. @@ -128,7 +128,7 @@ Also see \fB\-e\fR (\fB\-\-select\-tcpip\fR). Disable screensaver while scrcpy is running. .TP -.BI "\-\-display\-buffer ms +.BI "\-\-display\-buffer " ms Add a buffering delay (in milliseconds) before displaying. This increases latency to compensate for jitter. Default is 0 (no buffering). From 25e33566f5bca3b052dbb2b4262be96cd8f71caa Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 21 Nov 2023 08:41:09 +0100 Subject: [PATCH 1701/2244] Mention turning off audio in camera documentation --- doc/camera.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/doc/camera.md b/doc/camera.md index d1008bda..e11ad521 100644 --- a/doc/camera.md +++ b/doc/camera.md @@ -18,6 +18,17 @@ scrcpy --video-source=display --audio-source=mic # force display AND micropho scrcpy --video-source=camera --audio-source=output # force camera AND device audio output ``` +Audio can be disabled: + +```bash +# audio not captured at all +scrcpy --video-source=camera --no-audio +scrcpy --video-source=camera --no-audio --record=file.mp4 + +# audio captured and recorded, but not played +scrcpy --video-source=camera --no-audio-playback --record=file.mp4 +``` + ## List From bb88b60227427959b931b5a4417202a85c5b0cdc Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 19 Nov 2023 01:06:59 +0100 Subject: [PATCH 1702/2244] Add --display-orientation Deprecate the option --rotation and introduce a new option --display-orientation with the 8 possible orientations (0, 90, 180, 270, flip0, flip90, flip180 and flip270). New shortcuts MOD+Shift+(arrow) dynamically change the display (horizontal or vertical) flip. Fixes #1380 Fixes #3819 PR #4441 --- app/data/bash-completion/scrcpy | 9 ++-- app/data/zsh-completion/_scrcpy | 2 +- app/meson.build | 4 ++ app/scrcpy.1 | 20 +++++-- app/src/cli.c | 92 +++++++++++++++++++++++++++++++-- app/src/display.c | 16 +++--- app/src/display.h | 3 +- app/src/input_manager.c | 48 +++++++++++------ app/src/options.c | 38 +++++++++++++- app/src/options.h | 64 ++++++++++++++++++++++- app/src/scrcpy.c | 2 +- app/src/screen.c | 86 +++++++++++++++++------------- app/src/screen.h | 12 +++-- app/tests/test_orientation.c | 91 ++++++++++++++++++++++++++++++++ doc/shortcuts.md | 2 + 15 files changed, 409 insertions(+), 80 deletions(-) create mode 100644 app/tests/test_orientation.c diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index f94a70a6..5e359f4f 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -21,6 +21,7 @@ _scrcpy() { --disable-screensaver --display-buffer= --display-id= + --display-orientation= -e --select-tcpip -f --fullscreen --force-adb-forward @@ -112,6 +113,10 @@ _scrcpy() { COMPREPLY=($(compgen -W 'front back external' -- "$cur")) return ;; + --display-orientation) + COMPREPLY=($(compgen -> '0 90 180 270 flip0 flip90 flip180 flip270' -- "$cur")) + return + ;; --lock-video-orientation) COMPREPLY=($(compgen -W 'unlocked initial 0 1 2 3' -- "$cur")) return @@ -132,10 +137,6 @@ _scrcpy() { COMPREPLY=($(compgen -W 'direct3d opengl opengles2 opengles metal software' -- "$cur")) return ;; - --rotation) - COMPREPLY=($(compgen -W '0 1 2 3' -- "$cur")) - return - ;; --shortcut-mod) # Only auto-complete a single key COMPREPLY=($(compgen -W 'lctrl rctrl lalt ralt lsuper rsuper' -- "$cur")) diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index cc58b866..16729d2a 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -28,6 +28,7 @@ arguments=( '--disable-screensaver[Disable screensaver while scrcpy is running]' '--display-buffer=[Add a buffering delay \(in milliseconds\) before displaying]' '--display-id=[Specify the display id to mirror]' + '--display-orientation=[Set the initial display orientation]:orientation values:(0 90 180 270 flip0 flip90 flip180 flip270)' {-e,--select-tcpip}'[Use TCP/IP device]' {-f,--fullscreen}'[Start in fullscreen]' '--force-adb-forward[Do not attempt to use \"adb reverse\" to connect to the device]' @@ -68,7 +69,6 @@ arguments=( '--record-format=[Force recording format]:format:(mp4 mkv m4a mka opus aac flac wav)' '--render-driver=[Request SDL to use the given render driver]:driver name:(direct3d opengl opengles2 opengles metal software)' '--require-audio=[Make scrcpy fail if audio is enabled but does not work]' - '--rotation=[Set the initial display rotation]:rotation values:(0 1 2 3)' {-s,--serial=}'[The device serial number \(mandatory for multiple devices only\)]:serial:($("${ADB-adb}" devices | awk '\''$2 == "device" {print $1}'\''))' {-S,--turn-screen-off}'[Turn the device screen off immediately]' '--shortcut-mod=[\[key1,key2+key3,...\] Specify the modifiers to use for scrcpy shortcuts]:shortcut mod:(lctrl rctrl lalt ralt lsuper rsuper)' diff --git a/app/meson.build b/app/meson.build index e0d92050..b1233c6b 100644 --- a/app/meson.build +++ b/app/meson.build @@ -289,6 +289,10 @@ if get_option('buildtype') == 'debug' 'tests/test_device_msg_deserialize.c', 'src/device_msg.c', ]], + ['test_orientation', [ + 'tests/test_orientation.c', + 'src/options.c', + ]], ['test_strbuf', [ 'tests/test_strbuf.c', 'src/util/strbuf.c', diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 18941190..08a366ee 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -141,6 +141,14 @@ The available display ids can be listed by \fB\-\-list\-displays\fR. Default is 0. +.TP +.BI "\-\-display\-orientation " value +Set the initial display orientation. + +Possible values are 0, 90, 180, 270, flip0, flip90, flip180 and flip270. The number represents the clockwise rotation in degrees; the "flip" keyword applies a horizontal flip before the rotation. + +Default is 0. + .TP .B \-e, \-\-select\-tcpip Use TCP/IP device (if there is exactly one, like adb -e). @@ -369,10 +377,6 @@ Supported names are currently "direct3d", "opengl", "opengles2", "opengles", "me .B \-\-require\-audio By default, scrcpy mirrors only the video if audio capture fails on the device. This option makes scrcpy fail if audio is enabled but does not work. -.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. @@ -534,6 +538,14 @@ Rotate display left .B MOD+Right Rotate display right +.TP +.B MOD+Shift+Left, MOD+Shift+Right +Flip display horizontally + +.TP +.B MOD+Shift+Up, MOD+Shift+Down +Flip display vertically + .TP .B MOD+g Resize window to 1:1 (pixel\-perfect) diff --git a/app/src/cli.c b/app/src/cli.c index 12ed8111..668de31d 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -90,6 +90,7 @@ enum { OPT_CAMERA_AR, OPT_CAMERA_FPS, OPT_CAMERA_HIGH_SPEED, + OPT_DISPLAY_ORIENTATION, }; struct sc_option { @@ -309,6 +310,17 @@ static const struct sc_option options[] = { " scrcpy --list-displays\n" "Default is 0.", }, + { + .longopt_id = OPT_DISPLAY_ORIENTATION, + .longopt = "display-orientation", + .argdesc = "value", + .text = "Set the initial display orientation.\n" + "Possible values are 0, 90, 180, 270, flip0, flip90, flip180 " + "and flip270. The number represents the clockwise rotation " + "in degrees; the \"flip\" keyword applies a horizontal flip " + "before the rotation.\n" + "Default is 0.", + }, { .shortopt = 'e', .longopt = "select-tcpip", @@ -615,12 +627,10 @@ static const struct sc_option options[] = { "is enabled but does not work." }, { + // deprecated .longopt_id = OPT_ROTATION, .longopt = "rotation", .argdesc = "value", - .text = "Set the initial display rotation.\n" - "Possible values are 0, 1, 2 and 3. Each increment adds a 90 " - "degrees rotation counterclockwise.", }, { .shortopt = 's', @@ -824,6 +834,14 @@ static const struct sc_shortcut shortcuts[] = { .shortcuts = { "MOD+Right" }, .text = "Rotate display right", }, + { + .shortcuts = { "MOD+Shift+Left", "MOD+Shift+Right" }, + .text = "Flip display horizontally", + }, + { + .shortcuts = { "MOD+Shift+Up", "MOD+Shift+Down" }, + .text = "Flip display vertically", + }, { .shortcuts = { "MOD+g" }, .text = "Resize window to 1:1 (pixel-perfect)", @@ -1405,6 +1423,45 @@ parse_rotation(const char *s, uint8_t *rotation) { return true; } +static bool +parse_orientation(const char *s, enum sc_orientation *orientation) { + if (!strcmp(s, "0")) { + *orientation = SC_ORIENTATION_0; + return true; + } + if (!strcmp(s, "90")) { + *orientation = SC_ORIENTATION_90; + return true; + } + if (!strcmp(s, "180")) { + *orientation = SC_ORIENTATION_180; + return true; + } + if (!strcmp(s, "270")) { + *orientation = SC_ORIENTATION_270; + return true; + } + if (!strcmp(s, "flip0")) { + *orientation = SC_ORIENTATION_FLIP_0; + return true; + } + if (!strcmp(s, "flip90")) { + *orientation = SC_ORIENTATION_FLIP_90; + return true; + } + if (!strcmp(s, "flip180")) { + *orientation = SC_ORIENTATION_FLIP_180; + return true; + } + if (!strcmp(s, "flip270")) { + *orientation = SC_ORIENTATION_FLIP_270; + return true; + } + LOGE("Unsupported orientation: %s (expected 0, 90, 180, 270, flip0, " + "flip90, flip180 or flip270)", optarg); + return false; +} + static bool parse_window_position(const char *s, int16_t *position) { // special value for "auto" @@ -2008,7 +2065,34 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], opts->key_inject_mode = SC_KEY_INJECT_MODE_RAW; break; case OPT_ROTATION: - if (!parse_rotation(optarg, &opts->rotation)) { + LOGW("--rotation is deprecated, use --display-orientation " + "instead."); + uint8_t rotation; + if (!parse_rotation(optarg, &rotation)) { + return false; + } + assert(rotation <= 3); + switch (rotation) { + case 0: + opts->display_orientation = SC_ORIENTATION_0; + break; + case 1: + // rotation 1 was 90° counterclockwise, but orientation + // is expressed clockwise + opts->display_orientation = SC_ORIENTATION_270; + break; + case 2: + opts->display_orientation = SC_ORIENTATION_180; + break; + case 3: + // rotation 3 was 270° counterclockwise, but orientation + // is expressed clockwise + opts->display_orientation = SC_ORIENTATION_90; + break; + } + break; + case OPT_DISPLAY_ORIENTATION: + if (!parse_orientation(optarg, &opts->display_orientation)) { return false; } break; diff --git a/app/src/display.c b/app/src/display.c index cf26e776..906b5d65 100644 --- a/app/src/display.c +++ b/app/src/display.c @@ -234,7 +234,7 @@ sc_display_update_texture(struct sc_display *display, const AVFrame *frame) { enum sc_display_result sc_display_render(struct sc_display *display, const SDL_Rect *geometry, - unsigned rotation) { + enum sc_orientation orientation) { SDL_RenderClear(display->renderer); if (display->pending.flags) { @@ -247,33 +247,33 @@ sc_display_render(struct sc_display *display, const SDL_Rect *geometry, SDL_Renderer *renderer = display->renderer; SDL_Texture *texture = display->texture; - if (rotation == 0) { + if (orientation == SC_ORIENTATION_0) { int ret = SDL_RenderCopy(renderer, texture, NULL, geometry); if (ret) { LOGE("Could not render texture: %s", SDL_GetError()); return SC_DISPLAY_RESULT_ERROR; } } else { - // rotation in RenderCopyEx() is clockwise, while screen->rotation is - // counterclockwise (to be consistent with --lock-video-orientation) - int cw_rotation = (4 - rotation) % 4; + unsigned cw_rotation = sc_orientation_get_rotation(orientation); double angle = 90 * cw_rotation; const SDL_Rect *dstrect = NULL; SDL_Rect rect; - if (rotation & 1) { + if (sc_orientation_is_swap(orientation)) { rect.x = geometry->x + (geometry->w - geometry->h) / 2; rect.y = geometry->y + (geometry->h - geometry->w) / 2; rect.w = geometry->h; rect.h = geometry->w; dstrect = ▭ } else { - assert(rotation == 2); dstrect = geometry; } + SDL_RendererFlip flip = sc_orientation_is_mirror(orientation) + ? SDL_FLIP_HORIZONTAL : 0; + int ret = SDL_RenderCopyEx(renderer, texture, NULL, dstrect, angle, - NULL, 0); + NULL, flip); if (ret) { LOGE("Could not render texture: %s", SDL_GetError()); return SC_DISPLAY_RESULT_ERROR; diff --git a/app/src/display.h b/app/src/display.h index 6b83a5c9..643ce73c 100644 --- a/app/src/display.h +++ b/app/src/display.h @@ -9,6 +9,7 @@ #include "coords.h" #include "opengl.h" +#include "options.h" #ifdef __APPLE__ # define SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE @@ -54,6 +55,6 @@ sc_display_update_texture(struct sc_display *display, const AVFrame *frame); enum sc_display_result sc_display_render(struct sc_display *display, const SDL_Rect *geometry, - unsigned rotation); + enum sc_orientation orientation); #endif diff --git a/app/src/input_manager.c b/app/src/input_manager.c index c9e83d48..9a487836 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -293,15 +293,11 @@ rotate_device(struct sc_controller *controller) { } static void -rotate_client_left(struct sc_screen *screen) { - unsigned new_rotation = (screen->rotation + 1) % 4; - sc_screen_set_rotation(screen, new_rotation); -} - -static void -rotate_client_right(struct sc_screen *screen) { - unsigned new_rotation = (screen->rotation + 3) % 4; - sc_screen_set_rotation(screen, new_rotation); +apply_orientation_transform(struct sc_screen *screen, + enum sc_orientation transform) { + enum sc_orientation new_orientation = + sc_orientation_apply(screen->orientation, transform); + sc_screen_set_orientation(screen, new_orientation); } static void @@ -421,25 +417,47 @@ sc_input_manager_process_key(struct sc_input_manager *im, } return; case SDLK_DOWN: - if (controller && !shift) { + if (shift) { + if (!repeat & down) { + apply_orientation_transform(im->screen, + SC_ORIENTATION_FLIP_180); + } + } else if (controller) { // forward repeated events action_volume_down(controller, action); } return; case SDLK_UP: - if (controller && !shift) { + if (shift) { + if (!repeat & down) { + apply_orientation_transform(im->screen, + SC_ORIENTATION_FLIP_180); + } + } else if (controller) { // forward repeated events action_volume_up(controller, action); } return; case SDLK_LEFT: - if (!shift && !repeat && down) { - rotate_client_left(im->screen); + if (!repeat && down) { + if (shift) { + apply_orientation_transform(im->screen, + SC_ORIENTATION_FLIP_0); + } else { + apply_orientation_transform(im->screen, + SC_ORIENTATION_270); + } } return; case SDLK_RIGHT: - if (!shift && !repeat && down) { - rotate_client_right(im->screen); + if (!repeat && down) { + if (shift) { + apply_orientation_transform(im->screen, + SC_ORIENTATION_FLIP_0); + } else { + apply_orientation_transform(im->screen, + SC_ORIENTATION_90); + } } return; case SDLK_c: diff --git a/app/src/options.c b/app/src/options.c index 092fbd56..1454147a 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -39,7 +39,7 @@ const struct scrcpy_options scrcpy_options_default = { .audio_bit_rate = 0, .max_fps = 0, .lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_UNLOCKED, - .rotation = 0, + .display_orientation = SC_ORIENTATION_0, .window_x = SC_WINDOW_POSITION_UNDEFINED, .window_y = SC_WINDOW_POSITION_UNDEFINED, .window_width = 0, @@ -89,3 +89,39 @@ const struct scrcpy_options scrcpy_options_default = { .camera_high_speed = false, .list = 0, }; + +enum sc_orientation +sc_orientation_apply(enum sc_orientation src, enum sc_orientation transform) { + assert(!(src & ~7)); + assert(!(transform & ~7)); + + unsigned transform_hflip = transform & 4; + unsigned transform_rotation = transform & 3; + unsigned src_hflip = src & 4; + unsigned src_rotation = src & 3; + unsigned src_swap = src & 1; + if (src_swap && transform_hflip) { + // If the src is rotated by 90 or 270 degrees, applying a flipped + // transformation requires an additional 180 degrees rotation to + // compensate for the inversion of the order of multiplication: + // + // hflip1 × rotate1 × hflip2 × rotate2 + // `--------------' `--------------' + // src transform + // + // In the final result, we want all the hflips then all the rotations, + // so we must move hflip2 to the left: + // + // hflip1 × hflip2 × rotate1' × rotate2 + // + // with rotate1' = | rotate1 if src is 0° or 180° + // | rotate1 + 180° if src is 90° or 270° + + src_rotation += 2; + } + + unsigned result_hflip = src_hflip ^ transform_hflip; + unsigned result_rotation = (transform_rotation + src_rotation) % 4; + enum sc_orientation result = result_hflip | result_rotation; + return result; +} diff --git a/app/src/options.h b/app/src/options.h index c702ceeb..5a6c3276 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -3,6 +3,7 @@ #include "common.h" +#include #include #include #include @@ -67,6 +68,67 @@ enum sc_camera_facing { SC_CAMERA_FACING_EXTERNAL, }; + // ,----- hflip (applied before the rotation) + // | ,--- 180° + // | | ,- 90° clockwise + // | | | +enum sc_orientation { // v v v + SC_ORIENTATION_0, // 0 0 0 + SC_ORIENTATION_90, // 0 0 1 + SC_ORIENTATION_180, // 0 1 0 + SC_ORIENTATION_270, // 0 1 1 + SC_ORIENTATION_FLIP_0, // 1 0 0 + SC_ORIENTATION_FLIP_90, // 1 0 1 + SC_ORIENTATION_FLIP_180, // 1 1 0 + SC_ORIENTATION_FLIP_270, // 1 1 1 +}; + +static inline bool +sc_orientation_is_mirror(enum sc_orientation orientation) { + assert(!(orientation & ~7)); + return orientation & 4; +} + +// Does the orientation swap width and height? +static inline bool +sc_orientation_is_swap(enum sc_orientation orientation) { + assert(!(orientation & ~7)); + return orientation & 1; +} + +static inline enum sc_orientation +sc_orientation_get_rotation(enum sc_orientation orientation) { + assert(!(orientation & ~7)); + return orientation & 3; +} + +enum sc_orientation +sc_orientation_apply(enum sc_orientation src, enum sc_orientation transform); + +static inline const char * +sc_orientation_get_name(enum sc_orientation orientation) { + switch (orientation) { + case SC_ORIENTATION_0: + return "0"; + case SC_ORIENTATION_90: + return "90"; + case SC_ORIENTATION_180: + return "180"; + case SC_ORIENTATION_270: + return "270"; + case SC_ORIENTATION_FLIP_0: + return "flip0"; + case SC_ORIENTATION_FLIP_90: + return "flip90"; + case SC_ORIENTATION_FLIP_180: + return "flip180"; + case SC_ORIENTATION_FLIP_270: + return "flip270"; + default: + return "(unknown)"; + } +} + enum sc_lock_video_orientation { SC_LOCK_VIDEO_ORIENTATION_UNLOCKED = -1, // lock the current orientation when scrcpy starts @@ -157,7 +219,7 @@ struct scrcpy_options { uint32_t audio_bit_rate; uint16_t max_fps; enum sc_lock_video_orientation lock_video_orientation; - uint8_t rotation; + enum sc_orientation display_orientation; int16_t window_x; // SC_WINDOW_POSITION_UNDEFINED for "auto" int16_t window_y; // SC_WINDOW_POSITION_UNDEFINED for "auto" uint16_t window_width; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index ac2b8e33..9bbe14b8 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -691,7 +691,7 @@ aoa_hid_end: .window_width = options->window_width, .window_height = options->window_height, .window_borderless = options->window_borderless, - .rotation = options->rotation, + .orientation = options->display_orientation, .mipmaps = options->mipmaps, .fullscreen = options->fullscreen, .start_fps_counter = options->start_fps_counter, diff --git a/app/src/screen.c b/app/src/screen.c index 5b7a8808..091001bc 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -14,16 +14,16 @@ #define DOWNCAST(SINK) container_of(SINK, struct sc_screen, frame_sink) static inline struct sc_size -get_rotated_size(struct sc_size size, int rotation) { - struct sc_size rotated_size; - if (rotation & 1) { - rotated_size.width = size.height; - rotated_size.height = size.width; +get_oriented_size(struct sc_size size, enum sc_orientation orientation) { + struct sc_size oriented_size; + if (sc_orientation_is_swap(orientation)) { + oriented_size.width = size.height; + oriented_size.height = size.width; } else { - rotated_size.width = size.width; - rotated_size.height = size.height; + oriented_size.width = size.width; + oriented_size.height = size.height; } - return rotated_size; + return oriented_size; } // get the window size in a struct sc_size @@ -251,7 +251,7 @@ sc_screen_render(struct sc_screen *screen, bool update_content_rect) { } enum sc_display_result res = - sc_display_render(&screen->display, &screen->rect, screen->rotation); + sc_display_render(&screen->display, &screen->rect, screen->orientation); (void) res; // any error already logged } @@ -379,9 +379,10 @@ sc_screen_init(struct sc_screen *screen, goto error_destroy_frame_buffer; } - screen->rotation = params->rotation; - if (screen->rotation) { - LOGI("Initial display rotation set to %u", screen->rotation); + screen->orientation = params->orientation; + if (screen->orientation != SC_ORIENTATION_0) { + LOGI("Initial display orientation set to %s", + sc_orientation_get_name(screen->orientation)); } uint32_t window_flags = SDL_WINDOW_HIDDEN @@ -559,19 +560,19 @@ apply_pending_resize(struct sc_screen *screen) { } void -sc_screen_set_rotation(struct sc_screen *screen, unsigned rotation) { - assert(rotation < 4); - if (rotation == screen->rotation) { +sc_screen_set_orientation(struct sc_screen *screen, + enum sc_orientation orientation) { + if (orientation == screen->orientation) { return; } struct sc_size new_content_size = - get_rotated_size(screen->frame_size, rotation); + get_oriented_size(screen->frame_size, orientation); set_content_size(screen, new_content_size); - screen->rotation = rotation; - LOGI("Display rotation set to %u", rotation); + screen->orientation = orientation; + LOGI("Display orientation set to %s", sc_orientation_get_name(orientation)); sc_screen_render(screen, true); } @@ -584,7 +585,7 @@ sc_screen_init_size(struct sc_screen *screen) { // The requested size is passed via screen->frame_size struct sc_size content_size = - get_rotated_size(screen->frame_size, screen->rotation); + get_oriented_size(screen->frame_size, screen->orientation); screen->content_size = content_size; enum sc_display_result res = @@ -604,7 +605,7 @@ prepare_for_frame(struct sc_screen *screen, struct sc_size new_frame_size) { screen->frame_size = new_frame_size; struct sc_size new_content_size = - get_rotated_size(new_frame_size, screen->rotation); + get_oriented_size(new_frame_size, screen->orientation); set_content_size(screen, new_content_size); sc_screen_update_content_rect(screen); @@ -843,8 +844,7 @@ sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) { struct sc_point sc_screen_convert_drawable_to_frame_coords(struct sc_screen *screen, int32_t x, int32_t y) { - unsigned rotation = screen->rotation; - assert(rotation < 4); + enum sc_orientation orientation = screen->orientation; int32_t w = screen->content_size.width; int32_t h = screen->content_size.height; @@ -855,27 +855,43 @@ sc_screen_convert_drawable_to_frame_coords(struct sc_screen *screen, x = (int64_t) (x - screen->rect.x) * w / screen->rect.w; y = (int64_t) (y - screen->rect.y) * h / screen->rect.h; - // rotate struct sc_point result; - switch (rotation) { - case 0: + switch (orientation) { + case SC_ORIENTATION_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); + case SC_ORIENTATION_90: result.x = y; result.y = w - x; break; + case SC_ORIENTATION_180: + result.x = w - x; + result.y = h - y; + break; + case SC_ORIENTATION_270: + result.x = h - y; + result.y = x; + break; + case SC_ORIENTATION_FLIP_0: + result.x = w - x; + result.y = y; + break; + case SC_ORIENTATION_FLIP_90: + result.x = h - y; + result.y = w - x; + break; + case SC_ORIENTATION_FLIP_180: + result.x = x; + result.y = h - y; + break; + default: + assert(orientation == SC_ORIENTATION_FLIP_270); + result.x = y; + result.y = x; + break; } + return result; } diff --git a/app/src/screen.h b/app/src/screen.h index acbaab4b..46591be5 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -14,6 +14,7 @@ #include "frame_buffer.h" #include "input_manager.h" #include "opengl.h" +#include "options.h" #include "trait/key_processor.h" #include "trait/frame_sink.h" #include "trait/mouse_processor.h" @@ -49,8 +50,8 @@ struct sc_screen { // fullscreen (meaningful only when resize_pending is true) struct sc_size windowed_content_size; - // client rotation: 0, 1, 2 or 3 (x90 degrees counterclockwise) - unsigned rotation; + // client orientation + enum sc_orientation orientation; // rectangle of the content (excluding black borders) struct SDL_Rect rect; bool has_frame; @@ -86,7 +87,7 @@ struct sc_screen_params { bool window_borderless; - uint8_t rotation; + enum sc_orientation orientation; bool mipmaps; bool fullscreen; @@ -129,9 +130,10 @@ sc_screen_resize_to_fit(struct sc_screen *screen); void sc_screen_resize_to_pixel_perfect(struct sc_screen *screen); -// set the display rotation (0, 1, 2 or 3, x90 degrees counterclockwise) +// set the display orientation void -sc_screen_set_rotation(struct sc_screen *screen, unsigned rotation); +sc_screen_set_orientation(struct sc_screen *screen, + enum sc_orientation orientation); // react to SDL events // If this function returns false, scrcpy must exit with an error. diff --git a/app/tests/test_orientation.c b/app/tests/test_orientation.c new file mode 100644 index 00000000..153211fa --- /dev/null +++ b/app/tests/test_orientation.c @@ -0,0 +1,91 @@ +#include "common.h" + +#include + +#include "options.h" + +static void test_transforms(void) { + #define O(X) SC_ORIENTATION_ ## X + #define ASSERT_TRANSFORM(SRC, TR, RES) \ + assert(sc_orientation_apply(O(SRC), O(TR)) == O(RES)); + + ASSERT_TRANSFORM(0, 0, 0); + ASSERT_TRANSFORM(0, 90, 90); + ASSERT_TRANSFORM(0, 180, 180); + ASSERT_TRANSFORM(0, 270, 270); + ASSERT_TRANSFORM(0, FLIP_0, FLIP_0); + ASSERT_TRANSFORM(0, FLIP_90, FLIP_90); + ASSERT_TRANSFORM(0, FLIP_180, FLIP_180); + ASSERT_TRANSFORM(0, FLIP_270, FLIP_270); + + ASSERT_TRANSFORM(90, 0, 90); + ASSERT_TRANSFORM(90, 90, 180); + ASSERT_TRANSFORM(90, 180, 270); + ASSERT_TRANSFORM(90, 270, 0); + ASSERT_TRANSFORM(90, FLIP_0, FLIP_270); + ASSERT_TRANSFORM(90, FLIP_90, FLIP_0); + ASSERT_TRANSFORM(90, FLIP_180, FLIP_90); + ASSERT_TRANSFORM(90, FLIP_270, FLIP_180); + + ASSERT_TRANSFORM(180, 0, 180); + ASSERT_TRANSFORM(180, 90, 270); + ASSERT_TRANSFORM(180, 180, 0); + ASSERT_TRANSFORM(180, 270, 90); + ASSERT_TRANSFORM(180, FLIP_0, FLIP_180); + ASSERT_TRANSFORM(180, FLIP_90, FLIP_270); + ASSERT_TRANSFORM(180, FLIP_180, FLIP_0); + ASSERT_TRANSFORM(180, FLIP_270, FLIP_90); + + ASSERT_TRANSFORM(270, 0, 270); + ASSERT_TRANSFORM(270, 90, 0); + ASSERT_TRANSFORM(270, 180, 90); + ASSERT_TRANSFORM(270, 270, 180); + ASSERT_TRANSFORM(270, FLIP_0, FLIP_90); + ASSERT_TRANSFORM(270, FLIP_90, FLIP_180); + ASSERT_TRANSFORM(270, FLIP_180, FLIP_270); + ASSERT_TRANSFORM(270, FLIP_270, FLIP_0); + + ASSERT_TRANSFORM(FLIP_0, 0, FLIP_0); + ASSERT_TRANSFORM(FLIP_0, 90, FLIP_90); + ASSERT_TRANSFORM(FLIP_0, 180, FLIP_180); + ASSERT_TRANSFORM(FLIP_0, 270, FLIP_270); + ASSERT_TRANSFORM(FLIP_0, FLIP_0, 0); + ASSERT_TRANSFORM(FLIP_0, FLIP_90, 90); + ASSERT_TRANSFORM(FLIP_0, FLIP_180, 180); + ASSERT_TRANSFORM(FLIP_0, FLIP_270, 270); + + ASSERT_TRANSFORM(FLIP_90, 0, FLIP_90); + ASSERT_TRANSFORM(FLIP_90, 90, FLIP_180); + ASSERT_TRANSFORM(FLIP_90, 180, FLIP_270); + ASSERT_TRANSFORM(FLIP_90, 270, FLIP_0); + ASSERT_TRANSFORM(FLIP_90, FLIP_0, 270); + ASSERT_TRANSFORM(FLIP_90, FLIP_90, 0); + ASSERT_TRANSFORM(FLIP_90, FLIP_180, 90); + ASSERT_TRANSFORM(FLIP_90, FLIP_270, 180); + + ASSERT_TRANSFORM(FLIP_180, 0, FLIP_180); + ASSERT_TRANSFORM(FLIP_180, 90, FLIP_270); + ASSERT_TRANSFORM(FLIP_180, 180, FLIP_0); + ASSERT_TRANSFORM(FLIP_180, 270, FLIP_90); + ASSERT_TRANSFORM(FLIP_180, FLIP_0, 180); + ASSERT_TRANSFORM(FLIP_180, FLIP_90, 270); + ASSERT_TRANSFORM(FLIP_180, FLIP_180, 0); + ASSERT_TRANSFORM(FLIP_180, FLIP_270, 90); + + ASSERT_TRANSFORM(FLIP_270, 0, FLIP_270); + ASSERT_TRANSFORM(FLIP_270, 90, FLIP_0); + ASSERT_TRANSFORM(FLIP_270, 180, FLIP_90); + ASSERT_TRANSFORM(FLIP_270, 270, FLIP_180); + ASSERT_TRANSFORM(FLIP_270, FLIP_0, 90); + ASSERT_TRANSFORM(FLIP_270, FLIP_90, 180); + ASSERT_TRANSFORM(FLIP_270, FLIP_180, 270); + ASSERT_TRANSFORM(FLIP_270, FLIP_270, 0); +} + +int main(int argc, char *argv[]) { + (void) argc; + (void) argv; + + test_transforms(); + return 0; +} diff --git a/doc/shortcuts.md b/doc/shortcuts.md index 5e706402..c0fc2842 100644 --- a/doc/shortcuts.md +++ b/doc/shortcuts.md @@ -26,6 +26,8 @@ _[Super] is typically the Windows or Cmd key._ | Switch fullscreen mode | MOD+f | Rotate display left | MOD+ _(left)_ | Rotate display right | MOD+ _(right)_ + | Flip display horizontally | MOD+Shift+ _(left)_ \| MOD+Shift+ _(right)_ + | Flip display vertically | MOD+Shift+ _(up)_ \| MOD+Shift+ _(down)_ | Resize window to 1:1 (pixel-perfect) | MOD+g | Resize window to remove black borders | MOD+w \| _Double-left-click¹_ | Click on `HOME` | MOD+h \| _Middle-click_ From 2f926869300fdcdc1c668994739d0c379a9d525d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 20 Nov 2023 13:49:14 +0100 Subject: [PATCH 1703/2244] Pass --lock-video-orientation argument in degrees For consistency with the new --display-orientation option, express the --lock-video-orientation in degrees clockwise: * --lock-video-orientation=0 -> --lock-video-orientation=0 * --lock-video-orientation=3 -> --lock-video-orientation=90 * --lock-video-orientation=2 -> --lock-video-orientation=180 * --lock-video-orientation=1 -> --lock-video-orientation=270 PR #4441 --- app/data/bash-completion/scrcpy | 2 +- app/data/zsh-completion/_scrcpy | 2 +- app/scrcpy.1 | 4 ++- app/src/cli.c | 57 ++++++++++++++++++++++++++------- app/src/options.h | 6 ++-- 5 files changed, 54 insertions(+), 17 deletions(-) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index 5e359f4f..0ecace96 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -118,7 +118,7 @@ _scrcpy() { return ;; --lock-video-orientation) - COMPREPLY=($(compgen -W 'unlocked initial 0 1 2 3' -- "$cur")) + COMPREPLY=($(compgen -W 'unlocked initial 0 90 180 270' -- "$cur")) return ;; --pause-on-exit) diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 16729d2a..3f65cb4e 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -41,7 +41,7 @@ arguments=( '--list-cameras[List cameras available on the device]' '--list-displays[List displays available on the device]' '--list-encoders[List video and audio encoders available on the device]' - '--lock-video-orientation=[Lock video orientation]:orientation:(unlocked initial 0 1 2 3)' + '--lock-video-orientation=[Lock video orientation]:orientation:(unlocked initial 0 90 180 270)' {-m,--max-size=}'[Limit both the width and height of the video to value]' {-M,--hid-mouse}'[Simulate a physical mouse by using HID over AOAv2]' '--max-fps=[Limit the frame rate of screen capture]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 08a366ee..266ba1f4 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -215,7 +215,9 @@ List displays available on the device. .TP \fB\-\-lock\-video\-orientation\fR[=\fIvalue\fR] -Lock video orientation to \fIvalue\fR. Possible values are "unlocked", "initial" (locked to the initial orientation), 0, 1, 2 and 3. Natural device orientation is 0, and each increment adds a 90 degrees rotation counterclockwise. +Lock capture video orientation to \fIvalue\fR. + +Possible values are "unlocked", "initial" (locked to the initial orientation), 0, 90, 180, and 270. The values represent the clockwise rotation from the natural device orientation, in degrees. Default is "unlocked". diff --git a/app/src/cli.c b/app/src/cli.c index 668de31d..37c2274c 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -411,11 +411,11 @@ static const struct sc_option options[] = { .longopt = "lock-video-orientation", .argdesc = "value", .optional_arg = true, - .text = "Lock video orientation to value.\n" + .text = "Lock capture video orientation to value.\n" "Possible values are \"unlocked\", \"initial\" (locked to the " - "initial orientation), 0, 1, 2 and 3. Natural device " - "orientation is 0, and each increment adds a 90 degrees " - "rotation counterclockwise.\n" + "initial orientation), 0, 90, 180 and 270. The values " + "represent the clockwise rotation from the natural device " + "orientation, in degrees.\n" "Default is \"unlocked\".\n" "Passing the option without argument is equivalent to passing " "\"initial\".", @@ -1400,15 +1400,50 @@ parse_lock_video_orientation(const char *s, return true; } - long value; - bool ok = parse_integer_arg(s, &value, false, 0, 3, - "lock video orientation"); - if (!ok) { - return false; + if (!strcmp(s, "0")) { + *lock_mode = SC_LOCK_VIDEO_ORIENTATION_0; + return true; } - *lock_mode = (enum sc_lock_video_orientation) value; - return true; + if (!strcmp(s, "90")) { + *lock_mode = SC_LOCK_VIDEO_ORIENTATION_90; + return true; + } + + if (!strcmp(s, "180")) { + *lock_mode = SC_LOCK_VIDEO_ORIENTATION_180; + return true; + } + + if (!strcmp(s, "270")) { + *lock_mode = SC_LOCK_VIDEO_ORIENTATION_270; + return true; + } + + if (!strcmp(s, "1")) { + LOGW("--lock-video-orientation=1 is deprecated, use " + "--lock-video-orientation=270 instead."); + *lock_mode = SC_LOCK_VIDEO_ORIENTATION_270; + return true; + } + + if (!strcmp(s, "2")) { + LOGW("--lock-video-orientation=2 is deprecated, use " + "--lock-video-orientation=180 instead."); + *lock_mode = SC_LOCK_VIDEO_ORIENTATION_180; + return true; + } + + if (!strcmp(s, "3")) { + LOGW("--lock-video-orientation=3 is deprecated, use " + "--lock-video-orientation=90 instead."); + *lock_mode = SC_LOCK_VIDEO_ORIENTATION_90; + return true; + } + + LOGE("Unsupported --lock-video-orientation value: %s (expected initial, " + "unlocked, 0, 90, 180 or 270).", s); + return false; } static bool diff --git a/app/src/options.h b/app/src/options.h index 5a6c3276..4fb45840 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -134,9 +134,9 @@ enum sc_lock_video_orientation { // lock the current orientation when scrcpy starts SC_LOCK_VIDEO_ORIENTATION_INITIAL = -2, SC_LOCK_VIDEO_ORIENTATION_0 = 0, - SC_LOCK_VIDEO_ORIENTATION_1, - SC_LOCK_VIDEO_ORIENTATION_2, - SC_LOCK_VIDEO_ORIENTATION_3, + SC_LOCK_VIDEO_ORIENTATION_90 = 3, + SC_LOCK_VIDEO_ORIENTATION_180 = 2, + SC_LOCK_VIDEO_ORIENTATION_270 = 1, }; enum sc_keyboard_input_mode { From a9d6cb58374e27d34988db2245bfddf9402ad57d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 20 Nov 2023 14:02:46 +0100 Subject: [PATCH 1704/2244] Add --record-orientation Add an option to store the orientation to apply in a recorded file. Only rotations are supported (not flips). PR #4441 --- app/data/bash-completion/scrcpy | 5 ++++ app/data/zsh-completion/_scrcpy | 1 + app/scrcpy.1 | 8 +++++ app/src/cli.c | 24 +++++++++++++++ app/src/compat.h | 11 +++++++ app/src/options.c | 1 + app/src/options.h | 1 + app/src/recorder.c | 52 +++++++++++++++++++++++++++++++++ app/src/recorder.h | 3 ++ app/src/scrcpy.c | 3 +- 10 files changed, 108 insertions(+), 1 deletion(-) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index 0ecace96..f08df996 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -62,6 +62,7 @@ _scrcpy() { -r --record= --raw-key-events --record-format= + --record-orientation= --render-driver= --require-audio --rotation= @@ -117,6 +118,10 @@ _scrcpy() { COMPREPLY=($(compgen -> '0 90 180 270 flip0 flip90 flip180 flip270' -- "$cur")) return ;; + --record-orientation) + COMPREPLY=($(compgen -> '0 90 180 270' -- "$cur")) + return + ;; --lock-video-orientation) COMPREPLY=($(compgen -W 'unlocked initial 0 90 180 270' -- "$cur")) return diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 3f65cb4e..0e39f96b 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -67,6 +67,7 @@ arguments=( {-r,--record=}'[Record screen to file]:record file:_files' '--raw-key-events[Inject key events for all input keys, and ignore text events]' '--record-format=[Force recording format]:format:(mp4 mkv m4a mka opus aac flac wav)' + '--record-orientation=[Set the record orientation]:orientation values:(0 90 180 270)' '--render-driver=[Request SDL to use the given render driver]:driver name:(direct3d opengl opengles2 opengles metal software)' '--require-audio=[Make scrcpy fail if audio is enabled but does not work]' {-s,--serial=}'[The device serial number \(mandatory for multiple devices only\)]:serial:($("${ADB-adb}" devices | awk '\''$2 == "device" {print $1}'\''))' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 266ba1f4..1a9386ee 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -367,6 +367,14 @@ Inject key events for all input keys, and ignore text events. .BI "\-\-record\-format " format Force recording format (mp4, mkv, m4a, mka, opus, aac, flac or wav). +.TP +.BI "\-\-record\-orientation " value +Set the record orientation. + +Possible values are 0, 90, 180 and 270. The number represents the clockwise rotation in degrees. + +Default is 0. + .TP .BI "\-\-render\-driver " name Request SDL to use the given render driver (this is just a hint). diff --git a/app/src/cli.c b/app/src/cli.c index 37c2274c..fb0f43d5 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -91,6 +91,7 @@ enum { OPT_CAMERA_FPS, OPT_CAMERA_HIGH_SPEED, OPT_DISPLAY_ORIENTATION, + OPT_RECORD_ORIENTATION, }; struct sc_option { @@ -609,6 +610,15 @@ static const struct sc_option options[] = { .text = "Force recording format (mp4, mkv, m4a, mka, opus, aac, flac " "or wav).", }, + { + .longopt_id = OPT_RECORD_ORIENTATION, + .longopt = "record-orientation", + .argdesc = "value", + .text = "Set the record orientation.\n" + "Possible values are 0, 90, 180 and 270. The number represents " + "the clockwise rotation in degrees.\n" + "Default is 0.", + }, { .longopt_id = OPT_RENDER_DRIVER, .longopt = "render-driver", @@ -2131,6 +2141,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; } break; + case OPT_RECORD_ORIENTATION: + if (!parse_orientation(optarg, &opts->record_orientation)) { + return false; + } + break; case OPT_RENDER_DRIVER: opts->render_driver = optarg; break; @@ -2497,6 +2512,15 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } } + if (opts->record_orientation != SC_ORIENTATION_0) { + if (sc_orientation_is_mirror(opts->record_orientation)) { + LOGE("Record orientation only supports rotation, not " + "flipping: %s", + sc_orientation_get_name(opts->record_orientation)); + return false; + } + } + if (opts->video && sc_record_format_is_audio_only(opts->record_format)) { LOGE("Audio container does not support video stream"); diff --git a/app/src/compat.h b/app/src/compat.h index e80a9dd2..fd610c02 100644 --- a/app/src/compat.h +++ b/app/src/compat.h @@ -3,7 +3,9 @@ #include "config.h" +#include #include +#include #include #ifndef __WIN32 @@ -50,6 +52,15 @@ # define SCRCPY_LAVU_HAS_CHLAYOUT #endif +// In ffmpeg/doc/APIchanges: +// 2023-10-06 - 5432d2aacad - lavc 60.15.100 - avformat.h +// Deprecate AVFormatContext.{nb_,}side_data, av_stream_add_side_data(), +// av_stream_new_side_data(), and av_stream_get_side_data(). Side data fields +// from AVFormatContext.codecpar should be used from now on. +#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(60, 15, 100) +# define SCRCPY_LAVC_HAS_CODECPAR_CODEC_SIDEDATA +#endif + #if SDL_VERSION_ATLEAST(2, 0, 6) // # define SCRCPY_SDL_HAS_HINT_TOUCH_MOUSE_EVENTS diff --git a/app/src/options.c b/app/src/options.c index 1454147a..a13df585 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -40,6 +40,7 @@ const struct scrcpy_options scrcpy_options_default = { .max_fps = 0, .lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_UNLOCKED, .display_orientation = SC_ORIENTATION_0, + .record_orientation = SC_ORIENTATION_0, .window_x = SC_WINDOW_POSITION_UNDEFINED, .window_y = SC_WINDOW_POSITION_UNDEFINED, .window_width = 0, diff --git a/app/src/options.h b/app/src/options.h index 4fb45840..11e64fa1 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -220,6 +220,7 @@ struct scrcpy_options { uint16_t max_fps; enum sc_lock_video_orientation lock_video_orientation; enum sc_orientation display_orientation; + enum sc_orientation record_orientation; int16_t window_x; // SC_WINDOW_POSITION_UNDEFINED for "auto" int16_t window_y; // SC_WINDOW_POSITION_UNDEFINED for "auto" uint16_t window_width; diff --git a/app/src/recorder.c b/app/src/recorder.c index c9d5f131..9e0b3395 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -4,6 +4,7 @@ #include #include #include +#include #include "util/log.h" #include "util/str.h" @@ -493,6 +494,42 @@ run_recorder(void *data) { return 0; } +static bool +sc_recorder_set_orientation(AVStream *stream, enum sc_orientation orientation) { + assert(!sc_orientation_is_mirror(orientation)); + + uint8_t *raw_data; +#ifdef SCRCPY_LAVC_HAS_CODECPAR_CODEC_SIDEDATA + AVPacketSideData *sd = + av_packet_side_data_new(&stream->codecpar->coded_side_data, + &stream->codecpar->nb_coded_side_data, + AV_PKT_DATA_DISPLAYMATRIX, + sizeof(int32_t) * 9, 0); + if (!sd) { + LOG_OOM(); + return false; + } + + raw_data = sd->data; +#else + raw_data = av_stream_new_side_data(stream, AV_PKT_DATA_DISPLAYMATRIX, + sizeof(int32_t) * 9); + if (!raw_data) { + LOG_OOM(); + return false; + } +#endif + + int32_t *matrix = (int32_t *) raw_data; + + unsigned rotation = orientation; + unsigned angle = rotation * 90; + + av_display_rotation_set(matrix, angle); + + return true; +} + static bool sc_recorder_video_packet_sink_open(struct sc_packet_sink *sink, AVCodecContext *ctx) { @@ -520,6 +557,16 @@ sc_recorder_video_packet_sink_open(struct sc_packet_sink *sink, recorder->video_stream.index = stream->index; + if (recorder->orientation != SC_ORIENTATION_0) { + if (!sc_recorder_set_orientation(stream, recorder->orientation)) { + sc_mutex_unlock(&recorder->mutex); + return false; + } + + LOGI("Record orientation set to %s", + sc_orientation_get_name(recorder->orientation)); + } + recorder->video_init = true; sc_cond_signal(&recorder->cond); sc_mutex_unlock(&recorder->mutex); @@ -689,7 +736,10 @@ sc_recorder_stream_init(struct sc_recorder_stream *stream) { bool sc_recorder_init(struct sc_recorder *recorder, const char *filename, enum sc_record_format format, bool video, bool audio, + enum sc_orientation orientation, const struct sc_recorder_callbacks *cbs, void *cbs_userdata) { + assert(!sc_orientation_is_mirror(orientation)); + recorder->filename = strdup(filename); if (!recorder->filename) { LOG_OOM(); @@ -710,6 +760,8 @@ sc_recorder_init(struct sc_recorder *recorder, const char *filename, recorder->video = video; recorder->audio = audio; + recorder->orientation = orientation; + sc_vecdeque_init(&recorder->video_queue); sc_vecdeque_init(&recorder->audio_queue); recorder->stopped = false; diff --git a/app/src/recorder.h b/app/src/recorder.h index 16327584..d096e79a 100644 --- a/app/src/recorder.h +++ b/app/src/recorder.h @@ -34,6 +34,8 @@ struct sc_recorder { bool audio; bool video; + enum sc_orientation orientation; + char *filename; enum sc_record_format format; AVFormatContext *ctx; @@ -67,6 +69,7 @@ struct sc_recorder_callbacks { bool sc_recorder_init(struct sc_recorder *recorder, const char *filename, enum sc_record_format format, bool video, bool audio, + enum sc_orientation orientation, const struct sc_recorder_callbacks *cbs, void *cbs_userdata); bool diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 9bbe14b8..d62a5f52 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -507,7 +507,8 @@ scrcpy(struct scrcpy_options *options) { }; if (!sc_recorder_init(&s->recorder, options->record_filename, options->record_format, options->video, - options->audio, &recorder_cbs, NULL)) { + options->audio, options->record_orientation, + &recorder_cbs, NULL)) { goto end; } recorder_initialized = true; From b43a9e8e7a70e3b847d13eeb17ebf18708c4d7fc Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 20 Nov 2023 17:49:04 +0100 Subject: [PATCH 1705/2244] Add --orientation Add a shortcut to set both the display and record orientations. PR #4441 --- app/data/bash-completion/scrcpy | 2 ++ app/data/zsh-completion/_scrcpy | 1 + app/scrcpy.1 | 4 ++++ app/src/cli.c | 16 ++++++++++++++++ 4 files changed, 23 insertions(+) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index f08df996..0c854310 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -51,6 +51,7 @@ _scrcpy() { --no-power-on --no-video --no-video-playback + --orientation= --otg -p --port= --pause-on-exit @@ -114,6 +115,7 @@ _scrcpy() { COMPREPLY=($(compgen -W 'front back external' -- "$cur")) return ;; + --orientation --display-orientation) COMPREPLY=($(compgen -> '0 90 180 270 flip0 flip90 flip180 flip270' -- "$cur")) return diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 0e39f96b..3c7ca217 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -57,6 +57,7 @@ arguments=( '--no-power-on[Do not power on the device on start]' '--no-video[Disable video forwarding]' '--no-video-playback[Disable video playback]' + '--orientation=[Set the video orientation]:orientation values:(0 90 180 270 flip0 flip90 flip180 flip270)' '--otg[Run in OTG mode \(simulating physical keyboard and mouse\)]' {-p,--port=}'[\[port\[\:port\]\] Set the TCP port \(range\) used by the client to listen]' '--pause-on-exit=[Make scrcpy pause before exiting]:mode:(true false if-error)' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 1a9386ee..0c34b4e2 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -299,6 +299,10 @@ Disable video forwarding. .B \-\-no\-video\-playback Disable video playback on the computer. +.TP +.BI "\-\-orientation " value +Same as --display-orientation=value --record-orientation=value. + .TP .B \-\-otg Run in OTG mode: simulate physical keyboard and mouse, as if the computer keyboard and mouse were plugged directly to the device via an OTG cable. diff --git a/app/src/cli.c b/app/src/cli.c index fb0f43d5..f57b75ef 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -92,6 +92,7 @@ enum { OPT_CAMERA_HIGH_SPEED, OPT_DISPLAY_ORIENTATION, OPT_RECORD_ORIENTATION, + OPT_ORIENTATION, }; struct sc_option { @@ -525,6 +526,13 @@ static const struct sc_option options[] = { .longopt = "no-video-playback", .text = "Disable video playback on the computer.", }, + { + .longopt_id = OPT_ORIENTATION, + .longopt = "orientation", + .argdesc = "value", + .text = "Same as --display-orientation=value " + "--record-orientation=value.", + }, { .longopt_id = OPT_OTG, .longopt = "otg", @@ -2146,6 +2154,14 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; } break; + case OPT_ORIENTATION: + enum sc_orientation orientation; + if (!parse_orientation(optarg, &orientation)) { + return false; + } + opts->display_orientation = orientation; + opts->record_orientation = orientation; + break; case OPT_RENDER_DRIVER: opts->render_driver = optarg; break; From 94031dfe97402d322e50c4d156456057fad53da0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 20 Nov 2023 20:56:36 +0100 Subject: [PATCH 1706/2244] Update documentation about video orientation PR #4441 --- doc/camera.md | 10 ++++++++++ doc/recording.md | 6 ++++++ doc/video.md | 45 ++++++++++++++++++++++++++++----------------- 3 files changed, 44 insertions(+), 17 deletions(-) diff --git a/doc/camera.md b/doc/camera.md index d1008bda..fcc410fa 100644 --- a/doc/camera.md +++ b/doc/camera.md @@ -101,6 +101,16 @@ scrcpy --video-source=camera --camera-size=1920x1080 -m3000 # error ``` +## Rotation + +To rotate the captured video, use the [video orientation](video.md#orientation) +option: + +``` +scrcpy --video-source=camera --camera-size=1920x1080 --orientation=90 +``` + + ## Frame rate By default, camera is captured at Android's default frame rate (30 fps). diff --git a/doc/recording.md b/doc/recording.md index c1a8445e..216542e9 100644 --- a/doc/recording.md +++ b/doc/recording.md @@ -50,6 +50,12 @@ scrcpy --record=file --record-format=mkv ``` +## Rotation + +The video can be recorded rotated. See [video +orientation](video.md#orientation). + + ## No playback To disable playback while recording: diff --git a/doc/video.md b/doc/video.md index 512e0aba..ed92cb22 100644 --- a/doc/video.md +++ b/doc/video.md @@ -97,39 +97,50 @@ scrcpy --video-codec=h264 --video-encoder='OMX.qcom.video.encoder.avc' ``` -## Rotation +## Orientation -The rotation may be applied at 3 different levels: +The orientation may be applied at 3 different levels: - The [shortcut](shortcuts.md) MOD+r requests the device to switch between portrait and landscape (the current running app may refuse, if it does not 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` rotates only the window content. This only affects the display, - not the recording. It may be changed dynamically at any time using the - [shortcuts](shortcuts.md) MOD+ and - MOD+. + - `--orientation` is applied on the client side, and affects display and + recording. For the display, it can be changed dynamically using + [shortcuts](shortcuts.md). -To lock the mirroring orientation: +To lock the mirroring orientation (on the capture side): ```bash -scrcpy --lock-video-orientation # initial (current) orientation -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 +scrcpy --lock-video-orientation # initial (current) orientation +scrcpy --lock-video-orientation=0 # natural orientation +scrcpy --lock-video-orientation=90 # 90° clockwise +scrcpy --lock-video-orientation=180 # 180° +scrcpy --lock-video-orientation=270 # 270° clockwise ``` -To set an initial window rotation: +To orient the video (on the rendering side): ```bash -scrcpy --rotation=0 # no rotation -scrcpy --rotation=1 # 90 degrees counterclockwise -scrcpy --rotation=2 # 180 degrees -scrcpy --rotation=3 # 90 degrees clockwise +scrcpy --orientation=0 +scrcpy --orientation=90 # 90° clockwise +scrcpy --orientation=180 # 180° +scrcpy --orientation=270 # 270° clockwise +scrcpy --orientation=flip0 # hflip +scrcpy --orientation=flip90 # hflip + 90° clockwise +scrcpy --orientation=flip180 # vflip (hflip + 180°) +scrcpy --orientation=flip270 # hflip + 270° clockwise ``` +The orientation can be set separately for display and record if necessary, via +`--display-orientation` and `--record-orientation`. + +The rotation is applied to a recorded file by writing a display transformation +to the MP4 or MKV target file. Flipping is not supported, so only the 4 first +values are allowed when recording. + + ## Crop The device screen may be cropped to mirror only part of the screen. From 85a94dd4b563e961304b2d9082932c5c1cc2e582 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 21 Nov 2023 14:59:24 +0100 Subject: [PATCH 1707/2244] Fix meson deprecated 'pkgconfig' to 'pkg-config' When running ./release.sh: > DEPRECATION: "pkgconfig" entry is deprecated and should be replaced by > "pkg-config" --- cross_win32.txt | 2 +- cross_win64.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cross_win32.txt b/cross_win32.txt index e24f3722..bf3c118e 100644 --- a/cross_win32.txt +++ b/cross_win32.txt @@ -6,7 +6,7 @@ c = 'i686-w64-mingw32-gcc' cpp = 'i686-w64-mingw32-g++' ar = 'i686-w64-mingw32-ar' strip = 'i686-w64-mingw32-strip' -pkgconfig = 'i686-w64-mingw32-pkg-config' +pkg-config = 'i686-w64-mingw32-pkg-config' windres = 'i686-w64-mingw32-windres' [host_machine] diff --git a/cross_win64.txt b/cross_win64.txt index 39e79944..81bb0309 100644 --- a/cross_win64.txt +++ b/cross_win64.txt @@ -6,7 +6,7 @@ c = 'x86_64-w64-mingw32-gcc' cpp = 'x86_64-w64-mingw32-g++' ar = 'x86_64-w64-mingw32-ar' strip = 'x86_64-w64-mingw32-strip' -pkgconfig = 'x86_64-w64-mingw32-pkg-config' +pkg-config = 'x86_64-w64-mingw32-pkg-config' windres = 'x86_64-w64-mingw32-windres' [host_machine] From acb29888377580d21ab67c805576f97d5bda8bc7 Mon Sep 17 00:00:00 2001 From: sam80180 Date: Sat, 11 Nov 2023 02:01:51 +0800 Subject: [PATCH 1708/2244] Do not hardcode server path on the device The path can be retrieved from the classpath. PR #4416 Co-authored-by: Romain Vimont Signed-off-by: Romain Vimont --- server/src/main/java/com/genymobile/scrcpy/CleanUp.java | 6 ++---- server/src/main/java/com/genymobile/scrcpy/Server.java | 8 ++++++++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java index 0bcd1a54..b3a1aac1 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java +++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java @@ -14,8 +14,6 @@ import java.io.IOException; */ public final class CleanUp { - public static final String SERVER_PATH = "/data/local/tmp/scrcpy-server.jar"; - // A simple struct to be passed from the main process to the cleanup process public static class Config implements Parcelable { @@ -135,13 +133,13 @@ public final class CleanUp { String[] cmd = {"app_process", "/", CleanUp.class.getName(), config.toBase64()}; ProcessBuilder builder = new ProcessBuilder(cmd); - builder.environment().put("CLASSPATH", SERVER_PATH); + builder.environment().put("CLASSPATH", Server.SERVER_PATH); builder.start(); } public static void unlinkSelf() { try { - new File(SERVER_PATH).delete(); + new File(Server.SERVER_PATH).delete(); } catch (Exception e) { Ln.e("Could not unlink server", e); } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 61d3497b..2a8387e0 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -3,12 +3,20 @@ package com.genymobile.scrcpy; import android.os.BatteryManager; import android.os.Build; +import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; public final class Server { + public static final String SERVER_PATH; + static { + String[] classPaths = System.getProperty("java.class.path").split(File.pathSeparator); + // By convention, scrcpy is always executed with the absolute path of scrcpy-server.jar as the first item in the classpath + SERVER_PATH = classPaths[0]; + } + private static class Completion { private int running; private boolean fatalError; From c573bd2a33c8fcd64005daca85a67a26bc958dfe Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 23 Nov 2023 23:50:00 +0100 Subject: [PATCH 1709/2244] Fix java code style --- .../com/genymobile/scrcpy/AsyncProcessor.java | 2 ++ .../com/genymobile/scrcpy/CameraCapture.java | 21 +++++++++---------- .../com/genymobile/scrcpy/Controller.java | 10 ++++----- .../java/com/genymobile/scrcpy/Device.java | 2 +- .../scrcpy/DeviceMessageSender.java | 1 + .../java/com/genymobile/scrcpy/Server.java | 1 + .../java/com/genymobile/scrcpy/Settings.java | 2 +- .../scrcpy/wrappers/ClipboardManager.java | 4 ++-- .../scrcpy/wrappers/PowerManager.java | 2 +- .../scrcpy/wrappers/ServiceManager.java | 1 + .../scrcpy/wrappers/WindowManager.java | 2 +- 11 files changed, 25 insertions(+), 23 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/AsyncProcessor.java b/server/src/main/java/com/genymobile/scrcpy/AsyncProcessor.java index b9b6745c..d5da6a90 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AsyncProcessor.java +++ b/server/src/main/java/com/genymobile/scrcpy/AsyncProcessor.java @@ -11,6 +11,8 @@ public interface AsyncProcessor { } void start(TerminationListener listener); + void stop(); + void join() throws InterruptedException; } diff --git a/server/src/main/java/com/genymobile/scrcpy/CameraCapture.java b/server/src/main/java/com/genymobile/scrcpy/CameraCapture.java index b9da3658..a1003829 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CameraCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/CameraCapture.java @@ -289,18 +289,17 @@ public class CameraCapture extends SurfaceCapture { List outputs = Arrays.asList(outputConfig); int sessionType = highSpeed ? SessionConfiguration.SESSION_HIGH_SPEED : SessionConfiguration.SESSION_REGULAR; - SessionConfiguration sessionConfig = new SessionConfiguration(sessionType, outputs, cameraExecutor, - new CameraCaptureSession.StateCallback() { - @Override - public void onConfigured(CameraCaptureSession session) { - future.complete(session); - } + SessionConfiguration sessionConfig = new SessionConfiguration(sessionType, outputs, cameraExecutor, new CameraCaptureSession.StateCallback() { + @Override + public void onConfigured(CameraCaptureSession session) { + future.complete(session); + } - @Override - public void onConfigureFailed(CameraCaptureSession session) { - future.completeExceptionally(new CameraAccessException(CameraAccessException.CAMERA_ERROR)); - } - }); + @Override + public void onConfigureFailed(CameraCaptureSession session) { + future.completeExceptionally(new CameraAccessException(CameraAccessException.CAMERA_ERROR)); + } + }); camera.createCaptureSession(sessionConfig); diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index 733a2032..3b0e9031 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -318,9 +318,8 @@ public class Controller implements AsyncProcessor { } } - MotionEvent event = MotionEvent - .obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, - 0); + MotionEvent event = MotionEvent.obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, + DEFAULT_DEVICE_ID, 0, source, 0); return device.injectEvent(event, Device.INJECT_MODE_ASYNC); } @@ -341,9 +340,8 @@ public class Controller implements AsyncProcessor { coords.setAxisValue(MotionEvent.AXIS_HSCROLL, hScroll); coords.setAxisValue(MotionEvent.AXIS_VSCROLL, vScroll); - MotionEvent event = MotionEvent - .obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, - InputDevice.SOURCE_MOUSE, 0); + MotionEvent event = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, + DEFAULT_DEVICE_ID, 0, InputDevice.SOURCE_MOUSE, 0); return device.injectEvent(event, Device.INJECT_MODE_ASYNC); } diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index 4ab689b0..9fbe9239 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -11,8 +11,8 @@ import android.graphics.Rect; import android.os.Build; import android.os.IBinder; import android.os.SystemClock; -import android.view.IRotationWatcher; import android.view.IDisplayFoldListener; +import android.view.IRotationWatcher; import android.view.InputDevice; import android.view.InputEvent; import android.view.KeyCharacterMap; diff --git a/server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java b/server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java index 628c1d3c..94e842ee 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java +++ b/server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java @@ -51,6 +51,7 @@ public final class DeviceMessageSender { } } } + public void start() { thread = new Thread(() -> { try { diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 2a8387e0..e4a95140 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -11,6 +11,7 @@ import java.util.List; public final class Server { public static final String SERVER_PATH; + static { String[] classPaths = System.getProperty("java.class.path").split(File.pathSeparator); // By convention, scrcpy is always executed with the absolute path of scrcpy-server.jar as the first item in the classpath diff --git a/server/src/main/java/com/genymobile/scrcpy/Settings.java b/server/src/main/java/com/genymobile/scrcpy/Settings.java index 1d38814d..1b5e5f98 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Settings.java +++ b/server/src/main/java/com/genymobile/scrcpy/Settings.java @@ -75,7 +75,7 @@ public final class Settings { String oldValue = getValue(table, key); if (!value.equals(oldValue)) { - putValue(table, key, value); + putValue(table, key, value); } return oldValue; } 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 eae66858..783a3407 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java @@ -138,8 +138,8 @@ public final class ClipboardManager { } } - private static void addPrimaryClipChangedListener(Method method, int methodVersion, IInterface manager, - IOnPrimaryClipChangedListener listener) throws InvocationTargetException, IllegalAccessException { + private static void addPrimaryClipChangedListener(Method method, int methodVersion, IInterface manager, IOnPrimaryClipChangedListener listener) + throws InvocationTargetException, IllegalAccessException { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { method.invoke(manager, listener, FakeContext.PACKAGE_NAME); return; diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java index 8ff074b3..93722687 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java @@ -20,7 +20,7 @@ public final class PowerManager { private Method getIsScreenOnMethod() throws NoSuchMethodException { if (isScreenOnMethod == null) { @SuppressLint("ObsoleteSdkInt") // we may lower minSdkVersion in the future - String methodName = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH ? "isInteractive" : "isScreenOn"; + String methodName = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH ? "isInteractive" : "isScreenOn"; isScreenOnMethod = manager.getClass().getMethod(methodName); } return isScreenOnMethod; 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 ae04a6d2..85602c19 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 @@ import java.lang.reflect.Method; public final class ServiceManager { private static final Method GET_SERVICE_METHOD; + static { try { GET_SERVICE_METHOD = Class.forName("android.os.ServiceManager").getDeclaredMethod("getService", String.class); 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 ce748855..a746be5c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java @@ -4,8 +4,8 @@ import com.genymobile.scrcpy.Ln; import android.annotation.TargetApi; import android.os.IInterface; -import android.view.IRotationWatcher; import android.view.IDisplayFoldListener; +import android.view.IRotationWatcher; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; From 67f356f881898e21edabd4ecd2fb509ad4c92362 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 24 Nov 2023 21:25:13 +0100 Subject: [PATCH 1710/2244] Improve crossbuild Install all the prebuilt dependencies for Windows to a specific folder, and use meson command line options to specify their location. This removes crossbuild-specific code from the meson scripts and will simplify dependency upgrades. PR #4460 --- app/meson.build | 79 +++++------------------------ app/prebuilt-deps/prepare-ffmpeg.sh | 4 +- app/prebuilt-deps/prepare-libusb.sh | 12 +++-- cross_win32.txt | 5 -- cross_win64.txt | 5 -- release.mk | 68 +++++++++++++------------ 6 files changed, 59 insertions(+), 114 deletions(-) diff --git a/app/meson.build b/app/meson.build index b1233c6b..88e2df9a 100644 --- a/app/meson.build +++ b/app/meson.build @@ -98,77 +98,24 @@ endif cc = meson.get_compiler('c') -crossbuild_windows = meson.is_cross_build() and host_machine.system() == 'windows' +dependencies = [ + dependency('libavformat', version: '>= 57.33'), + dependency('libavcodec', version: '>= 57.37'), + dependency('libavutil'), + dependency('libswresample'), + dependency('sdl2', version: '>= 2.0.5'), +] -if not crossbuild_windows - - # native build - dependencies = [ - dependency('libavformat', version: '>= 57.33'), - dependency('libavcodec', version: '>= 57.37'), - dependency('libavutil'), - dependency('libswresample'), - dependency('sdl2', version: '>= 2.0.5'), - ] - - if v4l2_support - dependencies += dependency('libavdevice') - endif - - if usb_support - dependencies += dependency('libusb-1.0') - endif - -else - # cross-compile mingw32 build (from Linux to Windows) - prebuilt_sdl2 = meson.get_cross_property('prebuilt_sdl2') - sdl2_bin_dir = meson.current_source_dir() + '/prebuilt-deps/data/' + prebuilt_sdl2 + '/bin' - sdl2_lib_dir = meson.current_source_dir() + '/prebuilt-deps/data/' + prebuilt_sdl2 + '/lib' - sdl2_include_dir = 'prebuilt-deps/data/' + prebuilt_sdl2 + '/include' - - sdl2 = declare_dependency( - dependencies: [ - cc.find_library('SDL2', dirs: sdl2_bin_dir), - cc.find_library('SDL2main', dirs: sdl2_lib_dir), - ], - include_directories: include_directories(sdl2_include_dir) - ) - - prebuilt_ffmpeg = meson.get_cross_property('prebuilt_ffmpeg') - ffmpeg_bin_dir = meson.current_source_dir() + '/prebuilt-deps/data/' + prebuilt_ffmpeg + '/bin' - ffmpeg_include_dir = 'prebuilt-deps/data/' + prebuilt_ffmpeg + '/include' - - ffmpeg = declare_dependency( - dependencies: [ - cc.find_library('avcodec-60', dirs: ffmpeg_bin_dir), - cc.find_library('avformat-60', dirs: ffmpeg_bin_dir), - cc.find_library('avutil-58', dirs: ffmpeg_bin_dir), - cc.find_library('swresample-4', dirs: ffmpeg_bin_dir), - ], - include_directories: include_directories(ffmpeg_include_dir) - ) - - prebuilt_libusb = meson.get_cross_property('prebuilt_libusb') - libusb_bin_dir = meson.current_source_dir() + '/prebuilt-deps/data/' + prebuilt_libusb + '/bin' - libusb_include_dir = 'prebuilt-deps/data/' + prebuilt_libusb + '/include' - - libusb = declare_dependency( - dependencies: [ - cc.find_library('msys-usb-1.0', dirs: libusb_bin_dir), - ], - include_directories: include_directories(libusb_include_dir) - ) - - dependencies = [ - ffmpeg, - sdl2, - libusb, - cc.find_library('mingw32') - ] +if v4l2_support + dependencies += dependency('libavdevice') +endif +if usb_support + dependencies += dependency('libusb-1.0') endif if host_machine.system() == 'windows' + dependencies += cc.find_library('mingw32') dependencies += cc.find_library('ws2_32') endif diff --git a/app/prebuilt-deps/prepare-ffmpeg.sh b/app/prebuilt-deps/prepare-ffmpeg.sh index 96ea3ee7..19840afb 100755 --- a/app/prebuilt-deps/prepare-ffmpeg.sh +++ b/app/prebuilt-deps/prepare-ffmpeg.sh @@ -6,11 +6,11 @@ cd "$DIR" mkdir -p "$PREBUILT_DATA_DIR" cd "$PREBUILT_DATA_DIR" -VERSION=6.1-scrcpy-2 +VERSION=6.1-scrcpy-3 DEP_DIR="ffmpeg-$VERSION" FILENAME="$DEP_DIR".7z -SHA256SUM=7f25f638dc24a0f5d4af07a088b6a604cf33548900bbfd2f6ce0bae050b7664d +SHA256SUM=b646d18a3d543a4e4c46881568213499f22e4454a464e1552f03f2ac9cc3a05a if [[ -d "$DEP_DIR" ]] then diff --git a/app/prebuilt-deps/prepare-libusb.sh b/app/prebuilt-deps/prepare-libusb.sh index 47cf1df4..6a052f0d 100755 --- a/app/prebuilt-deps/prepare-libusb.sh +++ b/app/prebuilt-deps/prepare-libusb.sh @@ -23,11 +23,15 @@ mkdir "$DEP_DIR" cd "$DEP_DIR" 7z x "../$FILENAME" \ - libusb-1.0.26-binaries/libusb-MinGW-Win32/bin/msys-usb-1.0.dll \ - libusb-1.0.26-binaries/libusb-MinGW-Win32/include/ \ - libusb-1.0.26-binaries/libusb-MinGW-x64/bin/msys-usb-1.0.dll \ - libusb-1.0.26-binaries/libusb-MinGW-x64/include/ + libusb-1.0.26-binaries/libusb-MinGW-Win32/ \ + libusb-1.0.26-binaries/libusb-MinGW-Win32/ \ + libusb-1.0.26-binaries/libusb-MinGW-x64/ \ + libusb-1.0.26-binaries/libusb-MinGW-x64/ mv libusb-1.0.26-binaries/libusb-MinGW-Win32 . mv libusb-1.0.26-binaries/libusb-MinGW-x64 . rm -rf libusb-1.0.26-binaries + +# Rename the dll to get the same library name on all platforms +mv libusb-MinGW-Win32/bin/msys-usb-1.0.dll libusb-MinGW-Win32/bin/libusb-1.0.dll +mv libusb-MinGW-x64/bin/msys-usb-1.0.dll libusb-MinGW-x64/bin/libusb-1.0.dll diff --git a/cross_win32.txt b/cross_win32.txt index bf3c118e..05f9a86b 100644 --- a/cross_win32.txt +++ b/cross_win32.txt @@ -14,8 +14,3 @@ system = 'windows' cpu_family = 'x86' cpu = 'i686' endian = 'little' - -[properties] -prebuilt_ffmpeg = 'ffmpeg-6.1-scrcpy-2/win32' -prebuilt_sdl2 = 'SDL2-2.28.4/i686-w64-mingw32' -prebuilt_libusb = 'libusb-1.0.26/libusb-MinGW-Win32' diff --git a/cross_win64.txt b/cross_win64.txt index 81bb0309..86364ad6 100644 --- a/cross_win64.txt +++ b/cross_win64.txt @@ -14,8 +14,3 @@ system = 'windows' cpu_family = 'x86' cpu = 'x86_64' endian = 'little' - -[properties] -prebuilt_ffmpeg = 'ffmpeg-6.1-scrcpy-2/win64' -prebuilt_sdl2 = 'SDL2-2.28.4/x86_64-w64-mingw32' -prebuilt_libusb = 'libusb-1.0.26/libusb-MinGW-x64' diff --git a/release.mk b/release.mk index 57fa994e..7a686f49 100644 --- a/release.mk +++ b/release.mk @@ -69,58 +69,62 @@ prepare-deps: @app/prebuilt-deps/prepare-libusb.sh build-win32: prepare-deps - [ -d "$(WIN32_BUILD_DIR)" ] || ( mkdir "$(WIN32_BUILD_DIR)" && \ - meson setup "$(WIN32_BUILD_DIR)" \ - --cross-file cross_win32.txt \ - --buildtype release --strip -Db_lto=true \ - -Dcompile_server=false \ - -Dportable=true ) + rm -rf "$(WIN32_BUILD_DIR)" + mkdir -p "$(WIN32_BUILD_DIR)/local" + cp -r app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-3/win32/. "$(WIN32_BUILD_DIR)/local/" + cp -r app/prebuilt-deps/data/SDL2-2.28.4/i686-w64-mingw32/. "$(WIN32_BUILD_DIR)/local/" + cp -r app/prebuilt-deps/data/libusb-1.0.26/libusb-MinGW-Win32/. "$(WIN32_BUILD_DIR)/local/" + meson setup "$(WIN32_BUILD_DIR)" \ + --pkg-config-path="$(WIN32_BUILD_DIR)/local/lib/pkgconfig" \ + -Dc_args="-I$(PWD)/$(WIN32_BUILD_DIR)/local/include" \ + -Dc_link_args="-L$(PWD)/$(WIN32_BUILD_DIR)/local/lib" \ + --cross-file=cross_win32.txt \ + --buildtype=release --strip -Db_lto=true \ + -Dcompile_server=false \ + -Dportable=true ninja -C "$(WIN32_BUILD_DIR)" build-win64: prepare-deps - [ -d "$(WIN64_BUILD_DIR)" ] || ( mkdir "$(WIN64_BUILD_DIR)" && \ - meson setup "$(WIN64_BUILD_DIR)" \ - --cross-file cross_win64.txt \ - --buildtype release --strip -Db_lto=true \ - -Dcompile_server=false \ - -Dportable=true ) + rm -rf "$(WIN64_BUILD_DIR)" + mkdir -p "$(WIN64_BUILD_DIR)/local" + cp -r app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-3/win64/. "$(WIN64_BUILD_DIR)/local/" + cp -r app/prebuilt-deps/data/SDL2-2.28.4/x86_64-w64-mingw32/. "$(WIN64_BUILD_DIR)/local/" + cp -r app/prebuilt-deps/data/libusb-1.0.26/libusb-MinGW-x64/. "$(WIN64_BUILD_DIR)/local/" + meson setup "$(WIN64_BUILD_DIR)" \ + --pkg-config-path="$(WIN64_BUILD_DIR)/local/lib/pkgconfig" \ + -Dc_args="-I$(PWD)/$(WIN64_BUILD_DIR)/local/include" \ + -Dc_link_args="-L$(PWD)/$(WIN64_BUILD_DIR)/local/lib" \ + --cross-file=cross_win64.txt \ + --buildtype=release --strip -Db_lto=true \ + -Dcompile_server=false \ + -Dportable=true ninja -C "$(WIN64_BUILD_DIR)" dist-win32: build-server build-win32 mkdir -p "$(DIST)/$(WIN32_TARGET_DIR)" cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(DIST)/$(WIN32_TARGET_DIR)/" cp "$(WIN32_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/data/scrcpy-console.bat "$(DIST)/$(WIN32_TARGET_DIR)" - cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN32_TARGET_DIR)" - cp app/data/icon.png "$(DIST)/$(WIN32_TARGET_DIR)" - cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN32_TARGET_DIR)" - cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-2/win32/bin/avutil-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-2/win32/bin/avcodec-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-2/win32/bin/avformat-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-2/win32/bin/swresample-4.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/data/scrcpy-console.bat "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/data/icon.png "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-34.0.5/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-34.0.5/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-34.0.5/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/SDL2-2.28.4/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/libusb-1.0.26/libusb-MinGW-Win32/bin/msys-usb-1.0.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp "$(WIN32_BUILD_DIR)"/local/bin/*.dll "$(DIST)/$(WIN32_TARGET_DIR)/" dist-win64: build-server build-win64 mkdir -p "$(DIST)/$(WIN64_TARGET_DIR)" cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(DIST)/$(WIN64_TARGET_DIR)/" cp "$(WIN64_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/data/scrcpy-console.bat "$(DIST)/$(WIN64_TARGET_DIR)" - cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN64_TARGET_DIR)" - cp app/data/icon.png "$(DIST)/$(WIN64_TARGET_DIR)" - cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN64_TARGET_DIR)" - cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-2/win64/bin/avutil-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-2/win64/bin/avcodec-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-2/win64/bin/avformat-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-2/win64/bin/swresample-4.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/data/scrcpy-console.bat "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/data/icon.png "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-34.0.5/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-34.0.5/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-34.0.5/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/SDL2-2.28.4/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/libusb-1.0.26/libusb-MinGW-x64/bin/msys-usb-1.0.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp "$(WIN64_BUILD_DIR)"/local/bin/*.dll "$(DIST)/$(WIN64_TARGET_DIR)/" zip-win32: dist-win32 cd "$(DIST)"; \ From 2370298b612bfd86da746a7480b8e159fc92c326 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 24 Nov 2023 18:41:13 +0100 Subject: [PATCH 1711/2244] Download SDL prebuilt binaries from github The server is faster than libsdl.org. --- app/prebuilt-deps/prepare-sdl.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/prebuilt-deps/prepare-sdl.sh b/app/prebuilt-deps/prepare-sdl.sh index 645646de..f53f090c 100755 --- a/app/prebuilt-deps/prepare-sdl.sh +++ b/app/prebuilt-deps/prepare-sdl.sh @@ -17,7 +17,7 @@ then exit 0 fi -get_file "https://libsdl.org/release/$FILENAME" "$FILENAME" "$SHA256SUM" +get_file "https://github.com/libsdl-org/SDL/releases/download/release-2.28.4/$FILENAME" "$FILENAME" "$SHA256SUM" mkdir "$DEP_DIR" cd "$DEP_DIR" From 825d7f72c0a81d2aac5594aaa8fe265b468280d4 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 24 Nov 2023 21:19:18 +0100 Subject: [PATCH 1712/2244] Extract $VERSION for dependency scripts This will allow to update the version only once in these files. --- app/prebuilt-deps/prepare-libusb.sh | 22 ++++++++++++---------- app/prebuilt-deps/prepare-sdl.sh | 8 +++++--- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/app/prebuilt-deps/prepare-libusb.sh b/app/prebuilt-deps/prepare-libusb.sh index 6a052f0d..228a5bfa 100755 --- a/app/prebuilt-deps/prepare-libusb.sh +++ b/app/prebuilt-deps/prepare-libusb.sh @@ -6,9 +6,10 @@ cd "$DIR" mkdir -p "$PREBUILT_DATA_DIR" cd "$PREBUILT_DATA_DIR" -DEP_DIR=libusb-1.0.26 +VERSION=1.0.26 +DEP_DIR="libusb-$VERSION" -FILENAME=libusb-1.0.26-binaries.7z +FILENAME="libusb-$VERSION-binaries.7z" SHA256SUM=9c242696342dbde9cdc47239391f71833939bf9f7aa2bbb28cdaabe890465ec5 if [[ -d "$DEP_DIR" ]] @@ -17,20 +18,21 @@ then exit 0 fi -get_file "https://github.com/libusb/libusb/releases/download/v1.0.26/$FILENAME" "$FILENAME" "$SHA256SUM" +get_file "https://github.com/libusb/libusb/releases/download/v$VERSION/$FILENAME" \ + "$FILENAME" "$SHA256SUM" mkdir "$DEP_DIR" cd "$DEP_DIR" 7z x "../$FILENAME" \ - libusb-1.0.26-binaries/libusb-MinGW-Win32/ \ - libusb-1.0.26-binaries/libusb-MinGW-Win32/ \ - libusb-1.0.26-binaries/libusb-MinGW-x64/ \ - libusb-1.0.26-binaries/libusb-MinGW-x64/ + "libusb-$VERSION-binaries/libusb-MinGW-Win32/" \ + "libusb-$VERSION-binaries/libusb-MinGW-Win32/" \ + "libusb-$VERSION-binaries/libusb-MinGW-x64/" \ + "libusb-$VERSION-binaries/libusb-MinGW-x64/" -mv libusb-1.0.26-binaries/libusb-MinGW-Win32 . -mv libusb-1.0.26-binaries/libusb-MinGW-x64 . -rm -rf libusb-1.0.26-binaries +mv "libusb-$VERSION-binaries/libusb-MinGW-Win32" . +mv "libusb-$VERSION-binaries/libusb-MinGW-x64" . +rm -rf "libusb-$VERSION-binaries" # Rename the dll to get the same library name on all platforms mv libusb-MinGW-Win32/bin/msys-usb-1.0.dll libusb-MinGW-Win32/bin/libusb-1.0.dll diff --git a/app/prebuilt-deps/prepare-sdl.sh b/app/prebuilt-deps/prepare-sdl.sh index f53f090c..580461d2 100755 --- a/app/prebuilt-deps/prepare-sdl.sh +++ b/app/prebuilt-deps/prepare-sdl.sh @@ -6,9 +6,10 @@ cd "$DIR" mkdir -p "$PREBUILT_DATA_DIR" cd "$PREBUILT_DATA_DIR" -DEP_DIR=SDL2-2.28.4 +VERSION=2.28.4 +DEP_DIR="SDL2-$VERSION" -FILENAME=SDL2-devel-2.28.4-mingw.tar.gz +FILENAME="SDL2-devel-$VERSION-mingw.tar.gz" SHA256SUM=779d091072cf97291f80030f5232d97aa3d48ab0f2c14fe0b9d9a33c593cdc35 if [[ -d "$DEP_DIR" ]] @@ -17,7 +18,8 @@ then exit 0 fi -get_file "https://github.com/libsdl-org/SDL/releases/download/release-2.28.4/$FILENAME" "$FILENAME" "$SHA256SUM" +get_file "https://github.com/libsdl-org/SDL/releases/download/release-$VERSION/$FILENAME" \ + "$FILENAME" "$SHA256SUM" mkdir "$DEP_DIR" cd "$DEP_DIR" From eed06b141aa6cba15f1c163f8ec69adf13118b0e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 24 Nov 2023 21:46:05 +0100 Subject: [PATCH 1713/2244] Upgrade sdl (2.28.5) for Windows Include the latest version of SDL in Windows releases. --- app/prebuilt-deps/prepare-sdl.sh | 4 ++-- release.mk | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/prebuilt-deps/prepare-sdl.sh b/app/prebuilt-deps/prepare-sdl.sh index 580461d2..7569744f 100755 --- a/app/prebuilt-deps/prepare-sdl.sh +++ b/app/prebuilt-deps/prepare-sdl.sh @@ -6,11 +6,11 @@ cd "$DIR" mkdir -p "$PREBUILT_DATA_DIR" cd "$PREBUILT_DATA_DIR" -VERSION=2.28.4 +VERSION=2.28.5 DEP_DIR="SDL2-$VERSION" FILENAME="SDL2-devel-$VERSION-mingw.tar.gz" -SHA256SUM=779d091072cf97291f80030f5232d97aa3d48ab0f2c14fe0b9d9a33c593cdc35 +SHA256SUM=3c0c655c2ebf67cad48fead72761d1601740ded30808952c3274ba223d226c21 if [[ -d "$DEP_DIR" ]] then diff --git a/release.mk b/release.mk index 7a686f49..fd969e5a 100644 --- a/release.mk +++ b/release.mk @@ -72,7 +72,7 @@ build-win32: prepare-deps rm -rf "$(WIN32_BUILD_DIR)" mkdir -p "$(WIN32_BUILD_DIR)/local" cp -r app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-3/win32/. "$(WIN32_BUILD_DIR)/local/" - cp -r app/prebuilt-deps/data/SDL2-2.28.4/i686-w64-mingw32/. "$(WIN32_BUILD_DIR)/local/" + cp -r app/prebuilt-deps/data/SDL2-2.28.5/i686-w64-mingw32/. "$(WIN32_BUILD_DIR)/local/" cp -r app/prebuilt-deps/data/libusb-1.0.26/libusb-MinGW-Win32/. "$(WIN32_BUILD_DIR)/local/" meson setup "$(WIN32_BUILD_DIR)" \ --pkg-config-path="$(WIN32_BUILD_DIR)/local/lib/pkgconfig" \ @@ -88,7 +88,7 @@ build-win64: prepare-deps rm -rf "$(WIN64_BUILD_DIR)" mkdir -p "$(WIN64_BUILD_DIR)/local" cp -r app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-3/win64/. "$(WIN64_BUILD_DIR)/local/" - cp -r app/prebuilt-deps/data/SDL2-2.28.4/x86_64-w64-mingw32/. "$(WIN64_BUILD_DIR)/local/" + cp -r app/prebuilt-deps/data/SDL2-2.28.5/x86_64-w64-mingw32/. "$(WIN64_BUILD_DIR)/local/" cp -r app/prebuilt-deps/data/libusb-1.0.26/libusb-MinGW-x64/. "$(WIN64_BUILD_DIR)/local/" meson setup "$(WIN64_BUILD_DIR)" \ --pkg-config-path="$(WIN64_BUILD_DIR)/local/lib/pkgconfig" \ From 5d4b8a7e6d18ee0e19a764945dc67efbd46f3e97 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 23 Nov 2023 23:50:41 +0100 Subject: [PATCH 1714/2244] Fix turn screen off on Android 14 On Android 14, the methods to access the display have been moved to DisplayControl, which is not in the core framework. Use a specific ClassLoader to access this class and its native dependencies. Fixes #3927 Refs #3927 comment Refs #4446 comment PR #4456 Co-authored-by: Simon Chan <1330321+yume-chan@users.noreply.github.com> Signed-off-by: Romain Vimont --- .../java/com/genymobile/scrcpy/Device.java | 10 ++- .../scrcpy/wrappers/DisplayControl.java | 80 +++++++++++++++++++ .../scrcpy/wrappers/SurfaceControl.java | 9 +++ 3 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayControl.java diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index 9fbe9239..b51ad8d3 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -1,6 +1,7 @@ package com.genymobile.scrcpy; import com.genymobile.scrcpy.wrappers.ClipboardManager; +import com.genymobile.scrcpy.wrappers.DisplayControl; import com.genymobile.scrcpy.wrappers.InputManager; import com.genymobile.scrcpy.wrappers.ServiceManager; import com.genymobile.scrcpy.wrappers.SurfaceControl; @@ -315,8 +316,12 @@ public final class Device { */ public static boolean setScreenPowerMode(int mode) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + // On Android 14, these internal methods have been moved to DisplayControl + boolean useDisplayControl = + Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && !SurfaceControl.hasPhysicalDisplayIdsMethod(); + // Change the power mode for all physical displays - long[] physicalDisplayIds = SurfaceControl.getPhysicalDisplayIds(); + long[] physicalDisplayIds = useDisplayControl ? DisplayControl.getPhysicalDisplayIds() : SurfaceControl.getPhysicalDisplayIds(); if (physicalDisplayIds == null) { Ln.e("Could not get physical display ids"); return false; @@ -324,7 +329,8 @@ public final class Device { boolean allOk = true; for (long physicalDisplayId : physicalDisplayIds) { - IBinder binder = SurfaceControl.getPhysicalDisplayToken(physicalDisplayId); + IBinder binder = useDisplayControl ? DisplayControl.getPhysicalDisplayToken( + physicalDisplayId) : SurfaceControl.getPhysicalDisplayToken(physicalDisplayId); allOk &= SurfaceControl.setDisplayPowerMode(binder, mode); } return allOk; diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayControl.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayControl.java new file mode 100644 index 00000000..4e19beb9 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayControl.java @@ -0,0 +1,80 @@ +package com.genymobile.scrcpy.wrappers; + +import com.genymobile.scrcpy.Ln; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.os.Build; +import android.os.IBinder; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +@SuppressLint({"PrivateApi", "SoonBlockedPrivateApi", "BlockedPrivateApi"}) +@TargetApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) +public final class DisplayControl { + + private static final Class CLASS; + + static { + Class displayControlClass = null; + try { + Class classLoaderFactoryClass = Class.forName("com.android.internal.os.ClassLoaderFactory"); + Method createClassLoaderMethod = classLoaderFactoryClass.getDeclaredMethod("createClassLoader", String.class, String.class, String.class, + ClassLoader.class, int.class, boolean.class, String.class); + ClassLoader classLoader = (ClassLoader) createClassLoaderMethod.invoke(null, "/system/framework/services.jar", null, null, + ClassLoader.getSystemClassLoader(), 0, true, null); + + displayControlClass = classLoader.loadClass("com.android.server.display.DisplayControl"); + + Method loadMethod = Runtime.class.getDeclaredMethod("loadLibrary0", Class.class, String.class); + loadMethod.setAccessible(true); + loadMethod.invoke(Runtime.getRuntime(), displayControlClass, "android_servers"); + } catch (Throwable e) { + Ln.e("Could not initialize DisplayControl", e); + // Do not throw an exception here, the methods will fail when they are called + } + CLASS = displayControlClass; + } + + private static Method getPhysicalDisplayTokenMethod; + private static Method getPhysicalDisplayIdsMethod; + + private DisplayControl() { + // only static methods + } + + private static Method getGetPhysicalDisplayTokenMethod() throws NoSuchMethodException { + if (getPhysicalDisplayTokenMethod == null) { + getPhysicalDisplayTokenMethod = CLASS.getMethod("getPhysicalDisplayToken", long.class); + } + return getPhysicalDisplayTokenMethod; + } + + public static IBinder getPhysicalDisplayToken(long physicalDisplayId) { + try { + Method method = getGetPhysicalDisplayTokenMethod(); + return (IBinder) method.invoke(null, physicalDisplayId); + } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + Ln.e("Could not invoke method", e); + return null; + } + } + + private static Method getGetPhysicalDisplayIdsMethod() throws NoSuchMethodException { + if (getPhysicalDisplayIdsMethod == null) { + getPhysicalDisplayIdsMethod = CLASS.getMethod("getPhysicalDisplayIds"); + } + return getPhysicalDisplayIdsMethod; + } + + public static long[] getPhysicalDisplayIds() { + try { + Method method = getGetPhysicalDisplayIdsMethod(); + return (long[]) method.invoke(null); + } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + Ln.e("Could not invoke method", e); + return null; + } + } +} 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 595ee6d4..98259e7f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java @@ -139,6 +139,15 @@ public final class SurfaceControl { return getPhysicalDisplayIdsMethod; } + public static boolean hasPhysicalDisplayIdsMethod() { + try { + getGetPhysicalDisplayIdsMethod(); + return true; + } catch (NoSuchMethodException e) { + return false; + } + } + public static long[] getPhysicalDisplayIds() { try { Method method = getGetPhysicalDisplayIdsMethod(); From 8db4e78b34f9b2b08ce78910389a7f8f4c030f12 Mon Sep 17 00:00:00 2001 From: Kid <44045911+kidonng@users.noreply.github.com> Date: Wed, 22 Nov 2023 02:13:56 +0800 Subject: [PATCH 1715/2244] Fix Linux desktop files There were too many backslashes in the Exec line. Fixes #4367 PR #4448 Signed-off-by: Romain Vimont --- app/data/scrcpy-console.desktop | 2 +- app/data/scrcpy.desktop | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/data/scrcpy-console.desktop b/app/data/scrcpy-console.desktop index 6ca1e36a..77501456 100644 --- a/app/data/scrcpy-console.desktop +++ b/app/data/scrcpy-console.desktop @@ -5,7 +5,7 @@ Comment=Display and control your Android device # For some users, the PATH or ADB environment variables are set from the shell # startup file, like .bashrc or .zshrc… Run an interactive shell to get # environment correctly initialized. -Exec=/bin/sh -c "\"\\$SHELL\" -i -c scrcpy --pause-on-exit=if-error" +Exec=/bin/sh -c "\"\$SHELL\" -i -c scrcpy --pause-on-exit=if-error" Icon=scrcpy Terminal=true Type=Application diff --git a/app/data/scrcpy.desktop b/app/data/scrcpy.desktop index 1be86a2b..4557e71a 100644 --- a/app/data/scrcpy.desktop +++ b/app/data/scrcpy.desktop @@ -5,7 +5,7 @@ Comment=Display and control your Android device # For some users, the PATH or ADB environment variables are set from the shell # startup file, like .bashrc or .zshrc… Run an interactive shell to get # environment correctly initialized. -Exec=/bin/sh -c "\"\\$SHELL\" -i -c scrcpy" +Exec=/bin/sh -c "\"\$SHELL\" -i -c scrcpy" Icon=scrcpy Terminal=false Type=Application From 89761213c3dd0d8ebe1ab0e8c2dd04b177b47dc2 Mon Sep 17 00:00:00 2001 From: Kid <44045911+kidonng@users.noreply.github.com> Date: Wed, 22 Nov 2023 02:32:19 +0800 Subject: [PATCH 1716/2244] Do not quote $SHELL in .desktop files This does not work properly on some desktop environments (KDE), and $SHELL is unlikely to require quoting. Fixes #4367 PR #4448 Signed-off-by: Romain Vimont --- app/data/scrcpy-console.desktop | 2 +- app/data/scrcpy.desktop | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/data/scrcpy-console.desktop b/app/data/scrcpy-console.desktop index 77501456..71c0f001 100644 --- a/app/data/scrcpy-console.desktop +++ b/app/data/scrcpy-console.desktop @@ -5,7 +5,7 @@ Comment=Display and control your Android device # For some users, the PATH or ADB environment variables are set from the shell # startup file, like .bashrc or .zshrc… Run an interactive shell to get # environment correctly initialized. -Exec=/bin/sh -c "\"\$SHELL\" -i -c scrcpy --pause-on-exit=if-error" +Exec=/bin/sh -c "\\$SHELL -i -c scrcpy --pause-on-exit=if-error" Icon=scrcpy Terminal=true Type=Application diff --git a/app/data/scrcpy.desktop b/app/data/scrcpy.desktop index 4557e71a..9fb81d47 100644 --- a/app/data/scrcpy.desktop +++ b/app/data/scrcpy.desktop @@ -5,7 +5,7 @@ Comment=Display and control your Android device # For some users, the PATH or ADB environment variables are set from the shell # startup file, like .bashrc or .zshrc… Run an interactive shell to get # environment correctly initialized. -Exec=/bin/sh -c "\"\$SHELL\" -i -c scrcpy" +Exec=/bin/sh -c "\\$SHELL -i -c scrcpy" Icon=scrcpy Terminal=false Type=Application From d037b02cc2205a4b4767340347bd562b6e7c68bc Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 24 Nov 2023 21:39:46 +0100 Subject: [PATCH 1717/2244] Fix scrcpy-console.desktop The argument passed to scrcpy was not applied, the full command must be passed as a single argument. PR #4448 --- app/data/scrcpy-console.desktop | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/data/scrcpy-console.desktop b/app/data/scrcpy-console.desktop index 71c0f001..fccd42b7 100644 --- a/app/data/scrcpy-console.desktop +++ b/app/data/scrcpy-console.desktop @@ -5,7 +5,7 @@ Comment=Display and control your Android device # For some users, the PATH or ADB environment variables are set from the shell # startup file, like .bashrc or .zshrc… Run an interactive shell to get # environment correctly initialized. -Exec=/bin/sh -c "\\$SHELL -i -c scrcpy --pause-on-exit=if-error" +Exec=/bin/sh -c "\\$SHELL -i -c 'scrcpy --pause-on-exit=if-error'" Icon=scrcpy Terminal=true Type=Application From 5f3fb843f59b058b7c38b5f7b1a561befc64e706 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 25 Nov 2023 21:38:23 +0100 Subject: [PATCH 1718/2244] Bump version to 2.3 The previous version bump to 2.2 was incorrect, it was updated by: ./bump_version v2.2 instead of: ./bump_version 2.2 Correctly bump to version 2.3. Refs #4433 --- app/scrcpy-windows.rc | 2 +- meson.build | 2 +- server/build.gradle | 4 ++-- server/build_without_gradle.sh | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/scrcpy-windows.rc b/app/scrcpy-windows.rc index 832817d8..4540077c 100644 --- a/app/scrcpy-windows.rc +++ b/app/scrcpy-windows.rc @@ -13,7 +13,7 @@ BEGIN VALUE "LegalCopyright", "Romain Vimont, Genymobile" VALUE "OriginalFilename", "scrcpy.exe" VALUE "ProductName", "scrcpy" - VALUE "ProductVersion", "v2.2" + VALUE "ProductVersion", "2.3" END END BLOCK "VarFileInfo" diff --git a/meson.build b/meson.build index d1f67e38..43898157 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('scrcpy', 'c', - version: 'v2.2', + version: '2.3', meson_version: '>= 0.48', default_options: [ 'c_std=c11', diff --git a/server/build.gradle b/server/build.gradle index 1bb31360..45bf0cc8 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -7,8 +7,8 @@ android { applicationId "com.genymobile.scrcpy" minSdkVersion 21 targetSdkVersion 34 - versionCode 200 - versionName "v2.2" + versionCode 20300 + versionName "2.3" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index 5ab90a0a..9f153e2a 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=v2.2 +SCRCPY_VERSION_NAME=2.3 PLATFORM=${ANDROID_PLATFORM:-34} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-34.0.0} From 5e061636f65a5b95432d8dd5a64b4dcd2b7dd8c9 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 25 Nov 2023 22:15:07 +0100 Subject: [PATCH 1719/2244] Update links to v2.3 --- README.md | 2 +- doc/build.md | 6 +++--- doc/windows.md | 12 ++++++------ install_release.sh | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index b8ef9df8..9a8d688a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# scrcpy (v2.2) +# scrcpy (v2.3) scrcpy diff --git a/doc/build.md b/doc/build.md index 15c567b5..91e2fac8 100644 --- a/doc/build.md +++ b/doc/build.md @@ -233,10 +233,10 @@ install` must be run as root)._ #### Option 2: Use prebuilt server - - [`scrcpy-server-v2.2`][direct-scrcpy-server] - SHA-256: `c85c4aa84305efb69115cd497a120ebdd10258993b4cf123a8245b3d99d49874` + - [`scrcpy-server-v2.3`][direct-scrcpy-server] + SHA-256: `8daed514d7796fca6987dc973e201bd15ba51d0f7258973dec92d9ded00dbd5f` -[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v2.2/scrcpy-server-v2.2 +[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v2.3/scrcpy-server-v2.3 Download the prebuilt server somewhere, and specify its path during the Meson configuration: diff --git a/doc/windows.md b/doc/windows.md index bd4a69f7..93231795 100644 --- a/doc/windows.md +++ b/doc/windows.md @@ -4,14 +4,14 @@ Download the [latest release]: - - [`scrcpy-win64-v2.2.zip`][direct-win64] (64-bit) - SHA-256: `9f9da88ac4c8319dcb9bf852f2d9bba942bac663413383419cddf64eaa5685bd` - - [`scrcpy-win32-v2.2.zip`][direct-win32] (32-bit) - SHA-256: `cb84269fc847b8b880e320879492a1ae6c017b42175f03e199530f7a53be9d74` + - [`scrcpy-win64-v2.3.zip`][direct-win64] (64-bit) + SHA-256: `a2fdd2733bd337261bb493e77d990078a23e7a40149dd0c0dc45725c929a715f` + - [`scrcpy-win32-v2.3.zip`][direct-win32] (32-bit) + SHA-256: `dfdbb69a872d717aed5bcfe352e571564c357fdb7a9c172d69f450fdf5154a0a` [latest release]: https://github.com/Genymobile/scrcpy/releases/latest -[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v2.2/scrcpy-win64-v2.2.zip -[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v2.2/scrcpy-win32-v2.2.zip +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v2.3/scrcpy-win64-v2.3.zip +[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v2.3/scrcpy-win32-v2.3.zip and extract it. diff --git a/install_release.sh b/install_release.sh index adad85f7..a81b7a45 100755 --- a/install_release.sh +++ b/install_release.sh @@ -2,8 +2,8 @@ set -e BUILDDIR=build-auto -PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v2.2/scrcpy-server-v2.2 -PREBUILT_SERVER_SHA256=c85c4aa84305efb69115cd497a120ebdd10258993b4cf123a8245b3d99d49874 +PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v2.3/scrcpy-server-v2.3 +PREBUILT_SERVER_SHA256=8daed514d7796fca6987dc973e201bd15ba51d0f7258973dec92d9ded00dbd5f echo "[scrcpy] Downloading prebuilt server..." wget "$PREBUILT_SERVER_URL" -O scrcpy-server From 4135c411af419f4f86dc9ec9301c88012d616c49 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 25 Nov 2023 23:53:30 +0100 Subject: [PATCH 1720/2244] Fix compilation error Fix the following warning/error: ../app/src/cli.c:2158:17: warning: a label can only be part of a statement and a declaration is not a statement [-Wpedantic] With some compilers, this is an error rather than a pedantic warning. Refs --- app/src/cli.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/cli.c b/app/src/cli.c index f57b75ef..fd4525f5 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -2154,7 +2154,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; } break; - case OPT_ORIENTATION: + case OPT_ORIENTATION: { enum sc_orientation orientation; if (!parse_orientation(optarg, &orientation)) { return false; @@ -2162,6 +2162,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], opts->display_orientation = orientation; opts->record_orientation = orientation; break; + } case OPT_RENDER_DRIVER: opts->render_driver = optarg; break; From 140a49b8bee1122b28d47b4385eccece7480f18e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 26 Nov 2023 19:20:24 +0100 Subject: [PATCH 1721/2244] Add workaround for Samsung devices issues On some Samsung devices, DisplayManagerGlobal.getDisplayInfoLocked() calls ActivityThread.currentActivityThread().getConfiguration(), which requires a non-null ConfigurationController. Fixes --- .../com/genymobile/scrcpy/Workarounds.java | 33 +++++++++++++++++-- 1 file changed, 31 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 db9c9629..8781a783 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java +++ b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java @@ -49,6 +49,7 @@ public final class Workarounds { } public static void apply(boolean audio, boolean camera) { + boolean mustFillConfigurationController = false; boolean mustFillAppInfo = false; boolean mustFillAppContext = false; @@ -85,11 +86,23 @@ public final class Workarounds { mustFillAppContext = true; } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + // On some Samsung devices, DisplayManagerGlobal.getDisplayInfoLocked() calls ActivityThread.currentActivityThread().getConfiguration(), + // which requires a non-null ConfigurationController. + // ConfigurationController was introduced in Android 12, so do not attempt to set it on lower versions. + // + mustFillConfigurationController = true; + } + + if (mustFillConfigurationController) { + // Must be call before fillAppContext() because it is necessary to get a valid system context + fillConfigurationController(); + } if (mustFillAppInfo) { - Workarounds.fillAppInfo(); + fillAppInfo(); } if (mustFillAppContext) { - Workarounds.fillAppContext(); + fillAppContext(); } } @@ -149,6 +162,22 @@ public final class Workarounds { } } + private static void fillConfigurationController() { + try { + Class configurationControllerClass = Class.forName("android.app.ConfigurationController"); + Class activityThreadInternalClass = Class.forName("android.app.ActivityThreadInternal"); + Constructor configurationControllerConstructor = configurationControllerClass.getDeclaredConstructor(activityThreadInternalClass); + configurationControllerConstructor.setAccessible(true); + Object configurationController = configurationControllerConstructor.newInstance(ACTIVITY_THREAD); + + Field configurationControllerField = ACTIVITY_THREAD_CLASS.getDeclaredField("mConfigurationController"); + configurationControllerField.setAccessible(true); + configurationControllerField.set(ACTIVITY_THREAD, configurationController); + } catch (Throwable throwable) { + Ln.d("Could not fill configuration: " + throwable.getMessage()); + } + } + static Context getSystemContext() { try { Method getSystemContextMethod = ACTIVITY_THREAD_CLASS.getDeclaredMethod("getSystemContext"); From bd9292931e868621294ec64ea85179a2803976ef Mon Sep 17 00:00:00 2001 From: Johannes Neyer Date: Fri, 17 Nov 2023 16:24:34 +0100 Subject: [PATCH 1722/2244] Mention exclusive_caps mode in v4l2 documentation PR #4435 Signed-off-by: Romain Vimont --- doc/v4l2.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/doc/v4l2.md b/doc/v4l2.md index 23c99912..54272b2b 100644 --- a/doc/v4l2.md +++ b/doc/v4l2.md @@ -21,6 +21,13 @@ This will create a new video device in `/dev/videoN`, where `N` is an integer (more [options](https://github.com/umlaeute/v4l2loopback#options) are available to create several devices or devices with specific IDs). +If you encounter problems detecting your device with Chrome/WebRTC, you can try +`exclusive_caps` mode: + +``` +sudo modprobe v4l2loopback exclusive_caps=1 +``` + To list the enabled devices: ```bash From bf056b1fee904391bc932381c01052fb975ac62e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 29 Nov 2023 12:14:07 +0100 Subject: [PATCH 1723/2244] Do not initialize SDL video when not necessary The SDL video subsystem is required for video playback and clipboard synchronization. If neither is used, it is not necessary to initialize it. Refs 5e59ed31352251791679e5931d7e5abf0c2d18f6 Refs 110b3a16f6d02124a4567d2ab79fcb74d78f949f Refs #4418 Refs #4477 --- app/src/scrcpy.c | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index d62a5f52..0b0b8bad 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -419,12 +419,16 @@ scrcpy(struct scrcpy_options *options) { sdl_set_hints(options->render_driver); } - // Initialize the video subsystem even if --no-video or --no-video-playback - // is passed so that clipboard synchronization still works. - // - if (SDL_Init(SDL_INIT_VIDEO)) { - LOGE("Could not initialize SDL video: %s", SDL_GetError()); - goto end; + if (options->video_playback || + (options->control && options->clipboard_autosync)) { + // Initialize the video subsystem even if --no-video or + // --no-video-playback is passed so that clipboard synchronization + // still works. + // + if (SDL_Init(SDL_INIT_VIDEO)) { + LOGE("Could not initialize SDL video: %s", SDL_GetError()); + goto end; + } } if (options->audio_playback) { From 9497f39fb47e899df1247942f90eba5daa0cf204 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 29 Nov 2023 12:16:05 +0100 Subject: [PATCH 1724/2244] Do not fail if SDL_INIT_VIDEO fails without video The SDL video subsystem may be initialized so that clipboard synchronization works even without video playback. But if the video subsystem initialization fails (e.g. because no video device is available), consider it as an error only if video playback is enabled. Refs 5e59ed31352251791679e5931d7e5abf0c2d18f6 Fixes #4477 --- app/src/scrcpy.c | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 0b0b8bad..cf2e7e47 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -426,8 +426,13 @@ scrcpy(struct scrcpy_options *options) { // still works. // if (SDL_Init(SDL_INIT_VIDEO)) { - LOGE("Could not initialize SDL video: %s", SDL_GetError()); - goto end; + // If it fails, it is an error only if video playback is enabled + if (options->video_playback) { + LOGE("Could not initialize SDL video: %s", SDL_GetError()); + goto end; + } else { + LOGW("Could not initialize SDL video: %s", SDL_GetError()); + } } } From ef79fcbbd27d9f6e8096c278d2975c7c71bc67b9 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 1 Dec 2023 21:13:01 +0100 Subject: [PATCH 1725/2244] Fix AV1 demuxing For AV1, the config packet must not be merged with the next non-config packet. This fixes the following error when passing --video-codec=av1: > INFO: [FFmpeg] libdav1d 1.3.0 > ERROR: [FFmpeg] Unknown OBU type 0 of size 29393 > ERROR: [FFmpeg] Error parsing OBU data > ERROR: Decoder 'video': could not send video packet: -1094995529 PR #4487 --- app/src/demuxer.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/demuxer.c b/app/src/demuxer.c index c9ee8f3c..c27ea292 100644 --- a/app/src/demuxer.c +++ b/app/src/demuxer.c @@ -227,8 +227,9 @@ run_demuxer(void *data) { } // Config packets must be merged with the next non-config packet only for - // video streams - bool must_merge_config_packet = codec->type == AVMEDIA_TYPE_VIDEO; + // H.26x + bool must_merge_config_packet = raw_codec_id == SC_CODEC_ID_H264 + || raw_codec_id == SC_CODEC_ID_H265; struct sc_packet_merger merger; From 40f2560d987fbc88711b3aac14398d38c0e2a8f6 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 2 Dec 2023 12:30:19 +0100 Subject: [PATCH 1726/2244] Bump version to 2.3.1 --- app/scrcpy-windows.rc | 2 +- meson.build | 2 +- server/build.gradle | 4 ++-- server/build_without_gradle.sh | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/scrcpy-windows.rc b/app/scrcpy-windows.rc index 4540077c..895b9c93 100644 --- a/app/scrcpy-windows.rc +++ b/app/scrcpy-windows.rc @@ -13,7 +13,7 @@ BEGIN VALUE "LegalCopyright", "Romain Vimont, Genymobile" VALUE "OriginalFilename", "scrcpy.exe" VALUE "ProductName", "scrcpy" - VALUE "ProductVersion", "2.3" + VALUE "ProductVersion", "2.3.1" END END BLOCK "VarFileInfo" diff --git a/meson.build b/meson.build index 43898157..11b974e0 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('scrcpy', 'c', - version: '2.3', + version: '2.3.1', meson_version: '>= 0.48', default_options: [ 'c_std=c11', diff --git a/server/build.gradle b/server/build.gradle index 45bf0cc8..1a18d997 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -7,8 +7,8 @@ android { applicationId "com.genymobile.scrcpy" minSdkVersion 21 targetSdkVersion 34 - versionCode 20300 - versionName "2.3" + versionCode 20301 + versionName "2.3.1" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index 9f153e2a..69d85679 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -12,7 +12,7 @@ set -e SCRCPY_DEBUG=false -SCRCPY_VERSION_NAME=2.3 +SCRCPY_VERSION_NAME=2.3.1 PLATFORM=${ANDROID_PLATFORM:-34} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-34.0.0} From c6ff78f4147ad5505b45cce8f1d6f44c8585a9b5 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 2 Dec 2023 12:39:05 +0100 Subject: [PATCH 1727/2244] Update links to v2.3.1 --- README.md | 2 +- doc/build.md | 6 +++--- doc/windows.md | 12 ++++++------ install_release.sh | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 9a8d688a..8fabd556 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# scrcpy (v2.3) +# scrcpy (v2.3.1) scrcpy diff --git a/doc/build.md b/doc/build.md index 91e2fac8..7e3c84e9 100644 --- a/doc/build.md +++ b/doc/build.md @@ -233,10 +233,10 @@ install` must be run as root)._ #### Option 2: Use prebuilt server - - [`scrcpy-server-v2.3`][direct-scrcpy-server] - SHA-256: `8daed514d7796fca6987dc973e201bd15ba51d0f7258973dec92d9ded00dbd5f` + - [`scrcpy-server-v2.3.1`][direct-scrcpy-server] + SHA-256: `f6814822fc308a7a532f253485c9038183c6296a6c5df470a9e383b4f8e7605b` -[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v2.3/scrcpy-server-v2.3 +[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v2.3.1/scrcpy-server-v2.3.1 Download the prebuilt server somewhere, and specify its path during the Meson configuration: diff --git a/doc/windows.md b/doc/windows.md index 93231795..60fd7986 100644 --- a/doc/windows.md +++ b/doc/windows.md @@ -4,14 +4,14 @@ Download the [latest release]: - - [`scrcpy-win64-v2.3.zip`][direct-win64] (64-bit) - SHA-256: `a2fdd2733bd337261bb493e77d990078a23e7a40149dd0c0dc45725c929a715f` - - [`scrcpy-win32-v2.3.zip`][direct-win32] (32-bit) - SHA-256: `dfdbb69a872d717aed5bcfe352e571564c357fdb7a9c172d69f450fdf5154a0a` + - [`scrcpy-win64-v2.3.1.zip`][direct-win64] (64-bit) + SHA-256: `f1f78ac98214078425804e524a1bed515b9d4b8a05b78d210a4ced2b910b262d` + - [`scrcpy-win32-v2.3.1.zip`][direct-win32] (32-bit) + SHA-256: `5dffc2d432e9b8b5b0e16f12e71428c37c70d9124cfbe7620df0b41b7efe91ff` [latest release]: https://github.com/Genymobile/scrcpy/releases/latest -[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v2.3/scrcpy-win64-v2.3.zip -[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v2.3/scrcpy-win32-v2.3.zip +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v2.3.1/scrcpy-win64-v2.3.1.zip +[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v2.3.1/scrcpy-win32-v2.3.1.zip and extract it. diff --git a/install_release.sh b/install_release.sh index a81b7a45..d8dbd951 100755 --- a/install_release.sh +++ b/install_release.sh @@ -2,8 +2,8 @@ set -e BUILDDIR=build-auto -PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v2.3/scrcpy-server-v2.3 -PREBUILT_SERVER_SHA256=8daed514d7796fca6987dc973e201bd15ba51d0f7258973dec92d9ded00dbd5f +PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v2.3.1/scrcpy-server-v2.3.1 +PREBUILT_SERVER_SHA256=f6814822fc308a7a532f253485c9038183c6296a6c5df470a9e383b4f8e7605b echo "[scrcpy] Downloading prebuilt server..." wget "$PREBUILT_SERVER_URL" -O scrcpy-server From 3001f8a2d581a921920d18c088d42e0cd747bf41 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 3 Dec 2023 18:00:05 +0100 Subject: [PATCH 1728/2244] Adapt AudioRecord workaround to Android 14 Android 14 added a new int parameter "halInputFlags" to an internal method: Fixes #4492 --- .../com/genymobile/scrcpy/Workarounds.java | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java index 8781a783..448e7099 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java +++ b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java @@ -285,16 +285,28 @@ public final class Workarounds { Method getParcelMethod = attributionSourceState.getClass().getDeclaredMethod("getParcel"); Parcel attributionSourceParcel = (Parcel) getParcelMethod.invoke(attributionSourceState); - // private native int native_setup(Object audiorecordThis, - // Object /*AudioAttributes*/ attributes, - // int[] sampleRate, int channelMask, int channelIndexMask, int audioFormat, - // int buffSizeInBytes, int[] sessionId, @NonNull Parcel attributionSource, - // long nativeRecordInJavaObj, int maxSharedAudioHistoryMs); - Method nativeSetupMethod = AudioRecord.class.getDeclaredMethod("native_setup", Object.class, Object.class, int[].class, int.class, - int.class, int.class, int.class, int[].class, Parcel.class, long.class, int.class); - nativeSetupMethod.setAccessible(true); - initResult = (int) nativeSetupMethod.invoke(audioRecord, new WeakReference(audioRecord), attributes, sampleRateArray, - channelMask, channelIndexMask, audioRecord.getAudioFormat(), bufferSizeInBytes, session, attributionSourceParcel, 0L, 0); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + // private native int native_setup(Object audiorecordThis, + // Object /*AudioAttributes*/ attributes, + // int[] sampleRate, int channelMask, int channelIndexMask, int audioFormat, + // int buffSizeInBytes, int[] sessionId, @NonNull Parcel attributionSource, + // long nativeRecordInJavaObj, int maxSharedAudioHistoryMs); + Method nativeSetupMethod = AudioRecord.class.getDeclaredMethod("native_setup", Object.class, Object.class, int[].class, + int.class, int.class, int.class, int.class, int[].class, Parcel.class, long.class, int.class); + nativeSetupMethod.setAccessible(true); + initResult = (int) nativeSetupMethod.invoke(audioRecord, new WeakReference(audioRecord), attributes, + sampleRateArray, channelMask, channelIndexMask, audioRecord.getAudioFormat(), bufferSizeInBytes, session, + attributionSourceParcel, 0L, 0); + } else { + // Android 14 added a new int parameter "halInputFlags" + // + Method nativeSetupMethod = AudioRecord.class.getDeclaredMethod("native_setup", Object.class, Object.class, int[].class, + int.class, int.class, int.class, int.class, int[].class, Parcel.class, long.class, int.class, int.class); + nativeSetupMethod.setAccessible(true); + initResult = (int) nativeSetupMethod.invoke(audioRecord, new WeakReference(audioRecord), attributes, + sampleRateArray, channelMask, channelIndexMask, audioRecord.getAudioFormat(), bufferSizeInBytes, session, + attributionSourceParcel, 0L, 0, 0); + } } } From 5ce8672ebc56b7286e1078a39abc64903e5664d0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 3 Dec 2023 21:22:30 +0100 Subject: [PATCH 1729/2244] Add clipboard workaround for IQOO device Fixes #4492 --- .../scrcpy/wrappers/ClipboardManager.java | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 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 783a3407..0866d42d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java @@ -41,8 +41,13 @@ public final class ClipboardManager { getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class, int.class); getMethodVersion = 2; } catch (NoSuchMethodException e3) { - getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class, String.class); - getMethodVersion = 3; + try { + getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class, String.class); + getMethodVersion = 3; + } catch (NoSuchMethodException e4) { + getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class, int.class, boolean.class); + getMethodVersion = 4; + } } } } @@ -87,8 +92,11 @@ public final class ClipboardManager { return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID); case 2: return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0); - default: + case 3: return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID, null); + default: + // The last boolean parameter is "userOperate" + return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0, true); } } From 1beec99f8283713b1fbf0b3704eb4dceecc9a590 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 3 Dec 2023 10:48:53 +0100 Subject: [PATCH 1730/2244] Explicitly exit cleanup process This avoids an internal crash reported in `adb logcat`. Refs #4456 --- server/src/main/java/com/genymobile/scrcpy/CleanUp.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java index b3a1aac1..c84e25bb 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java +++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java @@ -187,5 +187,7 @@ public final class CleanUp { Device.setScreenPowerMode(Device.POWER_MODE_NORMAL); } } + + System.exit(0); } } From c9a4d2b38f318a4887e3db48a7d1bcf0affc0250 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 1 Dec 2023 19:14:22 +0100 Subject: [PATCH 1731/2244] Use up-to-date values on display fold change When a display is folded or unfolded, the maxSize may have been updated since the option was passed, and deviceSize must be updated. Refs #4469 --- server/src/main/java/com/genymobile/scrcpy/Device.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index b51ad8d3..2324ce90 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -45,11 +45,11 @@ public final class Device { void onClipboardTextChanged(String text); } - private final Size deviceSize; private final Rect crop; private int maxSize; private final int lockVideoOrientation; + private Size deviceSize; private ScreenInfo screenInfo; private RotationListener rotationListener; private FoldListener foldListener; @@ -116,8 +116,8 @@ public final class Device { return; } - screenInfo = ScreenInfo.computeScreenInfo(displayInfo.getRotation(), displayInfo.getSize(), options.getCrop(), - options.getMaxSize(), options.getLockVideoOrientation()); + deviceSize = displayInfo.getSize(); + screenInfo = ScreenInfo.computeScreenInfo(displayInfo.getRotation(), deviceSize, crop, maxSize, lockVideoOrientation); // notify if (foldListener != null) { foldListener.onFoldChanged(displayId, folded); From 5a6b8310cae1e0741b4375ca760d9c8dd49822c4 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 13 Dec 2023 12:50:06 +0100 Subject: [PATCH 1732/2244] Add note about official website --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 8fabd556..0a3d03cb 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ +**This GitHub repo () is the only official +source for the project. Do not download releases from random websites, even if +their name contains `scrcpy`.** + # scrcpy (v2.3.1) scrcpy From cbce42336dce0bb4cd2c47ed7dfa56d40fc3aca1 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 13 Dec 2023 13:41:34 +0100 Subject: [PATCH 1733/2244] Fix manpage syntax The '-' character must be escaped. Fixes #4528 --- app/scrcpy.1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 0c34b4e2..8ca4a773 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -124,7 +124,7 @@ Use USB device (if there is exactly one, like adb -d). Also see \fB\-e\fR (\fB\-\-select\-tcpip\fR). .TP -.BI "\-\-disable-screensaver" +.BI "\-\-disable\-screensaver" Disable screensaver while scrcpy is running. .TP From af69689ec1d52b442b0a4e1461ca71853c660cc6 Mon Sep 17 00:00:00 2001 From: Harsh Shandilya Date: Thu, 14 Dec 2023 21:21:57 +0530 Subject: [PATCH 1734/2244] Fix bash completion syntax PR #4532 Signed-off-by: Romain Vimont --- app/data/bash-completion/scrcpy | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index 0c854310..a0490157 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -115,8 +115,7 @@ _scrcpy() { COMPREPLY=($(compgen -W 'front back external' -- "$cur")) return ;; - --orientation - --display-orientation) + --orientation|--display-orientation) COMPREPLY=($(compgen -> '0 90 180 270 flip0 flip90 flip180 flip270' -- "$cur")) return ;; From 604dfd7c6b07f583521c92f8391d3fdb6b1576e8 Mon Sep 17 00:00:00 2001 From: Harsh Shandilya Date: Thu, 14 Dec 2023 21:25:32 +0530 Subject: [PATCH 1735/2244] Fix incorrect compgen usage PR #4532 Signed-off-by: Romain Vimont --- app/data/bash-completion/scrcpy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index a0490157..78aa539d 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -116,11 +116,11 @@ _scrcpy() { return ;; --orientation|--display-orientation) - COMPREPLY=($(compgen -> '0 90 180 270 flip0 flip90 flip180 flip270' -- "$cur")) + COMPREPLY=($(compgen -W '0 90 180 270 flip0 flip90 flip180 flip270' -- "$cur")) return ;; --record-orientation) - COMPREPLY=($(compgen -> '0 90 180 270' -- "$cur")) + COMPREPLY=($(compgen -W '0 90 180 270' -- "$cur")) return ;; --lock-video-orientation) From d2ed4510a76e0d273f9088bce6e85334b922c9cb Mon Sep 17 00:00:00 2001 From: Till Rathmann Date: Wed, 13 Dec 2023 17:04:02 +0100 Subject: [PATCH 1736/2244] Simulate tilt multitouch event by pressing Shift PR #4529 Signed-off-by: Romain Vimont --- app/scrcpy.1 | 6 +++++- app/src/cli.c | 6 +++++- app/src/input_manager.c | 40 ++++++++++++++++++++++++++++++++-------- app/src/input_manager.h | 2 ++ doc/control.md | 8 ++++++-- doc/shortcuts.md | 3 ++- 6 files changed, 52 insertions(+), 13 deletions(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 8ca4a773..beaa99ab 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -642,7 +642,11 @@ Enable/disable FPS counter (print frames/second in logs) .TP .B Ctrl+click-and-move -Pinch-to-zoom from the center of the screen +Pinch-to-zoom and rotate from the center of the screen + +.TP +.B Shift+click-and-move +Tilt (slide vertically with two fingers) .TP .B Drag & drop APK file diff --git a/app/src/cli.c b/app/src/cli.c index fd4525f5..c580c959 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -947,7 +947,11 @@ static const struct sc_shortcut shortcuts[] = { }, { .shortcuts = { "Ctrl+click-and-move" }, - .text = "Pinch-to-zoom from the center of the screen", + .text = "Pinch-to-zoom and rotate from the center of the screen", + }, + { + .shortcuts = { "Shift+click-and-move" }, + .text = "Tilt (slide vertically with two fingers)", }, { .shortcuts = { "Drag & drop APK file" }, diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 9a487836..76cfbd92 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -76,6 +76,8 @@ sc_input_manager_init(struct sc_input_manager *im, im->sdl_shortcut_mods.count = shortcut_mods->count; im->vfinger_down = false; + im->vfinger_invert_x = false; + im->vfinger_invert_y = false; im->last_keycode = SDLK_UNKNOWN; im->last_mod = 0; @@ -347,9 +349,14 @@ simulate_virtual_finger(struct sc_input_manager *im, } static struct sc_point -inverse_point(struct sc_point point, struct sc_size size) { - point.x = size.width - point.x; - point.y = size.height - point.y; +inverse_point(struct sc_point point, struct sc_size size, + bool invert_x, bool invert_y) { + if (invert_x) { + point.x = size.width - point.x; + } + if (invert_y) { + point.y = size.height - point.y; + } return point; } @@ -605,7 +612,9 @@ sc_input_manager_process_mouse_motion(struct sc_input_manager *im, struct sc_point mouse = sc_screen_convert_window_to_frame_coords(im->screen, event->x, event->y); - struct sc_point vfinger = inverse_point(mouse, im->screen->frame_size); + struct sc_point vfinger = inverse_point(mouse, im->screen->frame_size, + im->vfinger_invert_x, + im->vfinger_invert_y); simulate_virtual_finger(im, AMOTION_EVENT_ACTION_MOVE, vfinger); } } @@ -726,7 +735,7 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im, return; } - // Pinch-to-zoom simulation. + // Pinch-to-zoom, rotate and tilt 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 @@ -735,14 +744,29 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im, // // In other words, the center of the rotation/scaling is the center of the // screen. -#define CTRL_PRESSED (SDL_GetModState() & (KMOD_LCTRL | KMOD_RCTRL)) + // + // To simulate a tilt gesture (a vertical slide with two fingers), Shift + // can be used instead of Ctrl. The "virtual finger" has a position + // inverted with respect to the vertical axis of symmetry in the middle of + // the screen. + const SDL_Keymod keymod = SDL_GetModState(); + const bool ctrl_pressed = keymod & KMOD_CTRL; + const bool shift_pressed = keymod & KMOD_SHIFT; if (event->button == SDL_BUTTON_LEFT && - ((down && !im->vfinger_down && CTRL_PRESSED) || + ((down && !im->vfinger_down && + ((ctrl_pressed && !shift_pressed) || + (!ctrl_pressed && shift_pressed))) || (!down && im->vfinger_down))) { struct sc_point mouse = sc_screen_convert_window_to_frame_coords(im->screen, event->x, event->y); - struct sc_point vfinger = inverse_point(mouse, im->screen->frame_size); + if (down) { + im->vfinger_invert_x = ctrl_pressed || shift_pressed; + im->vfinger_invert_y = ctrl_pressed; + } + struct sc_point vfinger = inverse_point(mouse, im->screen->frame_size, + im->vfinger_invert_x, + im->vfinger_invert_y); enum android_motionevent_action action = down ? AMOTION_EVENT_ACTION_DOWN : AMOTION_EVENT_ACTION_UP; diff --git a/app/src/input_manager.h b/app/src/input_manager.h index b5a762eb..2ce11b03 100644 --- a/app/src/input_manager.h +++ b/app/src/input_manager.h @@ -32,6 +32,8 @@ struct sc_input_manager { } sdl_shortcut_mods; bool vfinger_down; + bool vfinger_invert_x; + bool vfinger_invert_y; // Tracks the number of identical consecutive shortcut key down events. // Not to be confused with event->repeat, which counts the number of diff --git a/doc/control.md b/doc/control.md index 0b060775..595e910e 100644 --- a/doc/control.md +++ b/doc/control.md @@ -85,7 +85,7 @@ way as MOD+Shift+v). To disable automatic clipboard synchronization, use `--no-clipboard-autosync`. -## Pinch-to-zoom +## Pinch-to-zoom, rotate and tilt simulation To simulate "pinch-to-zoom": Ctrl+_click-and-move_. @@ -93,8 +93,12 @@ More precisely, hold down 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. +To simulate a tilt gesture: Shift+_click-and-move-up-or-down_. + Technically, _scrcpy_ generates additional touch events from a "virtual finger" -at a location inverted through the center of the screen. +at a location inverted through the center of the screen. When pressing +Ctrl the x and y coordinates are inverted. Using Shift +only inverts x. ## Key repeat diff --git a/doc/shortcuts.md b/doc/shortcuts.md index c0fc2842..21bccbd9 100644 --- a/doc/shortcuts.md +++ b/doc/shortcuts.md @@ -49,7 +49,8 @@ _[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_ + | Pinch-to-zoom/rotate | Ctrl+_click-and-move_ + | Tilt (slide vertically with 2 fingers) | Shift+_click-and-move_ | Drag & drop APK file | Install APK from computer | Drag & drop non-APK file | [Push file to device](control.md#push-file-to-device) From 4cd61b5a9001043f1054502f0c29465707260cb1 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 16 Dec 2023 20:12:58 +0100 Subject: [PATCH 1737/2244] Fix checkstyle violation Reported by checkstyle: > [ant:checkstyle] [INFO] > scrcpy/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java:48: > Line is longer than 150 characters (found 167). [LineLength] --- .../java/com/genymobile/scrcpy/wrappers/ClipboardManager.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 0866d42d..15f0ee74 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java @@ -45,7 +45,8 @@ public final class ClipboardManager { getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class, String.class); getMethodVersion = 3; } catch (NoSuchMethodException e4) { - getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class, int.class, boolean.class); + getPrimaryClipMethod = manager.getClass() + .getMethod("getPrimaryClip", String.class, String.class, int.class, int.class, boolean.class); getMethodVersion = 4; } } From ec41896c853325d670e361f641f4490ed71b641f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 16 Dec 2023 20:06:45 +0100 Subject: [PATCH 1738/2244] Fix integer overflow for audio packet duration The result is assigned to a long (64-bit signed integer), but the intermediate multiplication was stored in an int (32-bit signed integer). This value is only used as a fallback when no timestamp could be retrieved, that's why it did not cause too much harm so far. Fixes #4536 --- server/src/main/java/com/genymobile/scrcpy/AudioCapture.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java b/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java index e3de50e6..76e2f63b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java @@ -159,7 +159,7 @@ public final class AudioCapture { pts = nextPts; } - long durationUs = r * 1000000 / (CHANNELS * BYTES_PER_SAMPLE * SAMPLE_RATE); + long durationUs = r * 1000000L / (CHANNELS * BYTES_PER_SAMPLE * SAMPLE_RATE); nextPts = pts + durationUs; if (previousPts != 0 && pts < previousPts + ONE_SAMPLE_US) { From 6a58891e13bc3d5b531aa93718591ec495c4fb6f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 16 Dec 2023 20:09:08 +0100 Subject: [PATCH 1739/2244] Use current time as initial timestamp on error If the initial timestamp could not be retrieved, use the current time as returned by System.nanoTime(). In practice, it is the same time base as AudioRecord timestamps. Fixes #4536 --- server/src/main/java/com/genymobile/scrcpy/AudioCapture.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java b/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java index 76e2f63b..45634c70 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java @@ -153,7 +153,8 @@ public final class AudioCapture { previousRecorderTimestamp = timestamp.nanoTime; } else { if (nextPts == 0) { - Ln.w("Could not get any audio timestamp"); + Ln.w("Could not get initial audio timestamp"); + nextPts = System.nanoTime() / 1000; } // compute from previous timestamp and packet size pts = nextPts; From cd4056d0f333aee26efa8ba083e08c59b6d63a70 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 2 Jan 2024 10:22:06 +0100 Subject: [PATCH 1740/2244] Fix include formatting --- app/src/audio_player.h | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/src/audio_player.h b/app/src/audio_player.h index a03e9e35..30378246 100644 --- a/app/src/audio_player.h +++ b/app/src/audio_player.h @@ -4,16 +4,16 @@ #include "common.h" #include -#include "trait/frame_sink.h" -#include -#include -#include -#include - #include #include #include +#include "trait/frame_sink.h" +#include "util/audiobuf.h" +#include "util/average.h" +#include "util/thread.h" +#include "util/tick.h" + struct sc_audio_player { struct sc_frame_sink frame_sink; From d067a11478e5a6312325bdd051c4ffd1362945c5 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 7 Jan 2024 21:06:09 +0100 Subject: [PATCH 1741/2244] Do not power on if no video Power on the device on start only if video capture is enabled. Note that it only impacts display mirroring, since control is completely disabled if video source is camera. Refs 110b3a16f6d02124a4567d2ab79fcb74d78f949f --- app/src/cli.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/cli.c b/app/src/cli.c index c580c959..f7d7e390 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -2398,6 +2398,8 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], if (!opts->video) { opts->video_playback = false; + // Do not power on the device on start if video capture is disabled + opts->power_on = false; } if (!opts->audio) { From 2ad93d1fc0094ba8263a05f0b162f2607aa68ea9 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 15 Jan 2024 22:01:19 +0100 Subject: [PATCH 1742/2244] Fix scrcpy_otg() return value on error The function now returns an enum scrcpy_exit_code, not a bool. --- app/src/usb/scrcpy_otg.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/usb/scrcpy_otg.c b/app/src/usb/scrcpy_otg.c index 6a7fd79b..dfb0b9e9 100644 --- a/app/src/usb/scrcpy_otg.c +++ b/app/src/usb/scrcpy_otg.c @@ -62,7 +62,7 @@ scrcpy_otg(struct scrcpy_options *options) { // Minimal SDL initialization if (SDL_Init(SDL_INIT_EVENTS)) { LOGE("Could not initialize SDL: %s", SDL_GetError()); - return false; + return SCRCPY_EXIT_FAILURE; } atexit(SDL_Quit); From 5187f7254e4b556e3731fdb77fda68ae2e9fddce Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 16 Jan 2024 18:59:36 +0100 Subject: [PATCH 1743/2244] Add another clipboard workaround for IQOO device Fixes #4589 Refs 5ce8672ebc56b7286e1078a39abc64903e5664d0 Refs #4492 --- .../scrcpy/wrappers/ClipboardManager.java | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 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 15f0ee74..daea6db3 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java @@ -45,9 +45,16 @@ public final class ClipboardManager { getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class, String.class); getMethodVersion = 3; } catch (NoSuchMethodException e4) { - getPrimaryClipMethod = manager.getClass() - .getMethod("getPrimaryClip", String.class, String.class, int.class, int.class, boolean.class); - getMethodVersion = 4; + try { + getPrimaryClipMethod = manager.getClass() + .getMethod("getPrimaryClip", String.class, String.class, int.class, int.class, boolean.class); + getMethodVersion = 4; + } catch (NoSuchMethodException e5) { + getPrimaryClipMethod = manager.getClass() + .getMethod("getPrimaryClip", String.class, String.class, String.class, String.class, int.class, int.class, + boolean.class); + getMethodVersion = 5; + } } } } @@ -95,9 +102,11 @@ public final class ClipboardManager { return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0); case 3: return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID, null); - default: + case 4: // The last boolean parameter is "userOperate" return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0, true); + default: + return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, null, null, FakeContext.ROOT_UID, 0, true); } } From 7c53a29d72cb0725e960c1b92732193251984558 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 26 Jan 2024 13:13:14 +0100 Subject: [PATCH 1744/2244] Remove useless run script This script was outdated and redundant with ./run. --- meson.build | 2 -- scripts/run-scrcpy.sh | 2 -- 2 files changed, 4 deletions(-) delete mode 100755 scripts/run-scrcpy.sh diff --git a/meson.build b/meson.build index 11b974e0..4ae91f69 100644 --- a/meson.build +++ b/meson.build @@ -16,5 +16,3 @@ endif if get_option('compile_server') subdir('server') endif - -run_target('run', command: ['scripts/run-scrcpy.sh']) diff --git a/scripts/run-scrcpy.sh b/scripts/run-scrcpy.sh deleted file mode 100755 index e93b639f..00000000 --- a/scripts/run-scrcpy.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/env bash -SCRCPY_SERVER_PATH="$MESON_BUILD_ROOT/server/scrcpy-server" "$MESON_BUILD_ROOT/app/scrcpy" From 3333e67452e52a1e0cd1c68181067f9eccdeb582 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 1 Feb 2024 09:18:14 +0100 Subject: [PATCH 1745/2244] Fix memory leak on error Fixes #4636 --- app/src/adb/adb.c | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/adb/adb.c b/app/src/adb/adb.c index 54375451..15c9c85a 100644 --- a/app/src/adb/adb.c +++ b/app/src/adb/adb.c @@ -458,6 +458,7 @@ sc_adb_list_devices(struct sc_intr *intr, unsigned flags, // in the buffer in a single pass LOGW("Result of \"adb devices -l\" does not fit in 64Kb. " "Please report an issue."); + free(buf); return false; } From d25cbc55f2238e2f9e621c83f27d675a7de913aa Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 9 Feb 2024 18:32:48 +0100 Subject: [PATCH 1746/2244] Remove unused field --- .../java/com/genymobile/scrcpy/wrappers/ContentProvider.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java index 8171988e..89c1d0e2 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java @@ -42,8 +42,6 @@ public final class ContentProvider implements Closeable { private Method callMethod; private int callMethodVersion; - private Object attributionSource; - ContentProvider(ActivityManager manager, Object provider, String name, IBinder token) { this.manager = manager; this.provider = provider; From 05b5deacadf25f706a62b981a1558c75b37dea93 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 9 Feb 2024 18:36:27 +0100 Subject: [PATCH 1747/2244] Move service managers creation Create the service managers from each manager wrapper class rather than from their getter in ServiceManager. The way a wrapper retrieve the underlying service is an implementation detail, and it must be consistent with the way it accesses it, so it is better to write the creation in the wrapper. --- .../scrcpy/wrappers/ActivityManager.java | 15 ++++- .../scrcpy/wrappers/ClipboardManager.java | 13 ++++- .../scrcpy/wrappers/DisplayManager.java | 17 +++++- .../scrcpy/wrappers/InputManager.java | 24 +++++++- .../scrcpy/wrappers/PowerManager.java | 7 ++- .../scrcpy/wrappers/ServiceManager.java | 58 +++---------------- .../scrcpy/wrappers/StatusBarManager.java | 7 ++- .../scrcpy/wrappers/WindowManager.java | 7 ++- 8 files changed, 92 insertions(+), 56 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java index 75115618..fd0a7798 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java @@ -26,7 +26,20 @@ public final class ActivityManager { private Method startActivityAsUserWithFeatureMethod; private Method forceStopPackageMethod; - public ActivityManager(IInterface manager) { + static ActivityManager create() { + 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); + return new ActivityManager(am); + } catch (Exception e) { + throw new AssertionError(e); + } + } + + private ActivityManager(IInterface manager) { this.manager = manager; } 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 daea6db3..2a09b200 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java @@ -20,7 +20,18 @@ public final class ClipboardManager { private int setMethodVersion; private int addListenerMethodVersion; - public ClipboardManager(IInterface manager) { + static ClipboardManager create() { + IInterface clipboard = ServiceManager.getService("clipboard", "android.content.IClipboard"); + if (clipboard == null) { + // Some devices have no clipboard manager + // + // + return null; + } + return new ClipboardManager(clipboard); + } + + private ClipboardManager(IInterface manager) { this.manager = manager; } 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 17b9ae4d..80785a9f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java @@ -5,16 +5,31 @@ import com.genymobile.scrcpy.DisplayInfo; import com.genymobile.scrcpy.Ln; import com.genymobile.scrcpy.Size; +import android.annotation.SuppressLint; import android.view.Display; import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.util.regex.Matcher; import java.util.regex.Pattern; +@SuppressLint("PrivateApi,DiscouragedPrivateApi") public final class DisplayManager { private final Object manager; // instance of hidden class android.hardware.display.DisplayManagerGlobal - public DisplayManager(Object manager) { + static DisplayManager create() { + try { + Class clazz = Class.forName("android.hardware.display.DisplayManagerGlobal"); + Method getInstanceMethod = clazz.getDeclaredMethod("getInstance"); + Object dmg = getInstanceMethod.invoke(null); + return new DisplayManager(dmg); + } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + throw new AssertionError(e); + } + } + + private DisplayManager(Object manager) { this.manager = manager; } 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 ef0a4f50..c7c72dc9 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java @@ -2,12 +2,14 @@ package com.genymobile.scrcpy.wrappers; import com.genymobile.scrcpy.Ln; +import android.annotation.SuppressLint; import android.view.InputEvent; import android.view.MotionEvent; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +@SuppressLint("PrivateApi,DiscouragedPrivateApi") public final class InputManager { public static final int INJECT_INPUT_EVENT_MODE_ASYNC = 0; @@ -20,7 +22,27 @@ public final class InputManager { private static Method setDisplayIdMethod; private static Method setActionButtonMethod; - public InputManager(Object manager) { + static InputManager create() { + try { + Class inputManagerClass = getInputManagerClass(); + Method getInstanceMethod = inputManagerClass.getDeclaredMethod("getInstance"); + Object im = getInstanceMethod.invoke(null); + return new InputManager(im); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + throw new AssertionError(e); + } + } + + private static Class getInputManagerClass() { + try { + // Parts of the InputManager class have been moved to a new InputManagerGlobal class in Android 14 preview + return Class.forName("android.hardware.input.InputManagerGlobal"); + } catch (ClassNotFoundException e) { + return android.hardware.input.InputManager.class; + } + } + + private InputManager(Object manager) { this.manager = manager; } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java index 93722687..942a5880 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java @@ -13,7 +13,12 @@ public final class PowerManager { private final IInterface manager; private Method isScreenOnMethod; - public PowerManager(IInterface manager) { + static PowerManager create() { + IInterface manager = ServiceManager.getService("power", "android.os.IPowerManager"); + return new PowerManager(manager); + } + + private PowerManager(IInterface manager) { this.manager = manager; } 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 85602c19..a8a56dab 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java @@ -9,7 +9,6 @@ import android.os.IBinder; import android.os.IInterface; import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @SuppressLint("PrivateApi,DiscouragedPrivateApi") @@ -38,7 +37,7 @@ public final class ServiceManager { /* not instantiable */ } - private static IInterface getService(String service, String type) { + static IInterface getService(String service, String type) { try { IBinder binder = (IBinder) GET_SERVICE_METHOD.invoke(null, service); Method asInterfaceMethod = Class.forName(type + "$Stub").getMethod("asInterface", IBinder.class); @@ -50,90 +49,51 @@ public final class ServiceManager { public static WindowManager getWindowManager() { if (windowManager == null) { - windowManager = new WindowManager(getService("window", "android.view.IWindowManager")); + windowManager = WindowManager.create(); } return windowManager; } public static DisplayManager getDisplayManager() { if (displayManager == null) { - try { - Class clazz = Class.forName("android.hardware.display.DisplayManagerGlobal"); - Method getInstanceMethod = clazz.getDeclaredMethod("getInstance"); - Object dmg = getInstanceMethod.invoke(null); - displayManager = new DisplayManager(dmg); - } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { - throw new AssertionError(e); - } + displayManager = DisplayManager.create(); } return displayManager; } - public static Class getInputManagerClass() { - try { - // Parts of the InputManager class have been moved to a new InputManagerGlobal class in Android 14 preview - return Class.forName("android.hardware.input.InputManagerGlobal"); - } catch (ClassNotFoundException e) { - return android.hardware.input.InputManager.class; - } - } - public static InputManager getInputManager() { if (inputManager == null) { - try { - Class inputManagerClass = getInputManagerClass(); - Method getInstanceMethod = inputManagerClass.getDeclaredMethod("getInstance"); - Object im = getInstanceMethod.invoke(null); - inputManager = new InputManager(im); - } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { - throw new AssertionError(e); - } + inputManager = InputManager.create(); } return inputManager; } public static PowerManager getPowerManager() { if (powerManager == null) { - powerManager = new PowerManager(getService("power", "android.os.IPowerManager")); + powerManager = PowerManager.create(); } return powerManager; } public static StatusBarManager getStatusBarManager() { if (statusBarManager == null) { - statusBarManager = new StatusBarManager(getService("statusbar", "com.android.internal.statusbar.IStatusBarService")); + statusBarManager = StatusBarManager.create(); } return statusBarManager; } public static ClipboardManager getClipboardManager() { if (clipboardManager == null) { - IInterface clipboard = getService("clipboard", "android.content.IClipboard"); - if (clipboard == null) { - // Some devices have no clipboard manager - // - // - return null; - } - clipboardManager = new ClipboardManager(clipboard); + // May be null, some devices have no clipboard manager + clipboardManager = ClipboardManager.create(); } return clipboardManager; } public static 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); - } + activityManager = ActivityManager.create(); } - return activityManager; } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java index 9126d5ed..e65cef5c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java @@ -16,7 +16,12 @@ public final class StatusBarManager { private boolean expandSettingsPanelMethodNewVersion = true; private Method collapsePanelsMethod; - public StatusBarManager(IInterface manager) { + static StatusBarManager create() { + IInterface manager = ServiceManager.getService("statusbar", "com.android.internal.statusbar.IStatusBarService"); + return new StatusBarManager(manager); + } + + private StatusBarManager(IInterface manager) { this.manager = manager; } 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 a746be5c..99b9148f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java @@ -17,7 +17,12 @@ public final class WindowManager { private Method isRotationFrozenMethod; private Method thawRotationMethod; - public WindowManager(IInterface manager) { + static WindowManager create() { + IInterface manager = ServiceManager.getService("window", "android.view.IWindowManager"); + return new WindowManager(manager); + } + + private WindowManager(IInterface manager) { this.manager = manager; } From f7b4a18b4398c013b2bed9630b7bb35eb50ad97e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 9 Feb 2024 18:39:57 +0100 Subject: [PATCH 1748/2244] Catch generic ReflectiveOperationException This exception is a super-type of: - ClassNotFoundException - IllegalAccessException - InstantiationException - InvocationTargetException - NoSuchFieldException - NoSuchMethodException Use it to simplify. --- .../scrcpy/wrappers/ActivityManager.java | 7 +++---- .../scrcpy/wrappers/ClipboardManager.java | 15 ++++++--------- .../scrcpy/wrappers/ContentProvider.java | 6 ++---- .../scrcpy/wrappers/DisplayControl.java | 5 ++--- .../scrcpy/wrappers/DisplayManager.java | 9 ++++----- .../genymobile/scrcpy/wrappers/InputManager.java | 9 ++++----- .../genymobile/scrcpy/wrappers/PowerManager.java | 3 +-- .../scrcpy/wrappers/StatusBarManager.java | 7 +++---- .../scrcpy/wrappers/SurfaceControl.java | 9 ++++----- .../genymobile/scrcpy/wrappers/WindowManager.java | 9 ++++----- 10 files changed, 33 insertions(+), 46 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java index fd0a7798..367ea2e7 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java @@ -13,7 +13,6 @@ import android.os.IBinder; import android.os.IInterface; import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @SuppressLint("PrivateApi,DiscouragedPrivateApi") @@ -34,7 +33,7 @@ public final class ActivityManager { Method getDefaultMethod = cls.getDeclaredMethod("getDefault"); IInterface am = (IInterface) getDefaultMethod.invoke(null); return new ActivityManager(am); - } catch (Exception e) { + } catch (ReflectiveOperationException e) { throw new AssertionError(e); } } @@ -89,7 +88,7 @@ public final class ActivityManager { return null; } return new ContentProvider(this, provider, name, token); - } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException | NoSuchFieldException e) { + } catch (ReflectiveOperationException e) { Ln.e("Could not invoke method", e); return null; } @@ -99,7 +98,7 @@ public final class ActivityManager { try { Method method = getRemoveContentProviderExternalMethod(); method.invoke(manager, name, token); - } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + } catch (ReflectiveOperationException e) { Ln.e("Could not invoke method", e); } } 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 2a09b200..2c8d9907 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java @@ -8,7 +8,6 @@ import android.content.IOnPrimaryClipChangedListener; import android.os.Build; import android.os.IInterface; -import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public final class ClipboardManager { @@ -98,8 +97,7 @@ public final class ClipboardManager { return setPrimaryClipMethod; } - private static ClipData getPrimaryClip(Method method, int methodVersion, IInterface manager) - throws InvocationTargetException, IllegalAccessException { + private static ClipData getPrimaryClip(Method method, int methodVersion, IInterface manager) throws ReflectiveOperationException { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME); } @@ -121,8 +119,7 @@ public final class ClipboardManager { } } - private static void setPrimaryClip(Method method, int methodVersion, IInterface manager, ClipData clipData) - throws InvocationTargetException, IllegalAccessException { + private static void setPrimaryClip(Method method, int methodVersion, IInterface manager, ClipData clipData) throws ReflectiveOperationException { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { method.invoke(manager, clipData, FakeContext.PACKAGE_NAME); return; @@ -149,7 +146,7 @@ public final class ClipboardManager { return null; } return clipData.getItemAt(0).getText(); - } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + } catch (ReflectiveOperationException e) { Ln.e("Could not invoke method", e); return null; } @@ -161,14 +158,14 @@ public final class ClipboardManager { ClipData clipData = ClipData.newPlainText(null, text); setPrimaryClip(method, setMethodVersion, manager, clipData); return true; - } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + } catch (ReflectiveOperationException e) { Ln.e("Could not invoke method", e); return false; } } private static void addPrimaryClipChangedListener(Method method, int methodVersion, IInterface manager, IOnPrimaryClipChangedListener listener) - throws InvocationTargetException, IllegalAccessException { + throws ReflectiveOperationException { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { method.invoke(manager, listener, FakeContext.PACKAGE_NAME); return; @@ -220,7 +217,7 @@ public final class ClipboardManager { Method method = getAddPrimaryClipChangedListener(); addPrimaryClipChangedListener(method, addListenerMethodVersion, manager, listener); return true; - } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + } catch (ReflectiveOperationException e) { Ln.e("Could not invoke method", e); return false; } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java index 89c1d0e2..a03f824e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java @@ -11,7 +11,6 @@ import android.os.Bundle; import android.os.IBinder; import java.io.Closeable; -import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public final class ContentProvider implements Closeable { @@ -75,8 +74,7 @@ public final class ContentProvider implements Closeable { return callMethod; } - private Bundle call(String callMethod, String arg, Bundle extras) - throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + private Bundle call(String callMethod, String arg, Bundle extras) throws ReflectiveOperationException { try { Method method = getCallMethod(); Object[] args; @@ -97,7 +95,7 @@ public final class ContentProvider implements Closeable { } } return (Bundle) method.invoke(provider, args); - } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + } catch (ReflectiveOperationException e) { Ln.e("Could not invoke method", e); throw e; } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayControl.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayControl.java index 4e19beb9..ba3e9ee0 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayControl.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayControl.java @@ -7,7 +7,6 @@ import android.annotation.TargetApi; import android.os.Build; import android.os.IBinder; -import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @SuppressLint({"PrivateApi", "SoonBlockedPrivateApi", "BlockedPrivateApi"}) @@ -55,7 +54,7 @@ public final class DisplayControl { try { Method method = getGetPhysicalDisplayTokenMethod(); return (IBinder) method.invoke(null, physicalDisplayId); - } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + } catch (ReflectiveOperationException e) { Ln.e("Could not invoke method", e); return null; } @@ -72,7 +71,7 @@ public final class DisplayControl { try { Method method = getGetPhysicalDisplayIdsMethod(); return (long[]) method.invoke(null); - } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + } catch (ReflectiveOperationException e) { Ln.e("Could not invoke method", e); return null; } 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 80785a9f..33a061ba 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java @@ -9,7 +9,6 @@ import android.annotation.SuppressLint; import android.view.Display; import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -24,7 +23,7 @@ public final class DisplayManager { Method getInstanceMethod = clazz.getDeclaredMethod("getInstance"); Object dmg = getInstanceMethod.invoke(null); return new DisplayManager(dmg); - } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + } catch (ReflectiveOperationException e) { throw new AssertionError(e); } } @@ -75,7 +74,7 @@ public final class DisplayManager { try { Field filed = Display.class.getDeclaredField(flagString); flags |= filed.getInt(null); - } catch (NoSuchFieldException | IllegalAccessException e) { + } catch (ReflectiveOperationException e) { // Silently ignore, some flags reported by "dumpsys display" are @TestApi } } @@ -97,7 +96,7 @@ public final class DisplayManager { 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) { + } catch (ReflectiveOperationException e) { throw new AssertionError(e); } } @@ -105,7 +104,7 @@ public final class DisplayManager { public int[] getDisplayIds() { try { return (int[]) manager.getClass().getMethod("getDisplayIds").invoke(manager); - } catch (Exception e) { + } catch (ReflectiveOperationException 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 c7c72dc9..16ecb09f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java @@ -6,7 +6,6 @@ import android.annotation.SuppressLint; import android.view.InputEvent; import android.view.MotionEvent; -import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @SuppressLint("PrivateApi,DiscouragedPrivateApi") @@ -28,7 +27,7 @@ public final class InputManager { Method getInstanceMethod = inputManagerClass.getDeclaredMethod("getInstance"); Object im = getInstanceMethod.invoke(null); return new InputManager(im); - } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + } catch (ReflectiveOperationException e) { throw new AssertionError(e); } } @@ -57,7 +56,7 @@ public final class InputManager { try { Method method = getInjectInputEventMethod(); return (boolean) method.invoke(manager, inputEvent, mode); - } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + } catch (ReflectiveOperationException e) { Ln.e("Could not invoke method", e); return false; } @@ -75,7 +74,7 @@ public final class InputManager { Method method = getSetDisplayIdMethod(); method.invoke(inputEvent, displayId); return true; - } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + } catch (ReflectiveOperationException e) { Ln.e("Cannot associate a display id to the input event", e); return false; } @@ -93,7 +92,7 @@ public final class InputManager { Method method = getSetActionButtonMethod(); method.invoke(motionEvent, actionButton); return true; - } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + } catch (ReflectiveOperationException e) { Ln.e("Cannot set action button on MotionEvent", e); return false; } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java index 942a5880..36d5f1ac 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java @@ -6,7 +6,6 @@ import android.annotation.SuppressLint; import android.os.Build; import android.os.IInterface; -import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public final class PowerManager { @@ -35,7 +34,7 @@ public final class PowerManager { try { Method method = getIsScreenOnMethod(); return (boolean) method.invoke(manager); - } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + } catch (ReflectiveOperationException e) { Ln.e("Could not invoke method", e); return false; } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java index e65cef5c..af217da2 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java @@ -4,7 +4,6 @@ import com.genymobile.scrcpy.Ln; import android.os.IInterface; -import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public final class StatusBarManager { @@ -67,7 +66,7 @@ public final class StatusBarManager { } else { method.invoke(manager); } - } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + } catch (ReflectiveOperationException e) { Ln.e("Could not invoke method", e); } } @@ -82,7 +81,7 @@ public final class StatusBarManager { // old version method.invoke(manager); } - } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + } catch (ReflectiveOperationException e) { Ln.e("Could not invoke method", e); } } @@ -91,7 +90,7 @@ public final class StatusBarManager { try { Method method = getCollapsePanelsMethod(); method.invoke(manager); - } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + } catch (ReflectiveOperationException e) { Ln.e("Could not invoke method", e); } } 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 98259e7f..4a3d0bfe 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java @@ -8,7 +8,6 @@ import android.os.Build; import android.os.IBinder; import android.view.Surface; -import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @SuppressLint("PrivateApi") @@ -109,7 +108,7 @@ public final class SurfaceControl { // call getInternalDisplayToken() return (IBinder) method.invoke(null); - } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + } catch (ReflectiveOperationException e) { Ln.e("Could not invoke method", e); return null; } @@ -126,7 +125,7 @@ public final class SurfaceControl { try { Method method = getGetPhysicalDisplayTokenMethod(); return (IBinder) method.invoke(null, physicalDisplayId); - } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + } catch (ReflectiveOperationException e) { Ln.e("Could not invoke method", e); return null; } @@ -152,7 +151,7 @@ public final class SurfaceControl { try { Method method = getGetPhysicalDisplayIdsMethod(); return (long[]) method.invoke(null); - } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + } catch (ReflectiveOperationException e) { Ln.e("Could not invoke method", e); return null; } @@ -170,7 +169,7 @@ public final class SurfaceControl { Method method = getSetDisplayPowerModeMethod(); method.invoke(null, displayToken, mode); return true; - } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + } catch (ReflectiveOperationException e) { Ln.e("Could not invoke method", e); return false; } 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 99b9148f..b19dace9 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java @@ -7,7 +7,6 @@ import android.os.IInterface; import android.view.IDisplayFoldListener; import android.view.IRotationWatcher; -import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public final class WindowManager { @@ -66,7 +65,7 @@ public final class WindowManager { try { Method method = getGetRotationMethod(); return (int) method.invoke(manager); - } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + } catch (ReflectiveOperationException e) { Ln.e("Could not invoke method", e); return 0; } @@ -76,7 +75,7 @@ public final class WindowManager { try { Method method = getFreezeRotationMethod(); method.invoke(manager, rotation); - } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + } catch (ReflectiveOperationException e) { Ln.e("Could not invoke method", e); } } @@ -85,7 +84,7 @@ public final class WindowManager { try { Method method = getIsRotationFrozenMethod(); return (boolean) method.invoke(manager); - } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + } catch (ReflectiveOperationException e) { Ln.e("Could not invoke method", e); return false; } @@ -95,7 +94,7 @@ public final class WindowManager { try { Method method = getThawRotationMethod(); method.invoke(manager); - } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + } catch (ReflectiveOperationException e) { Ln.e("Could not invoke method", e); } } From be3f949aa5c4dcad276ac0f2b36671a3309ce12f Mon Sep 17 00:00:00 2001 From: wuderek Date: Fri, 9 Feb 2024 16:02:48 +0800 Subject: [PATCH 1749/2244] Adapt to display API changes The method SurfaceControl.createDisplay() has been removed in AOSP. Use DisplayManager to create a VirtualDisplay object instead. Fixes #4646 Fixes #4656 PR #4657 Signed-off-by: Romain Vimont --- .../java/com/genymobile/scrcpy/Device.java | 4 +++ .../com/genymobile/scrcpy/ScreenCapture.java | 29 +++++++++++++++++-- .../scrcpy/wrappers/DisplayManager.java | 16 ++++++++++ .../scrcpy/wrappers/SurfaceControl.java | 8 ++--- 4 files changed, 48 insertions(+), 9 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index 2324ce90..33b09a57 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -164,6 +164,10 @@ public final class Device { } } + public int getDisplayId() { + return displayId; + } + public synchronized void setMaxSize(int newMaxSize) { maxSize = newMaxSize; screenInfo = ScreenInfo.computeScreenInfo(screenInfo.getReverseVideoRotation(), deviceSize, crop, newMaxSize, lockVideoOrientation); diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenCapture.java b/server/src/main/java/com/genymobile/scrcpy/ScreenCapture.java index e048354a..95214188 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenCapture.java @@ -1,8 +1,10 @@ package com.genymobile.scrcpy; +import com.genymobile.scrcpy.wrappers.ServiceManager; import com.genymobile.scrcpy.wrappers.SurfaceControl; import android.graphics.Rect; +import android.hardware.display.VirtualDisplay; import android.os.Build; import android.os.IBinder; import android.view.Surface; @@ -11,6 +13,7 @@ public class ScreenCapture extends SurfaceCapture implements Device.RotationList private final Device device; private IBinder display; + private VirtualDisplay virtualDisplay; public ScreenCapture(Device device) { this.device = device; @@ -34,9 +37,29 @@ public class ScreenCapture extends SurfaceCapture implements Device.RotationList if (display != null) { SurfaceControl.destroyDisplay(display); + display = null; + } + if (virtualDisplay != null) { + virtualDisplay.release(); + virtualDisplay = null; + } + + try { + display = createDisplay(); + setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack); + Ln.d("Display: using SurfaceControl API"); + } catch (Exception surfaceControlException) { + Rect videoRect = screenInfo.getVideoSize().toRect(); + try { + virtualDisplay = ServiceManager.getDisplayManager() + .createVirtualDisplay("scrcpy", videoRect.width(), videoRect.height(), device.getDisplayId(), surface); + Ln.d("Display: using DisplayManager API"); + } catch (Exception displayManagerException) { + Ln.e("Could not create display using SurfaceControl", surfaceControlException); + Ln.e("Could not create display using DisplayManager", displayManagerException); + throw new AssertionError("Could not create display"); + } } - display = createDisplay(); - setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack); } @Override @@ -69,7 +92,7 @@ public class ScreenCapture extends SurfaceCapture implements Device.RotationList requestReset(); } - private static IBinder createDisplay() { + private static IBinder createDisplay() throws Exception { // Since Android 12 (preview), secure displays could not be created with shell permissions anymore. // On Android 12 preview, SDK_INT is still R (not S), but CODENAME is "S". boolean secure = Build.VERSION.SDK_INT < Build.VERSION_CODES.R || (Build.VERSION.SDK_INT == Build.VERSION_CODES.R && !"S".equals( 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 33a061ba..2ff82d04 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java @@ -6,7 +6,9 @@ import com.genymobile.scrcpy.Ln; import com.genymobile.scrcpy.Size; import android.annotation.SuppressLint; +import android.hardware.display.VirtualDisplay; import android.view.Display; +import android.view.Surface; import java.lang.reflect.Field; import java.lang.reflect.Method; @@ -16,6 +18,7 @@ import java.util.regex.Pattern; @SuppressLint("PrivateApi,DiscouragedPrivateApi") public final class DisplayManager { private final Object manager; // instance of hidden class android.hardware.display.DisplayManagerGlobal + private Method createVirtualDisplayMethod; static DisplayManager create() { try { @@ -108,4 +111,17 @@ public final class DisplayManager { throw new AssertionError(e); } } + + private Method getCreateVirtualDisplayMethod() throws NoSuchMethodException { + if (createVirtualDisplayMethod == null) { + createVirtualDisplayMethod = android.hardware.display.DisplayManager.class + .getMethod("createVirtualDisplay", String.class, int.class, int.class, int.class, Surface.class); + } + return createVirtualDisplayMethod; + } + + public VirtualDisplay createVirtualDisplay(String name, int width, int height, int displayIdToMirror, Surface surface) throws Exception { + Method method = getCreateVirtualDisplayMethod(); + return (VirtualDisplay) method.invoke(null, name, width, height, displayIdToMirror, surface); + } } 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 4a3d0bfe..f0e351a2 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java @@ -77,12 +77,8 @@ public final class SurfaceControl { } } - public static IBinder createDisplay(String name, boolean secure) { - try { - return (IBinder) CLASS.getMethod("createDisplay", String.class, boolean.class).invoke(null, name, secure); - } catch (Exception e) { - throw new AssertionError(e); - } + public static IBinder createDisplay(String name, boolean secure) throws Exception { + return (IBinder) CLASS.getMethod("createDisplay", String.class, boolean.class).invoke(null, name, secure); } private static Method getGetBuiltInDisplayMethod() throws NoSuchMethodException { From 9efa162949c2a3e3e42564862ff390700270394d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 3 Feb 2024 18:52:10 +0100 Subject: [PATCH 1750/2244] Configure clean up actions dynamically Some actions may be performed when scrcpy exits, currently: - disable "show touches" - restore "stay on while plugged in" - power off screen - restore "power mode" (to disable "turn screen off") They are performed from a separate process so that they can be executed even when scrcpy-server is killed (e.g. if the device is unplugged). The clean up actions to perform were configured when scrcpy started. Given that there is no method to read the current "power mode" in Android, and that "turn screen off" can be applied at any time using an scrcpy shortcut, there was no way to determine if "power mode" had to be restored on exit. Therefore, it was always restored to "normal", even when not necessary. However, setting the "power mode" is quite fragile on some devices, and may cause some issues, so it is preferable to call it only when necessary (when "turn screen off" has actually been called). For that purpose, make the scrcpy-server main process and the clean up process communicate the actions to perform over a pipe (stdin/stdout), so that they can be changed dynamically. In particular, when the power mode is changed at runtime, notify the clean up process. Refs 1beec99f8283713b1fbf0b3704eb4dceecc9a590 Refs #4456 Refs #4624 PR #4649 --- .../java/com/genymobile/scrcpy/CleanUp.java | 230 +++++++----------- .../com/genymobile/scrcpy/Controller.java | 8 +- .../java/com/genymobile/scrcpy/Server.java | 89 ++++--- 3 files changed, 149 insertions(+), 178 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java index c84e25bb..f9b1efd6 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java +++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java @@ -1,11 +1,8 @@ package com.genymobile.scrcpy; -import android.os.Parcel; -import android.os.Parcelable; -import android.util.Base64; - import java.io.File; import java.io.IOException; +import java.io.OutputStream; /** * Handle the cleanup of scrcpy, even if the main process is killed. @@ -14,127 +11,59 @@ import java.io.IOException; */ public final class CleanUp { - // A simple struct to be passed from the main process to the cleanup process - public static class Config implements Parcelable { + private static final int MSG_TYPE_MASK = 0b11; + private static final int MSG_TYPE_RESTORE_STAY_ON = 0; + private static final int MSG_TYPE_DISABLE_SHOW_TOUCHES = 1; + private static final int MSG_TYPE_RESTORE_NORMAL_POWER_MODE = 2; + private static final int MSG_TYPE_POWER_OFF_SCREEN = 3; - public static final Creator CREATOR = new Creator() { - @Override - public Config createFromParcel(Parcel in) { - return new Config(in); - } + private static final int MSG_PARAM_SHIFT = 2; - @Override - public Config[] newArray(int size) { - return new Config[size]; - } - }; + private final OutputStream out; - private static final int FLAG_DISABLE_SHOW_TOUCHES = 1; - private static final int FLAG_RESTORE_NORMAL_POWER_MODE = 2; - private static final int FLAG_POWER_OFF_SCREEN = 4; - - private int displayId; - - // Restore the value (between 0 and 7), -1 to not restore - // - private int restoreStayOn = -1; - - private boolean disableShowTouches; - private boolean restoreNormalPowerMode; - private boolean powerOffScreen; - - public Config() { - // Default constructor, the fields are initialized by CleanUp.configure() - } - - protected Config(Parcel in) { - displayId = in.readInt(); - restoreStayOn = in.readInt(); - byte options = in.readByte(); - disableShowTouches = (options & FLAG_DISABLE_SHOW_TOUCHES) != 0; - restoreNormalPowerMode = (options & FLAG_RESTORE_NORMAL_POWER_MODE) != 0; - powerOffScreen = (options & FLAG_POWER_OFF_SCREEN) != 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(displayId); - dest.writeInt(restoreStayOn); - byte options = 0; - if (disableShowTouches) { - options |= FLAG_DISABLE_SHOW_TOUCHES; - } - if (restoreNormalPowerMode) { - options |= FLAG_RESTORE_NORMAL_POWER_MODE; - } - if (powerOffScreen) { - options |= FLAG_POWER_OFF_SCREEN; - } - dest.writeByte(options); - } - - private boolean hasWork() { - return disableShowTouches || restoreStayOn != -1 || restoreNormalPowerMode || powerOffScreen; - } - - @Override - public int describeContents() { - return 0; - } - - byte[] serialize() { - Parcel parcel = Parcel.obtain(); - writeToParcel(parcel, 0); - byte[] bytes = parcel.marshall(); - parcel.recycle(); - return bytes; - } - - static Config deserialize(byte[] bytes) { - Parcel parcel = Parcel.obtain(); - parcel.unmarshall(bytes, 0, bytes.length); - parcel.setDataPosition(0); - return CREATOR.createFromParcel(parcel); - } - - static Config fromBase64(String base64) { - byte[] bytes = Base64.decode(base64, Base64.NO_WRAP); - return deserialize(bytes); - } - - String toBase64() { - byte[] bytes = serialize(); - return Base64.encodeToString(bytes, Base64.NO_WRAP); - } + public CleanUp(OutputStream out) { + this.out = out; } - private CleanUp() { - // not instantiable - } - - public static void configure(int displayId, int restoreStayOn, boolean disableShowTouches, boolean restoreNormalPowerMode, boolean powerOffScreen) - throws IOException { - Config config = new Config(); - config.displayId = displayId; - config.disableShowTouches = disableShowTouches; - config.restoreStayOn = restoreStayOn; - config.restoreNormalPowerMode = restoreNormalPowerMode; - config.powerOffScreen = powerOffScreen; - - if (config.hasWork()) { - startProcess(config); - } else { - // There is no additional clean up to do when scrcpy dies - unlinkSelf(); - } - } - - private static void startProcess(Config config) throws IOException { - String[] cmd = {"app_process", "/", CleanUp.class.getName(), config.toBase64()}; + public static CleanUp configure(int displayId) throws IOException { + String[] cmd = {"app_process", "/", CleanUp.class.getName(), String.valueOf(displayId)}; ProcessBuilder builder = new ProcessBuilder(cmd); builder.environment().put("CLASSPATH", Server.SERVER_PATH); - builder.start(); + Process process = builder.start(); + return new CleanUp(process.getOutputStream()); + } + + private boolean sendMessage(int type, int param) { + assert (type & ~MSG_TYPE_MASK) == 0; + int msg = type | param << MSG_PARAM_SHIFT; + try { + out.write(msg); + out.flush(); + return true; + } catch (IOException e) { + Ln.w("Could not configure cleanup (type=" + type + ", param=" + param + ")", e); + return false; + } + } + + public boolean setRestoreStayOn(int restoreValue) { + // Restore the value (between 0 and 7), -1 to not restore + // + assert restoreValue >= -1 && restoreValue <= 7; + return sendMessage(MSG_TYPE_RESTORE_STAY_ON, restoreValue & 0b1111); + } + + public boolean setDisableShowTouches(boolean disableOnExit) { + return sendMessage(MSG_TYPE_DISABLE_SHOW_TOUCHES, disableOnExit ? 1 : 0); + } + + public boolean setRestoreNormalPowerMode(boolean restoreOnExit) { + return sendMessage(MSG_TYPE_RESTORE_NORMAL_POWER_MODE, restoreOnExit ? 1 : 0); + } + + public boolean setPowerOffScreen(boolean powerOffScreenOnExit) { + return sendMessage(MSG_TYPE_POWER_OFF_SCREEN, powerOffScreenOnExit ? 1 : 0); } public static void unlinkSelf() { @@ -148,41 +77,66 @@ public final class CleanUp { public static void main(String... args) { unlinkSelf(); + int displayId = Integer.parseInt(args[0]); + + int restoreStayOn = -1; + boolean disableShowTouches = false; + boolean restoreNormalPowerMode = false; + boolean powerOffScreen = false; + try { // Wait for the server to die - System.in.read(); + int msg; + while ((msg = System.in.read()) != -1) { + int type = msg & MSG_TYPE_MASK; + int param = msg >> MSG_PARAM_SHIFT; + switch (type) { + case MSG_TYPE_RESTORE_STAY_ON: + restoreStayOn = param > 7 ? -1 : param; + break; + case MSG_TYPE_DISABLE_SHOW_TOUCHES: + disableShowTouches = param != 0; + break; + case MSG_TYPE_RESTORE_NORMAL_POWER_MODE: + restoreNormalPowerMode = param != 0; + break; + case MSG_TYPE_POWER_OFF_SCREEN: + powerOffScreen = param != 0; + break; + default: + Ln.w("Unexpected msg type: " + type); + break; + } + } } catch (IOException e) { // Expected when the server is dead } Ln.i("Cleaning up"); - Config config = Config.fromBase64(args[0]); - - if (config.disableShowTouches || config.restoreStayOn != -1) { - if (config.disableShowTouches) { - Ln.i("Disabling \"show touches\""); - try { - Settings.putValue(Settings.TABLE_SYSTEM, "show_touches", "0"); - } catch (SettingsException e) { - Ln.e("Could not restore \"show_touches\"", e); - } + if (disableShowTouches) { + Ln.i("Disabling \"show touches\""); + try { + Settings.putValue(Settings.TABLE_SYSTEM, "show_touches", "0"); + } catch (SettingsException e) { + Ln.e("Could not restore \"show_touches\"", e); } - if (config.restoreStayOn != -1) { - Ln.i("Restoring \"stay awake\""); - try { - Settings.putValue(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(config.restoreStayOn)); - } catch (SettingsException e) { - Ln.e("Could not restore \"stay_on_while_plugged_in\"", e); - } + } + + if (restoreStayOn != -1) { + Ln.i("Restoring \"stay awake\""); + try { + Settings.putValue(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(restoreStayOn)); + } catch (SettingsException e) { + Ln.e("Could not restore \"stay_on_while_plugged_in\"", e); } } if (Device.isScreenOn()) { - if (config.powerOffScreen) { + if (powerOffScreen) { Ln.i("Power off screen"); - Device.powerOffScreen(config.displayId); - } else if (config.restoreNormalPowerMode) { + Device.powerOffScreen(displayId); + } else if (restoreNormalPowerMode) { Ln.i("Restoring normal power mode"); Device.setScreenPowerMode(Device.POWER_MODE_NORMAL); } diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index 3b0e9031..c0763012 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -28,6 +28,7 @@ public class Controller implements AsyncProcessor { private final Device device; private final DesktopConnection connection; + private final CleanUp cleanUp; private final DeviceMessageSender sender; private final boolean clipboardAutosync; private final boolean powerOn; @@ -41,9 +42,10 @@ public class Controller implements AsyncProcessor { private boolean keepPowerModeOff; - public Controller(Device device, DesktopConnection connection, boolean clipboardAutosync, boolean powerOn) { + public Controller(Device device, DesktopConnection connection, CleanUp cleanUp, boolean clipboardAutosync, boolean powerOn) { this.device = device; this.connection = connection; + this.cleanUp = cleanUp; this.clipboardAutosync = clipboardAutosync; this.powerOn = powerOn; initPointers(); @@ -170,6 +172,10 @@ public class Controller implements AsyncProcessor { if (setPowerModeOk) { keepPowerModeOff = mode == Device.POWER_MODE_OFF; Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on")); + if (cleanUp != null) { + boolean mustRestoreOnExit = mode != Device.POWER_MODE_NORMAL; + cleanUp.setRestoreNormalPowerMode(mustRestoreOnExit); + } } } break; diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index e4a95140..bcafa133 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -51,46 +51,47 @@ public final class Server { // not instantiable } - private static void initAndCleanUp(Options options) { - boolean mustDisableShowTouchesOnCleanUp = false; - int restoreStayOn = -1; - boolean restoreNormalPowerMode = options.getControl(); // only restore power mode if control is enabled - if (options.getShowTouches() || options.getStayAwake()) { - if (options.getShowTouches()) { - try { - String oldValue = Settings.getAndPutValue(Settings.TABLE_SYSTEM, "show_touches", "1"); - // If "show touches" was disabled, it must be disabled back on clean up - mustDisableShowTouchesOnCleanUp = !"1".equals(oldValue); - } catch (SettingsException e) { - Ln.e("Could not change \"show_touches\"", e); - } - } + private static void initAndCleanUp(Options options, CleanUp cleanUp) { + // This method is called from its own thread, so it may only configure cleanup actions which are NOT dynamic (i.e. they are configured once + // and for all, they cannot be changed from another thread) - if (options.getStayAwake()) { - int stayOn = BatteryManager.BATTERY_PLUGGED_AC | BatteryManager.BATTERY_PLUGGED_USB | BatteryManager.BATTERY_PLUGGED_WIRELESS; - try { - String oldValue = Settings.getAndPutValue(Settings.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; + if (options.getShowTouches()) { + try { + String oldValue = Settings.getAndPutValue(Settings.TABLE_SYSTEM, "show_touches", "1"); + // If "show touches" was disabled, it must be disabled back on clean up + if (!"1".equals(oldValue)) { + if (!cleanUp.setDisableShowTouches(true)) { + Ln.e("Could not disable show touch on exit"); } - } catch (SettingsException e) { - Ln.e("Could not change \"stay_on_while_plugged_in\"", e); } + } catch (SettingsException e) { + Ln.e("Could not change \"show_touches\"", e); } } - if (options.getCleanup()) { + if (options.getStayAwake()) { + int stayOn = BatteryManager.BATTERY_PLUGGED_AC | BatteryManager.BATTERY_PLUGGED_USB | BatteryManager.BATTERY_PLUGGED_WIRELESS; try { - CleanUp.configure(options.getDisplayId(), restoreStayOn, mustDisableShowTouchesOnCleanUp, restoreNormalPowerMode, - options.getPowerOffScreenOnClose()); - } catch (IOException e) { - Ln.e("Could not configure cleanup", e); + String oldValue = Settings.getAndPutValue(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(stayOn)); + try { + int restoreStayOn = Integer.parseInt(oldValue); + if (restoreStayOn != stayOn) { + // Restore only if the current value is different + if (!cleanUp.setRestoreStayOn(restoreStayOn)) { + Ln.e("Could not restore stay on on exit"); + } + } + } catch (NumberFormatException e) { + // ignore + } + } catch (SettingsException e) { + Ln.e("Could not change \"stay_on_while_plugged_in\"", e); + } + } + + if (options.getPowerOffScreenOnClose()) { + if (!cleanUp.setPowerOffScreen(true)) { + Ln.e("Could not power off screen on exit"); } } } @@ -101,7 +102,13 @@ public final class Server { throw new ConfigurationException("Camera mirroring is not supported"); } - Thread initThread = startInitThread(options); + CleanUp cleanUp = null; + Thread initThread = null; + + if (options.getCleanup()) { + cleanUp = CleanUp.configure(options.getDisplayId()); + initThread = startInitThread(options, cleanUp); + } int scid = options.getScid(); boolean tunnelForward = options.isTunnelForward(); @@ -124,7 +131,7 @@ public final class Server { } if (control) { - Controller controller = new Controller(device, connection, options.getClipboardAutosync(), options.getPowerOn()); + Controller controller = new Controller(device, connection, cleanUp, options.getClipboardAutosync(), options.getPowerOn()); device.setClipboardListener(text -> controller.getSender().pushClipboardText(text)); asyncProcessors.add(controller); } @@ -167,7 +174,9 @@ public final class Server { completion.await(); } finally { - initThread.interrupt(); + if (initThread != null) { + initThread.interrupt(); + } for (AsyncProcessor asyncProcessor : asyncProcessors) { asyncProcessor.stop(); } @@ -175,7 +184,9 @@ public final class Server { connection.shutdown(); try { - initThread.join(); + if (initThread != null) { + initThread.join(); + } for (AsyncProcessor asyncProcessor : asyncProcessors) { asyncProcessor.join(); } @@ -187,8 +198,8 @@ public final class Server { } } - private static Thread startInitThread(final Options options) { - Thread thread = new Thread(() -> initAndCleanUp(options), "init-cleanup"); + private static Thread startInitThread(final Options options, final CleanUp cleanUp) { + Thread thread = new Thread(() -> initAndCleanUp(options, cleanUp), "init-cleanup"); thread.start(); return thread; } From d47ecef1b561322872437368edbfff69dc5ad0bd Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 1 Feb 2024 17:27:14 +0100 Subject: [PATCH 1751/2244] Limit buffering time value This avoids unreasonable values which could lead to integer overflow. PR #4572 --- app/src/cli.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/cli.c b/app/src/cli.c index f7d7e390..b2b02ecd 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -1385,7 +1385,11 @@ parse_max_fps(const char *s, uint16_t *max_fps) { static bool parse_buffering_time(const char *s, sc_tick *tick) { long value; - bool ok = parse_integer_arg(s, &value, false, 0, 0x7FFFFFFF, + // In practice, buffering time should not exceed a few seconds. + // Limit it to some arbitrary value (1 hour) to prevent 32-bit overflow + // when multiplied by the audio sample size and the number of samples per + // millisecond. + bool ok = parse_integer_arg(s, &value, false, 0, 60 * 60 * 1000, "buffering time"); if (!ok) { return false; From cfa4f7e2f2ac867dd6d6278a48cc470e82d42f37 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 1 Jan 2024 19:22:55 +0100 Subject: [PATCH 1752/2244] Replace locks by atomics in audio player The audio output thread only reads samples from the buffer, and most of the time, the audio receiver thread only writes samples to the buffer. In these cases, using atomics avoids lock contention. There are still corner cases where the audio receiver thread needs to "read" samples (and drop them), so lock only in these cases. PR #4572 --- app/meson.build | 9 +- app/src/audio_player.c | 194 ++++++++++++++++++-------------------- app/src/audio_player.h | 22 ++--- app/src/util/audiobuf.c | 112 ++++++++++++++++++++++ app/src/util/audiobuf.h | 79 +++++----------- app/src/util/bytebuf.c | 104 -------------------- app/src/util/bytebuf.h | 114 ---------------------- app/tests/test_audiobuf.c | 128 +++++++++++++++++++++++++ app/tests/test_bytebuf.c | 126 ------------------------- 9 files changed, 372 insertions(+), 516 deletions(-) create mode 100644 app/src/util/audiobuf.c delete mode 100644 app/src/util/bytebuf.c delete mode 100644 app/src/util/bytebuf.h create mode 100644 app/tests/test_audiobuf.c delete mode 100644 app/tests/test_bytebuf.c diff --git a/app/meson.build b/app/meson.build index 88e2df9a..caf5ee5c 100644 --- a/app/meson.build +++ b/app/meson.build @@ -34,8 +34,8 @@ src = [ 'src/trait/frame_source.c', 'src/trait/packet_source.c', 'src/util/acksync.c', + 'src/util/audiobuf.c', 'src/util/average.c', - 'src/util/bytebuf.c', 'src/util/file.c', 'src/util/intmap.c', 'src/util/intr.c', @@ -212,9 +212,10 @@ if get_option('buildtype') == 'debug' ['test_binary', [ 'tests/test_binary.c', ]], - ['test_bytebuf', [ - 'tests/test_bytebuf.c', - 'src/util/bytebuf.c', + ['test_audiobuf', [ + 'tests/test_audiobuf.c', + 'src/util/audiobuf.c', + 'src/util/memory.c', ]], ['test_cli', [ 'tests/test_cli.c', diff --git a/app/src/audio_player.c b/app/src/audio_player.c index 8f0ad7fb..728d3f2a 100644 --- a/app/src/audio_player.c +++ b/app/src/audio_player.c @@ -66,8 +66,7 @@ static void SDLCALL sc_audio_player_sdl_callback(void *userdata, uint8_t *stream, int len_int) { struct sc_audio_player *ap = userdata; - // This callback is called with the lock used by SDL_AudioDeviceLock(), so - // the audiobuf is protected + // This callback is called with the lock used by SDL_LockAudioDevice() assert(len_int > 0); size_t len = len_int; @@ -77,8 +76,9 @@ sc_audio_player_sdl_callback(void *userdata, uint8_t *stream, int len_int) { LOGD("[Audio] SDL callback requests %" PRIu32 " samples", count); #endif - uint32_t buffered_samples = sc_audiobuf_can_read(&ap->buf); - if (!ap->played) { + bool played = atomic_load_explicit(&ap->played, memory_order_relaxed); + if (!played) { + uint32_t buffered_samples = sc_audiobuf_can_read(&ap->buf); // Part of the buffering is handled by inserting initial silence. The // remaining (margin) last samples will be handled by compensation. uint32_t margin = 30 * ap->sample_rate / 1000; // 30ms @@ -93,10 +93,7 @@ sc_audio_player_sdl_callback(void *userdata, uint8_t *stream, int len_int) { } } - uint32_t read = MIN(buffered_samples, count); - if (read) { - sc_audiobuf_read(&ap->buf, stream, read); - } + uint32_t read = sc_audiobuf_read(&ap->buf, stream, count); if (read < count) { uint32_t silence = count - read; @@ -109,13 +106,16 @@ sc_audio_player_sdl_callback(void *userdata, uint8_t *stream, int len_int) { silence); memset(stream + TO_BYTES(read), 0, TO_BYTES(silence)); - if (ap->received) { + bool received = atomic_load_explicit(&ap->received, + memory_order_relaxed); + if (received) { // Inserting additional samples immediately increases buffering - ap->underflow += silence; + atomic_fetch_add_explicit(&ap->underflow, silence, + memory_order_relaxed); } } - ap->played = true; + atomic_store_explicit(&ap->played, true, memory_order_relaxed); } static uint8_t * @@ -162,123 +162,119 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink, // swr_convert() returns the number of samples which would have been // written if the buffer was big enough. - uint32_t samples_written = MIN(ret, dst_nb_samples); + uint32_t samples = MIN(ret, dst_nb_samples); #ifndef SC_AUDIO_PLAYER_NDEBUG - LOGD("[Audio] %" PRIu32 " samples written to buffer", samples_written); + LOGD("[Audio] %" PRIu32 " samples written to buffer", samples); #endif - // Since this function is the only writer, the current available space is - // at least the previous available space. In practice, it should almost - // always be possible to write without lock. - bool lockless_write = samples_written <= ap->previous_can_write; - if (lockless_write) { - sc_audiobuf_prepare_write(&ap->buf, swr_buf, samples_written); + uint32_t cap = sc_audiobuf_capacity(&ap->buf); + if (samples > cap) { + // Very very unlikely: a single resampled frame should never + // exceed the audio buffer size (or something is very wrong). + // Ignore the first bytes in swr_buf to avoid memory corruption anyway. + swr_buf += TO_BYTES(samples - cap); + samples = cap; } - SDL_LockAudioDevice(ap->device); + uint32_t skipped_samples = 0; - uint32_t buffered_samples = sc_audiobuf_can_read(&ap->buf); + uint32_t written = sc_audiobuf_write(&ap->buf, swr_buf, samples); + if (written < samples) { + uint32_t remaining = samples - written; - if (lockless_write) { - sc_audiobuf_commit_write(&ap->buf, samples_written); - } else { - uint32_t can_write = sc_audiobuf_can_write(&ap->buf); - if (samples_written > can_write) { - // Entering this branch is very unlikely, the audio buffer is - // allocated with a size sufficient to store 1 second more than the - // target buffering. If this happens, though, we have to skip old - // samples. - uint32_t cap = sc_audiobuf_capacity(&ap->buf); - if (samples_written > cap) { - // Very very unlikely: a single resampled frame should never - // exceed the audio buffer size (or something is very wrong). - // Ignore the first bytes in swr_buf - swr_buf += TO_BYTES(samples_written - cap); - // This change in samples_written will impact the - // instant_compensation below - samples_written = cap; - } + // All samples that could be written without locking have been written, + // now we need to lock to drop/consume old samples + SDL_LockAudioDevice(ap->device); - assert(samples_written >= can_write); - if (samples_written > can_write) { - uint32_t skip_samples = samples_written - can_write; - assert(buffered_samples >= skip_samples); - sc_audiobuf_skip(&ap->buf, skip_samples); - buffered_samples -= skip_samples; - if (ap->played) { - // Dropping input samples instantly decreases buffering - ap->avg_buffering.avg -= skip_samples; - } - } + // Retry with the lock + written += sc_audiobuf_write(&ap->buf, + swr_buf + TO_BYTES(written), + remaining); + if (written < samples) { + remaining = samples - written; + // Still insufficient, drop old samples to make space + skipped_samples = sc_audiobuf_read(&ap->buf, NULL, remaining); + assert(skipped_samples == remaining); - // It should remain exactly the expected size to write the new - // samples. - assert(sc_audiobuf_can_write(&ap->buf) == samples_written); + // Now there is enough space + uint32_t w = sc_audiobuf_write(&ap->buf, + swr_buf + TO_BYTES(written), + remaining); + assert(w == remaining); + (void) w; } - sc_audiobuf_write(&ap->buf, swr_buf, samples_written); + SDL_UnlockAudioDevice(ap->device); } - buffered_samples += samples_written; - assert(buffered_samples == sc_audiobuf_can_read(&ap->buf)); - - // Read with lock held, to be used after unlocking - bool played = ap->played; - uint32_t underflow = ap->underflow; - + uint32_t underflow = 0; + uint32_t max_buffered_samples; + bool played = atomic_load_explicit(&ap->played, memory_order_relaxed); if (played) { - uint32_t max_buffered_samples = ap->target_buffering - + 12 * ap->output_buffer - + ap->target_buffering / 10; - if (buffered_samples > max_buffered_samples) { - uint32_t skip_samples = buffered_samples - max_buffered_samples; - sc_audiobuf_skip(&ap->buf, skip_samples); - LOGD("[Audio] Buffering threshold exceeded, skipping %" PRIu32 - " samples", skip_samples); - } + underflow = atomic_exchange_explicit(&ap->underflow, 0, + memory_order_relaxed); - // reset (the current value was copied to a local variable) - ap->underflow = 0; + max_buffered_samples = ap->target_buffering + + 12 * ap->output_buffer + + ap->target_buffering / 10; } else { // SDL playback not started yet, do not accumulate more than // max_initial_buffering samples, this would cause unnecessary delay // (and glitches to compensate) on start. - uint32_t max_initial_buffering = ap->target_buffering - + 2 * ap->output_buffer; - if (buffered_samples > max_initial_buffering) { - uint32_t skip_samples = buffered_samples - max_initial_buffering; - sc_audiobuf_skip(&ap->buf, skip_samples); + max_buffered_samples = ap->target_buffering + 2 * ap->output_buffer; + } + + uint32_t can_read = sc_audiobuf_can_read(&ap->buf); + if (can_read > max_buffered_samples) { + uint32_t skip_samples = 0; + + SDL_LockAudioDevice(ap->device); + can_read = sc_audiobuf_can_read(&ap->buf); + if (can_read > max_buffered_samples) { + skip_samples = can_read - max_buffered_samples; + uint32_t r = sc_audiobuf_read(&ap->buf, NULL, skip_samples); + assert(r == skip_samples); + (void) r; + skipped_samples += skip_samples; + } + SDL_UnlockAudioDevice(ap->device); + + if (skip_samples) { + if (played) { + LOGD("[Audio] Buffering threshold exceeded, skipping %" PRIu32 + " samples", skip_samples); #ifndef SC_AUDIO_PLAYER_NDEBUG - LOGD("[Audio] Playback not started, skipping %" PRIu32 " samples", - skip_samples); + } else { + LOGD("[Audio] Playback not started, skipping %" PRIu32 + " samples", skip_samples); #endif + } } } - ap->previous_can_write = sc_audiobuf_can_write(&ap->buf); - ap->received = true; - - SDL_UnlockAudioDevice(ap->device); + atomic_store_explicit(&ap->received, true, memory_order_relaxed); if (played) { // Number of samples added (or removed, if negative) for compensation - int32_t instant_compensation = - (int32_t) samples_written - frame->nb_samples; + int32_t instant_compensation = (int32_t) written - frame->nb_samples; + // Inserting silence instantly increases buffering int32_t inserted_silence = (int32_t) underflow; + // Dropping input samples instantly decreases buffering + int32_t dropped = (int32_t) skipped_samples; // The compensation must apply instantly, it must not be smoothed - ap->avg_buffering.avg += instant_compensation + inserted_silence; - + ap->avg_buffering.avg += + instant_compensation + inserted_silence - dropped; // However, the buffering level must be smoothed - sc_average_push(&ap->avg_buffering, buffered_samples); + sc_average_push(&ap->avg_buffering, can_read); #ifndef SC_AUDIO_PLAYER_NDEBUG - LOGD("[Audio] buffered_samples=%" PRIu32 " avg_buffering=%f", - buffered_samples, sc_average_get(&ap->avg_buffering)); + LOGD("[Audio] can_read=%" PRIu32 " avg_buffering=%f", + can_read, sc_average_get(&ap->avg_buffering)); #endif - ap->samples_since_resync += samples_written; + ap->samples_since_resync += written; if (ap->samples_since_resync >= ap->sample_rate) { // Recompute compensation every second ap->samples_since_resync = 0; @@ -288,7 +284,7 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink, if (abs(diff) < (int) ap->sample_rate / 1000) { // Do not compensate for less than 1ms, the error is just noise diff = 0; - } else if (diff < 0 && buffered_samples < ap->target_buffering) { + } else if (diff < 0 && can_read < ap->target_buffering) { // Do not accelerate if the instant buffering level is below // the average, this would increase underflow diff = 0; @@ -300,8 +296,7 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink, int abs_max_diff = distance / 50; diff = CLAMP(diff, -abs_max_diff, abs_max_diff); LOGV("[Audio] Buffering: target=%" PRIu32 " avg=%f cur=%" PRIu32 - " compensation=%d", ap->target_buffering, avg, - buffered_samples, diff); + " compensation=%d", ap->target_buffering, avg, can_read, diff); if (diff != ap->compensation) { int ret = swr_set_compensation(swr_ctx, diff, distance); @@ -397,7 +392,7 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink, // producer and the consumer. It's too big on purpose, to guarantee that // the producer and the consumer will be able to access it in parallel // without locking. - size_t audiobuf_samples = ap->target_buffering + ap->sample_rate; + uint32_t audiobuf_samples = ap->target_buffering + ap->sample_rate; size_t sample_size = ap->nb_channels * ap->out_bytes_per_sample; bool ok = sc_audiobuf_init(&ap->buf, sample_size, audiobuf_samples); @@ -413,16 +408,15 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink, } ap->swr_buf_alloc_size = initial_swr_buf_size; - ap->previous_can_write = sc_audiobuf_can_write(&ap->buf); - // Samples are produced and consumed by blocks, so the buffering must be // smoothed to get a relatively stable value. sc_average_init(&ap->avg_buffering, 32); ap->samples_since_resync = 0; ap->received = false; - ap->played = false; - ap->underflow = 0; + atomic_init(&ap->played, false); + atomic_init(&ap->received, false); + atomic_init(&ap->underflow, 0); ap->compensation = 0; // The thread calling open() is the thread calling push(), which fills the diff --git a/app/src/audio_player.h b/app/src/audio_player.h index 30378246..0c677363 100644 --- a/app/src/audio_player.h +++ b/app/src/audio_player.h @@ -3,6 +3,7 @@ #include "common.h" +#include #include #include #include @@ -32,13 +33,9 @@ struct sc_audio_player { uint16_t output_buffer; // Audio buffer to communicate between the receiver and the SDL audio - // callback (protected by SDL_AudioDeviceLock()) + // callback struct sc_audiobuf buf; - // The previous empty space in the buffer (only used by the receiver - // thread) - uint32_t previous_can_write; - // Resampler (only used from the receiver thread) struct SwrContext *swr_ctx; @@ -47,7 +44,7 @@ struct sc_audio_player { // The number of channels is the same for input and output unsigned nb_channels; // The number of bytes per sample for a single channel - unsigned out_bytes_per_sample; + size_t out_bytes_per_sample; // Target buffer for resampling (only used by the receiver thread) uint8_t *swr_buf; @@ -61,19 +58,16 @@ struct sc_audio_player { uint32_t samples_since_resync; // Number of silence samples inserted since the last received packet - // (protected by SDL_AudioDeviceLock()) - uint32_t underflow; + atomic_uint_least32_t underflow; // Current applied compensation value (only used by the receiver thread) int compensation; - // Set to true the first time a sample is received (protected by - // SDL_AudioDeviceLock()) - bool received; + // Set to true the first time a sample is received + atomic_bool received; - // Set to true the first time the SDL callback is called (protected by - // SDL_AudioDeviceLock()) - bool played; + // Set to true the first time the SDL callback is called + atomic_bool played; const struct sc_audio_player_callbacks *cbs; void *cbs_userdata; diff --git a/app/src/util/audiobuf.c b/app/src/util/audiobuf.c new file mode 100644 index 00000000..3597f7ee --- /dev/null +++ b/app/src/util/audiobuf.c @@ -0,0 +1,112 @@ +#include "audiobuf.h" + +#include +#include +#include +#include + +bool +sc_audiobuf_init(struct sc_audiobuf *buf, size_t sample_size, + uint32_t capacity) { + assert(sample_size); + assert(capacity); + + // The actual capacity is (alloc_size - 1) so that head == tail is + // non-ambiguous + buf->alloc_size = capacity + 1; + buf->data = sc_allocarray(buf->alloc_size, sample_size); + if (!buf->data) { + LOG_OOM(); + return false; + } + + buf->sample_size = sample_size; + atomic_init(&buf->head, 0); + atomic_init(&buf->tail, 0); + + return true; +} + +void +sc_audiobuf_destroy(struct sc_audiobuf *buf) { + free(buf->data); +} + +uint32_t +sc_audiobuf_read(struct sc_audiobuf *buf, void *to_, uint32_t samples_count) { + assert(samples_count); + + uint8_t *to = to_; + + // Only the reader thread can write tail without synchronization, so + // memory_order_relaxed is sufficient + uint32_t tail = atomic_load_explicit(&buf->tail, memory_order_relaxed); + + // The head cursor is updated after the data is written to the array + uint32_t head = atomic_load_explicit(&buf->head, memory_order_acquire); + + uint32_t can_read = (buf->alloc_size + head - tail) % buf->alloc_size; + if (samples_count > can_read) { + samples_count = can_read; + } + + if (to) { + uint32_t right_count = buf->alloc_size - tail; + if (right_count > samples_count) { + right_count = samples_count; + } + memcpy(to, + buf->data + (tail * buf->sample_size), + right_count * buf->sample_size); + + if (samples_count > right_count) { + uint32_t left_count = samples_count - right_count; + memcpy(to + (right_count * buf->sample_size), + buf->data, + left_count * buf->sample_size); + } + } + + uint32_t new_tail = (tail + samples_count) % buf->alloc_size; + atomic_store_explicit(&buf->tail, new_tail, memory_order_release); + + return samples_count; +} + +uint32_t +sc_audiobuf_write(struct sc_audiobuf *buf, const void *from_, + uint32_t samples_count) { + const uint8_t *from = from_; + + // Only the writer thread can write head, so memory_order_relaxed is + // sufficient + uint32_t head = atomic_load_explicit(&buf->head, memory_order_relaxed); + + // The tail cursor is updated after the data is consumed by the reader + uint32_t tail = atomic_load_explicit(&buf->tail, memory_order_acquire); + + uint32_t can_write = (buf->alloc_size + tail - head - 1) % buf->alloc_size; + if (samples_count > can_write) { + samples_count = can_write; + } + + uint32_t right_count = buf->alloc_size - head; + if (right_count > samples_count) { + right_count = samples_count; + } + memcpy(buf->data + (head * buf->sample_size), + from, + right_count * buf->sample_size); + + if (samples_count > right_count) { + uint32_t left_count = samples_count - right_count; + memcpy(buf->data, + from + (right_count * buf->sample_size), + left_count * buf->sample_size); + } + + uint32_t new_head = (head + samples_count) % buf->alloc_size; + atomic_store_explicit(&buf->head, new_head, memory_order_release); + + return samples_count; +} diff --git a/app/src/util/audiobuf.h b/app/src/util/audiobuf.h index 8616d539..5e7dd4a0 100644 --- a/app/src/util/audiobuf.h +++ b/app/src/util/audiobuf.h @@ -3,19 +3,25 @@ #include "common.h" +#include +#include #include #include -#include "util/bytebuf.h" - /** * Wrapper around bytebuf to read and write samples * * Each sample takes sample_size bytes. */ struct sc_audiobuf { - struct sc_bytebuf buf; + uint8_t *data; + uint32_t alloc_size; // in samples size_t sample_size; + + atomic_uint_least32_t head; // writer cursor, in samples + atomic_uint_least32_t tail; // reader cursor, in samples + // empty: tail == head + // full: ((tail + 1) % alloc_size) == head }; static inline uint32_t @@ -29,66 +35,31 @@ sc_audiobuf_to_bytes(struct sc_audiobuf *buf, uint32_t samples) { return samples * buf->sample_size; } -static inline bool +bool sc_audiobuf_init(struct sc_audiobuf *buf, size_t sample_size, - uint32_t capacity) { - buf->sample_size = sample_size; - return sc_bytebuf_init(&buf->buf, capacity * sample_size + 1); -} + uint32_t capacity); -static inline void -sc_audiobuf_read(struct sc_audiobuf *buf, uint8_t *to, uint32_t samples) { - size_t bytes = sc_audiobuf_to_bytes(buf, samples); - sc_bytebuf_read(&buf->buf, to, bytes); -} +void +sc_audiobuf_destroy(struct sc_audiobuf *buf); -static inline void -sc_audiobuf_skip(struct sc_audiobuf *buf, uint32_t samples) { - size_t bytes = sc_audiobuf_to_bytes(buf, samples); - sc_bytebuf_skip(&buf->buf, bytes); -} +uint32_t +sc_audiobuf_read(struct sc_audiobuf *buf, void *to, uint32_t samples_count); -static inline void -sc_audiobuf_write(struct sc_audiobuf *buf, const uint8_t *from, - uint32_t samples) { - size_t bytes = sc_audiobuf_to_bytes(buf, samples); - sc_bytebuf_write(&buf->buf, from, bytes); -} +uint32_t +sc_audiobuf_write(struct sc_audiobuf *buf, const void *from, + uint32_t samples_count); -static inline void -sc_audiobuf_prepare_write(struct sc_audiobuf *buf, const uint8_t *from, - uint32_t samples) { - size_t bytes = sc_audiobuf_to_bytes(buf, samples); - sc_bytebuf_prepare_write(&buf->buf, from, bytes); -} - -static inline void -sc_audiobuf_commit_write(struct sc_audiobuf *buf, uint32_t samples) { - size_t bytes = sc_audiobuf_to_bytes(buf, samples); - sc_bytebuf_commit_write(&buf->buf, bytes); +static inline uint32_t +sc_audiobuf_capacity(struct sc_audiobuf *buf) { + assert(buf->alloc_size); + return buf->alloc_size - 1; } static inline uint32_t sc_audiobuf_can_read(struct sc_audiobuf *buf) { - size_t bytes = sc_bytebuf_can_read(&buf->buf); - return sc_audiobuf_to_samples(buf, bytes); -} - -static inline uint32_t -sc_audiobuf_can_write(struct sc_audiobuf *buf) { - size_t bytes = sc_bytebuf_can_write(&buf->buf); - return sc_audiobuf_to_samples(buf, bytes); -} - -static inline uint32_t -sc_audiobuf_capacity(struct sc_audiobuf *buf) { - size_t bytes = sc_bytebuf_capacity(&buf->buf); - return sc_audiobuf_to_samples(buf, bytes); -} - -static inline void -sc_audiobuf_destroy(struct sc_audiobuf *buf) { - sc_bytebuf_destroy(&buf->buf); + uint32_t head = atomic_load_explicit(&buf->head, memory_order_acquire); + uint32_t tail = atomic_load_explicit(&buf->tail, memory_order_acquire); + return (buf->alloc_size + head - tail) % buf->alloc_size; } #endif diff --git a/app/src/util/bytebuf.c b/app/src/util/bytebuf.c deleted file mode 100644 index 93544d72..00000000 --- a/app/src/util/bytebuf.c +++ /dev/null @@ -1,104 +0,0 @@ -#include "bytebuf.h" - -#include -#include -#include - -#include "util/log.h" - -bool -sc_bytebuf_init(struct sc_bytebuf *buf, size_t alloc_size) { - assert(alloc_size); - buf->data = malloc(alloc_size); - if (!buf->data) { - LOG_OOM(); - return false; - } - - buf->alloc_size = alloc_size; - buf->head = 0; - buf->tail = 0; - - return true; -} - -void -sc_bytebuf_destroy(struct sc_bytebuf *buf) { - free(buf->data); -} - -void -sc_bytebuf_read(struct sc_bytebuf *buf, uint8_t *to, size_t len) { - assert(len); - assert(len <= sc_bytebuf_can_read(buf)); - assert(buf->tail != buf->head); // the buffer could not be empty - - size_t right_limit = buf->tail < buf->head ? buf->head : buf->alloc_size; - size_t right_len = right_limit - buf->tail; - if (len < right_len) { - right_len = len; - } - memcpy(to, buf->data + buf->tail, right_len); - - if (len > right_len) { - memcpy(to + right_len, buf->data, len - right_len); - } - - buf->tail = (buf->tail + len) % buf->alloc_size; -} - -void -sc_bytebuf_skip(struct sc_bytebuf *buf, size_t len) { - assert(len); - assert(len <= sc_bytebuf_can_read(buf)); - assert(buf->tail != buf->head); // the buffer could not be empty - - buf->tail = (buf->tail + len) % buf->alloc_size; -} - -static inline void -sc_bytebuf_write_step0(struct sc_bytebuf *buf, const uint8_t *from, - size_t len) { - size_t right_len = buf->alloc_size - buf->head; - if (len < right_len) { - right_len = len; - } - memcpy(buf->data + buf->head, from, right_len); - - if (len > right_len) { - memcpy(buf->data, from + right_len, len - right_len); - } -} - -static inline void -sc_bytebuf_write_step1(struct sc_bytebuf *buf, size_t len) { - buf->head = (buf->head + len) % buf->alloc_size; -} - -void -sc_bytebuf_write(struct sc_bytebuf *buf, const uint8_t *from, size_t len) { - assert(len); - assert(len <= sc_bytebuf_can_write(buf)); - - sc_bytebuf_write_step0(buf, from, len); - sc_bytebuf_write_step1(buf, len); -} - -void -sc_bytebuf_prepare_write(struct sc_bytebuf *buf, const uint8_t *from, - size_t len) { - // *This function MUST NOT access buf->tail (even in assert()).* - // The purpose of this function is to allow a reader and a writer to access - // different parts of the buffer in parallel simultaneously. It is intended - // to be called without lock (only sc_bytebuf_commit_write() is intended to - // be called with lock held). - - assert(len < buf->alloc_size - 1); - sc_bytebuf_write_step0(buf, from, len); -} - -void -sc_bytebuf_commit_write(struct sc_bytebuf *buf, size_t len) { - assert(len <= sc_bytebuf_can_write(buf)); - sc_bytebuf_write_step1(buf, len); -} diff --git a/app/src/util/bytebuf.h b/app/src/util/bytebuf.h deleted file mode 100644 index 1448f752..00000000 --- a/app/src/util/bytebuf.h +++ /dev/null @@ -1,114 +0,0 @@ -#ifndef SC_BYTEBUF_H -#define SC_BYTEBUF_H - -#include "common.h" - -#include -#include - -struct sc_bytebuf { - uint8_t *data; - // The actual capacity is (allocated - 1) so that head == tail is - // non-ambiguous - size_t alloc_size; - size_t head; // writter cursor - size_t tail; // reader cursor - // empty: tail == head - // full: ((tail + 1) % alloc_size) == head -}; - -bool -sc_bytebuf_init(struct sc_bytebuf *buf, size_t alloc_size); - -/** - * Copy from the bytebuf to a user-provided array - * - * The caller must check that len <= sc_bytebuf_read_available() (it is an - * error to attempt to read more bytes than available). - * - * This function is guaranteed not to write to buf->head. - */ -void -sc_bytebuf_read(struct sc_bytebuf *buf, uint8_t *to, size_t len); - -/** - * Drop len bytes from the buffer - * - * The caller must check that len <= sc_bytebuf_read_available() (it is an - * error to attempt to skip more bytes than available). - * - * This function is guaranteed not to write to buf->head. - * - * It is equivalent to call sc_bytebuf_read() to some array and discard the - * array (but this function is more efficient since there is no copy). - */ -void -sc_bytebuf_skip(struct sc_bytebuf *buf, size_t len); - -/** - * Copy the user-provided array to the bytebuf - * - * The caller must check that len <= sc_bytebuf_write_available() (it is an - * error to write more bytes than the remaining available space). - * - * This function is guaranteed not to write to buf->tail. - */ -void -sc_bytebuf_write(struct sc_bytebuf *buf, const uint8_t *from, size_t len); - -/** - * Copy the user-provided array to the bytebuf, but do not advance the cursor - * - * The caller must check that len <= sc_bytebuf_write_available() (it is an - * error to write more bytes than the remaining available space). - * - * After this function is called, the write must be committed with - * sc_bytebuf_commit_write(). - * - * The purpose of this mechanism is to acquire a lock only to commit the write, - * but not to perform the actual copy. - * - * This function is guaranteed not to access buf->tail. - */ -void -sc_bytebuf_prepare_write(struct sc_bytebuf *buf, const uint8_t *from, - size_t len); - -/** - * Commit a prepared write - */ -void -sc_bytebuf_commit_write(struct sc_bytebuf *buf, size_t len); - -/** - * Return the number of bytes which can be read - * - * It is an error to read more bytes than available. - */ -static inline size_t -sc_bytebuf_can_read(struct sc_bytebuf *buf) { - return (buf->alloc_size + buf->head - buf->tail) % buf->alloc_size; -} - -/** - * Return the number of bytes which can be written - * - * It is an error to write more bytes than available. - */ -static inline size_t -sc_bytebuf_can_write(struct sc_bytebuf *buf) { - return (buf->alloc_size + buf->tail - buf->head - 1) % buf->alloc_size; -} - -/** - * Return the actual capacity of the buffer (can_read() + can_write()) - */ -static inline size_t -sc_bytebuf_capacity(struct sc_bytebuf *buf) { - return buf->alloc_size - 1; -} - -void -sc_bytebuf_destroy(struct sc_bytebuf *buf); - -#endif diff --git a/app/tests/test_audiobuf.c b/app/tests/test_audiobuf.c new file mode 100644 index 00000000..94d0f07a --- /dev/null +++ b/app/tests/test_audiobuf.c @@ -0,0 +1,128 @@ +#include "common.h" + +#include +#include + +#include "util/audiobuf.h" + +static void test_audiobuf_simple(void) { + struct sc_audiobuf buf; + uint32_t data[20]; + + bool ok = sc_audiobuf_init(&buf, 4, 20); + assert(ok); + + uint32_t samples[] = {1, 2, 3, 4, 5}; + uint32_t w = sc_audiobuf_write(&buf, samples, 5); + assert(w == 5); + + uint32_t r = sc_audiobuf_read(&buf, data, 4); + assert(r == 4); + assert(!memcmp(data, samples, 16)); + + uint32_t samples2[] = {6, 7, 8}; + w = sc_audiobuf_write(&buf, samples2, 3); + assert(w == 3); + + uint32_t single = 9; + w = sc_audiobuf_write(&buf, &single, 1); + assert(w == 1); + + r = sc_audiobuf_read(&buf, &data[4], 8); + assert(r == 5); + + uint32_t expected[] = {1, 2, 3, 4, 5, 6, 7, 8, 9}; + assert(!memcmp(data, expected, 36)); + + sc_audiobuf_destroy(&buf); +} + +static void test_audiobuf_boundaries(void) { + struct sc_audiobuf buf; + uint32_t data[20]; + + bool ok = sc_audiobuf_init(&buf, 4, 20); + assert(ok); + + uint32_t samples[] = {1, 2, 3, 4, 5, 6}; + uint32_t w = sc_audiobuf_write(&buf, samples, 6); + assert(w == 6); + + w = sc_audiobuf_write(&buf, samples, 6); + assert(w == 6); + + w = sc_audiobuf_write(&buf, samples, 6); + assert(w == 6); + + uint32_t r = sc_audiobuf_read(&buf, data, 9); + assert(r == 9); + + uint32_t expected[] = {1, 2, 3, 4, 5, 6, 1, 2, 3}; + assert(!memcmp(data, expected, 36)); + + uint32_t samples2[] = {7, 8, 9, 10, 11}; + w = sc_audiobuf_write(&buf, samples2, 5); + assert(w == 5); + + uint32_t single = 12; + w = sc_audiobuf_write(&buf, &single, 1); + assert(w == 1); + + w = sc_audiobuf_read(&buf, NULL, 3); + assert(w == 3); + + assert(sc_audiobuf_can_read(&buf) == 12); + + r = sc_audiobuf_read(&buf, data, 12); + assert(r == 12); + + uint32_t expected2[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}; + assert(!memcmp(data, expected2, 48)); + + sc_audiobuf_destroy(&buf); +} + +static void test_audiobuf_partial_read_write(void) { + struct sc_audiobuf buf; + uint32_t data[15]; + + bool ok = sc_audiobuf_init(&buf, 4, 10); + assert(ok); + + uint32_t samples[] = {1, 2, 3, 4, 5, 6}; + uint32_t w = sc_audiobuf_write(&buf, samples, 6); + assert(w == 6); + + w = sc_audiobuf_write(&buf, samples, 6); + assert(w == 4); + + w = sc_audiobuf_write(&buf, samples, 6); + assert(w == 0); + + uint32_t r = sc_audiobuf_read(&buf, data, 3); + assert(r == 3); + + uint32_t expected[] = {1, 2, 3}; + assert(!memcmp(data, expected, 12)); + + w = sc_audiobuf_write(&buf, samples, 6); + assert(w == 3); + + r = sc_audiobuf_read(&buf, data, 15); + assert(r == 10); + uint32_t expected2[] = {4, 5, 6, 1, 2, 3, 4, 1, 2, 3}; + assert(!memcmp(data, expected2, 12)); + + sc_audiobuf_destroy(&buf); +} + +int main(int argc, char *argv[]) { + (void) argc; + (void) argv; + + test_audiobuf_simple(); + test_audiobuf_boundaries(); + test_audiobuf_partial_read_write(); + + return 0; +} diff --git a/app/tests/test_bytebuf.c b/app/tests/test_bytebuf.c deleted file mode 100644 index 8e9d7c57..00000000 --- a/app/tests/test_bytebuf.c +++ /dev/null @@ -1,126 +0,0 @@ -#include "common.h" - -#include -#include - -#include "util/bytebuf.h" - -static void test_bytebuf_simple(void) { - struct sc_bytebuf buf; - uint8_t data[20]; - - bool ok = sc_bytebuf_init(&buf, 20); - assert(ok); - - sc_bytebuf_write(&buf, (uint8_t *) "hello", sizeof("hello") - 1); - assert(sc_bytebuf_can_read(&buf) == 5); - - sc_bytebuf_read(&buf, data, 4); - assert(!strncmp((char *) data, "hell", 4)); - - sc_bytebuf_write(&buf, (uint8_t *) " world", sizeof(" world") - 1); - assert(sc_bytebuf_can_read(&buf) == 7); - - sc_bytebuf_write(&buf, (uint8_t *) "!", 1); - assert(sc_bytebuf_can_read(&buf) == 8); - - sc_bytebuf_read(&buf, &data[4], 8); - assert(sc_bytebuf_can_read(&buf) == 0); - - data[12] = '\0'; - assert(!strcmp((char *) data, "hello world!")); - assert(sc_bytebuf_can_read(&buf) == 0); - - sc_bytebuf_destroy(&buf); -} - -static void test_bytebuf_boundaries(void) { - struct sc_bytebuf buf; - uint8_t data[20]; - - bool ok = sc_bytebuf_init(&buf, 20); - assert(ok); - - sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1); - assert(sc_bytebuf_can_read(&buf) == 6); - - sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1); - assert(sc_bytebuf_can_read(&buf) == 12); - - sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1); - assert(sc_bytebuf_can_read(&buf) == 18); - - sc_bytebuf_read(&buf, data, 9); - assert(!strncmp((char *) data, "hello hel", 9)); - assert(sc_bytebuf_can_read(&buf) == 9); - - sc_bytebuf_write(&buf, (uint8_t *) "world", sizeof("world") - 1); - assert(sc_bytebuf_can_read(&buf) == 14); - - sc_bytebuf_write(&buf, (uint8_t *) "!", 1); - assert(sc_bytebuf_can_read(&buf) == 15); - - sc_bytebuf_skip(&buf, 3); - assert(sc_bytebuf_can_read(&buf) == 12); - - sc_bytebuf_read(&buf, data, 12); - data[12] = '\0'; - assert(!strcmp((char *) data, "hello world!")); - assert(sc_bytebuf_can_read(&buf) == 0); - - sc_bytebuf_destroy(&buf); -} - -static void test_bytebuf_two_steps_write(void) { - struct sc_bytebuf buf; - uint8_t data[20]; - - bool ok = sc_bytebuf_init(&buf, 20); - assert(ok); - - sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1); - assert(sc_bytebuf_can_read(&buf) == 6); - - sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1); - assert(sc_bytebuf_can_read(&buf) == 12); - - sc_bytebuf_prepare_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1); - assert(sc_bytebuf_can_read(&buf) == 12); // write not committed yet - - sc_bytebuf_read(&buf, data, 9); - assert(!strncmp((char *) data, "hello hel", 3)); - assert(sc_bytebuf_can_read(&buf) == 3); - - sc_bytebuf_commit_write(&buf, sizeof("hello ") - 1); - assert(sc_bytebuf_can_read(&buf) == 9); - - sc_bytebuf_prepare_write(&buf, (uint8_t *) "world", sizeof("world") - 1); - assert(sc_bytebuf_can_read(&buf) == 9); // write not committed yet - - sc_bytebuf_commit_write(&buf, sizeof("world") - 1); - assert(sc_bytebuf_can_read(&buf) == 14); - - sc_bytebuf_write(&buf, (uint8_t *) "!", 1); - assert(sc_bytebuf_can_read(&buf) == 15); - - sc_bytebuf_skip(&buf, 3); - assert(sc_bytebuf_can_read(&buf) == 12); - - sc_bytebuf_read(&buf, data, 12); - data[12] = '\0'; - assert(!strcmp((char *) data, "hello world!")); - assert(sc_bytebuf_can_read(&buf) == 0); - - sc_bytebuf_destroy(&buf); -} - -int main(int argc, char *argv[]) { - (void) argc; - (void) argv; - - test_bytebuf_simple(); - test_bytebuf_boundaries(); - test_bytebuf_two_steps_write(); - - return 0; -} From 44abed5c68d657c664457eb562318168876d2208 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 7 Jan 2024 18:23:24 +0100 Subject: [PATCH 1753/2244] Improve audio compensation thresholds Use different thresholds for enabling and disabling compensation. Concretely, enable compensation if the difference between the average and the target buffering levels exceeds 4 ms (instead of 1 ms). This avoids unnecessary compensation due to small noise in buffering level estimation. But keep a smaller threshold (1 ms) for disabling compensation, so that the buffering level is restored closer to the target value. This avoids to keep the actual level close to the compensation threshold. PR #4572 --- app/src/audio_player.c | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/app/src/audio_player.c b/app/src/audio_player.c index 728d3f2a..c70964b9 100644 --- a/app/src/audio_player.c +++ b/app/src/audio_player.c @@ -281,8 +281,15 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink, float avg = sc_average_get(&ap->avg_buffering); int diff = ap->target_buffering - avg; - if (abs(diff) < (int) ap->sample_rate / 1000) { - // Do not compensate for less than 1ms, the error is just noise + + // Enable compensation when the difference exceeds +/- 4ms. + // Disable compensation when the difference is lower than +/- 1ms. + int threshold = ap->compensation != 0 + ? ap->sample_rate / 1000 /* 1ms */ + : ap->sample_rate * 4 / 1000; /* 4ms */ + + if (abs(diff) < threshold) { + // Do not compensate for small values, the error is just noise diff = 0; } else if (diff < 0 && can_read < ap->target_buffering) { // Do not accelerate if the instant buffering level is below From edac4b8a9a4a261a82400828b17d7b7ae0b33cd0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 7 Jan 2024 18:27:56 +0100 Subject: [PATCH 1754/2244] Increase buffering level smoothness The buffering level does not change continuously: it increases abruptly when a packet is received, and decreases abruptly when an audio block is consumed. To estimate the buffering level, a rolling average is used. To make the buffering more stable, increase the smoothness of this rolling average. This decreases the risk of enabling audio compensation due to an estimation error. PR #4572 --- app/src/audio_player.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/audio_player.c b/app/src/audio_player.c index c70964b9..4552b0f7 100644 --- a/app/src/audio_player.c +++ b/app/src/audio_player.c @@ -417,7 +417,7 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink, // Samples are produced and consumed by blocks, so the buffering must be // smoothed to get a relatively stable value. - sc_average_init(&ap->avg_buffering, 32); + sc_average_init(&ap->avg_buffering, 128); ap->samples_since_resync = 0; ap->received = false; From dfa3f97a87c0b42289920777731b4da95369d7ed Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 24 Jan 2024 14:46:16 +0100 Subject: [PATCH 1755/2244] Fix audio player comment PR #4572 --- app/src/audio_player.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/audio_player.c b/app/src/audio_player.c index 4552b0f7..4d101b01 100644 --- a/app/src/audio_player.c +++ b/app/src/audio_player.c @@ -293,7 +293,7 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink, diff = 0; } else if (diff < 0 && can_read < ap->target_buffering) { // Do not accelerate if the instant buffering level is below - // the average, this would increase underflow + // the target, this would increase underflow diff = 0; } // Compensate the diff over 4 seconds (but will be recomputed after From 4502126e3b2ab2d5a82f636e32524929b3b0d07e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 2 Feb 2024 15:02:09 +0100 Subject: [PATCH 1756/2244] Use early return to avoid additional indentation PR #4572 --- app/src/audio_player.c | 99 +++++++++++++++++++++--------------------- 1 file changed, 50 insertions(+), 49 deletions(-) diff --git a/app/src/audio_player.c b/app/src/audio_player.c index 4d101b01..e978cd9f 100644 --- a/app/src/audio_player.c +++ b/app/src/audio_player.c @@ -253,66 +253,67 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink, } atomic_store_explicit(&ap->received, true, memory_order_relaxed); + if (!played) { + // Nothing more to do + return true; + } - if (played) { - // Number of samples added (or removed, if negative) for compensation - int32_t instant_compensation = (int32_t) written - frame->nb_samples; - // Inserting silence instantly increases buffering - int32_t inserted_silence = (int32_t) underflow; - // Dropping input samples instantly decreases buffering - int32_t dropped = (int32_t) skipped_samples; + // Number of samples added (or removed, if negative) for compensation + int32_t instant_compensation = (int32_t) written - frame->nb_samples; + // Inserting silence instantly increases buffering + int32_t inserted_silence = (int32_t) underflow; + // Dropping input samples instantly decreases buffering + int32_t dropped = (int32_t) skipped_samples; - // The compensation must apply instantly, it must not be smoothed - ap->avg_buffering.avg += - instant_compensation + inserted_silence - dropped; + // The compensation must apply instantly, it must not be smoothed + ap->avg_buffering.avg += instant_compensation + inserted_silence - dropped; - // However, the buffering level must be smoothed - sc_average_push(&ap->avg_buffering, can_read); + // However, the buffering level must be smoothed + sc_average_push(&ap->avg_buffering, can_read); #ifndef SC_AUDIO_PLAYER_NDEBUG - LOGD("[Audio] can_read=%" PRIu32 " avg_buffering=%f", - can_read, sc_average_get(&ap->avg_buffering)); + LOGD("[Audio] can_read=%" PRIu32 " avg_buffering=%f", + can_read, sc_average_get(&ap->avg_buffering)); #endif - ap->samples_since_resync += written; - if (ap->samples_since_resync >= ap->sample_rate) { - // Recompute compensation every second - ap->samples_since_resync = 0; + ap->samples_since_resync += written; + if (ap->samples_since_resync >= ap->sample_rate) { + // Recompute compensation every second + ap->samples_since_resync = 0; - float avg = sc_average_get(&ap->avg_buffering); - int diff = ap->target_buffering - avg; + float avg = sc_average_get(&ap->avg_buffering); + int diff = ap->target_buffering - avg; - // Enable compensation when the difference exceeds +/- 4ms. - // Disable compensation when the difference is lower than +/- 1ms. - int threshold = ap->compensation != 0 - ? ap->sample_rate / 1000 /* 1ms */ - : ap->sample_rate * 4 / 1000; /* 4ms */ + // Enable compensation when the difference exceeds +/- 4ms. + // Disable compensation when the difference is lower than +/- 1ms. + int threshold = ap->compensation != 0 + ? ap->sample_rate / 1000 /* 1ms */ + : ap->sample_rate * 4 / 1000; /* 4ms */ - if (abs(diff) < threshold) { - // Do not compensate for small values, the error is just noise - diff = 0; - } else if (diff < 0 && can_read < ap->target_buffering) { - // Do not accelerate if the instant buffering level is below - // the target, this would increase underflow - diff = 0; - } - // Compensate the diff over 4 seconds (but will be recomputed after - // 1 second) - int distance = 4 * ap->sample_rate; - // Limit compensation rate to 2% - int abs_max_diff = distance / 50; - diff = CLAMP(diff, -abs_max_diff, abs_max_diff); - LOGV("[Audio] Buffering: target=%" PRIu32 " avg=%f cur=%" PRIu32 - " compensation=%d", ap->target_buffering, avg, can_read, diff); + if (abs(diff) < threshold) { + // Do not compensate for small values, the error is just noise + diff = 0; + } else if (diff < 0 && can_read < ap->target_buffering) { + // Do not accelerate if the instant buffering level is below the + // target, this would increase underflow + diff = 0; + } + // Compensate the diff over 4 seconds (but will be recomputed after 1 + // second) + int distance = 4 * ap->sample_rate; + // Limit compensation rate to 2% + int abs_max_diff = distance / 50; + diff = CLAMP(diff, -abs_max_diff, abs_max_diff); + LOGV("[Audio] Buffering: target=%" PRIu32 " avg=%f cur=%" PRIu32 + " compensation=%d", ap->target_buffering, avg, can_read, diff); - if (diff != ap->compensation) { - int ret = swr_set_compensation(swr_ctx, diff, distance); - if (ret < 0) { - LOGW("Resampling compensation failed: %d", ret); - // not fatal - } else { - ap->compensation = diff; - } + if (diff != ap->compensation) { + int ret = swr_set_compensation(swr_ctx, diff, distance); + if (ret < 0) { + LOGW("Resampling compensation failed: %d", ret); + // not fatal + } else { + ap->compensation = diff; } } } From c12fdf900fb82e8ce958881e24a9f4f6185c0db7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 7 Feb 2024 17:50:14 +0100 Subject: [PATCH 1757/2244] Minimize buffer underflow on starting If playback starts too early, insert silence until the buffer is filled up to at least target_buffering before playing. PR #4572 --- app/src/audio_player.c | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/src/audio_player.c b/app/src/audio_player.c index e978cd9f..ea44e8d9 100644 --- a/app/src/audio_player.c +++ b/app/src/audio_player.c @@ -79,10 +79,9 @@ sc_audio_player_sdl_callback(void *userdata, uint8_t *stream, int len_int) { bool played = atomic_load_explicit(&ap->played, memory_order_relaxed); if (!played) { uint32_t buffered_samples = sc_audiobuf_can_read(&ap->buf); - // Part of the buffering is handled by inserting initial silence. The - // remaining (margin) last samples will be handled by compensation. - uint32_t margin = 30 * ap->sample_rate / 1000; // 30ms - if (buffered_samples + margin < ap->target_buffering) { + // Wait until the buffer is filled up to at least target_buffering + // before playing + if (buffered_samples < ap->target_buffering) { LOGV("[Audio] Inserting initial buffering silence: %" PRIu32 " samples", count); // Delay playback starting to reach the target buffering. Fill the From a7cf4daf3bcb573add846c2f4c98b94944c05cb9 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 8 Feb 2024 12:31:03 +0100 Subject: [PATCH 1758/2244] Avoid negative average buffering The assumption that underflow and overbuffering are caused by jitter (and that the delay between the producer and consumer will be caught up) does not always hold. For example, if the consumer does not consume at the expected rate (the SDL callback is not called often enough, which is an audio output issue), many samples will be dropped due to overbuffering, decreasing the average buffering indefinitely. Prevent the average buffering to become negative to limit the consequences of an unexpected behavior. PR #4572 --- app/src/audio_player.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/src/audio_player.c b/app/src/audio_player.c index ea44e8d9..bd799c51 100644 --- a/app/src/audio_player.c +++ b/app/src/audio_player.c @@ -266,6 +266,16 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink, // The compensation must apply instantly, it must not be smoothed ap->avg_buffering.avg += instant_compensation + inserted_silence - dropped; + if (ap->avg_buffering.avg < 0) { + // Since dropping samples instantly reduces buffering, the difference + // is applied immediately to the average value, assuming that the delay + // between the producer and the consumer will be caught up. + // + // However, when this assumption is not valid, the average buffering + // may decrease indefinitely. Prevent it to become negative to limit + // the consequences. + ap->avg_buffering.avg = 0; + } // However, the buffering level must be smoothed sc_average_push(&ap->avg_buffering, can_read); From 25f1e703b7637c3eb1382e435113688520a38d36 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 22 Feb 2024 18:52:57 +0100 Subject: [PATCH 1759/2244] Extract ControlChannel class This prevents many components from depending on the whole DesktopConnection. --- .../com/genymobile/scrcpy/ControlChannel.java | 33 +++++++++++++++++++ .../com/genymobile/scrcpy/Controller.java | 10 +++--- .../genymobile/scrcpy/DesktopConnection.java | 32 ++++-------------- .../scrcpy/DeviceMessageSender.java | 10 +++--- .../java/com/genymobile/scrcpy/Server.java | 3 +- 5 files changed, 51 insertions(+), 37 deletions(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/ControlChannel.java diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlChannel.java b/server/src/main/java/com/genymobile/scrcpy/ControlChannel.java new file mode 100644 index 00000000..4677cfda --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/ControlChannel.java @@ -0,0 +1,33 @@ +package com.genymobile.scrcpy; + +import android.net.LocalSocket; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public final class ControlChannel { + private final InputStream inputStream; + private final OutputStream outputStream; + + private final ControlMessageReader reader = new ControlMessageReader(); + private final DeviceMessageWriter writer = new DeviceMessageWriter(); + + public ControlChannel(LocalSocket controlSocket) throws IOException { + this.inputStream = controlSocket.getInputStream(); + this.outputStream = controlSocket.getOutputStream(); + } + + public ControlMessage recv() throws IOException { + ControlMessage msg = reader.next(); + while (msg == null) { + reader.readFrom(inputStream); + msg = reader.next(); + } + return msg; + } + + public void send(DeviceMessage msg) throws IOException { + writer.writeTo(msg, outputStream); + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index c0763012..257f732b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -27,7 +27,7 @@ public class Controller implements AsyncProcessor { private Thread thread; private final Device device; - private final DesktopConnection connection; + private final ControlChannel controlChannel; private final CleanUp cleanUp; private final DeviceMessageSender sender; private final boolean clipboardAutosync; @@ -42,14 +42,14 @@ public class Controller implements AsyncProcessor { private boolean keepPowerModeOff; - public Controller(Device device, DesktopConnection connection, CleanUp cleanUp, boolean clipboardAutosync, boolean powerOn) { + public Controller(Device device, ControlChannel controlChannel, CleanUp cleanUp, boolean clipboardAutosync, boolean powerOn) { this.device = device; - this.connection = connection; + this.controlChannel = controlChannel; this.cleanUp = cleanUp; this.clipboardAutosync = clipboardAutosync; this.powerOn = powerOn; initPointers(); - sender = new DeviceMessageSender(connection); + sender = new DeviceMessageSender(controlChannel); } private void initPointers() { @@ -123,7 +123,7 @@ public class Controller implements AsyncProcessor { } private void handleEvent() throws IOException { - ControlMessage msg = connection.receiveControlMessage(); + ControlMessage msg = controlChannel.recv(); switch (msg.getType()) { case ControlMessage.TYPE_INJECT_KEYCODE: if (device.supportsInputEvents()) { diff --git a/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java b/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java index 8bc743f8..d693ad61 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java +++ b/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java @@ -7,8 +7,6 @@ import android.net.LocalSocketAddress; import java.io.Closeable; import java.io.FileDescriptor; import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; import java.nio.charset.StandardCharsets; public final class DesktopConnection implements Closeable { @@ -24,25 +22,16 @@ public final class DesktopConnection implements Closeable { private final FileDescriptor audioFd; private final LocalSocket controlSocket; - private final InputStream controlInputStream; - private final OutputStream controlOutputStream; - - private final ControlMessageReader reader = new ControlMessageReader(); - private final DeviceMessageWriter writer = new DeviceMessageWriter(); + private final ControlChannel controlChannel; private DesktopConnection(LocalSocket videoSocket, LocalSocket audioSocket, LocalSocket controlSocket) throws IOException { this.videoSocket = videoSocket; - this.controlSocket = controlSocket; this.audioSocket = audioSocket; - if (controlSocket != null) { - controlInputStream = controlSocket.getInputStream(); - controlOutputStream = controlSocket.getOutputStream(); - } else { - controlInputStream = null; - controlOutputStream = null; - } + this.controlSocket = controlSocket; + videoFd = videoSocket != null ? videoSocket.getFileDescriptor() : null; audioFd = audioSocket != null ? audioSocket.getFileDescriptor() : null; + controlChannel = controlSocket != null ? new ControlChannel(controlSocket) : null; } private static LocalSocket connect(String abstractName) throws IOException { @@ -179,16 +168,7 @@ public final class DesktopConnection implements Closeable { return audioFd; } - public ControlMessage receiveControlMessage() throws IOException { - ControlMessage msg = reader.next(); - while (msg == null) { - reader.readFrom(controlInputStream); - msg = reader.next(); - } - return msg; - } - - public void sendDeviceMessage(DeviceMessage msg) throws IOException { - writer.writeTo(msg, controlOutputStream); + public ControlChannel getControlChannel() { + return controlChannel; } } diff --git a/server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java b/server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java index 94e842ee..efb7b975 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java +++ b/server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java @@ -4,7 +4,7 @@ import java.io.IOException; public final class DeviceMessageSender { - private final DesktopConnection connection; + private final ControlChannel controlChannel; private Thread thread; @@ -12,8 +12,8 @@ public final class DeviceMessageSender { private long ack; - public DeviceMessageSender(DesktopConnection connection) { - this.connection = connection; + public DeviceMessageSender(ControlChannel controlChannel) { + this.controlChannel = controlChannel; } public synchronized void pushClipboardText(String text) { @@ -43,11 +43,11 @@ public final class DeviceMessageSender { if (sequence != DeviceMessage.SEQUENCE_INVALID) { DeviceMessage event = DeviceMessage.createAckClipboard(sequence); - connection.sendDeviceMessage(event); + controlChannel.send(event); } if (text != null) { DeviceMessage event = DeviceMessage.createClipboard(text); - connection.sendDeviceMessage(event); + controlChannel.send(event); } } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index bcafa133..3936648d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -131,7 +131,8 @@ public final class Server { } if (control) { - Controller controller = new Controller(device, connection, cleanUp, options.getClipboardAutosync(), options.getPowerOn()); + ControlChannel controlChannel = connection.getControlChannel(); + Controller controller = new Controller(device, controlChannel, cleanUp, options.getClipboardAutosync(), options.getPowerOn()); device.setClipboardListener(text -> controller.getSender().pushClipboardText(text)); asyncProcessors.add(controller); } From 9e22f3bf1cca9a957173193250eaeb084ab0c245 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 23 Feb 2024 19:59:54 +0100 Subject: [PATCH 1760/2244] Replace unsigned char by uint8_t for buffers For consistency. --- app/src/control_msg.c | 4 +- app/src/control_msg.h | 2 +- app/src/controller.c | 2 +- app/src/device_msg.c | 3 +- app/src/device_msg.h | 3 +- app/src/receiver.c | 5 ++- app/src/server.c | 2 +- app/src/usb/aoa_hid.c | 4 +- app/src/usb/aoa_hid.h | 2 +- app/tests/test_control_msg_serialize.c | 57 +++++++++++++------------ app/tests/test_device_msg_deserialize.c | 10 ++--- 11 files changed, 47 insertions(+), 47 deletions(-) diff --git a/app/src/control_msg.c b/app/src/control_msg.c index d4d6c62a..e173dac7 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -87,7 +87,7 @@ write_position(uint8_t *buf, const struct sc_position *position) { // write length (4 bytes) + string (non null-terminated) static size_t -write_string(const char *utf8, size_t max_len, unsigned char *buf) { +write_string(const char *utf8, size_t max_len, uint8_t *buf) { size_t len = sc_str_utf8_truncation_index(utf8, max_len); sc_write32be(buf, len); memcpy(&buf[4], utf8, len); @@ -95,7 +95,7 @@ write_string(const char *utf8, size_t max_len, unsigned char *buf) { } size_t -sc_control_msg_serialize(const struct sc_control_msg *msg, unsigned char *buf) { +sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) { buf[0] = msg->type; switch (msg->type) { case SC_CONTROL_MSG_TYPE_INJECT_KEYCODE: diff --git a/app/src/control_msg.h b/app/src/control_msg.h index b90a00b3..04eeb83b 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -98,7 +98,7 @@ struct sc_control_msg { // buf size must be at least CONTROL_MSG_MAX_SIZE // return the number of bytes written size_t -sc_control_msg_serialize(const struct sc_control_msg *msg, unsigned char *buf); +sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf); void sc_control_msg_log(const struct sc_control_msg *msg); diff --git a/app/src/controller.c b/app/src/controller.c index 0139e42c..250321fe 100644 --- a/app/src/controller.c +++ b/app/src/controller.c @@ -84,7 +84,7 @@ sc_controller_push_msg(struct sc_controller *controller, static bool process_msg(struct sc_controller *controller, const struct sc_control_msg *msg) { - static unsigned char serialized_msg[SC_CONTROL_MSG_MAX_SIZE]; + static uint8_t serialized_msg[SC_CONTROL_MSG_MAX_SIZE]; size_t length = sc_control_msg_serialize(msg, serialized_msg); if (!length) { return false; diff --git a/app/src/device_msg.c b/app/src/device_msg.c index 265c7505..9925cf97 100644 --- a/app/src/device_msg.c +++ b/app/src/device_msg.c @@ -8,8 +8,7 @@ #include "util/log.h" ssize_t -device_msg_deserialize(const unsigned char *buf, size_t len, - struct device_msg *msg) { +device_msg_deserialize(const uint8_t *buf, size_t len, struct device_msg *msg) { if (len < 5) { // at least type + empty string length return 0; // not available diff --git a/app/src/device_msg.h b/app/src/device_msg.h index e8d9fed4..3b68a61a 100644 --- a/app/src/device_msg.h +++ b/app/src/device_msg.h @@ -30,8 +30,7 @@ struct device_msg { // return the number of bytes consumed (0 for no msg available, -1 on error) ssize_t -device_msg_deserialize(const unsigned char *buf, size_t len, - struct device_msg *msg); +device_msg_deserialize(const uint8_t *buf, size_t len, struct device_msg *msg); void device_msg_destroy(struct device_msg *msg); diff --git a/app/src/receiver.c b/app/src/receiver.c index e715a8e6..c08cd6cf 100644 --- a/app/src/receiver.c +++ b/app/src/receiver.c @@ -1,6 +1,7 @@ #include "receiver.h" #include +#include #include #include "device_msg.h" @@ -51,7 +52,7 @@ process_msg(struct sc_receiver *receiver, struct device_msg *msg) { } static ssize_t -process_msgs(struct sc_receiver *receiver, const unsigned char *buf, size_t len) { +process_msgs(struct sc_receiver *receiver, const uint8_t *buf, size_t len) { size_t head = 0; for (;;) { struct device_msg msg; @@ -78,7 +79,7 @@ static int run_receiver(void *data) { struct sc_receiver *receiver = data; - static unsigned char buf[DEVICE_MSG_MAX_SIZE]; + static uint8_t buf[DEVICE_MSG_MAX_SIZE]; size_t head = 0; for (;;) { diff --git a/app/src/server.c b/app/src/server.c index d4726c2a..4d55e994 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -498,7 +498,7 @@ sc_server_init(struct sc_server *server, const struct sc_server_params *params, static bool device_read_info(struct sc_intr *intr, sc_socket device_socket, struct sc_server_info *info) { - unsigned char buf[SC_DEVICE_NAME_FIELD_LENGTH]; + uint8_t buf[SC_DEVICE_NAME_FIELD_LENGTH]; ssize_t r = net_recv_all_intr(intr, device_socket, buf, sizeof(buf)); if (r < SC_DEVICE_NAME_FIELD_LENGTH) { LOGE("Could not retrieve device information"); diff --git a/app/src/usb/aoa_hid.c b/app/src/usb/aoa_hid.c index fb64e57c..9bad5296 100644 --- a/app/src/usb/aoa_hid.c +++ b/app/src/usb/aoa_hid.c @@ -113,7 +113,7 @@ sc_aoa_register_hid(struct sc_aoa *aoa, uint16_t accessory_id, static bool sc_aoa_set_hid_report_desc(struct sc_aoa *aoa, uint16_t accessory_id, - const unsigned char *report_desc, + const uint8_t *report_desc, uint16_t report_desc_size) { uint8_t request_type = LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR; uint8_t request = ACCESSORY_SET_HID_REPORT_DESC; @@ -150,7 +150,7 @@ sc_aoa_set_hid_report_desc(struct sc_aoa *aoa, uint16_t accessory_id, bool sc_aoa_setup_hid(struct sc_aoa *aoa, uint16_t accessory_id, - const unsigned char *report_desc, uint16_t report_desc_size) { + const uint8_t *report_desc, uint16_t report_desc_size) { bool ok = sc_aoa_register_hid(aoa, accessory_id, report_desc_size); if (!ok) { return false; diff --git a/app/src/usb/aoa_hid.h b/app/src/usb/aoa_hid.h index 8803c1d9..fb5e1d28 100644 --- a/app/src/usb/aoa_hid.h +++ b/app/src/usb/aoa_hid.h @@ -57,7 +57,7 @@ sc_aoa_join(struct sc_aoa *aoa); bool sc_aoa_setup_hid(struct sc_aoa *aoa, uint16_t accessory_id, - const unsigned char *report_desc, uint16_t report_desc_size); + const uint8_t *report_desc, uint16_t report_desc_size); bool sc_aoa_unregister_hid(struct sc_aoa *aoa, uint16_t accessory_id); diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c index b2eef49c..80d33fc3 100644 --- a/app/tests/test_control_msg_serialize.c +++ b/app/tests/test_control_msg_serialize.c @@ -1,6 +1,7 @@ #include "common.h" #include +#include #include #include "control_msg.h" @@ -16,11 +17,11 @@ static void test_serialize_inject_keycode(void) { }, }; - unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; + uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; size_t size = sc_control_msg_serialize(&msg, buf); assert(size == 14); - const unsigned char expected[] = { + const uint8_t expected[] = { SC_CONTROL_MSG_TYPE_INJECT_KEYCODE, 0x01, // AKEY_EVENT_ACTION_UP 0x00, 0x00, 0x00, 0x42, // AKEYCODE_ENTER @@ -38,11 +39,11 @@ static void test_serialize_inject_text(void) { }, }; - unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; + uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; size_t size = sc_control_msg_serialize(&msg, buf); assert(size == 18); - const unsigned char expected[] = { + const uint8_t expected[] = { SC_CONTROL_MSG_TYPE_INJECT_TEXT, 0x00, 0x00, 0x00, 0x0d, // text length 'h', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', '!', // text @@ -58,11 +59,11 @@ static void test_serialize_inject_text_long(void) { text[SC_CONTROL_MSG_INJECT_TEXT_MAX_LENGTH] = '\0'; msg.inject_text.text = text; - unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; + uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; size_t size = sc_control_msg_serialize(&msg, buf); assert(size == 5 + SC_CONTROL_MSG_INJECT_TEXT_MAX_LENGTH); - unsigned char expected[5 + SC_CONTROL_MSG_INJECT_TEXT_MAX_LENGTH]; + uint8_t expected[5 + SC_CONTROL_MSG_INJECT_TEXT_MAX_LENGTH]; expected[0] = SC_CONTROL_MSG_TYPE_INJECT_TEXT; expected[1] = 0x00; expected[2] = 0x00; @@ -95,11 +96,11 @@ static void test_serialize_inject_touch_event(void) { }, }; - unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; + uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; size_t size = sc_control_msg_serialize(&msg, buf); assert(size == 32); - const unsigned char expected[] = { + const uint8_t expected[] = { SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, 0x00, // AKEY_EVENT_ACTION_DOWN 0x12, 0x34, 0x56, 0x78, 0x87, 0x65, 0x43, 0x21, // pointer id @@ -132,11 +133,11 @@ static void test_serialize_inject_scroll_event(void) { }, }; - unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; + uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; size_t size = sc_control_msg_serialize(&msg, buf); assert(size == 21); - const unsigned char expected[] = { + const uint8_t expected[] = { SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT, 0x00, 0x00, 0x01, 0x04, 0x00, 0x00, 0x04, 0x02, // 260 1026 0x04, 0x38, 0x07, 0x80, // 1080 1920 @@ -155,11 +156,11 @@ static void test_serialize_back_or_screen_on(void) { }, }; - unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; + uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; size_t size = sc_control_msg_serialize(&msg, buf); assert(size == 2); - const unsigned char expected[] = { + const uint8_t expected[] = { SC_CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON, 0x01, // AKEY_EVENT_ACTION_UP }; @@ -171,11 +172,11 @@ static void test_serialize_expand_notification_panel(void) { .type = SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL, }; - unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; + uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; size_t size = sc_control_msg_serialize(&msg, buf); assert(size == 1); - const unsigned char expected[] = { + const uint8_t expected[] = { SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL, }; assert(!memcmp(buf, expected, sizeof(expected))); @@ -186,11 +187,11 @@ static void test_serialize_expand_settings_panel(void) { .type = SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL, }; - unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; + uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; size_t size = sc_control_msg_serialize(&msg, buf); assert(size == 1); - const unsigned char expected[] = { + const uint8_t expected[] = { SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL, }; assert(!memcmp(buf, expected, sizeof(expected))); @@ -201,11 +202,11 @@ static void test_serialize_collapse_panels(void) { .type = SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS, }; - unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; + uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; size_t size = sc_control_msg_serialize(&msg, buf); assert(size == 1); - const unsigned char expected[] = { + const uint8_t expected[] = { SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS, }; assert(!memcmp(buf, expected, sizeof(expected))); @@ -219,11 +220,11 @@ static void test_serialize_get_clipboard(void) { }, }; - unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; + uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; size_t size = sc_control_msg_serialize(&msg, buf); assert(size == 2); - const unsigned char expected[] = { + const uint8_t expected[] = { SC_CONTROL_MSG_TYPE_GET_CLIPBOARD, SC_COPY_KEY_COPY, }; @@ -240,11 +241,11 @@ static void test_serialize_set_clipboard(void) { }, }; - unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; + uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; size_t size = sc_control_msg_serialize(&msg, buf); assert(size == 27); - const unsigned char expected[] = { + const uint8_t expected[] = { SC_CONTROL_MSG_TYPE_SET_CLIPBOARD, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, // sequence 1, // paste @@ -269,11 +270,11 @@ static void test_serialize_set_clipboard_long(void) { text[SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH] = '\0'; msg.set_clipboard.text = text; - unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; + uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; size_t size = sc_control_msg_serialize(&msg, buf); assert(size == SC_CONTROL_MSG_MAX_SIZE); - unsigned char expected[SC_CONTROL_MSG_MAX_SIZE] = { + uint8_t expected[SC_CONTROL_MSG_MAX_SIZE] = { SC_CONTROL_MSG_TYPE_SET_CLIPBOARD, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, // sequence 1, // paste @@ -296,11 +297,11 @@ static void test_serialize_set_screen_power_mode(void) { }, }; - unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; + uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; size_t size = sc_control_msg_serialize(&msg, buf); assert(size == 2); - const unsigned char expected[] = { + const uint8_t expected[] = { SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE, 0x02, // SC_SCREEN_POWER_MODE_NORMAL }; @@ -312,11 +313,11 @@ static void test_serialize_rotate_device(void) { .type = SC_CONTROL_MSG_TYPE_ROTATE_DEVICE, }; - unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; + uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; size_t size = sc_control_msg_serialize(&msg, buf); assert(size == 1); - const unsigned char expected[] = { + const uint8_t expected[] = { SC_CONTROL_MSG_TYPE_ROTATE_DEVICE, }; 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 835096c0..a1a3f695 100644 --- a/app/tests/test_device_msg_deserialize.c +++ b/app/tests/test_device_msg_deserialize.c @@ -1,14 +1,14 @@ #include "common.h" #include +#include +#include #include #include "device_msg.h" -#include - static void test_deserialize_clipboard(void) { - const unsigned char input[] = { + const uint8_t input[] = { DEVICE_MSG_TYPE_CLIPBOARD, 0x00, 0x00, 0x00, 0x03, // text length 0x41, 0x42, 0x43, // "ABC" @@ -26,7 +26,7 @@ static void test_deserialize_clipboard(void) { } static void test_deserialize_clipboard_big(void) { - unsigned char input[DEVICE_MSG_MAX_SIZE]; + uint8_t input[DEVICE_MSG_MAX_SIZE]; input[0] = DEVICE_MSG_TYPE_CLIPBOARD; input[1] = (DEVICE_MSG_TEXT_MAX_LENGTH & 0xff000000u) >> 24; input[2] = (DEVICE_MSG_TEXT_MAX_LENGTH & 0x00ff0000u) >> 16; @@ -48,7 +48,7 @@ static void test_deserialize_clipboard_big(void) { } static void test_deserialize_ack_set_clipboard(void) { - const unsigned char input[] = { + const uint8_t input[] = { DEVICE_MSG_TYPE_ACK_CLIPBOARD, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, // sequence }; From 9858eff85625abcbf392ae57220df1b1b03f793b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 23 Feb 2024 20:01:30 +0100 Subject: [PATCH 1761/2244] Fix device message deserialization checks If any message is incomplete, the deserialization method must return immediately. --- app/src/device_msg.c | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/app/src/device_msg.c b/app/src/device_msg.c index 9925cf97..f9f22a85 100644 --- a/app/src/device_msg.c +++ b/app/src/device_msg.c @@ -9,17 +9,20 @@ ssize_t device_msg_deserialize(const uint8_t *buf, size_t len, struct device_msg *msg) { - if (len < 5) { - // at least type + empty string length - return 0; // not available + if (!len) { + return 0; // no message } msg->type = buf[0]; switch (msg->type) { case DEVICE_MSG_TYPE_CLIPBOARD: { + if (len < 5) { + // at least type + empty string length + return 0; // no complete message + } size_t clipboard_len = sc_read32be(&buf[1]); if (clipboard_len > len - 5) { - return 0; // not available + return 0; // no complete message } char *text = malloc(clipboard_len + 1); if (!text) { @@ -35,6 +38,9 @@ device_msg_deserialize(const uint8_t *buf, size_t len, struct device_msg *msg) { return 5 + clipboard_len; } case DEVICE_MSG_TYPE_ACK_CLIPBOARD: { + if (len < 9) { + return 0; // no complete message + } uint64_t sequence = sc_read64be(&buf[1]); msg->ack_clipboard.sequence = sequence; return 9; From 78a7e4f293f59499fbb4be850a29e891171fcf4f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 23 Feb 2024 20:05:12 +0100 Subject: [PATCH 1762/2244] Use sc_ prefix for device sender --- app/src/device_msg.c | 5 +++-- app/src/device_msg.h | 11 ++++++----- app/src/receiver.c | 8 ++++---- app/tests/test_device_msg_deserialize.c | 16 ++++++++-------- 4 files changed, 21 insertions(+), 19 deletions(-) diff --git a/app/src/device_msg.c b/app/src/device_msg.c index f9f22a85..0cadc49c 100644 --- a/app/src/device_msg.c +++ b/app/src/device_msg.c @@ -8,7 +8,8 @@ #include "util/log.h" ssize_t -device_msg_deserialize(const uint8_t *buf, size_t len, struct device_msg *msg) { +sc_device_msg_deserialize(const uint8_t *buf, size_t len, + struct sc_device_msg *msg) { if (!len) { return 0; // no message } @@ -52,7 +53,7 @@ device_msg_deserialize(const uint8_t *buf, size_t len, struct device_msg *msg) { } void -device_msg_destroy(struct device_msg *msg) { +sc_device_msg_destroy(struct sc_device_msg *msg) { if (msg->type == DEVICE_MSG_TYPE_CLIPBOARD) { free(msg->clipboard.text); } diff --git a/app/src/device_msg.h b/app/src/device_msg.h index 3b68a61a..3f541cf5 100644 --- a/app/src/device_msg.h +++ b/app/src/device_msg.h @@ -11,13 +11,13 @@ // type: 1 byte; length: 4 bytes #define DEVICE_MSG_TEXT_MAX_LENGTH (DEVICE_MSG_MAX_SIZE - 5) -enum device_msg_type { +enum sc_device_msg_type { DEVICE_MSG_TYPE_CLIPBOARD, DEVICE_MSG_TYPE_ACK_CLIPBOARD, }; -struct device_msg { - enum device_msg_type type; +struct sc_device_msg { + enum sc_device_msg_type type; union { struct { char *text; // owned, to be freed by free() @@ -30,9 +30,10 @@ struct device_msg { // return the number of bytes consumed (0 for no msg available, -1 on error) ssize_t -device_msg_deserialize(const uint8_t *buf, size_t len, struct device_msg *msg); +sc_device_msg_deserialize(const uint8_t *buf, size_t len, + struct sc_device_msg *msg); void -device_msg_destroy(struct device_msg *msg); +sc_device_msg_destroy(struct sc_device_msg *msg); #endif diff --git a/app/src/receiver.c b/app/src/receiver.c index c08cd6cf..408e1db7 100644 --- a/app/src/receiver.c +++ b/app/src/receiver.c @@ -27,7 +27,7 @@ sc_receiver_destroy(struct sc_receiver *receiver) { } static void -process_msg(struct sc_receiver *receiver, struct device_msg *msg) { +process_msg(struct sc_receiver *receiver, struct sc_device_msg *msg) { switch (msg->type) { case DEVICE_MSG_TYPE_CLIPBOARD: { char *current = SDL_GetClipboardText(); @@ -55,8 +55,8 @@ static ssize_t process_msgs(struct sc_receiver *receiver, const uint8_t *buf, size_t len) { size_t head = 0; for (;;) { - struct device_msg msg; - ssize_t r = device_msg_deserialize(&buf[head], len - head, &msg); + struct sc_device_msg msg; + ssize_t r = sc_device_msg_deserialize(&buf[head], len - head, &msg); if (r == -1) { return -1; } @@ -65,7 +65,7 @@ process_msgs(struct sc_receiver *receiver, const uint8_t *buf, size_t len) { } process_msg(receiver, &msg); - device_msg_destroy(&msg); + sc_device_msg_destroy(&msg); head += r; assert(head <= len); diff --git a/app/tests/test_device_msg_deserialize.c b/app/tests/test_device_msg_deserialize.c index a1a3f695..bfbcefd6 100644 --- a/app/tests/test_device_msg_deserialize.c +++ b/app/tests/test_device_msg_deserialize.c @@ -14,15 +14,15 @@ static void test_deserialize_clipboard(void) { 0x41, 0x42, 0x43, // "ABC" }; - struct device_msg msg; - ssize_t r = device_msg_deserialize(input, sizeof(input), &msg); + struct sc_device_msg msg; + ssize_t r = sc_device_msg_deserialize(input, sizeof(input), &msg); assert(r == 8); assert(msg.type == DEVICE_MSG_TYPE_CLIPBOARD); assert(msg.clipboard.text); assert(!strcmp("ABC", msg.clipboard.text)); - device_msg_destroy(&msg); + sc_device_msg_destroy(&msg); } static void test_deserialize_clipboard_big(void) { @@ -35,8 +35,8 @@ static void test_deserialize_clipboard_big(void) { memset(input + 5, 'a', DEVICE_MSG_TEXT_MAX_LENGTH); - struct device_msg msg; - ssize_t r = device_msg_deserialize(input, sizeof(input), &msg); + struct sc_device_msg msg; + ssize_t r = sc_device_msg_deserialize(input, sizeof(input), &msg); assert(r == DEVICE_MSG_MAX_SIZE); assert(msg.type == DEVICE_MSG_TYPE_CLIPBOARD); @@ -44,7 +44,7 @@ static void test_deserialize_clipboard_big(void) { assert(strlen(msg.clipboard.text) == DEVICE_MSG_TEXT_MAX_LENGTH); assert(msg.clipboard.text[0] == 'a'); - device_msg_destroy(&msg); + sc_device_msg_destroy(&msg); } static void test_deserialize_ack_set_clipboard(void) { @@ -53,8 +53,8 @@ static void test_deserialize_ack_set_clipboard(void) { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, // sequence }; - struct device_msg msg; - ssize_t r = device_msg_deserialize(input, sizeof(input), &msg); + struct sc_device_msg msg; + ssize_t r = sc_device_msg_deserialize(input, sizeof(input), &msg); assert(r == 9); assert(msg.type == DEVICE_MSG_TYPE_ACK_CLIPBOARD); From 746eaea55683e8e97ba7763bc0fa567227004c5d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 26 Feb 2024 08:33:11 +0100 Subject: [PATCH 1763/2244] Add missing clipboard workaround for IQOO device The first part of the workaround fixed getPrimaryClip(). This part fixes setPrimaryClip(). Fixes #4703 Refs 5ce8672ebc56b7286e1078a39abc64903e5664d0 Refs #4492 --- .../scrcpy/wrappers/ClipboardManager.java | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 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 2c8d9907..ed5c8d75 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java @@ -87,9 +87,15 @@ public final class ClipboardManager { setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class); setMethodVersion = 1; } catch (NoSuchMethodException e2) { - setPrimaryClipMethod = manager.getClass() - .getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class, int.class); - setMethodVersion = 2; + try { + setPrimaryClipMethod = manager.getClass() + .getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class, int.class); + setMethodVersion = 2; + } catch (NoSuchMethodException e3) { + setPrimaryClipMethod = manager.getClass() + .getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class, int.class, boolean.class); + setMethodVersion = 3; + } } } } @@ -132,9 +138,12 @@ public final class ClipboardManager { case 1: method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID); break; - default: + case 2: method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0); break; + default: + // The last boolean parameter is "userOperate" + method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0, true); } } From d894e270a7719b92e38b4f5e0294b9d55e90a6df Mon Sep 17 00:00:00 2001 From: eiyooooo Date: Sat, 24 Feb 2024 01:10:35 +0800 Subject: [PATCH 1764/2244] Add rotation support for non-default display Use new methods introduced by this commit: PR #4698 Signed-off-by: Romain Vimont --- .../com/genymobile/scrcpy/Controller.java | 2 +- .../java/com/genymobile/scrcpy/Device.java | 19 +++-- .../scrcpy/wrappers/WindowManager.java | 76 ++++++++++++++++--- 3 files changed, 82 insertions(+), 15 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index 257f732b..73d6ad57 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -180,7 +180,7 @@ public class Controller implements AsyncProcessor { } break; case ControlMessage.TYPE_ROTATE_DEVICE: - Device.rotateDevice(); + device.rotateDevice(); break; default: // do nothing diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index 33b09a57..8d0ee231 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -359,21 +359,30 @@ public final class Device { /** * Disable auto-rotation (if enabled), set the screen rotation and re-enable auto-rotation (if it was enabled). */ - public static void rotateDevice() { + public void rotateDevice() { WindowManager wm = ServiceManager.getWindowManager(); - boolean accelerometerRotation = !wm.isRotationFrozen(); + boolean accelerometerRotation = !wm.isRotationFrozen(displayId); - int currentRotation = wm.getRotation(); + int currentRotation = getCurrentRotation(displayId); int newRotation = (currentRotation & 1) ^ 1; // 0->1, 1->0, 2->1, 3->0 String newRotationString = newRotation == 0 ? "portrait" : "landscape"; Ln.i("Device rotation requested: " + newRotationString); - wm.freezeRotation(newRotation); + wm.freezeRotation(displayId, newRotation); // restore auto-rotate if necessary if (accelerometerRotation) { - wm.thawRotation(); + wm.thawRotation(displayId); } } + + private static int getCurrentRotation(int displayId) { + if (displayId == 0) { + return ServiceManager.getWindowManager().getRotation(); + } + + DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(displayId); + return displayInfo.getRotation(); + } } 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 b19dace9..d9654b1b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java @@ -13,8 +13,11 @@ public final class WindowManager { private final IInterface manager; private Method getRotationMethod; private Method freezeRotationMethod; + private Method freezeDisplayRotationMethod; private Method isRotationFrozenMethod; + private Method isDisplayRotationFrozenMethod; private Method thawRotationMethod; + private Method thawDisplayRotationMethod; static WindowManager create() { IInterface manager = ServiceManager.getService("window", "android.view.IWindowManager"); @@ -47,6 +50,15 @@ public final class WindowManager { return freezeRotationMethod; } + // New method added by this commit: + // + private Method getFreezeDisplayRotationMethod() throws NoSuchMethodException { + if (freezeDisplayRotationMethod == null) { + freezeDisplayRotationMethod = manager.getClass().getMethod("freezeDisplayRotation", int.class, int.class); + } + return freezeDisplayRotationMethod; + } + private Method getIsRotationFrozenMethod() throws NoSuchMethodException { if (isRotationFrozenMethod == null) { isRotationFrozenMethod = manager.getClass().getMethod("isRotationFrozen"); @@ -54,6 +66,15 @@ public final class WindowManager { return isRotationFrozenMethod; } + // New method added by this commit: + // + private Method getIsDisplayRotationFrozenMethod() throws NoSuchMethodException { + if (isDisplayRotationFrozenMethod == null) { + isDisplayRotationFrozenMethod = manager.getClass().getMethod("isDisplayRotationFrozen", int.class); + } + return isDisplayRotationFrozenMethod; + } + private Method getThawRotationMethod() throws NoSuchMethodException { if (thawRotationMethod == null) { thawRotationMethod = manager.getClass().getMethod("thawRotation"); @@ -61,6 +82,15 @@ public final class WindowManager { return thawRotationMethod; } + // New method added by this commit: + // + private Method getThawDisplayRotationMethod() throws NoSuchMethodException { + if (thawDisplayRotationMethod == null) { + thawDisplayRotationMethod = manager.getClass().getMethod("thawDisplayRotation", int.class); + } + return thawDisplayRotationMethod; + } + public int getRotation() { try { Method method = getGetRotationMethod(); @@ -71,29 +101,57 @@ public final class WindowManager { } } - public void freezeRotation(int rotation) { + public void freezeRotation(int displayId, int rotation) { try { - Method method = getFreezeRotationMethod(); - method.invoke(manager, rotation); + try { + Method method = getFreezeDisplayRotationMethod(); + method.invoke(manager, displayId, rotation); + } catch (ReflectiveOperationException e) { + if (displayId == 0) { + Method method = getFreezeRotationMethod(); + method.invoke(manager, rotation); + } else { + Ln.e("Could not invoke method", e); + } + } } catch (ReflectiveOperationException e) { Ln.e("Could not invoke method", e); } } - public boolean isRotationFrozen() { + public boolean isRotationFrozen(int displayId) { try { - Method method = getIsRotationFrozenMethod(); - return (boolean) method.invoke(manager); + try { + Method method = getIsDisplayRotationFrozenMethod(); + return (boolean) method.invoke(manager, displayId); + } catch (ReflectiveOperationException e) { + if (displayId == 0) { + Method method = getIsRotationFrozenMethod(); + return (boolean) method.invoke(manager); + } else { + Ln.e("Could not invoke method", e); + return false; + } + } } catch (ReflectiveOperationException e) { Ln.e("Could not invoke method", e); return false; } } - public void thawRotation() { + public void thawRotation(int displayId) { try { - Method method = getThawRotationMethod(); - method.invoke(manager); + try { + Method method = getThawDisplayRotationMethod(); + method.invoke(manager, displayId); + } catch (ReflectiveOperationException e) { + if (displayId == 0) { + Method method = getThawRotationMethod(); + method.invoke(manager); + } else { + Ln.e("Could not invoke method", e); + } + } } catch (ReflectiveOperationException e) { Ln.e("Could not invoke method", e); } From 295102a6d91e5a71fbcc7fa3f17a2cea9c9eb9e6 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 27 Feb 2024 09:59:23 +0100 Subject: [PATCH 1765/2244] Check device messages assumptions at runtime Do not assume the server behaves correctly (scrcpy should not require the device to be trusted). --- app/src/receiver.c | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/app/src/receiver.c b/app/src/receiver.c index 408e1db7..6be705e3 100644 --- a/app/src/receiver.c +++ b/app/src/receiver.c @@ -43,9 +43,19 @@ process_msg(struct sc_receiver *receiver, struct sc_device_msg *msg) { break; } case DEVICE_MSG_TYPE_ACK_CLIPBOARD: - assert(receiver->acksync); LOGD("Ack device clipboard sequence=%" PRIu64_, msg->ack_clipboard.sequence); + + // This is a programming error to receive this message if there is + // no ACK synchronization mechanism + assert(receiver->acksync); + + // Also check at runtime (do not trust the server) + if (!receiver->acksync) { + LOGE("Received unexpected ack"); + return; + } + sc_acksync_ack(receiver->acksync, msg->ack_clipboard.sequence); break; } From f6459dd742f356fade275e2178aa9ecee05c23cc Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 29 Feb 2024 10:26:49 +0100 Subject: [PATCH 1766/2244] Fix FAQ link Refs ad05a018003a66b0a5f8afefb0d2f16a392d3077 --- FAQ.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FAQ.md b/FAQ.md index a6eaeefa..6d02361b 100644 --- a/FAQ.md +++ b/FAQ.md @@ -222,7 +222,7 @@ java.lang.IllegalStateException at android.media.MediaCodec.native_dequeueOutputBuffer(Native Method) ``` -then try with another [encoder](doc/video.md#codec). +then try with another [encoder](doc/video.md#encoder). ## Translations From ffa238b9d35bb9c882537d32724bbadbe4da7ef6 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 29 Feb 2024 23:55:44 +0100 Subject: [PATCH 1767/2244] Remove duplicate lines in libusb script --- app/prebuilt-deps/prepare-libusb.sh | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/prebuilt-deps/prepare-libusb.sh b/app/prebuilt-deps/prepare-libusb.sh index 228a5bfa..b31c45eb 100755 --- a/app/prebuilt-deps/prepare-libusb.sh +++ b/app/prebuilt-deps/prepare-libusb.sh @@ -26,8 +26,6 @@ cd "$DEP_DIR" 7z x "../$FILENAME" \ "libusb-$VERSION-binaries/libusb-MinGW-Win32/" \ - "libusb-$VERSION-binaries/libusb-MinGW-Win32/" \ - "libusb-$VERSION-binaries/libusb-MinGW-x64/" \ "libusb-$VERSION-binaries/libusb-MinGW-x64/" mv "libusb-$VERSION-binaries/libusb-MinGW-Win32" . From a97641757237fc9342f2d1b4b9573a99761ffc21 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 1 Mar 2024 00:11:48 +0100 Subject: [PATCH 1768/2244] Fix typo in error message --- app/src/display.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/display.c b/app/src/display.c index 906b5d65..ba15cd14 100644 --- a/app/src/display.c +++ b/app/src/display.c @@ -59,7 +59,7 @@ sc_display_init(struct sc_display *display, SDL_Window *window, bool mipmaps) { LOGI("Trilinear filtering disabled"); } } else if (mipmaps) { - LOGD("Trilinear filtering disabled (not an OpenGL renderer"); + LOGD("Trilinear filtering disabled (not an OpenGL renderer)"); } display->pending.flags = 0; From c0a1aee8cea2ce6a5dbe39117d0505786ea0db7b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 20 Jan 2024 18:19:39 +0100 Subject: [PATCH 1769/2244] Always pass input manager instance Some functions in input_manager.c only have access to a sub-object (for example the controller). For consistency, always pass the whole input manager instance. This will allow to add assertions when keyboard and mouse could be disabled separately. PR #4473 --- app/src/input_manager.c | 193 ++++++++++++++++++++++------------------ 1 file changed, 107 insertions(+), 86 deletions(-) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 76cfbd92..8e7a6402 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -87,8 +87,10 @@ sc_input_manager_init(struct sc_input_manager *im, } static void -send_keycode(struct sc_controller *controller, enum android_keycode keycode, +send_keycode(struct sc_input_manager *im, enum android_keycode keycode, enum sc_action action, const char *name) { + assert(im->controller); + // send DOWN event struct sc_control_msg msg; msg.type = SC_CONTROL_MSG_TYPE_INJECT_KEYCODE; @@ -99,100 +101,109 @@ send_keycode(struct sc_controller *controller, enum android_keycode keycode, msg.inject_keycode.metastate = 0; msg.inject_keycode.repeat = 0; - if (!sc_controller_push_msg(controller, &msg)) { + if (!sc_controller_push_msg(im->controller, &msg)) { LOGW("Could not request 'inject %s'", name); } } static inline void -action_home(struct sc_controller *controller, enum sc_action action) { - send_keycode(controller, AKEYCODE_HOME, action, "HOME"); +action_home(struct sc_input_manager *im, enum sc_action action) { + send_keycode(im, AKEYCODE_HOME, action, "HOME"); } static inline void -action_back(struct sc_controller *controller, enum sc_action action) { - send_keycode(controller, AKEYCODE_BACK, action, "BACK"); +action_back(struct sc_input_manager *im, enum sc_action action) { + send_keycode(im, AKEYCODE_BACK, action, "BACK"); } static inline void -action_app_switch(struct sc_controller *controller, enum sc_action action) { - send_keycode(controller, AKEYCODE_APP_SWITCH, action, "APP_SWITCH"); +action_app_switch(struct sc_input_manager *im, enum sc_action action) { + send_keycode(im, AKEYCODE_APP_SWITCH, action, "APP_SWITCH"); } static inline void -action_power(struct sc_controller *controller, enum sc_action action) { - send_keycode(controller, AKEYCODE_POWER, action, "POWER"); +action_power(struct sc_input_manager *im, enum sc_action action) { + send_keycode(im, AKEYCODE_POWER, action, "POWER"); } static inline void -action_volume_up(struct sc_controller *controller, enum sc_action action) { - send_keycode(controller, AKEYCODE_VOLUME_UP, action, "VOLUME_UP"); +action_volume_up(struct sc_input_manager *im, enum sc_action action) { + send_keycode(im, AKEYCODE_VOLUME_UP, action, "VOLUME_UP"); } static inline void -action_volume_down(struct sc_controller *controller, enum sc_action action) { - send_keycode(controller, AKEYCODE_VOLUME_DOWN, action, "VOLUME_DOWN"); +action_volume_down(struct sc_input_manager *im, enum sc_action action) { + send_keycode(im, AKEYCODE_VOLUME_DOWN, action, "VOLUME_DOWN"); } static inline void -action_menu(struct sc_controller *controller, enum sc_action action) { - send_keycode(controller, AKEYCODE_MENU, action, "MENU"); +action_menu(struct sc_input_manager *im, enum sc_action action) { + send_keycode(im, AKEYCODE_MENU, action, "MENU"); } // turn the screen on if it was off, press BACK otherwise // If the screen is off, it is turned on only on ACTION_DOWN static void -press_back_or_turn_screen_on(struct sc_controller *controller, +press_back_or_turn_screen_on(struct sc_input_manager *im, enum sc_action action) { + assert(im->controller); + struct sc_control_msg msg; msg.type = SC_CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON; msg.back_or_screen_on.action = action == SC_ACTION_DOWN ? AKEY_EVENT_ACTION_DOWN : AKEY_EVENT_ACTION_UP; - if (!sc_controller_push_msg(controller, &msg)) { + if (!sc_controller_push_msg(im->controller, &msg)) { LOGW("Could not request 'press back or turn screen on'"); } } static void -expand_notification_panel(struct sc_controller *controller) { +expand_notification_panel(struct sc_input_manager *im) { + assert(im->controller); + struct sc_control_msg msg; msg.type = SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL; - if (!sc_controller_push_msg(controller, &msg)) { + if (!sc_controller_push_msg(im->controller, &msg)) { LOGW("Could not request 'expand notification panel'"); } } static void -expand_settings_panel(struct sc_controller *controller) { +expand_settings_panel(struct sc_input_manager *im) { + assert(im->controller); + struct sc_control_msg msg; msg.type = SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL; - if (!sc_controller_push_msg(controller, &msg)) { + if (!sc_controller_push_msg(im->controller, &msg)) { LOGW("Could not request 'expand settings panel'"); } } static void -collapse_panels(struct sc_controller *controller) { +collapse_panels(struct sc_input_manager *im) { + assert(im->controller); + struct sc_control_msg msg; msg.type = SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS; - if (!sc_controller_push_msg(controller, &msg)) { + if (!sc_controller_push_msg(im->controller, &msg)) { LOGW("Could not request 'collapse notification panel'"); } } static bool -get_device_clipboard(struct sc_controller *controller, - enum sc_copy_key copy_key) { +get_device_clipboard(struct sc_input_manager *im, enum sc_copy_key copy_key) { + assert(im->controller); + struct sc_control_msg msg; msg.type = SC_CONTROL_MSG_TYPE_GET_CLIPBOARD; msg.get_clipboard.copy_key = copy_key; - if (!sc_controller_push_msg(controller, &msg)) { + if (!sc_controller_push_msg(im->controller, &msg)) { LOGW("Could not request 'get device clipboard'"); return false; } @@ -201,8 +212,10 @@ get_device_clipboard(struct sc_controller *controller, } static bool -set_device_clipboard(struct sc_controller *controller, bool paste, +set_device_clipboard(struct sc_input_manager *im, bool paste, uint64_t sequence) { + assert(im->controller); + char *text = SDL_GetClipboardText(); if (!text) { LOGW("Could not get clipboard text: %s", SDL_GetError()); @@ -222,7 +235,7 @@ set_device_clipboard(struct sc_controller *controller, bool paste, msg.set_clipboard.text = text_dup; msg.set_clipboard.paste = paste; - if (!sc_controller_push_msg(controller, &msg)) { + if (!sc_controller_push_msg(im->controller, &msg)) { free(text_dup); LOGW("Could not request 'set device clipboard'"); return false; @@ -232,19 +245,23 @@ set_device_clipboard(struct sc_controller *controller, bool paste, } static void -set_screen_power_mode(struct sc_controller *controller, +set_screen_power_mode(struct sc_input_manager *im, enum sc_screen_power_mode mode) { + assert(im->controller); + struct sc_control_msg msg; msg.type = SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE; msg.set_screen_power_mode.mode = mode; - if (!sc_controller_push_msg(controller, &msg)) { + if (!sc_controller_push_msg(im->controller, &msg)) { LOGW("Could not request 'set screen power mode'"); } } static void -switch_fps_counter_state(struct sc_fps_counter *fps_counter) { +switch_fps_counter_state(struct sc_input_manager *im) { + struct sc_fps_counter *fps_counter = &im->screen->fps_counter; + // the started state can only be written from the current thread, so there // is no ToCToU issue if (sc_fps_counter_is_started(fps_counter)) { @@ -256,7 +273,9 @@ switch_fps_counter_state(struct sc_fps_counter *fps_counter) { } static void -clipboard_paste(struct sc_controller *controller) { +clipboard_paste(struct sc_input_manager *im) { + assert(im->controller); + char *text = SDL_GetClipboardText(); if (!text) { LOGW("Could not get clipboard text: %s", SDL_GetError()); @@ -278,25 +297,28 @@ clipboard_paste(struct sc_controller *controller) { struct sc_control_msg msg; msg.type = SC_CONTROL_MSG_TYPE_INJECT_TEXT; msg.inject_text.text = text_dup; - if (!sc_controller_push_msg(controller, &msg)) { + if (!sc_controller_push_msg(im->controller, &msg)) { free(text_dup); LOGW("Could not request 'paste clipboard'"); } } static void -rotate_device(struct sc_controller *controller) { +rotate_device(struct sc_input_manager *im) { + assert(im->controller); + struct sc_control_msg msg; msg.type = SC_CONTROL_MSG_TYPE_ROTATE_DEVICE; - if (!sc_controller_push_msg(controller, &msg)) { + if (!sc_controller_push_msg(im->controller, &msg)) { LOGW("Could not request device rotation"); } } static void -apply_orientation_transform(struct sc_screen *screen, +apply_orientation_transform(struct sc_input_manager *im, enum sc_orientation transform) { + struct sc_screen *screen = im->screen; enum sc_orientation new_orientation = sc_orientation_apply(screen->orientation, transform); sc_screen_set_orientation(screen, new_orientation); @@ -364,7 +386,7 @@ static void sc_input_manager_process_key(struct sc_input_manager *im, const SDL_KeyboardEvent *event) { // controller is NULL if --no-control is requested - struct sc_controller *controller = im->controller; + bool control = im->controller; SDL_Keycode keycode = event->keysym.sym; uint16_t mod = event->keysym.mod; @@ -390,68 +412,68 @@ sc_input_manager_process_key(struct sc_input_manager *im, enum sc_action action = down ? SC_ACTION_DOWN : SC_ACTION_UP; switch (keycode) { case SDLK_h: - if (controller && !shift && !repeat) { - action_home(controller, action); + if (control && !shift && !repeat) { + action_home(im, action); } return; case SDLK_b: // fall-through case SDLK_BACKSPACE: - if (controller && !shift && !repeat) { - action_back(controller, action); + if (control && !shift && !repeat) { + action_back(im, action); } return; case SDLK_s: - if (controller && !shift && !repeat) { - action_app_switch(controller, action); + if (control && !shift && !repeat) { + action_app_switch(im, action); } return; case SDLK_m: - if (controller && !shift && !repeat) { - action_menu(controller, action); + if (control && !shift && !repeat) { + action_menu(im, action); } return; case SDLK_p: - if (controller && !shift && !repeat) { - action_power(controller, action); + if (control && !shift && !repeat) { + action_power(im, action); } return; case SDLK_o: - if (controller && !repeat && down) { + if (control && !repeat && down) { enum sc_screen_power_mode mode = shift ? SC_SCREEN_POWER_MODE_NORMAL : SC_SCREEN_POWER_MODE_OFF; - set_screen_power_mode(controller, mode); + set_screen_power_mode(im, mode); } return; case SDLK_DOWN: if (shift) { if (!repeat & down) { - apply_orientation_transform(im->screen, + apply_orientation_transform(im, SC_ORIENTATION_FLIP_180); } - } else if (controller) { + } else if (control) { // forward repeated events - action_volume_down(controller, action); + action_volume_down(im, action); } return; case SDLK_UP: if (shift) { if (!repeat & down) { - apply_orientation_transform(im->screen, + apply_orientation_transform(im, SC_ORIENTATION_FLIP_180); } - } else if (controller) { + } else if (control) { // forward repeated events - action_volume_up(controller, action); + action_volume_up(im, action); } return; case SDLK_LEFT: if (!repeat && down) { if (shift) { - apply_orientation_transform(im->screen, + apply_orientation_transform(im, SC_ORIENTATION_FLIP_0); } else { - apply_orientation_transform(im->screen, + apply_orientation_transform(im, SC_ORIENTATION_270); } } @@ -459,34 +481,33 @@ sc_input_manager_process_key(struct sc_input_manager *im, case SDLK_RIGHT: if (!repeat && down) { if (shift) { - apply_orientation_transform(im->screen, + apply_orientation_transform(im, SC_ORIENTATION_FLIP_0); } else { - apply_orientation_transform(im->screen, + apply_orientation_transform(im, SC_ORIENTATION_90); } } return; case SDLK_c: - if (controller && !shift && !repeat && down) { - get_device_clipboard(controller, SC_COPY_KEY_COPY); + if (control && !shift && !repeat && down) { + get_device_clipboard(im, SC_COPY_KEY_COPY); } return; case SDLK_x: - if (controller && !shift && !repeat && down) { - get_device_clipboard(controller, SC_COPY_KEY_CUT); + if (control && !shift && !repeat && down) { + get_device_clipboard(im, SC_COPY_KEY_CUT); } return; case SDLK_v: - if (controller && !repeat && down) { + if (control && !repeat && down) { if (shift || im->legacy_paste) { // inject the text as input events - clipboard_paste(controller); + clipboard_paste(im); } else { // store the text in the device clipboard and paste, // without requesting an acknowledgment - set_device_clipboard(controller, true, - SC_SEQUENCE_INVALID); + set_device_clipboard(im, true, SC_SEQUENCE_INVALID); } } return; @@ -507,23 +528,23 @@ sc_input_manager_process_key(struct sc_input_manager *im, return; case SDLK_i: if (!shift && !repeat && down) { - switch_fps_counter_state(&im->screen->fps_counter); + switch_fps_counter_state(im); } return; case SDLK_n: - if (controller && !repeat && down) { + if (control && !repeat && down) { if (shift) { - collapse_panels(controller); + collapse_panels(im); } else if (im->key_repeat == 0) { - expand_notification_panel(controller); + expand_notification_panel(im); } else { - expand_settings_panel(controller); + expand_settings_panel(im); } } return; case SDLK_r: - if (controller && !shift && !repeat && down) { - rotate_device(controller); + if (control && !shift && !repeat && down) { + rotate_device(im); } return; } @@ -531,7 +552,7 @@ sc_input_manager_process_key(struct sc_input_manager *im, return; } - if (!controller) { + if (!control) { return; } @@ -540,7 +561,7 @@ sc_input_manager_process_key(struct sc_input_manager *im, if (im->clipboard_autosync && is_ctrl_v) { if (im->legacy_paste) { // inject the text as input events - clipboard_paste(controller); + clipboard_paste(im); return; } @@ -550,7 +571,7 @@ sc_input_manager_process_key(struct sc_input_manager *im, // Synchronize the computer clipboard to the device clipboard before // sending Ctrl+v, to allow seamless copy-paste. - bool ok = set_device_clipboard(controller, false, sequence); + bool ok = set_device_clipboard(im, false, sequence); if (!ok) { LOGW("Clipboard could not be synchronized, Ctrl+v not injected"); return; @@ -652,7 +673,7 @@ sc_input_manager_process_touch(struct sc_input_manager *im, static void sc_input_manager_process_mouse_button(struct sc_input_manager *im, const SDL_MouseButtonEvent *event) { - struct sc_controller *controller = im->controller; + bool control = im->controller; if (event->which == SDL_TOUCH_MOUSEID) { // simulated from touch events, so it's a duplicate @@ -661,27 +682,27 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im, bool down = event->type == SDL_MOUSEBUTTONDOWN; if (!im->forward_all_clicks) { - if (controller) { + if (control) { enum sc_action action = down ? SC_ACTION_DOWN : SC_ACTION_UP; if (event->button == SDL_BUTTON_X1) { - action_app_switch(controller, action); + action_app_switch(im, action); return; } if (event->button == SDL_BUTTON_X2 && down) { if (event->clicks < 2) { - expand_notification_panel(controller); + expand_notification_panel(im); } else { - expand_settings_panel(controller); + expand_settings_panel(im); } return; } if (event->button == SDL_BUTTON_RIGHT) { - press_back_or_turn_screen_on(controller, action); + press_back_or_turn_screen_on(im, action); return; } if (event->button == SDL_BUTTON_MIDDLE) { - action_home(controller, action); + action_home(im, action); return; } } @@ -704,7 +725,7 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im, // otherwise, send the click event to the device } - if (!controller) { + if (!control) { return; } From 35add3daee0907f4ca4e706c60d1c00a27702a79 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 20 Jan 2024 18:34:33 +0100 Subject: [PATCH 1770/2244] Accept disabled keyboard or mouse The input manager assumed that if a controller was present, then both a key processor and a mouse processor were present. Remove this assumption, to support disabling keyboard and mouse separately. This prepares the introduction of new command line options --keyboard and --mouse. PR #4473 --- app/src/input_manager.c | 55 ++++++++++++++++++++++------------------- 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 8e7a6402..7186186f 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -52,8 +52,11 @@ is_shortcut_mod(struct sc_input_manager *im, uint16_t sdl_mod) { void sc_input_manager_init(struct sc_input_manager *im, const struct sc_input_manager_params *params) { - assert(!params->controller || (params->kp && params->kp->ops)); - assert(!params->controller || (params->mp && params->mp->ops)); + // A key/mouse processor may not be present if there is no controller + assert((!params->kp && !params->mp) || params->controller); + // A processor must have ops initialized + assert(!params->kp || params->kp->ops); + assert(!params->mp || params->mp->ops); im->controller = params->controller; im->fp = params->fp; @@ -89,7 +92,7 @@ sc_input_manager_init(struct sc_input_manager *im, static void send_keycode(struct sc_input_manager *im, enum android_keycode keycode, enum sc_action action, const char *name) { - assert(im->controller); + assert(im->controller && im->kp); // send DOWN event struct sc_control_msg msg; @@ -146,7 +149,7 @@ action_menu(struct sc_input_manager *im, enum sc_action action) { static void press_back_or_turn_screen_on(struct sc_input_manager *im, enum sc_action action) { - assert(im->controller); + assert(im->controller && im->kp); struct sc_control_msg msg; msg.type = SC_CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON; @@ -197,7 +200,7 @@ collapse_panels(struct sc_input_manager *im) { static bool get_device_clipboard(struct sc_input_manager *im, enum sc_copy_key copy_key) { - assert(im->controller); + assert(im->controller && im->kp); struct sc_control_msg msg; msg.type = SC_CONTROL_MSG_TYPE_GET_CLIPBOARD; @@ -214,7 +217,7 @@ get_device_clipboard(struct sc_input_manager *im, enum sc_copy_key copy_key) { static bool set_device_clipboard(struct sc_input_manager *im, bool paste, uint64_t sequence) { - assert(im->controller); + assert(im->controller && im->kp); char *text = SDL_GetClipboardText(); if (!text) { @@ -274,7 +277,7 @@ switch_fps_counter_state(struct sc_input_manager *im) { static void clipboard_paste(struct sc_input_manager *im) { - assert(im->controller); + assert(im->controller && im->kp); char *text = SDL_GetClipboardText(); if (!text) { @@ -412,28 +415,28 @@ sc_input_manager_process_key(struct sc_input_manager *im, enum sc_action action = down ? SC_ACTION_DOWN : SC_ACTION_UP; switch (keycode) { case SDLK_h: - if (control && !shift && !repeat) { + if (im->kp && !shift && !repeat) { action_home(im, action); } return; case SDLK_b: // fall-through case SDLK_BACKSPACE: - if (control && !shift && !repeat) { + if (im->kp && !shift && !repeat) { action_back(im, action); } return; case SDLK_s: - if (control && !shift && !repeat) { + if (im->kp && !shift && !repeat) { action_app_switch(im, action); } return; case SDLK_m: - if (control && !shift && !repeat) { + if (im->kp && !shift && !repeat) { action_menu(im, action); } return; case SDLK_p: - if (control && !shift && !repeat) { + if (im->kp && !shift && !repeat) { action_power(im, action); } return; @@ -451,7 +454,7 @@ sc_input_manager_process_key(struct sc_input_manager *im, apply_orientation_transform(im, SC_ORIENTATION_FLIP_180); } - } else if (control) { + } else if (im->kp) { // forward repeated events action_volume_down(im, action); } @@ -462,7 +465,7 @@ sc_input_manager_process_key(struct sc_input_manager *im, apply_orientation_transform(im, SC_ORIENTATION_FLIP_180); } - } else if (control) { + } else if (im->kp) { // forward repeated events action_volume_up(im, action); } @@ -490,17 +493,17 @@ sc_input_manager_process_key(struct sc_input_manager *im, } return; case SDLK_c: - if (control && !shift && !repeat && down) { + if (im->kp && !shift && !repeat && down) { get_device_clipboard(im, SC_COPY_KEY_COPY); } return; case SDLK_x: - if (control && !shift && !repeat && down) { + if (im->kp && !shift && !repeat && down) { get_device_clipboard(im, SC_COPY_KEY_CUT); } return; case SDLK_v: - if (control && !repeat && down) { + if (im->kp && !repeat && down) { if (shift || im->legacy_paste) { // inject the text as input events clipboard_paste(im); @@ -552,7 +555,7 @@ sc_input_manager_process_key(struct sc_input_manager *im, return; } - if (!control) { + if (!im->kp) { return; } @@ -685,7 +688,7 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im, if (control) { enum sc_action action = down ? SC_ACTION_DOWN : SC_ACTION_UP; - if (event->button == SDL_BUTTON_X1) { + if (im->kp && event->button == SDL_BUTTON_X1) { action_app_switch(im, action); return; } @@ -697,11 +700,11 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im, } return; } - if (event->button == SDL_BUTTON_RIGHT) { + if (im->kp && event->button == SDL_BUTTON_RIGHT) { press_back_or_turn_screen_on(im, action); return; } - if (event->button == SDL_BUTTON_MIDDLE) { + if (im->kp && event->button == SDL_BUTTON_MIDDLE) { action_home(im, action); return; } @@ -725,7 +728,7 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im, // otherwise, send the click event to the device } - if (!control) { + if (!im->mp) { return; } @@ -865,7 +868,7 @@ sc_input_manager_handle_event(struct sc_input_manager *im, bool control = im->controller; switch (event->type) { case SDL_TEXTINPUT: - if (!control) { + if (!im->kp) { break; } sc_input_manager_process_text_input(im, &event->text); @@ -877,13 +880,13 @@ sc_input_manager_handle_event(struct sc_input_manager *im, sc_input_manager_process_key(im, &event->key); break; case SDL_MOUSEMOTION: - if (!control) { + if (!im->mp) { break; } sc_input_manager_process_mouse_motion(im, &event->motion); break; case SDL_MOUSEWHEEL: - if (!control) { + if (!im->mp) { break; } sc_input_manager_process_mouse_wheel(im, &event->wheel); @@ -897,7 +900,7 @@ sc_input_manager_handle_event(struct sc_input_manager *im, case SDL_FINGERMOTION: case SDL_FINGERDOWN: case SDL_FINGERUP: - if (!control) { + if (!im->mp) { break; } sc_input_manager_process_touch(im, &event->tfinger); From ea98d49baed749eea0f3c9a02c9f019e37c85af6 Mon Sep 17 00:00:00 2001 From: Simon Chan <1330321+yume-chan@users.noreply.github.com> Date: Tue, 28 Nov 2023 17:17:35 +0800 Subject: [PATCH 1771/2244] Introduce --keyboard and --mouse Until now, there was two modes for keyboard and mouse: - event injection using the Android system API (default) - HID/AOA over USB For this reason, the options were exposed as simple flags: - -K or --hid-keyboard to enable physical keyboard simulation (AOA) - -M or --hid-mouse to enable physical mouse simulation (AOA) Replace them by explicit --keyboard and --mouse options, with 3 possible values: - disabled - sdk (default) - aoa This will allow to add a new mode (uhid). PR #4473 Co-authored-by: Romain Vimont Signed-off-by: Romain Vimont --- app/data/bash-completion/scrcpy | 12 ++- app/data/zsh-completion/_scrcpy | 4 +- app/scrcpy.1 | 47 +++++---- app/src/cli.c | 175 ++++++++++++++++++++++++++------ app/src/options.c | 4 +- app/src/options.h | 12 ++- app/src/scrcpy.c | 36 ++++--- app/src/usb/scrcpy_otg.c | 17 ++-- 8 files changed, 220 insertions(+), 87 deletions(-) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index 78aa539d..b2009c56 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -27,8 +27,8 @@ _scrcpy() { --force-adb-forward --forward-all-clicks -h --help + --keyboard= --kill-adb-on-close - -K --hid-keyboard --legacy-paste --list-camera-sizes --list-cameras @@ -37,8 +37,8 @@ _scrcpy() { --lock-video-orientation --lock-video-orientation= -m --max-size= - -M --hid-mouse --max-fps= + --mouse= -n --no-control -N --no-playback --no-audio @@ -115,6 +115,14 @@ _scrcpy() { COMPREPLY=($(compgen -W 'front back external' -- "$cur")) return ;; + --keyboard) + COMPREPLY=($(compgen -W 'disabled sdk aoa' -- "$cur")) + return + ;; + --mouse) + COMPREPLY=($(compgen -W 'disabled sdk aoa' -- "$cur")) + return + ;; --orientation|--display-orientation) COMPREPLY=($(compgen -W '0 90 180 270 flip0 flip90 flip180 flip270' -- "$cur")) return diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 3c7ca217..a4611632 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -34,8 +34,8 @@ arguments=( '--force-adb-forward[Do not attempt to use \"adb reverse\" to connect to the device]' '--forward-all-clicks[Forward clicks to device]' {-h,--help}'[Print the help]' + '--keyboard[Set the keyboard input mode]:mode:(disabled sdk aoa)' '--kill-adb-on-close[Kill adb when scrcpy terminates]' - {-K,--hid-keyboard}'[Simulate a physical keyboard by using HID over AOAv2]' '--legacy-paste[Inject computer clipboard text as a sequence of key events on Ctrl+v]' '--list-camera-sizes[List the valid camera capture sizes]' '--list-cameras[List cameras available on the device]' @@ -43,8 +43,8 @@ arguments=( '--list-encoders[List video and audio encoders available on the device]' '--lock-video-orientation=[Lock video orientation]:orientation:(unlocked initial 0 90 180 270)' {-m,--max-size=}'[Limit both the width and height of the video to value]' - {-M,--hid-mouse}'[Simulate a physical mouse by using HID over AOAv2]' '--max-fps=[Limit the frame rate of screen capture]' + '--mouse[Set the mouse input mode]:mode:(disabled sdk aoa)' {-n,--no-control}'[Disable device control \(mirror the device in read only\)]' {-N,--no-playback}'[Disable video and audio playback]' '--no-audio[Disable audio forwarding]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index beaa99ab..ed2e620e 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -172,24 +172,26 @@ By default, right-click triggers BACK (or POWER on) and middle-click triggers HO Print this help. .TP -.B \-\-kill\-adb\-on\-close -Kill adb when scrcpy terminates. +.BI "\-\-keyboard " mode +Select how to send keyboard inputs to the device. -.TP -.B \-K, \-\-hid\-keyboard -Simulate a physical keyboard by using HID over AOAv2. +Possible values are "disabled", "sdk" and "aoa": -This provides a better experience for IME users, and allows to generate non-ASCII characters, contrary to the default injection method. + - "disabled" does not send keyboard inputs to the device. + - "sdk" uses the Android system API to deliver keyboard events to applications. + - "aoa" simulates a physical keyboard using the AOAv2 protocol. It may only work over USB. -It may only work over USB. - -The keyboard layout must be configured (once and for all) on the device, via Settings -> System -> Languages and input -> Physical keyboard. This settings page can be started directly: +For "aoa", the keyboard layout must be configured (once and for all) on the device, via Settings -> System -> Languages and input -> Physical keyboard. This settings page can be started directly: adb shell am start -a android.settings.HARD_KEYBOARD_SETTINGS -However, the option is only available when the HID keyboard is enabled (or a physical keyboard is connected). +This option is only available when the HID keyboard is enabled (or a physical keyboard is connected). -Also see \fB\-\-hid\-mouse\fR. +Also see \fB\-\-mouse\fR. + +.TP +.B \-\-kill\-adb\-on\-close +Kill adb when scrcpy terminates. .TP .B \-\-legacy\-paste @@ -230,20 +232,25 @@ Limit both the width and height of the video to \fIvalue\fR. The other dimension Default is 0 (unlimited). .TP -.B \-M, \-\-hid\-mouse -Simulate a physical mouse by using HID over AOAv2. +.BI "\-\-max\-fps " value +Limit the framerate of screen capture (officially supported since Android 10, but may work on earlier versions). -In this mode, the computer mouse is captured to control the device directly (relative mouse mode). +.TP +.BI "\-\-mouse " mode +Select how to send mouse inputs to the device. + +Possible values are "disabled", "sdk" and "aoa": + + - "disabled" does not send mouse inputs to the device. + - "sdk" uses the Android system API to deliver mouse events to applications. + - "aoa" simulates a physical mouse using the AOAv2 protocol. It may only work over USB. + +In "aoa" mode, the computer mouse is captured to control the device directly (relative mouse mode). LAlt, LSuper or RSuper toggle the capture mode, to give control of the mouse back to the computer. -It may only work over USB. +Also see \fB\-\-keyboard\fR. -Also see \fB\-\-hid\-keyboard\fR. - -.TP -.BI "\-\-max\-fps " value -Limit the framerate of screen capture (officially supported since Android 10, but may work on earlier versions). .TP .B \-n, \-\-no\-control diff --git a/app/src/cli.c b/app/src/cli.c index b2b02ecd..364590a4 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -93,6 +93,8 @@ enum { OPT_DISPLAY_ORIENTATION, OPT_RECORD_ORIENTATION, OPT_ORIENTATION, + OPT_KEYBOARD, + OPT_MOUSE, }; struct sc_option { @@ -358,27 +360,35 @@ static const struct sc_option options[] = { .longopt = "help", .text = "Print this help.", }, + { + .longopt_id = OPT_KEYBOARD, + .longopt = "keyboard", + .argdesc = "mode", + .text = "Select how to send keyboard inputs to the device.\n" + "Possible values are \"disabled\", \"sdk\" and \"aoa\".\n" + "\"disabled\" does not send keyboard inputs to the device.\n" + "\"sdk\" uses the Android system API to deliver keyboard " + "events to applications.\n" + "\"aoa\" simulates a physical keyboard using the AOAv2 " + "protocol. It may only work over USB.\n" + "For \"aoa\", the keyboard layout must be configured (once and " + "for all) on the device, via Settings -> System -> Languages " + "and input -> Physical keyboard. This settings page can be " + "started directly: `adb shell am start -a " + "android.settings.HARD_KEYBOARD_SETTINGS`.\n" + "This option is only available when the HID keyboard is " + "enabled (or a physical keyboard is connected).\n" + "Also see --mouse.", + }, { .longopt_id = OPT_KILL_ADB_ON_CLOSE, .longopt = "kill-adb-on-close", .text = "Kill adb when scrcpy terminates.", }, { + // deprecated .shortopt = 'K', .longopt = "hid-keyboard", - .text = "Simulate a physical keyboard by using HID over AOAv2.\n" - "It provides a better experience for IME users, and allows to " - "generate non-ASCII characters, contrary to the default " - "injection method.\n" - "It may only work over USB.\n" - "The keyboard layout must be configured (once and for all) on " - "the device, via Settings -> System -> Languages and input -> " - "Physical keyboard. This settings page can be started " - "directly: `adb shell am start -a " - "android.settings.HARD_KEYBOARD_SETTINGS`.\n" - "However, the option is only available when the HID keyboard " - "is enabled (or a physical keyboard is connected).\n" - "Also see --hid-mouse.", }, { .longopt_id = OPT_LEGACY_PASTE, @@ -432,15 +442,9 @@ static const struct sc_option options[] = { "Default is 0 (unlimited).", }, { + // deprecated .shortopt = 'M', .longopt = "hid-mouse", - .text = "Simulate a physical mouse by using HID over AOAv2.\n" - "In this mode, the computer mouse is captured to control the " - "device directly (relative mouse mode).\n" - "LAlt, LSuper or RSuper toggle the capture mode, to give " - "control of the mouse back to the computer.\n" - "It may only work over USB.\n" - "Also see --hid-keyboard.", }, { .longopt_id = OPT_MAX_FPS, @@ -449,6 +453,23 @@ static const struct sc_option options[] = { .text = "Limit the frame rate of screen capture (officially supported " "since Android 10, but may work on earlier versions).", }, + { + .longopt_id = OPT_MOUSE, + .longopt = "mouse", + .argdesc = "mode", + .text = "Select how to send mouse inputs to the device.\n" + "Possible values are \"disabled\", \"sdk\" and \"aoa\".\n" + "\"disabled\" does not send mouse inputs to the device.\n" + "\"sdk\" uses the Android system API to deliver mouse events" + "to applications.\n" + "\"aoa\" simulates a physical mouse using the AOAv2 protocol. " + "It may only work over USB.\n" + "In \"aoa\" mode, the computer mouse is captured to control " + "the device directly (relative mouse mode).\n" + "LAlt, LSuper or RSuper toggle the capture mode, to give " + "control of the mouse back to the computer.\n" + "Also see --keyboard.", + }, { .shortopt = 'n', .longopt = "no-control", @@ -543,10 +564,10 @@ static const struct sc_option options[] = { "mirroring is disabled.\n" "LAlt, LSuper or RSuper toggle the mouse capture mode, to give " "control of the mouse back to the computer.\n" - "If any of --hid-keyboard or --hid-mouse is set, only enable " - "keyboard or mouse respectively, otherwise enable both.\n" + "Keyboard and mouse may be disabled separately using" + "--keyboard=disabled and --mouse=disabled.\n" "It may only work over USB.\n" - "See --hid-keyboard and --hid-mouse.", + "See --keyboard and --mouse.", }, { .shortopt = 'p', @@ -1906,6 +1927,58 @@ parse_camera_fps(const char *s, uint16_t *camera_fps) { return true; } +static bool +parse_keyboard(const char *optarg, enum sc_keyboard_input_mode *mode) { + if (!strcmp(optarg, "disabled")) { + *mode = SC_KEYBOARD_INPUT_MODE_DISABLED; + return true; + } + + if (!strcmp(optarg, "sdk")) { + *mode = SC_KEYBOARD_INPUT_MODE_SDK; + return true; + } + + if (!strcmp(optarg, "aoa")) { +#ifdef HAVE_USB + *mode = SC_KEYBOARD_INPUT_MODE_AOA; + return true; +#else + LOGE("--keyboard=aoa is disabled."); + return false; +#endif + } + + LOGE("Unsupported keyboard: %s (expected disabled, sdk or aoa)", optarg); + return false; +} + +static bool +parse_mouse(const char *optarg, enum sc_mouse_input_mode *mode) { + if (!strcmp(optarg, "disabled")) { + *mode = SC_MOUSE_INPUT_MODE_DISABLED; + return true; + } + + if (!strcmp(optarg, "sdk")) { + *mode = SC_MOUSE_INPUT_MODE_SDK; + return true; + } + + if (!strcmp(optarg, "aoa")) { +#ifdef HAVE_USB + *mode = SC_MOUSE_INPUT_MODE_AOA; + return true; +#else + LOGE("--mouse=aoa is disabled."); + return false; +#endif + } + + LOGE("Unsupported mouse: %s (expected disabled, sdk or aoa)", optarg); + return false; +} + static bool parse_time_limit(const char *s, sc_tick *tick) { long value; @@ -1995,12 +2068,19 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], break; case 'K': #ifdef HAVE_USB - opts->keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_HID; + LOGW("-K/--hid-keyboard is deprecated, use --keyboard=aoa " + "instead."); + opts->keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_AOA; break; #else LOGE("HID over AOA (-K/--hid-keyboard) is disabled."); return false; #endif + case OPT_KEYBOARD: + if (!parse_keyboard(optarg, &opts->keyboard_input_mode)) { + return false; + } + break; case OPT_MAX_FPS: if (!parse_max_fps(optarg, &opts->max_fps)) { return false; @@ -2013,12 +2093,18 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], break; case 'M': #ifdef HAVE_USB - opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_HID; + LOGW("-M/--hid-mouse is deprecated, use --mouse=aoa instead."); + opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_AOA; break; #else LOGE("HID over AOA (-M/--hid-mouse) is disabled."); return false; #endif + case OPT_MOUSE: + if (!parse_mouse(optarg, &opts->mouse_input_mode)) { + return false; + } + break; case OPT_LOCK_VIDEO_ORIENTATION: if (!parse_lock_video_orientation(optarg, &opts->lock_video_orientation)) { @@ -2465,6 +2551,37 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } #endif + if (opts->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_AUTO) { + opts->keyboard_input_mode = otg ? SC_KEYBOARD_INPUT_MODE_AOA + : SC_KEYBOARD_INPUT_MODE_SDK; + } + if (opts->mouse_input_mode == SC_MOUSE_INPUT_MODE_AUTO) { + opts->mouse_input_mode = otg ? SC_MOUSE_INPUT_MODE_AOA + : SC_MOUSE_INPUT_MODE_SDK; + } + + if (otg) { + enum sc_keyboard_input_mode kmode = opts->keyboard_input_mode; + if (kmode != SC_KEYBOARD_INPUT_MODE_AOA + && kmode != SC_KEYBOARD_INPUT_MODE_DISABLED) { + LOGE("In OTG mode, --keyboard only supports aoa or disabled."); + return false; + } + + enum sc_mouse_input_mode mmode = opts->mouse_input_mode; + if (mmode != SC_MOUSE_INPUT_MODE_AOA + && mmode != SC_MOUSE_INPUT_MODE_DISABLED) { + LOGE("In OTG mode, --mouse only supports aoa or disabled."); + return false; + } + + if (kmode == SC_KEYBOARD_INPUT_MODE_DISABLED + && mmode == SC_MOUSE_INPUT_MODE_DISABLED) { + LOGE("Could not disable both keyboard and mouse in OTG mode."); + return false; + } + } + if ((opts->tunnel_host || opts->tunnel_port) && !opts->force_adb_forward) { LOGI("Tunnel host/port is set, " "--force-adb-forward automatically enabled."); @@ -2625,12 +2742,12 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } # ifdef _WIN32 - if (!otg && (opts->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_HID - || opts->mouse_input_mode == SC_MOUSE_INPUT_MODE_HID)) { + if (!otg && (opts->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_AOA + || opts->mouse_input_mode == SC_MOUSE_INPUT_MODE_AOA)) { LOGE("On Windows, it is not possible to open a USB device already open " "by another process (like adb)."); - LOGE("Therefore, -K/--hid-keyboard and -M/--hid-mouse may only work in " - "OTG mode (--otg)."); + LOGE("Therefore, --keyboard=aoa and --mouse=aoa may only work in OTG" + "mode (--otg)."); return false; } # endif diff --git a/app/src/options.c b/app/src/options.c index a13df585..7a885aa5 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -21,8 +21,8 @@ const struct scrcpy_options scrcpy_options_default = { .video_source = SC_VIDEO_SOURCE_DISPLAY, .audio_source = SC_AUDIO_SOURCE_AUTO, .record_format = SC_RECORD_FORMAT_AUTO, - .keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_INJECT, - .mouse_input_mode = SC_MOUSE_INPUT_MODE_INJECT, + .keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_AUTO, + .mouse_input_mode = SC_MOUSE_INPUT_MODE_AUTO, .camera_facing = SC_CAMERA_FACING_ANY, .port_range = { .first = DEFAULT_LOCAL_PORT_RANGE_FIRST, diff --git a/app/src/options.h b/app/src/options.h index 11e64fa1..1fb31c1a 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -140,13 +140,17 @@ enum sc_lock_video_orientation { }; enum sc_keyboard_input_mode { - SC_KEYBOARD_INPUT_MODE_INJECT, - SC_KEYBOARD_INPUT_MODE_HID, + SC_KEYBOARD_INPUT_MODE_AUTO, + SC_KEYBOARD_INPUT_MODE_DISABLED, + SC_KEYBOARD_INPUT_MODE_SDK, + SC_KEYBOARD_INPUT_MODE_AOA, }; enum sc_mouse_input_mode { - SC_MOUSE_INPUT_MODE_INJECT, - SC_MOUSE_INPUT_MODE_HID, + SC_MOUSE_INPUT_MODE_AUTO, + SC_MOUSE_INPUT_MODE_DISABLED, + SC_MOUSE_INPUT_MODE_SDK, + SC_MOUSE_INPUT_MODE_AOA, }; enum sc_key_inject_mode { diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index cf2e7e47..24177f15 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -543,11 +543,11 @@ scrcpy(struct scrcpy_options *options) { if (options->control) { #ifdef HAVE_USB - bool use_hid_keyboard = - options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_HID; - bool use_hid_mouse = - options->mouse_input_mode == SC_MOUSE_INPUT_MODE_HID; - if (use_hid_keyboard || use_hid_mouse) { + bool use_aoa_keyboard = + options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_AOA; + bool use_aoa_mouse = + options->mouse_input_mode == SC_MOUSE_INPUT_MODE_AOA; + if (use_aoa_keyboard || use_aoa_mouse) { bool ok = sc_acksync_init(&s->acksync); if (!ok) { goto end; @@ -590,7 +590,7 @@ scrcpy(struct scrcpy_options *options) { goto aoa_hid_end; } - if (use_hid_keyboard) { + if (use_aoa_keyboard) { if (sc_hid_keyboard_init(&s->keyboard_hid, &s->aoa)) { hid_keyboard_initialized = true; kp = &s->keyboard_hid.key_processor; @@ -599,7 +599,7 @@ scrcpy(struct scrcpy_options *options) { } } - if (use_hid_mouse) { + if (use_aoa_mouse) { if (sc_hid_mouse_init(&s->mouse_hid, &s->aoa)) { hid_mouse_initialized = true; mp = &s->mouse_hid.mouse_processor; @@ -634,25 +634,23 @@ aoa_hid_end: } } - if (use_hid_keyboard && !hid_keyboard_initialized) { - LOGE("Fallback to default keyboard injection method " - "(-K/--hid-keyboard ignored)"); - options->keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_INJECT; + if (use_aoa_keyboard && !hid_keyboard_initialized) { + LOGE("Fallback to --keyboard=sdk (--keyboard=aoa ignored)"); + options->keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_SDK; } - if (use_hid_mouse && !hid_mouse_initialized) { - LOGE("Fallback to default mouse injection method " - "(-M/--hid-mouse ignored)"); - options->mouse_input_mode = SC_MOUSE_INPUT_MODE_INJECT; + if (use_aoa_mouse && !hid_mouse_initialized) { + LOGE("Fallback to --keyboard=sdk (--keyboard=aoa ignored)"); + options->mouse_input_mode = SC_MOUSE_INPUT_MODE_SDK; } } #else - assert(options->keyboard_input_mode != SC_KEYBOARD_INPUT_MODE_HID); - assert(options->mouse_input_mode != SC_MOUSE_INPUT_MODE_HID); + assert(options->keyboard_input_mode != SC_KEYBOARD_INPUT_MODE_AOA); + assert(options->mouse_input_mode != SC_MOUSE_INPUT_MODE_AOA); #endif // keyboard_input_mode may have been reset if HID mode failed - if (options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_INJECT) { + if (options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_SDK) { sc_keyboard_inject_init(&s->keyboard_inject, &s->controller, options->key_inject_mode, options->forward_key_repeat); @@ -660,7 +658,7 @@ aoa_hid_end: } // mouse_input_mode may have been reset if HID mode failed - if (options->mouse_input_mode == SC_MOUSE_INPUT_MODE_INJECT) { + if (options->mouse_input_mode == SC_MOUSE_INPUT_MODE_SDK) { sc_mouse_inject_init(&s->mouse_inject, &s->controller); mp = &s->mouse_inject.mouse_processor; } diff --git a/app/src/usb/scrcpy_otg.c b/app/src/usb/scrcpy_otg.c index dfb0b9e9..5955e909 100644 --- a/app/src/usb/scrcpy_otg.c +++ b/app/src/usb/scrcpy_otg.c @@ -117,16 +117,15 @@ scrcpy_otg(struct scrcpy_options *options) { } aoa_initialized = true; - bool enable_keyboard = - options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_HID; - bool enable_mouse = - options->mouse_input_mode == SC_MOUSE_INPUT_MODE_HID; + assert(options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_AOA + || options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_DISABLED); + assert(options->mouse_input_mode == SC_MOUSE_INPUT_MODE_AOA + || options->mouse_input_mode == SC_MOUSE_INPUT_MODE_DISABLED); - // If neither --hid-keyboard or --hid-mouse is passed, enable both - if (!enable_keyboard && !enable_mouse) { - enable_keyboard = true; - enable_mouse = true; - } + bool enable_keyboard = + options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_AOA; + bool enable_mouse = + options->mouse_input_mode == SC_MOUSE_INPUT_MODE_AOA; if (enable_keyboard) { ok = sc_hid_keyboard_init(&s->keyboard, &s->aoa); From 48adae1728c6870a88a596ea092f98c76c7586b7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 25 Jan 2024 19:39:35 +0100 Subject: [PATCH 1772/2244] Fix HID mouse documentation The size of a mouse HID event is 4 bytes. PR #4473 --- app/src/usb/hid_mouse.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/src/usb/hid_mouse.c b/app/src/usb/hid_mouse.c index bab89940..06e2a224 100644 --- a/app/src/usb/hid_mouse.c +++ b/app/src/usb/hid_mouse.c @@ -10,7 +10,8 @@ #define HID_MOUSE_ACCESSORY_ID 2 -// 1 byte for buttons + padding, 1 byte for X position, 1 byte for Y position +// 1 byte for buttons + padding, 1 byte for X position, 1 byte for Y position, +// 1 byte for wheel motion #define HID_MOUSE_EVENT_SIZE 4 /** @@ -90,11 +91,12 @@ static const unsigned char mouse_report_desc[] = { }; /** - * A mouse HID event is 3 bytes long: + * A mouse HID event is 4 bytes long: * * - byte 0: buttons state * - byte 1: relative x motion (signed byte from -127 to 127) * - byte 2: relative y motion (signed byte from -127 to 127) + * - byte 3: wheel motion (-1, 0 or 1) * * 7 6 5 4 3 2 1 0 * +---------------+ @@ -112,7 +114,7 @@ static const unsigned char mouse_report_desc[] = { * +---------------+ * byte 2: |. . . . . . . .| relative y motion * +---------------+ - * byte 3: |. . . . . . . .| wheel motion (-1, 0 or 1) + * byte 3: |. . . . . . . .| wheel motion * +---------------+ * * As an example, here is the report for a motion of (x=5, y=-4) with left From 29ce03e3370d6d79c042e02abaa6666c4930d0ee Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 25 Jan 2024 19:45:21 +0100 Subject: [PATCH 1773/2244] Rename "buffer" to "data" The variable name is intended to match the parameter name of libusb_control_transfer(). PR #4473 --- app/src/usb/aoa_hid.c | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/app/src/usb/aoa_hid.c b/app/src/usb/aoa_hid.c index 9bad5296..eb47f415 100644 --- a/app/src/usb/aoa_hid.c +++ b/app/src/usb/aoa_hid.c @@ -97,10 +97,10 @@ sc_aoa_register_hid(struct sc_aoa *aoa, uint16_t accessory_id, // index (arg1): total length of the HID report descriptor uint16_t value = accessory_id; uint16_t index = report_desc_size; - unsigned char *buffer = NULL; + unsigned char *data = NULL; uint16_t length = 0; int result = libusb_control_transfer(aoa->usb->handle, request_type, - request, value, index, buffer, length, + request, value, index, data, length, DEFAULT_TIMEOUT); if (result < 0) { LOGE("REGISTER_HID: libusb error: %s", libusb_strerror(result)); @@ -130,14 +130,14 @@ sc_aoa_set_hid_report_desc(struct sc_aoa *aoa, uint16_t accessory_id, * See */ // value (arg0): accessory assigned ID for the HID device - // index (arg1): offset of data (buffer) in descriptor + // index (arg1): offset of data in descriptor uint16_t value = accessory_id; uint16_t index = 0; // libusb_control_transfer expects a pointer to non-const - unsigned char *buffer = (unsigned char *) report_desc; + unsigned char *data = (unsigned char *) report_desc; uint16_t length = report_desc_size; int result = libusb_control_transfer(aoa->usb->handle, request_type, - request, value, index, buffer, length, + request, value, index, data, length, DEFAULT_TIMEOUT); if (result < 0) { LOGE("SET_HID_REPORT_DESC: libusb error: %s", libusb_strerror(result)); @@ -177,10 +177,10 @@ sc_aoa_send_hid_event(struct sc_aoa *aoa, const struct sc_hid_event *event) { // index (arg1): 0 (unused) uint16_t value = event->accessory_id; uint16_t index = 0; - unsigned char *buffer = event->buffer; + unsigned char *data = event->buffer; uint16_t length = event->size; int result = libusb_control_transfer(aoa->usb->handle, request_type, - request, value, index, buffer, length, + request, value, index, data, length, DEFAULT_TIMEOUT); if (result < 0) { LOGE("SEND_HID_EVENT: libusb error: %s", libusb_strerror(result)); @@ -200,10 +200,10 @@ sc_aoa_unregister_hid(struct sc_aoa *aoa, const uint16_t accessory_id) { // index (arg1): 0 uint16_t value = accessory_id; uint16_t index = 0; - unsigned char *buffer = NULL; + unsigned char *data = NULL; uint16_t length = 0; int result = libusb_control_transfer(aoa->usb->handle, request_type, - request, value, index, buffer, length, + request, value, index, data, length, DEFAULT_TIMEOUT); if (result < 0) { LOGE("UNREGISTER_HID: libusb error: %s", libusb_strerror(result)); From ae303b8d07bf84a96d97c7cead9e0a405a5e1482 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 25 Jan 2024 20:01:34 +0100 Subject: [PATCH 1774/2244] Rename hid event "buffer" to "data" This fields contains the HID event data (there is no "bufferization"). PR #4473 --- app/src/usb/aoa_hid.c | 12 ++++++------ app/src/usb/aoa_hid.h | 4 ++-- app/src/usb/hid_keyboard.c | 24 ++++++++++++------------ app/src/usb/hid_mouse.c | 36 ++++++++++++++++++------------------ 4 files changed, 38 insertions(+), 38 deletions(-) diff --git a/app/src/usb/aoa_hid.c b/app/src/usb/aoa_hid.c index eb47f415..1f2f7c79 100644 --- a/app/src/usb/aoa_hid.c +++ b/app/src/usb/aoa_hid.c @@ -27,7 +27,7 @@ sc_hid_event_log(const struct sc_hid_event *event) { return; } for (unsigned i = 0; i < event->size; ++i) { - snprintf(buffer + i * 3, 4, " %02x", event->buffer[i]); + snprintf(buffer + i * 3, 4, " %02x", event->data[i]); } LOGV("HID Event: [%d]%s", event->accessory_id, buffer); free(buffer); @@ -35,16 +35,16 @@ sc_hid_event_log(const struct sc_hid_event *event) { void sc_hid_event_init(struct sc_hid_event *hid_event, uint16_t accessory_id, - unsigned char *buffer, uint16_t buffer_size) { + unsigned char *data, uint16_t size) { hid_event->accessory_id = accessory_id; - hid_event->buffer = buffer; - hid_event->size = buffer_size; + hid_event->data = data; + hid_event->size = size; hid_event->ack_to_wait = SC_SEQUENCE_INVALID; } void sc_hid_event_destroy(struct sc_hid_event *hid_event) { - free(hid_event->buffer); + free(hid_event->data); } bool @@ -177,7 +177,7 @@ sc_aoa_send_hid_event(struct sc_aoa *aoa, const struct sc_hid_event *event) { // index (arg1): 0 (unused) uint16_t value = event->accessory_id; uint16_t index = 0; - unsigned char *data = event->buffer; + unsigned char *data = event->data; uint16_t length = event->size; int result = libusb_control_transfer(aoa->usb->handle, request_type, request, value, index, data, length, diff --git a/app/src/usb/aoa_hid.h b/app/src/usb/aoa_hid.h index fb5e1d28..a726938a 100644 --- a/app/src/usb/aoa_hid.h +++ b/app/src/usb/aoa_hid.h @@ -14,7 +14,7 @@ struct sc_hid_event { uint16_t accessory_id; - unsigned char *buffer; + unsigned char *data; uint16_t size; uint64_t ack_to_wait; }; @@ -22,7 +22,7 @@ struct sc_hid_event { // Takes ownership of buffer void sc_hid_event_init(struct sc_hid_event *hid_event, uint16_t accessory_id, - unsigned char *buffer, uint16_t buffer_size); + unsigned char *data, uint16_t size); void sc_hid_event_destroy(struct sc_hid_event *hid_event); diff --git a/app/src/usb/hid_keyboard.c b/app/src/usb/hid_keyboard.c index e717006a..8bd0866a 100644 --- a/app/src/usb/hid_keyboard.c +++ b/app/src/usb/hid_keyboard.c @@ -233,17 +233,17 @@ sdl_keymod_to_hid_modifiers(uint16_t mod) { static bool sc_hid_keyboard_event_init(struct sc_hid_event *hid_event) { - unsigned char *buffer = malloc(HID_KEYBOARD_EVENT_SIZE); - if (!buffer) { + unsigned char *data = malloc(HID_KEYBOARD_EVENT_SIZE); + if (!data) { LOG_OOM(); return false; } - buffer[HID_KEYBOARD_INDEX_MODIFIER] = HID_MODIFIER_NONE; - buffer[1] = HID_RESERVED; - memset(&buffer[HID_KEYBOARD_INDEX_KEYS], 0, HID_KEYBOARD_MAX_KEYS); + data[HID_KEYBOARD_INDEX_MODIFIER] = HID_MODIFIER_NONE; + data[1] = HID_RESERVED; + memset(&data[HID_KEYBOARD_INDEX_KEYS], 0, HID_KEYBOARD_MAX_KEYS); - sc_hid_event_init(hid_event, HID_KEYBOARD_ACCESSORY_ID, buffer, + sc_hid_event_init(hid_event, HID_KEYBOARD_ACCESSORY_ID, data, HID_KEYBOARD_EVENT_SIZE); return true; } @@ -282,9 +282,9 @@ convert_hid_keyboard_event(struct sc_hid_keyboard *kb, kb->keys[scancode] ? "true" : "false"); } - hid_event->buffer[HID_KEYBOARD_INDEX_MODIFIER] = modifiers; + hid_event->data[HID_KEYBOARD_INDEX_MODIFIER] = modifiers; - unsigned char *keys_buffer = &hid_event->buffer[HID_KEYBOARD_INDEX_KEYS]; + unsigned char *keys_data = &hid_event->data[HID_KEYBOARD_INDEX_KEYS]; // Re-calculate pressed keys every time int keys_pressed_count = 0; for (int i = 0; i < SC_HID_KEYBOARD_KEYS; ++i) { @@ -296,11 +296,11 @@ convert_hid_keyboard_event(struct sc_hid_keyboard *kb, // - Modifiers // - Reserved // - ErrorRollOver * HID_MAX_KEYS - memset(keys_buffer, HID_ERROR_ROLL_OVER, HID_KEYBOARD_MAX_KEYS); + memset(keys_data, HID_ERROR_ROLL_OVER, HID_KEYBOARD_MAX_KEYS); goto end; } - keys_buffer[keys_pressed_count] = i; + keys_data[keys_pressed_count] = i; ++keys_pressed_count; } } @@ -331,11 +331,11 @@ push_mod_lock_state(struct sc_hid_keyboard *kb, uint16_t mods_state) { unsigned i = 0; if (capslock) { - hid_event.buffer[HID_KEYBOARD_INDEX_KEYS + i] = SC_SCANCODE_CAPSLOCK; + hid_event.data[HID_KEYBOARD_INDEX_KEYS + i] = SC_SCANCODE_CAPSLOCK; ++i; } if (numlock) { - hid_event.buffer[HID_KEYBOARD_INDEX_KEYS + i] = SC_SCANCODE_NUMLOCK; + hid_event.data[HID_KEYBOARD_INDEX_KEYS + i] = SC_SCANCODE_NUMLOCK; ++i; } diff --git a/app/src/usb/hid_mouse.c b/app/src/usb/hid_mouse.c index 06e2a224..45ae6441 100644 --- a/app/src/usb/hid_mouse.c +++ b/app/src/usb/hid_mouse.c @@ -133,13 +133,13 @@ static const unsigned char mouse_report_desc[] = { static bool sc_hid_mouse_event_init(struct sc_hid_event *hid_event) { - unsigned char *buffer = calloc(1, HID_MOUSE_EVENT_SIZE); - if (!buffer) { + unsigned char *data = calloc(1, HID_MOUSE_EVENT_SIZE); + if (!data) { LOG_OOM(); return false; } - sc_hid_event_init(hid_event, HID_MOUSE_ACCESSORY_ID, buffer, + sc_hid_event_init(hid_event, HID_MOUSE_ACCESSORY_ID, data, HID_MOUSE_EVENT_SIZE); return true; } @@ -175,11 +175,11 @@ sc_mouse_processor_process_mouse_motion(struct sc_mouse_processor *mp, return; } - unsigned char *buffer = hid_event.buffer; - buffer[0] = buttons_state_to_hid_buttons(event->buttons_state); - buffer[1] = CLAMP(event->xrel, -127, 127); - buffer[2] = CLAMP(event->yrel, -127, 127); - buffer[3] = 0; // wheel coordinates only used for scrolling + unsigned char *data = hid_event.data; + data[0] = buttons_state_to_hid_buttons(event->buttons_state); + data[1] = CLAMP(event->xrel, -127, 127); + data[2] = CLAMP(event->yrel, -127, 127); + data[3] = 0; // wheel coordinates only used for scrolling if (!sc_aoa_push_hid_event(mouse->aoa, &hid_event)) { sc_hid_event_destroy(&hid_event); @@ -197,11 +197,11 @@ sc_mouse_processor_process_mouse_click(struct sc_mouse_processor *mp, return; } - unsigned char *buffer = hid_event.buffer; - buffer[0] = buttons_state_to_hid_buttons(event->buttons_state); - buffer[1] = 0; // no x motion - buffer[2] = 0; // no y motion - buffer[3] = 0; // wheel coordinates only used for scrolling + unsigned char *data = hid_event.data; + data[0] = buttons_state_to_hid_buttons(event->buttons_state); + data[1] = 0; // no x motion + data[2] = 0; // no y motion + data[3] = 0; // wheel coordinates only used for scrolling if (!sc_aoa_push_hid_event(mouse->aoa, &hid_event)) { sc_hid_event_destroy(&hid_event); @@ -219,13 +219,13 @@ sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp, return; } - unsigned char *buffer = hid_event.buffer; - buffer[0] = 0; // buttons state irrelevant (and unknown) - buffer[1] = 0; // no x motion - buffer[2] = 0; // no y motion + unsigned char *data = hid_event.data; + data[0] = 0; // buttons state irrelevant (and unknown) + data[1] = 0; // no x motion + data[2] = 0; // no y motion // In practice, vscroll is always -1, 0 or 1, but in theory other values // are possible - buffer[3] = CLAMP(event->vscroll, -127, 127); + data[3] = CLAMP(event->vscroll, -127, 127); // Horizontal scrolling ignored if (!sc_aoa_push_hid_event(mouse->aoa, &hid_event)) { From 2d32557fdea3e83dd8ec2a73e122338cbe5d417b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 25 Jan 2024 20:17:37 +0100 Subject: [PATCH 1775/2244] Embed HID event data In the implementation, an HID event is at most 8 bytes. Embed the data in the HID event structure to avoid allocations and simplify the code. PR #4473 --- app/src/usb/aoa_hid.c | 26 ++------------------------ app/src/usb/aoa_hid.h | 14 ++++---------- app/src/usb/hid_keyboard.c | 28 ++++++++-------------------- app/src/usb/hid_mouse.c | 31 +++++++++---------------------- 4 files changed, 23 insertions(+), 76 deletions(-) diff --git a/app/src/usb/aoa_hid.c b/app/src/usb/aoa_hid.c index 1f2f7c79..5db7ab94 100644 --- a/app/src/usb/aoa_hid.c +++ b/app/src/usb/aoa_hid.c @@ -33,20 +33,6 @@ sc_hid_event_log(const struct sc_hid_event *event) { free(buffer); } -void -sc_hid_event_init(struct sc_hid_event *hid_event, uint16_t accessory_id, - unsigned char *data, uint16_t size) { - hid_event->accessory_id = accessory_id; - hid_event->data = data; - hid_event->size = size; - hid_event->ack_to_wait = SC_SEQUENCE_INVALID; -} - -void -sc_hid_event_destroy(struct sc_hid_event *hid_event) { - free(hid_event->data); -} - bool sc_aoa_init(struct sc_aoa *aoa, struct sc_usb *usb, struct sc_acksync *acksync) { @@ -76,12 +62,7 @@ sc_aoa_init(struct sc_aoa *aoa, struct sc_usb *usb, void sc_aoa_destroy(struct sc_aoa *aoa) { - // Destroy remaining events - while (!sc_vecdeque_is_empty(&aoa->queue)) { - struct sc_hid_event *event = sc_vecdeque_popref(&aoa->queue); - assert(event); - sc_hid_event_destroy(event); - } + sc_vecdeque_destroy(&aoa->queue); sc_cond_destroy(&aoa->event_cond); sc_mutex_destroy(&aoa->mutex); @@ -177,7 +158,7 @@ sc_aoa_send_hid_event(struct sc_aoa *aoa, const struct sc_hid_event *event) { // index (arg1): 0 (unused) uint16_t value = event->accessory_id; uint16_t index = 0; - unsigned char *data = event->data; + unsigned char *data = (uint8_t *) event->data; // discard const uint16_t length = event->size; int result = libusb_control_transfer(aoa->usb->handle, request_type, request, value, index, data, length, @@ -271,17 +252,14 @@ run_aoa_thread(void *data) { if (result == SC_ACKSYNC_WAIT_TIMEOUT) { LOGW("Ack not received after 500ms, discarding HID event"); - sc_hid_event_destroy(&event); continue; } else if (result == SC_ACKSYNC_WAIT_INTR) { // stopped - sc_hid_event_destroy(&event); break; } } bool ok = sc_aoa_send_hid_event(aoa, &event); - sc_hid_event_destroy(&event); if (!ok) { LOGW("Could not send HID event to USB device"); } diff --git a/app/src/usb/aoa_hid.h b/app/src/usb/aoa_hid.h index a726938a..2cbd1a23 100644 --- a/app/src/usb/aoa_hid.h +++ b/app/src/usb/aoa_hid.h @@ -12,21 +12,15 @@ #include "util/tick.h" #include "util/vecdeque.h" +#define SC_HID_MAX_SIZE 8 + struct sc_hid_event { uint16_t accessory_id; - unsigned char *data; - uint16_t size; + uint8_t data[SC_HID_MAX_SIZE]; + uint8_t size; uint64_t ack_to_wait; }; -// Takes ownership of buffer -void -sc_hid_event_init(struct sc_hid_event *hid_event, uint16_t accessory_id, - unsigned char *data, uint16_t size); - -void -sc_hid_event_destroy(struct sc_hid_event *hid_event); - struct sc_hid_event_queue SC_VECDEQUE(struct sc_hid_event); struct sc_aoa { diff --git a/app/src/usb/hid_keyboard.c b/app/src/usb/hid_keyboard.c index 8bd0866a..dcf56313 100644 --- a/app/src/usb/hid_keyboard.c +++ b/app/src/usb/hid_keyboard.c @@ -231,21 +231,17 @@ sdl_keymod_to_hid_modifiers(uint16_t mod) { return modifiers; } -static bool +static void sc_hid_keyboard_event_init(struct sc_hid_event *hid_event) { - unsigned char *data = malloc(HID_KEYBOARD_EVENT_SIZE); - if (!data) { - LOG_OOM(); - return false; - } + hid_event->accessory_id = HID_KEYBOARD_ACCESSORY_ID; + hid_event->size = HID_KEYBOARD_EVENT_SIZE; + hid_event->ack_to_wait = SC_SEQUENCE_INVALID; + + uint8_t *data = hid_event->data; data[HID_KEYBOARD_INDEX_MODIFIER] = HID_MODIFIER_NONE; data[1] = HID_RESERVED; memset(&data[HID_KEYBOARD_INDEX_KEYS], 0, HID_KEYBOARD_MAX_KEYS); - - sc_hid_event_init(hid_event, HID_KEYBOARD_ACCESSORY_ID, data, - HID_KEYBOARD_EVENT_SIZE); - return true; } static inline bool @@ -268,10 +264,7 @@ convert_hid_keyboard_event(struct sc_hid_keyboard *kb, return false; } - if (!sc_hid_keyboard_event_init(hid_event)) { - LOGW("Could not initialize HID keyboard event"); - return false; - } + sc_hid_keyboard_event_init(hid_event); unsigned char modifiers = sdl_keymod_to_hid_modifiers(event->mods_state); @@ -324,10 +317,7 @@ push_mod_lock_state(struct sc_hid_keyboard *kb, uint16_t mods_state) { } struct sc_hid_event hid_event; - if (!sc_hid_keyboard_event_init(&hid_event)) { - LOGW("Could not initialize HID keyboard event"); - return false; - } + sc_hid_keyboard_event_init(&hid_event); unsigned i = 0; if (capslock) { @@ -340,7 +330,6 @@ push_mod_lock_state(struct sc_hid_keyboard *kb, uint16_t mods_state) { } if (!sc_aoa_push_hid_event(kb->aoa, &hid_event)) { - sc_hid_event_destroy(&hid_event); LOGW("Could not request HID event (mod lock state)"); return false; } @@ -382,7 +371,6 @@ sc_key_processor_process_key(struct sc_key_processor *kp, } if (!sc_aoa_push_hid_event(kb->aoa, &hid_event)) { - sc_hid_event_destroy(&hid_event); LOGW("Could not request HID event (key)"); } } diff --git a/app/src/usb/hid_mouse.c b/app/src/usb/hid_mouse.c index 45ae6441..a47534c1 100644 --- a/app/src/usb/hid_mouse.c +++ b/app/src/usb/hid_mouse.c @@ -131,17 +131,13 @@ static const unsigned char mouse_report_desc[] = { * +---------------+ */ -static bool +static void sc_hid_mouse_event_init(struct sc_hid_event *hid_event) { - unsigned char *data = calloc(1, HID_MOUSE_EVENT_SIZE); - if (!data) { - LOG_OOM(); - return false; - } - - sc_hid_event_init(hid_event, HID_MOUSE_ACCESSORY_ID, data, - HID_MOUSE_EVENT_SIZE); - return true; + hid_event->accessory_id = HID_MOUSE_ACCESSORY_ID; + hid_event->size = HID_MOUSE_EVENT_SIZE; + hid_event->ack_to_wait = SC_SEQUENCE_INVALID; + // Leave hid_event->data uninitialized, it will be fully initialized by + // callers } static unsigned char @@ -171,9 +167,7 @@ sc_mouse_processor_process_mouse_motion(struct sc_mouse_processor *mp, struct sc_hid_mouse *mouse = DOWNCAST(mp); struct sc_hid_event hid_event; - if (!sc_hid_mouse_event_init(&hid_event)) { - return; - } + sc_hid_mouse_event_init(&hid_event); unsigned char *data = hid_event.data; data[0] = buttons_state_to_hid_buttons(event->buttons_state); @@ -182,7 +176,6 @@ sc_mouse_processor_process_mouse_motion(struct sc_mouse_processor *mp, data[3] = 0; // wheel coordinates only used for scrolling if (!sc_aoa_push_hid_event(mouse->aoa, &hid_event)) { - sc_hid_event_destroy(&hid_event); LOGW("Could not request HID event (mouse motion)"); } } @@ -193,9 +186,7 @@ sc_mouse_processor_process_mouse_click(struct sc_mouse_processor *mp, struct sc_hid_mouse *mouse = DOWNCAST(mp); struct sc_hid_event hid_event; - if (!sc_hid_mouse_event_init(&hid_event)) { - return; - } + sc_hid_mouse_event_init(&hid_event); unsigned char *data = hid_event.data; data[0] = buttons_state_to_hid_buttons(event->buttons_state); @@ -204,7 +195,6 @@ sc_mouse_processor_process_mouse_click(struct sc_mouse_processor *mp, data[3] = 0; // wheel coordinates only used for scrolling if (!sc_aoa_push_hid_event(mouse->aoa, &hid_event)) { - sc_hid_event_destroy(&hid_event); LOGW("Could not request HID event (mouse click)"); } } @@ -215,9 +205,7 @@ sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp, struct sc_hid_mouse *mouse = DOWNCAST(mp); struct sc_hid_event hid_event; - if (!sc_hid_mouse_event_init(&hid_event)) { - return; - } + sc_hid_mouse_event_init(&hid_event); unsigned char *data = hid_event.data; data[0] = 0; // buttons state irrelevant (and unknown) @@ -229,7 +217,6 @@ sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp, // Horizontal scrolling ignored if (!sc_aoa_push_hid_event(mouse->aoa, &hid_event)) { - sc_hid_event_destroy(&hid_event); LOGW("Could not request HID event (mouse scroll)"); } } From f2d62031561b4d1660c2c5bb3910e7a723d317f7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 25 Jan 2024 20:32:37 +0100 Subject: [PATCH 1776/2244] Extract HID events struct An event contained several fields: - the accessory id - the HID event data - a field ack_to_wait specific to the AOA implementation. Extract the HID event part to prepare the factorization of HID event creation. PR #4473 --- app/src/hid/hid_event.h | 15 +++++++++++++++ app/src/usb/aoa_hid.c | 34 ++++++++++++++++++++++------------ app/src/usb/aoa_hid.h | 22 ++++++++++++++++------ app/src/usb/hid_keyboard.c | 21 ++++++++++----------- app/src/usb/hid_mouse.c | 11 ++++++----- 5 files changed, 69 insertions(+), 34 deletions(-) create mode 100644 app/src/hid/hid_event.h diff --git a/app/src/hid/hid_event.h b/app/src/hid/hid_event.h new file mode 100644 index 00000000..e17f8569 --- /dev/null +++ b/app/src/hid/hid_event.h @@ -0,0 +1,15 @@ +#ifndef SC_HID_EVENT_H +#define SC_HID_EVENT_H + +#include "common.h" + +#include + +#define SC_HID_MAX_SIZE 8 + +struct sc_hid_event { + uint8_t data[SC_HID_MAX_SIZE]; + uint8_t size; +}; + +#endif diff --git a/app/src/usb/aoa_hid.c b/app/src/usb/aoa_hid.c index 5db7ab94..d6b418a0 100644 --- a/app/src/usb/aoa_hid.c +++ b/app/src/usb/aoa_hid.c @@ -14,10 +14,10 @@ #define DEFAULT_TIMEOUT 1000 -#define SC_HID_EVENT_QUEUE_MAX 64 +#define SC_AOA_EVENT_QUEUE_MAX 64 static void -sc_hid_event_log(const struct sc_hid_event *event) { +sc_hid_event_log(uint16_t accessory_id, const struct sc_hid_event *event) { // HID Event: [00] FF FF FF FF... assert(event->size); unsigned buffer_size = event->size * 3 + 1; @@ -29,7 +29,7 @@ sc_hid_event_log(const struct sc_hid_event *event) { for (unsigned i = 0; i < event->size; ++i) { snprintf(buffer + i * 3, 4, " %02x", event->data[i]); } - LOGV("HID Event: [%d]%s", event->accessory_id, buffer); + LOGV("HID Event: [%d]%s", accessory_id, buffer); free(buffer); } @@ -38,7 +38,7 @@ sc_aoa_init(struct sc_aoa *aoa, struct sc_usb *usb, struct sc_acksync *acksync) { sc_vecdeque_init(&aoa->queue); - if (!sc_vecdeque_reserve(&aoa->queue, SC_HID_EVENT_QUEUE_MAX)) { + if (!sc_vecdeque_reserve(&aoa->queue, SC_AOA_EVENT_QUEUE_MAX)) { return false; } @@ -150,13 +150,14 @@ sc_aoa_setup_hid(struct sc_aoa *aoa, uint16_t accessory_id, } static bool -sc_aoa_send_hid_event(struct sc_aoa *aoa, const struct sc_hid_event *event) { +sc_aoa_send_hid_event(struct sc_aoa *aoa, uint16_t accessory_id, + const struct sc_hid_event *event) { uint8_t request_type = LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR; uint8_t request = ACCESSORY_SEND_HID_EVENT; // // value (arg0): accessory assigned ID for the HID device // index (arg1): 0 (unused) - uint16_t value = event->accessory_id; + uint16_t value = accessory_id; uint16_t index = 0; unsigned char *data = (uint8_t *) event->data; // discard const uint16_t length = event->size; @@ -173,7 +174,7 @@ sc_aoa_send_hid_event(struct sc_aoa *aoa, const struct sc_hid_event *event) { } bool -sc_aoa_unregister_hid(struct sc_aoa *aoa, const uint16_t accessory_id) { +sc_aoa_unregister_hid(struct sc_aoa *aoa, uint16_t accessory_id) { uint8_t request_type = LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR; uint8_t request = ACCESSORY_UNREGISTER_HID; // @@ -196,16 +197,25 @@ sc_aoa_unregister_hid(struct sc_aoa *aoa, const uint16_t accessory_id) { } bool -sc_aoa_push_hid_event(struct sc_aoa *aoa, const struct sc_hid_event *event) { +sc_aoa_push_hid_event_with_ack_to_wait(struct sc_aoa *aoa, + uint16_t accessory_id, + const struct sc_hid_event *event, + uint64_t ack_to_wait) { if (sc_get_log_level() <= SC_LOG_LEVEL_VERBOSE) { - sc_hid_event_log(event); + sc_hid_event_log(accessory_id, event); } sc_mutex_lock(&aoa->mutex); bool full = sc_vecdeque_is_full(&aoa->queue); if (!full) { bool was_empty = sc_vecdeque_is_empty(&aoa->queue); - sc_vecdeque_push_noresize(&aoa->queue, *event); + + struct sc_aoa_event *aoa_event = + sc_vecdeque_push_hole_noresize(&aoa->queue); + aoa_event->hid = *event; + aoa_event->accessory_id = accessory_id; + aoa_event->ack_to_wait = ack_to_wait; + if (was_empty) { sc_cond_signal(&aoa->event_cond); } @@ -233,7 +243,7 @@ run_aoa_thread(void *data) { } assert(!sc_vecdeque_is_empty(&aoa->queue)); - struct sc_hid_event event = sc_vecdeque_pop(&aoa->queue); + struct sc_aoa_event event = sc_vecdeque_pop(&aoa->queue); uint64_t ack_to_wait = event.ack_to_wait; sc_mutex_unlock(&aoa->mutex); @@ -259,7 +269,7 @@ run_aoa_thread(void *data) { } } - bool ok = sc_aoa_send_hid_event(aoa, &event); + bool ok = sc_aoa_send_hid_event(aoa, event.accessory_id, &event.hid); if (!ok) { LOGW("Could not send HID event to USB device"); } diff --git a/app/src/usb/aoa_hid.h b/app/src/usb/aoa_hid.h index 2cbd1a23..33a1f136 100644 --- a/app/src/usb/aoa_hid.h +++ b/app/src/usb/aoa_hid.h @@ -6,6 +6,7 @@ #include +#include "hid/hid_event.h" #include "usb.h" #include "util/acksync.h" #include "util/thread.h" @@ -14,14 +15,13 @@ #define SC_HID_MAX_SIZE 8 -struct sc_hid_event { +struct sc_aoa_event { + struct sc_hid_event hid; uint16_t accessory_id; - uint8_t data[SC_HID_MAX_SIZE]; - uint8_t size; uint64_t ack_to_wait; }; -struct sc_hid_event_queue SC_VECDEQUE(struct sc_hid_event); +struct sc_aoa_event_queue SC_VECDEQUE(struct sc_aoa_event); struct sc_aoa { struct sc_usb *usb; @@ -29,7 +29,7 @@ struct sc_aoa { sc_mutex mutex; sc_cond event_cond; bool stopped; - struct sc_hid_event_queue queue; + struct sc_aoa_event_queue queue; struct sc_acksync *acksync; }; @@ -57,6 +57,16 @@ bool sc_aoa_unregister_hid(struct sc_aoa *aoa, uint16_t accessory_id); bool -sc_aoa_push_hid_event(struct sc_aoa *aoa, const struct sc_hid_event *event); +sc_aoa_push_hid_event_with_ack_to_wait(struct sc_aoa *aoa, + uint16_t accessory_id, + const struct sc_hid_event *event, + uint64_t ack_to_wait); + +static inline bool +sc_aoa_push_hid_event(struct sc_aoa *aoa, uint16_t accessory_id, + const struct sc_hid_event *event) { + return sc_aoa_push_hid_event_with_ack_to_wait(aoa, accessory_id, event, + SC_SEQUENCE_INVALID); +} #endif diff --git a/app/src/usb/hid_keyboard.c b/app/src/usb/hid_keyboard.c index dcf56313..9b87a27a 100644 --- a/app/src/usb/hid_keyboard.c +++ b/app/src/usb/hid_keyboard.c @@ -233,9 +233,7 @@ sdl_keymod_to_hid_modifiers(uint16_t mod) { static void sc_hid_keyboard_event_init(struct sc_hid_event *hid_event) { - hid_event->accessory_id = HID_KEYBOARD_ACCESSORY_ID; hid_event->size = HID_KEYBOARD_EVENT_SIZE; - hid_event->ack_to_wait = SC_SEQUENCE_INVALID; uint8_t *data = hid_event->data; @@ -329,7 +327,8 @@ push_mod_lock_state(struct sc_hid_keyboard *kb, uint16_t mods_state) { ++i; } - if (!sc_aoa_push_hid_event(kb->aoa, &hid_event)) { + if (!sc_aoa_push_hid_event(kb->aoa, HID_KEYBOARD_ACCESSORY_ID, + &hid_event)) { LOGW("Could not request HID event (mod lock state)"); return false; } @@ -362,15 +361,15 @@ sc_key_processor_process_key(struct sc_key_processor *kp, } } - if (ack_to_wait) { - // Ctrl+v is pressed, so clipboard synchronization has been - // requested. Wait until clipboard synchronization is acknowledged - // by the server, otherwise it could paste the old clipboard - // content. - hid_event.ack_to_wait = ack_to_wait; - } + // If ack_to_wait is != SC_SEQUENCE_INVALID, then Ctrl+v is pressed, so + // clipboard synchronization has been requested. Wait until clipboard + // synchronization is acknowledged by the server, otherwise it could + // paste the old clipboard content. - if (!sc_aoa_push_hid_event(kb->aoa, &hid_event)) { + if (!sc_aoa_push_hid_event_with_ack_to_wait(kb->aoa, + HID_KEYBOARD_ACCESSORY_ID, + &hid_event, + ack_to_wait)) { LOGW("Could not request HID event (key)"); } } diff --git a/app/src/usb/hid_mouse.c b/app/src/usb/hid_mouse.c index a47534c1..de961265 100644 --- a/app/src/usb/hid_mouse.c +++ b/app/src/usb/hid_mouse.c @@ -133,9 +133,7 @@ static const unsigned char mouse_report_desc[] = { static void sc_hid_mouse_event_init(struct sc_hid_event *hid_event) { - hid_event->accessory_id = HID_MOUSE_ACCESSORY_ID; hid_event->size = HID_MOUSE_EVENT_SIZE; - hid_event->ack_to_wait = SC_SEQUENCE_INVALID; // Leave hid_event->data uninitialized, it will be fully initialized by // callers } @@ -175,7 +173,8 @@ sc_mouse_processor_process_mouse_motion(struct sc_mouse_processor *mp, data[2] = CLAMP(event->yrel, -127, 127); data[3] = 0; // wheel coordinates only used for scrolling - if (!sc_aoa_push_hid_event(mouse->aoa, &hid_event)) { + if (!sc_aoa_push_hid_event(mouse->aoa, HID_MOUSE_ACCESSORY_ID, + &hid_event)) { LOGW("Could not request HID event (mouse motion)"); } } @@ -194,7 +193,8 @@ sc_mouse_processor_process_mouse_click(struct sc_mouse_processor *mp, data[2] = 0; // no y motion data[3] = 0; // wheel coordinates only used for scrolling - if (!sc_aoa_push_hid_event(mouse->aoa, &hid_event)) { + if (!sc_aoa_push_hid_event(mouse->aoa, HID_MOUSE_ACCESSORY_ID, + &hid_event)) { LOGW("Could not request HID event (mouse click)"); } } @@ -216,7 +216,8 @@ sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp, data[3] = CLAMP(event->vscroll, -127, 127); // Horizontal scrolling ignored - if (!sc_aoa_push_hid_event(mouse->aoa, &hid_event)) { + if (!sc_aoa_push_hid_event(mouse->aoa, HID_MOUSE_ACCESSORY_ID, + &hid_event)) { LOGW("Could not request HID event (mouse scroll)"); } } From 91485e2863603732348d86a4ed06cafa20d1225f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 25 Jan 2024 22:57:02 +0100 Subject: [PATCH 1777/2244] Extract keyboard HID handling Split the keyboard implementation using AOA and the code handling HID events, so that HID events can be reused for another protocol (UHID). PR #4473 --- app/meson.build | 3 +- app/src/{usb => hid}/hid_keyboard.c | 251 +++++++++------------------- app/src/{usb => hid}/hid_keyboard.h | 26 +-- app/src/scrcpy.c | 32 ++-- app/src/usb/keyboard_aoa.c | 109 ++++++++++++ app/src/usb/keyboard_aoa.h | 27 +++ app/src/usb/scrcpy_otg.c | 8 +- app/src/usb/screen_otg.h | 6 +- 8 files changed, 259 insertions(+), 203 deletions(-) rename app/src/{usb => hid}/hid_keyboard.c (56%) rename app/src/{usb => hid}/hid_keyboard.h (68%) create mode 100644 app/src/usb/keyboard_aoa.c create mode 100644 app/src/usb/keyboard_aoa.h diff --git a/app/meson.build b/app/meson.build index caf5ee5c..6d572b7b 100644 --- a/app/meson.build +++ b/app/meson.build @@ -31,6 +31,7 @@ src = [ 'src/screen.c', 'src/server.c', 'src/version.c', + 'src/hid/hid_keyboard.c', 'src/trait/frame_source.c', 'src/trait/packet_source.c', 'src/util/acksync.c', @@ -88,7 +89,7 @@ usb_support = get_option('usb') if usb_support src += [ 'src/usb/aoa_hid.c', - 'src/usb/hid_keyboard.c', + 'src/usb/keyboard_aoa.c', 'src/usb/hid_mouse.c', 'src/usb/scrcpy_otg.c', 'src/usb/screen_otg.c', diff --git a/app/src/usb/hid_keyboard.c b/app/src/hid/hid_keyboard.c similarity index 56% rename from app/src/usb/hid_keyboard.c rename to app/src/hid/hid_keyboard.c index 9b87a27a..f3001df4 100644 --- a/app/src/usb/hid_keyboard.c +++ b/app/src/hid/hid_keyboard.c @@ -1,40 +1,34 @@ #include "hid_keyboard.h" -#include +#include -#include "input_events.h" #include "util/log.h" -/** Downcast key processor to hid_keyboard */ -#define DOWNCAST(KP) container_of(KP, struct sc_hid_keyboard, key_processor) +#define SC_HID_MOD_NONE 0x00 +#define SC_HID_MOD_LEFT_CONTROL (1 << 0) +#define SC_HID_MOD_LEFT_SHIFT (1 << 1) +#define SC_HID_MOD_LEFT_ALT (1 << 2) +#define SC_HID_MOD_LEFT_GUI (1 << 3) +#define SC_HID_MOD_RIGHT_CONTROL (1 << 4) +#define SC_HID_MOD_RIGHT_SHIFT (1 << 5) +#define SC_HID_MOD_RIGHT_ALT (1 << 6) +#define SC_HID_MOD_RIGHT_GUI (1 << 7) -#define HID_KEYBOARD_ACCESSORY_ID 1 - -#define HID_MODIFIER_NONE 0x00 -#define HID_MODIFIER_LEFT_CONTROL (1 << 0) -#define HID_MODIFIER_LEFT_SHIFT (1 << 1) -#define HID_MODIFIER_LEFT_ALT (1 << 2) -#define HID_MODIFIER_LEFT_GUI (1 << 3) -#define HID_MODIFIER_RIGHT_CONTROL (1 << 4) -#define HID_MODIFIER_RIGHT_SHIFT (1 << 5) -#define HID_MODIFIER_RIGHT_ALT (1 << 6) -#define HID_MODIFIER_RIGHT_GUI (1 << 7) - -#define HID_KEYBOARD_INDEX_MODIFIER 0 -#define HID_KEYBOARD_INDEX_KEYS 2 +#define SC_HID_KEYBOARD_INDEX_MODS 0 +#define SC_HID_KEYBOARD_INDEX_KEYS 2 // USB HID protocol says 6 keys in an event is the requirement for BIOS // keyboard support, though OS could support more keys via modifying the report // desc. 6 should be enough for scrcpy. -#define HID_KEYBOARD_MAX_KEYS 6 -#define HID_KEYBOARD_EVENT_SIZE \ - (HID_KEYBOARD_INDEX_KEYS + HID_KEYBOARD_MAX_KEYS) +#define SC_HID_KEYBOARD_MAX_KEYS 6 +#define SC_HID_KEYBOARD_EVENT_SIZE \ + (SC_HID_KEYBOARD_INDEX_KEYS + SC_HID_KEYBOARD_MAX_KEYS) -#define HID_RESERVED 0x00 -#define HID_ERROR_ROLL_OVER 0x01 +#define SC_HID_RESERVED 0x00 +#define SC_HID_ERROR_ROLL_OVER 0x01 /** - * For HID over AOAv2, only report descriptor is needed. + * For HID, only report descriptor is needed. * * The specification is available here: * @@ -53,7 +47,7 @@ * * (change vid:pid' to your device's vendor ID and product ID). */ -static const unsigned char keyboard_report_desc[] = { +const uint8_t SC_HID_KEYBOARD_REPORT_DESC[] = { // Usage Page (Generic Desktop) 0x05, 0x01, // Usage (Keyboard) @@ -119,7 +113,7 @@ static const unsigned char keyboard_report_desc[] = { // Report Size (8) 0x75, 0x08, // Report Count (6) - 0x95, HID_KEYBOARD_MAX_KEYS, + 0x95, SC_HID_KEYBOARD_MAX_KEYS, // Input (Data, Array): Keys 0x81, 0x00, @@ -127,6 +121,9 @@ static const unsigned char keyboard_report_desc[] = { 0xC0 }; +const size_t SC_HID_KEYBOARD_REPORT_DESC_LEN = + sizeof(SC_HID_KEYBOARD_REPORT_DESC); + /** * A keyboard HID event is 8 bytes long: * @@ -201,45 +198,50 @@ static const unsigned char keyboard_report_desc[] = { * +---------------+ */ -static unsigned char -sdl_keymod_to_hid_modifiers(uint16_t mod) { - unsigned char modifiers = HID_MODIFIER_NONE; - if (mod & SC_MOD_LCTRL) { - modifiers |= HID_MODIFIER_LEFT_CONTROL; - } - if (mod & SC_MOD_LSHIFT) { - modifiers |= HID_MODIFIER_LEFT_SHIFT; - } - if (mod & SC_MOD_LALT) { - modifiers |= HID_MODIFIER_LEFT_ALT; - } - if (mod & SC_MOD_LGUI) { - modifiers |= HID_MODIFIER_LEFT_GUI; - } - if (mod & SC_MOD_RCTRL) { - modifiers |= HID_MODIFIER_RIGHT_CONTROL; - } - if (mod & SC_MOD_RSHIFT) { - modifiers |= HID_MODIFIER_RIGHT_SHIFT; - } - if (mod & SC_MOD_RALT) { - modifiers |= HID_MODIFIER_RIGHT_ALT; - } - if (mod & SC_MOD_RGUI) { - modifiers |= HID_MODIFIER_RIGHT_GUI; - } - return modifiers; -} - static void sc_hid_keyboard_event_init(struct sc_hid_event *hid_event) { - hid_event->size = HID_KEYBOARD_EVENT_SIZE; + hid_event->size = SC_HID_KEYBOARD_EVENT_SIZE; uint8_t *data = hid_event->data; - data[HID_KEYBOARD_INDEX_MODIFIER] = HID_MODIFIER_NONE; - data[1] = HID_RESERVED; - memset(&data[HID_KEYBOARD_INDEX_KEYS], 0, HID_KEYBOARD_MAX_KEYS); + data[SC_HID_KEYBOARD_INDEX_MODS] = SC_HID_MOD_NONE; + data[1] = SC_HID_RESERVED; + memset(&data[SC_HID_KEYBOARD_INDEX_KEYS], 0, SC_HID_KEYBOARD_MAX_KEYS); +} + +static uint16_t +sc_hid_mod_from_sdl_keymod(uint16_t mod) { + uint16_t mods = SC_HID_MOD_NONE; + if (mod & SC_MOD_LCTRL) { + mods |= SC_HID_MOD_LEFT_CONTROL; + } + if (mod & SC_MOD_LSHIFT) { + mods |= SC_HID_MOD_LEFT_SHIFT; + } + if (mod & SC_MOD_LALT) { + mods |= SC_HID_MOD_LEFT_ALT; + } + if (mod & SC_MOD_LGUI) { + mods |= SC_HID_MOD_LEFT_GUI; + } + if (mod & SC_MOD_RCTRL) { + mods |= SC_HID_MOD_RIGHT_CONTROL; + } + if (mod & SC_MOD_RSHIFT) { + mods |= SC_HID_MOD_RIGHT_SHIFT; + } + if (mod & SC_MOD_RALT) { + mods |= SC_HID_MOD_RIGHT_ALT; + } + if (mod & SC_MOD_RGUI) { + mods |= SC_HID_MOD_RIGHT_GUI; + } + return mods; +} + +void +sc_hid_keyboard_init(struct sc_hid_keyboard *hid) { + memset(hid->keys, false, SC_HID_KEYBOARD_KEYS); } static inline bool @@ -247,10 +249,10 @@ scancode_is_modifier(enum sc_scancode scancode) { return scancode >= SC_SCANCODE_LCTRL && scancode <= SC_SCANCODE_RGUI; } -static bool -convert_hid_keyboard_event(struct sc_hid_keyboard *kb, - struct sc_hid_event *hid_event, - const struct sc_key_event *event) { +bool +sc_hid_keyboard_event_from_key(struct sc_hid_keyboard *hid, + struct sc_hid_event *hid_event, + const struct sc_key_event *event) { enum sc_scancode scancode = event->scancode; assert(scancode >= 0); @@ -264,30 +266,31 @@ convert_hid_keyboard_event(struct sc_hid_keyboard *kb, sc_hid_keyboard_event_init(hid_event); - unsigned char modifiers = sdl_keymod_to_hid_modifiers(event->mods_state); + uint16_t mods = sc_hid_mod_from_sdl_keymod(event->mods_state); if (scancode < SC_HID_KEYBOARD_KEYS) { // Pressed is true and released is false - kb->keys[scancode] = (event->action == SC_ACTION_DOWN); + hid->keys[scancode] = (event->action == SC_ACTION_DOWN); LOGV("keys[%02x] = %s", scancode, - kb->keys[scancode] ? "true" : "false"); + hid->keys[scancode] ? "true" : "false"); } - hid_event->data[HID_KEYBOARD_INDEX_MODIFIER] = modifiers; + hid_event->data[SC_HID_KEYBOARD_INDEX_MODS] = mods; - unsigned char *keys_data = &hid_event->data[HID_KEYBOARD_INDEX_KEYS]; + uint8_t *keys_data = &hid_event->data[SC_HID_KEYBOARD_INDEX_KEYS]; // Re-calculate pressed keys every time int keys_pressed_count = 0; for (int i = 0; i < SC_HID_KEYBOARD_KEYS; ++i) { - if (kb->keys[i]) { + if (hid->keys[i]) { // USB HID protocol says that if keys exceeds report count, a // phantom state should be reported - if (keys_pressed_count >= HID_KEYBOARD_MAX_KEYS) { + if (keys_pressed_count >= SC_HID_KEYBOARD_MAX_KEYS) { // Phantom state: // - Modifiers // - Reserved // - ErrorRollOver * HID_MAX_KEYS - memset(keys_data, HID_ERROR_ROLL_OVER, HID_KEYBOARD_MAX_KEYS); + memset(keys_data, SC_HID_ERROR_ROLL_OVER, + SC_HID_KEYBOARD_MAX_KEYS); goto end; } @@ -299,120 +302,32 @@ convert_hid_keyboard_event(struct sc_hid_keyboard *kb, end: LOGV("hid keyboard: key %-4s scancode=%02x (%u) mod=%02x", event->action == SC_ACTION_DOWN ? "down" : "up", event->scancode, - event->scancode, modifiers); + event->scancode, mods); return true; } - -static bool -push_mod_lock_state(struct sc_hid_keyboard *kb, uint16_t mods_state) { +bool +sc_hid_keyboard_event_from_mods(struct sc_hid_event *event, + uint16_t mods_state) { bool capslock = mods_state & SC_MOD_CAPS; bool numlock = mods_state & SC_MOD_NUM; if (!capslock && !numlock) { // Nothing to do - return true; + return false; } - struct sc_hid_event hid_event; - sc_hid_keyboard_event_init(&hid_event); + sc_hid_keyboard_event_init(event); unsigned i = 0; if (capslock) { - hid_event.data[HID_KEYBOARD_INDEX_KEYS + i] = SC_SCANCODE_CAPSLOCK; + event->data[SC_HID_KEYBOARD_INDEX_KEYS + i] = SC_SCANCODE_CAPSLOCK; ++i; } if (numlock) { - hid_event.data[HID_KEYBOARD_INDEX_KEYS + i] = SC_SCANCODE_NUMLOCK; + event->data[SC_HID_KEYBOARD_INDEX_KEYS + i] = SC_SCANCODE_NUMLOCK; ++i; } - if (!sc_aoa_push_hid_event(kb->aoa, HID_KEYBOARD_ACCESSORY_ID, - &hid_event)) { - LOGW("Could not request HID event (mod lock state)"); - return false; - } - - LOGD("HID keyboard state synchronized"); - return true; } - -static void -sc_key_processor_process_key(struct sc_key_processor *kp, - const struct sc_key_event *event, - uint64_t ack_to_wait) { - if (event->repeat) { - // In USB HID protocol, key repeat is handled by the host (Android), so - // just ignore key repeat here. - return; - } - - struct sc_hid_keyboard *kb = DOWNCAST(kp); - - struct sc_hid_event hid_event; - // Not all keys are supported, just ignore unsupported keys - if (convert_hid_keyboard_event(kb, &hid_event, event)) { - if (!kb->mod_lock_synchronized) { - // Inject CAPSLOCK and/or NUMLOCK if necessary to synchronize - // keyboard state - if (push_mod_lock_state(kb, event->mods_state)) { - kb->mod_lock_synchronized = true; - } - } - - // If ack_to_wait is != SC_SEQUENCE_INVALID, then Ctrl+v is pressed, so - // clipboard synchronization has been requested. Wait until clipboard - // synchronization is acknowledged by the server, otherwise it could - // paste the old clipboard content. - - if (!sc_aoa_push_hid_event_with_ack_to_wait(kb->aoa, - HID_KEYBOARD_ACCESSORY_ID, - &hid_event, - ack_to_wait)) { - LOGW("Could not request HID event (key)"); - } - } -} - -bool -sc_hid_keyboard_init(struct sc_hid_keyboard *kb, struct sc_aoa *aoa) { - kb->aoa = aoa; - - bool ok = sc_aoa_setup_hid(aoa, HID_KEYBOARD_ACCESSORY_ID, - keyboard_report_desc, - ARRAY_LEN(keyboard_report_desc)); - if (!ok) { - LOGW("Register HID keyboard failed"); - return false; - } - - // Reset all states - memset(kb->keys, false, SC_HID_KEYBOARD_KEYS); - - kb->mod_lock_synchronized = false; - - static const struct sc_key_processor_ops ops = { - .process_key = sc_key_processor_process_key, - // Never forward text input via HID (all the keys are injected - // separately) - .process_text = NULL, - }; - - // Clipboard synchronization is requested over the control socket, while HID - // events are sent over AOA, so it must wait for clipboard synchronization - // to be acknowledged by the device before injecting Ctrl+v. - kb->key_processor.async_paste = true; - kb->key_processor.ops = &ops; - - return true; -} - -void -sc_hid_keyboard_destroy(struct sc_hid_keyboard *kb) { - // Unregister HID keyboard so the soft keyboard shows again on Android - bool ok = sc_aoa_unregister_hid(kb->aoa, HID_KEYBOARD_ACCESSORY_ID); - if (!ok) { - LOGW("Could not unregister HID keyboard"); - } -} diff --git a/app/src/usb/hid_keyboard.h b/app/src/hid/hid_keyboard.h similarity index 68% rename from app/src/usb/hid_keyboard.h rename to app/src/hid/hid_keyboard.h index 7173a898..ddd2cc91 100644 --- a/app/src/usb/hid_keyboard.h +++ b/app/src/hid/hid_keyboard.h @@ -5,8 +5,8 @@ #include -#include "aoa_hid.h" -#include "trait/key_processor.h" +#include "hid/hid_event.h" +#include "input_events.h" // See "SDL2/SDL_scancode.h". // Maybe SDL_Keycode is used by most people, but SDL_Scancode is taken from USB @@ -14,6 +14,9 @@ // 0x65 is Application, typically AT-101 Keyboard ends here. #define SC_HID_KEYBOARD_KEYS 0x66 +extern const uint8_t SC_HID_KEYBOARD_REPORT_DESC[]; +extern const size_t SC_HID_KEYBOARD_REPORT_DESC_LEN; + /** * HID keyboard events are sequence-based, every time keyboard state changes * it sends an array of currently pressed keys, the host is responsible for @@ -27,18 +30,19 @@ * phantom state. */ struct sc_hid_keyboard { - struct sc_key_processor key_processor; // key processor trait - - struct sc_aoa *aoa; bool keys[SC_HID_KEYBOARD_KEYS]; - - bool mod_lock_synchronized; }; -bool -sc_hid_keyboard_init(struct sc_hid_keyboard *kb, struct sc_aoa *aoa); - void -sc_hid_keyboard_destroy(struct sc_hid_keyboard *kb); +sc_hid_keyboard_init(struct sc_hid_keyboard *hid); + +bool +sc_hid_keyboard_event_from_key(struct sc_hid_keyboard *hid, + struct sc_hid_event *hid_event, + const struct sc_key_event *event); + +bool +sc_hid_keyboard_event_from_mods(struct sc_hid_event *event, + uint16_t mods_state); #endif diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 24177f15..1d5e67dc 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -27,7 +27,7 @@ #include "server.h" #ifdef HAVE_USB # include "usb/aoa_hid.h" -# include "usb/hid_keyboard.h" +# include "usb/keyboard_aoa.h" # include "usb/hid_mouse.h" # include "usb/usb.h" #endif @@ -65,7 +65,7 @@ struct scrcpy { union { struct sc_keyboard_inject keyboard_inject; #ifdef HAVE_USB - struct sc_hid_keyboard keyboard_hid; + struct sc_keyboard_aoa keyboard_aoa; #endif }; union { @@ -330,7 +330,7 @@ scrcpy(struct scrcpy_options *options) { bool audio_demuxer_started = false; #ifdef HAVE_USB bool aoa_hid_initialized = false; - bool hid_keyboard_initialized = false; + bool keyboard_aoa_initialized = false; bool hid_mouse_initialized = false; #endif bool controller_initialized = false; @@ -543,11 +543,11 @@ scrcpy(struct scrcpy_options *options) { if (options->control) { #ifdef HAVE_USB - bool use_aoa_keyboard = + bool use_keyboard_aoa = options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_AOA; bool use_aoa_mouse = options->mouse_input_mode == SC_MOUSE_INPUT_MODE_AOA; - if (use_aoa_keyboard || use_aoa_mouse) { + if (use_keyboard_aoa || use_aoa_mouse) { bool ok = sc_acksync_init(&s->acksync); if (!ok) { goto end; @@ -590,10 +590,10 @@ scrcpy(struct scrcpy_options *options) { goto aoa_hid_end; } - if (use_aoa_keyboard) { - if (sc_hid_keyboard_init(&s->keyboard_hid, &s->aoa)) { - hid_keyboard_initialized = true; - kp = &s->keyboard_hid.key_processor; + if (use_keyboard_aoa) { + if (sc_keyboard_aoa_init(&s->keyboard_aoa, &s->aoa)) { + keyboard_aoa_initialized = true; + kp = &s->keyboard_aoa.key_processor; } else { LOGE("Could not initialize HID keyboard"); } @@ -608,7 +608,7 @@ scrcpy(struct scrcpy_options *options) { } } - bool need_aoa = hid_keyboard_initialized || hid_mouse_initialized; + bool need_aoa = keyboard_aoa_initialized || hid_mouse_initialized; if (!need_aoa || !sc_aoa_start(&s->aoa)) { sc_acksync_destroy(&s->acksync); @@ -624,9 +624,9 @@ scrcpy(struct scrcpy_options *options) { aoa_hid_end: if (!aoa_hid_initialized) { - if (hid_keyboard_initialized) { - sc_hid_keyboard_destroy(&s->keyboard_hid); - hid_keyboard_initialized = false; + if (keyboard_aoa_initialized) { + sc_keyboard_aoa_destroy(&s->keyboard_aoa); + keyboard_aoa_initialized = false; } if (hid_mouse_initialized) { sc_hid_mouse_destroy(&s->mouse_hid); @@ -634,7 +634,7 @@ aoa_hid_end: } } - if (use_aoa_keyboard && !hid_keyboard_initialized) { + if (use_keyboard_aoa && !keyboard_aoa_initialized) { LOGE("Fallback to --keyboard=sdk (--keyboard=aoa ignored)"); options->keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_SDK; } @@ -813,8 +813,8 @@ end: // end-of-stream #ifdef HAVE_USB if (aoa_hid_initialized) { - if (hid_keyboard_initialized) { - sc_hid_keyboard_destroy(&s->keyboard_hid); + if (keyboard_aoa_initialized) { + sc_keyboard_aoa_destroy(&s->keyboard_aoa); } if (hid_mouse_initialized) { sc_hid_mouse_destroy(&s->mouse_hid); diff --git a/app/src/usb/keyboard_aoa.c b/app/src/usb/keyboard_aoa.c new file mode 100644 index 00000000..b69d6cd8 --- /dev/null +++ b/app/src/usb/keyboard_aoa.c @@ -0,0 +1,109 @@ +#include "keyboard_aoa.h" + +#include + +#include "input_events.h" +#include "util/log.h" + +/** Downcast key processor to keyboard_aoa */ +#define DOWNCAST(KP) container_of(KP, struct sc_keyboard_aoa, key_processor) + +#define HID_KEYBOARD_ACCESSORY_ID 1 + +static bool +push_mod_lock_state(struct sc_keyboard_aoa *kb, uint16_t mods_state) { + struct sc_hid_event hid_event; + if (!sc_hid_keyboard_event_from_mods(&hid_event, mods_state)) { + // Nothing to do + return true; + } + + if (!sc_aoa_push_hid_event(kb->aoa, HID_KEYBOARD_ACCESSORY_ID, + &hid_event)) { + LOGW("Could not request HID event (mod lock state)"); + return false; + } + + LOGD("HID keyboard state synchronized"); + + return true; +} + +static void +sc_key_processor_process_key(struct sc_key_processor *kp, + const struct sc_key_event *event, + uint64_t ack_to_wait) { + if (event->repeat) { + // In USB HID protocol, key repeat is handled by the host (Android), so + // just ignore key repeat here. + return; + } + + struct sc_keyboard_aoa *kb = DOWNCAST(kp); + + struct sc_hid_event hid_event; + + // Not all keys are supported, just ignore unsupported keys + if (sc_hid_keyboard_event_from_key(&kb->hid, &hid_event, event)) { + if (!kb->mod_lock_synchronized) { + // Inject CAPSLOCK and/or NUMLOCK if necessary to synchronize + // keyboard state + if (push_mod_lock_state(kb, event->mods_state)) { + kb->mod_lock_synchronized = true; + } + } + + // If ack_to_wait is != SC_SEQUENCE_INVALID, then Ctrl+v is pressed, so + // clipboard synchronization has been requested. Wait until clipboard + // synchronization is acknowledged by the server, otherwise it could + // paste the old clipboard content. + + if (!sc_aoa_push_hid_event_with_ack_to_wait(kb->aoa, + HID_KEYBOARD_ACCESSORY_ID, + &hid_event, + ack_to_wait)) { + LOGW("Could not request HID event (key)"); + } + } +} + +bool +sc_keyboard_aoa_init(struct sc_keyboard_aoa *kb, struct sc_aoa *aoa) { + kb->aoa = aoa; + + bool ok = sc_aoa_setup_hid(aoa, HID_KEYBOARD_ACCESSORY_ID, + SC_HID_KEYBOARD_REPORT_DESC, + SC_HID_KEYBOARD_REPORT_DESC_LEN); + if (!ok) { + LOGW("Register HID keyboard failed"); + return false; + } + + sc_hid_keyboard_init(&kb->hid); + + kb->mod_lock_synchronized = false; + + static const struct sc_key_processor_ops ops = { + .process_key = sc_key_processor_process_key, + // Never forward text input via HID (all the keys are injected + // separately) + .process_text = NULL, + }; + + // Clipboard synchronization is requested over the control socket, while HID + // events are sent over AOA, so it must wait for clipboard synchronization + // to be acknowledged by the device before injecting Ctrl+v. + kb->key_processor.async_paste = true; + kb->key_processor.ops = &ops; + + return true; +} + +void +sc_keyboard_aoa_destroy(struct sc_keyboard_aoa *kb) { + // Unregister HID keyboard so the soft keyboard shows again on Android + bool ok = sc_aoa_unregister_hid(kb->aoa, HID_KEYBOARD_ACCESSORY_ID); + if (!ok) { + LOGW("Could not unregister HID keyboard"); + } +} diff --git a/app/src/usb/keyboard_aoa.h b/app/src/usb/keyboard_aoa.h new file mode 100644 index 00000000..565b9177 --- /dev/null +++ b/app/src/usb/keyboard_aoa.h @@ -0,0 +1,27 @@ +#ifndef SC_KEYBOARD_AOA_H +#define SC_KEYBOARD_AOA_H + +#include "common.h" + +#include + +#include "aoa_hid.h" +#include "hid/hid_keyboard.h" +#include "trait/key_processor.h" + +struct sc_keyboard_aoa { + struct sc_key_processor key_processor; // key processor trait + + struct sc_hid_keyboard hid; + struct sc_aoa *aoa; + + bool mod_lock_synchronized; +}; + +bool +sc_keyboard_aoa_init(struct sc_keyboard_aoa *kb, struct sc_aoa *aoa); + +void +sc_keyboard_aoa_destroy(struct sc_keyboard_aoa *kb); + +#endif diff --git a/app/src/usb/scrcpy_otg.c b/app/src/usb/scrcpy_otg.c index 5955e909..9064ad10 100644 --- a/app/src/usb/scrcpy_otg.c +++ b/app/src/usb/scrcpy_otg.c @@ -10,7 +10,7 @@ struct scrcpy_otg { struct sc_usb usb; struct sc_aoa aoa; - struct sc_hid_keyboard keyboard; + struct sc_keyboard_aoa keyboard; struct sc_hid_mouse mouse; struct sc_screen_otg screen_otg; @@ -73,7 +73,7 @@ scrcpy_otg(struct scrcpy_options *options) { enum scrcpy_exit_code ret = SCRCPY_EXIT_FAILURE; - struct sc_hid_keyboard *keyboard = NULL; + struct sc_keyboard_aoa *keyboard = NULL; struct sc_hid_mouse *mouse = NULL; bool usb_device_initialized = false; bool usb_connected = false; @@ -128,7 +128,7 @@ scrcpy_otg(struct scrcpy_options *options) { options->mouse_input_mode == SC_MOUSE_INPUT_MODE_AOA; if (enable_keyboard) { - ok = sc_hid_keyboard_init(&s->keyboard, &s->aoa); + ok = sc_keyboard_aoa_init(&s->keyboard, &s->aoa); if (!ok) { goto end; } @@ -188,7 +188,7 @@ end: sc_hid_mouse_destroy(&s->mouse); } if (keyboard) { - sc_hid_keyboard_destroy(&s->keyboard); + sc_keyboard_aoa_destroy(&s->keyboard); } if (aoa_initialized) { diff --git a/app/src/usb/screen_otg.h b/app/src/usb/screen_otg.h index a0acf40b..cfc3bfa2 100644 --- a/app/src/usb/screen_otg.h +++ b/app/src/usb/screen_otg.h @@ -6,11 +6,11 @@ #include #include -#include "hid_keyboard.h" +#include "keyboard_aoa.h" #include "hid_mouse.h" struct sc_screen_otg { - struct sc_hid_keyboard *keyboard; + struct sc_keyboard_aoa *keyboard; struct sc_hid_mouse *mouse; SDL_Window *window; @@ -22,7 +22,7 @@ struct sc_screen_otg { }; struct sc_screen_otg_params { - struct sc_hid_keyboard *keyboard; + struct sc_keyboard_aoa *keyboard; struct sc_hid_mouse *mouse; const char *window_title; From d95276467b93eb4b045fd15000eacaecf7c1874c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 25 Jan 2024 23:04:09 +0100 Subject: [PATCH 1778/2244] Extract mouse HID handling Split the mouse implementation using AOA and the code handling HID events, so that HID events can be reused for another protocol (UHID). PR #4473 --- app/meson.build | 3 +- app/src/{usb => hid}/hid_mouse.c | 113 +++++------------------ app/src/hid/hid_mouse.h | 26 ++++++ app/src/scrcpy.c | 32 +++---- app/src/usb/mouse_aoa.c | 89 ++++++++++++++++++ app/src/usb/{hid_mouse.h => mouse_aoa.h} | 10 +- app/src/usb/scrcpy_otg.c | 8 +- app/src/usb/screen_otg.h | 6 +- 8 files changed, 169 insertions(+), 118 deletions(-) rename app/src/{usb => hid}/hid_mouse.c (59%) create mode 100644 app/src/hid/hid_mouse.h create mode 100644 app/src/usb/mouse_aoa.c rename app/src/usb/{hid_mouse.h => mouse_aoa.h} (55%) diff --git a/app/meson.build b/app/meson.build index 6d572b7b..f78afa15 100644 --- a/app/meson.build +++ b/app/meson.build @@ -32,6 +32,7 @@ src = [ 'src/server.c', 'src/version.c', 'src/hid/hid_keyboard.c', + 'src/hid/hid_mouse.c', 'src/trait/frame_source.c', 'src/trait/packet_source.c', 'src/util/acksync.c', @@ -90,7 +91,7 @@ if usb_support src += [ 'src/usb/aoa_hid.c', 'src/usb/keyboard_aoa.c', - 'src/usb/hid_mouse.c', + 'src/usb/mouse_aoa.c', 'src/usb/scrcpy_otg.c', 'src/usb/screen_otg.c', 'src/usb/usb.c', diff --git a/app/src/usb/hid_mouse.c b/app/src/hid/hid_mouse.c similarity index 59% rename from app/src/usb/hid_mouse.c rename to app/src/hid/hid_mouse.c index de961265..9d814448 100644 --- a/app/src/usb/hid_mouse.c +++ b/app/src/hid/hid_mouse.c @@ -1,15 +1,5 @@ #include "hid_mouse.h" -#include - -#include "input_events.h" -#include "util/log.h" - -/** Downcast mouse processor to hid_mouse */ -#define DOWNCAST(MP) container_of(MP, struct sc_hid_mouse, mouse_processor) - -#define HID_MOUSE_ACCESSORY_ID 2 - // 1 byte for buttons + padding, 1 byte for X position, 1 byte for Y position, // 1 byte for wheel motion #define HID_MOUSE_EVENT_SIZE 4 @@ -24,7 +14,7 @@ * * §4 Generic Desktop Page (0x01) (p26) */ -static const unsigned char mouse_report_desc[] = { +const uint8_t SC_HID_MOUSE_REPORT_DESC[] = { // Usage Page (Generic Desktop) 0x05, 0x01, // Usage (Mouse) @@ -90,6 +80,9 @@ static const unsigned char mouse_report_desc[] = { 0xC0, }; +const size_t SC_HID_MOUSE_REPORT_DESC_LEN = + sizeof(SC_HID_MOUSE_REPORT_DESC); + /** * A mouse HID event is 4 bytes long: * @@ -138,9 +131,9 @@ sc_hid_mouse_event_init(struct sc_hid_event *hid_event) { // callers } -static unsigned char -buttons_state_to_hid_buttons(uint8_t buttons_state) { - unsigned char c = 0; +static uint8_t +sc_hid_buttons_from_buttons_state(uint8_t buttons_state) { + uint8_t c = 0; if (buttons_state & SC_MOUSE_BUTTON_LEFT) { c |= 1 << 0; } @@ -159,55 +152,36 @@ buttons_state_to_hid_buttons(uint8_t buttons_state) { return c; } -static void -sc_mouse_processor_process_mouse_motion(struct sc_mouse_processor *mp, - const struct sc_mouse_motion_event *event) { - struct sc_hid_mouse *mouse = DOWNCAST(mp); +void +sc_hid_mouse_event_from_motion(struct sc_hid_event *hid_event, + const struct sc_mouse_motion_event *event) { + sc_hid_mouse_event_init(hid_event); - struct sc_hid_event hid_event; - sc_hid_mouse_event_init(&hid_event); - - unsigned char *data = hid_event.data; - data[0] = buttons_state_to_hid_buttons(event->buttons_state); + uint8_t *data = hid_event->data; + data[0] = sc_hid_buttons_from_buttons_state(event->buttons_state); data[1] = CLAMP(event->xrel, -127, 127); data[2] = CLAMP(event->yrel, -127, 127); data[3] = 0; // wheel coordinates only used for scrolling - - if (!sc_aoa_push_hid_event(mouse->aoa, HID_MOUSE_ACCESSORY_ID, - &hid_event)) { - LOGW("Could not request HID event (mouse motion)"); - } } -static void -sc_mouse_processor_process_mouse_click(struct sc_mouse_processor *mp, - const struct sc_mouse_click_event *event) { - struct sc_hid_mouse *mouse = DOWNCAST(mp); +void +sc_hid_mouse_event_from_click(struct sc_hid_event *hid_event, + const struct sc_mouse_click_event *event) { + sc_hid_mouse_event_init(hid_event); - struct sc_hid_event hid_event; - sc_hid_mouse_event_init(&hid_event); - - unsigned char *data = hid_event.data; - data[0] = buttons_state_to_hid_buttons(event->buttons_state); + uint8_t *data = hid_event->data; + data[0] = sc_hid_buttons_from_buttons_state(event->buttons_state); data[1] = 0; // no x motion data[2] = 0; // no y motion data[3] = 0; // wheel coordinates only used for scrolling - - if (!sc_aoa_push_hid_event(mouse->aoa, HID_MOUSE_ACCESSORY_ID, - &hid_event)) { - LOGW("Could not request HID event (mouse click)"); - } } -static void -sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp, - const struct sc_mouse_scroll_event *event) { - struct sc_hid_mouse *mouse = DOWNCAST(mp); +void +sc_hid_mouse_event_from_scroll(struct sc_hid_event *hid_event, + const struct sc_mouse_scroll_event *event) { + sc_hid_mouse_event_init(hid_event); - struct sc_hid_event hid_event; - sc_hid_mouse_event_init(&hid_event); - - unsigned char *data = hid_event.data; + uint8_t *data = hid_event->data; data[0] = 0; // buttons state irrelevant (and unknown) data[1] = 0; // no x motion data[2] = 0; // no y motion @@ -215,43 +189,4 @@ sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp, // are possible data[3] = CLAMP(event->vscroll, -127, 127); // Horizontal scrolling ignored - - if (!sc_aoa_push_hid_event(mouse->aoa, HID_MOUSE_ACCESSORY_ID, - &hid_event)) { - LOGW("Could not request HID event (mouse scroll)"); - } -} - -bool -sc_hid_mouse_init(struct sc_hid_mouse *mouse, struct sc_aoa *aoa) { - mouse->aoa = aoa; - - bool ok = sc_aoa_setup_hid(aoa, HID_MOUSE_ACCESSORY_ID, mouse_report_desc, - ARRAY_LEN(mouse_report_desc)); - if (!ok) { - LOGW("Register HID mouse failed"); - return false; - } - - static const struct sc_mouse_processor_ops ops = { - .process_mouse_motion = sc_mouse_processor_process_mouse_motion, - .process_mouse_click = sc_mouse_processor_process_mouse_click, - .process_mouse_scroll = sc_mouse_processor_process_mouse_scroll, - // Touch events not supported (coordinates are not relative) - .process_touch = NULL, - }; - - mouse->mouse_processor.ops = &ops; - - mouse->mouse_processor.relative_mode = true; - - return true; -} - -void -sc_hid_mouse_destroy(struct sc_hid_mouse *mouse) { - bool ok = sc_aoa_unregister_hid(mouse->aoa, HID_MOUSE_ACCESSORY_ID); - if (!ok) { - LOGW("Could not unregister HID mouse"); - } } diff --git a/app/src/hid/hid_mouse.h b/app/src/hid/hid_mouse.h new file mode 100644 index 00000000..e514d7d9 --- /dev/null +++ b/app/src/hid/hid_mouse.h @@ -0,0 +1,26 @@ +#ifndef SC_HID_MOUSE_H +#define SC_HID_MOUSE_H + +#endif + +#include "common.h" + +#include + +#include "hid/hid_event.h" +#include "input_events.h" + +extern const uint8_t SC_HID_MOUSE_REPORT_DESC[]; +extern const size_t SC_HID_MOUSE_REPORT_DESC_LEN; + +void +sc_hid_mouse_event_from_motion(struct sc_hid_event *hid_event, + const struct sc_mouse_motion_event *event); + +void +sc_hid_mouse_event_from_click(struct sc_hid_event *hid_event, + const struct sc_mouse_click_event *event); + +void +sc_hid_mouse_event_from_scroll(struct sc_hid_event *hid_event, + const struct sc_mouse_scroll_event *event); diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 1d5e67dc..bd448052 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -28,7 +28,7 @@ #ifdef HAVE_USB # include "usb/aoa_hid.h" # include "usb/keyboard_aoa.h" -# include "usb/hid_mouse.h" +# include "usb/mouse_aoa.h" # include "usb/usb.h" #endif #include "util/acksync.h" @@ -71,7 +71,7 @@ struct scrcpy { union { struct sc_mouse_inject mouse_inject; #ifdef HAVE_USB - struct sc_hid_mouse mouse_hid; + struct sc_mouse_aoa mouse_aoa; #endif }; struct sc_timeout timeout; @@ -331,7 +331,7 @@ scrcpy(struct scrcpy_options *options) { #ifdef HAVE_USB bool aoa_hid_initialized = false; bool keyboard_aoa_initialized = false; - bool hid_mouse_initialized = false; + bool mouse_aoa_initialized = false; #endif bool controller_initialized = false; bool controller_started = false; @@ -545,9 +545,9 @@ scrcpy(struct scrcpy_options *options) { #ifdef HAVE_USB bool use_keyboard_aoa = options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_AOA; - bool use_aoa_mouse = + bool use_mouse_aoa = options->mouse_input_mode == SC_MOUSE_INPUT_MODE_AOA; - if (use_keyboard_aoa || use_aoa_mouse) { + if (use_keyboard_aoa || use_mouse_aoa) { bool ok = sc_acksync_init(&s->acksync); if (!ok) { goto end; @@ -599,16 +599,16 @@ scrcpy(struct scrcpy_options *options) { } } - if (use_aoa_mouse) { - if (sc_hid_mouse_init(&s->mouse_hid, &s->aoa)) { - hid_mouse_initialized = true; - mp = &s->mouse_hid.mouse_processor; + if (use_mouse_aoa) { + if (sc_mouse_aoa_init(&s->mouse_aoa, &s->aoa)) { + mouse_aoa_initialized = true; + mp = &s->mouse_aoa.mouse_processor; } else { LOGE("Could not initialized HID mouse"); } } - bool need_aoa = keyboard_aoa_initialized || hid_mouse_initialized; + bool need_aoa = keyboard_aoa_initialized || mouse_aoa_initialized; if (!need_aoa || !sc_aoa_start(&s->aoa)) { sc_acksync_destroy(&s->acksync); @@ -628,9 +628,9 @@ aoa_hid_end: sc_keyboard_aoa_destroy(&s->keyboard_aoa); keyboard_aoa_initialized = false; } - if (hid_mouse_initialized) { - sc_hid_mouse_destroy(&s->mouse_hid); - hid_mouse_initialized = false; + if (mouse_aoa_initialized) { + sc_mouse_aoa_destroy(&s->mouse_aoa); + mouse_aoa_initialized = false; } } @@ -639,7 +639,7 @@ aoa_hid_end: options->keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_SDK; } - if (use_aoa_mouse && !hid_mouse_initialized) { + if (use_mouse_aoa && !mouse_aoa_initialized) { LOGE("Fallback to --keyboard=sdk (--keyboard=aoa ignored)"); options->mouse_input_mode = SC_MOUSE_INPUT_MODE_SDK; } @@ -816,8 +816,8 @@ end: if (keyboard_aoa_initialized) { sc_keyboard_aoa_destroy(&s->keyboard_aoa); } - if (hid_mouse_initialized) { - sc_hid_mouse_destroy(&s->mouse_hid); + if (mouse_aoa_initialized) { + sc_mouse_aoa_destroy(&s->mouse_aoa); } sc_aoa_stop(&s->aoa); sc_usb_stop(&s->usb); diff --git a/app/src/usb/mouse_aoa.c b/app/src/usb/mouse_aoa.c new file mode 100644 index 00000000..93b32328 --- /dev/null +++ b/app/src/usb/mouse_aoa.c @@ -0,0 +1,89 @@ +#include "mouse_aoa.h" + +#include + +#include "hid/hid_mouse.h" +#include "input_events.h" +#include "util/log.h" + +/** Downcast mouse processor to mouse_aoa */ +#define DOWNCAST(MP) container_of(MP, struct sc_mouse_aoa, mouse_processor) + +#define HID_MOUSE_ACCESSORY_ID 2 + +static void +sc_mouse_processor_process_mouse_motion(struct sc_mouse_processor *mp, + const struct sc_mouse_motion_event *event) { + struct sc_mouse_aoa *mouse = DOWNCAST(mp); + + struct sc_hid_event hid_event; + sc_hid_mouse_event_from_motion(&hid_event, event); + + if (!sc_aoa_push_hid_event(mouse->aoa, HID_MOUSE_ACCESSORY_ID, + &hid_event)) { + LOGW("Could not request HID event (mouse motion)"); + } +} + +static void +sc_mouse_processor_process_mouse_click(struct sc_mouse_processor *mp, + const struct sc_mouse_click_event *event) { + struct sc_mouse_aoa *mouse = DOWNCAST(mp); + + struct sc_hid_event hid_event; + sc_hid_mouse_event_from_click(&hid_event, event); + + if (!sc_aoa_push_hid_event(mouse->aoa, HID_MOUSE_ACCESSORY_ID, + &hid_event)) { + LOGW("Could not request HID event (mouse click)"); + } +} + +static void +sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp, + const struct sc_mouse_scroll_event *event) { + struct sc_mouse_aoa *mouse = DOWNCAST(mp); + + struct sc_hid_event hid_event; + sc_hid_mouse_event_from_scroll(&hid_event, event); + + if (!sc_aoa_push_hid_event(mouse->aoa, HID_MOUSE_ACCESSORY_ID, + &hid_event)) { + LOGW("Could not request HID event (mouse scroll)"); + } +} + +bool +sc_mouse_aoa_init(struct sc_mouse_aoa *mouse, struct sc_aoa *aoa) { + mouse->aoa = aoa; + + bool ok = sc_aoa_setup_hid(aoa, HID_MOUSE_ACCESSORY_ID, + SC_HID_MOUSE_REPORT_DESC, + SC_HID_MOUSE_REPORT_DESC_LEN); + if (!ok) { + LOGW("Register HID mouse failed"); + return false; + } + + static const struct sc_mouse_processor_ops ops = { + .process_mouse_motion = sc_mouse_processor_process_mouse_motion, + .process_mouse_click = sc_mouse_processor_process_mouse_click, + .process_mouse_scroll = sc_mouse_processor_process_mouse_scroll, + // Touch events not supported (coordinates are not relative) + .process_touch = NULL, + }; + + mouse->mouse_processor.ops = &ops; + + mouse->mouse_processor.relative_mode = true; + + return true; +} + +void +sc_mouse_aoa_destroy(struct sc_mouse_aoa *mouse) { + bool ok = sc_aoa_unregister_hid(mouse->aoa, HID_MOUSE_ACCESSORY_ID); + if (!ok) { + LOGW("Could not unregister HID mouse"); + } +} diff --git a/app/src/usb/hid_mouse.h b/app/src/usb/mouse_aoa.h similarity index 55% rename from app/src/usb/hid_mouse.h rename to app/src/usb/mouse_aoa.h index b89f7795..afaed761 100644 --- a/app/src/usb/hid_mouse.h +++ b/app/src/usb/mouse_aoa.h @@ -1,5 +1,5 @@ -#ifndef SC_HID_MOUSE_H -#define SC_HID_MOUSE_H +#ifndef SC_MOUSE_AOA_H +#define SC_MOUSE_AOA_H #include "common.h" @@ -8,16 +8,16 @@ #include "aoa_hid.h" #include "trait/mouse_processor.h" -struct sc_hid_mouse { +struct sc_mouse_aoa { struct sc_mouse_processor mouse_processor; // mouse processor trait struct sc_aoa *aoa; }; bool -sc_hid_mouse_init(struct sc_hid_mouse *mouse, struct sc_aoa *aoa); +sc_mouse_aoa_init(struct sc_mouse_aoa *mouse, struct sc_aoa *aoa); void -sc_hid_mouse_destroy(struct sc_hid_mouse *mouse); +sc_mouse_aoa_destroy(struct sc_mouse_aoa *mouse); #endif diff --git a/app/src/usb/scrcpy_otg.c b/app/src/usb/scrcpy_otg.c index 9064ad10..c1d38da3 100644 --- a/app/src/usb/scrcpy_otg.c +++ b/app/src/usb/scrcpy_otg.c @@ -11,7 +11,7 @@ struct scrcpy_otg { struct sc_usb usb; struct sc_aoa aoa; struct sc_keyboard_aoa keyboard; - struct sc_hid_mouse mouse; + struct sc_mouse_aoa mouse; struct sc_screen_otg screen_otg; }; @@ -74,7 +74,7 @@ scrcpy_otg(struct scrcpy_options *options) { enum scrcpy_exit_code ret = SCRCPY_EXIT_FAILURE; struct sc_keyboard_aoa *keyboard = NULL; - struct sc_hid_mouse *mouse = NULL; + struct sc_mouse_aoa *mouse = NULL; bool usb_device_initialized = false; bool usb_connected = false; bool aoa_started = false; @@ -136,7 +136,7 @@ scrcpy_otg(struct scrcpy_options *options) { } if (enable_mouse) { - ok = sc_hid_mouse_init(&s->mouse, &s->aoa); + ok = sc_mouse_aoa_init(&s->mouse, &s->aoa); if (!ok) { goto end; } @@ -185,7 +185,7 @@ end: sc_usb_stop(&s->usb); if (mouse) { - sc_hid_mouse_destroy(&s->mouse); + sc_mouse_aoa_destroy(&s->mouse); } if (keyboard) { sc_keyboard_aoa_destroy(&s->keyboard); diff --git a/app/src/usb/screen_otg.h b/app/src/usb/screen_otg.h index cfc3bfa2..c4e03b87 100644 --- a/app/src/usb/screen_otg.h +++ b/app/src/usb/screen_otg.h @@ -7,11 +7,11 @@ #include #include "keyboard_aoa.h" -#include "hid_mouse.h" +#include "mouse_aoa.h" struct sc_screen_otg { struct sc_keyboard_aoa *keyboard; - struct sc_hid_mouse *mouse; + struct sc_mouse_aoa *mouse; SDL_Window *window; SDL_Renderer *renderer; @@ -23,7 +23,7 @@ struct sc_screen_otg { struct sc_screen_otg_params { struct sc_keyboard_aoa *keyboard; - struct sc_hid_mouse *mouse; + struct sc_mouse_aoa *mouse; const char *window_title; bool always_on_top; From 2e7f6a6fc4b3bf8347b7bba85335cb005d0fc63e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 25 Jan 2024 23:08:20 +0100 Subject: [PATCH 1779/2244] Rename default keyboard implementation to "sdk" Rename {keyboard,mouse}_inject to {keyboard,mouse}_sdk. All implementations "inject" key events and mouse events, what differs is the mechanism. For these implementations, the Android SDK API is used to inject events. Note that the input mode enum variants were already renamed (SC_KEYBOARD_INPUT_MODE_SDK and SC_MOUSE_INPUT_MODE_SDK). PR #4473 --- app/meson.build | 4 +- app/src/{keyboard_inject.c => keyboard_sdk.c} | 46 +++++++++---------- app/src/{keyboard_inject.h => keyboard_sdk.h} | 14 +++--- app/src/{mouse_inject.c => mouse_sdk.c} | 31 ++++++------- app/src/{mouse_inject.h => mouse_sdk.h} | 9 ++-- app/src/scrcpy.c | 20 ++++---- 6 files changed, 61 insertions(+), 63 deletions(-) rename app/src/{keyboard_inject.c => keyboard_sdk.c} (91%) rename app/src/{keyboard_inject.h => keyboard_sdk.h} (61%) rename app/src/{mouse_inject.c => mouse_sdk.c} (84%) rename app/src/{mouse_inject.h => mouse_sdk.h} (58%) diff --git a/app/meson.build b/app/meson.build index f78afa15..3ec9781a 100644 --- a/app/meson.build +++ b/app/meson.build @@ -20,8 +20,8 @@ src = [ 'src/fps_counter.c', 'src/frame_buffer.c', 'src/input_manager.c', - 'src/keyboard_inject.c', - 'src/mouse_inject.c', + 'src/keyboard_sdk.c', + 'src/mouse_sdk.c', 'src/opengl.c', 'src/options.c', 'src/packet_merger.c', diff --git a/app/src/keyboard_inject.c b/app/src/keyboard_sdk.c similarity index 91% rename from app/src/keyboard_inject.c rename to app/src/keyboard_sdk.c index fe297310..726f65a9 100644 --- a/app/src/keyboard_inject.c +++ b/app/src/keyboard_sdk.c @@ -1,4 +1,4 @@ -#include "keyboard_inject.h" +#include "keyboard_sdk.h" #include @@ -9,8 +9,8 @@ #include "util/intmap.h" #include "util/log.h" -/** Downcast key processor to sc_keyboard_inject */ -#define DOWNCAST(KP) container_of(KP, struct sc_keyboard_inject, key_processor) +/** Downcast key processor to sc_keyboard_sdk */ +#define DOWNCAST(KP) container_of(KP, struct sc_keyboard_sdk, key_processor) static enum android_keyevent_action convert_keycode_action(enum sc_action action) { @@ -271,20 +271,20 @@ sc_key_processor_process_key(struct sc_key_processor *kp, // is set before injecting Ctrl+v. (void) ack_to_wait; - struct sc_keyboard_inject *ki = DOWNCAST(kp); + struct sc_keyboard_sdk *kb = DOWNCAST(kp); if (event->repeat) { - if (!ki->forward_key_repeat) { + if (!kb->forward_key_repeat) { return; } - ++ki->repeat; + ++kb->repeat; } else { - ki->repeat = 0; + kb->repeat = 0; } struct sc_control_msg msg; - if (convert_input_key(event, &msg, ki->key_inject_mode, ki->repeat)) { - if (!sc_controller_push_msg(ki->controller, &msg)) { + if (convert_input_key(event, &msg, kb->key_inject_mode, kb->repeat)) { + if (!sc_controller_push_msg(kb->controller, &msg)) { LOGW("Could not request 'inject keycode'"); } } @@ -293,14 +293,14 @@ sc_key_processor_process_key(struct sc_key_processor *kp, static void sc_key_processor_process_text(struct sc_key_processor *kp, const struct sc_text_event *event) { - struct sc_keyboard_inject *ki = DOWNCAST(kp); + struct sc_keyboard_sdk *kb = DOWNCAST(kp); - if (ki->key_inject_mode == SC_KEY_INJECT_MODE_RAW) { + if (kb->key_inject_mode == SC_KEY_INJECT_MODE_RAW) { // Never inject text events return; } - if (ki->key_inject_mode == SC_KEY_INJECT_MODE_MIXED) { + if (kb->key_inject_mode == SC_KEY_INJECT_MODE_MIXED) { char c = event->text[0]; if (isalpha(c) || c == ' ') { assert(event->text[1] == '\0'); @@ -316,22 +316,22 @@ sc_key_processor_process_text(struct sc_key_processor *kp, LOGW("Could not strdup input text"); return; } - if (!sc_controller_push_msg(ki->controller, &msg)) { + if (!sc_controller_push_msg(kb->controller, &msg)) { free(msg.inject_text.text); LOGW("Could not request 'inject text'"); } } void -sc_keyboard_inject_init(struct sc_keyboard_inject *ki, - struct sc_controller *controller, - enum sc_key_inject_mode key_inject_mode, - bool forward_key_repeat) { - ki->controller = controller; - ki->key_inject_mode = key_inject_mode; - ki->forward_key_repeat = forward_key_repeat; +sc_keyboard_sdk_init(struct sc_keyboard_sdk *kb, + struct sc_controller *controller, + enum sc_key_inject_mode key_inject_mode, + bool forward_key_repeat) { + kb->controller = controller; + kb->key_inject_mode = key_inject_mode; + kb->forward_key_repeat = forward_key_repeat; - ki->repeat = 0; + kb->repeat = 0; static const struct sc_key_processor_ops ops = { .process_key = sc_key_processor_process_key, @@ -339,6 +339,6 @@ sc_keyboard_inject_init(struct sc_keyboard_inject *ki, }; // Key injection and clipboard synchronization are serialized - ki->key_processor.async_paste = false; - ki->key_processor.ops = &ops; + kb->key_processor.async_paste = false; + kb->key_processor.ops = &ops; } diff --git a/app/src/keyboard_inject.h b/app/src/keyboard_sdk.h similarity index 61% rename from app/src/keyboard_inject.h rename to app/src/keyboard_sdk.h index b7781c1f..700ba90b 100644 --- a/app/src/keyboard_inject.h +++ b/app/src/keyboard_sdk.h @@ -1,5 +1,5 @@ -#ifndef SC_KEYBOARD_INJECT_H -#define SC_KEYBOARD_INJECT_H +#ifndef SC_KEYBOARD_SDK_H +#define SC_KEYBOARD_SDK_H #include "common.h" @@ -9,7 +9,7 @@ #include "options.h" #include "trait/key_processor.h" -struct sc_keyboard_inject { +struct sc_keyboard_sdk { struct sc_key_processor key_processor; // key processor trait struct sc_controller *controller; @@ -23,9 +23,9 @@ struct sc_keyboard_inject { }; void -sc_keyboard_inject_init(struct sc_keyboard_inject *ki, - struct sc_controller *controller, - enum sc_key_inject_mode key_inject_mode, - bool forward_key_repeat); +sc_keyboard_sdk_init(struct sc_keyboard_sdk *kb, + struct sc_controller *controller, + enum sc_key_inject_mode key_inject_mode, + bool forward_key_repeat); #endif diff --git a/app/src/mouse_inject.c b/app/src/mouse_sdk.c similarity index 84% rename from app/src/mouse_inject.c rename to app/src/mouse_sdk.c index 71b7a64d..620fb52c 100644 --- a/app/src/mouse_inject.c +++ b/app/src/mouse_sdk.c @@ -1,4 +1,4 @@ -#include "mouse_inject.h" +#include "mouse_sdk.h" #include @@ -9,8 +9,8 @@ #include "util/intmap.h" #include "util/log.h" -/** Downcast mouse processor to sc_mouse_inject */ -#define DOWNCAST(MP) container_of(MP, struct sc_mouse_inject, mouse_processor) +/** Downcast mouse processor to sc_mouse_sdk */ +#define DOWNCAST(MP) container_of(MP, struct sc_mouse_sdk, mouse_processor) static enum android_motionevent_buttons convert_mouse_buttons(uint32_t state) { @@ -63,7 +63,7 @@ sc_mouse_processor_process_mouse_motion(struct sc_mouse_processor *mp, return; } - struct sc_mouse_inject *mi = DOWNCAST(mp); + struct sc_mouse_sdk *m = DOWNCAST(mp); struct sc_control_msg msg = { .type = SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, @@ -76,7 +76,7 @@ sc_mouse_processor_process_mouse_motion(struct sc_mouse_processor *mp, }, }; - if (!sc_controller_push_msg(mi->controller, &msg)) { + if (!sc_controller_push_msg(m->controller, &msg)) { LOGW("Could not request 'inject mouse motion event'"); } } @@ -84,7 +84,7 @@ sc_mouse_processor_process_mouse_motion(struct sc_mouse_processor *mp, static void sc_mouse_processor_process_mouse_click(struct sc_mouse_processor *mp, const struct sc_mouse_click_event *event) { - struct sc_mouse_inject *mi = DOWNCAST(mp); + struct sc_mouse_sdk *m = DOWNCAST(mp); struct sc_control_msg msg = { .type = SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, @@ -98,7 +98,7 @@ sc_mouse_processor_process_mouse_click(struct sc_mouse_processor *mp, }, }; - if (!sc_controller_push_msg(mi->controller, &msg)) { + if (!sc_controller_push_msg(m->controller, &msg)) { LOGW("Could not request 'inject mouse click event'"); } } @@ -106,7 +106,7 @@ sc_mouse_processor_process_mouse_click(struct sc_mouse_processor *mp, static void sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp, const struct sc_mouse_scroll_event *event) { - struct sc_mouse_inject *mi = DOWNCAST(mp); + struct sc_mouse_sdk *m = DOWNCAST(mp); struct sc_control_msg msg = { .type = SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT, @@ -118,7 +118,7 @@ sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp, }, }; - if (!sc_controller_push_msg(mi->controller, &msg)) { + if (!sc_controller_push_msg(m->controller, &msg)) { LOGW("Could not request 'inject mouse scroll event'"); } } @@ -126,7 +126,7 @@ sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp, static void sc_mouse_processor_process_touch(struct sc_mouse_processor *mp, const struct sc_touch_event *event) { - struct sc_mouse_inject *mi = DOWNCAST(mp); + struct sc_mouse_sdk *m = DOWNCAST(mp); struct sc_control_msg msg = { .type = SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, @@ -139,15 +139,14 @@ sc_mouse_processor_process_touch(struct sc_mouse_processor *mp, }, }; - if (!sc_controller_push_msg(mi->controller, &msg)) { + if (!sc_controller_push_msg(m->controller, &msg)) { LOGW("Could not request 'inject touch event'"); } } void -sc_mouse_inject_init(struct sc_mouse_inject *mi, - struct sc_controller *controller) { - mi->controller = controller; +sc_mouse_sdk_init(struct sc_mouse_sdk *m, struct sc_controller *controller) { + m->controller = controller; static const struct sc_mouse_processor_ops ops = { .process_mouse_motion = sc_mouse_processor_process_mouse_motion, @@ -156,7 +155,7 @@ sc_mouse_inject_init(struct sc_mouse_inject *mi, .process_touch = sc_mouse_processor_process_touch, }; - mi->mouse_processor.ops = &ops; + m->mouse_processor.ops = &ops; - mi->mouse_processor.relative_mode = false; + m->mouse_processor.relative_mode = false; } diff --git a/app/src/mouse_inject.h b/app/src/mouse_sdk.h similarity index 58% rename from app/src/mouse_inject.h rename to app/src/mouse_sdk.h index 59a6a5d8..444a6ad5 100644 --- a/app/src/mouse_inject.h +++ b/app/src/mouse_sdk.h @@ -1,5 +1,5 @@ -#ifndef SC_MOUSE_INJECT_H -#define SC_MOUSE_INJECT_H +#ifndef SC_MOUSE_SDK_H +#define SC_MOUSE_SDK_H #include "common.h" @@ -9,14 +9,13 @@ #include "screen.h" #include "trait/mouse_processor.h" -struct sc_mouse_inject { +struct sc_mouse_sdk { struct sc_mouse_processor mouse_processor; // mouse processor trait struct sc_controller *controller; }; void -sc_mouse_inject_init(struct sc_mouse_inject *mi, - struct sc_controller *controller); +sc_mouse_sdk_init(struct sc_mouse_sdk *m, struct sc_controller *controller); #endif diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index bd448052..876b400a 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -20,8 +20,8 @@ #include "demuxer.h" #include "events.h" #include "file_pusher.h" -#include "keyboard_inject.h" -#include "mouse_inject.h" +#include "keyboard_sdk.h" +#include "mouse_sdk.h" #include "recorder.h" #include "screen.h" #include "server.h" @@ -63,13 +63,13 @@ struct scrcpy { struct sc_acksync acksync; #endif union { - struct sc_keyboard_inject keyboard_inject; + struct sc_keyboard_sdk keyboard_sdk; #ifdef HAVE_USB struct sc_keyboard_aoa keyboard_aoa; #endif }; union { - struct sc_mouse_inject mouse_inject; + struct sc_mouse_sdk mouse_sdk; #ifdef HAVE_USB struct sc_mouse_aoa mouse_aoa; #endif @@ -651,16 +651,16 @@ aoa_hid_end: // keyboard_input_mode may have been reset if HID mode failed if (options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_SDK) { - sc_keyboard_inject_init(&s->keyboard_inject, &s->controller, - options->key_inject_mode, - options->forward_key_repeat); - kp = &s->keyboard_inject.key_processor; + sc_keyboard_sdk_init(&s->keyboard_sdk, &s->controller, + options->key_inject_mode, + options->forward_key_repeat); + kp = &s->keyboard_sdk.key_processor; } // mouse_input_mode may have been reset if HID mode failed if (options->mouse_input_mode == SC_MOUSE_INPUT_MODE_SDK) { - sc_mouse_inject_init(&s->mouse_inject, &s->controller); - mp = &s->mouse_inject.mouse_processor; + sc_mouse_sdk_init(&s->mouse_sdk, &s->controller); + mp = &s->mouse_sdk.mouse_processor; } if (!sc_controller_init(&s->controller, s->server.control_socket, From 107f7a83abe60ff1a25db42aa864ed13f5a4c0c1 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 25 Feb 2024 14:54:17 +0100 Subject: [PATCH 1780/2244] Extract binary to hex string conversion PR #4473 --- app/src/usb/aoa_hid.c | 14 +++++--------- app/src/util/str.c | 20 ++++++++++++++++++++ app/src/util/str.h | 6 ++++++ 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/app/src/usb/aoa_hid.c b/app/src/usb/aoa_hid.c index d6b418a0..50bc33fe 100644 --- a/app/src/usb/aoa_hid.c +++ b/app/src/usb/aoa_hid.c @@ -5,6 +5,7 @@ #include "aoa_hid.h" #include "util/log.h" +#include "util/str.h" // See . #define ACCESSORY_REGISTER_HID 54 @@ -20,17 +21,12 @@ static void sc_hid_event_log(uint16_t accessory_id, const struct sc_hid_event *event) { // HID Event: [00] FF FF FF FF... assert(event->size); - unsigned buffer_size = event->size * 3 + 1; - char *buffer = malloc(buffer_size); - if (!buffer) { - LOG_OOM(); + char *hex = sc_str_to_hex_string(event->data, event->size); + if (!hex) { return; } - for (unsigned i = 0; i < event->size; ++i) { - snprintf(buffer + i * 3, 4, " %02x", event->data[i]); - } - LOGV("HID Event: [%d]%s", accessory_id, buffer); - free(buffer); + LOGV("HID Event: [%d] %s", accessory_id, hex); + free(hex); } bool diff --git a/app/src/util/str.c b/app/src/util/str.c index d78aa9d7..755369d8 100644 --- a/app/src/util/str.c +++ b/app/src/util/str.c @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -333,3 +334,22 @@ sc_str_remove_trailing_cr(char *s, size_t len) { } return len; } + +char * +sc_str_to_hex_string(const uint8_t *data, size_t size) { + size_t buffer_size = size * 3 + 1; + char *buffer = malloc(buffer_size); + if (!buffer) { + LOG_OOM(); + return NULL; + } + + for (size_t i = 0; i < size; ++i) { + snprintf(buffer + i * 3, 4, "%02X ", data[i]); + } + + // Remove the final space + buffer[size * 3] = '\0'; + + return buffer; +} diff --git a/app/src/util/str.h b/app/src/util/str.h index 4f7eeeda..20da26f0 100644 --- a/app/src/util/str.h +++ b/app/src/util/str.h @@ -138,4 +138,10 @@ sc_str_index_of_column(const char *s, unsigned col, const char *seps); size_t sc_str_remove_trailing_cr(char *s, size_t len); +/** + * Convert binary data to hexadecimal string + */ +char * +sc_str_to_hex_string(const uint8_t *data, size_t len); + #endif From 4d2c2514fc466fc1dd8cfb972c2697a843e28d70 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 24 Feb 2024 22:33:48 +0100 Subject: [PATCH 1781/2244] Initialize controller in two steps There is a dependency cycle in the initialization order: - keyboard depends on controller - controller depends on acksync - acksync depends on keyboard initialization To break this cycle, bind the async instance to the controller in a second step. PR #4473 --- app/src/controller.c | 11 ++++++++--- app/src/controller.h | 7 +++++-- app/src/receiver.c | 5 ++--- app/src/receiver.h | 3 +-- app/src/scrcpy.c | 5 +++-- 5 files changed, 19 insertions(+), 12 deletions(-) diff --git a/app/src/controller.c b/app/src/controller.c index 250321fe..5a5bfde9 100644 --- a/app/src/controller.c +++ b/app/src/controller.c @@ -7,8 +7,7 @@ #define SC_CONTROL_MSG_QUEUE_MAX 64 bool -sc_controller_init(struct sc_controller *controller, sc_socket control_socket, - struct sc_acksync *acksync) { +sc_controller_init(struct sc_controller *controller, sc_socket control_socket) { sc_vecdeque_init(&controller->queue); bool ok = sc_vecdeque_reserve(&controller->queue, SC_CONTROL_MSG_QUEUE_MAX); @@ -16,7 +15,7 @@ sc_controller_init(struct sc_controller *controller, sc_socket control_socket, return false; } - ok = sc_receiver_init(&controller->receiver, control_socket, acksync); + ok = sc_receiver_init(&controller->receiver, control_socket); if (!ok) { sc_vecdeque_destroy(&controller->queue); return false; @@ -43,6 +42,12 @@ sc_controller_init(struct sc_controller *controller, sc_socket control_socket, return true; } +void +sc_controller_set_acksync(struct sc_controller *controller, + struct sc_acksync *acksync) { + controller->receiver.acksync = acksync; +} + void sc_controller_destroy(struct sc_controller *controller) { sc_cond_destroy(&controller->msg_cond); diff --git a/app/src/controller.h b/app/src/controller.h index a044b2bf..767e1731 100644 --- a/app/src/controller.h +++ b/app/src/controller.h @@ -25,8 +25,11 @@ struct sc_controller { }; bool -sc_controller_init(struct sc_controller *controller, sc_socket control_socket, - struct sc_acksync *acksync); +sc_controller_init(struct sc_controller *controller, sc_socket control_socket); + +void +sc_controller_set_acksync(struct sc_controller *controller, + struct sc_acksync *acksync); void sc_controller_destroy(struct sc_controller *controller); diff --git a/app/src/receiver.c b/app/src/receiver.c index 6be705e3..97299b3f 100644 --- a/app/src/receiver.c +++ b/app/src/receiver.c @@ -8,15 +8,14 @@ #include "util/log.h" bool -sc_receiver_init(struct sc_receiver *receiver, sc_socket control_socket, - struct sc_acksync *acksync) { +sc_receiver_init(struct sc_receiver *receiver, sc_socket control_socket) { bool ok = sc_mutex_init(&receiver->mutex); if (!ok) { return false; } receiver->control_socket = control_socket; - receiver->acksync = acksync; + receiver->acksync = NULL; return true; } diff --git a/app/src/receiver.h b/app/src/receiver.h index eb959fb8..43f89615 100644 --- a/app/src/receiver.h +++ b/app/src/receiver.h @@ -20,8 +20,7 @@ struct sc_receiver { }; bool -sc_receiver_init(struct sc_receiver *receiver, sc_socket control_socket, - struct sc_acksync *acksync); +sc_receiver_init(struct sc_receiver *receiver, sc_socket control_socket); void sc_receiver_destroy(struct sc_receiver *receiver); diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 876b400a..7ecda6d0 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -663,12 +663,13 @@ aoa_hid_end: mp = &s->mouse_sdk.mouse_processor; } - if (!sc_controller_init(&s->controller, s->server.control_socket, - acksync)) { + if (!sc_controller_init(&s->controller, s->server.control_socket)) { goto end; } controller_initialized = true; + sc_controller_set_acksync(&s->controller, acksync); + if (!sc_controller_start(&s->controller)) { goto end; } From 604e59ac7bd907567348627712185f1fa88e4659 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 24 Feb 2024 22:33:58 +0100 Subject: [PATCH 1782/2244] Initialize controller before keyboards The UHID keyboard initializer will need the controller. PR #4473 --- app/src/scrcpy.c | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 7ecda6d0..a407dff1 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -542,6 +542,13 @@ scrcpy(struct scrcpy_options *options) { struct sc_mouse_processor *mp = NULL; if (options->control) { + if (!sc_controller_init(&s->controller, s->server.control_socket)) { + goto end; + } + controller_initialized = true; + + controller = &s->controller; + #ifdef HAVE_USB bool use_keyboard_aoa = options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_AOA; @@ -663,18 +670,12 @@ aoa_hid_end: mp = &s->mouse_sdk.mouse_processor; } - if (!sc_controller_init(&s->controller, s->server.control_socket)) { - goto end; - } - controller_initialized = true; - sc_controller_set_acksync(&s->controller, acksync); if (!sc_controller_start(&s->controller)) { goto end; } controller_started = true; - controller = &s->controller; } // There is a controller if and only if control is enabled From 4d5b67cc8018753a02a786cd4ca27e38cad50d65 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 26 Feb 2024 10:20:01 +0100 Subject: [PATCH 1783/2244] Log controller handling errors On close, the controller is expected to throw an IOException because the socket is closed, so the exception was ignored. However, message handling actions may also throw IOException, and they must not be silently ignored. PR #4473 --- .../com/genymobile/scrcpy/Controller.java | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index 73d6ad57..a3508c96 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -81,8 +81,9 @@ public class Controller implements AsyncProcessor { SystemClock.sleep(500); } - while (!Thread.currentThread().isInterrupted()) { - handleEvent(); + boolean alive = true; + while (!Thread.currentThread().isInterrupted() && alive) { + alive = handleEvent(); } } @@ -92,7 +93,7 @@ public class Controller implements AsyncProcessor { try { control(); } catch (IOException e) { - // this is expected on close + Ln.e("Controller error", e); } finally { Ln.d("Controller stopped"); listener.onTerminated(true); @@ -122,8 +123,15 @@ public class Controller implements AsyncProcessor { return sender; } - private void handleEvent() throws IOException { - ControlMessage msg = controlChannel.recv(); + private boolean handleEvent() throws IOException { + ControlMessage msg; + try { + msg = controlChannel.recv(); + } catch (IOException e) { + // this is expected on close + return false; + } + switch (msg.getType()) { case ControlMessage.TYPE_INJECT_KEYCODE: if (device.supportsInputEvents()) { @@ -185,6 +193,8 @@ public class Controller implements AsyncProcessor { default: // do nothing } + + return true; } private boolean injectKeycode(int action, int keycode, int repeat, int metaState) { From 840680f546be59d1581ae4014a6546e137447cbc Mon Sep 17 00:00:00 2001 From: Simon Chan <1330321+yume-chan@users.noreply.github.com> Date: Tue, 28 Nov 2023 17:17:35 +0800 Subject: [PATCH 1784/2244] Add UHID keyboard support Use the following command: scrcpy --keyboard=uhid PR #4473 Co-authored-by: Romain Vimont Signed-off-by: Romain Vimont --- app/data/bash-completion/scrcpy | 2 +- app/data/zsh-completion/_scrcpy | 2 +- app/meson.build | 1 + app/scrcpy.1 | 7 +- app/src/cli.c | 25 +++- app/src/control_msg.c | 28 ++++ app/src/control_msg.h | 13 ++ app/src/options.h | 1 + app/src/scrcpy.c | 13 +- app/src/uhid/keyboard_uhid.c | 72 +++++++++ app/src/uhid/keyboard_uhid.h | 23 +++ app/tests/test_control_msg_serialize.c | 49 +++++++ .../com/genymobile/scrcpy/ControlMessage.java | 28 ++++ .../scrcpy/ControlMessageReader.java | 61 +++++++- .../com/genymobile/scrcpy/Controller.java | 10 ++ .../com/genymobile/scrcpy/UhidManager.java | 138 ++++++++++++++++++ .../scrcpy/ControlMessageReaderTest.java | 44 ++++++ 17 files changed, 497 insertions(+), 20 deletions(-) create mode 100644 app/src/uhid/keyboard_uhid.c create mode 100644 app/src/uhid/keyboard_uhid.h create mode 100644 server/src/main/java/com/genymobile/scrcpy/UhidManager.java diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index b2009c56..904ccdeb 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -116,7 +116,7 @@ _scrcpy() { return ;; --keyboard) - COMPREPLY=($(compgen -W 'disabled sdk aoa' -- "$cur")) + COMPREPLY=($(compgen -W 'disabled sdk uhid aoa' -- "$cur")) return ;; --mouse) diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index a4611632..f81d2b22 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -34,7 +34,7 @@ arguments=( '--force-adb-forward[Do not attempt to use \"adb reverse\" to connect to the device]' '--forward-all-clicks[Forward clicks to device]' {-h,--help}'[Print the help]' - '--keyboard[Set the keyboard input mode]:mode:(disabled sdk aoa)' + '--keyboard[Set the keyboard input mode]:mode:(disabled sdk uhid aoa)' '--kill-adb-on-close[Kill adb when scrcpy terminates]' '--legacy-paste[Inject computer clipboard text as a sequence of key events on Ctrl+v]' '--list-camera-sizes[List the valid camera capture sizes]' diff --git a/app/meson.build b/app/meson.build index 3ec9781a..9a2d2838 100644 --- a/app/meson.build +++ b/app/meson.build @@ -35,6 +35,7 @@ src = [ 'src/hid/hid_mouse.c', 'src/trait/frame_source.c', 'src/trait/packet_source.c', + 'src/uhid/keyboard_uhid.c', 'src/util/acksync.c', 'src/util/audiobuf.c', 'src/util/average.c', diff --git a/app/scrcpy.1 b/app/scrcpy.1 index ed2e620e..1dfcab2b 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -175,13 +175,14 @@ Print this help. .BI "\-\-keyboard " mode Select how to send keyboard inputs to the device. -Possible values are "disabled", "sdk" and "aoa": +Possible values are "disabled", "sdk", "uhid" and "aoa": - "disabled" does not send keyboard inputs to the device. - "sdk" uses the Android system API to deliver keyboard events to applications. - - "aoa" simulates a physical keyboard using the AOAv2 protocol. It may only work over USB. + - "uhid" simulates a physical HID keyboard using the Linux HID kernel module on the device. + - "aoa" simulates a physical HID keyboard using the AOAv2 protocol. It may only work over USB. -For "aoa", the keyboard layout must be configured (once and for all) on the device, via Settings -> System -> Languages and input -> Physical keyboard. This settings page can be started directly: +For "uhid" and "aoa", the keyboard layout must be configured (once and for all) on the device, via Settings -> System -> Languages and input -> Physical keyboard. This settings page can be started directly: adb shell am start -a android.settings.HARD_KEYBOARD_SETTINGS diff --git a/app/src/cli.c b/app/src/cli.c index 364590a4..59cd5699 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -365,19 +365,22 @@ static const struct sc_option options[] = { .longopt = "keyboard", .argdesc = "mode", .text = "Select how to send keyboard inputs to the device.\n" - "Possible values are \"disabled\", \"sdk\" and \"aoa\".\n" + "Possible values are \"disabled\", \"sdk\", \"uhid\" and " + "\"aoa\".\n" "\"disabled\" does not send keyboard inputs to the device.\n" "\"sdk\" uses the Android system API to deliver keyboard " "events to applications.\n" + "\"uhid\" simulates a physical HID keyboard using the Linux " + "UHID kernel module on the device.\n" "\"aoa\" simulates a physical keyboard using the AOAv2 " "protocol. It may only work over USB.\n" - "For \"aoa\", the keyboard layout must be configured (once and " - "for all) on the device, via Settings -> System -> Languages " - "and input -> Physical keyboard. This settings page can be " - "started directly: `adb shell am start -a " + "For \"uhid\" and \"aoa\", the keyboard layout must be " + "configured (once and for all) on the device, via Settings -> " + "System -> Languages and input -> Physical keyboard. This " + "settings page can be started directly: `adb shell am start -a " "android.settings.HARD_KEYBOARD_SETTINGS`.\n" - "This option is only available when the HID keyboard is " - "enabled (or a physical keyboard is connected).\n" + "This option is only available when a HID keyboard is enabled " + "(or a physical keyboard is connected).\n" "Also see --mouse.", }, { @@ -1939,6 +1942,11 @@ parse_keyboard(const char *optarg, enum sc_keyboard_input_mode *mode) { return true; } + if (!strcmp(optarg, "uhid")) { + *mode = SC_KEYBOARD_INPUT_MODE_UHID; + return true; + } + if (!strcmp(optarg, "aoa")) { #ifdef HAVE_USB *mode = SC_KEYBOARD_INPUT_MODE_AOA; @@ -1949,7 +1957,8 @@ parse_keyboard(const char *optarg, enum sc_keyboard_input_mode *mode) { #endif } - LOGE("Unsupported keyboard: %s (expected disabled, sdk or aoa)", optarg); + LOGE("Unsupported keyboard: %s (expected disabled, sdk, uhid and aoa)", + optarg); return false; } diff --git a/app/src/control_msg.c b/app/src/control_msg.c index e173dac7..88575b4e 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -146,6 +146,17 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) { case SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE: buf[1] = msg->set_screen_power_mode.mode; return 2; + case SC_CONTROL_MSG_TYPE_UHID_CREATE: + sc_write16be(&buf[1], msg->uhid_create.id); + sc_write16be(&buf[3], msg->uhid_create.report_desc_size); + memcpy(&buf[5], msg->uhid_create.report_desc, + msg->uhid_create.report_desc_size); + return 5 + msg->uhid_create.report_desc_size; + case SC_CONTROL_MSG_TYPE_UHID_INPUT: + sc_write16be(&buf[1], msg->uhid_input.id); + sc_write16be(&buf[3], msg->uhid_input.size); + memcpy(&buf[5], msg->uhid_input.data, msg->uhid_input.size); + return 5 + msg->uhid_input.size; case SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL: case SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL: case SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS: @@ -242,6 +253,23 @@ sc_control_msg_log(const struct sc_control_msg *msg) { case SC_CONTROL_MSG_TYPE_ROTATE_DEVICE: LOG_CMSG("rotate device"); break; + case SC_CONTROL_MSG_TYPE_UHID_CREATE: + LOG_CMSG("UHID create [%" PRIu16 "] report_desc_size=%" PRIu16, + msg->uhid_create.id, msg->uhid_create.report_desc_size); + break; + case SC_CONTROL_MSG_TYPE_UHID_INPUT: { + char *hex = sc_str_to_hex_string(msg->uhid_input.data, + msg->uhid_input.size); + if (hex) { + LOG_CMSG("UHID input [%" PRIu16 "] %s", + msg->uhid_input.id, hex); + free(hex); + } else { + LOG_CMSG("UHID input [%" PRIu16 "] size=%" PRIu16, + msg->uhid_input.id, msg->uhid_input.size); + } + break; + } default: LOG_CMSG("unknown type: %u", (unsigned) msg->type); break; diff --git a/app/src/control_msg.h b/app/src/control_msg.h index 04eeb83b..550168c2 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -10,6 +10,7 @@ #include "android/input.h" #include "android/keycodes.h" #include "coords.h" +#include "hid/hid_event.h" #define SC_CONTROL_MSG_MAX_SIZE (1 << 18) // 256k @@ -37,6 +38,8 @@ enum sc_control_msg_type { SC_CONTROL_MSG_TYPE_SET_CLIPBOARD, SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE, SC_CONTROL_MSG_TYPE_ROTATE_DEVICE, + SC_CONTROL_MSG_TYPE_UHID_CREATE, + SC_CONTROL_MSG_TYPE_UHID_INPUT, }; enum sc_screen_power_mode { @@ -92,6 +95,16 @@ struct sc_control_msg { struct { enum sc_screen_power_mode mode; } set_screen_power_mode; + struct { + uint16_t id; + uint16_t report_desc_size; + const uint8_t *report_desc; // pointer to static data + } uhid_create; + struct { + uint16_t id; + uint16_t size; + uint8_t data[SC_HID_MAX_SIZE]; + } uhid_input; }; }; diff --git a/app/src/options.h b/app/src/options.h index 1fb31c1a..6d62fac0 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -143,6 +143,7 @@ enum sc_keyboard_input_mode { SC_KEYBOARD_INPUT_MODE_AUTO, SC_KEYBOARD_INPUT_MODE_DISABLED, SC_KEYBOARD_INPUT_MODE_SDK, + SC_KEYBOARD_INPUT_MODE_UHID, SC_KEYBOARD_INPUT_MODE_AOA, }; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index a407dff1..d01d3619 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -25,6 +25,7 @@ #include "recorder.h" #include "screen.h" #include "server.h" +#include "uhid/keyboard_uhid.h" #ifdef HAVE_USB # include "usb/aoa_hid.h" # include "usb/keyboard_aoa.h" @@ -64,6 +65,7 @@ struct scrcpy { #endif union { struct sc_keyboard_sdk keyboard_sdk; + struct sc_keyboard_uhid keyboard_uhid; #ifdef HAVE_USB struct sc_keyboard_aoa keyboard_aoa; #endif @@ -656,15 +658,22 @@ aoa_hid_end: assert(options->mouse_input_mode != SC_MOUSE_INPUT_MODE_AOA); #endif - // keyboard_input_mode may have been reset if HID mode failed + // keyboard_input_mode may have been reset if AOA mode failed if (options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_SDK) { sc_keyboard_sdk_init(&s->keyboard_sdk, &s->controller, options->key_inject_mode, options->forward_key_repeat); kp = &s->keyboard_sdk.key_processor; + } else if (options->keyboard_input_mode + == SC_KEYBOARD_INPUT_MODE_UHID) { + bool ok = sc_keyboard_uhid_init(&s->keyboard_uhid, &s->controller); + if (!ok) { + goto end; + } + kp = &s->keyboard_uhid.key_processor; } - // mouse_input_mode may have been reset if HID mode failed + // mouse_input_mode may have been reset if AOA mode failed if (options->mouse_input_mode == SC_MOUSE_INPUT_MODE_SDK) { sc_mouse_sdk_init(&s->mouse_sdk, &s->controller); mp = &s->mouse_sdk.mouse_processor; diff --git a/app/src/uhid/keyboard_uhid.c b/app/src/uhid/keyboard_uhid.c new file mode 100644 index 00000000..d974d578 --- /dev/null +++ b/app/src/uhid/keyboard_uhid.c @@ -0,0 +1,72 @@ +#include "keyboard_uhid.h" + +#include "util/log.h" + +/** Downcast key processor to keyboard_uhid */ +#define DOWNCAST(KP) container_of(KP, struct sc_keyboard_uhid, key_processor) + +#define UHID_KEYBOARD_ID 1 + +static void +sc_key_processor_process_key(struct sc_key_processor *kp, + const struct sc_key_event *event, + uint64_t ack_to_wait) { + (void) ack_to_wait; + + if (event->repeat) { + // In USB HID protocol, key repeat is handled by the host (Android), so + // just ignore key repeat here. + return; + } + + struct sc_keyboard_uhid *kb = DOWNCAST(kp); + + struct sc_hid_event hid_event; + + // Not all keys are supported, just ignore unsupported keys + if (sc_hid_keyboard_event_from_key(&kb->hid, &hid_event, event)) { + struct sc_control_msg msg; + msg.type = SC_CONTROL_MSG_TYPE_UHID_INPUT; + msg.uhid_input.id = UHID_KEYBOARD_ID; + + assert(hid_event.size <= SC_HID_MAX_SIZE); + memcpy(msg.uhid_input.data, hid_event.data, hid_event.size); + msg.uhid_input.size = hid_event.size; + + if (!sc_controller_push_msg(kb->controller, &msg)) { + LOGE("Could not send UHID_INPUT message (key)"); + } + } +} + +bool +sc_keyboard_uhid_init(struct sc_keyboard_uhid *kb, + struct sc_controller *controller) { + sc_hid_keyboard_init(&kb->hid); + + kb->controller = controller; + + static const struct sc_key_processor_ops ops = { + .process_key = sc_key_processor_process_key, + // Never forward text input via HID (all the keys are injected + // separately) + .process_text = NULL, + }; + + // Clipboard synchronization is requested over the same control socket, so + // there is no need for a specific synchronization mechanism + kb->key_processor.async_paste = false; + kb->key_processor.ops = &ops; + + struct sc_control_msg msg; + msg.type = SC_CONTROL_MSG_TYPE_UHID_CREATE; + msg.uhid_create.id = UHID_KEYBOARD_ID; + msg.uhid_create.report_desc = SC_HID_KEYBOARD_REPORT_DESC; + msg.uhid_create.report_desc_size = SC_HID_KEYBOARD_REPORT_DESC_LEN; + if (!sc_controller_push_msg(controller, &msg)) { + LOGE("Could not send UHID_CREATE message (keyboard)"); + return false; + } + + return true; +} diff --git a/app/src/uhid/keyboard_uhid.h b/app/src/uhid/keyboard_uhid.h new file mode 100644 index 00000000..854ba008 --- /dev/null +++ b/app/src/uhid/keyboard_uhid.h @@ -0,0 +1,23 @@ +#ifndef SC_KEYBOARD_UHID_H +#define SC_KEYBOARD_UHID_H + +#include "common.h" + +#include + +#include "controller.h" +#include "hid/hid_keyboard.h" +#include "trait/key_processor.h" + +struct sc_keyboard_uhid { + struct sc_key_processor key_processor; // key processor trait + + struct sc_hid_keyboard hid; + struct sc_controller *controller; +}; + +bool +sc_keyboard_uhid_init(struct sc_keyboard_uhid *kb, + struct sc_controller *controller); + +#endif diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c index 80d33fc3..0ab61153 100644 --- a/app/tests/test_control_msg_serialize.c +++ b/app/tests/test_control_msg_serialize.c @@ -323,6 +323,53 @@ static void test_serialize_rotate_device(void) { assert(!memcmp(buf, expected, sizeof(expected))); } +static void test_serialize_uhid_create(void) { + const uint8_t report_desc[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}; + struct sc_control_msg msg = { + .type = SC_CONTROL_MSG_TYPE_UHID_CREATE, + .uhid_create = { + .id = 42, + .report_desc_size = sizeof(report_desc), + .report_desc = report_desc, + }, + }; + + uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; + size_t size = sc_control_msg_serialize(&msg, buf); + assert(size == 16); + + const uint8_t expected[] = { + SC_CONTROL_MSG_TYPE_UHID_CREATE, + 0, 42, // id + 0, 11, // size + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, + }; + assert(!memcmp(buf, expected, sizeof(expected))); +} + +static void test_serialize_uhid_input(void) { + struct sc_control_msg msg = { + .type = SC_CONTROL_MSG_TYPE_UHID_INPUT, + .uhid_input = { + .id = 42, + .size = 5, + .data = {1, 2, 3, 4, 5}, + }, + }; + + uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; + size_t size = sc_control_msg_serialize(&msg, buf); + assert(size == 10); + + const uint8_t expected[] = { + SC_CONTROL_MSG_TYPE_UHID_INPUT, + 0, 42, // id + 0, 5, // size + 1, 2, 3, 4, 5, + }; + assert(!memcmp(buf, expected, sizeof(expected))); +} + int main(int argc, char *argv[]) { (void) argc; (void) argv; @@ -341,5 +388,7 @@ int main(int argc, char *argv[]) { test_serialize_set_clipboard_long(); test_serialize_set_screen_power_mode(); test_serialize_rotate_device(); + test_serialize_uhid_create(); + test_serialize_uhid_input(); return 0; } diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java index e1800374..74bf5610 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_CLIPBOARD = 9; public static final int TYPE_SET_SCREEN_POWER_MODE = 10; public static final int TYPE_ROTATE_DEVICE = 11; + public static final int TYPE_UHID_CREATE = 12; + public static final int TYPE_UHID_INPUT = 13; public static final long SEQUENCE_INVALID = 0; @@ -40,6 +42,8 @@ public final class ControlMessage { private boolean paste; private int repeat; private long sequence; + private int id; + private byte[] data; private ControlMessage() { } @@ -123,6 +127,22 @@ public final class ControlMessage { return msg; } + public static ControlMessage createUhidCreate(int id, byte[] reportDesc) { + ControlMessage msg = new ControlMessage(); + msg.type = TYPE_UHID_CREATE; + msg.id = id; + msg.data = reportDesc; + return msg; + } + + public static ControlMessage createUhidInput(int id, byte[] data) { + ControlMessage msg = new ControlMessage(); + msg.type = TYPE_UHID_INPUT; + msg.id = id; + msg.data = data; + return msg; + } + public int getType() { return type; } @@ -186,4 +206,12 @@ public final class ControlMessage { public long getSequence() { return sequence; } + + public int getId() { + return id; + } + + public byte[] getData() { + return data; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java index d95c36d8..24aa73c0 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java @@ -15,6 +15,8 @@ public class ControlMessageReader { static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1; static final int GET_CLIPBOARD_LENGTH = 1; static final int SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH = 9; + static final int UHID_CREATE_FIXED_PAYLOAD_LENGTH = 4; + static final int UHID_INPUT_FIXED_PAYLOAD_LENGTH = 4; private static final int MESSAGE_MAX_SIZE = 1 << 18; // 256k @@ -86,6 +88,12 @@ public class ControlMessageReader { case ControlMessage.TYPE_ROTATE_DEVICE: msg = ControlMessage.createEmpty(type); break; + case ControlMessage.TYPE_UHID_CREATE: + msg = parseUhidCreate(); + break; + case ControlMessage.TYPE_UHID_INPUT: + msg = parseUhidInput(); + break; default: Ln.w("Unknown event type: " + type); msg = null; @@ -110,12 +118,21 @@ public class ControlMessageReader { return ControlMessage.createInjectKeycode(action, keycode, repeat, metaState); } - private String parseString() { - if (buffer.remaining() < 4) { - return null; + private int parseBufferLength(int sizeBytes) { + assert sizeBytes > 0 && sizeBytes <= 4; + if (buffer.remaining() < sizeBytes) { + return -1; } - int len = buffer.getInt(); - if (buffer.remaining() < len) { + int value = 0; + for (int i = 0; i < sizeBytes; ++i) { + value = (value << 8) | (buffer.get() & 0xFF); + } + return value; + } + + private String parseString() { + int len = parseBufferLength(4); + if (len == -1 || buffer.remaining() < len) { return null; } int position = buffer.position(); @@ -124,6 +141,16 @@ public class ControlMessageReader { return new String(rawBuffer, position, len, StandardCharsets.UTF_8); } + private byte[] parseByteArray(int sizeBytes) { + int len = parseBufferLength(sizeBytes); + if (len == -1 || buffer.remaining() < len) { + return null; + } + byte[] data = new byte[len]; + buffer.get(data); + return data; + } + private ControlMessage parseInjectText() { String text = parseString(); if (text == null) { @@ -193,6 +220,30 @@ public class ControlMessageReader { return ControlMessage.createSetScreenPowerMode(mode); } + private ControlMessage parseUhidCreate() { + if (buffer.remaining() < UHID_CREATE_FIXED_PAYLOAD_LENGTH) { + return null; + } + int id = buffer.getShort(); + byte[] data = parseByteArray(2); + if (data == null) { + return null; + } + return ControlMessage.createUhidCreate(id, data); + } + + private ControlMessage parseUhidInput() { + if (buffer.remaining() < UHID_INPUT_FIXED_PAYLOAD_LENGTH) { + return null; + } + int id = buffer.getShort(); + byte[] data = parseByteArray(2); + if (data == null) { + return null; + } + return ControlMessage.createUhidInput(id, data); + } + private static Position readPosition(ByteBuffer buffer) { int x = buffer.getInt(); int y = buffer.getInt(); diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index a3508c96..d757d577 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -26,6 +26,8 @@ public class Controller implements AsyncProcessor { private Thread thread; + private final UhidManager uhidManager; + private final Device device; private final ControlChannel controlChannel; private final CleanUp cleanUp; @@ -50,6 +52,7 @@ public class Controller implements AsyncProcessor { this.powerOn = powerOn; initPointers(); sender = new DeviceMessageSender(controlChannel); + uhidManager = new UhidManager(); } private void initPointers() { @@ -96,6 +99,7 @@ public class Controller implements AsyncProcessor { Ln.e("Controller error", e); } finally { Ln.d("Controller stopped"); + uhidManager.closeAll(); listener.onTerminated(true); } }, "control-recv"); @@ -190,6 +194,12 @@ public class Controller implements AsyncProcessor { case ControlMessage.TYPE_ROTATE_DEVICE: device.rotateDevice(); break; + case ControlMessage.TYPE_UHID_CREATE: + uhidManager.open(msg.getId(), msg.getData()); + break; + case ControlMessage.TYPE_UHID_INPUT: + uhidManager.writeInput(msg.getId(), msg.getData()); + break; default: // do nothing } diff --git a/server/src/main/java/com/genymobile/scrcpy/UhidManager.java b/server/src/main/java/com/genymobile/scrcpy/UhidManager.java new file mode 100644 index 00000000..96458bf0 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/UhidManager.java @@ -0,0 +1,138 @@ +package com.genymobile.scrcpy; + +import android.system.ErrnoException; +import android.system.Os; +import android.system.OsConstants; +import android.util.ArrayMap; + +import java.io.FileDescriptor; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; + +public final class UhidManager { + + // Linux: include/uapi/linux/uhid.h + private static final int UHID_CREATE2 = 11; + private static final int UHID_INPUT2 = 12; + + // Linux: include/uapi/linux/input.h + private static final short BUS_VIRTUAL = 0x06; + + private final ArrayMap fds = new ArrayMap<>(); + + public void open(int id, byte[] reportDesc) throws IOException { + try { + FileDescriptor fd = Os.open("/dev/uhid", OsConstants.O_RDWR, 0); + try { + FileDescriptor old = fds.put(id, fd); + if (old != null) { + Ln.w("Duplicate UHID id: " + id); + close(old); + } + + byte[] req = buildUhidCreate2Req(reportDesc); + Os.write(fd, req, 0, req.length); + } catch (Exception e) { + close(fd); + throw e; + } + } catch (ErrnoException e) { + throw new IOException(e); + } + } + + public void writeInput(int id, byte[] data) throws IOException { + FileDescriptor fd = fds.get(id); + if (fd == null) { + Ln.w("Unknown UHID id: " + id); + return; + } + + try { + byte[] req = buildUhidInput2Req(data); + Os.write(fd, req, 0, req.length); + } catch (ErrnoException e) { + throw new IOException(e); + } + } + + private static byte[] buildUhidCreate2Req(byte[] reportDesc) { + /* + * struct uhid_event { + * uint32_t type; + * union { + * // ... + * struct uhid_create2_req { + * uint8_t name[128]; + * uint8_t phys[64]; + * uint8_t uniq[64]; + * uint16_t rd_size; + * uint16_t bus; + * uint32_t vendor; + * uint32_t product; + * uint32_t version; + * uint32_t country; + * uint8_t rd_data[HID_MAX_DESCRIPTOR_SIZE]; + * }; + * }; + * } __attribute__((__packed__)); + */ + + byte[] empty = new byte[256]; + ByteBuffer buf = ByteBuffer.allocate(280 + reportDesc.length).order(ByteOrder.nativeOrder()); + buf.putInt(UHID_CREATE2); + buf.put("scrcpy".getBytes(StandardCharsets.US_ASCII)); + buf.put(empty, 0, 256 - "scrcpy".length()); + buf.putShort((short) reportDesc.length); + buf.putShort(BUS_VIRTUAL); + buf.putInt(0); // vendor id + buf.putInt(0); // product id + buf.putInt(0); // version + buf.putInt(0); // country; + buf.put(reportDesc); + return buf.array(); + } + + private static byte[] buildUhidInput2Req(byte[] data) { + /* + * struct uhid_event { + * uint32_t type; + * union { + * // ... + * struct uhid_input2_req { + * uint16_t size; + * uint8_t data[UHID_DATA_MAX]; + * }; + * }; + * } __attribute__((__packed__)); + */ + + ByteBuffer buf = ByteBuffer.allocate(6 + data.length).order(ByteOrder.nativeOrder()); + buf.putInt(UHID_INPUT2); + buf.putShort((short) data.length); + buf.put(data); + return buf.array(); + } + + public void close(int id) { + FileDescriptor fd = fds.get(id); + assert fd != null; + close(fd); + } + + public void closeAll() { + for (FileDescriptor fd : fds.values()) { + close(fd); + } + } + + private static void close(FileDescriptor fd) { + try { + Os.close(fd); + } catch (ErrnoException e) { + Ln.e("Failed to close uhid: " + e.getMessage()); + } + } +} diff --git a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java index 47097c78..7cc67c3e 100644 --- a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java @@ -322,6 +322,50 @@ public class ControlMessageReaderTest { Assert.assertEquals(ControlMessage.TYPE_ROTATE_DEVICE, event.getType()); } + @Test + public void testParseUhidCreate() throws IOException { + ControlMessageReader reader = new ControlMessageReader(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(ControlMessage.TYPE_UHID_CREATE); + dos.writeShort(42); // id + byte[] data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}; + dos.writeShort(data.length); // size + dos.write(data); + + byte[] packet = bos.toByteArray(); + + reader.readFrom(new ByteArrayInputStream(packet)); + ControlMessage event = reader.next(); + + Assert.assertEquals(ControlMessage.TYPE_UHID_CREATE, event.getType()); + Assert.assertEquals(42, event.getId()); + Assert.assertArrayEquals(data, event.getData()); + } + + @Test + public void testParseUhidInput() throws IOException { + ControlMessageReader reader = new ControlMessageReader(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(ControlMessage.TYPE_UHID_INPUT); + dos.writeShort(42); // id + byte[] data = {1, 2, 3, 4, 5}; + dos.writeShort(data.length); // size + dos.write(data); + + byte[] packet = bos.toByteArray(); + + reader.readFrom(new ByteArrayInputStream(packet)); + ControlMessage event = reader.next(); + + Assert.assertEquals(ControlMessage.TYPE_UHID_INPUT, event.getType()); + Assert.assertEquals(42, event.getId()); + Assert.assertArrayEquals(data, event.getData()); + } + @Test public void testMultiEvents() throws IOException { ControlMessageReader reader = new ControlMessageReader(); From 021c5d371ad0d2c932b981151f45f794d8843ebe Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 24 Feb 2024 22:27:53 +0100 Subject: [PATCH 1785/2244] Refactor DeviceMessageSender Refactor DeviceMessage as a queue of message. This will allow to add other message types. PR #4473 --- .../com/genymobile/scrcpy/Controller.java | 6 ++- .../com/genymobile/scrcpy/DeviceMessage.java | 2 - .../scrcpy/DeviceMessageSender.java | 42 ++++--------------- .../java/com/genymobile/scrcpy/Server.java | 5 ++- 4 files changed, 17 insertions(+), 38 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index d757d577..b925dd80 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -413,7 +413,8 @@ public class Controller implements AsyncProcessor { if (!clipboardAutosync) { String clipboardText = Device.getClipboardText(); if (clipboardText != null) { - sender.pushClipboardText(clipboardText); + DeviceMessage msg = DeviceMessage.createClipboard(clipboardText); + sender.send(msg); } } } @@ -431,7 +432,8 @@ public class Controller implements AsyncProcessor { if (sequence != ControlMessage.SEQUENCE_INVALID) { // Acknowledgement requested - sender.pushAckClipboard(sequence); + DeviceMessage msg = DeviceMessage.createAckClipboard(sequence); + sender.send(msg); } return ok; diff --git a/server/src/main/java/com/genymobile/scrcpy/DeviceMessage.java b/server/src/main/java/com/genymobile/scrcpy/DeviceMessage.java index 5b7c4de5..2e333e3f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DeviceMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/DeviceMessage.java @@ -5,8 +5,6 @@ public final class DeviceMessage { public static final int TYPE_CLIPBOARD = 0; public static final int TYPE_ACK_CLIPBOARD = 1; - public static final long SEQUENCE_INVALID = ControlMessage.SEQUENCE_INVALID; - private int type; private String text; private long sequence; diff --git a/server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java b/server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java index efb7b975..af14bb4e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java +++ b/server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java @@ -1,54 +1,30 @@ package com.genymobile.scrcpy; import java.io.IOException; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; public final class DeviceMessageSender { private final ControlChannel controlChannel; private Thread thread; - - private String clipboardText; - - private long ack; + private final BlockingQueue queue = new ArrayBlockingQueue<>(16); public DeviceMessageSender(ControlChannel controlChannel) { this.controlChannel = controlChannel; } - public synchronized void pushClipboardText(String text) { - clipboardText = text; - notify(); - } - - public synchronized void pushAckClipboard(long sequence) { - ack = sequence; - notify(); + public void send(DeviceMessage msg) { + if (!queue.offer(msg)) { + Ln.w("Device message dropped: " + msg.getType()); + } } private void loop() throws IOException, InterruptedException { while (!Thread.currentThread().isInterrupted()) { - String text; - long sequence; - synchronized (this) { - while (ack == DeviceMessage.SEQUENCE_INVALID && clipboardText == null) { - wait(); - } - text = clipboardText; - clipboardText = null; - - sequence = ack; - ack = DeviceMessage.SEQUENCE_INVALID; - } - - if (sequence != DeviceMessage.SEQUENCE_INVALID) { - DeviceMessage event = DeviceMessage.createAckClipboard(sequence); - controlChannel.send(event); - } - if (text != null) { - DeviceMessage event = DeviceMessage.createClipboard(text); - controlChannel.send(event); - } + DeviceMessage msg = queue.take(); + controlChannel.send(msg); } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 3936648d..587a46df 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -133,7 +133,10 @@ public final class Server { if (control) { ControlChannel controlChannel = connection.getControlChannel(); Controller controller = new Controller(device, controlChannel, cleanUp, options.getClipboardAutosync(), options.getPowerOn()); - device.setClipboardListener(text -> controller.getSender().pushClipboardText(text)); + device.setClipboardListener(text -> { + DeviceMessage msg = DeviceMessage.createClipboard(text); + controller.getSender().send(msg); + }); asyncProcessors.add(controller); } From 87da68ee0d74831a2b44230c573a3b315c8fd7d3 Mon Sep 17 00:00:00 2001 From: Simon Chan <1330321+yume-chan@users.noreply.github.com> Date: Fri, 12 Jan 2024 23:32:30 +0800 Subject: [PATCH 1786/2244] Handle UHID output Use UHID output reports to synchronize CapsLock and VerrNum states. PR #4473 Co-authored-by: Romain Vimont Signed-off-by: Romain Vimont --- app/meson.build | 1 + app/src/controller.c | 6 +- app/src/controller.h | 5 +- app/src/device_msg.c | 37 +++++- app/src/device_msg.h | 6 + app/src/receiver.c | 38 ++++++ app/src/receiver.h | 2 + app/src/scrcpy.c | 9 +- app/src/uhid/keyboard_uhid.c | 111 ++++++++++++++++-- app/src/uhid/keyboard_uhid.h | 6 +- app/src/uhid/uhid_output.c | 25 ++++ app/src/uhid/uhid_output.h | 45 +++++++ app/tests/test_device_msg_deserialize.c | 23 ++++ .../com/genymobile/scrcpy/Controller.java | 2 +- .../com/genymobile/scrcpy/DeviceMessage.java | 19 +++ .../scrcpy/DeviceMessageWriter.java | 7 ++ .../com/genymobile/scrcpy/UhidManager.java | 80 +++++++++++++ .../scrcpy/DeviceMessageWriterTest.java | 23 ++++ 18 files changed, 424 insertions(+), 21 deletions(-) create mode 100644 app/src/uhid/uhid_output.c create mode 100644 app/src/uhid/uhid_output.h diff --git a/app/meson.build b/app/meson.build index 9a2d2838..3695e0f9 100644 --- a/app/meson.build +++ b/app/meson.build @@ -36,6 +36,7 @@ src = [ 'src/trait/frame_source.c', 'src/trait/packet_source.c', 'src/uhid/keyboard_uhid.c', + 'src/uhid/uhid_output.c', 'src/util/acksync.c', 'src/util/audiobuf.c', 'src/util/average.c', diff --git a/app/src/controller.c b/app/src/controller.c index 5a5bfde9..499cfd3c 100644 --- a/app/src/controller.c +++ b/app/src/controller.c @@ -43,9 +43,11 @@ sc_controller_init(struct sc_controller *controller, sc_socket control_socket) { } void -sc_controller_set_acksync(struct sc_controller *controller, - struct sc_acksync *acksync) { +sc_controller_configure(struct sc_controller *controller, + struct sc_acksync *acksync, + struct sc_uhid_devices *uhid_devices) { controller->receiver.acksync = acksync; + controller->receiver.uhid_devices = uhid_devices; } void diff --git a/app/src/controller.h b/app/src/controller.h index 767e1731..1e44427e 100644 --- a/app/src/controller.h +++ b/app/src/controller.h @@ -28,8 +28,9 @@ bool sc_controller_init(struct sc_controller *controller, sc_socket control_socket); void -sc_controller_set_acksync(struct sc_controller *controller, - struct sc_acksync *acksync); +sc_controller_configure(struct sc_controller *controller, + struct sc_acksync *acksync, + struct sc_uhid_devices *uhid_devices); void sc_controller_destroy(struct sc_controller *controller); diff --git a/app/src/device_msg.c b/app/src/device_msg.c index 0cadc49c..7621c040 100644 --- a/app/src/device_msg.c +++ b/app/src/device_msg.c @@ -46,6 +46,31 @@ sc_device_msg_deserialize(const uint8_t *buf, size_t len, msg->ack_clipboard.sequence = sequence; return 9; } + case DEVICE_MSG_TYPE_UHID_OUTPUT: { + if (len < 5) { + // at least id + size + return 0; // not available + } + uint16_t id = sc_read16be(&buf[1]); + size_t size = sc_read16be(&buf[3]); + if (size < len - 5) { + return 0; // not available + } + uint8_t *data = malloc(size); + if (!data) { + LOG_OOM(); + return -1; + } + if (size) { + memcpy(data, &buf[5], size); + } + + msg->uhid_output.id = id; + msg->uhid_output.size = size; + msg->uhid_output.data = data; + + return 5 + size; + } default: LOGW("Unknown device message type: %d", (int) msg->type); return -1; // error, we cannot recover @@ -54,7 +79,15 @@ sc_device_msg_deserialize(const uint8_t *buf, size_t len, void sc_device_msg_destroy(struct sc_device_msg *msg) { - if (msg->type == DEVICE_MSG_TYPE_CLIPBOARD) { - free(msg->clipboard.text); + switch (msg->type) { + case DEVICE_MSG_TYPE_CLIPBOARD: + free(msg->clipboard.text); + break; + case DEVICE_MSG_TYPE_UHID_OUTPUT: + free(msg->uhid_output.data); + break; + default: + // nothing to do + break; } } diff --git a/app/src/device_msg.h b/app/src/device_msg.h index 3f541cf5..86b2ccb7 100644 --- a/app/src/device_msg.h +++ b/app/src/device_msg.h @@ -14,6 +14,7 @@ enum sc_device_msg_type { DEVICE_MSG_TYPE_CLIPBOARD, DEVICE_MSG_TYPE_ACK_CLIPBOARD, + DEVICE_MSG_TYPE_UHID_OUTPUT, }; struct sc_device_msg { @@ -25,6 +26,11 @@ struct sc_device_msg { struct { uint64_t sequence; } ack_clipboard; + struct { + uint16_t id; + uint16_t size; + uint8_t *data; // owned, to be freed by free() + } uhid_output; }; }; diff --git a/app/src/receiver.c b/app/src/receiver.c index 97299b3f..f4ebd3f8 100644 --- a/app/src/receiver.c +++ b/app/src/receiver.c @@ -1,11 +1,13 @@ #include "receiver.h" #include +#include #include #include #include "device_msg.h" #include "util/log.h" +#include "util/str.h" bool sc_receiver_init(struct sc_receiver *receiver, sc_socket control_socket) { @@ -16,6 +18,7 @@ sc_receiver_init(struct sc_receiver *receiver, sc_socket control_socket) { receiver->control_socket = control_socket; receiver->acksync = NULL; + receiver->uhid_devices = NULL; return true; } @@ -57,6 +60,41 @@ process_msg(struct sc_receiver *receiver, struct sc_device_msg *msg) { sc_acksync_ack(receiver->acksync, msg->ack_clipboard.sequence); break; + case DEVICE_MSG_TYPE_UHID_OUTPUT: + if (sc_get_log_level() <= SC_LOG_LEVEL_VERBOSE) { + char *hex = sc_str_to_hex_string(msg->uhid_output.data, + msg->uhid_output.size); + if (hex) { + LOGV("UHID output [%" PRIu16 "] %s", + msg->uhid_output.id, hex); + free(hex); + } else { + LOGV("UHID output [%" PRIu16 "] size=%" PRIu16, + msg->uhid_output.id, msg->uhid_output.size); + } + } + + // This is a programming error to receive this message if there is + // no uhid_devices instance + assert(receiver->uhid_devices); + + // Also check at runtime (do not trust the server) + if (!receiver->uhid_devices) { + LOGE("Received unexpected HID output message"); + return; + } + + struct sc_uhid_receiver *uhid_receiver = + sc_uhid_devices_get_receiver(receiver->uhid_devices, + msg->uhid_output.id); + if (uhid_receiver) { + uhid_receiver->ops->process_output(uhid_receiver, + msg->uhid_output.data, + msg->uhid_output.size); + } else { + LOGW("No UHID receiver for id %" PRIu16, msg->uhid_output.id); + } + break; } } diff --git a/app/src/receiver.h b/app/src/receiver.h index 43f89615..ba84c0ab 100644 --- a/app/src/receiver.h +++ b/app/src/receiver.h @@ -5,6 +5,7 @@ #include +#include "uhid/uhid_output.h" #include "util/acksync.h" #include "util/net.h" #include "util/thread.h" @@ -17,6 +18,7 @@ struct sc_receiver { sc_mutex mutex; struct sc_acksync *acksync; + struct sc_uhid_devices *uhid_devices; }; bool diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index d01d3619..7b1a6d5c 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -62,6 +62,7 @@ struct scrcpy { struct sc_aoa aoa; // sequence/ack helper to synchronize clipboard and Ctrl+v via HID struct sc_acksync acksync; + struct sc_uhid_devices uhid_devices; #endif union { struct sc_keyboard_sdk keyboard_sdk; @@ -342,6 +343,7 @@ scrcpy(struct scrcpy_options *options) { bool timeout_started = false; struct sc_acksync *acksync = NULL; + struct sc_uhid_devices *uhid_devices = NULL; uint32_t scid = scrcpy_generate_scid(); @@ -666,10 +668,13 @@ aoa_hid_end: kp = &s->keyboard_sdk.key_processor; } else if (options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_UHID) { - bool ok = sc_keyboard_uhid_init(&s->keyboard_uhid, &s->controller); + sc_uhid_devices_init(&s->uhid_devices); + bool ok = sc_keyboard_uhid_init(&s->keyboard_uhid, &s->controller, + &s->uhid_devices); if (!ok) { goto end; } + uhid_devices = &s->uhid_devices; kp = &s->keyboard_uhid.key_processor; } @@ -679,7 +684,7 @@ aoa_hid_end: mp = &s->mouse_sdk.mouse_processor; } - sc_controller_set_acksync(&s->controller, acksync); + sc_controller_configure(&s->controller, acksync, uhid_devices); if (!sc_controller_start(&s->controller)) { goto end; diff --git a/app/src/uhid/keyboard_uhid.c b/app/src/uhid/keyboard_uhid.c index d974d578..f537bc29 100644 --- a/app/src/uhid/keyboard_uhid.c +++ b/app/src/uhid/keyboard_uhid.c @@ -5,8 +5,52 @@ /** Downcast key processor to keyboard_uhid */ #define DOWNCAST(KP) container_of(KP, struct sc_keyboard_uhid, key_processor) +/** Downcast uhid_receiver to keyboard_uhid */ +#define DOWNCAST_RECEIVER(UR) \ + container_of(UR, struct sc_keyboard_uhid, uhid_receiver) + #define UHID_KEYBOARD_ID 1 +static void +sc_keyboard_uhid_send_input(struct sc_keyboard_uhid *kb, + const struct sc_hid_event *event) { + struct sc_control_msg msg; + msg.type = SC_CONTROL_MSG_TYPE_UHID_INPUT; + msg.uhid_input.id = UHID_KEYBOARD_ID; + + assert(event->size <= SC_HID_MAX_SIZE); + memcpy(msg.uhid_input.data, event->data, event->size); + msg.uhid_input.size = event->size; + + if (!sc_controller_push_msg(kb->controller, &msg)) { + LOGE("Could not send UHID_INPUT message (key)"); + } +} + +static void +sc_keyboard_uhid_synchronize_mod(struct sc_keyboard_uhid *kb) { + SDL_Keymod sdl_mod = SDL_GetModState(); + uint16_t mod = sc_mods_state_from_sdl(sdl_mod) & (SC_MOD_CAPS | SC_MOD_NUM); + + uint16_t device_mod = + atomic_load_explicit(&kb->device_mod, memory_order_relaxed); + uint16_t diff = mod ^ device_mod; + + if (diff) { + // Inherently racy (the HID output reports arrive asynchronously in + // response to key presses), but will re-synchronize on next key press + // or HID output anyway + atomic_store_explicit(&kb->device_mod, mod, memory_order_relaxed); + + struct sc_hid_event hid_event; + sc_hid_keyboard_event_from_mods(&hid_event, diff); + + LOGV("HID keyboard state synchronized"); + + sc_keyboard_uhid_send_input(kb, &hid_event); + } +} + static void sc_key_processor_process_key(struct sc_key_processor *kp, const struct sc_key_event *event, @@ -25,26 +69,63 @@ sc_key_processor_process_key(struct sc_key_processor *kp, // Not all keys are supported, just ignore unsupported keys if (sc_hid_keyboard_event_from_key(&kb->hid, &hid_event, event)) { - struct sc_control_msg msg; - msg.type = SC_CONTROL_MSG_TYPE_UHID_INPUT; - msg.uhid_input.id = UHID_KEYBOARD_ID; - - assert(hid_event.size <= SC_HID_MAX_SIZE); - memcpy(msg.uhid_input.data, hid_event.data, hid_event.size); - msg.uhid_input.size = hid_event.size; - - if (!sc_controller_push_msg(kb->controller, &msg)) { - LOGE("Could not send UHID_INPUT message (key)"); + if (event->scancode == SC_SCANCODE_CAPSLOCK) { + atomic_fetch_xor_explicit(&kb->device_mod, SC_MOD_CAPS, + memory_order_relaxed); + } else if (event->scancode == SC_SCANCODE_NUMLOCK) { + atomic_fetch_xor_explicit(&kb->device_mod, SC_MOD_NUM, + memory_order_relaxed); + } else { + // Synchronize modifiers (only if the scancode itself does not + // change the modifiers) + sc_keyboard_uhid_synchronize_mod(kb); } + sc_keyboard_uhid_send_input(kb, &hid_event); } } +static unsigned +sc_keyboard_uhid_to_sc_mod(uint8_t hid_led) { + // + // (chapter 11: LED page) + unsigned mod = 0; + if (hid_led & 0x01) { + mod |= SC_MOD_NUM; + } + if (hid_led & 0x02) { + mod |= SC_MOD_CAPS; + } + return mod; +} + +static void +sc_uhid_receiver_process_output(struct sc_uhid_receiver *receiver, + const uint8_t *data, size_t len) { + // Called from the thread receiving device messages + + assert(len); + + // Also check at runtime (do not trust the server) + if (!len) { + LOGE("Unexpected empty HID output message"); + return; + } + + struct sc_keyboard_uhid *kb = DOWNCAST_RECEIVER(receiver); + + uint8_t hid_led = data[0]; + uint16_t device_mod = sc_keyboard_uhid_to_sc_mod(hid_led); + atomic_store_explicit(&kb->device_mod, device_mod, memory_order_relaxed); +} + bool sc_keyboard_uhid_init(struct sc_keyboard_uhid *kb, - struct sc_controller *controller) { + struct sc_controller *controller, + struct sc_uhid_devices *uhid_devices) { sc_hid_keyboard_init(&kb->hid); kb->controller = controller; + atomic_init(&kb->device_mod, 0); static const struct sc_key_processor_ops ops = { .process_key = sc_key_processor_process_key, @@ -58,6 +139,14 @@ sc_keyboard_uhid_init(struct sc_keyboard_uhid *kb, kb->key_processor.async_paste = false; kb->key_processor.ops = &ops; + static const struct sc_uhid_receiver_ops uhid_receiver_ops = { + .process_output = sc_uhid_receiver_process_output, + }; + + kb->uhid_receiver.id = UHID_KEYBOARD_ID; + kb->uhid_receiver.ops = &uhid_receiver_ops; + sc_uhid_devices_add_receiver(uhid_devices, &kb->uhid_receiver); + struct sc_control_msg msg; msg.type = SC_CONTROL_MSG_TYPE_UHID_CREATE; msg.uhid_create.id = UHID_KEYBOARD_ID; diff --git a/app/src/uhid/keyboard_uhid.h b/app/src/uhid/keyboard_uhid.h index 854ba008..5e1be70c 100644 --- a/app/src/uhid/keyboard_uhid.h +++ b/app/src/uhid/keyboard_uhid.h @@ -7,17 +7,21 @@ #include "controller.h" #include "hid/hid_keyboard.h" +#include "uhid/uhid_output.h" #include "trait/key_processor.h" struct sc_keyboard_uhid { struct sc_key_processor key_processor; // key processor trait + struct sc_uhid_receiver uhid_receiver; struct sc_hid_keyboard hid; struct sc_controller *controller; + atomic_uint_least16_t device_mod; }; bool sc_keyboard_uhid_init(struct sc_keyboard_uhid *kb, - struct sc_controller *controller); + struct sc_controller *controller, + struct sc_uhid_devices *uhid_devices); #endif diff --git a/app/src/uhid/uhid_output.c b/app/src/uhid/uhid_output.c new file mode 100644 index 00000000..3b095faf --- /dev/null +++ b/app/src/uhid/uhid_output.c @@ -0,0 +1,25 @@ +#include "uhid_output.h" + +#include + +void +sc_uhid_devices_init(struct sc_uhid_devices *devices) { + devices->count = 0; +} + +void +sc_uhid_devices_add_receiver(struct sc_uhid_devices *devices, + struct sc_uhid_receiver *receiver) { + assert(devices->count < SC_UHID_MAX_RECEIVERS); + devices->receivers[devices->count++] = receiver; +} + +struct sc_uhid_receiver * +sc_uhid_devices_get_receiver(struct sc_uhid_devices *devices, uint16_t id) { + for (size_t i = 0; i < devices->count; ++i) { + if (devices->receivers[i]->id == id) { + return devices->receivers[i]; + } + } + return NULL; +} diff --git a/app/src/uhid/uhid_output.h b/app/src/uhid/uhid_output.h new file mode 100644 index 00000000..e13eed87 --- /dev/null +++ b/app/src/uhid/uhid_output.h @@ -0,0 +1,45 @@ +#ifndef SC_UHID_OUTPUT_H +#define SC_UHID_OUTPUT_H + +#include "common.h" + +#include +#include + +/** + * The communication with UHID devices is bidirectional. + * + * This component manages the registration of receivers to handle UHID output + * messages (sent from the device to the computer). + */ + +struct sc_uhid_receiver { + uint16_t id; + + const struct sc_uhid_receiver_ops *ops; +}; + +struct sc_uhid_receiver_ops { + void + (*process_output)(struct sc_uhid_receiver *receiver, + const uint8_t *data, size_t len); +}; + +#define SC_UHID_MAX_RECEIVERS 1 + +struct sc_uhid_devices { + struct sc_uhid_receiver *receivers[SC_UHID_MAX_RECEIVERS]; + unsigned count; +}; + +void +sc_uhid_devices_init(struct sc_uhid_devices *devices); + +void +sc_uhid_devices_add_receiver(struct sc_uhid_devices *devices, + struct sc_uhid_receiver *receiver); + +struct sc_uhid_receiver * +sc_uhid_devices_get_receiver(struct sc_uhid_devices *devices, uint16_t id); + +#endif diff --git a/app/tests/test_device_msg_deserialize.c b/app/tests/test_device_msg_deserialize.c index bfbcefd6..a64a3eb7 100644 --- a/app/tests/test_device_msg_deserialize.c +++ b/app/tests/test_device_msg_deserialize.c @@ -61,6 +61,28 @@ static void test_deserialize_ack_set_clipboard(void) { assert(msg.ack_clipboard.sequence == UINT64_C(0x0102030405060708)); } +static void test_deserialize_uhid_output(void) { + const uint8_t input[] = { + DEVICE_MSG_TYPE_UHID_OUTPUT, + 0, 42, // id + 0, 5, // size + 0x01, 0x02, 0x03, 0x04, 0x05, // data + }; + + struct sc_device_msg msg; + ssize_t r = sc_device_msg_deserialize(input, sizeof(input), &msg); + assert(r == 10); + + assert(msg.type == DEVICE_MSG_TYPE_UHID_OUTPUT); + assert(msg.uhid_output.id == 42); + assert(msg.uhid_output.size == 5); + + uint8_t expected[] = {1, 2, 3, 4, 5}; + assert(!memcmp(msg.uhid_output.data, expected, sizeof(expected))); + + sc_device_msg_destroy(&msg); +} + int main(int argc, char *argv[]) { (void) argc; (void) argv; @@ -68,5 +90,6 @@ int main(int argc, char *argv[]) { test_deserialize_clipboard(); test_deserialize_clipboard_big(); test_deserialize_ack_set_clipboard(); + test_deserialize_uhid_output(); return 0; } diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index b925dd80..5ba0c577 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -52,7 +52,7 @@ public class Controller implements AsyncProcessor { this.powerOn = powerOn; initPointers(); sender = new DeviceMessageSender(controlChannel); - uhidManager = new UhidManager(); + uhidManager = new UhidManager(sender); } private void initPointers() { diff --git a/server/src/main/java/com/genymobile/scrcpy/DeviceMessage.java b/server/src/main/java/com/genymobile/scrcpy/DeviceMessage.java index 2e333e3f..a8987eb6 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DeviceMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/DeviceMessage.java @@ -4,10 +4,13 @@ public final class DeviceMessage { public static final int TYPE_CLIPBOARD = 0; public static final int TYPE_ACK_CLIPBOARD = 1; + public static final int TYPE_UHID_OUTPUT = 2; private int type; private String text; private long sequence; + private int id; + private byte[] data; private DeviceMessage() { } @@ -26,6 +29,14 @@ public final class DeviceMessage { return event; } + public static DeviceMessage createUhidOutput(int id, byte[] data) { + DeviceMessage event = new DeviceMessage(); + event.type = TYPE_UHID_OUTPUT; + event.id = id; + event.data = data; + return event; + } + public int getType() { return type; } @@ -37,4 +48,12 @@ public final class DeviceMessage { public long getSequence() { return sequence; } + + public int getId() { + return id; + } + + public byte[] getData() { + return data; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java b/server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java index bcd8d206..f5d57c98 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java +++ b/server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java @@ -29,6 +29,13 @@ public class DeviceMessageWriter { buffer.putLong(msg.getSequence()); output.write(rawBuffer, 0, buffer.position()); break; + case DeviceMessage.TYPE_UHID_OUTPUT: + buffer.putShort((short) msg.getId()); + byte[] data = msg.getData(); + buffer.putShort((short) data.length); + buffer.put(data); + output.write(rawBuffer, 0, buffer.position()); + break; default: Ln.w("Unknown device message: " + msg.getType()); break; diff --git a/server/src/main/java/com/genymobile/scrcpy/UhidManager.java b/server/src/main/java/com/genymobile/scrcpy/UhidManager.java index 96458bf0..a39288a5 100644 --- a/server/src/main/java/com/genymobile/scrcpy/UhidManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/UhidManager.java @@ -1,5 +1,8 @@ package com.genymobile.scrcpy; +import android.os.Build; +import android.os.HandlerThread; +import android.os.MessageQueue; import android.system.ErrnoException; import android.system.Os; import android.system.OsConstants; @@ -7,6 +10,7 @@ import android.util.ArrayMap; import java.io.FileDescriptor; import java.io.IOException; +import java.io.InterruptedIOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.charset.StandardCharsets; @@ -14,13 +18,31 @@ import java.nio.charset.StandardCharsets; public final class UhidManager { // Linux: include/uapi/linux/uhid.h + private static final int UHID_OUTPUT = 6; private static final int UHID_CREATE2 = 11; private static final int UHID_INPUT2 = 12; // Linux: include/uapi/linux/input.h private static final short BUS_VIRTUAL = 0x06; + private static final int SIZE_OF_UHID_EVENT = 4380; // sizeof(struct uhid_event) + private final ArrayMap fds = new ArrayMap<>(); + private final ByteBuffer buffer = ByteBuffer.allocate(SIZE_OF_UHID_EVENT).order(ByteOrder.nativeOrder()); + + private final DeviceMessageSender sender; + private final HandlerThread thread = new HandlerThread("UHidManager"); + private final MessageQueue queue; + + public UhidManager(DeviceMessageSender sender) { + this.sender = sender; + thread.start(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + queue = thread.getLooper().getQueue(); + } else { + queue = null; + } + } public void open(int id, byte[] reportDesc) throws IOException { try { @@ -34,6 +56,8 @@ public final class UhidManager { byte[] req = buildUhidCreate2Req(reportDesc); Os.write(fd, req, 0, req.length); + + registerUhidListener(id, fd); } catch (Exception e) { close(fd); throw e; @@ -43,6 +67,62 @@ public final class UhidManager { } } + private void registerUhidListener(int id, FileDescriptor fd) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + queue.addOnFileDescriptorEventListener(fd, MessageQueue.OnFileDescriptorEventListener.EVENT_INPUT, (fd2, events) -> { + try { + buffer.clear(); + int r = Os.read(fd2, buffer); + buffer.flip(); + if (r > 0) { + int type = buffer.getInt(); + if (type == UHID_OUTPUT) { + byte[] data = extractHidOutputData(buffer); + if (data != null) { + DeviceMessage msg = DeviceMessage.createUhidOutput(id, data); + sender.send(msg); + } + } + } + } catch (ErrnoException | InterruptedIOException e) { + Ln.e("Failed to read UHID output", e); + return 0; + } + return events; + }); + } + } + + private static byte[] extractHidOutputData(ByteBuffer buffer) { + /* + * #define UHID_DATA_MAX 4096 + * struct uhid_event { + * uint32_t type; + * union { + * // ... + * struct uhid_output_req { + * __u8 data[UHID_DATA_MAX]; + * __u16 size; + * __u8 rtype; + * }; + * }; + * } __attribute__((__packed__)); + */ + + if (buffer.remaining() < 4099) { + Ln.w("Incomplete HID output"); + return null; + } + int size = buffer.getShort(buffer.position() + 4096) & 0xFFFF; + if (size > 4096) { + Ln.w("Incorrect HID output size: " + size); + return null; + } + byte[] data = new byte[size]; + buffer.get(data); + return data; + } + public void writeInput(int id, byte[] data) throws IOException { FileDescriptor fd = fds.get(id); if (fd == null) { diff --git a/server/src/test/java/com/genymobile/scrcpy/DeviceMessageWriterTest.java b/server/src/test/java/com/genymobile/scrcpy/DeviceMessageWriterTest.java index 7b917d33..d7f926ba 100644 --- a/server/src/test/java/com/genymobile/scrcpy/DeviceMessageWriterTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/DeviceMessageWriterTest.java @@ -52,4 +52,27 @@ public class DeviceMessageWriterTest { Assert.assertArrayEquals(expected, actual); } + + @Test + public void testSerializeUhidOutput() throws IOException { + DeviceMessageWriter writer = new DeviceMessageWriter(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(DeviceMessage.TYPE_UHID_OUTPUT); + dos.writeShort(42); // id + byte[] data = {1, 2, 3, 4, 5}; + dos.writeShort(data.length); + dos.write(data); + + byte[] expected = bos.toByteArray(); + + DeviceMessage msg = DeviceMessage.createUhidOutput(42, data); + bos = new ByteArrayOutputStream(); + writer.writeTo(msg, bos); + + byte[] actual = bos.toByteArray(); + + Assert.assertArrayEquals(expected, actual); + } } From f557188dc835e0a1b108d56b30641510901ecf13 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 24 Feb 2024 22:38:32 +0100 Subject: [PATCH 1787/2244] Create UhidManager only on first use There is no need to create a UhidManager instance (with its thread) if no UHID is used. PR #4473 --- .../java/com/genymobile/scrcpy/Controller.java | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index 5ba0c577..fd320d3f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -26,7 +26,7 @@ public class Controller implements AsyncProcessor { private Thread thread; - private final UhidManager uhidManager; + private UhidManager uhidManager; private final Device device; private final ControlChannel controlChannel; @@ -52,7 +52,13 @@ public class Controller implements AsyncProcessor { this.powerOn = powerOn; initPointers(); sender = new DeviceMessageSender(controlChannel); - uhidManager = new UhidManager(sender); + } + + private UhidManager getUhidManager() { + if (uhidManager == null) { + uhidManager = new UhidManager(sender); + } + return uhidManager; } private void initPointers() { @@ -99,7 +105,9 @@ public class Controller implements AsyncProcessor { Ln.e("Controller error", e); } finally { Ln.d("Controller stopped"); - uhidManager.closeAll(); + if (uhidManager != null) { + uhidManager.closeAll(); + } listener.onTerminated(true); } }, "control-recv"); @@ -195,10 +203,10 @@ public class Controller implements AsyncProcessor { device.rotateDevice(); break; case ControlMessage.TYPE_UHID_CREATE: - uhidManager.open(msg.getId(), msg.getData()); + getUhidManager().open(msg.getId(), msg.getData()); break; case ControlMessage.TYPE_UHID_INPUT: - uhidManager.writeInput(msg.getId(), msg.getData()); + getUhidManager().writeInput(msg.getId(), msg.getData()); break; default: // do nothing From 54dede36307edc69553d7de620f6b4318e48c678 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 26 Feb 2024 20:22:34 +0100 Subject: [PATCH 1788/2244] Fix startActivity() for supporting API < 30 Call the older startActivityAsUser() instead of startActivityAsUserWithFeature() so that it also works on older Android versions. Fixes #4704 PR #4473 --- .../com/genymobile/scrcpy/AudioCapture.java | 2 +- .../scrcpy/wrappers/ActivityManager.java | 19 +++++++++---------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java b/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java index 45634c70..3934ad49 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java @@ -79,7 +79,7 @@ public final class AudioCapture { intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.addCategory(Intent.CATEGORY_LAUNCHER); intent.setComponent(new ComponentName(FakeContext.PACKAGE_NAME, "com.android.shell.HeapDumpActivity")); - ServiceManager.getActivityManager().startActivityAsUserWithFeature(intent); + ServiceManager.getActivityManager().startActivity(intent); } private static void stopWorkaroundAndroid11() { diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java index 367ea2e7..d4bee165 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java @@ -22,7 +22,7 @@ public final class ActivityManager { private Method getContentProviderExternalMethod; private boolean getContentProviderExternalMethodNewVersion = true; private Method removeContentProviderExternalMethod; - private Method startActivityAsUserWithFeatureMethod; + private Method startActivityAsUserMethod; private Method forceStopPackageMethod; static ActivityManager create() { @@ -107,26 +107,25 @@ public final class ActivityManager { return getContentProviderExternal("settings", new Binder()); } - private Method getStartActivityAsUserWithFeatureMethod() throws NoSuchMethodException, ClassNotFoundException { - if (startActivityAsUserWithFeatureMethod == null) { + private Method getStartActivityAsUserMethod() throws NoSuchMethodException, ClassNotFoundException { + if (startActivityAsUserMethod == null) { Class iApplicationThreadClass = Class.forName("android.app.IApplicationThread"); Class profilerInfo = Class.forName("android.app.ProfilerInfo"); - startActivityAsUserWithFeatureMethod = manager.getClass() - .getMethod("startActivityAsUserWithFeature", iApplicationThreadClass, String.class, String.class, Intent.class, String.class, - IBinder.class, String.class, int.class, int.class, profilerInfo, Bundle.class, int.class); + startActivityAsUserMethod = manager.getClass() + .getMethod("startActivityAsUser", iApplicationThreadClass, String.class, Intent.class, String.class, IBinder.class, String.class, + int.class, int.class, profilerInfo, Bundle.class, int.class); } - return startActivityAsUserWithFeatureMethod; + return startActivityAsUserMethod; } @SuppressWarnings("ConstantConditions") - public int startActivityAsUserWithFeature(Intent intent) { + public int startActivity(Intent intent) { try { - Method method = getStartActivityAsUserWithFeatureMethod(); + Method method = getStartActivityAsUserMethod(); return (int) method.invoke( /* this */ manager, /* caller */ null, /* callingPackage */ FakeContext.PACKAGE_NAME, - /* callingFeatureId */ null, /* intent */ intent, /* resolvedType */ null, /* resultTo */ null, From 151a6225d44f795bcc6066e8af9ccc65fefbaded Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 24 Feb 2024 22:30:30 +0100 Subject: [PATCH 1789/2244] Add shortcut to open keyboard settings The keyboard settings can be opened by: adb shell am start -a android.settings.HARD_KEYBOARD_SETTINGS Add a shortcut (MOD+k) for convenience if the current keyboard is HID. PR #4473 --- app/scrcpy.1 | 6 +++++- app/src/cli.c | 9 +++++++-- app/src/control_msg.c | 4 ++++ app/src/control_msg.h | 1 + app/src/input_manager.c | 19 +++++++++++++++++++ app/src/keyboard_sdk.c | 1 + app/src/trait/key_processor.h | 7 +++++++ app/src/uhid/keyboard_uhid.c | 1 + app/src/usb/keyboard_aoa.c | 1 + app/tests/test_control_msg_serialize.c | 16 ++++++++++++++++ doc/shortcuts.md | 1 + .../com/genymobile/scrcpy/ControlMessage.java | 1 + .../scrcpy/ControlMessageReader.java | 1 + .../com/genymobile/scrcpy/Controller.java | 10 ++++++++++ .../scrcpy/ControlMessageReaderTest.java | 16 ++++++++++++++++ 15 files changed, 91 insertions(+), 3 deletions(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 1dfcab2b..7e856664 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -182,7 +182,7 @@ Possible values are "disabled", "sdk", "uhid" and "aoa": - "uhid" simulates a physical HID keyboard using the Linux HID kernel module on the device. - "aoa" simulates a physical HID keyboard using the AOAv2 protocol. It may only work over USB. -For "uhid" and "aoa", the keyboard layout must be configured (once and for all) on the device, via Settings -> System -> Languages and input -> Physical keyboard. This settings page can be started directly: +For "uhid" and "aoa", the keyboard layout must be configured (once and for all) on the device, via Settings -> System -> Languages and input -> Physical keyboard. This settings page can be started directly using the shortcut MOD+k (except in OTG mode), or by executing: adb shell am start -a android.settings.HARD_KEYBOARD_SETTINGS @@ -644,6 +644,10 @@ Copy computer clipboard to device, then paste (inject PASTE keycode, Android >= .B MOD+Shift+v Inject computer clipboard text as a sequence of key events +.TP +.B MOD+k +Open keyboard settings on the device (for HID keyboard only) + .TP .B MOD+i Enable/disable FPS counter (print frames/second in logs) diff --git a/app/src/cli.c b/app/src/cli.c index 59cd5699..c1c68e92 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -377,8 +377,9 @@ static const struct sc_option options[] = { "For \"uhid\" and \"aoa\", the keyboard layout must be " "configured (once and for all) on the device, via Settings -> " "System -> Languages and input -> Physical keyboard. This " - "settings page can be started directly: `adb shell am start -a " - "android.settings.HARD_KEYBOARD_SETTINGS`.\n" + "settings page can be started directly using the shortcut " + "MOD+k (except in OTG mode) or by executing: `adb shell am " + "start -a android.settings.HARD_KEYBOARD_SETTINGS`.\n" "This option is only available when a HID keyboard is enabled " "(or a physical keyboard is connected).\n" "Also see --mouse.", @@ -965,6 +966,10 @@ static const struct sc_shortcut shortcuts[] = { .shortcuts = { "MOD+Shift+v" }, .text = "Inject computer clipboard text as a sequence of key events", }, + { + .shortcuts = { "MOD+k" }, + .text = "Open keyboard settings on the device (for HID keyboard only)", + }, { .shortcuts = { "MOD+i" }, .text = "Enable/disable FPS counter (print frames/second in logs)", diff --git a/app/src/control_msg.c b/app/src/control_msg.c index 88575b4e..b3da5fe5 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -161,6 +161,7 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) { case SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL: case SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS: case SC_CONTROL_MSG_TYPE_ROTATE_DEVICE: + case SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS: // no additional data return 1; default: @@ -270,6 +271,9 @@ sc_control_msg_log(const struct sc_control_msg *msg) { } break; } + case SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS: + LOG_CMSG("open hard keyboard settings"); + break; default: LOG_CMSG("unknown type: %u", (unsigned) msg->type); break; diff --git a/app/src/control_msg.h b/app/src/control_msg.h index 550168c2..cd1340ef 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -40,6 +40,7 @@ enum sc_control_msg_type { SC_CONTROL_MSG_TYPE_ROTATE_DEVICE, SC_CONTROL_MSG_TYPE_UHID_CREATE, SC_CONTROL_MSG_TYPE_UHID_INPUT, + SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS, }; enum sc_screen_power_mode { diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 7186186f..f26c4164 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -318,6 +318,18 @@ rotate_device(struct sc_input_manager *im) { } } +static void +open_hard_keyboard_settings(struct sc_input_manager *im) { + assert(im->controller); + + struct sc_control_msg msg; + msg.type = SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS; + + if (!sc_controller_push_msg(im->controller, &msg)) { + LOGW("Could not request opening hard keyboard settings"); + } +} + static void apply_orientation_transform(struct sc_input_manager *im, enum sc_orientation transform) { @@ -550,6 +562,13 @@ sc_input_manager_process_key(struct sc_input_manager *im, rotate_device(im); } return; + case SDLK_k: + if (control && !shift && !repeat && down + && im->kp && im->kp->hid) { + // Only if the current keyboard is hid + open_hard_keyboard_settings(im); + } + return; } return; diff --git a/app/src/keyboard_sdk.c b/app/src/keyboard_sdk.c index 726f65a9..00b7f92a 100644 --- a/app/src/keyboard_sdk.c +++ b/app/src/keyboard_sdk.c @@ -340,5 +340,6 @@ sc_keyboard_sdk_init(struct sc_keyboard_sdk *kb, // Key injection and clipboard synchronization are serialized kb->key_processor.async_paste = false; + kb->key_processor.hid = false; kb->key_processor.ops = &ops; } diff --git a/app/src/trait/key_processor.h b/app/src/trait/key_processor.h index 8c51b11d..96374413 100644 --- a/app/src/trait/key_processor.h +++ b/app/src/trait/key_processor.h @@ -23,6 +23,13 @@ struct sc_key_processor { */ bool async_paste; + /** + * Set by the implementation to indicate that the keyboard is HID. In + * practice, it is used to react on a shortcut to open the hard keyboard + * settings only if the keyboard is HID. + */ + bool hid; + const struct sc_key_processor_ops *ops; }; diff --git a/app/src/uhid/keyboard_uhid.c b/app/src/uhid/keyboard_uhid.c index f537bc29..515a3fd9 100644 --- a/app/src/uhid/keyboard_uhid.c +++ b/app/src/uhid/keyboard_uhid.c @@ -137,6 +137,7 @@ sc_keyboard_uhid_init(struct sc_keyboard_uhid *kb, // Clipboard synchronization is requested over the same control socket, so // there is no need for a specific synchronization mechanism kb->key_processor.async_paste = false; + kb->key_processor.hid = true; kb->key_processor.ops = &ops; static const struct sc_uhid_receiver_ops uhid_receiver_ops = { diff --git a/app/src/usb/keyboard_aoa.c b/app/src/usb/keyboard_aoa.c index b69d6cd8..736c97b0 100644 --- a/app/src/usb/keyboard_aoa.c +++ b/app/src/usb/keyboard_aoa.c @@ -94,6 +94,7 @@ sc_keyboard_aoa_init(struct sc_keyboard_aoa *kb, struct sc_aoa *aoa) { // events are sent over AOA, so it must wait for clipboard synchronization // to be acknowledged by the device before injecting Ctrl+v. kb->key_processor.async_paste = true; + kb->key_processor.hid = true; kb->key_processor.ops = &ops; return true; diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c index 0ab61153..7a978f2b 100644 --- a/app/tests/test_control_msg_serialize.c +++ b/app/tests/test_control_msg_serialize.c @@ -370,6 +370,21 @@ static void test_serialize_uhid_input(void) { assert(!memcmp(buf, expected, sizeof(expected))); } +static void test_serialize_open_hard_keyboard(void) { + struct sc_control_msg msg = { + .type = SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS, + }; + + uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; + size_t size = sc_control_msg_serialize(&msg, buf); + assert(size == 1); + + const uint8_t expected[] = { + SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS, + }; + assert(!memcmp(buf, expected, sizeof(expected))); +} + int main(int argc, char *argv[]) { (void) argc; (void) argv; @@ -390,5 +405,6 @@ int main(int argc, char *argv[]) { test_serialize_rotate_device(); test_serialize_uhid_create(); test_serialize_uhid_input(); + test_serialize_open_hard_keyboard(); return 0; } diff --git a/doc/shortcuts.md b/doc/shortcuts.md index 21bccbd9..8c402855 100644 --- a/doc/shortcuts.md +++ b/doc/shortcuts.md @@ -48,6 +48,7 @@ _[Super] is typically the Windows or Cmd key._ | Cut to clipboard⁵ | MOD+x | Synchronize clipboards and paste⁵ | MOD+v | Inject computer clipboard text | MOD+Shift+v + | Open keyboard settings (HID keyboard only) | MOD+k | Enable/disable FPS counter (on stdout) | MOD+i | Pinch-to-zoom/rotate | Ctrl+_click-and-move_ | Tilt (slide vertically with 2 fingers) | Shift+_click-and-move_ diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java index 74bf5610..bcbacb4b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java @@ -19,6 +19,7 @@ public final class ControlMessage { public static final int TYPE_ROTATE_DEVICE = 11; public static final int TYPE_UHID_CREATE = 12; public static final int TYPE_UHID_INPUT = 13; + public static final int TYPE_OPEN_HARD_KEYBOARD_SETTINGS = 14; public static final long SEQUENCE_INVALID = 0; diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java index 24aa73c0..1761d228 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java @@ -86,6 +86,7 @@ public class ControlMessageReader { case ControlMessage.TYPE_EXPAND_SETTINGS_PANEL: case ControlMessage.TYPE_COLLAPSE_PANELS: case ControlMessage.TYPE_ROTATE_DEVICE: + case ControlMessage.TYPE_OPEN_HARD_KEYBOARD_SETTINGS: msg = ControlMessage.createEmpty(type); break; case ControlMessage.TYPE_UHID_CREATE: diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index fd320d3f..87faf8ba 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -1,7 +1,9 @@ package com.genymobile.scrcpy; import com.genymobile.scrcpy.wrappers.InputManager; +import com.genymobile.scrcpy.wrappers.ServiceManager; +import android.content.Intent; import android.os.Build; import android.os.SystemClock; import android.view.InputDevice; @@ -208,6 +210,9 @@ public class Controller implements AsyncProcessor { case ControlMessage.TYPE_UHID_INPUT: getUhidManager().writeInput(msg.getId(), msg.getData()); break; + case ControlMessage.TYPE_OPEN_HARD_KEYBOARD_SETTINGS: + openHardKeyboardSettings(); + break; default: // do nothing } @@ -446,4 +451,9 @@ public class Controller implements AsyncProcessor { return ok; } + + private void openHardKeyboardSettings() { + Intent intent = new Intent("android.settings.HARD_KEYBOARD_SETTINGS"); + ServiceManager.getActivityManager().startActivity(intent); + } } diff --git a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java index 7cc67c3e..0c8086f7 100644 --- a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java @@ -366,6 +366,22 @@ public class ControlMessageReaderTest { Assert.assertArrayEquals(data, event.getData()); } + @Test + public void testParseOpenHardKeyboardSettings() throws IOException { + ControlMessageReader reader = new ControlMessageReader(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(ControlMessage.TYPE_OPEN_HARD_KEYBOARD_SETTINGS); + + byte[] packet = bos.toByteArray(); + + reader.readFrom(new ByteArrayInputStream(packet)); + ControlMessage event = reader.next(); + + Assert.assertEquals(ControlMessage.TYPE_OPEN_HARD_KEYBOARD_SETTINGS, event.getType()); + } + @Test public void testMultiEvents() throws IOException { ControlMessageReader reader = new ControlMessageReader(); From 6a103c809f4a208c25d7fd5d019bc6bc5b3046b4 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 25 Feb 2024 15:43:36 +0100 Subject: [PATCH 1790/2244] Add UHID mouse support Use the following command: scrcpy --mouse=uhid PR #4473 --- app/data/bash-completion/scrcpy | 2 +- app/data/zsh-completion/_scrcpy | 2 +- app/meson.build | 1 + app/scrcpy.1 | 5 +- app/src/cli.c | 16 ++++-- app/src/options.h | 1 + app/src/scrcpy.c | 8 +++ app/src/uhid/mouse_uhid.c | 89 +++++++++++++++++++++++++++++++++ app/src/uhid/mouse_uhid.h | 19 +++++++ 9 files changed, 135 insertions(+), 8 deletions(-) create mode 100644 app/src/uhid/mouse_uhid.c create mode 100644 app/src/uhid/mouse_uhid.h diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index 904ccdeb..8cc0b157 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -120,7 +120,7 @@ _scrcpy() { return ;; --mouse) - COMPREPLY=($(compgen -W 'disabled sdk aoa' -- "$cur")) + COMPREPLY=($(compgen -W 'disabled sdk uhid aoa' -- "$cur")) return ;; --orientation|--display-orientation) diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index f81d2b22..1cf2ae41 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -44,7 +44,7 @@ arguments=( '--lock-video-orientation=[Lock video orientation]:orientation:(unlocked initial 0 90 180 270)' {-m,--max-size=}'[Limit both the width and height of the video to value]' '--max-fps=[Limit the frame rate of screen capture]' - '--mouse[Set the mouse input mode]:mode:(disabled sdk aoa)' + '--mouse[Set the mouse input mode]:mode:(disabled sdk uhid aoa)' {-n,--no-control}'[Disable device control \(mirror the device in read only\)]' {-N,--no-playback}'[Disable video and audio playback]' '--no-audio[Disable audio forwarding]' diff --git a/app/meson.build b/app/meson.build index 3695e0f9..b0a6aadb 100644 --- a/app/meson.build +++ b/app/meson.build @@ -36,6 +36,7 @@ src = [ 'src/trait/frame_source.c', 'src/trait/packet_source.c', 'src/uhid/keyboard_uhid.c', + 'src/uhid/mouse_uhid.c', 'src/uhid/uhid_output.c', 'src/util/acksync.c', 'src/util/audiobuf.c', diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 7e856664..8c0c4cc6 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -240,13 +240,14 @@ Limit the framerate of screen capture (officially supported since Android 10, bu .BI "\-\-mouse " mode Select how to send mouse inputs to the device. -Possible values are "disabled", "sdk" and "aoa": +Possible values are "disabled", "sdk", "uhid" and "aoa": - "disabled" does not send mouse inputs to the device. - "sdk" uses the Android system API to deliver mouse events to applications. + - "uhid" simulates a physical HID mouse using the Linux HID kernel module on the device. - "aoa" simulates a physical mouse using the AOAv2 protocol. It may only work over USB. -In "aoa" mode, the computer mouse is captured to control the device directly (relative mouse mode). +In "uhid" and "aoa" modes, the computer mouse is captured to control the device directly (relative mouse mode). LAlt, LSuper or RSuper toggle the capture mode, to give control of the mouse back to the computer. diff --git a/app/src/cli.c b/app/src/cli.c index c1c68e92..dceb8fff 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -462,14 +462,17 @@ static const struct sc_option options[] = { .longopt = "mouse", .argdesc = "mode", .text = "Select how to send mouse inputs to the device.\n" - "Possible values are \"disabled\", \"sdk\" and \"aoa\".\n" + "Possible values are \"disabled\", \"sdk\", \"uhid\" and " + "\"aoa\".\n" "\"disabled\" does not send mouse inputs to the device.\n" "\"sdk\" uses the Android system API to deliver mouse events" "to applications.\n" + "\"uhid\" simulates a physical HID mouse using the Linux UHID " + "kernel module on the device.\n" "\"aoa\" simulates a physical mouse using the AOAv2 protocol. " "It may only work over USB.\n" - "In \"aoa\" mode, the computer mouse is captured to control " - "the device directly (relative mouse mode).\n" + "In \"uhid\" and \"aoa\" modes, the computer mouse is captured " + "to control the device directly (relative mouse mode).\n" "LAlt, LSuper or RSuper toggle the capture mode, to give " "control of the mouse back to the computer.\n" "Also see --keyboard.", @@ -1979,6 +1982,11 @@ parse_mouse(const char *optarg, enum sc_mouse_input_mode *mode) { return true; } + if (!strcmp(optarg, "uhid")) { + *mode = SC_MOUSE_INPUT_MODE_UHID; + return true; + } + if (!strcmp(optarg, "aoa")) { #ifdef HAVE_USB *mode = SC_MOUSE_INPUT_MODE_AOA; @@ -1989,7 +1997,7 @@ parse_mouse(const char *optarg, enum sc_mouse_input_mode *mode) { #endif } - LOGE("Unsupported mouse: %s (expected disabled, sdk or aoa)", optarg); + LOGE("Unsupported mouse: %s (expected disabled, sdk, uhid or aoa)", optarg); return false; } diff --git a/app/src/options.h b/app/src/options.h index 6d62fac0..5445e7c8 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -151,6 +151,7 @@ enum sc_mouse_input_mode { SC_MOUSE_INPUT_MODE_AUTO, SC_MOUSE_INPUT_MODE_DISABLED, SC_MOUSE_INPUT_MODE_SDK, + SC_MOUSE_INPUT_MODE_UHID, SC_MOUSE_INPUT_MODE_AOA, }; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 7b1a6d5c..a40a4dec 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -26,6 +26,7 @@ #include "screen.h" #include "server.h" #include "uhid/keyboard_uhid.h" +#include "uhid/mouse_uhid.h" #ifdef HAVE_USB # include "usb/aoa_hid.h" # include "usb/keyboard_aoa.h" @@ -73,6 +74,7 @@ struct scrcpy { }; union { struct sc_mouse_sdk mouse_sdk; + struct sc_mouse_uhid mouse_uhid; #ifdef HAVE_USB struct sc_mouse_aoa mouse_aoa; #endif @@ -682,6 +684,12 @@ aoa_hid_end: if (options->mouse_input_mode == SC_MOUSE_INPUT_MODE_SDK) { sc_mouse_sdk_init(&s->mouse_sdk, &s->controller); mp = &s->mouse_sdk.mouse_processor; + } else if (options->mouse_input_mode == SC_MOUSE_INPUT_MODE_UHID) { + bool ok = sc_mouse_uhid_init(&s->mouse_uhid, &s->controller); + if (!ok) { + goto end; + } + mp = &s->mouse_uhid.mouse_processor; } sc_controller_configure(&s->controller, acksync, uhid_devices); diff --git a/app/src/uhid/mouse_uhid.c b/app/src/uhid/mouse_uhid.c new file mode 100644 index 00000000..77446f9e --- /dev/null +++ b/app/src/uhid/mouse_uhid.c @@ -0,0 +1,89 @@ +#include "mouse_uhid.h" + +#include "hid/hid_mouse.h" +#include "input_events.h" +#include "util/log.h" + +/** Downcast mouse processor to mouse_uhid */ +#define DOWNCAST(MP) container_of(MP, struct sc_mouse_uhid, mouse_processor) + +#define UHID_MOUSE_ID 2 + +static void +sc_mouse_uhid_send_input(struct sc_mouse_uhid *mouse, + const struct sc_hid_event *event, const char *name) { + struct sc_control_msg msg; + msg.type = SC_CONTROL_MSG_TYPE_UHID_INPUT; + msg.uhid_input.id = UHID_MOUSE_ID; + + assert(event->size <= SC_HID_MAX_SIZE); + memcpy(msg.uhid_input.data, event->data, event->size); + msg.uhid_input.size = event->size; + + if (!sc_controller_push_msg(mouse->controller, &msg)) { + LOGE("Could not send UHID_INPUT message (%s)", name); + } +} + +static void +sc_mouse_processor_process_mouse_motion(struct sc_mouse_processor *mp, + const struct sc_mouse_motion_event *event) { + struct sc_mouse_uhid *mouse = DOWNCAST(mp); + + struct sc_hid_event hid_event; + sc_hid_mouse_event_from_motion(&hid_event, event); + + sc_mouse_uhid_send_input(mouse, &hid_event, "mouse motion"); +} + +static void +sc_mouse_processor_process_mouse_click(struct sc_mouse_processor *mp, + const struct sc_mouse_click_event *event) { + struct sc_mouse_uhid *mouse = DOWNCAST(mp); + + struct sc_hid_event hid_event; + sc_hid_mouse_event_from_click(&hid_event, event); + + sc_mouse_uhid_send_input(mouse, &hid_event, "mouse click"); +} + +static void +sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp, + const struct sc_mouse_scroll_event *event) { + struct sc_mouse_uhid *mouse = DOWNCAST(mp); + + struct sc_hid_event hid_event; + sc_hid_mouse_event_from_scroll(&hid_event, event); + + sc_mouse_uhid_send_input(mouse, &hid_event, "mouse scroll"); +} + +bool +sc_mouse_uhid_init(struct sc_mouse_uhid *mouse, + struct sc_controller *controller) { + mouse->controller = controller; + + static const struct sc_mouse_processor_ops ops = { + .process_mouse_motion = sc_mouse_processor_process_mouse_motion, + .process_mouse_click = sc_mouse_processor_process_mouse_click, + .process_mouse_scroll = sc_mouse_processor_process_mouse_scroll, + // Touch events not supported (coordinates are not relative) + .process_touch = NULL, + }; + + mouse->mouse_processor.ops = &ops; + + mouse->mouse_processor.relative_mode = true; + + struct sc_control_msg msg; + msg.type = SC_CONTROL_MSG_TYPE_UHID_CREATE; + msg.uhid_create.id = UHID_MOUSE_ID; + msg.uhid_create.report_desc = SC_HID_MOUSE_REPORT_DESC; + msg.uhid_create.report_desc_size = SC_HID_MOUSE_REPORT_DESC_LEN; + if (!sc_controller_push_msg(controller, &msg)) { + LOGE("Could not send UHID_CREATE message (mouse)"); + return false; + } + + return true; +} diff --git a/app/src/uhid/mouse_uhid.h b/app/src/uhid/mouse_uhid.h new file mode 100644 index 00000000..f117ba97 --- /dev/null +++ b/app/src/uhid/mouse_uhid.h @@ -0,0 +1,19 @@ +#ifndef SC_MOUSE_UHID_H +#define SC_MOUSE_UHID_H + +#include + +#include "controller.h" +#include "trait/mouse_processor.h" + +struct sc_mouse_uhid { + struct sc_mouse_processor mouse_processor; // mouse processor trait + + struct sc_controller *controller; +}; + +bool +sc_mouse_uhid_init(struct sc_mouse_uhid *mouse, + struct sc_controller *controller); + +#endif From 1c5ad0e8131c6e051e940c29acdc81a500df4673 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 25 Feb 2024 18:35:52 +0100 Subject: [PATCH 1791/2244] Reassign -K and -M to UHID keyboard and mouse The options were deprecated, but for convenience, reassign them to aliases for --keyboard=uhid and --mouse=uhid respectively. Their long version (--hid-keyboard and --hid-mouse) remain deprecated. PR #4473 --- app/data/bash-completion/scrcpy | 2 ++ app/data/zsh-completion/_scrcpy | 2 ++ app/scrcpy.1 | 8 +++++++ app/src/cli.c | 41 +++++++++++++++++++-------------- 4 files changed, 36 insertions(+), 17 deletions(-) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index 8cc0b157..e6b2c91a 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -27,6 +27,7 @@ _scrcpy() { --force-adb-forward --forward-all-clicks -h --help + -K --keyboard= --kill-adb-on-close --legacy-paste @@ -37,6 +38,7 @@ _scrcpy() { --lock-video-orientation --lock-video-orientation= -m --max-size= + -M --max-fps= --mouse= -n --no-control diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 1cf2ae41..a23240ec 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -34,6 +34,7 @@ arguments=( '--force-adb-forward[Do not attempt to use \"adb reverse\" to connect to the device]' '--forward-all-clicks[Forward clicks to device]' {-h,--help}'[Print the help]' + '-K[Use UHID keyboard (same as --keyboard=uhid)]' '--keyboard[Set the keyboard input mode]:mode:(disabled sdk uhid aoa)' '--kill-adb-on-close[Kill adb when scrcpy terminates]' '--legacy-paste[Inject computer clipboard text as a sequence of key events on Ctrl+v]' @@ -43,6 +44,7 @@ arguments=( '--list-encoders[List video and audio encoders available on the device]' '--lock-video-orientation=[Lock video orientation]:orientation:(unlocked initial 0 90 180 270)' {-m,--max-size=}'[Limit both the width and height of the video to value]' + '-M[Use UHID mouse (same as --mouse=uhid)]' '--max-fps=[Limit the frame rate of screen capture]' '--mouse[Set the mouse input mode]:mode:(disabled sdk uhid aoa)' {-n,--no-control}'[Disable device control \(mirror the device in read only\)]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 8c0c4cc6..13ad28f9 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -171,6 +171,10 @@ By default, right-click triggers BACK (or POWER on) and middle-click triggers HO .B \-h, \-\-help Print this help. +.TP +.B \-K +Same as \fB\-\-keyboard=uhid\fR. + .TP .BI "\-\-keyboard " mode Select how to send keyboard inputs to the device. @@ -232,6 +236,10 @@ Limit both the width and height of the video to \fIvalue\fR. The other dimension Default is 0 (unlimited). +.TP +.B \-M +Same as \fB\-\-mouse=uhid\fR. + .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 dceb8fff..cb5be008 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -95,6 +95,8 @@ enum { OPT_ORIENTATION, OPT_KEYBOARD, OPT_MOUSE, + OPT_HID_KEYBOARD_DEPRECATED, + OPT_HID_MOUSE_DEPRECATED, }; struct sc_option { @@ -360,6 +362,10 @@ static const struct sc_option options[] = { .longopt = "help", .text = "Print this help.", }, + { + .shortopt = 'K', + .text = "Same as --keyboard=uhid.", + }, { .longopt_id = OPT_KEYBOARD, .longopt = "keyboard", @@ -391,7 +397,8 @@ static const struct sc_option options[] = { }, { // deprecated - .shortopt = 'K', + //.shortopt = 'K', // old, reassigned + .longopt_id = OPT_HID_KEYBOARD_DEPRECATED, .longopt = "hid-keyboard", }, { @@ -447,9 +454,14 @@ static const struct sc_option options[] = { }, { // deprecated - .shortopt = 'M', + //.shortopt = 'M', // old, reassigned + .longopt_id = OPT_HID_MOUSE_DEPRECATED, .longopt = "hid-mouse", }, + { + .shortopt = 'M', + .text = "Same as --mouse=uhid.", + }, { .longopt_id = OPT_MAX_FPS, .longopt = "max-fps", @@ -2089,20 +2101,17 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], args->help = true; break; case 'K': -#ifdef HAVE_USB - LOGW("-K/--hid-keyboard is deprecated, use --keyboard=aoa " - "instead."); - opts->keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_AOA; + opts->keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_UHID; break; -#else - LOGE("HID over AOA (-K/--hid-keyboard) is disabled."); - return false; -#endif case OPT_KEYBOARD: if (!parse_keyboard(optarg, &opts->keyboard_input_mode)) { return false; } break; + case OPT_HID_KEYBOARD_DEPRECATED: + LOGE("--hid-keyboard has been removed, use --keyboard=aoa or " + "--keyboard=uhid instead."); + return false; case OPT_MAX_FPS: if (!parse_max_fps(optarg, &opts->max_fps)) { return false; @@ -2114,19 +2123,17 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } break; case 'M': -#ifdef HAVE_USB - LOGW("-M/--hid-mouse is deprecated, use --mouse=aoa instead."); - opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_AOA; + opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_UHID; break; -#else - LOGE("HID over AOA (-M/--hid-mouse) is disabled."); - return false; -#endif case OPT_MOUSE: if (!parse_mouse(optarg, &opts->mouse_input_mode)) { return false; } break; + case OPT_HID_MOUSE_DEPRECATED: + LOGE("--hid-mouse has been removed, use --mouse=aoa or " + "--mouse=uhid instead."); + return false; case OPT_LOCK_VIDEO_ORIENTATION: if (!parse_lock_video_orientation(optarg, &opts->lock_video_orientation)) { From 5f12132c47178d88ca73f17e90de6891aee33f2d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 28 Feb 2024 22:46:48 +0100 Subject: [PATCH 1792/2244] Do not fallback keyboard mode if AOA fails Initially, if AOA initialization failed, default injection method was used, in order to use the same command/shortcut when the device is connected via USB or via TCP/IP, without changing the arguments. Now that there are 3 keyboard modes, it seems unexpected to switch to another specific mode if AOA fails (and it is inconsistent). If the user explicitly requests AOA, then use AOA or fail. Refs #2632 comment PR #4473 --- app/src/scrcpy.c | 34 +++++----------------------------- 1 file changed, 5 insertions(+), 29 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index a40a4dec..c63a95c2 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -570,7 +570,7 @@ scrcpy(struct scrcpy_options *options) { if (!ok) { LOGE("Failed to initialize USB"); sc_acksync_destroy(&s->acksync); - goto aoa_hid_end; + goto end; } assert(serial); @@ -578,7 +578,7 @@ scrcpy(struct scrcpy_options *options) { ok = sc_usb_select_device(&s->usb, serial, &usb_device); if (!ok) { sc_usb_destroy(&s->usb); - goto aoa_hid_end; + goto end; } LOGI("USB device: %s (%04" PRIx16 ":%04" PRIx16 ") %s %s", @@ -591,7 +591,7 @@ scrcpy(struct scrcpy_options *options) { LOGE("Failed to connect to USB device %s", serial); sc_usb_destroy(&s->usb); sc_acksync_destroy(&s->acksync); - goto aoa_hid_end; + goto end; } ok = sc_aoa_init(&s->aoa, &s->usb, &s->acksync); @@ -600,7 +600,7 @@ scrcpy(struct scrcpy_options *options) { sc_usb_disconnect(&s->usb); sc_usb_destroy(&s->usb); sc_acksync_destroy(&s->acksync); - goto aoa_hid_end; + goto end; } if (use_keyboard_aoa) { @@ -628,41 +628,18 @@ scrcpy(struct scrcpy_options *options) { sc_usb_disconnect(&s->usb); sc_usb_destroy(&s->usb); sc_aoa_destroy(&s->aoa); - goto aoa_hid_end; + goto end; } acksync = &s->acksync; aoa_hid_initialized = true; - -aoa_hid_end: - if (!aoa_hid_initialized) { - if (keyboard_aoa_initialized) { - sc_keyboard_aoa_destroy(&s->keyboard_aoa); - keyboard_aoa_initialized = false; - } - if (mouse_aoa_initialized) { - sc_mouse_aoa_destroy(&s->mouse_aoa); - mouse_aoa_initialized = false; - } - } - - if (use_keyboard_aoa && !keyboard_aoa_initialized) { - LOGE("Fallback to --keyboard=sdk (--keyboard=aoa ignored)"); - options->keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_SDK; - } - - if (use_mouse_aoa && !mouse_aoa_initialized) { - LOGE("Fallback to --keyboard=sdk (--keyboard=aoa ignored)"); - options->mouse_input_mode = SC_MOUSE_INPUT_MODE_SDK; - } } #else assert(options->keyboard_input_mode != SC_KEYBOARD_INPUT_MODE_AOA); assert(options->mouse_input_mode != SC_MOUSE_INPUT_MODE_AOA); #endif - // keyboard_input_mode may have been reset if AOA mode failed if (options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_SDK) { sc_keyboard_sdk_init(&s->keyboard_sdk, &s->controller, options->key_inject_mode, @@ -680,7 +657,6 @@ aoa_hid_end: kp = &s->keyboard_uhid.key_processor; } - // mouse_input_mode may have been reset if AOA mode failed if (options->mouse_input_mode == SC_MOUSE_INPUT_MODE_SDK) { sc_mouse_sdk_init(&s->mouse_sdk, &s->controller); mp = &s->mouse_sdk.mouse_processor; From dd479ed17613e8f7d7c2cf2447f57045815192b8 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 29 Feb 2024 08:49:18 +0100 Subject: [PATCH 1793/2244] Check options specific to SDK keyboard Fail if an option specific to --keyboard=sdk is passed with another keyboard input mode. PR #4473 --- app/src/cli.c | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/app/src/cli.c b/app/src/cli.c index cb5be008..daa041cf 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -2611,6 +2611,23 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } } + if (opts->keyboard_input_mode != SC_KEYBOARD_INPUT_MODE_SDK) { + if (opts->key_inject_mode == SC_KEY_INJECT_MODE_TEXT) { + LOGE("--prefer-text is specific to --keyboard=sdk"); + return false; + } + + if (opts->key_inject_mode == SC_KEY_INJECT_MODE_RAW) { + LOGE("--raw-key-events is specific to --keyboard=sdk"); + return false; + } + + if (!opts->forward_key_repeat) { + LOGE("--no-key-repeat is specific to --keyboard=sdk"); + return false; + } + } + if ((opts->tunnel_host || opts->tunnel_port) && !opts->force_adb_forward) { LOGI("Tunnel host/port is set, " "--force-adb-forward automatically enabled."); From b9d244b4c9eaca05f9202126519200603ebd0cbe Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 29 Feb 2024 10:00:56 +0100 Subject: [PATCH 1794/2244] Document UHID Rework the documentation to present the keyboard and mouse input modes. PR #4473 --- FAQ.md | 9 ++-- README.md | 14 +++-- doc/control.md | 51 ++---------------- doc/develop.md | 2 +- doc/hid-otg.md | 112 --------------------------------------- doc/keyboard.md | 136 ++++++++++++++++++++++++++++++++++++++++++++++++ doc/mouse.md | 70 +++++++++++++++++++++++++ doc/otg.md | 37 +++++++++++++ 8 files changed, 262 insertions(+), 169 deletions(-) delete mode 100644 doc/hid-otg.md create mode 100644 doc/keyboard.md create mode 100644 doc/mouse.md create mode 100644 doc/otg.md diff --git a/FAQ.md b/FAQ.md index 6d02361b..5f089cd7 100644 --- a/FAQ.md +++ b/FAQ.md @@ -133,9 +133,9 @@ Try with another USB cable or plug it into another USB port. See [#281] and [#283]: https://github.com/Genymobile/scrcpy/issues/283 -## HID/OTG issues on Windows +## OTG issues on Windows -On Windows, if `scrcpy --otg` (or `--hid-keyboard`/`--hid-mouse`) results in: +On Windows, if `scrcpy --otg` (or `--keyboard=aoa`/`--mouse=aoa`) results in: > ERROR: Could not find any USB device @@ -170,12 +170,13 @@ The default text injection method is [limited to ASCII characters][text-input]. A trick allows to also inject some [accented characters][accented-characters], but that's all. See [#37]. -It is also possible to simulate a [physical keyboard][hid] (HID). +To avoid the problem, [change the keyboard mode to simulate a physical +keyboard][hid]. [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 -[hid]: doc/hid-otg.md +[hid]: doc/keyboard.md#physical-keyboard-simulation ## Client issues diff --git a/README.md b/README.md index 8fabd556..7a671018 100644 --- a/README.md +++ b/README.md @@ -32,10 +32,13 @@ Its features include: - [configurable quality](doc/video.md) - [camera mirroring](doc/camera.md) (Android 12+) - [mirroring as a webcam (V4L2)](doc/v4l2.md) (Linux-only) - - [physical keyboard/mouse simulation (HID)](doc/hid-otg.md) - - [OTG mode](doc/hid-otg.md#otg) + - physical [keyboard][hid-keyboard] and [mouse][hid-mouse] simulation (HID) + - [OTG mode](doc/otg.md) - and more… +[hid-keyboard]: doc/keyboard.md#physical-keyboard-simulation +[hid-mouse]: doc/mouse.md#physical-mouse-simulation + ## Prerequisites The Android device requires at least API 21 (Android 5.0). @@ -53,8 +56,7 @@ this option is set. [control]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323 -Note that USB debugging is not required to run scrcpy in [OTG -mode](doc/hid-otg.md#otg). +Note that USB debugging is not required to run scrcpy in [OTG mode](doc/otg.md). ## Get the app @@ -73,11 +75,13 @@ documented in the following pages: - [Video](doc/video.md) - [Audio](doc/audio.md) - [Control](doc/control.md) + - [Keyboard](doc/keyboard.md) + - [Mouse](doc/mouse.md) - [Device](doc/device.md) - [Window](doc/window.md) - [Recording](doc/recording.md) - [Tunnels](doc/tunnels.md) - - [HID/OTG](doc/hid-otg.md) + - [OTG](doc/otg.md) - [Camera](doc/camera.md) - [Video4Linux](doc/v4l2.md) - [Shortcuts](doc/shortcuts.md) diff --git a/doc/control.md b/doc/control.md index 595e910e..d6d1265c 100644 --- a/doc/control.md +++ b/doc/control.md @@ -10,36 +10,9 @@ scrcpy --no-control scrcpy -n # short version ``` +## Keyboard and mouse -## Text injection preference - -Two kinds of [events][textevents] are generated when typing text: - - _key events_, signaling that a key is pressed or released; - - _text events_, signaling that a text has been entered. - -By default, letters are injected using key events, so that the keyboard behaves -as expected in games (typically for WASD keys). - -But this may [cause issues][prefertext]. If you encounter such a problem, you -can avoid it by: - -```bash -scrcpy --prefer-text -``` - -(but this will break keyboard behavior in games) - -On the contrary, you could force to always inject raw key events: - -```bash -scrcpy --raw-key-events -``` - -These options have no effect on HID keyboard (all key events are sent as -scancodes in this mode). - -[textevents]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-text-input -[prefertext]: https://github.com/Genymobile/scrcpy/issues/650#issuecomment-512945343 +Read [keyboard](keyboard.md) and [mouse](mouse.md). ## Copy-paste @@ -85,6 +58,7 @@ way as MOD+Shift+v). To disable automatic clipboard synchronization, use `--no-clipboard-autosync`. + ## Pinch-to-zoom, rotate and tilt simulation To simulate "pinch-to-zoom": Ctrl+_click-and-move_. @@ -100,20 +74,7 @@ at a location inverted through the center of the screen. When pressing Ctrl the x and y coordinates are inverted. Using Shift only inverts x. - -## 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 -``` - -This option has no effect on HID keyboard (key repeat is handled by Android -directly in this mode). +This only works for the default mouse mode (`--mouse=sdk`). ## Right-click and middle-click @@ -147,7 +108,3 @@ The target directory can be changed on start: ```bash scrcpy --push-target=/sdcard/Movies/ ``` - -## Physical keyboard and mouse simulation - -See the dedicated [HID/OTG](hid-otg.md) page. diff --git a/doc/develop.md b/doc/develop.md index 67d7f9b0..e5274783 100644 --- a/doc/develop.md +++ b/doc/develop.md @@ -234,7 +234,7 @@ The video and audio streams are decoded by [FFmpeg]. The client parses the command line arguments, then [runs one of two code paths][run]: - scrcpy in "normal" mode ([`scrcpy.c`]) - - scrcpy in [OTG mode](hid-otg.md) ([`scrcpy_otg.c`]) + - scrcpy in [OTG mode](otg.md) ([`scrcpy_otg.c`]) [run]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/app/src/main.c#L81-L82 [`scrcpy.c`]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/app/src/scrcpy.c#L292-L293 diff --git a/doc/hid-otg.md b/doc/hid-otg.md deleted file mode 100644 index 7dfc60fc..00000000 --- a/doc/hid-otg.md +++ /dev/null @@ -1,112 +0,0 @@ -# HID/OTG - -By default, _scrcpy_ injects input events at the Android API level. As an -alternative, when connected over USB, it is possible to send HID events, so that -scrcpy behaves as if it was a physical keyboard and/or mouse connected to the -Android device. - -A special [OTG](#otg) mode allows to control the device without mirroring (and -without USB debugging). - - -## Physical keyboard simulation - -By default, _scrcpy_ uses Android key or text injection. It works everywhere, -but is limited to ASCII. - -Instead, it can simulate a physical USB keyboard on Android to provide a better -input experience (using [USB HID over AOAv2][hid-aoav2]): the virtual keyboard -is disabled and it works for all characters and IME. - -[hid-aoav2]: https://source.android.com/devices/accessories/aoa2#hid-support - -However, it only works if the device is connected via USB. - -Note: On Windows, it may only work in [OTG mode](#otg), not while mirroring (it -is not possible to open a USB device if it is already open by another process -like the _adb daemon_). - -To enable this mode: - -```bash -scrcpy --hid-keyboard -scrcpy -K # short version -``` - -If it fails for some reason (for example because the device is not connected via -USB), it automatically fallbacks to the default mode (with a log in the -console). This allows using the same command line options when connected over -USB and TCP/IP. - -In this mode, raw key events (scancodes) are sent to the device, independently -of the host key mapping. Therefore, if your keyboard layout does not match, it -must be configured on the Android device, in Settings → System → Languages and -input → [Physical keyboard]. - -This settings page can be started directly: - -```bash -adb shell am start -a android.settings.HARD_KEYBOARD_SETTINGS -``` - -However, the option is only available when the HID keyboard is enabled (or when -a physical keyboard is connected). - -[Physical keyboard]: https://github.com/Genymobile/scrcpy/pull/2632#issuecomment-923756915 - - -## Physical mouse simulation - -By default, _scrcpy_ uses Android mouse events injection with absolute -coordinates. By simulating a physical mouse, a mouse pointer appears on the -Android device, and relative mouse motion, clicks and scrolls are injected. - -To enable this mode: - -```bash -scrcpy --hid-mouse -scrcpy -M # short version -``` - -When this mode is enabled, the computer mouse is "captured" (the mouse pointer -disappears from the computer and appears on the Android device instead). - -Special capture keys, either Alt or Super, toggle -(disable or enable) the mouse capture. Use one of them to give the control of -the mouse back to the computer. - - -## OTG - -It is possible to run _scrcpy_ with only physical keyboard and mouse simulation -(HID), as if the computer keyboard and mouse were plugged directly to the device -via an OTG cable. - -In this mode, `adb` (USB debugging) is not necessary, and mirroring is disabled. - -This is similar to `--hid-keyboard --hid-mouse`, but without mirroring. - -To enable OTG mode: - -```bash -scrcpy --otg -# Pass the serial if several USB devices are available -scrcpy --otg -s 0123456789abcdef -``` - -It is possible to enable only HID keyboard or HID mouse: - -```bash -scrcpy --otg --hid-keyboard # keyboard only -scrcpy --otg --hid-mouse # mouse only -scrcpy --otg --hid-keyboard --hid-mouse # keyboard and mouse -# for convenience, enable both by default -scrcpy --otg # keyboard and mouse -``` - -Like `--hid-keyboard` and `--hid-mouse`, it only works if the device is -connected over USB. - -## HID/OTG issues on Windows - -See [FAQ](/FAQ.md#hidotg-issues-on-windows). diff --git a/doc/keyboard.md b/doc/keyboard.md new file mode 100644 index 00000000..80dfe070 --- /dev/null +++ b/doc/keyboard.md @@ -0,0 +1,136 @@ +# Keyboard + +Several keyboard input modes are available: + + - `--keyboard=sdk` (default) + - `--keyboard=uhid` (or `-K`): simulates a physical HID keyboard using the UHID + kernel module on the device + - `--keyboard=aoa`: simulates a physical HID keyboard using the AOAv2 protocol + - `--keyboard=disabled` + +By default, `sdk` is used, but if you use scrcpy regularly, it is recommended to +use [`uhid`](#uhid) and configure the keyboard layout once and for all. + + +## SDK keyboard + +In this mode (`--keyboard=sdk`, or if the parameter is omitted), keyboard input +events are injected at the Android API level. It works everywhere, but it is +limited to ASCII and some other characters. + +Note that on some devices, an additional option must be enabled in developer +options for this keyboard mode to work. See +[prerequisites](/README.md#prerequisites). + +Additional parameters (specific to `--keyboard=sdk`) described below allow to +customize the behavior. + + +### Text injection preference + +Two kinds of [events][textevents] are generated when typing text: + - _key events_, signaling that a key is pressed or released; + - _text events_, signaling that a text has been entered. + +By default, numbers and "special characters" are inserted using text events, but +letters are injected using key events, so that the keyboard behaves as expected +in games (typically for WASD keys). + +But this may [cause issues][prefertext]. If you encounter such a problem, you +can inject letters as text (or just switch to [UHID](#uhid)): + +```bash +scrcpy --prefer-text +``` + +(but this will break keyboard behavior in games) + +On the contrary, you could force to always inject raw key events: + +```bash +scrcpy --raw-key-events +``` + +[textevents]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-text-input +[prefertext]: https://github.com/Genymobile/scrcpy/issues/650#issuecomment-512945343 + + +### Key repeat + +By default, holding a key down generates repeated key events. Ths can cause +performance problems in some games, where these events are useless anyway. + +To avoid forwarding repeated key events: + +```bash +scrcpy --no-key-repeat +``` + + +## Physical keyboard simulation + +Two modes allow to simulate a physical HID keyboard on the device. + +To work properly, it is necessary to configure (once and for all) the keyboard +layout on the device to match that of the computer. + +The configuration page can be opened in one of the following ways: + - from the scrcpy window (when `uhid` or `aoa` is used), by pressing + MOD+k (see [shortcuts](shortcuts.md)) + - from the device, in Settings → System → Languages and input → Physical + devices + - from a terminal on the computer, by executing `adb shell am start -a + android.settings.HARD_KEYBOARD_SETTINGS` + +From this configuration page, it is also possible to enable or disable on-screen +keyboard. + + +### UHID + +This mode simulates a physical HID keyboard using the [UHID] kernel module on the +device. + +[UHID]: https://kernel.org/doc/Documentation/hid/uhid.txt + +To enable UHID keyboard, use: + +```bash +scrcpy --keyboard=uhid +scrcpy -K # short version +``` + +Once the keyboard layout is configured (see above), it is the best mode for +using the keyboard while mirroring: + + - it works for all characters and IME (contrary to `--keyboard=sdk`) + - the on-screen keyboard can be disabled (contrary to `--keyboard=sdk`) + - it works over TCP/IP (wirelessly) (contrary to `--keyboard=aoa`) + - there are no issues on Windows (contrary to `--keyboard=aoa`) + +One drawback is that it may not work on old Android versions due to permission +errors. + + +### AOA + +This mode simulates a physical HID keyboard using the [AOAv2] protocol. + +[AOAv2]: https://source.android.com/devices/accessories/aoa2#hid-support + +To enable AOA keyboard, use: + +```bash +scrcpy --keyboard=aoa +``` + +Contrary to the other modes, it works at the USB level directly (so it only +works over USB). + +It does not use the scrcpy server, and does not require `adb` (USB debugging). +Therefore, it is possible to control the device (but not mirror) even with USB +debugging disabled (see [OTG](otg.md)). + +Note: On Windows, it may only work in [OTG mode](otg.md), not while mirroring +(it is not possible to open a USB device if it is already open by another +process like the _adb daemon_). diff --git a/doc/mouse.md b/doc/mouse.md new file mode 100644 index 00000000..d0342954 --- /dev/null +++ b/doc/mouse.md @@ -0,0 +1,70 @@ +# Mouse + +Several mouse input modes are available: + + - `--mouse=sdk` (default) + - `--mouse=uhid` (or `-M`): simulates a physical HID mouse using the UHID + kernel module on the device + - `--mouse=aoa`: simulates a physical HID mouse using the AOAv2 protocol + - `--mouse=disabled` + + +## SDK mouse + +In this mode (`--mouse=sdk`, or if the parameter is omitted), mouse input events +are injected at the Android API level with absolute coordinates. + +Note that on some devices, an additional option must be enabled in developer +options for this mouse mode to work. See +[prerequisites](/README.md#prerequisites). + + +## Physical mouse simulation + +Two modes allow to simulate a physical HID mouse on the device. + +In these modes, the computer mouse is "captured": the mouse pointer disappears +from the computer and appears on the Android device instead. + +Special capture keys, either Alt or Super, toggle +(disable or enable) the mouse capture. Use one of them to give the control of +the mouse back to the computer. + + +### UHID + +This mode simulates a physical HID mouse using the [UHID] kernel module on the +device. + +[UHID]: https://kernel.org/doc/Documentation/hid/uhid.txt + +To enable UHID mouse, use: + +```bash +scrcpy --mouse=uhid +scrcpy -M # short version +``` + + +### AOA + +This mode simulates a physical HID mouse using the [AOAv2] protocol. + +[AOAv2]: https://source.android.com/devices/accessories/aoa2#hid-support + +To enable AOA mouse, use: + +```bash +scrcpy --mouse=aoa +``` + +Contrary to the other modes, it works at the USB level directly (so it only +works over USB). + +It does not use the scrcpy server, and does not require `adb` (USB debugging). +Therefore, it is possible to control the device (but not mirror) even with USB +debugging disabled (see [OTG](otg.md)). + +Note: On Windows, it may only work in [OTG mode](otg.md), not while mirroring +(it is not possible to open a USB device if it is already open by another +process like the _adb daemon_). diff --git a/doc/otg.md b/doc/otg.md new file mode 100644 index 00000000..3c7ed467 --- /dev/null +++ b/doc/otg.md @@ -0,0 +1,37 @@ +# OTG + +By default, _scrcpy_ injects input events at the Android API level. As an +alternative, when connected over USB, it is possible to send HID events, so that +scrcpy behaves as if it was a physical keyboard and/or mouse connected to the +Android device. + +A special mode allows to control the device without mirroring, using AOA +[keyboard](keyboard.md#aoa) and [mouse](mouse.md#aoa). Therefore, it is possible +to run _scrcpy_ with only physical keyboard and mouse simulation (HID), as if +the computer keyboard and mouse were plugged directly to the device via an OTG +cable. + +In this mode, `adb` (USB debugging) is not necessary, and mirroring is disabled. + +This is similar to `--keyboard=aoa --mouse=aoa`, but without mirroring. + +To enable OTG mode: + +```bash +scrcpy --otg +# Pass the serial if several USB devices are available +scrcpy --otg -s 0123456789abcdef +``` + +It is possible to disable HID keyboard or HID mouse: + +```bash +scrcpy --otg --keyboard=disabled +scrcpy --otg --mouse=disabled +``` + +It only works if the device is connected over USB. + +## OTG issues on Windows + +See [FAQ](/FAQ.md#otg-issues-on-windows). From bf069bd37bb064fb49b7f75c6ea665706b599784 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 29 Feb 2024 09:01:25 +0100 Subject: [PATCH 1795/2244] Document usage examples This exposes several common options on the front page. --- README.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/README.md b/README.md index 7a671018..701bb075 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,41 @@ Note that USB debugging is not required to run scrcpy in [OTG mode](doc/otg.md). - [macOS](doc/macos.md) +## Usage examples + +There are a lot of options, [documented](#user-documentation) in separate pages. +Here are just some common examples. + + - Capture the screen in H.265 (better quality), limit the size to 1920, limit + the frame rate to 60fps, disable audio, and control the device by simulating + a physical keyboard: + + ```bash + scrcpy --video-codec=h265 --max-size=1920 --max-fps=60 --no-audio --keyboard=uhid + scrcpy --video-codec=h265 -m1920 --max-fps=60 --no-audio -K # short version + ``` + + - Record the device camera in H.265 at 1920x1080 (and microphone) to an MP4 + file: + + ```bash + scrcpy --video-source=camera --video-codec=h265 --camera-size=1920x1080 --record=file.mp4 + ``` + + - Capture the device front camera and expose it as a webcam on the computer (on + Linux): + + ```bash + scrcpy --video-source=camera --camera-size=1920x1080 --camera-facing=front --v4l2-sink=/dev/video2 --no-playback + ``` + + - Control the device without mirroring by simulating a physical keyboard and + mouse (USB debugging not required): + + ```bash + scrcpy --otg + ``` + ## User documentation The application provides a lot of features and configuration options. They are From cdf09805c042cc760397b08d9e7cf58fbf8f76a4 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 29 Feb 2024 19:37:05 +0100 Subject: [PATCH 1796/2244] Add missing initialization --- app/src/display.c | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/display.c b/app/src/display.c index ba15cd14..c8df615d 100644 --- a/app/src/display.c +++ b/app/src/display.c @@ -62,6 +62,7 @@ sc_display_init(struct sc_display *display, SDL_Window *window, bool mipmaps) { LOGD("Trilinear filtering disabled (not an OpenGL renderer)"); } + display->texture = NULL; display->pending.flags = 0; display->pending.frame = NULL; From fd0f432e877153d83ed435474fb7b04e41de4269 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 29 Feb 2024 19:37:14 +0100 Subject: [PATCH 1797/2244] Detect missing initializations Write invalid data in memory to easily detect missing initializations in debug mode. --- app/src/scrcpy.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index c63a95c2..eb9cd201 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -312,6 +312,10 @@ scrcpy_generate_scid(void) { enum scrcpy_exit_code scrcpy(struct scrcpy_options *options) { static struct scrcpy scrcpy; +#ifndef NDEBUG + // Detect missing initializations + memset(&scrcpy, 42, sizeof(scrcpy)); +#endif struct scrcpy *s = &scrcpy; // Minimal SDL initialization From 4dca08cfe3eadd4438bf235bd62050059aec1801 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 1 Mar 2024 09:54:31 +0100 Subject: [PATCH 1798/2244] Set SDL hints before creating any thread To avoid race conditions in SDL (reported by TSAN). --- app/src/scrcpy.c | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index eb9cd201..961f6202 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -409,6 +409,12 @@ scrcpy(struct scrcpy_options *options) { return SCRCPY_EXIT_FAILURE; } + if (options->video_playback) { + // Set hints before starting the server thread to avoid race conditions + // in SDL + sdl_set_hints(options->render_driver); + } + if (!sc_server_start(&s->server)) { goto end; } @@ -425,10 +431,6 @@ scrcpy(struct scrcpy_options *options) { assert(!options->video_playback || options->video); assert(!options->audio_playback || options->audio); - if (options->video_playback) { - sdl_set_hints(options->render_driver); - } - if (options->video_playback || (options->control && options->clipboard_autosync)) { // Initialize the video subsystem even if --no-video or From 36189b90ea815d8fced961c36c80f146d5952324 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 1 Mar 2024 09:55:32 +0100 Subject: [PATCH 1799/2244] Remove spurious line --- app/src/scrcpy.c | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 961f6202..f43af35e 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -106,7 +106,6 @@ static BOOL WINAPI windows_ctrl_handler(DWORD ctrl_type) { static void sdl_set_hints(const char *render_driver) { - if (render_driver && !SDL_SetHint(SDL_HINT_RENDER_DRIVER, render_driver)) { LOGW("Could not set render driver"); } From 125b1103e1cdfe676d66a9223df82c423c5e75bf Mon Sep 17 00:00:00 2001 From: inson1 <75314629+inson1@users.noreply.github.com> Date: Sat, 2 Mar 2024 15:39:56 +0100 Subject: [PATCH 1800/2244] Happy new year 2024! PR #4716 Signed-off-by: Romain Vimont --- LICENSE | 2 +- README.md | 2 +- app/scrcpy.1 | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/LICENSE b/LICENSE index 55f96811..d9326a74 100644 --- a/LICENSE +++ b/LICENSE @@ -188,7 +188,7 @@ identification within third-party archives. Copyright (C) 2018 Genymobile - Copyright (C) 2018-2023 Romain Vimont + Copyright (C) 2018-2024 Romain Vimont Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index 0a3d03cb..5d9f04a9 100644 --- a/README.md +++ b/README.md @@ -134,7 +134,7 @@ work][donate]: ## Licence Copyright (C) 2018 Genymobile - Copyright (C) 2018-2023 Romain Vimont + Copyright (C) 2018-2024 Romain Vimont Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 0c34b4e2..eed1f355 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -689,7 +689,7 @@ Report bugs to . .SH COPYRIGHT Copyright \(co 2018 Genymobile -Copyright \(co 2018\-2023 Romain Vimont +Copyright \(co 2018\-2024 Romain Vimont Licensed under the Apache License, Version 2.0. From 8d87b91f692914ada1c146bd911ab4623552174b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 1 Mar 2024 20:02:00 +0100 Subject: [PATCH 1801/2244] Build dependencies from sources The project has 3 build dependencies: - SDL - FFmpeg - libusb For Windows, the release script downloaded pre-built build dependencies (either from upstream, or from the scrcpy-deps repository). Instead, download the source releases and build locally. This offers more flexibility. The official adb release is still downloaded and included as is in the release archive (it is not a build dependency). Also upgrade FFmpeg to 6.1.1 and libusb to 1.0.27. PR #4713 --- app/deps/.gitignore | 1 + app/deps/README | 27 ++++++ app/deps/adb.sh | 32 ++++++++ app/deps/common | 55 +++++++++++++ app/deps/ffmpeg.sh | 91 +++++++++++++++++++++ app/deps/libusb.sh | 44 ++++++++++ app/deps/patches/ffmpeg-6.1-fix-build.patch | 27 ++++++ app/deps/sdl.sh | 47 +++++++++++ app/prebuilt-deps/.gitignore | 1 - app/prebuilt-deps/common | 22 ----- app/prebuilt-deps/prepare-adb.sh | 32 -------- app/prebuilt-deps/prepare-ffmpeg.sh | 30 ------- app/prebuilt-deps/prepare-libusb.sh | 37 --------- app/prebuilt-deps/prepare-sdl.sh | 34 -------- release.mk | 50 ++++++----- 15 files changed, 347 insertions(+), 183 deletions(-) create mode 100644 app/deps/.gitignore create mode 100644 app/deps/README create mode 100755 app/deps/adb.sh create mode 100644 app/deps/common create mode 100755 app/deps/ffmpeg.sh create mode 100755 app/deps/libusb.sh create mode 100644 app/deps/patches/ffmpeg-6.1-fix-build.patch create mode 100755 app/deps/sdl.sh delete mode 100644 app/prebuilt-deps/.gitignore delete mode 100755 app/prebuilt-deps/common delete mode 100755 app/prebuilt-deps/prepare-adb.sh delete mode 100755 app/prebuilt-deps/prepare-ffmpeg.sh delete mode 100755 app/prebuilt-deps/prepare-libusb.sh delete mode 100755 app/prebuilt-deps/prepare-sdl.sh diff --git a/app/deps/.gitignore b/app/deps/.gitignore new file mode 100644 index 00000000..ccf6a49e --- /dev/null +++ b/app/deps/.gitignore @@ -0,0 +1 @@ +/work diff --git a/app/deps/README b/app/deps/README new file mode 100644 index 00000000..9cfb5c06 --- /dev/null +++ b/app/deps/README @@ -0,0 +1,27 @@ +This directory (app/deps/) contains: + +*.sh : shell scripts to download and build dependencies + +patches/ : patches to fix dependencies (used by scripts) + +work/sources/ : downloaded tarballs and extracted folders + ffmpeg-6.1.1.tar.xz + ffmpeg-6.1.1/ + libusb-1.0.27.tar.gz + libusb-1.0.27/ + ... +work/build/ : build dirs for each dependency/version/architecture + ffmpeg-6.1.1/win32/ + ffmpeg-6.1.1/win64/ + libusb-1.0.27/win32/ + libusb-1.0.27/win64/ + ... +work/install/ : install dirs for each architexture + win32/bin/ + win32/include/ + win32/lib/ + win32/share/ + win64/bin/ + win64/include/ + win64/lib/ + win64/share/ diff --git a/app/deps/adb.sh b/app/deps/adb.sh new file mode 100755 index 00000000..e2408216 --- /dev/null +++ b/app/deps/adb.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +set -ex +DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) +cd "$DEPS_DIR" +. common + +VERSION=34.0.5 +FILENAME=platform-tools_r$VERSION-windows.zip +PROJECT_DIR=platform-tools-$VERSION +SHA256SUM=3f8320152704377de150418a3c4c9d07d16d80a6c0d0d8f7289c22c499e33571 + +cd "$SOURCES_DIR" + +if [[ -d "$PROJECT_DIR" ]] +then + echo "$PWD/$PROJECT_DIR" found +else + get_file "https://dl.google.com/android/repository/$FILENAME" "$FILENAME" "$SHA256SUM" + mkdir -p "$PROJECT_DIR" + cd "$PROJECT_DIR" + ZIP_PREFIX=platform-tools + unzip "../$FILENAME" \ + "$ZIP_PREFIX"/AdbWinApi.dll \ + "$ZIP_PREFIX"/AdbWinUsbApi.dll \ + "$ZIP_PREFIX"/adb.exe + mv "$ZIP_PREFIX"/* . + rmdir "$ZIP_PREFIX" +fi + +mkdir -p "$INSTALL_DIR/$HOST/bin" +cd "$INSTALL_DIR/$HOST/bin" +cp -r "$SOURCES_DIR/$PROJECT_DIR"/. "$INSTALL_DIR/$HOST/bin/" diff --git a/app/deps/common b/app/deps/common new file mode 100644 index 00000000..c1cc7729 --- /dev/null +++ b/app/deps/common @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +# This file is intended to be sourced by other scripts, not executed + +if [[ $# != 1 ]] +then + # : win32 or win64 + echo "Syntax: $0 " >&2 + exit 1 +fi + +HOST="$1" + +if [[ "$HOST" = win32 ]] +then + HOST_TRIPLET=i686-w64-mingw32 +elif [[ "$HOST" = win64 ]] +then + HOST_TRIPLET=x86_64-w64-mingw32 +else + echo "Unsupported host: $HOST" >&2 + exit 1 +fi + +DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) +cd "$DEPS_DIR" + +PATCHES_DIR="$PWD/patches" + +WORK_DIR="$PWD/work" +SOURCES_DIR="$WORK_DIR/sources" +BUILD_DIR="$WORK_DIR/build" +INSTALL_DIR="$WORK_DIR/install" + +mkdir -p "$INSTALL_DIR" "$SOURCES_DIR" "$WORK_DIR" + +checksum() { + local file="$1" + local sum="$2" + echo "$file: verifying checksum..." + echo "$sum $file" | sha256sum -c +} + +get_file() { + local url="$1" + local file="$2" + local sum="$3" + if [[ -f "$file" ]] + then + echo "$file: found" + else + echo "$file: not found, downloading..." + wget "$url" -O "$file" + fi + checksum "$file" "$sum" +} diff --git a/app/deps/ffmpeg.sh b/app/deps/ffmpeg.sh new file mode 100755 index 00000000..19fb2991 --- /dev/null +++ b/app/deps/ffmpeg.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +set -ex +DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) +cd "$DEPS_DIR" +. common + +VERSION=6.1.1 +FILENAME=ffmpeg-$VERSION.tar.xz +PROJECT_DIR=ffmpeg-$VERSION +SHA256SUM=8684f4b00f94b85461884c3719382f1261f0d9eb3d59640a1f4ac0873616f968 + +cd "$SOURCES_DIR" + +if [[ -d "$PROJECT_DIR" ]] +then + echo "$PWD/$PROJECT_DIR" found +else + get_file "https://ffmpeg.org/releases/$FILENAME" "$FILENAME" "$SHA256SUM" + tar xf "$FILENAME" # First level directory is "$PROJECT_DIR" + patch -d "$PROJECT_DIR" -p1 < "$PATCHES_DIR"/ffmpeg-6.1-fix-build.patch +fi + +mkdir -p "$BUILD_DIR/$PROJECT_DIR" +cd "$BUILD_DIR/$PROJECT_DIR" + +if [[ "$HOST" = win32 ]] +then + ARCH=x86 +elif [[ "$HOST" = win64 ]] +then + ARCH=x86_64 +else + echo "Unsupported host: $HOST" >&2 + exit 1 +fi + +# -static-libgcc to avoid missing libgcc_s_dw2-1.dll +# -static to avoid dynamic dependency to zlib +export CFLAGS='-static-libgcc -static' +export CXXFLAGS="$CFLAGS" +export LDFLAGS='-static-libgcc -static' + +if [[ -d "$HOST" ]] +then + echo "'$PWD/$HOST' already exists, not reconfigured" + cd "$HOST" +else + mkdir "$HOST" + cd "$HOST" + + "$SOURCES_DIR/$PROJECT_DIR"/configure \ + --prefix="$INSTALL_DIR/$HOST" \ + --enable-cross-compile \ + --target-os=mingw32 \ + --arch="$ARCH" \ + --cross-prefix="${HOST_TRIPLET}-" \ + --cc="${HOST_TRIPLET}-gcc" \ + --extra-cflags="-O2 -fPIC" \ + --enable-shared \ + --disable-static \ + --disable-programs \ + --disable-doc \ + --disable-swscale \ + --disable-postproc \ + --disable-avfilter \ + --disable-avdevice \ + --disable-network \ + --disable-everything \ + --enable-swresample \ + --enable-decoder=h264 \ + --enable-decoder=hevc \ + --enable-decoder=av1 \ + --enable-decoder=pcm_s16le \ + --enable-decoder=opus \ + --enable-decoder=aac \ + --enable-decoder=flac \ + --enable-decoder=png \ + --enable-protocol=file \ + --enable-demuxer=image2 \ + --enable-parser=png \ + --enable-zlib \ + --enable-muxer=matroska \ + --enable-muxer=mp4 \ + --enable-muxer=opus \ + --enable-muxer=flac \ + --enable-muxer=wav \ + --disable-vulkan +fi + +make -j +make install diff --git a/app/deps/libusb.sh b/app/deps/libusb.sh new file mode 100755 index 00000000..97fc3c72 --- /dev/null +++ b/app/deps/libusb.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +set -ex +DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) +cd "$DEPS_DIR" +. common + +VERSION=1.0.27 +FILENAME=libusb-$VERSION.tar.bz2 +PROJECT_DIR=libusb-$VERSION +SHA256SUM=ffaa41d741a8a3bee244ac8e54a72ea05bf2879663c098c82fc5757853441575 + +cd "$SOURCES_DIR" + +if [[ -d "$PROJECT_DIR" ]] +then + echo "$PWD/$PROJECT_DIR" found +else + get_file "https://github.com/libusb/libusb/releases/download/v$VERSION/libusb-$VERSION.tar.bz2" "$FILENAME" "$SHA256SUM" + tar xf "$FILENAME" # First level directory is "$PROJECT_DIR" +fi + +mkdir -p "$BUILD_DIR/$PROJECT_DIR" +cd "$BUILD_DIR/$PROJECT_DIR" + +export CFLAGS='-O2' +export CXXFLAGS="$CFLAGS" + +if [[ -d "$HOST" ]] +then + echo "'$PWD/$HOST' already exists, not reconfigured" + cd "$HOST" +else + mkdir "$HOST" + cd "$HOST" + + "$SOURCES_DIR/$PROJECT_DIR"/configure \ + --prefix="$INSTALL_DIR/$HOST" \ + --host="$HOST_TRIPLET" \ + --enable-shared \ + --disable-static +fi + +make -j +make install-strip diff --git a/app/deps/patches/ffmpeg-6.1-fix-build.patch b/app/deps/patches/ffmpeg-6.1-fix-build.patch new file mode 100644 index 00000000..ed4df48d --- /dev/null +++ b/app/deps/patches/ffmpeg-6.1-fix-build.patch @@ -0,0 +1,27 @@ +From 03c80197afb324da38c9b70254231e3fdcfa68fc Mon Sep 17 00:00:00 2001 +From: Romain Vimont +Date: Sun, 12 Nov 2023 17:58:50 +0100 +Subject: [PATCH] Fix FFmpeg 6.1 build + +Build failed on tag n6.1 With --enable-decoder=av1 but without +--enable-muxer=av1. +--- + libavcodec/Makefile | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/libavcodec/Makefile b/libavcodec/Makefile +index 580a8d6b54..aff19b670c 100644 +--- a/libavcodec/Makefile ++++ b/libavcodec/Makefile +@@ -249,7 +249,7 @@ OBJS-$(CONFIG_ATRAC3PAL_DECODER) += atrac3plusdec.o atrac3plus.o \ + OBJS-$(CONFIG_ATRAC9_DECODER) += atrac9dec.o + OBJS-$(CONFIG_AURA_DECODER) += cyuv.o + OBJS-$(CONFIG_AURA2_DECODER) += aura.o +-OBJS-$(CONFIG_AV1_DECODER) += av1dec.o ++OBJS-$(CONFIG_AV1_DECODER) += av1dec.o av1_parse.o + OBJS-$(CONFIG_AV1_CUVID_DECODER) += cuviddec.o + OBJS-$(CONFIG_AV1_MEDIACODEC_DECODER) += mediacodecdec.o + OBJS-$(CONFIG_AV1_MEDIACODEC_ENCODER) += mediacodecenc.o +-- +2.42.0 + diff --git a/app/deps/sdl.sh b/app/deps/sdl.sh new file mode 100755 index 00000000..36c7ab1c --- /dev/null +++ b/app/deps/sdl.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +set -ex +DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) +cd "$DEPS_DIR" +. common + +VERSION=2.28.5 +FILENAME=SDL-$VERSION.tar.gz +PROJECT_DIR=SDL-release-$VERSION +SHA256SUM=9f0556e4a24ef5b267010038ad9e9948b62f236d5bcc4b22179f95ef62d84023 + +cd "$SOURCES_DIR" + +if [[ -d "$PROJECT_DIR" ]] +then + echo "$PWD/$PROJECT_DIR" found +else + get_file "https://github.com/libsdl-org/SDL/archive/refs/tags/release-$VERSION.tar.gz" "$FILENAME" "$SHA256SUM" + tar xf "$FILENAME" # First level directory is "$PROJECT_DIR" +fi + +mkdir -p "$BUILD_DIR/$PROJECT_DIR" +cd "$BUILD_DIR/$PROJECT_DIR" + +export CFLAGS='-O2' +export CXXFLAGS="$CFLAGS" + +if [[ -d "$HOST" ]] +then + echo "'$PWD/$HOST' already exists, not reconfigured" + cd "$HOST" +else + mkdir "$HOST" + cd "$HOST" + + "$SOURCES_DIR/$PROJECT_DIR"/configure \ + --prefix="$INSTALL_DIR/$HOST" \ + --host="$HOST_TRIPLET" \ + --enable-shared \ + --disable-static +fi + +make -j +# There is no "make install-strip" +make install +# Strip manually +${HOST_TRIPLET}-strip "$INSTALL_DIR/$HOST/bin/SDL2.dll" diff --git a/app/prebuilt-deps/.gitignore b/app/prebuilt-deps/.gitignore deleted file mode 100644 index 3af0ccb6..00000000 --- a/app/prebuilt-deps/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/data diff --git a/app/prebuilt-deps/common b/app/prebuilt-deps/common deleted file mode 100755 index c97f7de4..00000000 --- a/app/prebuilt-deps/common +++ /dev/null @@ -1,22 +0,0 @@ -PREBUILT_DATA_DIR=data - -checksum() { - local file="$1" - local sum="$2" - echo "$file: verifying checksum..." - echo "$sum $file" | sha256sum -c -} - -get_file() { - local url="$1" - local file="$2" - local sum="$3" - if [[ -f "$file" ]] - then - echo "$file: found" - else - echo "$file: not found, downloading..." - wget "$url" -O "$file" - fi - checksum "$file" "$sum" -} diff --git a/app/prebuilt-deps/prepare-adb.sh b/app/prebuilt-deps/prepare-adb.sh deleted file mode 100755 index 4fb6fd7d..00000000 --- a/app/prebuilt-deps/prepare-adb.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env bash -set -e -DIR=$(dirname ${BASH_SOURCE[0]}) -cd "$DIR" -. common -mkdir -p "$PREBUILT_DATA_DIR" -cd "$PREBUILT_DATA_DIR" - -DEP_DIR=platform-tools-34.0.5 - -FILENAME=platform-tools_r34.0.5-windows.zip -SHA256SUM=3f8320152704377de150418a3c4c9d07d16d80a6c0d0d8f7289c22c499e33571 - -if [[ -d "$DEP_DIR" ]] -then - echo "$DEP_DIR" found - exit 0 -fi - -get_file "https://dl.google.com/android/repository/$FILENAME" \ - "$FILENAME" "$SHA256SUM" - -mkdir "$DEP_DIR" -cd "$DEP_DIR" - -ZIP_PREFIX=platform-tools -unzip "../$FILENAME" \ - "$ZIP_PREFIX"/AdbWinApi.dll \ - "$ZIP_PREFIX"/AdbWinUsbApi.dll \ - "$ZIP_PREFIX"/adb.exe -mv "$ZIP_PREFIX"/* . -rmdir "$ZIP_PREFIX" diff --git a/app/prebuilt-deps/prepare-ffmpeg.sh b/app/prebuilt-deps/prepare-ffmpeg.sh deleted file mode 100755 index 19840afb..00000000 --- a/app/prebuilt-deps/prepare-ffmpeg.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env bash -set -e -DIR=$(dirname ${BASH_SOURCE[0]}) -cd "$DIR" -. common -mkdir -p "$PREBUILT_DATA_DIR" -cd "$PREBUILT_DATA_DIR" - -VERSION=6.1-scrcpy-3 -DEP_DIR="ffmpeg-$VERSION" - -FILENAME="$DEP_DIR".7z -SHA256SUM=b646d18a3d543a4e4c46881568213499f22e4454a464e1552f03f2ac9cc3a05a - -if [[ -d "$DEP_DIR" ]] -then - echo "$DEP_DIR" found - exit 0 -fi - -get_file "https://github.com/rom1v/scrcpy-deps/releases/download/$VERSION/$FILENAME" \ - "$FILENAME" "$SHA256SUM" - -mkdir "$DEP_DIR" -cd "$DEP_DIR" - -ZIP_PREFIX=ffmpeg -7z x "../$FILENAME" -mv "$ZIP_PREFIX"/* . -rmdir "$ZIP_PREFIX" diff --git a/app/prebuilt-deps/prepare-libusb.sh b/app/prebuilt-deps/prepare-libusb.sh deleted file mode 100755 index b31c45eb..00000000 --- a/app/prebuilt-deps/prepare-libusb.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env bash -set -e -DIR=$(dirname ${BASH_SOURCE[0]}) -cd "$DIR" -. common -mkdir -p "$PREBUILT_DATA_DIR" -cd "$PREBUILT_DATA_DIR" - -VERSION=1.0.26 -DEP_DIR="libusb-$VERSION" - -FILENAME="libusb-$VERSION-binaries.7z" -SHA256SUM=9c242696342dbde9cdc47239391f71833939bf9f7aa2bbb28cdaabe890465ec5 - -if [[ -d "$DEP_DIR" ]] -then - echo "$DEP_DIR" found - exit 0 -fi - -get_file "https://github.com/libusb/libusb/releases/download/v$VERSION/$FILENAME" \ - "$FILENAME" "$SHA256SUM" - -mkdir "$DEP_DIR" -cd "$DEP_DIR" - -7z x "../$FILENAME" \ - "libusb-$VERSION-binaries/libusb-MinGW-Win32/" \ - "libusb-$VERSION-binaries/libusb-MinGW-x64/" - -mv "libusb-$VERSION-binaries/libusb-MinGW-Win32" . -mv "libusb-$VERSION-binaries/libusb-MinGW-x64" . -rm -rf "libusb-$VERSION-binaries" - -# Rename the dll to get the same library name on all platforms -mv libusb-MinGW-Win32/bin/msys-usb-1.0.dll libusb-MinGW-Win32/bin/libusb-1.0.dll -mv libusb-MinGW-x64/bin/msys-usb-1.0.dll libusb-MinGW-x64/bin/libusb-1.0.dll diff --git a/app/prebuilt-deps/prepare-sdl.sh b/app/prebuilt-deps/prepare-sdl.sh deleted file mode 100755 index 7569744f..00000000 --- a/app/prebuilt-deps/prepare-sdl.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env bash -set -e -DIR=$(dirname ${BASH_SOURCE[0]}) -cd "$DIR" -. common -mkdir -p "$PREBUILT_DATA_DIR" -cd "$PREBUILT_DATA_DIR" - -VERSION=2.28.5 -DEP_DIR="SDL2-$VERSION" - -FILENAME="SDL2-devel-$VERSION-mingw.tar.gz" -SHA256SUM=3c0c655c2ebf67cad48fead72761d1601740ded30808952c3274ba223d226c21 - -if [[ -d "$DEP_DIR" ]] -then - echo "$DEP_DIR" found - exit 0 -fi - -get_file "https://github.com/libsdl-org/SDL/releases/download/release-$VERSION/$FILENAME" \ - "$FILENAME" "$SHA256SUM" - -mkdir "$DEP_DIR" -cd "$DEP_DIR" - -TAR_PREFIX="$DEP_DIR" # root directory inside the tar has the same name -tar xf "../$FILENAME" --strip-components=1 \ - "$TAR_PREFIX"/i686-w64-mingw32/bin/SDL2.dll \ - "$TAR_PREFIX"/i686-w64-mingw32/include/ \ - "$TAR_PREFIX"/i686-w64-mingw32/lib/ \ - "$TAR_PREFIX"/x86_64-w64-mingw32/bin/SDL2.dll \ - "$TAR_PREFIX"/x86_64-w64-mingw32/include/ \ - "$TAR_PREFIX"/x86_64-w64-mingw32/lib/ \ diff --git a/release.mk b/release.mk index fd969e5a..89f3da21 100644 --- a/release.mk +++ b/release.mk @@ -62,38 +62,38 @@ build-server: meson setup "$(SERVER_BUILD_DIR)" --buildtype release -Dcompile_app=false ) ninja -C "$(SERVER_BUILD_DIR)" -prepare-deps: - @app/prebuilt-deps/prepare-adb.sh - @app/prebuilt-deps/prepare-sdl.sh - @app/prebuilt-deps/prepare-ffmpeg.sh - @app/prebuilt-deps/prepare-libusb.sh +prepare-deps-win32: + @app/deps/adb.sh win32 + @app/deps/sdl.sh win32 + @app/deps/ffmpeg.sh win32 + @app/deps/libusb.sh win32 -build-win32: prepare-deps +prepare-deps-win64: + @app/deps/adb.sh win64 + @app/deps/sdl.sh win64 + @app/deps/ffmpeg.sh win64 + @app/deps/libusb.sh win64 + +build-win32: prepare-deps-win32 rm -rf "$(WIN32_BUILD_DIR)" mkdir -p "$(WIN32_BUILD_DIR)/local" - cp -r app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-3/win32/. "$(WIN32_BUILD_DIR)/local/" - cp -r app/prebuilt-deps/data/SDL2-2.28.5/i686-w64-mingw32/. "$(WIN32_BUILD_DIR)/local/" - cp -r app/prebuilt-deps/data/libusb-1.0.26/libusb-MinGW-Win32/. "$(WIN32_BUILD_DIR)/local/" meson setup "$(WIN32_BUILD_DIR)" \ - --pkg-config-path="$(WIN32_BUILD_DIR)/local/lib/pkgconfig" \ - -Dc_args="-I$(PWD)/$(WIN32_BUILD_DIR)/local/include" \ - -Dc_link_args="-L$(PWD)/$(WIN32_BUILD_DIR)/local/lib" \ + --pkg-config-path="app/deps/work/install/win32/lib/pkgconfig" \ + -Dc_args="-I$(PWD)/app/deps/work/install/win32/include" \ + -Dc_link_args="-L$(PWD)/app/deps/work/install/win32/lib" \ --cross-file=cross_win32.txt \ --buildtype=release --strip -Db_lto=true \ -Dcompile_server=false \ -Dportable=true ninja -C "$(WIN32_BUILD_DIR)" -build-win64: prepare-deps +build-win64: prepare-deps-win64 rm -rf "$(WIN64_BUILD_DIR)" mkdir -p "$(WIN64_BUILD_DIR)/local" - cp -r app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-3/win64/. "$(WIN64_BUILD_DIR)/local/" - cp -r app/prebuilt-deps/data/SDL2-2.28.5/x86_64-w64-mingw32/. "$(WIN64_BUILD_DIR)/local/" - cp -r app/prebuilt-deps/data/libusb-1.0.26/libusb-MinGW-x64/. "$(WIN64_BUILD_DIR)/local/" meson setup "$(WIN64_BUILD_DIR)" \ - --pkg-config-path="$(WIN64_BUILD_DIR)/local/lib/pkgconfig" \ - -Dc_args="-I$(PWD)/$(WIN64_BUILD_DIR)/local/include" \ - -Dc_link_args="-L$(PWD)/$(WIN64_BUILD_DIR)/local/lib" \ + --pkg-config-path="app/deps/work/install/win64/lib/pkgconfig" \ + -Dc_args="-I$(PWD)/app/deps/work/install/win64/include" \ + -Dc_link_args="-L$(PWD)/app/deps/work/install/win64/lib" \ --cross-file=cross_win64.txt \ --buildtype=release --strip -Db_lto=true \ -Dcompile_server=false \ @@ -108,10 +108,8 @@ dist-win32: build-server build-win32 cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/data/icon.png "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/platform-tools-34.0.5/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/platform-tools-34.0.5/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/platform-tools-34.0.5/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp "$(WIN32_BUILD_DIR)"/local/bin/*.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/deps/work/install/win32/bin/*.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/deps/work/install/win32/bin/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/" dist-win64: build-server build-win64 mkdir -p "$(DIST)/$(WIN64_TARGET_DIR)" @@ -121,10 +119,8 @@ dist-win64: build-server build-win64 cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/data/icon.png "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/platform-tools-34.0.5/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/platform-tools-34.0.5/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/platform-tools-34.0.5/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp "$(WIN64_BUILD_DIR)"/local/bin/*.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/deps/work/install/win64/bin/*.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/deps/work/install/win64/bin/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/" zip-win32: dist-win32 cd "$(DIST)"; \ From af573090741e73c66c4a543dcd94fd771c51b7be Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 2 Mar 2024 23:22:09 +0100 Subject: [PATCH 1802/2244] Bump version to 2.4 --- app/scrcpy-windows.rc | 2 +- meson.build | 2 +- server/build.gradle | 4 ++-- server/build_without_gradle.sh | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/scrcpy-windows.rc b/app/scrcpy-windows.rc index 895b9c93..059e91d4 100644 --- a/app/scrcpy-windows.rc +++ b/app/scrcpy-windows.rc @@ -13,7 +13,7 @@ BEGIN VALUE "LegalCopyright", "Romain Vimont, Genymobile" VALUE "OriginalFilename", "scrcpy.exe" VALUE "ProductName", "scrcpy" - VALUE "ProductVersion", "2.3.1" + VALUE "ProductVersion", "2.4" END END BLOCK "VarFileInfo" diff --git a/meson.build b/meson.build index 4ae91f69..22d0f4ef 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('scrcpy', 'c', - version: '2.3.1', + version: '2.4', meson_version: '>= 0.48', default_options: [ 'c_std=c11', diff --git a/server/build.gradle b/server/build.gradle index 1a18d997..6a1b09df 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -7,8 +7,8 @@ android { applicationId "com.genymobile.scrcpy" minSdkVersion 21 targetSdkVersion 34 - versionCode 20301 - versionName "2.3.1" + versionCode 20400 + versionName "2.4" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index 69d85679..7f7d7921 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -12,7 +12,7 @@ set -e SCRCPY_DEBUG=false -SCRCPY_VERSION_NAME=2.3.1 +SCRCPY_VERSION_NAME=2.4 PLATFORM=${ANDROID_PLATFORM:-34} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-34.0.0} From 0c94b75eefee510d3de6bc724eaeedfc600faadc Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 3 Mar 2024 00:00:24 +0100 Subject: [PATCH 1803/2244] Update links to 2.4 --- README.md | 2 +- doc/build.md | 6 +++--- doc/windows.md | 12 ++++++------ install_release.sh | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 30fc0a04..a672b327 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ source for the project. Do not download releases from random websites, even if their name contains `scrcpy`.** -# scrcpy (v2.3.1) +# scrcpy (v2.4) scrcpy diff --git a/doc/build.md b/doc/build.md index 7e3c84e9..751cf831 100644 --- a/doc/build.md +++ b/doc/build.md @@ -233,10 +233,10 @@ install` must be run as root)._ #### Option 2: Use prebuilt server - - [`scrcpy-server-v2.3.1`][direct-scrcpy-server] - SHA-256: `f6814822fc308a7a532f253485c9038183c6296a6c5df470a9e383b4f8e7605b` + - [`scrcpy-server-v2.4`][direct-scrcpy-server] + SHA-256: `93c272b7438605c055e127f7444064ed78fa9ca49f81156777fd201e79ce7ba3` -[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v2.3.1/scrcpy-server-v2.3.1 +[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v2.4/scrcpy-server-v2.4 Download the prebuilt server somewhere, and specify its path during the Meson configuration: diff --git a/doc/windows.md b/doc/windows.md index 60fd7986..a3711f26 100644 --- a/doc/windows.md +++ b/doc/windows.md @@ -4,14 +4,14 @@ Download the [latest release]: - - [`scrcpy-win64-v2.3.1.zip`][direct-win64] (64-bit) - SHA-256: `f1f78ac98214078425804e524a1bed515b9d4b8a05b78d210a4ced2b910b262d` - - [`scrcpy-win32-v2.3.1.zip`][direct-win32] (32-bit) - SHA-256: `5dffc2d432e9b8b5b0e16f12e71428c37c70d9124cfbe7620df0b41b7efe91ff` + - [`scrcpy-win64-v2.4.zip`][direct-win64] (64-bit) + SHA-256: `9dc56f21bfa455352ec0c58b40feaf2fb02d67372910a4235e298ece286ff3a9` + - [`scrcpy-win32-v2.4.zip`][direct-win32] (32-bit) + SHA-256: `cf92acc45eef37c6ee2db819f92e420ced3bc50f1348dd57f7d6ca1fc80f6116` [latest release]: https://github.com/Genymobile/scrcpy/releases/latest -[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v2.3.1/scrcpy-win64-v2.3.1.zip -[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v2.3.1/scrcpy-win32-v2.3.1.zip +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v2.4/scrcpy-win64-v2.4.zip +[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v2.4/scrcpy-win32-v2.4.zip and extract it. diff --git a/install_release.sh b/install_release.sh index d8dbd951..0be5675c 100755 --- a/install_release.sh +++ b/install_release.sh @@ -2,8 +2,8 @@ set -e BUILDDIR=build-auto -PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v2.3.1/scrcpy-server-v2.3.1 -PREBUILT_SERVER_SHA256=f6814822fc308a7a532f253485c9038183c6296a6c5df470a9e383b4f8e7605b +PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v2.4/scrcpy-server-v2.4 +PREBUILT_SERVER_SHA256=93c272b7438605c055e127f7444064ed78fa9ca49f81156777fd201e79ce7ba3 echo "[scrcpy] Downloading prebuilt server..." wget "$PREBUILT_SERVER_URL" -O scrcpy-server From cc7719079ab6301e6ab42b7cd078a1da5acfa5b2 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 3 Mar 2024 00:05:26 +0100 Subject: [PATCH 1804/2244] Italicize coordinates letters in documentation --- doc/control.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/control.md b/doc/control.md index d6d1265c..abc9d1bf 100644 --- a/doc/control.md +++ b/doc/control.md @@ -71,8 +71,8 @@ To simulate a tilt gesture: Shift+_click-and-move-up-or-down_. Technically, _scrcpy_ generates additional touch events from a "virtual finger" at a location inverted through the center of the screen. When pressing -Ctrl the x and y coordinates are inverted. Using Shift -only inverts x. +Ctrl the _x_ and _y_ coordinates are inverted. Using Shift +only inverts _x_. This only works for the default mouse mode (`--mouse=sdk`). From 7f23ff3f2ca64dae6eb8b0ca9a881230184ae756 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 3 Mar 2024 00:06:54 +0100 Subject: [PATCH 1805/2244] Add videos for pinch-to-zoom and tilt A video is worth a thousand words. --- doc/control.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/control.md b/doc/control.md index abc9d1bf..e9fd9e9b 100644 --- a/doc/control.md +++ b/doc/control.md @@ -67,8 +67,12 @@ More precisely, hold down 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. +https://github.com/Genymobile/scrcpy/assets/543275/26c4a920-9805-43f1-8d4c-608752d04767 + To simulate a tilt gesture: Shift+_click-and-move-up-or-down_. +https://github.com/Genymobile/scrcpy/assets/543275/1e252341-4a90-4b29-9d11-9153b324669f + Technically, _scrcpy_ generates additional touch events from a "virtual finger" at a location inverted through the center of the screen. When pressing Ctrl the _x_ and _y_ coordinates are inverted. Using Shift From 79968a0ae63179c39d5f1d4f4d97da020c9e07dd Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 11 Mar 2024 18:05:27 +0100 Subject: [PATCH 1806/2244] Reorder documentation Present the --tcpip option without arguments first. --- doc/connection.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/doc/connection.md b/doc/connection.md index 90ced010..17efbbdc 100644 --- a/doc/connection.md +++ b/doc/connection.md @@ -67,14 +67,6 @@ computer. An option `--tcpip` allows to configure the connection automatically. There are two variants. -If the device (accessible at 192.168.1.1 in this example) already listens on a -port (typically 5555) for incoming _adb_ connections, then run: - -```bash -scrcpy --tcpip=192.168.1.1 # default port is 5555 -scrcpy --tcpip=192.168.1.1:5555 -``` - If _adb_ TCP/IP mode is disabled on the device (or if you don't know the IP address), connect the device over USB, then run: @@ -85,6 +77,14 @@ scrcpy --tcpip # without arguments It will automatically find the device IP address and adb port, enable TCP/IP mode if necessary, then connect to the device before starting. +If the device (accessible at 192.168.1.1 in this example) already listens on a +port (typically 5555) for incoming _adb_ connections, then run: + +```bash +scrcpy --tcpip=192.168.1.1 # default port is 5555 +scrcpy --tcpip=192.168.1.1:5555 +``` + ### Manual From be3d357a6dcdf0f3fccabd795b869a30e1e5bee1 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 30 Mar 2024 11:23:41 +0100 Subject: [PATCH 1807/2244] Use source repo tarball for libusb Legitimate or not, we should not use sources that do not match the repository. Refs Refs Refs #4713 --- app/deps/libusb.sh | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/deps/libusb.sh b/app/deps/libusb.sh index 97fc3c72..26f0140b 100755 --- a/app/deps/libusb.sh +++ b/app/deps/libusb.sh @@ -5,9 +5,9 @@ cd "$DEPS_DIR" . common VERSION=1.0.27 -FILENAME=libusb-$VERSION.tar.bz2 +FILENAME=libusb-$VERSION.tar.gz PROJECT_DIR=libusb-$VERSION -SHA256SUM=ffaa41d741a8a3bee244ac8e54a72ea05bf2879663c098c82fc5757853441575 +SHA256SUM=e8f18a7a36ecbb11fb820bd71540350d8f61bcd9db0d2e8c18a6fb80b214a3de cd "$SOURCES_DIR" @@ -15,7 +15,7 @@ if [[ -d "$PROJECT_DIR" ]] then echo "$PWD/$PROJECT_DIR" found else - get_file "https://github.com/libusb/libusb/releases/download/v$VERSION/libusb-$VERSION.tar.bz2" "$FILENAME" "$SHA256SUM" + get_file "https://github.com/libusb/libusb/archive/refs/tags/v$VERSION.tar.gz" "$FILENAME" "$SHA256SUM" tar xf "$FILENAME" # First level directory is "$PROJECT_DIR" fi @@ -33,6 +33,7 @@ else mkdir "$HOST" cd "$HOST" + "$SOURCES_DIR/$PROJECT_DIR"/bootstrap.sh "$SOURCES_DIR/$PROJECT_DIR"/configure \ --prefix="$INSTALL_DIR/$HOST" \ --host="$HOST_TRIPLET" \ From 1c3801a0b1624258e5004e651c9dbcff7eafe324 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 10 Mar 2024 22:04:17 +0100 Subject: [PATCH 1808/2244] Add a shortcut to pause/unpause display Pause/unpause display on MOD+z and MOD+Shift+z. It only impacts rendering, the device is still captured, the video stream continues to be transmitted to the device and recorded (if recording is enabled). Fixes #1632 PR #4748 --- app/scrcpy.1 | 8 ++++++ app/src/cli.c | 8 ++++++ app/src/input_manager.c | 54 +++++++++++++++++++++----------------- app/src/screen.c | 57 +++++++++++++++++++++++++++++++++++++---- app/src/screen.h | 7 +++++ doc/shortcuts.md | 2 ++ 6 files changed, 107 insertions(+), 29 deletions(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 1e3c91b1..eb09f530 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -577,6 +577,14 @@ Flip display horizontally .B MOD+Shift+Up, MOD+Shift+Down Flip display vertically +.TP +.B MOD+z +Pause or re-pause display + +.TP +.B MOD+Shift+z +Unpause display + .TP .B MOD+g Resize window to 1:1 (pixel\-perfect) diff --git a/app/src/cli.c b/app/src/cli.c index daa041cf..92807947 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -900,6 +900,14 @@ static const struct sc_shortcut shortcuts[] = { .shortcuts = { "MOD+Shift+Up", "MOD+Shift+Down" }, .text = "Flip display vertically", }, + { + .shortcuts = { "MOD+z" }, + .text = "Pause or re-pause display", + }, + { + .shortcuts = { "MOD+Shift+z" }, + .text = "Unpause display", + }, { .shortcuts = { "MOD+g" }, .text = "Resize window to 1:1 (pixel-perfect)", diff --git a/app/src/input_manager.c b/app/src/input_manager.c index f26c4164..c7a758f4 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -402,6 +402,7 @@ sc_input_manager_process_key(struct sc_input_manager *im, const SDL_KeyboardEvent *event) { // controller is NULL if --no-control is requested bool control = im->controller; + bool paused = im->screen->paused; SDL_Keycode keycode = event->keysym.sym; uint16_t mod = event->keysym.mod; @@ -427,46 +428,51 @@ sc_input_manager_process_key(struct sc_input_manager *im, enum sc_action action = down ? SC_ACTION_DOWN : SC_ACTION_UP; switch (keycode) { case SDLK_h: - if (im->kp && !shift && !repeat) { + if (im->kp && !shift && !repeat && !paused) { action_home(im, action); } return; case SDLK_b: // fall-through case SDLK_BACKSPACE: - if (im->kp && !shift && !repeat) { + if (im->kp && !shift && !repeat && !paused) { action_back(im, action); } return; case SDLK_s: - if (im->kp && !shift && !repeat) { + if (im->kp && !shift && !repeat && !paused) { action_app_switch(im, action); } return; case SDLK_m: - if (im->kp && !shift && !repeat) { + if (im->kp && !shift && !repeat && !paused) { action_menu(im, action); } return; case SDLK_p: - if (im->kp && !shift && !repeat) { + if (im->kp && !shift && !repeat && !paused) { action_power(im, action); } return; case SDLK_o: - if (control && !repeat && down) { + if (control && !repeat && down && !paused) { enum sc_screen_power_mode mode = shift ? SC_SCREEN_POWER_MODE_NORMAL : SC_SCREEN_POWER_MODE_OFF; set_screen_power_mode(im, mode); } return; + case SDLK_z: + if (down && !repeat) { + sc_screen_set_paused(im->screen, !shift); + } + return; case SDLK_DOWN: if (shift) { if (!repeat & down) { apply_orientation_transform(im, SC_ORIENTATION_FLIP_180); } - } else if (im->kp) { + } else if (im->kp && !paused) { // forward repeated events action_volume_down(im, action); } @@ -477,7 +483,7 @@ sc_input_manager_process_key(struct sc_input_manager *im, apply_orientation_transform(im, SC_ORIENTATION_FLIP_180); } - } else if (im->kp) { + } else if (im->kp && !paused) { // forward repeated events action_volume_up(im, action); } @@ -505,17 +511,17 @@ sc_input_manager_process_key(struct sc_input_manager *im, } return; case SDLK_c: - if (im->kp && !shift && !repeat && down) { + if (im->kp && !shift && !repeat && down && !paused) { get_device_clipboard(im, SC_COPY_KEY_COPY); } return; case SDLK_x: - if (im->kp && !shift && !repeat && down) { + if (im->kp && !shift && !repeat && down && !paused) { get_device_clipboard(im, SC_COPY_KEY_CUT); } return; case SDLK_v: - if (im->kp && !repeat && down) { + if (im->kp && !repeat && down && !paused) { if (shift || im->legacy_paste) { // inject the text as input events clipboard_paste(im); @@ -547,7 +553,7 @@ sc_input_manager_process_key(struct sc_input_manager *im, } return; case SDLK_n: - if (control && !repeat && down) { + if (control && !repeat && down && !paused) { if (shift) { collapse_panels(im); } else if (im->key_repeat == 0) { @@ -558,12 +564,12 @@ sc_input_manager_process_key(struct sc_input_manager *im, } return; case SDLK_r: - if (control && !shift && !repeat && down) { + if (control && !shift && !repeat && down && !paused) { rotate_device(im); } return; case SDLK_k: - if (control && !shift && !repeat && down + if (control && !shift && !repeat && down && !paused && im->kp && im->kp->hid) { // Only if the current keyboard is hid open_hard_keyboard_settings(im); @@ -574,7 +580,7 @@ sc_input_manager_process_key(struct sc_input_manager *im, return; } - if (!im->kp) { + if (!im->kp || paused) { return; } @@ -622,7 +628,6 @@ sc_input_manager_process_key(struct sc_input_manager *im, static void sc_input_manager_process_mouse_motion(struct sc_input_manager *im, const SDL_MouseMotionEvent *event) { - if (event->which == SDL_TOUCH_MOUSEID) { // simulated from touch events, so it's a duplicate return; @@ -695,16 +700,16 @@ sc_input_manager_process_touch(struct sc_input_manager *im, static void sc_input_manager_process_mouse_button(struct sc_input_manager *im, const SDL_MouseButtonEvent *event) { - bool control = im->controller; - if (event->which == SDL_TOUCH_MOUSEID) { // simulated from touch events, so it's a duplicate return; } + bool control = im->controller; + bool paused = im->screen->paused; bool down = event->type == SDL_MOUSEBUTTONDOWN; if (!im->forward_all_clicks) { - if (control) { + if (control && !paused) { enum sc_action action = down ? SC_ACTION_DOWN : SC_ACTION_UP; if (im->kp && event->button == SDL_BUTTON_X1) { @@ -747,7 +752,7 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im, // otherwise, send the click event to the device } - if (!im->mp) { + if (!im->mp || paused) { return; } @@ -885,9 +890,10 @@ void sc_input_manager_handle_event(struct sc_input_manager *im, const SDL_Event *event) { bool control = im->controller; + bool paused = im->screen->paused; switch (event->type) { case SDL_TEXTINPUT: - if (!im->kp) { + if (!im->kp || paused) { break; } sc_input_manager_process_text_input(im, &event->text); @@ -899,13 +905,13 @@ sc_input_manager_handle_event(struct sc_input_manager *im, sc_input_manager_process_key(im, &event->key); break; case SDL_MOUSEMOTION: - if (!im->mp) { + if (!im->mp || paused) { break; } sc_input_manager_process_mouse_motion(im, &event->motion); break; case SDL_MOUSEWHEEL: - if (!im->mp) { + if (!im->mp || paused) { break; } sc_input_manager_process_mouse_wheel(im, &event->wheel); @@ -919,7 +925,7 @@ sc_input_manager_handle_event(struct sc_input_manager *im, case SDL_FINGERMOTION: case SDL_FINGERDOWN: case SDL_FINGERUP: - if (!im->mp) { + if (!im->mp || paused) { break; } sc_input_manager_process_touch(im, &event->tfinger); diff --git a/app/src/screen.c b/app/src/screen.c index 091001bc..351eb3fb 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -362,6 +362,8 @@ sc_screen_init(struct sc_screen *screen, screen->maximized = false; screen->minimized = false; screen->mouse_capture_key_pressed = 0; + screen->paused = false; + screen->resume_frame = NULL; screen->req.x = params->window_x; screen->req.y = params->window_y; @@ -614,13 +616,10 @@ prepare_for_frame(struct sc_screen *screen, struct sc_size new_frame_size) { } static bool -sc_screen_update_frame(struct sc_screen *screen) { - av_frame_unref(screen->frame); - sc_frame_buffer_consume(&screen->fb, screen->frame); - AVFrame *frame = screen->frame; - +sc_screen_apply_frame(struct sc_screen *screen) { sc_fps_counter_add_rendered_frame(&screen->fps_counter); + AVFrame *frame = screen->frame; struct sc_size new_frame_size = {frame->width, frame->height}; enum sc_display_result res = prepare_for_frame(screen, new_frame_size); if (res == SC_DISPLAY_RESULT_ERROR) { @@ -655,6 +654,54 @@ sc_screen_update_frame(struct sc_screen *screen) { return true; } +static bool +sc_screen_update_frame(struct sc_screen *screen) { + if (screen->paused) { + if (!screen->resume_frame) { + screen->resume_frame = av_frame_alloc(); + if (!screen->resume_frame) { + LOG_OOM(); + return false; + } + } else { + av_frame_unref(screen->resume_frame); + } + sc_frame_buffer_consume(&screen->fb, screen->resume_frame); + return true; + } + + av_frame_unref(screen->frame); + sc_frame_buffer_consume(&screen->fb, screen->frame); + return sc_screen_apply_frame(screen); +} + +void +sc_screen_set_paused(struct sc_screen *screen, bool paused) { + if (!paused && !screen->paused) { + // nothing to do + return; + } + + if (screen->paused && screen->resume_frame) { + // If display screen was paused, refresh the frame immediately, even if + // the new state is also paused. + av_frame_free(&screen->frame); + screen->frame = screen->resume_frame; + screen->resume_frame = NULL; + sc_screen_apply_frame(screen); + } + + if (!paused) { + LOGI("Display screen unpaused"); + } else if (!screen->paused) { + LOGI("Display screen paused"); + } else { + LOGI("Display screen re-paused"); + } + + screen->paused = paused; +} + void sc_screen_switch_fullscreen(struct sc_screen *screen) { uint32_t new_mode = screen->fullscreen ? 0 : SDL_WINDOW_FULLSCREEN_DESKTOP; diff --git a/app/src/screen.h b/app/src/screen.h index 46591be5..361ce455 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -64,6 +64,9 @@ struct sc_screen { SDL_Keycode mouse_capture_key_pressed; AVFrame *frame; + + bool paused; + AVFrame *resume_frame; }; struct sc_screen_params { @@ -135,6 +138,10 @@ void sc_screen_set_orientation(struct sc_screen *screen, enum sc_orientation orientation); +// set the display pause state +void +sc_screen_set_paused(struct sc_screen *screen, bool paused); + // react to SDL events // If this function returns false, scrcpy must exit with an error. bool diff --git a/doc/shortcuts.md b/doc/shortcuts.md index 8c402855..d0f6ebec 100644 --- a/doc/shortcuts.md +++ b/doc/shortcuts.md @@ -28,6 +28,8 @@ _[Super] is typically the Windows or Cmd key._ | Rotate display right | MOD+ _(right)_ | Flip display horizontally | MOD+Shift+ _(left)_ \| MOD+Shift+ _(right)_ | Flip display vertically | MOD+Shift+ _(up)_ \| MOD+Shift+ _(down)_ + | Pause or re-pause display | MOD+z + | Unpause display | MOD+Shift+z | Resize window to 1:1 (pixel-perfect) | MOD+g | Resize window to remove black borders | MOD+w \| _Double-left-click¹_ | Click on `HOME` | MOD+h \| _Middle-click_ From db55edb196134ab39f14c76edfd35225055f1227 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 30 Mar 2024 15:18:44 +0100 Subject: [PATCH 1809/2244] Fix YUV conversion for full color range Take the color range (full vs limited) into account to render the picture. Note that with the current version of SDL, it has no impact with the SDL opengl render driver. Fixes #4756 Refs Refs libusb/#9311 Suggested-by: Simon Chan <1330321+yume-chan@users.noreply.github.com> --- app/src/display.c | 18 ++++++++++++++++++ app/src/display.h | 2 ++ 2 files changed, 20 insertions(+) diff --git a/app/src/display.c b/app/src/display.c index c8df615d..25c23265 100644 --- a/app/src/display.c +++ b/app/src/display.c @@ -1,6 +1,7 @@ #include "display.h" #include +#include #include "util/log.h" @@ -65,6 +66,7 @@ sc_display_init(struct sc_display *display, SDL_Window *window, bool mipmaps) { display->texture = NULL; display->pending.flags = 0; display->pending.frame = NULL; + display->has_frame = false; return true; } @@ -196,9 +198,25 @@ sc_display_set_texture_size(struct sc_display *display, struct sc_size size) { return SC_DISPLAY_RESULT_OK; } +static SDL_YUV_CONVERSION_MODE +sc_display_to_sdl_color_range(enum AVColorRange color_range) { + return color_range == AVCOL_RANGE_JPEG ? SDL_YUV_CONVERSION_JPEG + : SDL_YUV_CONVERSION_AUTOMATIC; +} + static bool sc_display_update_texture_internal(struct sc_display *display, const AVFrame *frame) { + if (!display->has_frame) { + // First frame + display->has_frame = true; + + // Configure YUV color range conversion + SDL_YUV_CONVERSION_MODE sdl_color_range = + sc_display_to_sdl_color_range(frame->color_range); + SDL_SetYUVConversionMode(sdl_color_range); + } + int ret = SDL_UpdateYUVTexture(display->texture, NULL, frame->data[0], frame->linesize[0], frame->data[1], frame->linesize[1], diff --git a/app/src/display.h b/app/src/display.h index 643ce73c..590715ee 100644 --- a/app/src/display.h +++ b/app/src/display.h @@ -33,6 +33,8 @@ struct sc_display { struct sc_size size; AVFrame *frame; } pending; + + bool has_frame; }; enum sc_display_result { From bf625790faccd86b5cdf89234ee405825f76b662 Mon Sep 17 00:00:00 2001 From: Simon Chan <1330321+yume-chan@users.noreply.github.com> Date: Sat, 30 Mar 2024 15:25:14 +0100 Subject: [PATCH 1810/2244] Request limited color range by default Most devices currently use limited color range, but some recent devices encode in full color range, which is currently not supported by the SDL opengl render driver. Fixes #4756 Refs Refs libusb/#9311 Signed-off-by: Romain Vimont --- .../src/main/java/com/genymobile/scrcpy/SurfaceEncoder.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/server/src/main/java/com/genymobile/scrcpy/SurfaceEncoder.java b/server/src/main/java/com/genymobile/scrcpy/SurfaceEncoder.java index 28435c09..8eda8231 100644 --- a/server/src/main/java/com/genymobile/scrcpy/SurfaceEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/SurfaceEncoder.java @@ -3,6 +3,7 @@ package com.genymobile.scrcpy; import android.media.MediaCodec; import android.media.MediaCodecInfo; import android.media.MediaFormat; +import android.os.Build; import android.os.Looper; import android.os.SystemClock; import android.view.Surface; @@ -220,6 +221,9 @@ public class SurfaceEncoder implements AsyncProcessor { // must be present to configure the encoder, but does not impact the actual frame rate, which is variable format.setInteger(MediaFormat.KEY_FRAME_RATE, 60); format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + format.setInteger(MediaFormat.KEY_COLOR_RANGE, MediaFormat.COLOR_RANGE_LIMITED); + } format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, DEFAULT_I_FRAME_INTERVAL); // display the very first frame, and recover from bad quality when no new frames format.setLong(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER, REPEAT_FRAME_DELAY_US); // µs From 206809a99affad9a7aa58fcf7593cea71f48954d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 2 Apr 2024 18:01:21 +0200 Subject: [PATCH 1811/2244] Fix typo in documentation --- doc/audio.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/audio.md b/doc/audio.md index ecae4468..f1d4d8e7 100644 --- a/doc/audio.md +++ b/doc/audio.md @@ -35,7 +35,7 @@ scrcpy --no-video # interrupt with Ctrl+C ``` -Without video, the audio latency is typically not criticial, so it might be +Without video, the audio latency is typically not critical, so it might be interesting to add [buffering](#buffering) to minimize glitches: ``` From aa34d63171c86a23942d575ac62410a0f8765a4d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 3 Apr 2024 08:57:18 +0200 Subject: [PATCH 1812/2244] Fix segfault on close with --no-video Do not call sc_screen_hide_window() if screen is not initialized. To reproduce: scrcpy --no-video --record=file.mp4 This only segfaults in debug mode since commit fd0f432e877153d83ed435474fb7b04e41de4269. --- app/src/scrcpy.c | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index f43af35e..537562f4 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -805,9 +805,12 @@ scrcpy(struct scrcpy_options *options) { ret = event_loop(s); LOGD("quit..."); - // Close the window immediately on closing, because screen_destroy() may - // only be called once the video demuxer thread is joined (it may take time) - sc_screen_hide_window(&s->screen); + if (options->video_playback) { + // Close the window immediately on closing, because screen_destroy() + // may only be called once the video demuxer thread is joined (it may + // take time) + sc_screen_hide_window(&s->screen); + } end: if (timeout_started) { From ee6620d123e87d4af8e51cd272de5eafb677122a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 1 Apr 2024 15:17:31 +0200 Subject: [PATCH 1813/2244] Refactor WindowManager methods Select the available method to invoke the same way as in other wrappers (using a version field). Refs d894e270a7719b92e38b4f5e0294b9d55e90a6df Refs #4740 --- .../scrcpy/wrappers/WindowManager.java | 125 +++++++++--------- 1 file changed, 64 insertions(+), 61 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java index d9654b1b..2fc7ee02 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java @@ -12,12 +12,15 @@ import java.lang.reflect.Method; public final class WindowManager { private final IInterface manager; private Method getRotationMethod; - private Method freezeRotationMethod; + private Method freezeDisplayRotationMethod; - private Method isRotationFrozenMethod; + private int freezeDisplayRotationMethodVersion; + private Method isDisplayRotationFrozenMethod; - private Method thawRotationMethod; + private int isDisplayRotationFrozenMethodVersion; + private Method thawDisplayRotationMethod; + private int thawDisplayRotationMethodVersion; static WindowManager create() { IInterface manager = ServiceManager.getService("window", "android.view.IWindowManager"); @@ -43,50 +46,47 @@ public final class WindowManager { return getRotationMethod; } - private Method getFreezeRotationMethod() throws NoSuchMethodException { - if (freezeRotationMethod == null) { - freezeRotationMethod = manager.getClass().getMethod("freezeRotation", int.class); - } - return freezeRotationMethod; - } - - // New method added by this commit: - // private Method getFreezeDisplayRotationMethod() throws NoSuchMethodException { if (freezeDisplayRotationMethod == null) { - freezeDisplayRotationMethod = manager.getClass().getMethod("freezeDisplayRotation", int.class, int.class); + try { + freezeDisplayRotationMethod = manager.getClass().getMethod("freezeRotation", int.class); + freezeDisplayRotationMethodVersion = 0; + } catch (NoSuchMethodException e) { + // New method added by this commit: + // + freezeDisplayRotationMethod = manager.getClass().getMethod("freezeDisplayRotation", int.class, int.class); + freezeDisplayRotationMethodVersion = 1; + } } return freezeDisplayRotationMethod; } - private Method getIsRotationFrozenMethod() throws NoSuchMethodException { - if (isRotationFrozenMethod == null) { - isRotationFrozenMethod = manager.getClass().getMethod("isRotationFrozen"); - } - return isRotationFrozenMethod; - } - - // New method added by this commit: - // private Method getIsDisplayRotationFrozenMethod() throws NoSuchMethodException { if (isDisplayRotationFrozenMethod == null) { - isDisplayRotationFrozenMethod = manager.getClass().getMethod("isDisplayRotationFrozen", int.class); + try { + isDisplayRotationFrozenMethod = manager.getClass().getMethod("isRotationFrozen"); + isDisplayRotationFrozenMethodVersion = 0; + } catch (NoSuchMethodException e) { + // New method added by this commit: + // + isDisplayRotationFrozenMethod = manager.getClass().getMethod("isDisplayRotationFrozen", int.class); + isDisplayRotationFrozenMethodVersion = 1; + } } return isDisplayRotationFrozenMethod; } - private Method getThawRotationMethod() throws NoSuchMethodException { - if (thawRotationMethod == null) { - thawRotationMethod = manager.getClass().getMethod("thawRotation"); - } - return thawRotationMethod; - } - - // New method added by this commit: - // private Method getThawDisplayRotationMethod() throws NoSuchMethodException { if (thawDisplayRotationMethod == null) { - thawDisplayRotationMethod = manager.getClass().getMethod("thawDisplayRotation", int.class); + try { + thawDisplayRotationMethod = manager.getClass().getMethod("thawRotation"); + thawDisplayRotationMethodVersion = 0; + } catch (NoSuchMethodException e) { + // New method added by this commit: + // + thawDisplayRotationMethod = manager.getClass().getMethod("thawDisplayRotation", int.class); + thawDisplayRotationMethodVersion = 1; + } } return thawDisplayRotationMethod; } @@ -103,16 +103,18 @@ public final class WindowManager { public void freezeRotation(int displayId, int rotation) { try { - try { - Method method = getFreezeDisplayRotationMethod(); - method.invoke(manager, displayId, rotation); - } catch (ReflectiveOperationException e) { - if (displayId == 0) { - Method method = getFreezeRotationMethod(); + Method method = getFreezeDisplayRotationMethod(); + switch (freezeDisplayRotationMethodVersion) { + case 0: + if (displayId != 0) { + Ln.e("Secondary display rotation not supported on this device"); + return; + } method.invoke(manager, rotation); - } else { - Ln.e("Could not invoke method", e); - } + break; + default: + method.invoke(manager, displayId, rotation); + break; } } catch (ReflectiveOperationException e) { Ln.e("Could not invoke method", e); @@ -121,17 +123,16 @@ public final class WindowManager { public boolean isRotationFrozen(int displayId) { try { - try { - Method method = getIsDisplayRotationFrozenMethod(); - return (boolean) method.invoke(manager, displayId); - } catch (ReflectiveOperationException e) { - if (displayId == 0) { - Method method = getIsRotationFrozenMethod(); + Method method = getIsDisplayRotationFrozenMethod(); + switch (isDisplayRotationFrozenMethodVersion) { + case 0: + if (displayId != 0) { + Ln.e("Secondary display rotation not supported on this device"); + return false; + } return (boolean) method.invoke(manager); - } else { - Ln.e("Could not invoke method", e); - return false; - } + default: + return (boolean) method.invoke(manager, displayId); } } catch (ReflectiveOperationException e) { Ln.e("Could not invoke method", e); @@ -141,16 +142,18 @@ public final class WindowManager { public void thawRotation(int displayId) { try { - try { - Method method = getThawDisplayRotationMethod(); - method.invoke(manager, displayId); - } catch (ReflectiveOperationException e) { - if (displayId == 0) { - Method method = getThawRotationMethod(); + Method method = getThawDisplayRotationMethod(); + switch (thawDisplayRotationMethodVersion) { + case 0: + if (displayId != 0) { + Ln.e("Secondary display rotation not supported on this device"); + return; + } method.invoke(manager); - } else { - Ln.e("Could not invoke method", e); - } + break; + default: + method.invoke(manager, displayId); + break; } } catch (ReflectiveOperationException e) { Ln.e("Could not invoke method", e); From 7011dd1ef0fe0f745387251c0a97bac338ce767d Mon Sep 17 00:00:00 2001 From: Stepan Salenikovich Date: Thu, 7 Mar 2024 14:13:22 -0500 Subject: [PATCH 1814/2244] Fix freeze and thaw rotation for Android 14 Changed since AOSP/framework_base commit 670fb7f5c0d23cf51ead25538bcb017e03ed73ac, included in tag android-14.0.0_r29. Refs PR #4740 Signed-off-by: Romain Vimont --- .../scrcpy/wrappers/WindowManager.java | 40 ++++++++++++++----- 1 file changed, 30 insertions(+), 10 deletions(-) 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 2fc7ee02..e1a3340a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java @@ -52,10 +52,17 @@ public final class WindowManager { freezeDisplayRotationMethod = manager.getClass().getMethod("freezeRotation", int.class); freezeDisplayRotationMethodVersion = 0; } catch (NoSuchMethodException e) { - // New method added by this commit: - // - freezeDisplayRotationMethod = manager.getClass().getMethod("freezeDisplayRotation", int.class, int.class); - freezeDisplayRotationMethodVersion = 1; + try { + // New method added by this commit: + // + freezeDisplayRotationMethod = manager.getClass().getMethod("freezeDisplayRotation", int.class, int.class); + freezeDisplayRotationMethodVersion = 1; + } catch (NoSuchMethodException e1) { + // Android 15 preview and 14 QPR3 Beta added a String caller parameter for debugging: + // + freezeDisplayRotationMethod = manager.getClass().getMethod("freezeDisplayRotation", int.class, int.class, String.class); + freezeDisplayRotationMethodVersion = 2; + } } } return freezeDisplayRotationMethod; @@ -82,10 +89,17 @@ public final class WindowManager { thawDisplayRotationMethod = manager.getClass().getMethod("thawRotation"); thawDisplayRotationMethodVersion = 0; } catch (NoSuchMethodException e) { - // New method added by this commit: - // - thawDisplayRotationMethod = manager.getClass().getMethod("thawDisplayRotation", int.class); - thawDisplayRotationMethodVersion = 1; + try { + // New method added by this commit: + // + thawDisplayRotationMethod = manager.getClass().getMethod("thawDisplayRotation", int.class); + thawDisplayRotationMethodVersion = 1; + } catch (NoSuchMethodException e1) { + // Android 15 preview and 14 QPR3 Beta added a String caller parameter for debugging: + // + thawDisplayRotationMethod = manager.getClass().getMethod("thawDisplayRotation", int.class, String.class); + thawDisplayRotationMethodVersion = 2; + } } } return thawDisplayRotationMethod; @@ -112,9 +126,12 @@ public final class WindowManager { } method.invoke(manager, rotation); break; - default: + case 1: method.invoke(manager, displayId, rotation); break; + default: + method.invoke(manager, displayId, rotation, "scrcpy#freezeRotation"); + break; } } catch (ReflectiveOperationException e) { Ln.e("Could not invoke method", e); @@ -151,9 +168,12 @@ public final class WindowManager { } method.invoke(manager); break; - default: + case 1: method.invoke(manager, displayId); break; + default: + method.invoke(manager, displayId, "scrcpy#thawRotation"); + break; } } catch (ReflectiveOperationException e) { Ln.e("Could not invoke method", e); From a73bf932d61a1677f8898ba28adbffafacc97886 Mon Sep 17 00:00:00 2001 From: Kaiming Hu Date: Fri, 12 Apr 2024 14:16:42 +0800 Subject: [PATCH 1815/2244] Fix could not rotate secondary display The version of the methods with the display id parameter must be tried first, otherwise they will never be used (since the old versions without the display id are still present). Regression introduced by ee6620d123e87d4af8e51cd272de5eafb677122a. Refs #4740 PR #4841 Signed-off-by: Romain Vimont --- .../scrcpy/wrappers/WindowManager.java | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) 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 e1a3340a..ae1468f4 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java @@ -49,7 +49,9 @@ public final class WindowManager { private Method getFreezeDisplayRotationMethod() throws NoSuchMethodException { if (freezeDisplayRotationMethod == null) { try { - freezeDisplayRotationMethod = manager.getClass().getMethod("freezeRotation", int.class); + // Android 15 preview and 14 QPR3 Beta added a String caller parameter for debugging: + // + freezeDisplayRotationMethod = manager.getClass().getMethod("freezeDisplayRotation", int.class, int.class, String.class); freezeDisplayRotationMethodVersion = 0; } catch (NoSuchMethodException e) { try { @@ -58,9 +60,7 @@ public final class WindowManager { freezeDisplayRotationMethod = manager.getClass().getMethod("freezeDisplayRotation", int.class, int.class); freezeDisplayRotationMethodVersion = 1; } catch (NoSuchMethodException e1) { - // Android 15 preview and 14 QPR3 Beta added a String caller parameter for debugging: - // - freezeDisplayRotationMethod = manager.getClass().getMethod("freezeDisplayRotation", int.class, int.class, String.class); + freezeDisplayRotationMethod = manager.getClass().getMethod("freezeRotation", int.class); freezeDisplayRotationMethodVersion = 2; } } @@ -71,12 +71,12 @@ public final class WindowManager { private Method getIsDisplayRotationFrozenMethod() throws NoSuchMethodException { if (isDisplayRotationFrozenMethod == null) { try { - isDisplayRotationFrozenMethod = manager.getClass().getMethod("isRotationFrozen"); - isDisplayRotationFrozenMethodVersion = 0; - } catch (NoSuchMethodException e) { // New method added by this commit: // isDisplayRotationFrozenMethod = manager.getClass().getMethod("isDisplayRotationFrozen", int.class); + isDisplayRotationFrozenMethodVersion = 0; + } catch (NoSuchMethodException e) { + isDisplayRotationFrozenMethod = manager.getClass().getMethod("isRotationFrozen"); isDisplayRotationFrozenMethodVersion = 1; } } @@ -86,7 +86,9 @@ public final class WindowManager { private Method getThawDisplayRotationMethod() throws NoSuchMethodException { if (thawDisplayRotationMethod == null) { try { - thawDisplayRotationMethod = manager.getClass().getMethod("thawRotation"); + // Android 15 preview and 14 QPR3 Beta added a String caller parameter for debugging: + // + thawDisplayRotationMethod = manager.getClass().getMethod("thawDisplayRotation", int.class, String.class); thawDisplayRotationMethodVersion = 0; } catch (NoSuchMethodException e) { try { @@ -95,9 +97,7 @@ public final class WindowManager { thawDisplayRotationMethod = manager.getClass().getMethod("thawDisplayRotation", int.class); thawDisplayRotationMethodVersion = 1; } catch (NoSuchMethodException e1) { - // Android 15 preview and 14 QPR3 Beta added a String caller parameter for debugging: - // - thawDisplayRotationMethod = manager.getClass().getMethod("thawDisplayRotation", int.class, String.class); + thawDisplayRotationMethod = manager.getClass().getMethod("thawRotation"); thawDisplayRotationMethodVersion = 2; } } @@ -120,17 +120,17 @@ public final class WindowManager { Method method = getFreezeDisplayRotationMethod(); switch (freezeDisplayRotationMethodVersion) { case 0: - if (displayId != 0) { - Ln.e("Secondary display rotation not supported on this device"); - return; - } - method.invoke(manager, rotation); + method.invoke(manager, displayId, rotation, "scrcpy#freezeRotation"); break; case 1: method.invoke(manager, displayId, rotation); break; default: - method.invoke(manager, displayId, rotation, "scrcpy#freezeRotation"); + if (displayId != 0) { + Ln.e("Secondary display rotation not supported on this device"); + return; + } + method.invoke(manager, rotation); break; } } catch (ReflectiveOperationException e) { @@ -143,13 +143,13 @@ public final class WindowManager { Method method = getIsDisplayRotationFrozenMethod(); switch (isDisplayRotationFrozenMethodVersion) { case 0: + return (boolean) method.invoke(manager, displayId); + default: if (displayId != 0) { Ln.e("Secondary display rotation not supported on this device"); return false; } return (boolean) method.invoke(manager); - default: - return (boolean) method.invoke(manager, displayId); } } catch (ReflectiveOperationException e) { Ln.e("Could not invoke method", e); @@ -162,17 +162,17 @@ public final class WindowManager { Method method = getThawDisplayRotationMethod(); switch (thawDisplayRotationMethodVersion) { case 0: - if (displayId != 0) { - Ln.e("Secondary display rotation not supported on this device"); - return; - } - method.invoke(manager); + method.invoke(manager, displayId, "scrcpy#thawRotation"); break; case 1: method.invoke(manager, displayId); break; default: - method.invoke(manager, displayId, "scrcpy#thawRotation"); + if (displayId != 0) { + Ln.e("Secondary display rotation not supported on this device"); + return; + } + method.invoke(manager); break; } } catch (ReflectiveOperationException e) { From bd8b945bb321ac73b00353c7bea74b0a5292d9a3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 12 Apr 2024 17:22:45 +0200 Subject: [PATCH 1816/2244] Register rotation watcher only when possible Old Android versions may not be able to register a rotation watcher for a secondary display. In that case, report the error instead of registering a rotation watcher for the default display. Refs Suggested by: Kaiming Hu --- .../java/com/genymobile/scrcpy/wrappers/WindowManager.java | 4 ++++ 1 file changed, 4 insertions(+) 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 ae1468f4..44394ba9 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java @@ -189,6 +189,10 @@ public final class WindowManager { cls.getMethod("watchRotation", IRotationWatcher.class, int.class).invoke(manager, rotationWatcher, displayId); } catch (NoSuchMethodException e) { // old version + if (displayId != 0) { + Ln.e("Secondary display rotation not supported on this device"); + return; + } cls.getMethod("watchRotation", IRotationWatcher.class).invoke(manager, rotationWatcher); } } catch (Exception e) { From 54e08b4eaef3fb6b603332e6aa67f95a9627ea51 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 7 Apr 2024 15:56:49 +0200 Subject: [PATCH 1817/2244] Fix code style Limit to 80 columns. --- app/src/cli.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index 92807947..3f65b5f0 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -2556,9 +2556,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], if (opts->audio_playback && opts->audio_buffer == -1) { if (opts->audio_codec == SC_CODEC_FLAC) { - // Use 50 ms audio buffer by default, but use a higher value for FLAC, - // which is not low latency (the default encoder produces blocks of - // 4096 samples, which represent ~85.333ms). + // Use 50 ms audio buffer by default, but use a higher value for + // FLAC, which is not low latency (the default encoder produces + // blocks of 4096 samples, which represent ~85.333ms). LOGI("FLAC audio: audio buffer increased to 120 ms (use " "--audio-buffer to set a custom value)"); opts->audio_buffer = SC_TICK_FROM_MS(120); From 9aa6cc71be3116be4195e3df0bc0b13ae038a944 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 7 Apr 2024 15:58:55 +0200 Subject: [PATCH 1818/2244] Forbid --no-control in OTG mode The whole purpose of OTG is to only control the device. --- app/src/cli.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/cli.c b/app/src/cli.c index 3f65b5f0..89347651 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -2598,6 +2598,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } if (otg) { + if (!opts->control) { + LOGE("--no-control is not allowed in OTG mode"); + return false; + } + enum sc_keyboard_input_mode kmode = opts->keyboard_input_mode; if (kmode != SC_KEYBOARD_INPUT_MODE_AOA && kmode != SC_KEYBOARD_INPUT_MODE_DISABLED) { From bcb8503b261969c1ec176c7b852f9e0a4924db7b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 17 Apr 2024 10:43:48 +0200 Subject: [PATCH 1819/2244] Handle reported camera sizes array is null The array of sizes may be null. Handle this case gracefully. Fixes #4852 --- .../main/java/com/genymobile/scrcpy/CameraCapture.java | 4 ++++ .../src/main/java/com/genymobile/scrcpy/LogUtils.java | 10 +++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/CameraCapture.java b/server/src/main/java/com/genymobile/scrcpy/CameraCapture.java index a1003829..df3cf7c4 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CameraCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/CameraCapture.java @@ -127,6 +127,10 @@ public class CameraCapture extends SurfaceCapture { StreamConfigurationMap configs = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); android.util.Size[] sizes = highSpeed ? configs.getHighSpeedVideoSizes() : configs.getOutputSizes(MediaCodec.class); + if (sizes == null) { + return null; + } + Stream stream = Arrays.stream(sizes); if (maxSize > 0) { stream = stream.filter(it -> it.getWidth() <= maxSize && it.getHeight() <= maxSize); diff --git a/server/src/main/java/com/genymobile/scrcpy/LogUtils.java b/server/src/main/java/com/genymobile/scrcpy/LogUtils.java index efa0672b..1ffb19d3 100644 --- a/server/src/main/java/com/genymobile/scrcpy/LogUtils.java +++ b/server/src/main/java/com/genymobile/scrcpy/LogUtils.java @@ -118,12 +118,16 @@ public final class LogUtils { StreamConfigurationMap configs = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); android.util.Size[] sizes = configs.getOutputSizes(MediaCodec.class); - for (android.util.Size size : sizes) { - builder.append("\n - ").append(size.getWidth()).append('x').append(size.getHeight()); + if (sizes == null || sizes.length == 0) { + builder.append("\n (none)"); + } else { + for (android.util.Size size : sizes) { + builder.append("\n - ").append(size.getWidth()).append('x').append(size.getHeight()); + } } android.util.Size[] highSpeedSizes = configs.getHighSpeedVideoSizes(); - if (highSpeedSizes.length > 0) { + if (highSpeedSizes != null && highSpeedSizes.length > 0) { builder.append("\n High speed capture (--camera-high-speed):"); for (android.util.Size size : highSpeedSizes) { Range[] highFpsRanges = configs.getHighSpeedVideoFpsRanges(); From 22d78e8a82bc3d6d1c18a1cc419be536201a003c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 19 Apr 2024 12:49:03 +0200 Subject: [PATCH 1820/2244] Fix boolean condition Use the short-circuit operator && between booleans. --- 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 c7a758f4..cb606d40 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -468,7 +468,7 @@ sc_input_manager_process_key(struct sc_input_manager *im, return; case SDLK_DOWN: if (shift) { - if (!repeat & down) { + if (!repeat && down) { apply_orientation_transform(im, SC_ORIENTATION_FLIP_180); } @@ -479,7 +479,7 @@ sc_input_manager_process_key(struct sc_input_manager *im, return; case SDLK_UP: if (shift) { - if (!repeat & down) { + if (!repeat && down) { apply_orientation_transform(im, SC_ORIENTATION_FLIP_180); } From cca2c9ffb7a7d400c20ee3a9962462ed4631d6db Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 19 Apr 2024 12:57:04 +0200 Subject: [PATCH 1821/2244] Disable FPS counter when no video playback There is no frame rate to count. --- app/src/cli.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/cli.c b/app/src/cli.c index 89347651..b1cc62ac 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -2811,6 +2811,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } # endif + if (opts->start_fps_counter && !opts->video_playback) { + LOGW("--print-fps has no effect without video playback"); + opts->start_fps_counter = false; + } + if (otg) { // OTG mode is compatible with only very few options. // Only report obvious errors. From 45fe6b602b4c050c5b1fba87cec7160093052af3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 7 Apr 2024 16:01:26 +0200 Subject: [PATCH 1822/2244] Add scrcpy window without video playback Add the possibility to solely control the device without screen mirroring: scrcpy --no-video --no-audio This is different from OTG mode, which does not require USB debugging at all. Here, the standard mode is used but with the possibility to disable video playback. By default, always open a window (even without video playback), and add an option --no-window. Fixes #4727 Fixes #4793 PR #4868 --- app/scrcpy.1 | 4 ++ app/src/cli.c | 51 ++++++++++++++--- app/src/display.c | 36 +++++++++++- app/src/display.h | 3 +- app/src/input_manager.c | 59 ++++++++++--------- app/src/options.c | 1 + app/src/options.h | 1 + app/src/scrcpy.c | 26 +++++---- app/src/screen.c | 123 ++++++++++++++++++++++++++++++++++------ app/src/screen.h | 4 ++ 10 files changed, 243 insertions(+), 65 deletions(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index eb09f530..f9ef3498 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -316,6 +316,10 @@ Disable video forwarding. .B \-\-no\-video\-playback Disable video playback on the computer. +.TP +.B \-\-no\-window +Disable scrcpy window. Implies --no-video-playback and --no-control. + .TP .BI "\-\-orientation " value Same as --display-orientation=value --record-orientation=value. diff --git a/app/src/cli.c b/app/src/cli.c index b1cc62ac..0caeea5c 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -97,6 +97,7 @@ enum { OPT_MOUSE, OPT_HID_KEYBOARD_DEPRECATED, OPT_HID_MOUSE_DEPRECATED, + OPT_NO_WINDOW, }; struct sc_option { @@ -566,6 +567,12 @@ static const struct sc_option options[] = { .longopt = "no-video-playback", .text = "Disable video playback on the computer.", }, + { + .longopt_id = OPT_NO_WINDOW, + .longopt = "no-window", + .text = "Disable scrcpy window. Implies --no-video-playback and " + "--no-control.", + }, { .longopt_id = OPT_ORIENTATION, .longopt = "orientation", @@ -2486,6 +2493,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], case OPT_CAMERA_HIGH_SPEED: opts->camera_high_speed = true; break; + case OPT_NO_WINDOW: + opts->window = false; + break; default: // getopt prints the error message on stderr return false; @@ -2523,6 +2533,12 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], v4l2 = !!opts->v4l2_device; #endif + if (!opts->window) { + // Without window, there cannot be any video playback or control + opts->video_playback = false; + opts->control = false; + } + if (!opts->video) { opts->video_playback = false; // Do not power on the device on start if video capture is disabled @@ -2544,8 +2560,8 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], opts->audio = false; } - if (!opts->video && !opts->audio && !otg) { - LOGE("No video, no audio, no OTG: nothing to do"); + if (!opts->video && !opts->audio && !opts->control && !otg) { + LOGE("No video, no audio, no control, no OTG: nothing to do"); return false; } @@ -2569,6 +2585,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], #ifdef HAVE_V4L2 if (v4l2) { + if (!opts->video) { + LOGE("V4L2 sink requires video capture, but --no-video was set."); + return false; + } + if (opts->lock_video_orientation == SC_LOCK_VIDEO_ORIENTATION_UNLOCKED) { LOGI("Video orientation is locked for v4l2 sink. " @@ -2588,13 +2609,25 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } #endif - if (opts->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_AUTO) { - opts->keyboard_input_mode = otg ? SC_KEYBOARD_INPUT_MODE_AOA - : SC_KEYBOARD_INPUT_MODE_SDK; - } - if (opts->mouse_input_mode == SC_MOUSE_INPUT_MODE_AUTO) { - opts->mouse_input_mode = otg ? SC_MOUSE_INPUT_MODE_AOA - : SC_MOUSE_INPUT_MODE_SDK; + if (opts->control) { + if (opts->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_AUTO) { + opts->keyboard_input_mode = otg ? SC_KEYBOARD_INPUT_MODE_AOA + : SC_KEYBOARD_INPUT_MODE_SDK; + } + if (opts->mouse_input_mode == SC_MOUSE_INPUT_MODE_AUTO) { + if (otg) { + opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_AOA; + } else if (!opts->video_playback) { + LOGI("No video mirroring, mouse mode switched to UHID"); + opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_UHID; + } else { + opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_SDK; + } + } else if (opts->mouse_input_mode == SC_MOUSE_INPUT_MODE_SDK + && !opts->video_playback) { + LOGE("SDK mouse mode requires video playback. Try --mouse=uhid."); + return false; + } } if (otg) { diff --git a/app/src/display.c b/app/src/display.c index 25c23265..9f5fb0c6 100644 --- a/app/src/display.c +++ b/app/src/display.c @@ -5,8 +5,30 @@ #include "util/log.h" +static bool +sc_display_init_novideo_icon(struct sc_display *display, + SDL_Surface *icon_novideo) { + assert(icon_novideo); + + if (SDL_RenderSetLogicalSize(display->renderer, + icon_novideo->w, icon_novideo->h)) { + LOGW("Could not set renderer logical size: %s", SDL_GetError()); + // don't fail + } + + display->texture = SDL_CreateTextureFromSurface(display->renderer, + icon_novideo); + if (!display->texture) { + LOGE("Could not create texture: %s", SDL_GetError()); + return false; + } + + return true; +} + bool -sc_display_init(struct sc_display *display, SDL_Window *window, bool mipmaps) { +sc_display_init(struct sc_display *display, SDL_Window *window, + SDL_Surface *icon_novideo, bool mipmaps) { display->renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED); if (!display->renderer) { @@ -68,6 +90,18 @@ sc_display_init(struct sc_display *display, SDL_Window *window, bool mipmaps) { display->pending.frame = NULL; display->has_frame = false; + if (icon_novideo) { + // Without video, set a static scrcpy icon as window content + bool ok = sc_display_init_novideo_icon(display, icon_novideo); + if (!ok) { +#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE + SDL_GL_DeleteContext(display->gl_context); +#endif + SDL_DestroyRenderer(display->renderer); + return false; + } + } + return true; } diff --git a/app/src/display.h b/app/src/display.h index 590715ee..064bb7bf 100644 --- a/app/src/display.h +++ b/app/src/display.h @@ -44,7 +44,8 @@ enum sc_display_result { }; bool -sc_display_init(struct sc_display *display, SDL_Window *window, bool mipmaps); +sc_display_init(struct sc_display *display, SDL_Window *window, + SDL_Surface *icon_novideo, bool mipmaps); void sc_display_destroy(struct sc_display *display); diff --git a/app/src/input_manager.c b/app/src/input_manager.c index cb606d40..3a5fc6ed 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -403,6 +403,7 @@ sc_input_manager_process_key(struct sc_input_manager *im, // controller is NULL if --no-control is requested bool control = im->controller; bool paused = im->screen->paused; + bool video = im->screen->video; SDL_Keycode keycode = event->keysym.sym; uint16_t mod = event->keysym.mod; @@ -462,13 +463,13 @@ sc_input_manager_process_key(struct sc_input_manager *im, } return; case SDLK_z: - if (down && !repeat) { + if (video && down && !repeat) { sc_screen_set_paused(im->screen, !shift); } return; case SDLK_DOWN: if (shift) { - if (!repeat && down) { + if (video && !repeat && down) { apply_orientation_transform(im, SC_ORIENTATION_FLIP_180); } @@ -479,7 +480,7 @@ sc_input_manager_process_key(struct sc_input_manager *im, return; case SDLK_UP: if (shift) { - if (!repeat && down) { + if (video && !repeat && down) { apply_orientation_transform(im, SC_ORIENTATION_FLIP_180); } @@ -489,7 +490,7 @@ sc_input_manager_process_key(struct sc_input_manager *im, } return; case SDLK_LEFT: - if (!repeat && down) { + if (video && !repeat && down) { if (shift) { apply_orientation_transform(im, SC_ORIENTATION_FLIP_0); @@ -500,7 +501,7 @@ sc_input_manager_process_key(struct sc_input_manager *im, } return; case SDLK_RIGHT: - if (!repeat && down) { + if (video && !repeat && down) { if (shift) { apply_orientation_transform(im, SC_ORIENTATION_FLIP_0); @@ -533,22 +534,22 @@ sc_input_manager_process_key(struct sc_input_manager *im, } return; case SDLK_f: - if (!shift && !repeat && down) { + if (video && !shift && !repeat && down) { sc_screen_switch_fullscreen(im->screen); } return; case SDLK_w: - if (!shift && !repeat && down) { + if (video && !shift && !repeat && down) { sc_screen_resize_to_fit(im->screen); } return; case SDLK_g: - if (!shift && !repeat && down) { + if (video && !shift && !repeat && down) { sc_screen_resize_to_pixel_perfect(im->screen); } return; case SDLK_i: - if (!shift && !repeat && down) { + if (video && !shift && !repeat && down) { switch_fps_counter_state(im); } return; @@ -625,6 +626,23 @@ sc_input_manager_process_key(struct sc_input_manager *im, im->kp->ops->process_key(im->kp, &evt, ack_to_wait); } +static struct sc_position +sc_input_manager_get_position(struct sc_input_manager *im, int32_t x, + int32_t y) { + if (im->mp->relative_mode) { + // No absolute position + return (struct sc_position) { + .screen_size = {0, 0}, + .point = {0, 0}, + }; + } + + return (struct sc_position) { + .screen_size = im->screen->frame_size, + .point = sc_screen_convert_window_to_frame_coords(im->screen, x, y), + }; +} + static void sc_input_manager_process_mouse_motion(struct sc_input_manager *im, const SDL_MouseMotionEvent *event) { @@ -634,12 +652,7 @@ sc_input_manager_process_mouse_motion(struct sc_input_manager *im, } struct sc_mouse_motion_event evt = { - .position = { - .screen_size = im->screen->frame_size, - .point = sc_screen_convert_window_to_frame_coords(im->screen, - event->x, - event->y), - }, + .position = sc_input_manager_get_position(im, event->x, event->y), .pointer_id = im->forward_all_clicks ? POINTER_ID_MOUSE : POINTER_ID_GENERIC_FINGER, .xrel = event->xrel, @@ -735,7 +748,8 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im, } // double-click on black borders resize to fit the device screen - if (event->button == SDL_BUTTON_LEFT && event->clicks == 2) { + bool video = im->screen->video; + if (video && event->button == SDL_BUTTON_LEFT && event->clicks == 2) { int32_t x = event->x; int32_t y = event->y; sc_screen_hidpi_scale_coords(im->screen, &x, &y); @@ -759,12 +773,7 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im, uint32_t sdl_buttons_state = SDL_GetMouseState(NULL, NULL); struct sc_mouse_click_event evt = { - .position = { - .screen_size = im->screen->frame_size, - .point = sc_screen_convert_window_to_frame_coords(im->screen, - event->x, - event->y), - }, + .position = sc_input_manager_get_position(im, event->x, event->y), .action = sc_action_from_sdl_mousebutton_type(event->type), .button = sc_mouse_button_from_sdl(event->button), .pointer_id = im->forward_all_clicks ? POINTER_ID_MOUSE @@ -839,11 +848,7 @@ sc_input_manager_process_mouse_wheel(struct sc_input_manager *im, uint32_t buttons = SDL_GetMouseState(&mouse_x, &mouse_y); struct sc_mouse_scroll_event evt = { - .position = { - .screen_size = im->screen->frame_size, - .point = sc_screen_convert_window_to_frame_coords(im->screen, - mouse_x, mouse_y), - }, + .position = sc_input_manager_get_position(im, mouse_x, mouse_y), #if SDL_VERSION_ATLEAST(2, 0, 18) .hscroll = CLAMP(event->preciseX, -1.0f, 1.0f), .vscroll = CLAMP(event->preciseY, -1.0f, 1.0f), diff --git a/app/src/options.c b/app/src/options.c index 7a885aa5..d6bf9158 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -89,6 +89,7 @@ const struct scrcpy_options scrcpy_options_default = { .kill_adb_on_close = false, .camera_high_speed = false, .list = 0, + .window = true, }; enum sc_orientation diff --git a/app/src/options.h b/app/src/options.h index 5445e7c8..1fb61ddf 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -279,6 +279,7 @@ struct scrcpy_options { #define SC_OPTION_LIST_CAMERAS 0x4 #define SC_OPTION_LIST_CAMERA_SIZES 0x8 uint8_t list; + bool window; }; extern const struct scrcpy_options scrcpy_options_default; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 537562f4..5e7b19fd 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -408,7 +408,7 @@ scrcpy(struct scrcpy_options *options) { return SCRCPY_EXIT_FAILURE; } - if (options->video_playback) { + if (options->window) { // Set hints before starting the server thread to avoid race conditions // in SDL sdl_set_hints(options->render_driver); @@ -430,7 +430,7 @@ scrcpy(struct scrcpy_options *options) { assert(!options->video_playback || options->video); assert(!options->audio_playback || options->audio); - if (options->video_playback || + if (options->window || (options->control && options->clipboard_autosync)) { // Initialize the video subsystem even if --no-video or // --no-video-playback is passed so that clipboard synchronization @@ -684,11 +684,12 @@ scrcpy(struct scrcpy_options *options) { // There is a controller if and only if control is enabled assert(options->control == !!controller); - if (options->video_playback) { + if (options->window) { const char *window_title = options->window_title ? options->window_title : info->device_name; struct sc_screen_params screen_params = { + .video = options->video_playback, .controller = controller, .fp = fp, .kp = kp, @@ -710,12 +711,15 @@ scrcpy(struct scrcpy_options *options) { .start_fps_counter = options->start_fps_counter, }; - struct sc_frame_source *src = &s->video_decoder.frame_source; - if (options->display_buffer) { - sc_delay_buffer_init(&s->display_buffer, options->display_buffer, - true); - sc_frame_source_add_sink(src, &s->display_buffer.frame_sink); - src = &s->display_buffer.frame_source; + struct sc_frame_source *src; + if (options->video_playback) { + src = &s->video_decoder.frame_source; + if (options->display_buffer) { + sc_delay_buffer_init(&s->display_buffer, + options->display_buffer, true); + sc_frame_source_add_sink(src, &s->display_buffer.frame_sink); + src = &s->display_buffer.frame_source; + } } if (!sc_screen_init(&s->screen, &screen_params)) { @@ -723,7 +727,9 @@ scrcpy(struct scrcpy_options *options) { } screen_initialized = true; - sc_frame_source_add_sink(src, &s->screen.frame_sink); + if (options->video_playback) { + sc_frame_source_add_sink(src, &s->screen.frame_sink); + } } if (options->audio_playback) { diff --git a/app/src/screen.c b/app/src/screen.c index 351eb3fb..56f13f99 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -205,6 +205,8 @@ sc_screen_toggle_mouse_capture(struct sc_screen *screen) { static void sc_screen_update_content_rect(struct sc_screen *screen) { + assert(screen->video); + int dw; int dh; SDL_GL_GetDrawableSize(screen->window, &dw, &dh); @@ -246,6 +248,8 @@ sc_screen_update_content_rect(struct sc_screen *screen) { // changed, so that the content rectangle is recomputed static void sc_screen_render(struct sc_screen *screen, bool update_content_rect) { + assert(screen->video); + if (update_content_rect) { sc_screen_update_content_rect(screen); } @@ -255,6 +259,13 @@ sc_screen_render(struct sc_screen *screen, bool update_content_rect) { (void) res; // any error already logged } +static void +sc_screen_render_novideo(struct sc_screen *screen) { + enum sc_display_result res = + sc_display_render(&screen->display, NULL, SC_ORIENTATION_0); + (void) res; // any error already logged +} + #if defined(__APPLE__) || defined(__WINDOWS__) # define CONTINUOUS_RESIZING_WORKAROUND #endif @@ -268,6 +279,8 @@ sc_screen_render(struct sc_screen *screen, bool update_content_rect) { static int event_watcher(void *data, SDL_Event *event) { struct sc_screen *screen = data; + assert(screen->video); + if (event->type == SDL_WINDOWEVENT && event->window.event == SDL_WINDOWEVENT_RESIZED) { // In practice, it seems to always be called from the same thread in @@ -326,6 +339,7 @@ sc_screen_frame_sink_close(struct sc_frame_sink *sink) { static bool sc_screen_frame_sink_push(struct sc_frame_sink *sink, const AVFrame *frame) { struct sc_screen *screen = DOWNCAST(sink); + assert(screen->video); bool previous_skipped; bool ok = sc_frame_buffer_push(&screen->fb, frame, &previous_skipped); @@ -364,6 +378,9 @@ sc_screen_init(struct sc_screen *screen, screen->mouse_capture_key_pressed = 0; screen->paused = false; screen->resume_frame = NULL; + screen->orientation = SC_ORIENTATION_0; + + screen->video = params->video; screen->req.x = params->window_x; screen->req.y = params->window_y; @@ -381,41 +398,75 @@ sc_screen_init(struct sc_screen *screen, goto error_destroy_frame_buffer; } - screen->orientation = params->orientation; - if (screen->orientation != SC_ORIENTATION_0) { - LOGI("Initial display orientation set to %s", - sc_orientation_get_name(screen->orientation)); + if (screen->video) { + screen->orientation = params->orientation; + if (screen->orientation != SC_ORIENTATION_0) { + LOGI("Initial display orientation set to %s", + sc_orientation_get_name(screen->orientation)); + } } - uint32_t window_flags = SDL_WINDOW_HIDDEN - | SDL_WINDOW_RESIZABLE - | SDL_WINDOW_ALLOW_HIGHDPI; + uint32_t window_flags = SDL_WINDOW_ALLOW_HIGHDPI; if (params->always_on_top) { window_flags |= SDL_WINDOW_ALWAYS_ON_TOP; } if (params->window_borderless) { window_flags |= SDL_WINDOW_BORDERLESS; } + if (params->video) { + // The window will be shown on first frame + window_flags |= SDL_WINDOW_HIDDEN + | SDL_WINDOW_RESIZABLE; + } + + const char *title = params->window_title; + assert(title); + + int x = SDL_WINDOWPOS_UNDEFINED; + int y = SDL_WINDOWPOS_UNDEFINED; + int width = 256; + int height = 256; + if (params->window_x != SC_WINDOW_POSITION_UNDEFINED) { + x = params->window_x; + } + if (params->window_y != SC_WINDOW_POSITION_UNDEFINED) { + y = params->window_y; + } + if (params->window_width) { + width = params->window_width; + } + if (params->window_height) { + height = params->window_height; + } // The window will be positioned and sized on first video frame - screen->window = - SDL_CreateWindow(params->window_title, 0, 0, 0, 0, window_flags); + screen->window = SDL_CreateWindow(title, x, y, width, height, window_flags); if (!screen->window) { LOGE("Could not create window: %s", SDL_GetError()); goto error_destroy_fps_counter; } - ok = sc_display_init(&screen->display, screen->window, params->mipmaps); - if (!ok) { - goto error_destroy_window; - } - SDL_Surface *icon = scrcpy_icon_load(); if (icon) { SDL_SetWindowIcon(screen->window, icon); - scrcpy_icon_destroy(icon); - } else { + } else if (params->video) { + // just a warning LOGW("Could not load icon"); + } else { + // without video, the icon is used as window content, it must be present + LOGE("Could not load icon"); + goto error_destroy_fps_counter; + } + + SDL_Surface *icon_novideo = params->video ? NULL : icon; + bool mipmaps = params->video && params->mipmaps; + ok = sc_display_init(&screen->display, screen->window, icon_novideo, + mipmaps); + if (icon) { + scrcpy_icon_destroy(icon); + } + if (!ok) { + goto error_destroy_window; } screen->frame = av_frame_alloc(); @@ -439,7 +490,9 @@ sc_screen_init(struct sc_screen *screen, sc_input_manager_init(&screen->im, &im_params); #ifdef CONTINUOUS_RESIZING_WORKAROUND - SDL_AddEventWatch(event_watcher, screen); + if (screen->video) { + SDL_AddEventWatch(event_watcher, screen); + } #endif static const struct sc_frame_sink_ops ops = { @@ -454,6 +507,11 @@ sc_screen_init(struct sc_screen *screen, screen->open = false; #endif + if (!screen->video && sc_screen_is_relative_mode(screen)) { + // Capture mouse immediately if video mirroring is disabled + sc_screen_set_mouse_capture(screen, true); + } + return true; error_destroy_display: @@ -524,6 +582,8 @@ sc_screen_destroy(struct sc_screen *screen) { static void resize_for_content(struct sc_screen *screen, struct sc_size old_content_size, struct sc_size new_content_size) { + assert(screen->video); + struct sc_size window_size = get_window_size(screen); struct sc_size target_size = { .width = (uint32_t) window_size.width * new_content_size.width @@ -537,6 +597,8 @@ resize_for_content(struct sc_screen *screen, struct sc_size old_content_size, static void set_content_size(struct sc_screen *screen, struct sc_size new_content_size) { + assert(screen->video); + if (!screen->fullscreen && !screen->maximized && !screen->minimized) { resize_for_content(screen, screen->content_size, new_content_size); } else if (!screen->resize_pending) { @@ -551,6 +613,8 @@ set_content_size(struct sc_screen *screen, struct sc_size new_content_size) { static void apply_pending_resize(struct sc_screen *screen) { + assert(screen->video); + assert(!screen->fullscreen); assert(!screen->maximized); assert(!screen->minimized); @@ -564,6 +628,8 @@ apply_pending_resize(struct sc_screen *screen) { void sc_screen_set_orientation(struct sc_screen *screen, enum sc_orientation orientation) { + assert(screen->video); + if (orientation == screen->orientation) { return; } @@ -598,6 +664,8 @@ sc_screen_init_size(struct sc_screen *screen) { // recreate the texture and resize the window if the frame size has changed static enum sc_display_result prepare_for_frame(struct sc_screen *screen, struct sc_size new_frame_size) { + assert(screen->video); + if (screen->frame_size.width == new_frame_size.width && screen->frame_size.height == new_frame_size.height) { return SC_DISPLAY_RESULT_OK; @@ -617,6 +685,8 @@ prepare_for_frame(struct sc_screen *screen, struct sc_size new_frame_size) { static bool sc_screen_apply_frame(struct sc_screen *screen) { + assert(screen->video); + sc_fps_counter_add_rendered_frame(&screen->fps_counter); AVFrame *frame = screen->frame; @@ -656,6 +726,8 @@ sc_screen_apply_frame(struct sc_screen *screen) { static bool sc_screen_update_frame(struct sc_screen *screen) { + assert(screen->video); + if (screen->paused) { if (!screen->resume_frame) { screen->resume_frame = av_frame_alloc(); @@ -677,6 +749,8 @@ sc_screen_update_frame(struct sc_screen *screen) { void sc_screen_set_paused(struct sc_screen *screen, bool paused) { + assert(screen->video); + if (!paused && !screen->paused) { // nothing to do return; @@ -704,6 +778,8 @@ sc_screen_set_paused(struct sc_screen *screen, bool paused) { void sc_screen_switch_fullscreen(struct sc_screen *screen) { + assert(screen->video); + uint32_t new_mode = screen->fullscreen ? 0 : SDL_WINDOW_FULLSCREEN_DESKTOP; if (SDL_SetWindowFullscreen(screen->window, new_mode)) { LOGW("Could not switch fullscreen mode: %s", SDL_GetError()); @@ -721,6 +797,8 @@ sc_screen_switch_fullscreen(struct sc_screen *screen) { void sc_screen_resize_to_fit(struct sc_screen *screen) { + assert(screen->video); + if (screen->fullscreen || screen->maximized || screen->minimized) { return; } @@ -745,6 +823,8 @@ sc_screen_resize_to_fit(struct sc_screen *screen) { void sc_screen_resize_to_pixel_perfect(struct sc_screen *screen) { + assert(screen->video); + if (screen->fullscreen || screen->minimized) { return; } @@ -788,6 +868,13 @@ sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) { return true; } case SDL_WINDOWEVENT: + if (!screen->video + && event->window.event == SDL_WINDOWEVENT_EXPOSED) { + sc_screen_render_novideo(screen); + } + + // !video implies !has_frame + assert(screen->video || !screen->has_frame); if (!screen->has_frame) { // Do nothing return true; @@ -891,6 +978,8 @@ sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) { struct sc_point sc_screen_convert_drawable_to_frame_coords(struct sc_screen *screen, int32_t x, int32_t y) { + assert(screen->video); + enum sc_orientation orientation = screen->orientation; int32_t w = screen->content_size.width; diff --git a/app/src/screen.h b/app/src/screen.h index 361ce455..3e205cdc 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -26,6 +26,8 @@ struct sc_screen { bool open; // track the open/close state to assert correct behavior #endif + bool video; + struct sc_display display; struct sc_input_manager im; struct sc_frame_buffer fb; @@ -70,6 +72,8 @@ struct sc_screen { }; struct sc_screen_params { + bool video; + struct sc_controller *controller; struct sc_file_pusher *fp; struct sc_key_processor *kp; From b5c8de08e0439b1d6c5827a36faf341fad8986af Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 20 Apr 2024 18:12:20 +0200 Subject: [PATCH 1823/2244] Update documentation for --no-window PR #4868 --- doc/audio.md | 11 +++++++++-- doc/control.md | 25 +++++++++++++++++++++++++ doc/otg.md | 43 ++++++++++++++++++++++++++++++++----------- doc/recording.md | 13 +++++++++---- doc/window.md | 9 +++++++++ 5 files changed, 84 insertions(+), 17 deletions(-) diff --git a/doc/audio.md b/doc/audio.md index ecae4468..30dd0f97 100644 --- a/doc/audio.md +++ b/doc/audio.md @@ -28,10 +28,17 @@ To disable only the audio playback, see [no playback](video.md#no-playback). ## Audio only -To play audio only, disable the video: +To play audio only, disable video and control: ```bash -scrcpy --no-video +scrcpy --no-video --no-control +``` + +To play audio without a window: + +```bash +# --no-video and --no-control are implied by --no-window +scrcpy --no-window # interrupt with Ctrl+C ``` diff --git a/doc/control.md b/doc/control.md index e9fd9e9b..87897894 100644 --- a/doc/control.md +++ b/doc/control.md @@ -15,6 +15,31 @@ scrcpy -n # short version Read [keyboard](keyboard.md) and [mouse](mouse.md). +## Control only + +To control the device without mirroring: + +```bash +scrcpy --no-video --no-audio +``` + +By default, mouse mode is switched to UHID if video mirroring is disabled (a +relative mouse mode is required). + +To also use a UHID keyboard, set it explicitly: + +```bash +scrcpy --no-video --no-audio --keyboard=uhid +scrcpy --no-video --no-audio -K # short version +``` + +To use AOA instead (over USB only): + +```bash +scrcpy --no-video --no-audio --keyboard=aoa --mouse=aoa +``` + + ## Copy-paste Any time the Android clipboard changes, it is automatically synchronized to the diff --git a/doc/otg.md b/doc/otg.md index 3c7ed467..5f42ac9c 100644 --- a/doc/otg.md +++ b/doc/otg.md @@ -1,19 +1,21 @@ # OTG By default, _scrcpy_ injects input events at the Android API level. As an -alternative, when connected over USB, it is possible to send HID events, so that -scrcpy behaves as if it was a physical keyboard and/or mouse connected to the -Android device. +alternative, it is possible to send HID events, so that scrcpy behaves as if it +was a [physical keyboard] and/or a [physical mouse] connected to the Android +device (see [keyboard](keyboard.md) and [mouse](mouse.md)). -A special mode allows to control the device without mirroring, using AOA -[keyboard](keyboard.md#aoa) and [mouse](mouse.md#aoa). Therefore, it is possible -to run _scrcpy_ with only physical keyboard and mouse simulation (HID), as if -the computer keyboard and mouse were plugged directly to the device via an OTG -cable. +[physical keyboard]: keyboard.md#physical-keyboard-simulation +[physical mouse]: physical-keyboard-simulation -In this mode, `adb` (USB debugging) is not necessary, and mirroring is disabled. +A special mode (OTG) allows to control the device using AOA +[keyboard](keyboard.md#aoa) and [mouse](mouse.md#aoa), without using _adb_ at +all (so USB debugging is not necessary). In this mode, video and audio are +disabled, and `--keyboard=aoa and `--mouse=aoa` are implicitly set. -This is similar to `--keyboard=aoa --mouse=aoa`, but without mirroring. +Therefore, it is possible to run _scrcpy_ with only physical keyboard and mouse +simulation, as if the computer keyboard and mouse were plugged directly to the +device via an OTG cable. To enable OTG mode: @@ -23,7 +25,7 @@ scrcpy --otg scrcpy --otg -s 0123456789abcdef ``` -It is possible to disable HID keyboard or HID mouse: +It is possible to disable keyboard or mouse: ```bash scrcpy --otg --keyboard=disabled @@ -35,3 +37,22 @@ It only works if the device is connected over USB. ## OTG issues on Windows See [FAQ](/FAQ.md#otg-issues-on-windows). + + +## Control only + +Note that the purpose of OTG is to control the device without USB debugging +(adb). + +If you want to solely control the device without mirroring while USB debugging +is enabled, then OTG mode is not necessary. + +Instead, disable video and audio, and select UHID (or AOA): + +```bash +scrcpy --no-video --no-audio --keyboard=uhid --mouse=uhid +scrcpy --no-video --no-audio -KM # short version +scrcpy --no-video --no-audio --keyboard=aoa --mouse=aoa +``` + +One benefit of UHID is that it also works wirelessly. diff --git a/doc/recording.md b/doc/recording.md index 216542e9..f1a5a6e7 100644 --- a/doc/recording.md +++ b/doc/recording.md @@ -58,12 +58,10 @@ orientation](video.md#orientation). ## No playback -To disable playback while recording: +To disable playback and control while recording: ```bash -scrcpy --no-playback --record=file.mp4 -scrcpy -Nr file.mkv -# interrupt recording with Ctrl+C +scrcpy --no-playback --no-control --record=file.mp4 ``` It is also possible to disable video and audio playback separately: @@ -73,6 +71,13 @@ It is also possible to disable video and audio playback separately: scrcpy --record=file.mkv --no-audio-playback ``` +To also disable the window: + +```bash +scrcpy --no-playback --no-window --record=file.mp4 +# interrupt recording with Ctrl+C +``` + ## Time limit To limit the recording time: diff --git a/doc/window.md b/doc/window.md index b5b73921..b72c716c 100644 --- a/doc/window.md +++ b/doc/window.md @@ -1,5 +1,14 @@ # Window +## Disable window + +To disable window (may be useful for recording or for playing audio only): + +```bash +scrcpy --no-window --record=file.mp4 +# Ctrl+C to interrupt +``` + ## Title By default, the window title is the device model. It can be changed: From 063a8339ed27b94a8fe1e53a284507eb2d044e15 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 11 May 2024 16:40:22 +0200 Subject: [PATCH 1824/2244] Terminate on controller error This is particularly important to react to server socket disconnection since video and audio may be disabled. PR #4868 --- app/src/controller.c | 32 +++++++++++++++++++++++++++++--- app/src/controller.h | 11 ++++++++++- app/src/events.h | 1 + app/src/receiver.c | 9 ++++++++- app/src/receiver.h | 10 +++++++++- app/src/scrcpy.c | 20 +++++++++++++++++++- 6 files changed, 76 insertions(+), 7 deletions(-) diff --git a/app/src/controller.c b/app/src/controller.c index 499cfd3c..edd767eb 100644 --- a/app/src/controller.c +++ b/app/src/controller.c @@ -6,8 +6,19 @@ #define SC_CONTROL_MSG_QUEUE_MAX 64 +static void +sc_controller_receiver_on_error(struct sc_receiver *receiver, void *userdata) { + (void) receiver; + + struct sc_controller *controller = userdata; + // Forward the event to the controller listener + controller->cbs->on_error(controller, controller->cbs_userdata); +} + bool -sc_controller_init(struct sc_controller *controller, sc_socket control_socket) { +sc_controller_init(struct sc_controller *controller, sc_socket control_socket, + const struct sc_controller_callbacks *cbs, + void *cbs_userdata) { sc_vecdeque_init(&controller->queue); bool ok = sc_vecdeque_reserve(&controller->queue, SC_CONTROL_MSG_QUEUE_MAX); @@ -15,7 +26,12 @@ sc_controller_init(struct sc_controller *controller, sc_socket control_socket) { return false; } - ok = sc_receiver_init(&controller->receiver, control_socket); + static const struct sc_receiver_callbacks receiver_cbs = { + .on_error = sc_controller_receiver_on_error, + }; + + ok = sc_receiver_init(&controller->receiver, control_socket, &receiver_cbs, + controller); if (!ok) { sc_vecdeque_destroy(&controller->queue); return false; @@ -39,6 +55,10 @@ sc_controller_init(struct sc_controller *controller, sc_socket control_socket) { controller->control_socket = control_socket; controller->stopped = false; + assert(cbs && cbs->on_error); + controller->cbs = cbs; + controller->cbs_userdata = cbs_userdata; + return true; } @@ -125,10 +145,16 @@ run_controller(void *data) { sc_control_msg_destroy(&msg); if (!ok) { LOGD("Could not write msg to socket"); - break; + goto error; } } + return 0; + +error: + controller->cbs->on_error(controller, controller->cbs_userdata); + + return 1; // ignored } bool diff --git a/app/src/controller.h b/app/src/controller.h index 1e44427e..353d4d0d 100644 --- a/app/src/controller.h +++ b/app/src/controller.h @@ -22,10 +22,19 @@ struct sc_controller { bool stopped; struct sc_control_msg_queue queue; struct sc_receiver receiver; + + const struct sc_controller_callbacks *cbs; + void *cbs_userdata; +}; + +struct sc_controller_callbacks { + void (*on_error)(struct sc_controller *controller, void *userdata); }; bool -sc_controller_init(struct sc_controller *controller, sc_socket control_socket); +sc_controller_init(struct sc_controller *controller, sc_socket control_socket, + const struct sc_controller_callbacks *cbs, + void *cbs_userdata); void sc_controller_configure(struct sc_controller *controller, diff --git a/app/src/events.h b/app/src/events.h index 8bfa2582..3cf2b1dd 100644 --- a/app/src/events.h +++ b/app/src/events.h @@ -7,3 +7,4 @@ #define SC_EVENT_RECORDER_ERROR (SDL_USEREVENT + 6) #define SC_EVENT_SCREEN_INIT_SIZE (SDL_USEREVENT + 7) #define SC_EVENT_TIME_LIMIT_REACHED (SDL_USEREVENT + 8) +#define SC_EVENT_CONTROLLER_ERROR (SDL_USEREVENT + 9) diff --git a/app/src/receiver.c b/app/src/receiver.c index f4ebd3f8..fb923ac4 100644 --- a/app/src/receiver.c +++ b/app/src/receiver.c @@ -10,7 +10,8 @@ #include "util/str.h" bool -sc_receiver_init(struct sc_receiver *receiver, sc_socket control_socket) { +sc_receiver_init(struct sc_receiver *receiver, sc_socket control_socket, + const struct sc_receiver_callbacks *cbs, void *cbs_userdata) { bool ok = sc_mutex_init(&receiver->mutex); if (!ok) { return false; @@ -20,6 +21,10 @@ sc_receiver_init(struct sc_receiver *receiver, sc_socket control_socket) { receiver->acksync = NULL; receiver->uhid_devices = NULL; + assert(cbs && cbs->on_error); + receiver->cbs = cbs; + receiver->cbs_userdata = cbs_userdata; + return true; } @@ -152,6 +157,8 @@ run_receiver(void *data) { } } + receiver->cbs->on_error(receiver, receiver->cbs_userdata); + return 0; } diff --git a/app/src/receiver.h b/app/src/receiver.h index ba84c0ab..ef83978f 100644 --- a/app/src/receiver.h +++ b/app/src/receiver.h @@ -19,10 +19,18 @@ struct sc_receiver { struct sc_acksync *acksync; struct sc_uhid_devices *uhid_devices; + + const struct sc_receiver_callbacks *cbs; + void *cbs_userdata; +}; + +struct sc_receiver_callbacks { + void (*on_error)(struct sc_receiver *receiver, void *userdata); }; bool -sc_receiver_init(struct sc_receiver *receiver, sc_socket control_socket); +sc_receiver_init(struct sc_receiver *receiver, sc_socket control_socket, + const struct sc_receiver_callbacks *cbs, void *cbs_userdata); void sc_receiver_destroy(struct sc_receiver *receiver); diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 5e7b19fd..b07611f1 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -174,6 +174,9 @@ event_loop(struct scrcpy *s) { case SC_EVENT_DEMUXER_ERROR: LOGE("Demuxer error"); return SCRCPY_EXIT_FAILURE; + case SC_EVENT_CONTROLLER_ERROR: + LOGE("Controller error"); + return SCRCPY_EXIT_FAILURE; case SC_EVENT_RECORDER_ERROR: LOGE("Recorder error"); return SCRCPY_EXIT_FAILURE; @@ -265,6 +268,16 @@ sc_audio_demuxer_on_ended(struct sc_demuxer *demuxer, } } +static void +sc_controller_on_error(struct sc_controller *controller, void *userdata) { + // Note: this function may be called twice, once from the controller thread + // and once from the receiver thread + (void) controller; + (void) userdata; + + PUSH_EVENT(SC_EVENT_CONTROLLER_ERROR); +} + static void sc_server_on_connection_failed(struct sc_server *server, void *userdata) { (void) server; @@ -553,7 +566,12 @@ scrcpy(struct scrcpy_options *options) { struct sc_mouse_processor *mp = NULL; if (options->control) { - if (!sc_controller_init(&s->controller, s->server.control_socket)) { + static const struct sc_controller_callbacks controller_cbs = { + .on_error = sc_controller_on_error, + }; + + if (!sc_controller_init(&s->controller, s->server.control_socket, + &controller_cbs, NULL)) { goto end; } controller_initialized = true; From da484b7ab9904beae0128aa2066f5d04a9c9e840 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 12 May 2024 10:44:27 +0200 Subject: [PATCH 1825/2244] Reject recording with control only If video and audio are disabled, there is nothing to record. --- app/src/cli.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/cli.c b/app/src/cli.c index 0caeea5c..a180c0e6 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -2738,6 +2738,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } if (opts->record_filename) { + if (!opts->video && !opts->audio) { + LOGE("Video and audio disabled, nothing to record"); + return false; + } + if (!opts->record_format) { opts->record_format = guess_record_format(opts->record_filename); if (!opts->record_format) { From 09e8c20168a7d608fa850aabada4f404e1c698b4 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 14 May 2024 08:23:18 +0200 Subject: [PATCH 1826/2244] Rename streamScreen() to streamCapture() The capture source may be either the screen or the camera. --- .../src/main/java/com/genymobile/scrcpy/SurfaceEncoder.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/SurfaceEncoder.java b/server/src/main/java/com/genymobile/scrcpy/SurfaceEncoder.java index 8eda8231..4a0fdf4e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/SurfaceEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/SurfaceEncoder.java @@ -48,7 +48,7 @@ public class SurfaceEncoder implements AsyncProcessor { this.downsizeOnError = downsizeOnError; } - private void streamScreen() throws IOException, ConfigurationException { + private void streamCapture() throws IOException, ConfigurationException { Codec codec = streamer.getCodec(); MediaCodec mediaCodec = createMediaCodec(codec, encoderName); MediaFormat format = createFormat(codec.getMimeType(), videoBitRate, maxFps, codecOptions); @@ -254,7 +254,7 @@ public class SurfaceEncoder implements AsyncProcessor { Looper.prepare(); try { - streamScreen(); + streamCapture(); } catch (ConfigurationException e) { // Do not print stack trace, a user-friendly error-message has already been logged } catch (IOException e) { From b5849db32fdd4bd1bba4e8084e5bfa368f6e9747 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 29 May 2024 10:32:58 +0200 Subject: [PATCH 1827/2244] Document missing package to build for Windows To build ffmpeg, libz is necessary. Refs #4955 --- doc/build.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/build.md b/doc/build.md index 751cf831..01319a10 100644 --- a/doc/build.md +++ b/doc/build.md @@ -94,7 +94,7 @@ This is the preferred method (and the way the release is built). From _Debian_, install _mingw_: ```bash -sudo apt install mingw-w64 mingw-w64-tools +sudo apt install mingw-w64 mingw-w64-tools libz-mingw-w64-dev ``` You also need the JDK to build the server: From c27ab46efbcab0b9558a91e691d799ffef496c97 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 30 May 2024 08:23:42 +0200 Subject: [PATCH 1828/2244] Remove suggestion to install from winget It does not work. Refs #4027 Refs #4389 Refs #4956 --- doc/windows.md | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/doc/windows.md b/doc/windows.md index a3711f26..e3053188 100644 --- a/doc/windows.md +++ b/doc/windows.md @@ -15,13 +15,7 @@ Download the [latest release]: and extract it. -Alternatively, you could install it from packages manager, like [Winget]: - -```bash -winget install scrcpy -``` - -or [Chocolatey]: +Alternatively, you could install it from packages manager, like [Chocolatey]: ```bash choco install scrcpy From fd9498e07c949828a6aedfc17340641b6ec56c0c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 30 May 2024 15:49:07 +0200 Subject: [PATCH 1829/2244] Avoid zero-length copies Return early if there is nothing to read/write. --- app/src/util/audiobuf.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/util/audiobuf.c b/app/src/util/audiobuf.c index 3597f7ee..3cc5cad1 100644 --- a/app/src/util/audiobuf.c +++ b/app/src/util/audiobuf.c @@ -46,6 +46,9 @@ sc_audiobuf_read(struct sc_audiobuf *buf, void *to_, uint32_t samples_count) { uint32_t head = atomic_load_explicit(&buf->head, memory_order_acquire); uint32_t can_read = (buf->alloc_size + head - tail) % buf->alloc_size; + if (!can_read) { + return 0; + } if (samples_count > can_read) { samples_count = can_read; } @@ -86,6 +89,9 @@ sc_audiobuf_write(struct sc_audiobuf *buf, const void *from_, uint32_t tail = atomic_load_explicit(&buf->tail, memory_order_acquire); uint32_t can_write = (buf->alloc_size + tail - head - 1) % buf->alloc_size; + if (!can_write) { + return 0; + } if (samples_count > can_write) { samples_count = can_write; } From 5d1d5bdc169fdc1ef836a8c04f794fabe363f44b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 9 Jun 2024 18:27:30 +0200 Subject: [PATCH 1830/2244] Fix thread leak on Windows Fixes #4973 --- app/src/sys/win/process.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/sys/win/process.c b/app/src/sys/win/process.c index 6e9da09c..6ae33d86 100644 --- a/app/src/sys/win/process.c +++ b/app/src/sys/win/process.c @@ -176,6 +176,8 @@ sc_process_execute_p(const char *const argv[], HANDLE *handle, unsigned flags, free(lpAttributeList); } + CloseHandle(pi.hThread); + // These handles are used by the child process, close them for this process if (pin) { CloseHandle(stdin_read_handle); From 9ea4446369e53936032668d483aede39e49c84c1 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 9 Jun 2024 19:25:32 +0200 Subject: [PATCH 1831/2244] Release the audio lock early The final write from the writer thread does not require a lock: it is guaranteed that enough space is available since the reader thread never writes. --- app/src/audio_player.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/audio_player.c b/app/src/audio_player.c index bd799c51..dac85bf9 100644 --- a/app/src/audio_player.c +++ b/app/src/audio_player.c @@ -194,7 +194,11 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink, // Still insufficient, drop old samples to make space skipped_samples = sc_audiobuf_read(&ap->buf, NULL, remaining); assert(skipped_samples == remaining); + } + SDL_UnlockAudioDevice(ap->device); + + if (written < samples) { // Now there is enough space uint32_t w = sc_audiobuf_write(&ap->buf, swr_buf + TO_BYTES(written), @@ -202,8 +206,6 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink, assert(w == remaining); (void) w; } - - SDL_UnlockAudioDevice(ap->device); } uint32_t underflow = 0; From 24b9e0a9705ab0b283fd2796493a25c6e4d7db42 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 11 Jun 2024 08:58:19 +0200 Subject: [PATCH 1832/2244] Retrieve icon decoder directly The call to av_find_best_stream() gives the decoder directly, this avoids to retrieve it afterwards in a separate step. --- app/src/icon.c | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/app/src/icon.c b/app/src/icon.c index a9aad875..0dddefa3 100644 --- a/app/src/icon.c +++ b/app/src/icon.c @@ -78,7 +78,19 @@ decode_image(const char *path) { goto close_input; } - int stream = av_find_best_stream(ctx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0); + +// In ffmpeg/doc/APIchanges: +// 2021-04-27 - 46dac8cf3d - lavf 59.0.100 - avformat.h +// av_find_best_stream now uses a const AVCodec ** parameter +// for the returned decoder. +#if LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(59, 0, 100) + const AVCodec *codec; +#else + AVCodec *codec; +#endif + + int stream = + av_find_best_stream(ctx, AVMEDIA_TYPE_VIDEO, -1, -1, &codec, 0); if (stream < 0 ) { LOGE("Could not find best image stream"); goto close_input; @@ -86,12 +98,6 @@ decode_image(const char *path) { AVCodecParameters *params = ctx->streams[stream]->codecpar; - const AVCodec *codec = avcodec_find_decoder(params->codec_id); - if (!codec) { - LOGE("Could not find image decoder"); - goto close_input; - } - AVCodecContext *codec_ctx = avcodec_alloc_context3(codec); if (!codec_ctx) { LOG_OOM(); From 576e7552a29e30b40205f81f2ff4d461f018313f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 13 Jun 2024 09:11:32 +0200 Subject: [PATCH 1833/2244] Mention that the Debian package is obsolete It cannot be updated until the android-framework-XX Debian package is fixed. Refs --- doc/linux.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/linux.md b/doc/linux.md index 68b4ee10..6bfe3454 100644 --- a/doc/linux.md +++ b/doc/linux.md @@ -6,7 +6,7 @@ Scrcpy is packaged in several distributions and package managers: - - Debian/Ubuntu: `apt install scrcpy` + - Debian/Ubuntu: ~~`apt install scrcpy`~~ _(obsolete version)_ - Arch Linux: `pacman -S scrcpy` - Fedora: `dnf copr enable zeno/scrcpy && dnf install scrcpy` - Gentoo: `emerge scrcpy` From 9030bd8be434ee54abbcd7aad0d4e37c699a362b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 21 Jun 2024 12:12:13 +0200 Subject: [PATCH 1834/2244] Upgrade AGP from 8.1.3 to 8.3.0 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index b27befb6..f81f7d27 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:8.1.3' + classpath 'com.android.tools.build:gradle:8.3.0' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files From 30e42af2d4ca660b06bce2ada7b1a0d303c73239 Mon Sep 17 00:00:00 2001 From: wuderek Date: Fri, 21 Jun 2024 13:41:15 +0800 Subject: [PATCH 1835/2244] Add missing virtual display release() PR #5008 Signed-off-by: Romain Vimont --- .../src/main/java/com/genymobile/scrcpy/ScreenCapture.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenCapture.java b/server/src/main/java/com/genymobile/scrcpy/ScreenCapture.java index 95214188..1d878d78 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenCapture.java @@ -68,6 +68,11 @@ public class ScreenCapture extends SurfaceCapture implements Device.RotationList device.setFoldListener(null); if (display != null) { SurfaceControl.destroyDisplay(display); + display = null; + } + if (virtualDisplay != null) { + virtualDisplay.release(); + virtualDisplay = null; } } From 592ca0b59b3f57e6d721a57554e0efc5a52bb70f Mon Sep 17 00:00:00 2001 From: wuderek Date: Fri, 21 Jun 2024 13:41:15 +0800 Subject: [PATCH 1836/2244] Try newer display API first The old createDisplay() API has been removed from Android. Try the newer API first, since more and more devices will use that version. PR #5008 --- .../com/genymobile/scrcpy/ScreenCapture.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenCapture.java b/server/src/main/java/com/genymobile/scrcpy/ScreenCapture.java index 1d878d78..090c96f0 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenCapture.java @@ -45,18 +45,18 @@ public class ScreenCapture extends SurfaceCapture implements Device.RotationList } try { - display = createDisplay(); - setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack); - Ln.d("Display: using SurfaceControl API"); - } catch (Exception surfaceControlException) { Rect videoRect = screenInfo.getVideoSize().toRect(); + virtualDisplay = ServiceManager.getDisplayManager() + .createVirtualDisplay("scrcpy", videoRect.width(), videoRect.height(), device.getDisplayId(), surface); + Ln.d("Display: using DisplayManager API"); + } catch (Exception displayManagerException) { try { - virtualDisplay = ServiceManager.getDisplayManager() - .createVirtualDisplay("scrcpy", videoRect.width(), videoRect.height(), device.getDisplayId(), surface); - Ln.d("Display: using DisplayManager API"); - } catch (Exception displayManagerException) { - Ln.e("Could not create display using SurfaceControl", surfaceControlException); + display = createDisplay(); + setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack); + Ln.d("Display: using SurfaceControl API"); + } catch (Exception surfaceControlException) { Ln.e("Could not create display using DisplayManager", displayManagerException); + Ln.e("Could not create display using SurfaceControl", surfaceControlException); throw new AssertionError("Could not create display"); } } From 24bcc3fa2b4323091a487ad1a2568d02ad0d2042 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 7 Mar 2024 23:07:01 +0100 Subject: [PATCH 1837/2244] Simplify shortcut modifiers Restrict shortcut modifiers to be composed of only one item each. Before, it was possible to select a list of multiple combinations of modifier keys, like --shortcut-mod='lctrl+lalt,rctrl+rsuper', meaning that shortcuts would be triggered either by LCtrl+LAlt+key or RCtrl+RSuper+key. This was overly generic, probably not used very much, and it prevents to solve inconsistencies between UP and DOWN events of modifier keys sent to the device. Refs #4732 PR #4741 --- app/scrcpy.1 | 4 +- app/src/cli.c | 100 ++++++++++++++++------------------------ app/src/cli.h | 2 +- app/src/input_manager.c | 23 ++------- app/src/input_manager.h | 7 +-- app/src/options.c | 5 +- app/src/options.h | 9 +--- app/src/scrcpy.c | 2 +- app/src/screen.h | 2 +- app/tests/test_cli.c | 24 +++------- doc/shortcuts.md | 4 +- 11 files changed, 62 insertions(+), 120 deletions(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index f9ef3498..2be9ef59 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -424,9 +424,9 @@ Turn the device screen off immediately. .BI "\-\-shortcut\-mod " key\fR[+...]][,...] Specify the modifiers to use for scrcpy shortcuts. Possible keys are "lctrl", "rctrl", "lalt", "ralt", "lsuper" and "rsuper". -A shortcut can consist in several keys, separated by '+'. Several shortcuts can be specified, separated by ','. +Several shortcut modifiers can be specified, separated by ','. -For example, to use either LCtrl+LAlt or LSuper for scrcpy shortcuts, pass "lctrl+lalt,lsuper". +For example, to use either LCtrl or LSuper for scrcpy shortcuts, pass "lctrl,lsuper". Default is "lalt,lsuper" (left-Alt or left-Super). diff --git a/app/src/cli.c b/app/src/cli.c index a180c0e6..a0c0b338 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -716,10 +716,10 @@ static const struct sc_option options[] = { .text = "Specify the modifiers to use for scrcpy shortcuts.\n" "Possible keys are \"lctrl\", \"rctrl\", \"lalt\", \"ralt\", " "\"lsuper\" and \"rsuper\".\n" - "A shortcut can consist in several keys, separated by '+'. " - "Several shortcuts can be specified, separated by ','.\n" - "For example, to use either LCtrl+LAlt or LSuper for scrcpy " - "shortcuts, pass \"lctrl+lalt,lsuper\".\n" + "Several shortcut modifiers can be specified, separated by " + "','.\n" + "For example, to use either LCtrl or LSuper for scrcpy " + "shortcuts, pass \"lctrl,lsuper\".\n" "Default is \"lalt,lsuper\" (left-Alt or left-Super).", }, { @@ -1687,82 +1687,62 @@ parse_log_level(const char *s, enum sc_log_level *log_level) { return false; } -// item is a list of mod keys separated by '+' (e.g. "lctrl+lalt") -// returns a bitwise-or of SC_SHORTCUT_MOD_* constants (or 0 on error) -static unsigned +static enum sc_shortcut_mod parse_shortcut_mods_item(const char *item, size_t len) { - unsigned mod = 0; - - for (;;) { - char *plus = strchr(item, '+'); - // strchr() does not consider the "len" parameter, to it could find an - // occurrence too far in the string (there is no strnchr()) - bool has_plus = plus && plus < item + len; - - assert(!has_plus || plus > item); - size_t key_len = has_plus ? (size_t) (plus - item) : len; - #define STREQ(literal, s, len) \ ((sizeof(literal)-1 == len) && !memcmp(literal, s, len)) - if (STREQ("lctrl", item, key_len)) { - mod |= SC_SHORTCUT_MOD_LCTRL; - } else if (STREQ("rctrl", item, key_len)) { - mod |= SC_SHORTCUT_MOD_RCTRL; - } else if (STREQ("lalt", item, key_len)) { - mod |= SC_SHORTCUT_MOD_LALT; - } else if (STREQ("ralt", item, key_len)) { - mod |= SC_SHORTCUT_MOD_RALT; - } else if (STREQ("lsuper", item, key_len)) { - mod |= SC_SHORTCUT_MOD_LSUPER; - } else if (STREQ("rsuper", item, key_len)) { - mod |= SC_SHORTCUT_MOD_RSUPER; - } else { - LOGE("Unknown modifier key: %.*s " - "(must be one of: lctrl, rctrl, lalt, ralt, lsuper, rsuper)", - (int) key_len, item); - return 0; - } + if (STREQ("lctrl", item, len)) { + return SC_SHORTCUT_MOD_LCTRL; + } + if (STREQ("rctrl", item, len)) { + return SC_SHORTCUT_MOD_RCTRL; + } + if (STREQ("lalt", item, len)) { + return SC_SHORTCUT_MOD_LALT; + } + if (STREQ("ralt", item, len)) { + return SC_SHORTCUT_MOD_RALT; + } + if (STREQ("lsuper", item, len)) { + return SC_SHORTCUT_MOD_LSUPER; + } + if (STREQ("rsuper", item, len)) { + return SC_SHORTCUT_MOD_RSUPER; + } #undef STREQ - if (!has_plus) { - break; - } - - item = plus + 1; - assert(len >= key_len + 1); - len -= key_len + 1; + bool has_plus = strchr(item, '+'); + if (has_plus) { + LOGE("Shortcut mod combination with '+' is not supported anymore: " + "'%.*s' (see #4741)", (int) len, item); + return 0; } - return mod; + LOGE("Unknown modifier key: %.*s " + "(must be one of: lctrl, rctrl, lalt, ralt, lsuper, rsuper)", + (int) len, item); + + return 0; } static bool -parse_shortcut_mods(const char *s, struct sc_shortcut_mods *mods) { - unsigned count = 0; - unsigned current = 0; +parse_shortcut_mods(const char *s, uint8_t *shortcut_mods) { + uint8_t mods = 0; - // LCtrl+LAlt or RCtrl or LCtrl+RSuper: "lctrl+lalt,rctrl,lctrl+rsuper" + // A list of shortcut modifiers, for example "lctrl,rctrl,rsuper" for (;;) { char *comma = strchr(s, ','); - if (comma && count == SC_MAX_SHORTCUT_MODS - 1) { - assert(count < SC_MAX_SHORTCUT_MODS); - LOGW("Too many shortcut modifiers alternatives"); - return false; - } - assert(!comma || comma > s); size_t limit = comma ? (size_t) (comma - s) : strlen(s); - unsigned mod = parse_shortcut_mods_item(s, limit); + enum sc_shortcut_mod mod = parse_shortcut_mods_item(s, limit); if (!mod) { - LOGE("Invalid modifier keys: %.*s", (int) limit, s); return false; } - mods->data[current++] = mod; - ++count; + mods |= mod; if (!comma) { break; @@ -1771,7 +1751,7 @@ parse_shortcut_mods(const char *s, struct sc_shortcut_mods *mods) { s = comma + 1; } - mods->count = count; + *shortcut_mods = mods; return true; } @@ -1779,7 +1759,7 @@ parse_shortcut_mods(const char *s, struct sc_shortcut_mods *mods) { #ifdef SC_TEST // expose the function to unit-tests bool -sc_parse_shortcut_mods(const char *s, struct sc_shortcut_mods *mods) { +sc_parse_shortcut_mods(const char *s, uint8_t *mods) { return parse_shortcut_mods(s, mods); } #endif diff --git a/app/src/cli.h b/app/src/cli.h index 23d34fcd..6fd579a4 100644 --- a/app/src/cli.h +++ b/app/src/cli.h @@ -28,7 +28,7 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]); #ifdef SC_TEST bool -sc_parse_shortcut_mods(const char *s, struct sc_shortcut_mods *mods); +sc_parse_shortcut_mods(const char *s, uint8_t *shortcut_mods); #endif #endif diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 3a5fc6ed..91e65bfd 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -10,7 +10,7 @@ #define SC_SDL_SHORTCUT_MODS_MASK (KMOD_CTRL | KMOD_ALT | KMOD_GUI) static inline uint16_t -to_sdl_mod(unsigned shortcut_mod) { +to_sdl_mod(uint8_t shortcut_mod) { uint16_t sdl_mod = 0; if (shortcut_mod & SC_SHORTCUT_MOD_LCTRL) { sdl_mod |= KMOD_LCTRL; @@ -38,15 +38,8 @@ is_shortcut_mod(struct sc_input_manager *im, uint16_t sdl_mod) { // keep only the relevant modifier keys sdl_mod &= SC_SDL_SHORTCUT_MODS_MASK; - assert(im->sdl_shortcut_mods.count); - assert(im->sdl_shortcut_mods.count < SC_MAX_SHORTCUT_MODS); - for (unsigned i = 0; i < im->sdl_shortcut_mods.count; ++i) { - if (im->sdl_shortcut_mods.data[i] == sdl_mod) { - return true; - } - } - - return false; + // at least one shortcut mod pressed? + return sdl_mod & im->sdl_shortcut_mods; } void @@ -68,15 +61,7 @@ sc_input_manager_init(struct sc_input_manager *im, im->legacy_paste = params->legacy_paste; im->clipboard_autosync = params->clipboard_autosync; - const struct sc_shortcut_mods *shortcut_mods = params->shortcut_mods; - assert(shortcut_mods->count); - assert(shortcut_mods->count < SC_MAX_SHORTCUT_MODS); - for (unsigned i = 0; i < shortcut_mods->count; ++i) { - uint16_t sdl_mod = to_sdl_mod(shortcut_mods->data[i]); - assert(sdl_mod); - im->sdl_shortcut_mods.data[i] = sdl_mod; - } - im->sdl_shortcut_mods.count = shortcut_mods->count; + im->sdl_shortcut_mods = to_sdl_mod(params->shortcut_mods); im->vfinger_down = false; im->vfinger_invert_x = false; diff --git a/app/src/input_manager.h b/app/src/input_manager.h index 2ce11b03..8c45c165 100644 --- a/app/src/input_manager.h +++ b/app/src/input_manager.h @@ -26,10 +26,7 @@ struct sc_input_manager { bool legacy_paste; bool clipboard_autosync; - struct { - unsigned data[SC_MAX_SHORTCUT_MODS]; - unsigned count; - } sdl_shortcut_mods; + uint16_t sdl_shortcut_mods; bool vfinger_down; bool vfinger_invert_x; @@ -55,7 +52,7 @@ struct sc_input_manager_params { bool forward_all_clicks; bool legacy_paste; bool clipboard_autosync; - const struct sc_shortcut_mods *shortcut_mods; + uint8_t shortcut_mods; // OR of enum sc_shortcut_mod values }; void diff --git a/app/src/options.c b/app/src/options.c index d6bf9158..4b75ed6a 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -30,10 +30,7 @@ const struct scrcpy_options scrcpy_options_default = { }, .tunnel_host = 0, .tunnel_port = 0, - .shortcut_mods = { - .data = {SC_SHORTCUT_MOD_LALT, SC_SHORTCUT_MOD_LSUPER}, - .count = 2, - }, + .shortcut_mods = SC_SHORTCUT_MOD_LALT | SC_SHORTCUT_MOD_LSUPER, .max_size = 0, .video_bit_rate = 0, .audio_bit_rate = 0, diff --git a/app/src/options.h b/app/src/options.h index 1fb61ddf..85817341 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -169,8 +169,6 @@ enum sc_key_inject_mode { SC_KEY_INJECT_MODE_RAW, }; -#define SC_MAX_SHORTCUT_MODS 8 - enum sc_shortcut_mod { SC_SHORTCUT_MOD_LCTRL = 1 << 0, SC_SHORTCUT_MOD_RCTRL = 1 << 1, @@ -180,11 +178,6 @@ enum sc_shortcut_mod { SC_SHORTCUT_MOD_RSUPER = 1 << 5, }; -struct sc_shortcut_mods { - unsigned data[SC_MAX_SHORTCUT_MODS]; - unsigned count; -}; - struct sc_port_range { uint16_t first; uint16_t last; @@ -219,7 +212,7 @@ struct scrcpy_options { struct sc_port_range port_range; uint32_t tunnel_host; uint16_t tunnel_port; - struct sc_shortcut_mods shortcut_mods; + uint8_t shortcut_mods; // OR of enum sc_shortcut_mod values uint16_t max_size; uint32_t video_bit_rate; uint32_t audio_bit_rate; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index b07611f1..5f13ee53 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -715,7 +715,7 @@ scrcpy(struct scrcpy_options *options) { .forward_all_clicks = options->forward_all_clicks, .legacy_paste = options->legacy_paste, .clipboard_autosync = options->clipboard_autosync, - .shortcut_mods = &options->shortcut_mods, + .shortcut_mods = options->shortcut_mods, .window_title = window_title, .always_on_top = options->always_on_top, .window_x = options->window_x, diff --git a/app/src/screen.h b/app/src/screen.h index 3e205cdc..437e7633 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -82,7 +82,7 @@ struct sc_screen_params { bool forward_all_clicks; bool legacy_paste; bool clipboard_autosync; - const struct sc_shortcut_mods *shortcut_mods; + uint8_t shortcut_mods; // OR of enum sc_shortcut_mod values const char *window_title; bool always_on_top; diff --git a/app/tests/test_cli.c b/app/tests/test_cli.c index f2a17272..cef8df3e 100644 --- a/app/tests/test_cli.c +++ b/app/tests/test_cli.c @@ -124,32 +124,22 @@ static void test_options2(void) { } static void test_parse_shortcut_mods(void) { - struct sc_shortcut_mods mods; + uint8_t mods; bool ok; ok = sc_parse_shortcut_mods("lctrl", &mods); assert(ok); - assert(mods.count == 1); - assert(mods.data[0] == SC_SHORTCUT_MOD_LCTRL); - - ok = sc_parse_shortcut_mods("lctrl+lalt", &mods); - assert(ok); - assert(mods.count == 1); - assert(mods.data[0] == (SC_SHORTCUT_MOD_LCTRL | SC_SHORTCUT_MOD_LALT)); + assert(mods == SC_SHORTCUT_MOD_LCTRL); ok = sc_parse_shortcut_mods("rctrl,lalt", &mods); assert(ok); - assert(mods.count == 2); - assert(mods.data[0] == SC_SHORTCUT_MOD_RCTRL); - assert(mods.data[1] == SC_SHORTCUT_MOD_LALT); + assert(mods == (SC_SHORTCUT_MOD_RCTRL | SC_SHORTCUT_MOD_LALT)); - ok = sc_parse_shortcut_mods("lsuper,rsuper+lalt,lctrl+rctrl+ralt", &mods); + ok = sc_parse_shortcut_mods("lsuper,rsuper,lctrl", &mods); assert(ok); - assert(mods.count == 3); - assert(mods.data[0] == SC_SHORTCUT_MOD_LSUPER); - assert(mods.data[1] == (SC_SHORTCUT_MOD_RSUPER | SC_SHORTCUT_MOD_LALT)); - assert(mods.data[2] == (SC_SHORTCUT_MOD_LCTRL | SC_SHORTCUT_MOD_RCTRL | - SC_SHORTCUT_MOD_RALT)); + assert(mods == (SC_SHORTCUT_MOD_LSUPER + | SC_SHORTCUT_MOD_RSUPER + | SC_SHORTCUT_MOD_LCTRL)); ok = sc_parse_shortcut_mods("", &mods); assert(!ok); diff --git a/doc/shortcuts.md b/doc/shortcuts.md index d0f6ebec..841ceaa6 100644 --- a/doc/shortcuts.md +++ b/doc/shortcuts.md @@ -13,8 +13,8 @@ It can be changed using `--shortcut-mod`. Possible keys are `lctrl`, `rctrl`, # use RCtrl for shortcuts scrcpy --shortcut-mod=rctrl -# use either LCtrl+LAlt or LSuper for shortcuts -scrcpy --shortcut-mod=lctrl+lalt,lsuper +# use either LCtrl or LSuper for shortcuts +scrcpy --shortcut-mod=lctrl,lsuper ``` _[Super] is typically the Windows or Cmd key._ From 0b926922bc169eab704f9805e6d35f79f7a1aa95 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 7 Mar 2024 23:08:27 +0100 Subject: [PATCH 1838/2244] Ignore shortcut keycodes Never inject keycodes used as shortcut modifiers. Refs #4732 PR #4741 --- app/src/input_manager.c | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 91e65bfd..1e46b30e 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -42,6 +42,16 @@ is_shortcut_mod(struct sc_input_manager *im, uint16_t sdl_mod) { return sdl_mod & im->sdl_shortcut_mods; } +static bool +is_shortcut_key(struct sc_input_manager *im, SDL_Keycode keycode) { + return (im->sdl_shortcut_mods & KMOD_LCTRL && keycode == SDLK_LCTRL) + || (im->sdl_shortcut_mods & KMOD_RCTRL && keycode == SDLK_RCTRL) + || (im->sdl_shortcut_mods & KMOD_LALT && keycode == SDLK_LALT) + || (im->sdl_shortcut_mods & KMOD_RALT && keycode == SDLK_RALT) + || (im->sdl_shortcut_mods & KMOD_LGUI && keycode == SDLK_LGUI) + || (im->sdl_shortcut_mods & KMOD_RGUI && keycode == SDLK_RGUI); +} + void sc_input_manager_init(struct sc_input_manager *im, const struct sc_input_manager_params *params) { @@ -397,7 +407,12 @@ sc_input_manager_process_key(struct sc_input_manager *im, bool shift = event->keysym.mod & KMOD_SHIFT; bool repeat = event->repeat; - bool smod = is_shortcut_mod(im, mod); + // Either the modifier includes a shortcut modifier, or the key + // press/release is a modifier key. + // The second condition is necessary to ignore the release of the modifier + // key (because in this case mod is 0). + bool is_shortcut = is_shortcut_mod(im, mod) + || is_shortcut_key(im, keycode); if (down && !repeat) { if (keycode == im->last_keycode && mod == im->last_mod) { @@ -409,8 +424,7 @@ sc_input_manager_process_key(struct sc_input_manager *im, } } - // The shortcut modifier is pressed - if (smod) { + if (is_shortcut) { enum sc_action action = down ? SC_ACTION_DOWN : SC_ACTION_UP; switch (keycode) { case SDLK_h: From 9fa30ab1aeeb5cff32d2f6ad603a3a426bc04d69 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 24 Jun 2024 22:55:24 +0200 Subject: [PATCH 1839/2244] Fix error message parameter Use the local argument value, not the global optarg variable (even if it has the same value in practice, as it's passed as argument). --- 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 a0c0b338..a2eb4254 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -2038,7 +2038,7 @@ parse_pause_on_exit(const char *s, enum sc_pause_on_exit *pause_on_exit) { } LOGE("Unsupported pause on exit mode: %s " - "(expected true, false or if-error)", optarg); + "(expected true, false or if-error)", s); return false; } From 09ce0307feb239487982bfd62f57bd762f2a9165 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 24 Jun 2024 22:56:49 +0200 Subject: [PATCH 1840/2244] Fix zsh completion script An '=' was missing for some options with an argument. --- app/data/zsh-completion/_scrcpy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index a23240ec..db04ca10 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -35,7 +35,7 @@ arguments=( '--forward-all-clicks[Forward clicks to device]' {-h,--help}'[Print the help]' '-K[Use UHID keyboard (same as --keyboard=uhid)]' - '--keyboard[Set the keyboard input mode]:mode:(disabled sdk uhid aoa)' + '--keyboard=[Set the keyboard input mode]:mode:(disabled sdk uhid aoa)' '--kill-adb-on-close[Kill adb when scrcpy terminates]' '--legacy-paste[Inject computer clipboard text as a sequence of key events on Ctrl+v]' '--list-camera-sizes[List the valid camera capture sizes]' @@ -46,7 +46,7 @@ arguments=( {-m,--max-size=}'[Limit both the width and height of the video to value]' '-M[Use UHID mouse (same as --mouse=uhid)]' '--max-fps=[Limit the frame rate of screen capture]' - '--mouse[Set the mouse input mode]:mode:(disabled sdk uhid aoa)' + '--mouse=[Set the mouse input mode]:mode:(disabled sdk uhid aoa)' {-n,--no-control}'[Disable device control \(mirror the device in read only\)]' {-N,--no-playback}'[Disable video and audio playback]' '--no-audio[Disable audio forwarding]' From 40493dff608cda7f0f37c166a2d8fef92581acf0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 24 Jun 2024 23:00:33 +0200 Subject: [PATCH 1841/2244] Fix "resize to fit" when all clicks are forwarded To resize the window to fit the device screen, it is possible to double-click in the "black bars". This feature was mistakenly disabled when --forward-all-clicks was set. Instead, disable it only if mouse relative mode is enabled (AOA or UHID), because in that case the mouse cursor is on the device. --- app/src/input_manager.c | 75 ++++++++++++++++++++--------------------- 1 file changed, 37 insertions(+), 38 deletions(-) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 1e46b30e..f5f7992a 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -720,49 +720,48 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im, bool control = im->controller; bool paused = im->screen->paused; bool down = event->type == SDL_MOUSEBUTTONDOWN; - if (!im->forward_all_clicks) { - if (control && !paused) { - enum sc_action action = down ? SC_ACTION_DOWN : SC_ACTION_UP; + if (control && !paused && !im->forward_all_clicks) { + enum sc_action action = down ? SC_ACTION_DOWN : SC_ACTION_UP; - if (im->kp && event->button == SDL_BUTTON_X1) { - action_app_switch(im, action); - return; - } - if (event->button == SDL_BUTTON_X2 && down) { - if (event->clicks < 2) { - expand_notification_panel(im); - } else { - expand_settings_panel(im); - } - return; - } - if (im->kp && event->button == SDL_BUTTON_RIGHT) { - press_back_or_turn_screen_on(im, action); - return; - } - if (im->kp && event->button == SDL_BUTTON_MIDDLE) { - action_home(im, action); - return; - } + if (im->kp && event->button == SDL_BUTTON_X1) { + action_app_switch(im, action); + return; } + if (event->button == SDL_BUTTON_X2 && down) { + if (event->clicks < 2) { + expand_notification_panel(im); + } else { + expand_settings_panel(im); + } + return; + } + if (im->kp && event->button == SDL_BUTTON_RIGHT) { + press_back_or_turn_screen_on(im, action); + return; + } + if (im->kp && event->button == SDL_BUTTON_MIDDLE) { + action_home(im, action); + return; + } + } - // double-click on black borders resize to fit the device screen - bool video = im->screen->video; - if (video && event->button == SDL_BUTTON_LEFT && event->clicks == 2) { - int32_t x = event->x; - int32_t y = event->y; - sc_screen_hidpi_scale_coords(im->screen, &x, &y); - SDL_Rect *r = &im->screen->rect; - bool outside = x < r->x || x >= r->x + r->w - || y < r->y || y >= r->y + r->h; - if (outside) { - if (down) { - sc_screen_resize_to_fit(im->screen); - } - return; + // double-click on black borders resizes to fit the device screen + bool video = im->screen->video; + bool mouse_relative_mode = im->mp && im->mp->relative_mode; + if (video && !mouse_relative_mode && event->button == SDL_BUTTON_LEFT + && event->clicks == 2) { + int32_t x = event->x; + int32_t y = event->y; + sc_screen_hidpi_scale_coords(im->screen, &x, &y); + SDL_Rect *r = &im->screen->rect; + bool outside = x < r->x || x >= r->x + r->w + || y < r->y || y >= r->y + r->h; + if (outside) { + if (down) { + sc_screen_resize_to_fit(im->screen); } + return; } - // otherwise, send the click event to the device } if (!im->mp || paused) { From 035d60cf5d3f4c83d48735b4cb4cd108a5b5f413 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 24 Jun 2024 23:07:08 +0200 Subject: [PATCH 1842/2244] Add option to configure mouse bindings Add a new option --mouse-bind=xxxx. The argument must be exactly 4 characters, one for each secondary click: --mouse-bind=xxxx ^^^^ |||| ||| `- 5th click || `-- 4th click | `--- middle click `---- right click Each character must be one of the following: - `+`: forward the click to the device - `-`: ignore the click - `b`: trigger shortcut BACK (or turn screen on if off) - `h`: trigger shortcut HOME - `s`: trigger shortcut APP_SWITCH - `n`: trigger shortcut "expand notification panel" This deprecates --forward-all-clicks (use --mouse-bind=++++ instead). Refs PR #5022 --- app/data/bash-completion/scrcpy | 2 +- app/data/zsh-completion/_scrcpy | 2 +- app/scrcpy.1 | 21 ++++-- app/src/cli.c | 88 ++++++++++++++++++++++++-- app/src/input_events.h | 19 ++++-- app/src/input_manager.c | 109 ++++++++++++++++++++++---------- app/src/input_manager.h | 5 +- app/src/options.c | 7 +- app/src/options.h | 18 +++++- app/src/scrcpy.c | 2 +- app/src/screen.c | 2 +- app/src/screen.h | 2 +- app/src/usb/screen_otg.c | 6 +- doc/control.md | 9 --- doc/mouse.md | 38 +++++++++++ 15 files changed, 261 insertions(+), 69 deletions(-) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index e6b2c91a..f00fadae 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -25,7 +25,6 @@ _scrcpy() { -e --select-tcpip -f --fullscreen --force-adb-forward - --forward-all-clicks -h --help -K --keyboard= @@ -41,6 +40,7 @@ _scrcpy() { -M --max-fps= --mouse= + --mouse-bind= -n --no-control -N --no-playback --no-audio diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index db04ca10..86fe0b0e 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -32,7 +32,6 @@ arguments=( {-e,--select-tcpip}'[Use TCP/IP device]' {-f,--fullscreen}'[Start in fullscreen]' '--force-adb-forward[Do not attempt to use \"adb reverse\" to connect to the device]' - '--forward-all-clicks[Forward clicks to device]' {-h,--help}'[Print the help]' '-K[Use UHID keyboard (same as --keyboard=uhid)]' '--keyboard=[Set the keyboard input mode]:mode:(disabled sdk uhid aoa)' @@ -47,6 +46,7 @@ arguments=( '-M[Use UHID mouse (same as --mouse=uhid)]' '--max-fps=[Limit the frame rate of screen capture]' '--mouse=[Set the mouse input mode]:mode:(disabled sdk uhid aoa)' + '--mouse-bind=[Configure bindings of secondary clicks]' {-n,--no-control}'[Disable device control \(mirror the device in read only\)]' {-N,--no-playback}'[Disable video and audio playback]' '--no-audio[Disable audio forwarding]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 2be9ef59..7a0b3dfb 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -163,10 +163,6 @@ Start in fullscreen. .B \-\-force\-adb\-forward Do not attempt to use "adb reverse" to connect to the device. -.TP -.B \-\-forward\-all\-clicks -By default, right-click triggers BACK (or POWER on) and middle-click triggers HOME. This option disables these shortcuts and forward the clicks to the device instead. - .TP .B \-h, \-\-help Print this help. @@ -261,6 +257,23 @@ LAlt, LSuper or RSuper toggle the capture mode, to give control of the mouse bac Also see \fB\-\-keyboard\fR. +.TP +.BI "\-\-mouse\-bind " xxxx +Configure bindings of secondary clicks. + +The argument must be exactly 4 characters, one for each secondary click (in order: right click, middle click, 4th click, 5th click). + +Each character must be one of the following: + + - '+': forward the click to the device + - '-': ignore the click + - 'b': trigger shortcut BACK (or turn screen on if off) + - 'h': trigger shortcut HOME + - 's': trigger shortcut APP_SWITCH + - 'n': trigger shortcut "expand notification panel" + +Default is 'bhsn'. + .TP .B \-n, \-\-no\-control diff --git a/app/src/cli.c b/app/src/cli.c index a2eb4254..35230a9a 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -98,6 +98,7 @@ enum { OPT_HID_KEYBOARD_DEPRECATED, OPT_HID_MOUSE_DEPRECATED, OPT_NO_WINDOW, + OPT_MOUSE_BIND, }; struct sc_option { @@ -352,11 +353,9 @@ static const struct sc_option options[] = { "device.", }, { + // deprecated .longopt_id = OPT_FORWARD_ALL_CLICKS, .longopt = "forward-all-clicks", - .text = "By default, right-click triggers BACK (or POWER on) and " - "middle-click triggers HOME. This option disables these " - "shortcuts and forwards the clicks to the device instead.", }, { .shortopt = 'h', @@ -490,6 +489,23 @@ static const struct sc_option options[] = { "control of the mouse back to the computer.\n" "Also see --keyboard.", }, + { + .longopt_id = OPT_MOUSE_BIND, + .longopt = "mouse-bind", + .argdesc = "xxxx", + .text = "Configure bindings of secondary clicks.\n" + "The argument must be exactly 4 characters, one for each " + "secondary click (in order: right click, middle click, 4th " + "click, 5th click).\n" + "Each character must be one of the following:\n" + " '+': forward the click to the device\n" + " '-': ignore the click\n" + " 'b': trigger shortcut BACK (or turn screen on if off)\n" + " 'h': trigger shortcut HOME\n" + " 's': trigger shortcut APP_SWITCH\n" + " 'n': trigger shortcut \"expand notification panel\"\n" + "Default is 'bhsn'.", + }, { .shortopt = 'n', .longopt = "no-control", @@ -2043,6 +2059,58 @@ parse_pause_on_exit(const char *s, enum sc_pause_on_exit *pause_on_exit) { } +static bool +parse_mouse_binding(char c, enum sc_mouse_binding *b) { + switch (c) { + case '+': + *b = SC_MOUSE_BINDING_CLICK; + return true; + case '-': + *b = SC_MOUSE_BINDING_DISABLED; + return true; + case 'b': + *b = SC_MOUSE_BINDING_BACK; + return true; + case 'h': + *b = SC_MOUSE_BINDING_HOME; + return true; + case 's': + *b = SC_MOUSE_BINDING_APP_SWITCH; + return true; + case 'n': + *b = SC_MOUSE_BINDING_EXPAND_NOTIFICATION_PANEL; + return true; + default: + LOGE("Invalid mouse binding: '%c' " + "(expected '+', '-', 'b', 'h', 's' or 'n')", c); + return false; + } +} + +static bool +parse_mouse_bindings(const char *s, struct sc_mouse_bindings *mb) { + if (strlen(s) != 4) { + LOGE("Invalid mouse bindings: '%s' (expected exactly 4 characters from " + "{'+', '-', 'b', 'h', 's', 'n'})", s); + return false; + } + + if (!parse_mouse_binding(s[0], &mb->right_click)) { + return false; + } + if (!parse_mouse_binding(s[1], &mb->middle_click)) { + return false; + } + if (!parse_mouse_binding(s[2], &mb->click4)) { + return false; + } + if (!parse_mouse_binding(s[3], &mb->click5)) { + return false; + } + + return true; +} + static bool parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], const char *optstring, const struct option *longopts) { @@ -2125,6 +2193,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; } break; + case OPT_MOUSE_BIND: + if (!parse_mouse_bindings(optarg, &opts->mouse_bindings)) { + return false; + } + break; case OPT_HID_MOUSE_DEPRECATED: LOGE("--hid-mouse has been removed, use --mouse=aoa or " "--mouse=uhid instead."); @@ -2322,7 +2395,14 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } break; case OPT_FORWARD_ALL_CLICKS: - opts->forward_all_clicks = true; + LOGW("--forward-all-clicks is deprecated, " + "use --mouse-bind=++++ instead."); + opts->mouse_bindings = (struct sc_mouse_bindings) { + .right_click = SC_MOUSE_BINDING_CLICK, + .middle_click = SC_MOUSE_BINDING_CLICK, + .click4 = SC_MOUSE_BINDING_CLICK, + .click5 = SC_MOUSE_BINDING_CLICK, + }; break; case OPT_LEGACY_PASTE: opts->legacy_paste = true; diff --git a/app/src/input_events.h b/app/src/input_events.h index 5831ba0f..ed77bcb4 100644 --- a/app/src/input_events.h +++ b/app/src/input_events.h @@ -9,6 +9,7 @@ #include #include "coords.h" +#include "options.h" /* The representation of input events in scrcpy is very close to the SDL API, * for simplicity. @@ -437,15 +438,21 @@ sc_mouse_button_from_sdl(uint8_t button) { static inline uint8_t sc_mouse_buttons_state_from_sdl(uint32_t buttons_state, - bool forward_all_clicks) { + const struct sc_mouse_bindings *mb) { assert(buttons_state < 0x100); // fits in uint8_t uint8_t mask = SC_MOUSE_BUTTON_LEFT; - if (forward_all_clicks) { - mask |= SC_MOUSE_BUTTON_RIGHT - | SC_MOUSE_BUTTON_MIDDLE - | SC_MOUSE_BUTTON_X1 - | SC_MOUSE_BUTTON_X2; + if (!mb || mb->right_click == SC_MOUSE_BINDING_CLICK) { + mask |= SC_MOUSE_BUTTON_RIGHT; + } + if (!mb || mb->middle_click == SC_MOUSE_BINDING_CLICK) { + mask |= SC_MOUSE_BUTTON_MIDDLE; + } + if (!mb || mb->click4 == SC_MOUSE_BINDING_CLICK) { + mask |= SC_MOUSE_BUTTON_X1; + } + if (!mb || mb->click5 == SC_MOUSE_BINDING_CLICK) { + mask |= SC_MOUSE_BUTTON_X2; } return buttons_state & mask; diff --git a/app/src/input_manager.c b/app/src/input_manager.c index f5f7992a..3166fbff 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -52,6 +52,14 @@ is_shortcut_key(struct sc_input_manager *im, SDL_Keycode keycode) { || (im->sdl_shortcut_mods & KMOD_RGUI && keycode == SDLK_RGUI); } +static inline bool +mouse_bindings_has_secondary_click(const struct sc_mouse_bindings *mb) { + return mb->right_click == SC_MOUSE_BINDING_CLICK + || mb->middle_click == SC_MOUSE_BINDING_CLICK + || mb->click4 == SC_MOUSE_BINDING_CLICK + || mb->click5 == SC_MOUSE_BINDING_CLICK; +} + void sc_input_manager_init(struct sc_input_manager *im, const struct sc_input_manager_params *params) { @@ -67,7 +75,9 @@ sc_input_manager_init(struct sc_input_manager *im, im->kp = params->kp; im->mp = params->mp; - im->forward_all_clicks = params->forward_all_clicks; + im->mouse_bindings = params->mouse_bindings; + im->has_secondary_click = + mouse_bindings_has_secondary_click(&im->mouse_bindings); im->legacy_paste = params->legacy_paste; im->clipboard_autosync = params->clipboard_autosync; @@ -366,8 +376,8 @@ simulate_virtual_finger(struct sc_input_manager *im, msg.inject_touch_event.position.screen_size = im->screen->frame_size; msg.inject_touch_event.position.point = point; msg.inject_touch_event.pointer_id = - im->forward_all_clicks ? POINTER_ID_VIRTUAL_MOUSE - : POINTER_ID_VIRTUAL_FINGER; + im->has_secondary_click ? POINTER_ID_VIRTUAL_MOUSE + : POINTER_ID_VIRTUAL_FINGER; msg.inject_touch_event.pressure = up ? 0.0f : 1.0f; msg.inject_touch_event.action_button = 0; msg.inject_touch_event.buttons = 0; @@ -652,13 +662,12 @@ sc_input_manager_process_mouse_motion(struct sc_input_manager *im, struct sc_mouse_motion_event evt = { .position = sc_input_manager_get_position(im, event->x, event->y), - .pointer_id = im->forward_all_clicks ? POINTER_ID_MOUSE - : POINTER_ID_GENERIC_FINGER, + .pointer_id = im->has_secondary_click ? POINTER_ID_MOUSE + : POINTER_ID_GENERIC_FINGER, .xrel = event->xrel, .yrel = event->yrel, .buttons_state = - sc_mouse_buttons_state_from_sdl(event->state, - im->forward_all_clicks), + sc_mouse_buttons_state_from_sdl(event->state, &im->mouse_bindings), }; assert(im->mp->ops->process_mouse_motion); @@ -709,6 +718,25 @@ sc_input_manager_process_touch(struct sc_input_manager *im, im->mp->ops->process_touch(im->mp, &evt); } +static enum sc_mouse_binding +sc_input_manager_get_binding(const struct sc_mouse_bindings *bindings, + uint8_t sdl_button) { + switch (sdl_button) { + case SDL_BUTTON_LEFT: + return SC_MOUSE_BINDING_CLICK; + case SDL_BUTTON_RIGHT: + return bindings->right_click; + case SDL_BUTTON_MIDDLE: + return bindings->middle_click; + case SDL_BUTTON_X1: + return bindings->click4; + case SDL_BUTTON_X2: + return bindings->click5; + default: + return SC_MOUSE_BINDING_DISABLED; + } +} + static void sc_input_manager_process_mouse_button(struct sc_input_manager *im, const SDL_MouseButtonEvent *event) { @@ -720,28 +748,42 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im, bool control = im->controller; bool paused = im->screen->paused; bool down = event->type == SDL_MOUSEBUTTONDOWN; - if (control && !paused && !im->forward_all_clicks) { + if (control && !paused) { enum sc_action action = down ? SC_ACTION_DOWN : SC_ACTION_UP; - if (im->kp && event->button == SDL_BUTTON_X1) { - action_app_switch(im, action); - return; - } - if (event->button == SDL_BUTTON_X2 && down) { - if (event->clicks < 2) { - expand_notification_panel(im); - } else { - expand_settings_panel(im); - } - return; - } - if (im->kp && event->button == SDL_BUTTON_RIGHT) { - press_back_or_turn_screen_on(im, action); - return; - } - if (im->kp && event->button == SDL_BUTTON_MIDDLE) { - action_home(im, action); - return; + enum sc_mouse_binding binding = + sc_input_manager_get_binding(&im->mouse_bindings, event->button); + switch (binding) { + case SC_MOUSE_BINDING_DISABLED: + // ignore click + return; + case SC_MOUSE_BINDING_BACK: + if (im->kp) { + press_back_or_turn_screen_on(im, action); + } + return; + case SC_MOUSE_BINDING_HOME: + if (im->kp) { + action_home(im, action); + } + return; + case SC_MOUSE_BINDING_APP_SWITCH: + if (im->kp) { + action_app_switch(im, action); + } + return; + case SC_MOUSE_BINDING_EXPAND_NOTIFICATION_PANEL: + if (down) { + if (event->clicks < 2) { + expand_notification_panel(im); + } else { + expand_settings_panel(im); + } + } + return; + default: + assert(binding == SC_MOUSE_BINDING_CLICK); + break; } } @@ -774,11 +816,10 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im, .position = sc_input_manager_get_position(im, event->x, event->y), .action = sc_action_from_sdl_mousebutton_type(event->type), .button = sc_mouse_button_from_sdl(event->button), - .pointer_id = im->forward_all_clicks ? POINTER_ID_MOUSE - : POINTER_ID_GENERIC_FINGER, - .buttons_state = - sc_mouse_buttons_state_from_sdl(sdl_buttons_state, - im->forward_all_clicks), + .pointer_id = im->has_secondary_click ? POINTER_ID_MOUSE + : POINTER_ID_GENERIC_FINGER, + .buttons_state = sc_mouse_buttons_state_from_sdl(sdl_buttons_state, + &im->mouse_bindings), }; assert(im->mp->ops->process_mouse_click); @@ -854,8 +895,8 @@ sc_input_manager_process_mouse_wheel(struct sc_input_manager *im, .hscroll = CLAMP(event->x, -1, 1), .vscroll = CLAMP(event->y, -1, 1), #endif - .buttons_state = - sc_mouse_buttons_state_from_sdl(buttons, im->forward_all_clicks), + .buttons_state = sc_mouse_buttons_state_from_sdl(buttons, + &im->mouse_bindings), }; im->mp->ops->process_mouse_scroll(im->mp, &evt); diff --git a/app/src/input_manager.h b/app/src/input_manager.h index 8c45c165..03c42fe6 100644 --- a/app/src/input_manager.h +++ b/app/src/input_manager.h @@ -22,7 +22,8 @@ struct sc_input_manager { struct sc_key_processor *kp; struct sc_mouse_processor *mp; - bool forward_all_clicks; + struct sc_mouse_bindings mouse_bindings; + bool has_secondary_click; bool legacy_paste; bool clipboard_autosync; @@ -49,7 +50,7 @@ struct sc_input_manager_params { struct sc_key_processor *kp; struct sc_mouse_processor *mp; - bool forward_all_clicks; + struct sc_mouse_bindings mouse_bindings; bool legacy_paste; bool clipboard_autosync; uint8_t shortcut_mods; // OR of enum sc_shortcut_mod values diff --git a/app/src/options.c b/app/src/options.c index 4b75ed6a..939108cf 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -23,6 +23,12 @@ const struct scrcpy_options scrcpy_options_default = { .record_format = SC_RECORD_FORMAT_AUTO, .keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_AUTO, .mouse_input_mode = SC_MOUSE_INPUT_MODE_AUTO, + .mouse_bindings = { + .right_click = SC_MOUSE_BINDING_BACK, + .middle_click = SC_MOUSE_BINDING_HOME, + .click4 = SC_MOUSE_BINDING_APP_SWITCH, + .click5 = SC_MOUSE_BINDING_EXPAND_NOTIFICATION_PANEL, + }, .camera_facing = SC_CAMERA_FACING_ANY, .port_range = { .first = DEFAULT_LOCAL_PORT_RANGE_FIRST, @@ -68,7 +74,6 @@ const struct scrcpy_options scrcpy_options_default = { .force_adb_forward = false, .disable_screensaver = false, .forward_key_repeat = true, - .forward_all_clicks = false, .legacy_paste = false, .power_off_on_close = false, .clipboard_autosync = true, diff --git a/app/src/options.h b/app/src/options.h index 85817341..17615c75 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -155,6 +155,22 @@ enum sc_mouse_input_mode { SC_MOUSE_INPUT_MODE_AOA, }; +enum sc_mouse_binding { + SC_MOUSE_BINDING_DISABLED, + SC_MOUSE_BINDING_CLICK, + SC_MOUSE_BINDING_BACK, + SC_MOUSE_BINDING_HOME, + SC_MOUSE_BINDING_APP_SWITCH, + SC_MOUSE_BINDING_EXPAND_NOTIFICATION_PANEL, +}; + +struct sc_mouse_bindings { + enum sc_mouse_binding right_click; + enum sc_mouse_binding middle_click; + enum sc_mouse_binding click4; + enum sc_mouse_binding click5; +}; + enum sc_key_inject_mode { // Inject special keys, letters and space as key events. // Inject numbers and punctuation as text events. @@ -208,6 +224,7 @@ struct scrcpy_options { enum sc_record_format record_format; enum sc_keyboard_input_mode keyboard_input_mode; enum sc_mouse_input_mode mouse_input_mode; + struct sc_mouse_bindings mouse_bindings; enum sc_camera_facing camera_facing; struct sc_port_range port_range; uint32_t tunnel_host; @@ -250,7 +267,6 @@ struct scrcpy_options { bool force_adb_forward; bool disable_screensaver; bool forward_key_repeat; - bool forward_all_clicks; bool legacy_paste; bool power_off_on_close; bool clipboard_autosync; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 5f13ee53..85b89935 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -712,7 +712,7 @@ scrcpy(struct scrcpy_options *options) { .fp = fp, .kp = kp, .mp = mp, - .forward_all_clicks = options->forward_all_clicks, + .mouse_bindings = options->mouse_bindings, .legacy_paste = options->legacy_paste, .clipboard_autosync = options->clipboard_autosync, .shortcut_mods = options->shortcut_mods, diff --git a/app/src/screen.c b/app/src/screen.c index 56f13f99..55a06ab3 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -481,7 +481,7 @@ sc_screen_init(struct sc_screen *screen, .screen = screen, .kp = params->kp, .mp = params->mp, - .forward_all_clicks = params->forward_all_clicks, + .mouse_bindings = params->mouse_bindings, .legacy_paste = params->legacy_paste, .clipboard_autosync = params->clipboard_autosync, .shortcut_mods = params->shortcut_mods, diff --git a/app/src/screen.h b/app/src/screen.h index 437e7633..079d4fbb 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -79,7 +79,7 @@ struct sc_screen_params { struct sc_key_processor *kp; struct sc_mouse_processor *mp; - bool forward_all_clicks; + struct sc_mouse_bindings mouse_bindings; bool legacy_paste; bool clipboard_autosync; uint8_t shortcut_mods; // OR of enum sc_shortcut_mod values diff --git a/app/src/usb/screen_otg.c b/app/src/usb/screen_otg.c index e1d5cb01..33500e0c 100644 --- a/app/src/usb/screen_otg.c +++ b/app/src/usb/screen_otg.c @@ -169,7 +169,7 @@ sc_screen_otg_process_mouse_motion(struct sc_screen_otg *screen, // .position not used for HID events .xrel = event->xrel, .yrel = event->yrel, - .buttons_state = sc_mouse_buttons_state_from_sdl(event->state, true), + .buttons_state = sc_mouse_buttons_state_from_sdl(event->state, NULL), }; assert(mp->ops->process_mouse_motion); @@ -189,7 +189,7 @@ sc_screen_otg_process_mouse_button(struct sc_screen_otg *screen, .action = sc_action_from_sdl_mousebutton_type(event->type), .button = sc_mouse_button_from_sdl(event->button), .buttons_state = - sc_mouse_buttons_state_from_sdl(sdl_buttons_state, true), + sc_mouse_buttons_state_from_sdl(sdl_buttons_state, NULL), }; assert(mp->ops->process_mouse_click); @@ -209,7 +209,7 @@ sc_screen_otg_process_mouse_wheel(struct sc_screen_otg *screen, .hscroll = event->x, .vscroll = event->y, .buttons_state = - sc_mouse_buttons_state_from_sdl(sdl_buttons_state, true), + sc_mouse_buttons_state_from_sdl(sdl_buttons_state, NULL), }; assert(mp->ops->process_mouse_scroll); diff --git a/doc/control.md b/doc/control.md index 87897894..34eb7a6a 100644 --- a/doc/control.md +++ b/doc/control.md @@ -106,15 +106,6 @@ only inverts _x_. This only works for the default mouse mode (`--mouse=sdk`). -## Right-click and middle-click - -By default, right-click triggers BACK (or POWER on) and middle-click triggers -HOME. To disable these shortcuts and forward the clicks to the device instead: - -```bash -scrcpy --forward-all-clicks -``` - ## File drop ### Install APK diff --git a/doc/mouse.md b/doc/mouse.md index d0342954..146956f5 100644 --- a/doc/mouse.md +++ b/doc/mouse.md @@ -68,3 +68,41 @@ debugging disabled (see [OTG](otg.md)). Note: On Windows, it may only work in [OTG mode](otg.md), not while mirroring (it is not possible to open a USB device if it is already open by another process like the _adb daemon_). + + +## Mouse bindings + +By default, right-click triggers BACK (or POWER on) and middle-click triggers +HOME. In addition, the 4th click triggers APP_SWITCH and the 5th click expands +the notification panel. + +The shortcuts can be configured using `--mouse-bind=xxxx`. The argument must be +exactly 4 characters, one for each secondary click: + +``` +--mouse-bind=xxxx + ^^^^ + |||| + ||| `- 5th click + || `-- 4th click + | `--- middle click + `---- right click +``` + +Each character must be one of the following: + + - `+`: forward the click to the device + - `-`: ignore the click + - `b`: trigger shortcut BACK (or turn screen on if off) + - `h`: trigger shortcut HOME + - `s`: trigger shortcut APP_SWITCH + - `n`: trigger shortcut "expand notification panel" + +For example: + +```bash +scrcpy --mouse-bind=bhsn # the default mode +scrcpy --mouse-bind=++++ # forward all clicks +scrcpy --mouse-bind=++bh # forward right and middle clicks, + # use 4th and 5th for BACK and HOME +``` From f5e6b8092afd82bab402e7c2c3d00b1719f9bb57 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 24 Jun 2024 23:11:42 +0200 Subject: [PATCH 1843/2244] Forward all clicks by default for UHID/AOA By default, only the left click is forwarded to the device, and secondary clicks trigger shortcuts (the behavior can be configured by --mouse-bind=xxxx). But when the mouse mode is relative (AOA and UHID modes), forward all clicks by default. This makes more sense since the cursor is handled on the device side, the user expects all mouse buttons to be forwarded. Refs PR #5022 --- app/scrcpy.1 | 2 +- app/src/cli.c | 26 +++++++++++++++++++++++++- app/src/input_manager.c | 1 + app/src/options.c | 8 ++++---- app/src/options.h | 1 + doc/mouse.md | 16 +++++++++------- 6 files changed, 41 insertions(+), 13 deletions(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 7a0b3dfb..b2021f26 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -272,7 +272,7 @@ Each character must be one of the following: - 's': trigger shortcut APP_SWITCH - 'n': trigger shortcut "expand notification panel" -Default is 'bhsn'. +Default is 'bhsn' for SDK mouse, and '++++' for AOA and UHID. .TP diff --git a/app/src/cli.c b/app/src/cli.c index 35230a9a..d3ba3463 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -504,7 +504,7 @@ static const struct sc_option options[] = { " 'h': trigger shortcut HOME\n" " 's': trigger shortcut APP_SWITCH\n" " 'n': trigger shortcut \"expand notification panel\"\n" - "Default is 'bhsn'.", + "Default is 'bhsn' for SDK mouse, and '++++' for AOA and UHID.", }, { .shortopt = 'n', @@ -2690,6 +2690,30 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } } + // If mouse bindings are not explictly set, configure default bindings + if (opts->mouse_bindings.right_click == SC_MOUSE_BINDING_AUTO) { + assert(opts->mouse_bindings.middle_click == SC_MOUSE_BINDING_AUTO); + assert(opts->mouse_bindings.click4 == SC_MOUSE_BINDING_AUTO); + assert(opts->mouse_bindings.click5 == SC_MOUSE_BINDING_AUTO); + + // By default, forward all clicks only for UHID and AOA + if (opts->mouse_input_mode == SC_MOUSE_INPUT_MODE_SDK) { + opts->mouse_bindings = (struct sc_mouse_bindings) { + .right_click = SC_MOUSE_BINDING_BACK, + .middle_click = SC_MOUSE_BINDING_HOME, + .click4 = SC_MOUSE_BINDING_APP_SWITCH, + .click5 = SC_MOUSE_BINDING_EXPAND_NOTIFICATION_PANEL, + }; + } else { + opts->mouse_bindings = (struct sc_mouse_bindings) { + .right_click = SC_MOUSE_BINDING_CLICK, + .middle_click = SC_MOUSE_BINDING_CLICK, + .click4 = SC_MOUSE_BINDING_CLICK, + .click5 = SC_MOUSE_BINDING_CLICK, + }; + } + } + if (otg) { if (!opts->control) { LOGE("--no-control is not allowed in OTG mode"); diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 3166fbff..43b10d2d 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -753,6 +753,7 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im, enum sc_mouse_binding binding = sc_input_manager_get_binding(&im->mouse_bindings, event->button); + assert(binding != SC_MOUSE_BINDING_AUTO); switch (binding) { case SC_MOUSE_BINDING_DISABLED: // ignore click diff --git a/app/src/options.c b/app/src/options.c index 939108cf..352d9895 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -24,10 +24,10 @@ const struct scrcpy_options scrcpy_options_default = { .keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_AUTO, .mouse_input_mode = SC_MOUSE_INPUT_MODE_AUTO, .mouse_bindings = { - .right_click = SC_MOUSE_BINDING_BACK, - .middle_click = SC_MOUSE_BINDING_HOME, - .click4 = SC_MOUSE_BINDING_APP_SWITCH, - .click5 = SC_MOUSE_BINDING_EXPAND_NOTIFICATION_PANEL, + .right_click = SC_MOUSE_BINDING_AUTO, + .middle_click = SC_MOUSE_BINDING_AUTO, + .click4 = SC_MOUSE_BINDING_AUTO, + .click5 = SC_MOUSE_BINDING_AUTO, }, .camera_facing = SC_CAMERA_FACING_ANY, .port_range = { diff --git a/app/src/options.h b/app/src/options.h index 17615c75..d5b090b7 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -156,6 +156,7 @@ enum sc_mouse_input_mode { }; enum sc_mouse_binding { + SC_MOUSE_BINDING_AUTO, SC_MOUSE_BINDING_DISABLED, SC_MOUSE_BINDING_CLICK, SC_MOUSE_BINDING_BACK, diff --git a/doc/mouse.md b/doc/mouse.md index 146956f5..42d70766 100644 --- a/doc/mouse.md +++ b/doc/mouse.md @@ -72,12 +72,14 @@ process like the _adb daemon_). ## Mouse bindings -By default, right-click triggers BACK (or POWER on) and middle-click triggers -HOME. In addition, the 4th click triggers APP_SWITCH and the 5th click expands -the notification panel. +By default, with SDK mouse, right-click triggers BACK (or POWER on) and +middle-click triggers HOME. In addition, the 4th click triggers APP_SWITCH and +the 5th click expands the notification panel. -The shortcuts can be configured using `--mouse-bind=xxxx`. The argument must be -exactly 4 characters, one for each secondary click: +In AOA and UHID mouse modes, all clicks are forwarded by default. + +The shortcuts can be configured using `--mouse-bind=xxxx` for any mouse mode. +The argument must be exactly 4 characters, one for each secondary click: ``` --mouse-bind=xxxx @@ -101,8 +103,8 @@ Each character must be one of the following: For example: ```bash -scrcpy --mouse-bind=bhsn # the default mode -scrcpy --mouse-bind=++++ # forward all clicks +scrcpy --mouse-bind=bhsn # the default mode with SDK mouse +scrcpy --mouse-bind=++++ # forward all clicks (default for AOA/UHID) scrcpy --mouse-bind=++bh # forward right and middle clicks, # use 4th and 5th for BACK and HOME ``` From 76332282783f72bc611dcb1b871f4baacd59de1d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 21 Jun 2024 16:54:00 +0200 Subject: [PATCH 1844/2244] Forward mouse hover events Also add an option --no-mouse-hover to get the old behavior. Fixes #2743 Fixes #3070 PR #5039 --- app/data/bash-completion/scrcpy | 1 + app/data/zsh-completion/_scrcpy | 1 + app/scrcpy.1 | 4 ++++ app/src/cli.c | 16 ++++++++++++++++ app/src/mouse_sdk.c | 13 ++++++++----- app/src/mouse_sdk.h | 4 +++- app/src/options.c | 1 + app/src/options.h | 1 + app/src/scrcpy.c | 3 ++- doc/mouse.md | 8 ++++++++ 10 files changed, 45 insertions(+), 7 deletions(-) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index f00fadae..b35ea5e4 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -50,6 +50,7 @@ _scrcpy() { --no-downsize-on-error --no-key-repeat --no-mipmaps + --no-mouse-hover --no-power-on --no-video --no-video-playback diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 86fe0b0e..5afca977 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -56,6 +56,7 @@ arguments=( '--no-downsize-on-error[Disable lowering definition on MediaCodec error]' '--no-key-repeat[Do not forward repeated key events when a key is held down]' '--no-mipmaps[Disable the generation of mipmaps]' + '--no-mouse-hover[Do not forward mouse hover events]' '--no-power-on[Do not power on the device on start]' '--no-video[Disable video forwarding]' '--no-video-playback[Disable video playback]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index b2021f26..cf8dfa7f 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -317,6 +317,10 @@ Do not forward repeated key events when a key is held down. .B \-\-no\-mipmaps If the renderer is OpenGL 3.0+ or OpenGL ES 2.0+, then mipmaps are automatically generated to improve downscaling quality. This option disables the generation of mipmaps. +.TP +.B \-\-no\-mouse\-hover +Do not forward mouse hover (mouse motion without any clicks) events. + .TP .B \-\-no\-power\-on Do not power on the device on start. diff --git a/app/src/cli.c b/app/src/cli.c index d3ba3463..08a4aa3f 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -99,6 +99,7 @@ enum { OPT_HID_MOUSE_DEPRECATED, OPT_NO_WINDOW, OPT_MOUSE_BIND, + OPT_NO_MOUSE_HOVER, }; struct sc_option { @@ -568,6 +569,12 @@ static const struct sc_option options[] = { "mipmaps are automatically generated to improve downscaling " "quality. This option disables the generation of mipmaps.", }, + { + .longopt_id = OPT_NO_MOUSE_HOVER, + .longopt = "no-mouse-hover", + .text = "Do not forward mouse hover (mouse motion without any clicks) " + "events.", + }, { .longopt_id = OPT_NO_POWER_ON, .longopt = "no-power-on", @@ -2198,6 +2205,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; } break; + case OPT_NO_MOUSE_HOVER: + opts->mouse_hover = false; + break; case OPT_HID_MOUSE_DEPRECATED: LOGE("--hid-mouse has been removed, use --mouse=aoa or " "--mouse=uhid instead."); @@ -2758,6 +2768,12 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } } + if (opts->mouse_input_mode != SC_MOUSE_INPUT_MODE_SDK + && !opts->mouse_hover) { + LOGE("--no-mouse-over is specific to --mouse=sdk"); + return false; + } + if ((opts->tunnel_host || opts->tunnel_port) && !opts->force_adb_forward) { LOGI("Tunnel host/port is set, " "--force-adb-forward automatically enabled."); diff --git a/app/src/mouse_sdk.c b/app/src/mouse_sdk.c index 620fb52c..a7998972 100644 --- a/app/src/mouse_sdk.c +++ b/app/src/mouse_sdk.c @@ -58,17 +58,18 @@ convert_touch_action(enum sc_touch_action action) { static void sc_mouse_processor_process_mouse_motion(struct sc_mouse_processor *mp, const struct sc_mouse_motion_event *event) { - if (!event->buttons_state) { + struct sc_mouse_sdk *m = DOWNCAST(mp); + + if (!m->mouse_hover && !event->buttons_state) { // Do not send motion events when no click is pressed return; } - struct sc_mouse_sdk *m = DOWNCAST(mp); - struct sc_control_msg msg = { .type = SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, .inject_touch_event = { - .action = AMOTION_EVENT_ACTION_MOVE, + .action = event->buttons_state ? AMOTION_EVENT_ACTION_MOVE + : AMOTION_EVENT_ACTION_HOVER_MOVE, .pointer_id = event->pointer_id, .position = event->position, .pressure = 1.f, @@ -145,8 +146,10 @@ sc_mouse_processor_process_touch(struct sc_mouse_processor *mp, } void -sc_mouse_sdk_init(struct sc_mouse_sdk *m, struct sc_controller *controller) { +sc_mouse_sdk_init(struct sc_mouse_sdk *m, struct sc_controller *controller, + bool mouse_hover) { m->controller = controller; + m->mouse_hover = mouse_hover; static const struct sc_mouse_processor_ops ops = { .process_mouse_motion = sc_mouse_processor_process_mouse_motion, diff --git a/app/src/mouse_sdk.h b/app/src/mouse_sdk.h index 444a6ad5..142b89bb 100644 --- a/app/src/mouse_sdk.h +++ b/app/src/mouse_sdk.h @@ -13,9 +13,11 @@ struct sc_mouse_sdk { struct sc_mouse_processor mouse_processor; // mouse processor trait struct sc_controller *controller; + bool mouse_hover; }; void -sc_mouse_sdk_init(struct sc_mouse_sdk *m, struct sc_controller *controller); +sc_mouse_sdk_init(struct sc_mouse_sdk *m, struct sc_controller *controller, + bool mouse_hover); #endif diff --git a/app/src/options.c b/app/src/options.c index 352d9895..5556d1f9 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -92,6 +92,7 @@ const struct scrcpy_options scrcpy_options_default = { .camera_high_speed = false, .list = 0, .window = true, + .mouse_hover = true, }; enum sc_orientation diff --git a/app/src/options.h b/app/src/options.h index d5b090b7..f840a989 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -290,6 +290,7 @@ struct scrcpy_options { #define SC_OPTION_LIST_CAMERA_SIZES 0x8 uint8_t list; bool window; + bool mouse_hover; }; extern const struct scrcpy_options scrcpy_options_default; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 85b89935..5e78dbf3 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -681,7 +681,8 @@ scrcpy(struct scrcpy_options *options) { } if (options->mouse_input_mode == SC_MOUSE_INPUT_MODE_SDK) { - sc_mouse_sdk_init(&s->mouse_sdk, &s->controller); + sc_mouse_sdk_init(&s->mouse_sdk, &s->controller, + options->mouse_hover); mp = &s->mouse_sdk.mouse_processor; } else if (options->mouse_input_mode == SC_MOUSE_INPUT_MODE_UHID) { bool ok = sc_mouse_uhid_init(&s->mouse_uhid, &s->controller); diff --git a/doc/mouse.md b/doc/mouse.md index 42d70766..1c62ddd0 100644 --- a/doc/mouse.md +++ b/doc/mouse.md @@ -18,6 +18,14 @@ Note that on some devices, an additional option must be enabled in developer options for this mouse mode to work. See [prerequisites](/README.md#prerequisites). +### Mouse hover + +By default, mouse hover (mouse motion without any clicks) events are forwarded +to the device. This can be disabled with: + +``` +scrcpy --no-mouse-hover +``` ## Physical mouse simulation From 1e3deabd6c9e9937094bba27c8a7c452453534be Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 29 Jun 2024 13:05:46 +0200 Subject: [PATCH 1845/2244] Do not call avcodec_close() The documentation of avcodec_close() says: > Do not use this function. Use avcodec_free_context() to destroy a > codec context (either open or closed). It was deprecated in FFmpeg 7 by commit 1cc24d749569a42510399a29b034f7a77bdec34e: > Its use has been discouraged since 2016, but now is no longer used in > avformat, so there is no reason to keep it public. --- app/src/demuxer.c | 1 - app/src/icon.c | 12 +++++------- app/src/v4l2_sink.c | 5 +---- 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/app/src/demuxer.c b/app/src/demuxer.c index c27ea292..7223b553 100644 --- a/app/src/demuxer.c +++ b/app/src/demuxer.c @@ -278,7 +278,6 @@ run_demuxer(void *data) { finally_close_sinks: sc_packet_source_sinks_close(&demuxer->packet_source); finally_free_context: - // This also calls avcodec_close() internally avcodec_free_context(&codec_ctx); end: demuxer->cbs->on_ended(demuxer, status, demuxer->cbs_userdata); diff --git a/app/src/icon.c b/app/src/icon.c index 0dddefa3..a76a85c9 100644 --- a/app/src/icon.c +++ b/app/src/icon.c @@ -117,21 +117,21 @@ decode_image(const char *path) { AVFrame *frame = av_frame_alloc(); if (!frame) { LOG_OOM(); - goto close_codec; + goto free_codec_ctx; } AVPacket *packet = av_packet_alloc(); if (!packet) { LOG_OOM(); av_frame_free(&frame); - goto close_codec; + goto free_codec_ctx; } if (av_read_frame(ctx, packet) < 0) { LOGE("Could not read frame"); av_packet_free(&packet); av_frame_free(&frame); - goto close_codec; + goto free_codec_ctx; } int ret; @@ -139,22 +139,20 @@ decode_image(const char *path) { LOGE("Could not send icon packet: %d", ret); av_packet_free(&packet); av_frame_free(&frame); - goto close_codec; + goto free_codec_ctx; } if ((ret = avcodec_receive_frame(codec_ctx, frame)) != 0) { LOGE("Could not receive icon frame: %d", ret); av_packet_free(&packet); av_frame_free(&frame); - goto close_codec; + goto free_codec_ctx; } av_packet_free(&packet); result = frame; -close_codec: - avcodec_close(codec_ctx); free_codec_ctx: avcodec_free_context(&codec_ctx); close_input: diff --git a/app/src/v4l2_sink.c b/app/src/v4l2_sink.c index 3b3eb8d0..087e9af4 100644 --- a/app/src/v4l2_sink.c +++ b/app/src/v4l2_sink.c @@ -240,7 +240,7 @@ sc_v4l2_sink_open(struct sc_v4l2_sink *vs, const AVCodecContext *ctx) { vs->frame = av_frame_alloc(); if (!vs->frame) { LOG_OOM(); - goto error_avcodec_close; + goto error_avcodec_free_context; } vs->packet = av_packet_alloc(); @@ -268,8 +268,6 @@ error_av_packet_free: av_packet_free(&vs->packet); error_av_frame_free: av_frame_free(&vs->frame); -error_avcodec_close: - avcodec_close(vs->encoder_ctx); error_avcodec_free_context: avcodec_free_context(&vs->encoder_ctx); error_avio_close: @@ -297,7 +295,6 @@ sc_v4l2_sink_close(struct sc_v4l2_sink *vs) { av_packet_free(&vs->packet); av_frame_free(&vs->frame); - avcodec_close(vs->encoder_ctx); avcodec_free_context(&vs->encoder_ctx); avio_close(vs->format_ctx->pb); avformat_free_context(vs->format_ctx); From 48c2c030938a87426ce8e5e7e1f26b8f059d1932 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 29 Jun 2024 12:48:32 +0200 Subject: [PATCH 1846/2244] Upgrade FFmpeg (7.0.1) for Windows --- app/deps/ffmpeg.sh | 5 ++-- app/deps/patches/ffmpeg-6.1-fix-build.patch | 27 --------------------- 2 files changed, 2 insertions(+), 30 deletions(-) delete mode 100644 app/deps/patches/ffmpeg-6.1-fix-build.patch diff --git a/app/deps/ffmpeg.sh b/app/deps/ffmpeg.sh index 19fb2991..ef92d4a5 100755 --- a/app/deps/ffmpeg.sh +++ b/app/deps/ffmpeg.sh @@ -4,10 +4,10 @@ DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) cd "$DEPS_DIR" . common -VERSION=6.1.1 +VERSION=7.0.1 FILENAME=ffmpeg-$VERSION.tar.xz PROJECT_DIR=ffmpeg-$VERSION -SHA256SUM=8684f4b00f94b85461884c3719382f1261f0d9eb3d59640a1f4ac0873616f968 +SHA256SUM=bce9eeb0f17ef8982390b1f37711a61b4290dc8c2a0c1a37b5857e85bfb0e4ff cd "$SOURCES_DIR" @@ -17,7 +17,6 @@ then else get_file "https://ffmpeg.org/releases/$FILENAME" "$FILENAME" "$SHA256SUM" tar xf "$FILENAME" # First level directory is "$PROJECT_DIR" - patch -d "$PROJECT_DIR" -p1 < "$PATCHES_DIR"/ffmpeg-6.1-fix-build.patch fi mkdir -p "$BUILD_DIR/$PROJECT_DIR" diff --git a/app/deps/patches/ffmpeg-6.1-fix-build.patch b/app/deps/patches/ffmpeg-6.1-fix-build.patch deleted file mode 100644 index ed4df48d..00000000 --- a/app/deps/patches/ffmpeg-6.1-fix-build.patch +++ /dev/null @@ -1,27 +0,0 @@ -From 03c80197afb324da38c9b70254231e3fdcfa68fc Mon Sep 17 00:00:00 2001 -From: Romain Vimont -Date: Sun, 12 Nov 2023 17:58:50 +0100 -Subject: [PATCH] Fix FFmpeg 6.1 build - -Build failed on tag n6.1 With --enable-decoder=av1 but without ---enable-muxer=av1. ---- - libavcodec/Makefile | 2 +- - 1 file changed, 1 insertion(+), 1 deletion(-) - -diff --git a/libavcodec/Makefile b/libavcodec/Makefile -index 580a8d6b54..aff19b670c 100644 ---- a/libavcodec/Makefile -+++ b/libavcodec/Makefile -@@ -249,7 +249,7 @@ OBJS-$(CONFIG_ATRAC3PAL_DECODER) += atrac3plusdec.o atrac3plus.o \ - OBJS-$(CONFIG_ATRAC9_DECODER) += atrac9dec.o - OBJS-$(CONFIG_AURA_DECODER) += cyuv.o - OBJS-$(CONFIG_AURA2_DECODER) += aura.o --OBJS-$(CONFIG_AV1_DECODER) += av1dec.o -+OBJS-$(CONFIG_AV1_DECODER) += av1dec.o av1_parse.o - OBJS-$(CONFIG_AV1_CUVID_DECODER) += cuviddec.o - OBJS-$(CONFIG_AV1_MEDIACODEC_DECODER) += mediacodecdec.o - OBJS-$(CONFIG_AV1_MEDIACODEC_ENCODER) += mediacodecenc.o --- -2.42.0 - From f13f00021fddebb3249748dca82ccb4ac7547505 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 29 Jun 2024 12:49:40 +0200 Subject: [PATCH 1847/2244] Upgrade SDL (2.30.4) for Windows --- app/deps/sdl.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/deps/sdl.sh b/app/deps/sdl.sh index 36c7ab1c..589f93e5 100755 --- a/app/deps/sdl.sh +++ b/app/deps/sdl.sh @@ -4,10 +4,10 @@ DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) cd "$DEPS_DIR" . common -VERSION=2.28.5 +VERSION=2.30.4 FILENAME=SDL-$VERSION.tar.gz PROJECT_DIR=SDL-release-$VERSION -SHA256SUM=9f0556e4a24ef5b267010038ad9e9948b62f236d5bcc4b22179f95ef62d84023 +SHA256SUM=dcc2c8c9c3e9e1a7c8d61d9522f1cba4e9b740feb560dcb15234030984610ee2 cd "$SOURCES_DIR" From 343f715323580398d446d6d1935a31cdbb668031 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 29 Jun 2024 12:53:39 +0200 Subject: [PATCH 1848/2244] Upgrade platform-tools (35.0.0) for Windows --- app/deps/adb.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/deps/adb.sh b/app/deps/adb.sh index e2408216..58a54659 100755 --- a/app/deps/adb.sh +++ b/app/deps/adb.sh @@ -4,10 +4,10 @@ DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) cd "$DEPS_DIR" . common -VERSION=34.0.5 +VERSION=35.0.0 FILENAME=platform-tools_r$VERSION-windows.zip PROJECT_DIR=platform-tools-$VERSION -SHA256SUM=3f8320152704377de150418a3c4c9d07d16d80a6c0d0d8f7289c22c499e33571 +SHA256SUM=7ab78a8f8b305ae4d0de647d99c43599744de61a0838d3a47bda0cdffefee87e cd "$SOURCES_DIR" From 89df38f64160556aaf3db5b56e93d165c5eb76fd Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 29 Jun 2024 16:52:45 +0200 Subject: [PATCH 1849/2244] Bump version to 2.5 --- app/scrcpy-windows.rc | 2 +- meson.build | 2 +- server/build.gradle | 4 ++-- server/build_without_gradle.sh | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/scrcpy-windows.rc b/app/scrcpy-windows.rc index 059e91d4..717d9cb2 100644 --- a/app/scrcpy-windows.rc +++ b/app/scrcpy-windows.rc @@ -13,7 +13,7 @@ BEGIN VALUE "LegalCopyright", "Romain Vimont, Genymobile" VALUE "OriginalFilename", "scrcpy.exe" VALUE "ProductName", "scrcpy" - VALUE "ProductVersion", "2.4" + VALUE "ProductVersion", "2.5" END END BLOCK "VarFileInfo" diff --git a/meson.build b/meson.build index 22d0f4ef..1d11e574 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('scrcpy', 'c', - version: '2.4', + version: '2.5', meson_version: '>= 0.48', default_options: [ 'c_std=c11', diff --git a/server/build.gradle b/server/build.gradle index 6a1b09df..d17ffcb2 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -7,8 +7,8 @@ android { applicationId "com.genymobile.scrcpy" minSdkVersion 21 targetSdkVersion 34 - versionCode 20400 - versionName "2.4" + versionCode 20500 + versionName "2.5" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index 7f7d7921..74bbd8ae 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -12,7 +12,7 @@ set -e SCRCPY_DEBUG=false -SCRCPY_VERSION_NAME=2.4 +SCRCPY_VERSION_NAME=2.5 PLATFORM=${ANDROID_PLATFORM:-34} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-34.0.0} From a8871bfad77ed1d0b968f3919df685a301849f8f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 29 Jun 2024 17:51:36 +0200 Subject: [PATCH 1850/2244] Update links to 2.5 --- README.md | 2 +- doc/build.md | 6 +++--- doc/windows.md | 12 ++++++------ install_release.sh | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index a672b327..3185652b 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ source for the project. Do not download releases from random websites, even if their name contains `scrcpy`.** -# scrcpy (v2.4) +# scrcpy (v2.5) scrcpy diff --git a/doc/build.md b/doc/build.md index 01319a10..a35910f8 100644 --- a/doc/build.md +++ b/doc/build.md @@ -233,10 +233,10 @@ install` must be run as root)._ #### Option 2: Use prebuilt server - - [`scrcpy-server-v2.4`][direct-scrcpy-server] - SHA-256: `93c272b7438605c055e127f7444064ed78fa9ca49f81156777fd201e79ce7ba3` + - [`scrcpy-server-v2.5`][direct-scrcpy-server] + SHA-256: `1488b1105d6aff534873a26bf610cd2aea06ee867dd7a4d9c6bb2c091396eb15` -[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v2.4/scrcpy-server-v2.4 +[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v2.5/scrcpy-server-v2.5 Download the prebuilt server somewhere, and specify its path during the Meson configuration: diff --git a/doc/windows.md b/doc/windows.md index e3053188..139c3419 100644 --- a/doc/windows.md +++ b/doc/windows.md @@ -4,14 +4,14 @@ Download the [latest release]: - - [`scrcpy-win64-v2.4.zip`][direct-win64] (64-bit) - SHA-256: `9dc56f21bfa455352ec0c58b40feaf2fb02d67372910a4235e298ece286ff3a9` - - [`scrcpy-win32-v2.4.zip`][direct-win32] (32-bit) - SHA-256: `cf92acc45eef37c6ee2db819f92e420ced3bc50f1348dd57f7d6ca1fc80f6116` + - [`scrcpy-win64-v2.5.zip`][direct-win64] (64-bit) + SHA-256: `345cf04a66a9144281dce72ca4e82adfd2c3092463196e586051df4c69e1507b` + - [`scrcpy-win32-v2.5.zip`][direct-win32] (32-bit) + SHA-256: `d56312a92471565fa4f3a6b94e8eb07717c4c90f2c0f05b03ba444e1001806ec` [latest release]: https://github.com/Genymobile/scrcpy/releases/latest -[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v2.4/scrcpy-win64-v2.4.zip -[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v2.4/scrcpy-win32-v2.4.zip +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v2.5/scrcpy-win64-v2.5.zip +[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v2.5/scrcpy-win32-v2.5.zip and extract it. diff --git a/install_release.sh b/install_release.sh index 0be5675c..2bd6d7e6 100755 --- a/install_release.sh +++ b/install_release.sh @@ -2,8 +2,8 @@ set -e BUILDDIR=build-auto -PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v2.4/scrcpy-server-v2.4 -PREBUILT_SERVER_SHA256=93c272b7438605c055e127f7444064ed78fa9ca49f81156777fd201e79ce7ba3 +PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v2.5/scrcpy-server-v2.5 +PREBUILT_SERVER_SHA256=1488b1105d6aff534873a26bf610cd2aea06ee867dd7a4d9c6bb2c091396eb15 echo "[scrcpy] Downloading prebuilt server..." wget "$PREBUILT_SERVER_URL" -O scrcpy-server From a4f8c025027964a9978925396459d36b8337623d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 2 Jul 2024 08:11:32 +0200 Subject: [PATCH 1851/2244] Reorder initialization to simplify MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This also avoids a warning with some compilers which do not understand that the condition to initialize the variable is the same as the condition to use it: ../app/src/scrcpy.c: In function ‘scrcpy’: ../app/src/scrcpy.c:750:13: warning: ‘src’ may be used uninitialized in this function [-Wmaybe-uninitialized] 750 | sc_frame_source_add_sink(src, &s->screen.frame_sink); | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Refs 45fe6b602b4c050c5b1fba87cec7160093052af3 Refs --- app/src/scrcpy.c | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 5e78dbf3..84f7c571 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -730,23 +730,20 @@ scrcpy(struct scrcpy_options *options) { .start_fps_counter = options->start_fps_counter, }; - struct sc_frame_source *src; - if (options->video_playback) { - src = &s->video_decoder.frame_source; - if (options->display_buffer) { - sc_delay_buffer_init(&s->display_buffer, - options->display_buffer, true); - sc_frame_source_add_sink(src, &s->display_buffer.frame_sink); - src = &s->display_buffer.frame_source; - } - } - if (!sc_screen_init(&s->screen, &screen_params)) { goto end; } screen_initialized = true; if (options->video_playback) { + struct sc_frame_source *src = &s->video_decoder.frame_source; + if (options->display_buffer) { + sc_delay_buffer_init(&s->display_buffer, + options->display_buffer, true); + sc_frame_source_add_sink(src, &s->display_buffer.frame_sink); + src = &s->display_buffer.frame_source; + } + sc_frame_source_add_sink(src, &s->screen.frame_sink); } } From 1d3b6dac6962d67d91b6e5ec93a39d99c5f44641 Mon Sep 17 00:00:00 2001 From: Fr_Dae Date: Wed, 3 Jul 2024 11:49:01 +0200 Subject: [PATCH 1852/2244] Improve bug report template Use titles and capital letters. PR #5051 Signed-off-by: Romain Vimont --- .github/ISSUE_TEMPLATE/bug_report.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 1c04da7f..c06ac786 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -10,14 +10,16 @@ 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...] - - device model: - - Android version: [e.g. 10] +## Environment + + - **OS:** [e.g. Debian, Windows, macOS...] + - **Scrcpy version:** [e.g. 1.12.1] + - **Installation method:** [e.g. manual build, apt, snap, brew, Windows release...] + - **Device model:** + - **Android version:** [e.g. 10] + +## Describe the bug -**Describe the bug** A clear and concise description of what the bug is. On errors, please provide the output of the console (and `adb logcat` if relevant). From 126da0cb18576cc40b9506c676af3a2d569c760c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 5 Jul 2024 23:57:33 +0200 Subject: [PATCH 1853/2244] Rework bug report template checks Remove explicit checkboxes, and add a link to prerequisites. --- .github/ISSUE_TEMPLATE/bug_report.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index c06ac786..977f2d8d 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -7,8 +7,14 @@ assignees: '' --- - - [ ] I have read the [FAQ](https://github.com/Genymobile/scrcpy/blob/master/FAQ.md). - - [ ] I have searched in existing [issues](https://github.com/Genymobile/scrcpy/issues). +_Please read the [prerequisites] to run scrcpy._ + +[prerequisites]: https://github.com/Genymobile/scrcpy#prerequisites + +_Also read the [FAQ] and check if your [issue][issues] already exists._ + +[FAQ]: https://github.com/Genymobile/scrcpy/blob/master/FAQ.md +[issues]: https://github.com/Genymobile/scrcpy/issues ## Environment From cc8e6133b0c9975ef2ed16086960e822ab750404 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 5 Jul 2024 23:58:00 +0200 Subject: [PATCH 1854/2244] Upgrade default versions in bug report template --- .github/ISSUE_TEMPLATE/bug_report.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 977f2d8d..576d4666 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -19,10 +19,10 @@ _Also read the [FAQ] and check if your [issue][issues] already exists._ ## Environment - **OS:** [e.g. Debian, Windows, macOS...] - - **Scrcpy version:** [e.g. 1.12.1] + - **Scrcpy version:** [e.g. 2.5] - **Installation method:** [e.g. manual build, apt, snap, brew, Windows release...] - **Device model:** - - **Android version:** [e.g. 10] + - **Android version:** [e.g. 14] ## Describe the bug From b419eef55e19a4700af3ea0a170d8d0a9ce6bd16 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 1 Jul 2024 08:17:33 +0200 Subject: [PATCH 1855/2244] Do not report error on device disconnected A device disconnection (when the adb connection is closed) makes the read() on the "receiver" socket fail. Since commit 063a8339ed27b94a8fe1e53a284507eb2d044e15, this is reported as an error. As a consequence, scrcpy fails with: ERROR: Controller error instead of: WARN: Device disconnected To fix the issue, report a device disconnection in that case. PR #5044 --- app/src/controller.c | 40 ++++++++++++++++++++++++++-------------- app/src/controller.h | 3 ++- app/src/receiver.c | 8 ++++++-- app/src/receiver.h | 2 +- app/src/scrcpy.c | 11 ++++++++--- 5 files changed, 43 insertions(+), 21 deletions(-) diff --git a/app/src/controller.c b/app/src/controller.c index edd767eb..d50e1921 100644 --- a/app/src/controller.c +++ b/app/src/controller.c @@ -7,12 +7,13 @@ #define SC_CONTROL_MSG_QUEUE_MAX 64 static void -sc_controller_receiver_on_error(struct sc_receiver *receiver, void *userdata) { +sc_controller_receiver_on_ended(struct sc_receiver *receiver, bool error, + void *userdata) { (void) receiver; struct sc_controller *controller = userdata; // Forward the event to the controller listener - controller->cbs->on_error(controller, controller->cbs_userdata); + controller->cbs->on_ended(controller, error, controller->cbs_userdata); } bool @@ -27,7 +28,7 @@ sc_controller_init(struct sc_controller *controller, sc_socket control_socket, } static const struct sc_receiver_callbacks receiver_cbs = { - .on_error = sc_controller_receiver_on_error, + .on_ended = sc_controller_receiver_on_ended, }; ok = sc_receiver_init(&controller->receiver, control_socket, &receiver_cbs, @@ -55,7 +56,7 @@ sc_controller_init(struct sc_controller *controller, sc_socket control_socket, controller->control_socket = control_socket; controller->stopped = false; - assert(cbs && cbs->on_error); + assert(cbs && cbs->on_ended); controller->cbs = cbs; controller->cbs_userdata = cbs_userdata; @@ -110,21 +111,30 @@ sc_controller_push_msg(struct sc_controller *controller, static bool process_msg(struct sc_controller *controller, - const struct sc_control_msg *msg) { + const struct sc_control_msg *msg, bool *eos) { static uint8_t serialized_msg[SC_CONTROL_MSG_MAX_SIZE]; size_t length = sc_control_msg_serialize(msg, serialized_msg); if (!length) { + *eos = false; return false; } + ssize_t w = net_send_all(controller->control_socket, serialized_msg, length); - return (size_t) w == length; + if ((size_t) w != length) { + *eos = true; + return false; + } + + return true; } static int run_controller(void *data) { struct sc_controller *controller = data; + bool error = false; + for (;;) { sc_mutex_lock(&controller->mutex); while (!controller->stopped @@ -134,6 +144,7 @@ run_controller(void *data) { if (controller->stopped) { // stop immediately, do not process further msgs sc_mutex_unlock(&controller->mutex); + LOGD("Controller stopped"); break; } @@ -141,20 +152,21 @@ run_controller(void *data) { struct sc_control_msg msg = sc_vecdeque_pop(&controller->queue); sc_mutex_unlock(&controller->mutex); - bool ok = process_msg(controller, &msg); + bool eos; + bool ok = process_msg(controller, &msg, &eos); sc_control_msg_destroy(&msg); if (!ok) { - LOGD("Could not write msg to socket"); - goto error; + if (eos) { + LOGD("Controller stopped (socket closed)"); + } // else error already logged + error = !eos; + break; } } + controller->cbs->on_ended(controller, error, controller->cbs_userdata); + return 0; - -error: - controller->cbs->on_error(controller, controller->cbs_userdata); - - return 1; // ignored } bool diff --git a/app/src/controller.h b/app/src/controller.h index 353d4d0d..57ad79b3 100644 --- a/app/src/controller.h +++ b/app/src/controller.h @@ -28,7 +28,8 @@ struct sc_controller { }; struct sc_controller_callbacks { - void (*on_error)(struct sc_controller *controller, void *userdata); + void (*on_ended)(struct sc_controller *controller, bool error, + void *userdata); }; bool diff --git a/app/src/receiver.c b/app/src/receiver.c index fb923ac4..3e572067 100644 --- a/app/src/receiver.c +++ b/app/src/receiver.c @@ -21,7 +21,7 @@ sc_receiver_init(struct sc_receiver *receiver, sc_socket control_socket, receiver->acksync = NULL; receiver->uhid_devices = NULL; - assert(cbs && cbs->on_error); + assert(cbs && cbs->on_ended); receiver->cbs = cbs; receiver->cbs_userdata = cbs_userdata; @@ -134,12 +134,15 @@ run_receiver(void *data) { static uint8_t buf[DEVICE_MSG_MAX_SIZE]; size_t head = 0; + bool error = false; + for (;;) { assert(head < DEVICE_MSG_MAX_SIZE); ssize_t r = net_recv(receiver->control_socket, buf + head, DEVICE_MSG_MAX_SIZE - head); if (r <= 0) { LOGD("Receiver stopped"); + // device disconnected: keep error=false break; } @@ -147,6 +150,7 @@ run_receiver(void *data) { ssize_t consumed = process_msgs(receiver, buf, head); if (consumed == -1) { // an error occurred + error = true; break; } @@ -157,7 +161,7 @@ run_receiver(void *data) { } } - receiver->cbs->on_error(receiver, receiver->cbs_userdata); + receiver->cbs->on_ended(receiver, error, receiver->cbs_userdata); return 0; } diff --git a/app/src/receiver.h b/app/src/receiver.h index ef83978f..b1ae4fde 100644 --- a/app/src/receiver.h +++ b/app/src/receiver.h @@ -25,7 +25,7 @@ struct sc_receiver { }; struct sc_receiver_callbacks { - void (*on_error)(struct sc_receiver *receiver, void *userdata); + void (*on_ended)(struct sc_receiver *receiver, bool error, void *userdata); }; bool diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 84f7c571..376d5839 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -269,13 +269,18 @@ sc_audio_demuxer_on_ended(struct sc_demuxer *demuxer, } static void -sc_controller_on_error(struct sc_controller *controller, void *userdata) { +sc_controller_on_ended(struct sc_controller *controller, bool error, + void *userdata) { // Note: this function may be called twice, once from the controller thread // and once from the receiver thread (void) controller; (void) userdata; - PUSH_EVENT(SC_EVENT_CONTROLLER_ERROR); + if (error) { + PUSH_EVENT(SC_EVENT_CONTROLLER_ERROR); + } else { + PUSH_EVENT(SC_EVENT_DEVICE_DISCONNECTED); + } } static void @@ -567,7 +572,7 @@ scrcpy(struct scrcpy_options *options) { if (options->control) { static const struct sc_controller_callbacks controller_cbs = { - .on_error = sc_controller_on_error, + .on_ended = sc_controller_on_ended, }; if (!sc_controller_init(&s->controller, s->server.control_socket, From 46041e0cc0d2467d470b444cb50cd9b6ae1fa695 Mon Sep 17 00:00:00 2001 From: Yan Date: Fri, 5 Jul 2024 01:38:41 +0200 Subject: [PATCH 1856/2244] Always initialize display->gl_context on macOS Otherwise SDL_GL_DeleteContext() tried to access an uninitialized pointer upon exit when not using the OpenGL renderer. SDL_GL_DeleteContext() doesn't try to delete a NULL pointer, so no need to check for that. Fixes #5057 PR #5058 Signed-off-by: Romain Vimont --- app/src/display.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/display.c b/app/src/display.c index 9f5fb0c6..39018834 100644 --- a/app/src/display.c +++ b/app/src/display.c @@ -43,6 +43,10 @@ sc_display_init(struct sc_display *display, SDL_Window *window, display->mipmaps = false; +#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE + display->gl_context = NULL; +#endif + // starts with "opengl" bool use_opengl = renderer_name && !strncmp(renderer_name, "opengl", 6); if (use_opengl) { From b50f9eb41d1bd7b75a77e2c388b0144365e3cd5f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 8 Jul 2024 16:58:44 +0200 Subject: [PATCH 1857/2244] Add workaround for Skyworth devices The vendor-modified ROM of Skyworth devices needs a valid app info/context. Fixes #4922 --- server/src/main/java/com/genymobile/scrcpy/Workarounds.java | 4 +++- 1 file changed, 3 insertions(+), 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 448e7099..c9a26d78 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java +++ b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java @@ -63,9 +63,11 @@ public final class Workarounds { // - // - mustFillAppInfo = true; - } else if (Build.BRAND.equalsIgnoreCase("honor")) { + } else if (Build.BRAND.equalsIgnoreCase("honor") || Build.MANUFACTURER.equalsIgnoreCase("skyworth")) { // More workarounds must be applied for Honor devices: // - + // and Skyworth devices: + // - // // The system context must not be set for all devices, because it would cause other problems: // - From 487a6b9cf4a2ddc777a336d4b4c747ed33fa87a6 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 8 Jul 2024 16:17:11 +0200 Subject: [PATCH 1858/2244] Remove top-level const For consistency, never use top-level const for local variables. PR #5076 --- 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 43b10d2d..9da3a727 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -846,9 +846,9 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im, // can be used instead of Ctrl. The "virtual finger" has a position // inverted with respect to the vertical axis of symmetry in the middle of // the screen. - const SDL_Keymod keymod = SDL_GetModState(); - const bool ctrl_pressed = keymod & KMOD_CTRL; - const bool shift_pressed = keymod & KMOD_SHIFT; + SDL_Keymod keymod = SDL_GetModState(); + bool ctrl_pressed = keymod & KMOD_CTRL; + bool shift_pressed = keymod & KMOD_SHIFT; if (event->button == SDL_BUTTON_LEFT && ((down && !im->vfinger_down && ((ctrl_pressed && !shift_pressed) || From 6d98766cd5cf93804fd14d1eb9765ab9e1357cb1 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 8 Jul 2024 16:18:06 +0200 Subject: [PATCH 1859/2244] Simplify boolean condition using XOR (A && !B) || (!A && B) <==> A ^ B PR #5076 --- app/src/input_manager.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 9da3a727..96075f84 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -850,9 +850,7 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im, bool ctrl_pressed = keymod & KMOD_CTRL; bool shift_pressed = keymod & KMOD_SHIFT; if (event->button == SDL_BUTTON_LEFT && - ((down && !im->vfinger_down && - ((ctrl_pressed && !shift_pressed) || - (!ctrl_pressed && shift_pressed))) || + ((down && !im->vfinger_down && (ctrl_pressed ^ shift_pressed)) || (!down && im->vfinger_down))) { struct sc_point mouse = sc_screen_convert_window_to_frame_coords(im->screen, event->x, From 0bce4d7f56a4d109d450c4e41e4387a086c820ef Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 8 Jul 2024 16:29:47 +0200 Subject: [PATCH 1860/2244] Add missing SC_ prefix for pointer id constants PR #5076 --- app/src/control_msg.c | 8 ++++---- app/src/control_msg.h | 8 ++++---- app/src/input_manager.c | 12 ++++++------ 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/app/src/control_msg.c b/app/src/control_msg.c index b3da5fe5..5a800040 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -64,13 +64,13 @@ static const char *const copy_key_labels[] = { static inline const char * get_well_known_pointer_id_name(uint64_t pointer_id) { switch (pointer_id) { - case POINTER_ID_MOUSE: + case SC_POINTER_ID_MOUSE: return "mouse"; - case POINTER_ID_GENERIC_FINGER: + case SC_POINTER_ID_GENERIC_FINGER: return "finger"; - case POINTER_ID_VIRTUAL_MOUSE: + case SC_POINTER_ID_VIRTUAL_MOUSE: return "vmouse"; - case POINTER_ID_VIRTUAL_FINGER: + case SC_POINTER_ID_VIRTUAL_FINGER: return "vfinger"; default: return NULL; diff --git a/app/src/control_msg.h b/app/src/control_msg.h index cd1340ef..2ec7b5be 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -18,12 +18,12 @@ // type: 1 byte; sequence: 8 bytes; paste flag: 1 byte; length: 4 bytes #define SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH (SC_CONTROL_MSG_MAX_SIZE - 14) -#define POINTER_ID_MOUSE UINT64_C(-1) -#define POINTER_ID_GENERIC_FINGER UINT64_C(-2) +#define SC_POINTER_ID_MOUSE UINT64_C(-1) +#define SC_POINTER_ID_GENERIC_FINGER UINT64_C(-2) // Used for injecting an additional virtual pointer for pinch-to-zoom -#define POINTER_ID_VIRTUAL_MOUSE UINT64_C(-3) -#define POINTER_ID_VIRTUAL_FINGER UINT64_C(-4) +#define SC_POINTER_ID_VIRTUAL_MOUSE UINT64_C(-3) +#define SC_POINTER_ID_VIRTUAL_FINGER UINT64_C(-4) enum sc_control_msg_type { SC_CONTROL_MSG_TYPE_INJECT_KEYCODE, diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 96075f84..415c1293 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -376,8 +376,8 @@ simulate_virtual_finger(struct sc_input_manager *im, msg.inject_touch_event.position.screen_size = im->screen->frame_size; msg.inject_touch_event.position.point = point; msg.inject_touch_event.pointer_id = - im->has_secondary_click ? POINTER_ID_VIRTUAL_MOUSE - : POINTER_ID_VIRTUAL_FINGER; + im->has_secondary_click ? SC_POINTER_ID_VIRTUAL_MOUSE + : SC_POINTER_ID_VIRTUAL_FINGER; msg.inject_touch_event.pressure = up ? 0.0f : 1.0f; msg.inject_touch_event.action_button = 0; msg.inject_touch_event.buttons = 0; @@ -662,8 +662,8 @@ sc_input_manager_process_mouse_motion(struct sc_input_manager *im, struct sc_mouse_motion_event evt = { .position = sc_input_manager_get_position(im, event->x, event->y), - .pointer_id = im->has_secondary_click ? POINTER_ID_MOUSE - : POINTER_ID_GENERIC_FINGER, + .pointer_id = im->has_secondary_click ? SC_POINTER_ID_MOUSE + : SC_POINTER_ID_GENERIC_FINGER, .xrel = event->xrel, .yrel = event->yrel, .buttons_state = @@ -817,8 +817,8 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im, .position = sc_input_manager_get_position(im, event->x, event->y), .action = sc_action_from_sdl_mousebutton_type(event->type), .button = sc_mouse_button_from_sdl(event->button), - .pointer_id = im->has_secondary_click ? POINTER_ID_MOUSE - : POINTER_ID_GENERIC_FINGER, + .pointer_id = im->has_secondary_click ? SC_POINTER_ID_MOUSE + : SC_POINTER_ID_GENERIC_FINGER, .buttons_state = sc_mouse_buttons_state_from_sdl(sdl_buttons_state, &im->mouse_bindings), }; From 6808288823239b0f3a76f9be377e4de82e91b35a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 8 Jul 2024 16:38:15 +0200 Subject: [PATCH 1861/2244] Make pointer id independent of mouse bindings The device source (MOUSE or FINGER) to use depended on whether a secondary click was possible via mouse bindings. As a first step, always use a mouse source to break this dependency. Note that this change might cause regressions in some (unknown) cases (refs f70359f14fb13f277c65b96f43ec83aba4722457), but hopefully not. Further commits will restore a finger source in some specific use cases, but independent of secondary clicks. Refs #5055 Fixes #5067 PR #5076 --- app/src/input_manager.c | 20 +++----------------- app/src/input_manager.h | 1 - 2 files changed, 3 insertions(+), 18 deletions(-) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 415c1293..71c4434b 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -52,14 +52,6 @@ is_shortcut_key(struct sc_input_manager *im, SDL_Keycode keycode) { || (im->sdl_shortcut_mods & KMOD_RGUI && keycode == SDLK_RGUI); } -static inline bool -mouse_bindings_has_secondary_click(const struct sc_mouse_bindings *mb) { - return mb->right_click == SC_MOUSE_BINDING_CLICK - || mb->middle_click == SC_MOUSE_BINDING_CLICK - || mb->click4 == SC_MOUSE_BINDING_CLICK - || mb->click5 == SC_MOUSE_BINDING_CLICK; -} - void sc_input_manager_init(struct sc_input_manager *im, const struct sc_input_manager_params *params) { @@ -76,8 +68,6 @@ sc_input_manager_init(struct sc_input_manager *im, im->mp = params->mp; im->mouse_bindings = params->mouse_bindings; - im->has_secondary_click = - mouse_bindings_has_secondary_click(&im->mouse_bindings); im->legacy_paste = params->legacy_paste; im->clipboard_autosync = params->clipboard_autosync; @@ -375,9 +365,7 @@ simulate_virtual_finger(struct sc_input_manager *im, 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 = - im->has_secondary_click ? SC_POINTER_ID_VIRTUAL_MOUSE - : SC_POINTER_ID_VIRTUAL_FINGER; + msg.inject_touch_event.pointer_id = SC_POINTER_ID_VIRTUAL_MOUSE; msg.inject_touch_event.pressure = up ? 0.0f : 1.0f; msg.inject_touch_event.action_button = 0; msg.inject_touch_event.buttons = 0; @@ -662,8 +650,7 @@ sc_input_manager_process_mouse_motion(struct sc_input_manager *im, struct sc_mouse_motion_event evt = { .position = sc_input_manager_get_position(im, event->x, event->y), - .pointer_id = im->has_secondary_click ? SC_POINTER_ID_MOUSE - : SC_POINTER_ID_GENERIC_FINGER, + .pointer_id = SC_POINTER_ID_MOUSE, .xrel = event->xrel, .yrel = event->yrel, .buttons_state = @@ -817,8 +804,7 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im, .position = sc_input_manager_get_position(im, event->x, event->y), .action = sc_action_from_sdl_mousebutton_type(event->type), .button = sc_mouse_button_from_sdl(event->button), - .pointer_id = im->has_secondary_click ? SC_POINTER_ID_MOUSE - : SC_POINTER_ID_GENERIC_FINGER, + .pointer_id = SC_POINTER_ID_MOUSE, .buttons_state = sc_mouse_buttons_state_from_sdl(sdl_buttons_state, &im->mouse_bindings), }; diff --git a/app/src/input_manager.h b/app/src/input_manager.h index 03c42fe6..d5a5a64d 100644 --- a/app/src/input_manager.h +++ b/app/src/input_manager.h @@ -23,7 +23,6 @@ struct sc_input_manager { struct sc_mouse_processor *mp; struct sc_mouse_bindings mouse_bindings; - bool has_secondary_click; bool legacy_paste; bool clipboard_autosync; From 51fee79bf50b124223523eb51c437c1267c2724a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 8 Jul 2024 16:27:59 +0200 Subject: [PATCH 1862/2244] Use finger source when a pointer is simulated For pinch-to-zoom, rotation and tilt simulation, always use a finger source (instead of a mouse) for both pointers (the real one and the simulated one). A "virtual" mouse does not work on all devices (e.g. on Pixel 8). PR #5076 --- app/src/input_manager.c | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 71c4434b..6fbd801c 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -365,7 +365,7 @@ simulate_virtual_finger(struct sc_input_manager *im, 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 = SC_POINTER_ID_VIRTUAL_MOUSE; + msg.inject_touch_event.pointer_id = SC_POINTER_ID_VIRTUAL_FINGER; msg.inject_touch_event.pressure = up ? 0.0f : 1.0f; msg.inject_touch_event.action_button = 0; msg.inject_touch_event.buttons = 0; @@ -650,7 +650,8 @@ sc_input_manager_process_mouse_motion(struct sc_input_manager *im, struct sc_mouse_motion_event evt = { .position = sc_input_manager_get_position(im, event->x, event->y), - .pointer_id = SC_POINTER_ID_MOUSE, + .pointer_id = im->vfinger_down ? SC_POINTER_ID_GENERIC_FINGER + : SC_POINTER_ID_MOUSE, .xrel = event->xrel, .yrel = event->yrel, .buttons_state = @@ -800,11 +801,20 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im, uint32_t sdl_buttons_state = SDL_GetMouseState(NULL, NULL); + SDL_Keymod keymod = SDL_GetModState(); + bool ctrl_pressed = keymod & KMOD_CTRL; + bool shift_pressed = keymod & KMOD_SHIFT; + bool change_vfinger = event->button == SDL_BUTTON_LEFT && + ((down && !im->vfinger_down && (ctrl_pressed ^ shift_pressed)) || + (!down && im->vfinger_down)); + bool use_finger = im->vfinger_down || change_vfinger; + struct sc_mouse_click_event evt = { .position = sc_input_manager_get_position(im, event->x, event->y), .action = sc_action_from_sdl_mousebutton_type(event->type), .button = sc_mouse_button_from_sdl(event->button), - .pointer_id = SC_POINTER_ID_MOUSE, + .pointer_id = use_finger ? SC_POINTER_ID_GENERIC_FINGER + : SC_POINTER_ID_MOUSE, .buttons_state = sc_mouse_buttons_state_from_sdl(sdl_buttons_state, &im->mouse_bindings), }; @@ -832,12 +842,7 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im, // can be used instead of Ctrl. The "virtual finger" has a position // inverted with respect to the vertical axis of symmetry in the middle of // the screen. - SDL_Keymod keymod = SDL_GetModState(); - bool ctrl_pressed = keymod & KMOD_CTRL; - bool shift_pressed = keymod & KMOD_SHIFT; - if (event->button == SDL_BUTTON_LEFT && - ((down && !im->vfinger_down && (ctrl_pressed ^ shift_pressed)) || - (!down && im->vfinger_down))) { + if (change_vfinger) { struct sc_point mouse = sc_screen_convert_window_to_frame_coords(im->screen, event->x, event->y); From 86b8286217b0909bf409e68ac1885ee59cc4cefc Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 8 Jul 2024 16:33:10 +0200 Subject: [PATCH 1863/2244] Remove unused virtual mouse PR #5076 --- app/src/control_msg.c | 2 -- app/src/control_msg.h | 3 +-- server/src/main/java/com/genymobile/scrcpy/Controller.java | 5 ++--- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/app/src/control_msg.c b/app/src/control_msg.c index 5a800040..9b0fab67 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -68,8 +68,6 @@ get_well_known_pointer_id_name(uint64_t pointer_id) { return "mouse"; case SC_POINTER_ID_GENERIC_FINGER: return "finger"; - case SC_POINTER_ID_VIRTUAL_MOUSE: - return "vmouse"; case SC_POINTER_ID_VIRTUAL_FINGER: return "vfinger"; default: diff --git a/app/src/control_msg.h b/app/src/control_msg.h index 2ec7b5be..80714096 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -22,8 +22,7 @@ #define SC_POINTER_ID_GENERIC_FINGER UINT64_C(-2) // Used for injecting an additional virtual pointer for pinch-to-zoom -#define SC_POINTER_ID_VIRTUAL_MOUSE UINT64_C(-3) -#define SC_POINTER_ID_VIRTUAL_FINGER UINT64_C(-4) +#define SC_POINTER_ID_VIRTUAL_FINGER UINT64_C(-3) enum sc_control_msg_type { SC_CONTROL_MSG_TYPE_INJECT_KEYCODE, diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index 87faf8ba..b7d2f93e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -22,7 +22,6 @@ public class Controller implements AsyncProcessor { // control_msg.h values of the pointerId field in inject_touch_event message private static final int POINTER_ID_MOUSE = -1; - private static final int POINTER_ID_VIRTUAL_MOUSE = -3; private static final ScheduledExecutorService EXECUTOR = Executors.newSingleThreadScheduledExecutor(); @@ -273,8 +272,8 @@ public class Controller implements AsyncProcessor { pointer.setPressure(pressure); int source; - if (pointerId == POINTER_ID_MOUSE || pointerId == POINTER_ID_VIRTUAL_MOUSE) { - // real mouse event (forced by the client when --forward-on-click) + if (pointerId == POINTER_ID_MOUSE) { + // real mouse event pointerProperties[pointerIndex].toolType = MotionEvent.TOOL_TYPE_MOUSE; source = InputDevice.SOURCE_MOUSE; pointer.setUp(buttons == 0); From 6baea57987a1867f2157f5c1001e77ca3cb1c6c5 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 9 Jul 2024 18:37:53 +0200 Subject: [PATCH 1864/2244] Track mouse buttons state manually The buttons state was tracked by SDL_GetMouseState(), and scrcpy applied a mask to ignore buttons used for shortcuts. Instead, track the buttons actually pressed (ignoring shortcuts) manually, to prepare the introduction of more dynamic mouse shortcuts. PR #5076 --- app/src/input_events.h | 20 +++----------------- app/src/input_manager.c | 24 +++++++++++++++++------- app/src/input_manager.h | 2 ++ app/src/usb/screen_otg.c | 8 +++----- 4 files changed, 25 insertions(+), 29 deletions(-) diff --git a/app/src/input_events.h b/app/src/input_events.h index ed77bcb4..bbf4372f 100644 --- a/app/src/input_events.h +++ b/app/src/input_events.h @@ -437,25 +437,11 @@ sc_mouse_button_from_sdl(uint8_t button) { } static inline uint8_t -sc_mouse_buttons_state_from_sdl(uint32_t buttons_state, - const struct sc_mouse_bindings *mb) { +sc_mouse_buttons_state_from_sdl(uint32_t buttons_state) { assert(buttons_state < 0x100); // fits in uint8_t - uint8_t mask = SC_MOUSE_BUTTON_LEFT; - if (!mb || mb->right_click == SC_MOUSE_BINDING_CLICK) { - mask |= SC_MOUSE_BUTTON_RIGHT; - } - if (!mb || mb->middle_click == SC_MOUSE_BINDING_CLICK) { - mask |= SC_MOUSE_BUTTON_MIDDLE; - } - if (!mb || mb->click4 == SC_MOUSE_BINDING_CLICK) { - mask |= SC_MOUSE_BUTTON_X1; - } - if (!mb || mb->click5 == SC_MOUSE_BINDING_CLICK) { - mask |= SC_MOUSE_BUTTON_X2; - } - - return buttons_state & mask; + // SC_MOUSE_BUTTON_* constants are initialized from SDL_BUTTON(index) + return buttons_state; } #endif diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 6fbd801c..2cc34afa 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -77,6 +77,8 @@ sc_input_manager_init(struct sc_input_manager *im, im->vfinger_invert_x = false; im->vfinger_invert_y = false; + im->mouse_buttons_state = 0; + im->last_keycode = SDLK_UNKNOWN; im->last_mod = 0; im->key_repeat = 0; @@ -654,8 +656,7 @@ sc_input_manager_process_mouse_motion(struct sc_input_manager *im, : SC_POINTER_ID_MOUSE, .xrel = event->xrel, .yrel = event->yrel, - .buttons_state = - sc_mouse_buttons_state_from_sdl(event->state, &im->mouse_bindings), + .buttons_state = im->mouse_buttons_state, }; assert(im->mp->ops->process_mouse_motion); @@ -736,6 +737,13 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im, bool control = im->controller; bool paused = im->screen->paused; bool down = event->type == SDL_MOUSEBUTTONDOWN; + + enum sc_mouse_button button = sc_mouse_button_from_sdl(event->button); + if (!down) { + // Mark the button as released + im->mouse_buttons_state &= ~button; + } + if (control && !paused) { enum sc_action action = down ? SC_ACTION_DOWN : SC_ACTION_UP; @@ -799,7 +807,10 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im, return; } - uint32_t sdl_buttons_state = SDL_GetMouseState(NULL, NULL); + if (down) { + // Mark the button as pressed + im->mouse_buttons_state |= button; + } SDL_Keymod keymod = SDL_GetModState(); bool ctrl_pressed = keymod & KMOD_CTRL; @@ -815,8 +826,7 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im, .button = sc_mouse_button_from_sdl(event->button), .pointer_id = use_finger ? SC_POINTER_ID_GENERIC_FINGER : SC_POINTER_ID_MOUSE, - .buttons_state = sc_mouse_buttons_state_from_sdl(sdl_buttons_state, - &im->mouse_bindings), + .buttons_state = im->mouse_buttons_state, }; assert(im->mp->ops->process_mouse_click); @@ -875,6 +885,7 @@ sc_input_manager_process_mouse_wheel(struct sc_input_manager *im, int mouse_x; int mouse_y; uint32_t buttons = SDL_GetMouseState(&mouse_x, &mouse_y); + (void) buttons; // Actual buttons are tracked manually to ignore shortcuts struct sc_mouse_scroll_event evt = { .position = sc_input_manager_get_position(im, mouse_x, mouse_y), @@ -885,8 +896,7 @@ sc_input_manager_process_mouse_wheel(struct sc_input_manager *im, .hscroll = CLAMP(event->x, -1, 1), .vscroll = CLAMP(event->y, -1, 1), #endif - .buttons_state = sc_mouse_buttons_state_from_sdl(buttons, - &im->mouse_bindings), + .buttons_state = im->mouse_buttons_state, }; im->mp->ops->process_mouse_scroll(im->mp, &evt); diff --git a/app/src/input_manager.h b/app/src/input_manager.h index d5a5a64d..88558549 100644 --- a/app/src/input_manager.h +++ b/app/src/input_manager.h @@ -32,6 +32,8 @@ struct sc_input_manager { bool vfinger_invert_x; bool vfinger_invert_y; + uint8_t mouse_buttons_state; // OR of enum sc_mouse_button values + // Tracks the number of identical consecutive shortcut key down events. // Not to be confused with event->repeat, which counts the number of // system-generated repeated key presses. diff --git a/app/src/usb/screen_otg.c b/app/src/usb/screen_otg.c index 33500e0c..5c4f97f0 100644 --- a/app/src/usb/screen_otg.c +++ b/app/src/usb/screen_otg.c @@ -169,7 +169,7 @@ sc_screen_otg_process_mouse_motion(struct sc_screen_otg *screen, // .position not used for HID events .xrel = event->xrel, .yrel = event->yrel, - .buttons_state = sc_mouse_buttons_state_from_sdl(event->state, NULL), + .buttons_state = sc_mouse_buttons_state_from_sdl(event->state), }; assert(mp->ops->process_mouse_motion); @@ -188,8 +188,7 @@ sc_screen_otg_process_mouse_button(struct sc_screen_otg *screen, // .position not used for HID events .action = sc_action_from_sdl_mousebutton_type(event->type), .button = sc_mouse_button_from_sdl(event->button), - .buttons_state = - sc_mouse_buttons_state_from_sdl(sdl_buttons_state, NULL), + .buttons_state = sc_mouse_buttons_state_from_sdl(sdl_buttons_state), }; assert(mp->ops->process_mouse_click); @@ -208,8 +207,7 @@ sc_screen_otg_process_mouse_wheel(struct sc_screen_otg *screen, // .position not used for HID events .hscroll = event->x, .vscroll = event->y, - .buttons_state = - sc_mouse_buttons_state_from_sdl(sdl_buttons_state, NULL), + .buttons_state = sc_mouse_buttons_state_from_sdl(sdl_buttons_state), }; assert(mp->ops->process_mouse_scroll); From 9989668226f100534452e0af812807562ff5212f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 9 Jul 2024 20:45:49 +0200 Subject: [PATCH 1865/2244] Add mouse secondary bindings Add secondary bindings (Shift+click) for mouse buttons. In addition to: --mouse-bind=xxxx It is now possible to pass a sequence of secondary bindings: --mouse-bind=xxxx:xxxx <--> <--> primary secondary bindings bindings If the second sequence is omitted, then it is the same as the first one. By default, for SDK mouse, primary bindings trigger shortcuts and secondary bindings forward all clicks. For AOA and UHID, the default bindings are reversed: all clicks are forwarded by default, whereas pressing Shift+click trigger shortcuts. --mouse-bind=bhsn:++++ # default for SDK --mouse-bind=++++:bhsn # default for AOA and UHID Refs 035d60cf5d3f4c83d48735b4cb4cd108a5b5f413 Refs f5e6b8092afd82bab402e7c2c3d00b1719f9bb57 Fixes #5055 PR #5076 --- app/scrcpy.1 | 10 +++- app/src/cli.c | 119 ++++++++++++++++++++++++++++------------ app/src/input_manager.c | 14 +++-- app/src/options.c | 16 ++++-- app/src/options.h | 7 ++- doc/mouse.md | 52 +++++++++++++----- 6 files changed, 156 insertions(+), 62 deletions(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index cf8dfa7f..1c0c0f7a 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -258,10 +258,14 @@ LAlt, LSuper or RSuper toggle the capture mode, to give control of the mouse bac Also see \fB\-\-keyboard\fR. .TP -.BI "\-\-mouse\-bind " xxxx +.BI "\-\-mouse\-bind " xxxx[:xxxx] Configure bindings of secondary clicks. -The argument must be exactly 4 characters, one for each secondary click (in order: right click, middle click, 4th click, 5th click). +The argument must be one or two sequences (separated by ':') of exactly 4 characters, one for each secondary click (in order: right click, middle click, 4th click, 5th click). + +The first sequence defines the primary bindings, used when a mouse button is pressed alone. The second sequence defines the secondary bindings, used when a mouse button is pressed while the Shift key is held. + +If the second sequence of bindings is omitted, then it is the same as the first one. Each character must be one of the following: @@ -272,7 +276,7 @@ Each character must be one of the following: - 's': trigger shortcut APP_SWITCH - 'n': trigger shortcut "expand notification panel" -Default is 'bhsn' for SDK mouse, and '++++' for AOA and UHID. +Default is 'bhsn:++++' for SDK mouse, and '++++:bhsn' for AOA and UHID. .TP diff --git a/app/src/cli.c b/app/src/cli.c index 08a4aa3f..9dd49538 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -493,11 +493,17 @@ static const struct sc_option options[] = { { .longopt_id = OPT_MOUSE_BIND, .longopt = "mouse-bind", - .argdesc = "xxxx", + .argdesc = "xxxx[:xxxx]", .text = "Configure bindings of secondary clicks.\n" - "The argument must be exactly 4 characters, one for each " - "secondary click (in order: right click, middle click, 4th " - "click, 5th click).\n" + "The argument must be one or two sequences (separated by ':') " + "of exactly 4 characters, one for each secondary click (in " + "order: right click, middle click, 4th click, 5th click).\n" + "The first sequence defines the primary bindings, used when a " + "mouse button is pressed alone. The second sequence defines " + "the secondary bindings, used when a mouse button is pressed " + "while the Shift key is held.\n" + "If the second sequence of bindings is omitted, then it is the " + "same as the first one.\n" "Each character must be one of the following:\n" " '+': forward the click to the device\n" " '-': ignore the click\n" @@ -505,7 +511,8 @@ static const struct sc_option options[] = { " 'h': trigger shortcut HOME\n" " 's': trigger shortcut APP_SWITCH\n" " 'n': trigger shortcut \"expand notification panel\"\n" - "Default is 'bhsn' for SDK mouse, and '++++' for AOA and UHID.", + "Default is 'bhsn:++++' for SDK mouse, and '++++:bhsn' for AOA " + "and UHID.", }, { .shortopt = 'n', @@ -2095,24 +2102,46 @@ parse_mouse_binding(char c, enum sc_mouse_binding *b) { } static bool -parse_mouse_bindings(const char *s, struct sc_mouse_bindings *mb) { - if (strlen(s) != 4) { - LOGE("Invalid mouse bindings: '%s' (expected exactly 4 characters from " - "{'+', '-', 'b', 'h', 's', 'n'})", s); +parse_mouse_binding_set(const char *s, struct sc_mouse_binding_set *mbs) { + assert(strlen(s) >= 4); + + if (!parse_mouse_binding(s[0], &mbs->right_click)) { + return false; + } + if (!parse_mouse_binding(s[1], &mbs->middle_click)) { + return false; + } + if (!parse_mouse_binding(s[2], &mbs->click4)) { + return false; + } + if (!parse_mouse_binding(s[3], &mbs->click5)) { return false; } - if (!parse_mouse_binding(s[0], &mb->right_click)) { + return true; +} + +static bool +parse_mouse_bindings(const char *s, struct sc_mouse_bindings *mb) { + size_t len = strlen(s); + // either "xxxx" or "xxxx:xxxx" + if (len != 4 && (len != 9 || s[4] != ':')) { + LOGE("Invalid mouse bindings: '%s' (expected 'xxxx' or 'xxxx:xxxx', " + "with each 'x' being in {'+', '-', 'b', 'h', 's', 'n'})", s); return false; } - if (!parse_mouse_binding(s[1], &mb->middle_click)) { + + if (!parse_mouse_binding_set(s, &mb->pri)) { return false; } - if (!parse_mouse_binding(s[2], &mb->click4)) { - return false; - } - if (!parse_mouse_binding(s[3], &mb->click5)) { - return false; + + if (len == 9) { + if (!parse_mouse_binding_set(s + 5, &mb->sec)) { + return false; + } + } else { + // use the same bindings for Shift+click + mb->sec = mb->pri; } return true; @@ -2408,10 +2437,18 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], LOGW("--forward-all-clicks is deprecated, " "use --mouse-bind=++++ instead."); opts->mouse_bindings = (struct sc_mouse_bindings) { - .right_click = SC_MOUSE_BINDING_CLICK, - .middle_click = SC_MOUSE_BINDING_CLICK, - .click4 = SC_MOUSE_BINDING_CLICK, - .click5 = SC_MOUSE_BINDING_CLICK, + .pri = { + .right_click = SC_MOUSE_BINDING_CLICK, + .middle_click = SC_MOUSE_BINDING_CLICK, + .click4 = SC_MOUSE_BINDING_CLICK, + .click5 = SC_MOUSE_BINDING_CLICK, + }, + .sec = { + .right_click = SC_MOUSE_BINDING_CLICK, + .middle_click = SC_MOUSE_BINDING_CLICK, + .click4 = SC_MOUSE_BINDING_CLICK, + .click5 = SC_MOUSE_BINDING_CLICK, + }, }; break; case OPT_LEGACY_PASTE: @@ -2701,26 +2738,36 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } // If mouse bindings are not explictly set, configure default bindings - if (opts->mouse_bindings.right_click == SC_MOUSE_BINDING_AUTO) { - assert(opts->mouse_bindings.middle_click == SC_MOUSE_BINDING_AUTO); - assert(opts->mouse_bindings.click4 == SC_MOUSE_BINDING_AUTO); - assert(opts->mouse_bindings.click5 == SC_MOUSE_BINDING_AUTO); + if (opts->mouse_bindings.pri.right_click == SC_MOUSE_BINDING_AUTO) { + assert(opts->mouse_bindings.pri.middle_click == SC_MOUSE_BINDING_AUTO); + assert(opts->mouse_bindings.pri.click4 == SC_MOUSE_BINDING_AUTO); + assert(opts->mouse_bindings.pri.click5 == SC_MOUSE_BINDING_AUTO); + assert(opts->mouse_bindings.sec.right_click == SC_MOUSE_BINDING_AUTO); + assert(opts->mouse_bindings.sec.middle_click == SC_MOUSE_BINDING_AUTO); + assert(opts->mouse_bindings.sec.click4 == SC_MOUSE_BINDING_AUTO); + assert(opts->mouse_bindings.sec.click5 == SC_MOUSE_BINDING_AUTO); + + static struct sc_mouse_binding_set default_shortcuts = { + .right_click = SC_MOUSE_BINDING_BACK, + .middle_click = SC_MOUSE_BINDING_HOME, + .click4 = SC_MOUSE_BINDING_APP_SWITCH, + .click5 = SC_MOUSE_BINDING_EXPAND_NOTIFICATION_PANEL, + }; + + static struct sc_mouse_binding_set forward = { + .right_click = SC_MOUSE_BINDING_CLICK, + .middle_click = SC_MOUSE_BINDING_CLICK, + .click4 = SC_MOUSE_BINDING_CLICK, + .click5 = SC_MOUSE_BINDING_CLICK, + }; // By default, forward all clicks only for UHID and AOA if (opts->mouse_input_mode == SC_MOUSE_INPUT_MODE_SDK) { - opts->mouse_bindings = (struct sc_mouse_bindings) { - .right_click = SC_MOUSE_BINDING_BACK, - .middle_click = SC_MOUSE_BINDING_HOME, - .click4 = SC_MOUSE_BINDING_APP_SWITCH, - .click5 = SC_MOUSE_BINDING_EXPAND_NOTIFICATION_PANEL, - }; + opts->mouse_bindings.pri = default_shortcuts; + opts->mouse_bindings.sec = forward; } else { - opts->mouse_bindings = (struct sc_mouse_bindings) { - .right_click = SC_MOUSE_BINDING_CLICK, - .middle_click = SC_MOUSE_BINDING_CLICK, - .click4 = SC_MOUSE_BINDING_CLICK, - .click5 = SC_MOUSE_BINDING_CLICK, - }; + opts->mouse_bindings.pri = forward; + opts->mouse_bindings.sec = default_shortcuts; } } diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 2cc34afa..d3c94d03 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -708,7 +708,7 @@ sc_input_manager_process_touch(struct sc_input_manager *im, } static enum sc_mouse_binding -sc_input_manager_get_binding(const struct sc_mouse_bindings *bindings, +sc_input_manager_get_binding(const struct sc_mouse_binding_set *bindings, uint8_t sdl_button) { switch (sdl_button) { case SDL_BUTTON_LEFT: @@ -744,11 +744,18 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im, im->mouse_buttons_state &= ~button; } + SDL_Keymod keymod = SDL_GetModState(); + bool ctrl_pressed = keymod & KMOD_CTRL; + bool shift_pressed = keymod & KMOD_SHIFT; + if (control && !paused) { enum sc_action action = down ? SC_ACTION_DOWN : SC_ACTION_UP; + struct sc_mouse_binding_set *bindings = !shift_pressed + ? &im->mouse_bindings.pri + : &im->mouse_bindings.sec; enum sc_mouse_binding binding = - sc_input_manager_get_binding(&im->mouse_bindings, event->button); + sc_input_manager_get_binding(bindings, event->button); assert(binding != SC_MOUSE_BINDING_AUTO); switch (binding) { case SC_MOUSE_BINDING_DISABLED: @@ -812,9 +819,6 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im, im->mouse_buttons_state |= button; } - SDL_Keymod keymod = SDL_GetModState(); - bool ctrl_pressed = keymod & KMOD_CTRL; - bool shift_pressed = keymod & KMOD_SHIFT; bool change_vfinger = event->button == SDL_BUTTON_LEFT && ((down && !im->vfinger_down && (ctrl_pressed ^ shift_pressed)) || (!down && im->vfinger_down)); diff --git a/app/src/options.c b/app/src/options.c index 5556d1f9..5eec6427 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -24,10 +24,18 @@ const struct scrcpy_options scrcpy_options_default = { .keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_AUTO, .mouse_input_mode = SC_MOUSE_INPUT_MODE_AUTO, .mouse_bindings = { - .right_click = SC_MOUSE_BINDING_AUTO, - .middle_click = SC_MOUSE_BINDING_AUTO, - .click4 = SC_MOUSE_BINDING_AUTO, - .click5 = SC_MOUSE_BINDING_AUTO, + .pri = { + .right_click = SC_MOUSE_BINDING_AUTO, + .middle_click = SC_MOUSE_BINDING_AUTO, + .click4 = SC_MOUSE_BINDING_AUTO, + .click5 = SC_MOUSE_BINDING_AUTO, + }, + .sec = { + .right_click = SC_MOUSE_BINDING_AUTO, + .middle_click = SC_MOUSE_BINDING_AUTO, + .click4 = SC_MOUSE_BINDING_AUTO, + .click5 = SC_MOUSE_BINDING_AUTO, + }, }, .camera_facing = SC_CAMERA_FACING_ANY, .port_range = { diff --git a/app/src/options.h b/app/src/options.h index f840a989..5ec809f0 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -165,13 +165,18 @@ enum sc_mouse_binding { SC_MOUSE_BINDING_EXPAND_NOTIFICATION_PANEL, }; -struct sc_mouse_bindings { +struct sc_mouse_binding_set { enum sc_mouse_binding right_click; enum sc_mouse_binding middle_click; enum sc_mouse_binding click4; enum sc_mouse_binding click5; }; +struct sc_mouse_bindings { + struct sc_mouse_binding_set pri; + struct sc_mouse_binding_set sec; // When Shift is pressed +}; + enum sc_key_inject_mode { // Inject special keys, letters and space as key events. // Inject numbers and punctuation as text events. diff --git a/doc/mouse.md b/doc/mouse.md index 1c62ddd0..ec4aea63 100644 --- a/doc/mouse.md +++ b/doc/mouse.md @@ -80,21 +80,37 @@ process like the _adb daemon_). ## Mouse bindings -By default, with SDK mouse, right-click triggers BACK (or POWER on) and -middle-click triggers HOME. In addition, the 4th click triggers APP_SWITCH and -the 5th click expands the notification panel. +By default, with SDK mouse: + - right-click triggers BACK (or POWER on) + - middle-click triggers HOME + - the 4th click triggers APP_SWITCH + - the 5th click expands the notification panel -In AOA and UHID mouse modes, all clicks are forwarded by default. +The secondary clicks may be forwarded to the device instead by pressing the +Shift key (e.g. Shift+right-click injects a right click to +the device). -The shortcuts can be configured using `--mouse-bind=xxxx` for any mouse mode. -The argument must be exactly 4 characters, one for each secondary click: +In AOA and UHID mouse modes, the default bindings are reversed: all clicks are +forwarded by default, and pressing Shift gives access to the +shortcuts (since the cursor is handled on the device side, it makes more sense +to forward all mouse buttons by default in these modes). + +The shortcuts can be configured using `--mouse-bind=xxxx:xxxx` for any mouse +mode. The argument must be one or two sequences (separated by `:`) of exactly 4 +characters, one for each secondary click: ``` ---mouse-bind=xxxx + .---- Shift + right click + SECONDARY |.--- Shift + middle click + BINDINGS ||.-- Shift + 4th click + |||.- Shift + 5th click + |||| + vvvv +--mouse-bind=xxxx:xxxx ^^^^ |||| - ||| `- 5th click - || `-- 4th click + PRIMARY ||| `- 5th click + BINDINGS || `-- 4th click | `--- middle click `---- right click ``` @@ -111,8 +127,18 @@ Each character must be one of the following: For example: ```bash -scrcpy --mouse-bind=bhsn # the default mode with SDK mouse -scrcpy --mouse-bind=++++ # forward all clicks (default for AOA/UHID) -scrcpy --mouse-bind=++bh # forward right and middle clicks, - # use 4th and 5th for BACK and HOME +scrcpy --mouse-bind=bhsn:++++ # the default mode for SDK mouse +scrcpy --mouse-bind=++++:bhsn # the default mode for AOA and UHID +scrcpy --mouse-bind=++bh:++sn # forward right and middle clicks, + # use 4th and 5th for BACK and HOME, + # use Shift+4th and Shift+5th for APP_SWITCH + # and expand notification panel +``` + +The second sequence of bindings may be omitted. In that case, it is the same as +the first one: + +```bash +scrcpy --mouse-bind=bhsn +scrcpy --mouse-bind=bhsn:bhsn # equivalent ``` From fe7494c4922b2dd9a8af18842d82af1e5ebf8897 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 11 Jul 2024 12:19:47 +0200 Subject: [PATCH 1866/2244] Linearize try-catch blocks There are many possible method signatures for getPrimaryClip() and setPrimaryClip(). Avoid the nested try-catch blocks. --- .../scrcpy/wrappers/ClipboardManager.java | 127 +++++++++++------- 1 file changed, 76 insertions(+), 51 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 ed5c8d75..bdbba21d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java @@ -38,38 +38,53 @@ public final class ClipboardManager { if (getPrimaryClipMethod == null) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class); - } else { - try { - getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class); - getMethodVersion = 0; - } catch (NoSuchMethodException e1) { - try { - getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class); - getMethodVersion = 1; - } catch (NoSuchMethodException e2) { - try { - getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class, int.class); - getMethodVersion = 2; - } catch (NoSuchMethodException e3) { - try { - getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class, String.class); - getMethodVersion = 3; - } catch (NoSuchMethodException e4) { - try { - getPrimaryClipMethod = manager.getClass() - .getMethod("getPrimaryClip", String.class, String.class, int.class, int.class, boolean.class); - getMethodVersion = 4; - } catch (NoSuchMethodException e5) { - getPrimaryClipMethod = manager.getClass() - .getMethod("getPrimaryClip", String.class, String.class, String.class, String.class, int.class, int.class, - boolean.class); - getMethodVersion = 5; - } - } - } - } - } + return getPrimaryClipMethod; } + + try { + getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class); + getMethodVersion = 0; + return getPrimaryClipMethod; + } catch (NoSuchMethodException e) { + // fall-through + } + + try { + getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class); + getMethodVersion = 1; + return getPrimaryClipMethod; + } catch (NoSuchMethodException e) { + // fall-through + } + + try { + getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class, int.class); + getMethodVersion = 2; + return getPrimaryClipMethod; + } catch (NoSuchMethodException e) { + // fall-through + } + + try { + getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class, String.class); + getMethodVersion = 3; + return getPrimaryClipMethod; + } catch (NoSuchMethodException e) { + // fall-through + } + + try { + getPrimaryClipMethod = manager.getClass() + .getMethod("getPrimaryClip", String.class, String.class, int.class, int.class, boolean.class); + getMethodVersion = 4; + return getPrimaryClipMethod; + } catch (NoSuchMethodException e) { + // fall-through + } + + getPrimaryClipMethod = manager.getClass() + .getMethod("getPrimaryClip", String.class, String.class, String.class, String.class, int.class, int.class, boolean.class); + getMethodVersion = 5; } return getPrimaryClipMethod; } @@ -78,27 +93,37 @@ public final class ClipboardManager { if (setPrimaryClipMethod == null) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class); - } else { - try { - setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, int.class); - setMethodVersion = 0; - } catch (NoSuchMethodException e1) { - try { - setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class); - setMethodVersion = 1; - } catch (NoSuchMethodException e2) { - try { - setPrimaryClipMethod = manager.getClass() - .getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class, int.class); - setMethodVersion = 2; - } catch (NoSuchMethodException e3) { - setPrimaryClipMethod = manager.getClass() - .getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class, int.class, boolean.class); - setMethodVersion = 3; - } - } - } + return setPrimaryClipMethod; } + + try { + setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, int.class); + setMethodVersion = 0; + return setPrimaryClipMethod; + } catch (NoSuchMethodException e1) { + // fall-through + } + + try { + setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class); + setMethodVersion = 1; + return setPrimaryClipMethod; + } catch (NoSuchMethodException e2) { + // fall-through + } + + try { + setPrimaryClipMethod = manager.getClass() + .getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class, int.class); + setMethodVersion = 2; + return setPrimaryClipMethod; + } catch (NoSuchMethodException e3) { + // fall-through + } + + setPrimaryClipMethod = manager.getClass() + .getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class, int.class, boolean.class); + setMethodVersion = 3; } return setPrimaryClipMethod; } From 79242957a0ddba1c326f414cf5eafff6cc7b39c1 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 11 Jul 2024 12:21:38 +0200 Subject: [PATCH 1867/2244] Add clipboard workaround for Honor device Fixes #5073 --- .../scrcpy/wrappers/ClipboardManager.java | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 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 bdbba21d..a0e3a7e1 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java @@ -82,9 +82,17 @@ public final class ClipboardManager { // fall-through } - getPrimaryClipMethod = manager.getClass() - .getMethod("getPrimaryClip", String.class, String.class, String.class, String.class, int.class, int.class, boolean.class); - getMethodVersion = 5; + try { + getPrimaryClipMethod = manager.getClass() + .getMethod("getPrimaryClip", String.class, String.class, String.class, String.class, int.class, int.class, boolean.class); + getMethodVersion = 5; + return getPrimaryClipMethod; + } catch (NoSuchMethodException e) { + // fall-through + } + + getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class, int.class, String.class); + getMethodVersion = 6; } return getPrimaryClipMethod; } @@ -145,8 +153,10 @@ public final class ClipboardManager { case 4: // The last boolean parameter is "userOperate" return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0, true); - default: + case 5: return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, null, null, FakeContext.ROOT_UID, 0, true); + default: + return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0, null); } } From 80ca7b15e5f8df6d6940b0dce779fb73d3bad1c9 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 11 Jul 2024 22:34:58 +0200 Subject: [PATCH 1868/2244] Extract sources paths in build_without_gradle.sh This avoids duplication, and will be useful to add more packages. --- server/build_without_gradle.sh | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index 74bbd8ae..845b0104 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -50,14 +50,24 @@ cd "$SERVER_DIR/src/main/aidl" android/content/IOnPrimaryClipChangedListener.aidl "$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" android/view/IDisplayFoldListener.aidl +SRC=( \ + com/genymobile/scrcpy/*.java \ + com/genymobile/scrcpy/wrappers/*.java \ +) + +CLASSES=() +for src in "${SRC[@]}" +do + CLASSES+=("${src%.java}.class") +done + echo "Compiling java sources..." cd ../java javac -bootclasspath "$ANDROID_JAR" \ -cp "$LAMBDA_JAR:$GEN_DIR" \ -d "$CLASSES_DIR" \ -source 1.8 -target 1.8 \ - com/genymobile/scrcpy/*.java \ - com/genymobile/scrcpy/wrappers/*.java + ${SRC[@]} echo "Dexing..." cd "$CLASSES_DIR" @@ -68,8 +78,7 @@ then "$BUILD_TOOLS_DIR/dx" --dex --output "$BUILD_DIR/classes.dex" \ android/view/*.class \ android/content/*.class \ - com/genymobile/scrcpy/*.class \ - com/genymobile/scrcpy/wrappers/*.class + ${CLASSES[@]} echo "Archiving..." cd "$BUILD_DIR" @@ -81,8 +90,7 @@ else --output "$BUILD_DIR/classes.zip" \ android/view/*.class \ android/content/*.class \ - com/genymobile/scrcpy/*.class \ - com/genymobile/scrcpy/wrappers/*.class + ${CLASSES[@]} cd "$BUILD_DIR" mv classes.zip "$SERVER_BINARY" From e84db2914dfa99ac9be7d789016f08b3e97e089e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 11 Jul 2024 22:38:00 +0200 Subject: [PATCH 1869/2244] Reorganize server packages There are now a lot of classes in the server, reorganize them into subpackages. --- server/build_without_gradle.sh | 5 +++++ .../java/com/genymobile/scrcpy/CleanUp.java | 5 +++++ .../java/com/genymobile/scrcpy/Options.java | 10 +++++++++ .../java/com/genymobile/scrcpy/Server.java | 21 +++++++++++++++++++ .../com/genymobile/scrcpy/Workarounds.java | 2 ++ .../scrcpy/{ => audio}/AudioCapture.java | 5 ++++- .../AudioCaptureForegroundException.java | 2 +- .../scrcpy/{ => audio}/AudioCodec.java | 4 +++- .../scrcpy/{ => audio}/AudioEncoder.java | 12 ++++++++++- .../scrcpy/{ => audio}/AudioRawRecorder.java | 7 ++++++- .../scrcpy/{ => audio}/AudioSource.java | 4 ++-- .../scrcpy/{ => control}/ControlChannel.java | 2 +- .../scrcpy/{ => control}/ControlMessage.java | 4 +++- .../{ => control}/ControlMessageReader.java | 6 +++++- .../scrcpy/{ => control}/Controller.java | 8 ++++++- .../scrcpy/{ => control}/DeviceMessage.java | 2 +- .../{ => control}/DeviceMessageSender.java | 4 +++- .../{ => control}/DeviceMessageWriter.java | 5 ++++- .../scrcpy/{ => control}/KeyComposition.java | 2 +- .../scrcpy/{ => control}/Pointer.java | 4 +++- .../scrcpy/{ => control}/PointersState.java | 4 +++- .../scrcpy/{ => control}/UhidManager.java | 4 +++- .../{ => device}/ConfigurationException.java | 2 +- .../{ => device}/DesktopConnection.java | 6 +++++- .../scrcpy/{ => device}/Device.java | 6 +++++- .../scrcpy/{ => device}/DisplayInfo.java | 2 +- .../genymobile/scrcpy/{ => device}/Point.java | 2 +- .../scrcpy/{ => device}/Position.java | 2 +- .../genymobile/scrcpy/{ => device}/Size.java | 2 +- .../scrcpy/{ => device}/Streamer.java | 6 +++++- .../genymobile/scrcpy/{ => util}/Binary.java | 2 +- .../genymobile/scrcpy/{ => util}/Codec.java | 2 +- .../scrcpy/{ => util}/CodecOption.java | 2 +- .../scrcpy/{ => util}/CodecUtils.java | 5 ++++- .../genymobile/scrcpy/{ => util}/Command.java | 2 +- .../scrcpy/{ => util}/HandlerExecutor.java | 2 +- .../com/genymobile/scrcpy/{ => util}/IO.java | 4 +++- .../com/genymobile/scrcpy/{ => util}/Ln.java | 4 ++-- .../scrcpy/{ => util}/LogUtils.java | 4 +++- .../scrcpy/{ => util}/Settings.java | 2 +- .../scrcpy/{ => util}/SettingsException.java | 2 +- .../scrcpy/{ => util}/StringUtils.java | 2 +- .../scrcpy/{ => video}/CameraAspectRatio.java | 2 +- .../scrcpy/{ => video}/CameraCapture.java | 5 ++++- .../scrcpy/{ => video}/CameraFacing.java | 4 ++-- .../scrcpy/{ => video}/ScreenCapture.java | 5 ++++- .../scrcpy/{ => video}/ScreenInfo.java | 7 ++++++- .../scrcpy/{ => video}/SurfaceCapture.java | 4 +++- .../scrcpy/{ => video}/SurfaceEncoder.java | 13 +++++++++++- .../scrcpy/{ => video}/VideoCodec.java | 4 +++- .../scrcpy/{ => video}/VideoSource.java | 4 ++-- .../scrcpy/wrappers/ActivityManager.java | 2 +- .../scrcpy/wrappers/ClipboardManager.java | 2 +- .../scrcpy/wrappers/ContentProvider.java | 4 ++-- .../scrcpy/wrappers/DisplayControl.java | 2 +- .../scrcpy/wrappers/DisplayManager.java | 8 +++---- .../scrcpy/wrappers/InputManager.java | 2 +- .../scrcpy/wrappers/PowerManager.java | 2 +- .../scrcpy/wrappers/StatusBarManager.java | 2 +- .../scrcpy/wrappers/SurfaceControl.java | 2 +- .../scrcpy/wrappers/WindowManager.java | 2 +- .../ControlMessageReaderTest.java | 4 +++- .../DeviceMessageWriterTest.java | 2 +- .../scrcpy/{ => util}/BinaryTest.java | 2 +- .../scrcpy/{ => util}/CodecOptionsTest.java | 2 +- .../scrcpy/{ => util}/CommandParserTest.java | 3 ++- .../scrcpy/{ => util}/StringUtilsTest.java | 2 +- 67 files changed, 204 insertions(+), 70 deletions(-) rename server/src/main/java/com/genymobile/scrcpy/{ => audio}/AudioCapture.java (97%) rename server/src/main/java/com/genymobile/scrcpy/{ => audio}/AudioCaptureForegroundException.java (84%) rename server/src/main/java/com/genymobile/scrcpy/{ => audio}/AudioCodec.java (93%) rename server/src/main/java/com/genymobile/scrcpy/{ => audio}/AudioEncoder.java (96%) rename server/src/main/java/com/genymobile/scrcpy/{ => audio}/AudioRawRecorder.java (93%) rename server/src/main/java/com/genymobile/scrcpy/{ => audio}/AudioSource.java (86%) rename server/src/main/java/com/genymobile/scrcpy/{ => control}/ControlChannel.java (96%) rename server/src/main/java/com/genymobile/scrcpy/{ => control}/ControlMessage.java (98%) rename server/src/main/java/com/genymobile/scrcpy/{ => control}/ControlMessageReader.java (98%) rename server/src/main/java/com/genymobile/scrcpy/{ => control}/Controller.java (98%) rename server/src/main/java/com/genymobile/scrcpy/{ => control}/DeviceMessage.java (97%) rename server/src/main/java/com/genymobile/scrcpy/{ => control}/DeviceMessageSender.java (94%) rename server/src/main/java/com/genymobile/scrcpy/{ => control}/DeviceMessageWriter.java (93%) rename server/src/main/java/com/genymobile/scrcpy/{ => control}/KeyComposition.java (99%) rename server/src/main/java/com/genymobile/scrcpy/{ => control}/Pointer.java (92%) rename server/src/main/java/com/genymobile/scrcpy/{ => control}/PointersState.java (97%) rename server/src/main/java/com/genymobile/scrcpy/{ => control}/UhidManager.java (98%) rename server/src/main/java/com/genymobile/scrcpy/{ => device}/ConfigurationException.java (78%) rename server/src/main/java/com/genymobile/scrcpy/{ => device}/DesktopConnection.java (97%) rename server/src/main/java/com/genymobile/scrcpy/{ => device}/Device.java (98%) rename server/src/main/java/com/genymobile/scrcpy/{ => device}/DisplayInfo.java (95%) rename server/src/main/java/com/genymobile/scrcpy/{ => device}/Point.java (95%) rename server/src/main/java/com/genymobile/scrcpy/{ => device}/Position.java (97%) rename server/src/main/java/com/genymobile/scrcpy/{ => device}/Size.java (96%) rename server/src/main/java/com/genymobile/scrcpy/{ => device}/Streamer.java (97%) rename server/src/main/java/com/genymobile/scrcpy/{ => util}/Binary.java (96%) rename server/src/main/java/com/genymobile/scrcpy/{ => util}/Codec.java (82%) rename server/src/main/java/com/genymobile/scrcpy/{ => util}/CodecOption.java (98%) rename server/src/main/java/com/genymobile/scrcpy/{ => util}/CodecUtils.java (95%) rename server/src/main/java/com/genymobile/scrcpy/{ => util}/Command.java (97%) rename server/src/main/java/com/genymobile/scrcpy/{ => util}/HandlerExecutor.java (93%) rename server/src/main/java/com/genymobile/scrcpy/{ => util}/IO.java (96%) rename server/src/main/java/com/genymobile/scrcpy/{ => util}/Ln.java (98%) rename server/src/main/java/com/genymobile/scrcpy/{ => util}/LogUtils.java (98%) rename server/src/main/java/com/genymobile/scrcpy/{ => util}/Settings.java (98%) rename server/src/main/java/com/genymobile/scrcpy/{ => util}/SettingsException.java (92%) rename server/src/main/java/com/genymobile/scrcpy/{ => util}/StringUtils.java (94%) rename server/src/main/java/com/genymobile/scrcpy/{ => video}/CameraAspectRatio.java (96%) rename server/src/main/java/com/genymobile/scrcpy/{ => video}/CameraCapture.java (98%) rename server/src/main/java/com/genymobile/scrcpy/{ => video}/CameraFacing.java (89%) rename server/src/main/java/com/genymobile/scrcpy/{ => video}/ScreenCapture.java (96%) rename server/src/main/java/com/genymobile/scrcpy/{ => video}/ScreenInfo.java (96%) rename server/src/main/java/com/genymobile/scrcpy/{ => video}/SurfaceCapture.java (95%) rename server/src/main/java/com/genymobile/scrcpy/{ => video}/SurfaceEncoder.java (95%) rename server/src/main/java/com/genymobile/scrcpy/{ => video}/VideoCodec.java (93%) rename server/src/main/java/com/genymobile/scrcpy/{ => video}/VideoSource.java (80%) rename server/src/test/java/com/genymobile/scrcpy/{ => control}/ControlMessageReaderTest.java (99%) rename server/src/test/java/com/genymobile/scrcpy/{ => control}/DeviceMessageWriterTest.java (98%) rename server/src/test/java/com/genymobile/scrcpy/{ => util}/BinaryTest.java (98%) rename server/src/test/java/com/genymobile/scrcpy/{ => util}/CodecOptionsTest.java (99%) rename server/src/test/java/com/genymobile/scrcpy/{ => util}/CommandParserTest.java (99%) rename server/src/test/java/com/genymobile/scrcpy/{ => util}/StringUtilsTest.java (97%) diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index 845b0104..6dc547d9 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -52,6 +52,11 @@ cd "$SERVER_DIR/src/main/aidl" SRC=( \ com/genymobile/scrcpy/*.java \ + com/genymobile/scrcpy/audio/*.java \ + com/genymobile/scrcpy/control/*.java \ + com/genymobile/scrcpy/device/*.java \ + com/genymobile/scrcpy/util/*.java \ + com/genymobile/scrcpy/video/*.java \ com/genymobile/scrcpy/wrappers/*.java \ ) diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java index f9b1efd6..1b8d4248 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java +++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java @@ -1,5 +1,10 @@ package com.genymobile.scrcpy; +import com.genymobile.scrcpy.device.Device; +import com.genymobile.scrcpy.util.Ln; +import com.genymobile.scrcpy.util.Settings; +import com.genymobile.scrcpy.util.SettingsException; + import java.io.File; import java.io.IOException; import java.io.OutputStream; diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 9b1d8d8d..143fbb9a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -1,5 +1,15 @@ package com.genymobile.scrcpy; +import com.genymobile.scrcpy.audio.AudioCodec; +import com.genymobile.scrcpy.audio.AudioSource; +import com.genymobile.scrcpy.device.Size; +import com.genymobile.scrcpy.util.CodecOption; +import com.genymobile.scrcpy.util.Ln; +import com.genymobile.scrcpy.video.CameraAspectRatio; +import com.genymobile.scrcpy.video.CameraFacing; +import com.genymobile.scrcpy.video.VideoCodec; +import com.genymobile.scrcpy.video.VideoSource; + import android.graphics.Rect; import java.util.List; diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 587a46df..263c784d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -1,5 +1,26 @@ package com.genymobile.scrcpy; +import com.genymobile.scrcpy.audio.AudioCapture; +import com.genymobile.scrcpy.audio.AudioCodec; +import com.genymobile.scrcpy.audio.AudioEncoder; +import com.genymobile.scrcpy.audio.AudioRawRecorder; +import com.genymobile.scrcpy.control.ControlChannel; +import com.genymobile.scrcpy.control.Controller; +import com.genymobile.scrcpy.control.DeviceMessage; +import com.genymobile.scrcpy.device.ConfigurationException; +import com.genymobile.scrcpy.device.DesktopConnection; +import com.genymobile.scrcpy.device.Device; +import com.genymobile.scrcpy.device.Streamer; +import com.genymobile.scrcpy.util.Ln; +import com.genymobile.scrcpy.util.LogUtils; +import com.genymobile.scrcpy.util.Settings; +import com.genymobile.scrcpy.util.SettingsException; +import com.genymobile.scrcpy.video.CameraCapture; +import com.genymobile.scrcpy.video.ScreenCapture; +import com.genymobile.scrcpy.video.SurfaceCapture; +import com.genymobile.scrcpy.video.SurfaceEncoder; +import com.genymobile.scrcpy.video.VideoSource; + import android.os.BatteryManager; import android.os.Build; diff --git a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java index c9a26d78..8b8b4233 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java +++ b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java @@ -1,5 +1,7 @@ package com.genymobile.scrcpy; +import com.genymobile.scrcpy.util.Ln; + import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.app.Application; diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java similarity index 97% rename from server/src/main/java/com/genymobile/scrcpy/AudioCapture.java rename to server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java index 3934ad49..414bfa5d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java @@ -1,5 +1,8 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.audio; +import com.genymobile.scrcpy.FakeContext; +import com.genymobile.scrcpy.util.Ln; +import com.genymobile.scrcpy.Workarounds; import com.genymobile.scrcpy.wrappers.ServiceManager; import android.annotation.SuppressLint; diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioCaptureForegroundException.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioCaptureForegroundException.java similarity index 84% rename from server/src/main/java/com/genymobile/scrcpy/AudioCaptureForegroundException.java rename to server/src/main/java/com/genymobile/scrcpy/audio/AudioCaptureForegroundException.java index baa7d846..49cfe70f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioCaptureForegroundException.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioCaptureForegroundException.java @@ -1,4 +1,4 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.audio; /** * Exception thrown if audio capture failed on Android 11 specifically because the running App (shell) was not in foreground. diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioCodec.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioCodec.java similarity index 93% rename from server/src/main/java/com/genymobile/scrcpy/AudioCodec.java rename to server/src/main/java/com/genymobile/scrcpy/audio/AudioCodec.java index b4ea3680..8f9e59b3 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioCodec.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioCodec.java @@ -1,4 +1,6 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.audio; + +import com.genymobile.scrcpy.util.Codec; import android.media.MediaFormat; diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java similarity index 96% rename from server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java rename to server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java index 0b59369b..78b6de55 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java @@ -1,4 +1,14 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.audio; + +import com.genymobile.scrcpy.AsyncProcessor; +import com.genymobile.scrcpy.util.Codec; +import com.genymobile.scrcpy.util.CodecOption; +import com.genymobile.scrcpy.util.CodecUtils; +import com.genymobile.scrcpy.device.ConfigurationException; +import com.genymobile.scrcpy.util.IO; +import com.genymobile.scrcpy.util.Ln; +import com.genymobile.scrcpy.util.LogUtils; +import com.genymobile.scrcpy.device.Streamer; import android.annotation.TargetApi; import android.media.MediaCodec; diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioRawRecorder.java similarity index 93% rename from server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java rename to server/src/main/java/com/genymobile/scrcpy/audio/AudioRawRecorder.java index 7e052f32..72527600 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioRawRecorder.java @@ -1,4 +1,9 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.audio; + +import com.genymobile.scrcpy.AsyncProcessor; +import com.genymobile.scrcpy.util.IO; +import com.genymobile.scrcpy.util.Ln; +import com.genymobile.scrcpy.device.Streamer; import android.media.MediaCodec; import android.os.Build; diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioSource.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioSource.java similarity index 86% rename from server/src/main/java/com/genymobile/scrcpy/AudioSource.java rename to server/src/main/java/com/genymobile/scrcpy/audio/AudioSource.java index 466ea297..2324f1a4 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioSource.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioSource.java @@ -1,4 +1,4 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.audio; import android.media.MediaRecorder; @@ -18,7 +18,7 @@ public enum AudioSource { return value; } - static AudioSource findByName(String name) { + public static AudioSource findByName(String name) { for (AudioSource audioSource : AudioSource.values()) { if (name.equals(audioSource.name)) { return audioSource; diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlChannel.java b/server/src/main/java/com/genymobile/scrcpy/control/ControlChannel.java similarity index 96% rename from server/src/main/java/com/genymobile/scrcpy/ControlChannel.java rename to server/src/main/java/com/genymobile/scrcpy/control/ControlChannel.java index 4677cfda..f24ca117 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlChannel.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlChannel.java @@ -1,4 +1,4 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.control; import android.net.LocalSocket; diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java similarity index 98% rename from server/src/main/java/com/genymobile/scrcpy/ControlMessage.java rename to server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java index bcbacb4b..c414f2a5 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java @@ -1,4 +1,6 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.control; + +import com.genymobile.scrcpy.device.Position; /** * Union of all supported event types, identified by their {@code type}. diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java similarity index 98% rename from server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java rename to server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java index 1761d228..f5cfee75 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java @@ -1,4 +1,8 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.control; + +import com.genymobile.scrcpy.util.Binary; +import com.genymobile.scrcpy.util.Ln; +import com.genymobile.scrcpy.device.Position; import java.io.EOFException; import java.io.IOException; diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java similarity index 98% rename from server/src/main/java/com/genymobile/scrcpy/Controller.java rename to server/src/main/java/com/genymobile/scrcpy/control/Controller.java index b7d2f93e..85425113 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -1,5 +1,11 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.control; +import com.genymobile.scrcpy.AsyncProcessor; +import com.genymobile.scrcpy.CleanUp; +import com.genymobile.scrcpy.device.Device; +import com.genymobile.scrcpy.util.Ln; +import com.genymobile.scrcpy.device.Point; +import com.genymobile.scrcpy.device.Position; import com.genymobile.scrcpy.wrappers.InputManager; import com.genymobile.scrcpy.wrappers.ServiceManager; diff --git a/server/src/main/java/com/genymobile/scrcpy/DeviceMessage.java b/server/src/main/java/com/genymobile/scrcpy/control/DeviceMessage.java similarity index 97% rename from server/src/main/java/com/genymobile/scrcpy/DeviceMessage.java rename to server/src/main/java/com/genymobile/scrcpy/control/DeviceMessage.java index a8987eb6..079a7a04 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DeviceMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/DeviceMessage.java @@ -1,4 +1,4 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.control; public final class DeviceMessage { diff --git a/server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java b/server/src/main/java/com/genymobile/scrcpy/control/DeviceMessageSender.java similarity index 94% rename from server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java rename to server/src/main/java/com/genymobile/scrcpy/control/DeviceMessageSender.java index af14bb4e..dc5e6be0 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/DeviceMessageSender.java @@ -1,4 +1,6 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.control; + +import com.genymobile.scrcpy.util.Ln; import java.io.IOException; import java.util.concurrent.ArrayBlockingQueue; diff --git a/server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java b/server/src/main/java/com/genymobile/scrcpy/control/DeviceMessageWriter.java similarity index 93% rename from server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java rename to server/src/main/java/com/genymobile/scrcpy/control/DeviceMessageWriter.java index f5d57c98..6bf53bed 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/DeviceMessageWriter.java @@ -1,4 +1,7 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.control; + +import com.genymobile.scrcpy.util.Ln; +import com.genymobile.scrcpy.util.StringUtils; import java.io.IOException; import java.io.OutputStream; diff --git a/server/src/main/java/com/genymobile/scrcpy/KeyComposition.java b/server/src/main/java/com/genymobile/scrcpy/control/KeyComposition.java similarity index 99% rename from server/src/main/java/com/genymobile/scrcpy/KeyComposition.java rename to server/src/main/java/com/genymobile/scrcpy/control/KeyComposition.java index 2f2835c9..5b988f53 100644 --- a/server/src/main/java/com/genymobile/scrcpy/KeyComposition.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/KeyComposition.java @@ -1,4 +1,4 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.control; import java.util.HashMap; import java.util.Map; diff --git a/server/src/main/java/com/genymobile/scrcpy/Pointer.java b/server/src/main/java/com/genymobile/scrcpy/control/Pointer.java similarity index 92% rename from server/src/main/java/com/genymobile/scrcpy/Pointer.java rename to server/src/main/java/com/genymobile/scrcpy/control/Pointer.java index b89cc256..02e33e10 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Pointer.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Pointer.java @@ -1,4 +1,6 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.control; + +import com.genymobile.scrcpy.device.Point; public class Pointer { diff --git a/server/src/main/java/com/genymobile/scrcpy/PointersState.java b/server/src/main/java/com/genymobile/scrcpy/control/PointersState.java similarity index 97% rename from server/src/main/java/com/genymobile/scrcpy/PointersState.java rename to server/src/main/java/com/genymobile/scrcpy/control/PointersState.java index d8daaff2..a12da71d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/PointersState.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/PointersState.java @@ -1,4 +1,6 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.control; + +import com.genymobile.scrcpy.device.Point; import android.view.MotionEvent; diff --git a/server/src/main/java/com/genymobile/scrcpy/UhidManager.java b/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java similarity index 98% rename from server/src/main/java/com/genymobile/scrcpy/UhidManager.java rename to server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java index a39288a5..b1e6a9b9 100644 --- a/server/src/main/java/com/genymobile/scrcpy/UhidManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java @@ -1,4 +1,6 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.control; + +import com.genymobile.scrcpy.util.Ln; import android.os.Build; import android.os.HandlerThread; diff --git a/server/src/main/java/com/genymobile/scrcpy/ConfigurationException.java b/server/src/main/java/com/genymobile/scrcpy/device/ConfigurationException.java similarity index 78% rename from server/src/main/java/com/genymobile/scrcpy/ConfigurationException.java rename to server/src/main/java/com/genymobile/scrcpy/device/ConfigurationException.java index 76c8f52e..17729342 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ConfigurationException.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/ConfigurationException.java @@ -1,4 +1,4 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.device; public class ConfigurationException extends Exception { public ConfigurationException(String message) { diff --git a/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java b/server/src/main/java/com/genymobile/scrcpy/device/DesktopConnection.java similarity index 97% rename from server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java rename to server/src/main/java/com/genymobile/scrcpy/device/DesktopConnection.java index d693ad61..db75aec6 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/DesktopConnection.java @@ -1,4 +1,8 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.device; + +import com.genymobile.scrcpy.control.ControlChannel; +import com.genymobile.scrcpy.util.IO; +import com.genymobile.scrcpy.util.StringUtils; import android.net.LocalServerSocket; import android.net.LocalSocket; diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/device/Device.java similarity index 98% rename from server/src/main/java/com/genymobile/scrcpy/Device.java rename to server/src/main/java/com/genymobile/scrcpy/device/Device.java index 8d0ee231..ae4f50e5 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Device.java @@ -1,5 +1,9 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.device; +import com.genymobile.scrcpy.Options; +import com.genymobile.scrcpy.util.Ln; +import com.genymobile.scrcpy.util.LogUtils; +import com.genymobile.scrcpy.video.ScreenInfo; import com.genymobile.scrcpy.wrappers.ClipboardManager; import com.genymobile.scrcpy.wrappers.DisplayControl; import com.genymobile.scrcpy.wrappers.InputManager; diff --git a/server/src/main/java/com/genymobile/scrcpy/DisplayInfo.java b/server/src/main/java/com/genymobile/scrcpy/device/DisplayInfo.java similarity index 95% rename from server/src/main/java/com/genymobile/scrcpy/DisplayInfo.java rename to server/src/main/java/com/genymobile/scrcpy/device/DisplayInfo.java index 4b8036f8..2973710d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DisplayInfo.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/DisplayInfo.java @@ -1,4 +1,4 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.device; public final class DisplayInfo { private final int displayId; diff --git a/server/src/main/java/com/genymobile/scrcpy/Point.java b/server/src/main/java/com/genymobile/scrcpy/device/Point.java similarity index 95% rename from server/src/main/java/com/genymobile/scrcpy/Point.java rename to server/src/main/java/com/genymobile/scrcpy/device/Point.java index c2a30fa8..361b9958 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Point.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Point.java @@ -1,4 +1,4 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.device; import java.util.Objects; diff --git a/server/src/main/java/com/genymobile/scrcpy/Position.java b/server/src/main/java/com/genymobile/scrcpy/device/Position.java similarity index 97% rename from server/src/main/java/com/genymobile/scrcpy/Position.java rename to server/src/main/java/com/genymobile/scrcpy/device/Position.java index 2d298645..7ce4e256 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Position.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Position.java @@ -1,4 +1,4 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.device; import java.util.Objects; diff --git a/server/src/main/java/com/genymobile/scrcpy/Size.java b/server/src/main/java/com/genymobile/scrcpy/device/Size.java similarity index 96% rename from server/src/main/java/com/genymobile/scrcpy/Size.java rename to server/src/main/java/com/genymobile/scrcpy/device/Size.java index fd4b6971..bc9dce1c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Size.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Size.java @@ -1,4 +1,4 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.device; import android.graphics.Rect; diff --git a/server/src/main/java/com/genymobile/scrcpy/Streamer.java b/server/src/main/java/com/genymobile/scrcpy/device/Streamer.java similarity index 97% rename from server/src/main/java/com/genymobile/scrcpy/Streamer.java rename to server/src/main/java/com/genymobile/scrcpy/device/Streamer.java index 8b6c9dcc..f54d0567 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Streamer.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Streamer.java @@ -1,4 +1,8 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.device; + +import com.genymobile.scrcpy.audio.AudioCodec; +import com.genymobile.scrcpy.util.Codec; +import com.genymobile.scrcpy.util.IO; import android.media.MediaCodec; diff --git a/server/src/main/java/com/genymobile/scrcpy/Binary.java b/server/src/main/java/com/genymobile/scrcpy/util/Binary.java similarity index 96% rename from server/src/main/java/com/genymobile/scrcpy/Binary.java rename to server/src/main/java/com/genymobile/scrcpy/util/Binary.java index 29534f59..f46ba695 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Binary.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/Binary.java @@ -1,4 +1,4 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.util; public final class Binary { private Binary() { diff --git a/server/src/main/java/com/genymobile/scrcpy/Codec.java b/server/src/main/java/com/genymobile/scrcpy/util/Codec.java similarity index 82% rename from server/src/main/java/com/genymobile/scrcpy/Codec.java rename to server/src/main/java/com/genymobile/scrcpy/util/Codec.java index 7e905af3..a363bd8b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Codec.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/Codec.java @@ -1,4 +1,4 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.util; public interface Codec { diff --git a/server/src/main/java/com/genymobile/scrcpy/CodecOption.java b/server/src/main/java/com/genymobile/scrcpy/util/CodecOption.java similarity index 98% rename from server/src/main/java/com/genymobile/scrcpy/CodecOption.java rename to server/src/main/java/com/genymobile/scrcpy/util/CodecOption.java index 22c45a90..bed2be9a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CodecOption.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/CodecOption.java @@ -1,4 +1,4 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.util; import java.util.ArrayList; import java.util.List; diff --git a/server/src/main/java/com/genymobile/scrcpy/CodecUtils.java b/server/src/main/java/com/genymobile/scrcpy/util/CodecUtils.java similarity index 95% rename from server/src/main/java/com/genymobile/scrcpy/CodecUtils.java rename to server/src/main/java/com/genymobile/scrcpy/util/CodecUtils.java index afb6f904..5b0c95e8 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CodecUtils.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/CodecUtils.java @@ -1,4 +1,7 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.util; + +import com.genymobile.scrcpy.audio.AudioCodec; +import com.genymobile.scrcpy.video.VideoCodec; import android.media.MediaCodecInfo; import android.media.MediaCodecList; diff --git a/server/src/main/java/com/genymobile/scrcpy/Command.java b/server/src/main/java/com/genymobile/scrcpy/util/Command.java similarity index 97% rename from server/src/main/java/com/genymobile/scrcpy/Command.java rename to server/src/main/java/com/genymobile/scrcpy/util/Command.java index 362504ff..b26158e6 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Command.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/Command.java @@ -1,4 +1,4 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.util; import java.io.IOException; import java.util.Arrays; diff --git a/server/src/main/java/com/genymobile/scrcpy/HandlerExecutor.java b/server/src/main/java/com/genymobile/scrcpy/util/HandlerExecutor.java similarity index 93% rename from server/src/main/java/com/genymobile/scrcpy/HandlerExecutor.java rename to server/src/main/java/com/genymobile/scrcpy/util/HandlerExecutor.java index 1f5f0a4f..03309989 100644 --- a/server/src/main/java/com/genymobile/scrcpy/HandlerExecutor.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/HandlerExecutor.java @@ -1,4 +1,4 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.util; import android.os.Handler; diff --git a/server/src/main/java/com/genymobile/scrcpy/IO.java b/server/src/main/java/com/genymobile/scrcpy/util/IO.java similarity index 96% rename from server/src/main/java/com/genymobile/scrcpy/IO.java rename to server/src/main/java/com/genymobile/scrcpy/util/IO.java index 4a55c152..ab3fa59f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/IO.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/IO.java @@ -1,4 +1,6 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.util; + +import com.genymobile.scrcpy.BuildConfig; import android.system.ErrnoException; import android.system.Os; diff --git a/server/src/main/java/com/genymobile/scrcpy/Ln.java b/server/src/main/java/com/genymobile/scrcpy/util/Ln.java similarity index 98% rename from server/src/main/java/com/genymobile/scrcpy/Ln.java rename to server/src/main/java/com/genymobile/scrcpy/util/Ln.java index cdd57b9f..c0700125 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Ln.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/Ln.java @@ -1,4 +1,4 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.util; import android.util.Log; @@ -19,7 +19,7 @@ public final class Ln { private static final PrintStream CONSOLE_OUT = new PrintStream(new FileOutputStream(FileDescriptor.out)); private static final PrintStream CONSOLE_ERR = new PrintStream(new FileOutputStream(FileDescriptor.err)); - enum Level { + public enum Level { VERBOSE, DEBUG, INFO, WARN, ERROR } diff --git a/server/src/main/java/com/genymobile/scrcpy/LogUtils.java b/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java similarity index 98% rename from server/src/main/java/com/genymobile/scrcpy/LogUtils.java rename to server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java index 1ffb19d3..aee1594a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/LogUtils.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java @@ -1,5 +1,7 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.util; +import com.genymobile.scrcpy.device.DisplayInfo; +import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.wrappers.DisplayManager; import com.genymobile.scrcpy.wrappers.ServiceManager; diff --git a/server/src/main/java/com/genymobile/scrcpy/Settings.java b/server/src/main/java/com/genymobile/scrcpy/util/Settings.java similarity index 98% rename from server/src/main/java/com/genymobile/scrcpy/Settings.java rename to server/src/main/java/com/genymobile/scrcpy/util/Settings.java index 1b5e5f98..d9e82d62 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Settings.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/Settings.java @@ -1,4 +1,4 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.util; import com.genymobile.scrcpy.wrappers.ContentProvider; import com.genymobile.scrcpy.wrappers.ServiceManager; diff --git a/server/src/main/java/com/genymobile/scrcpy/SettingsException.java b/server/src/main/java/com/genymobile/scrcpy/util/SettingsException.java similarity index 92% rename from server/src/main/java/com/genymobile/scrcpy/SettingsException.java rename to server/src/main/java/com/genymobile/scrcpy/util/SettingsException.java index 36ef63ee..87fa3884 100644 --- a/server/src/main/java/com/genymobile/scrcpy/SettingsException.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/SettingsException.java @@ -1,4 +1,4 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.util; public class SettingsException extends Exception { private static String createMessage(String method, String table, String key, String value) { diff --git a/server/src/main/java/com/genymobile/scrcpy/StringUtils.java b/server/src/main/java/com/genymobile/scrcpy/util/StringUtils.java similarity index 94% rename from server/src/main/java/com/genymobile/scrcpy/StringUtils.java rename to server/src/main/java/com/genymobile/scrcpy/util/StringUtils.java index dac05466..8b19ca3d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/StringUtils.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/StringUtils.java @@ -1,4 +1,4 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.util; public final class StringUtils { private StringUtils() { diff --git a/server/src/main/java/com/genymobile/scrcpy/CameraAspectRatio.java b/server/src/main/java/com/genymobile/scrcpy/video/CameraAspectRatio.java similarity index 96% rename from server/src/main/java/com/genymobile/scrcpy/CameraAspectRatio.java rename to server/src/main/java/com/genymobile/scrcpy/video/CameraAspectRatio.java index 4fdf4c74..bf1cba5d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CameraAspectRatio.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/CameraAspectRatio.java @@ -1,4 +1,4 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.video; public final class CameraAspectRatio { private static final float SENSOR = -1; diff --git a/server/src/main/java/com/genymobile/scrcpy/CameraCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java similarity index 98% rename from server/src/main/java/com/genymobile/scrcpy/CameraCapture.java rename to server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java index df3cf7c4..7d2e2055 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CameraCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java @@ -1,5 +1,8 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.video; +import com.genymobile.scrcpy.util.HandlerExecutor; +import com.genymobile.scrcpy.util.Ln; +import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.wrappers.ServiceManager; import android.annotation.SuppressLint; diff --git a/server/src/main/java/com/genymobile/scrcpy/CameraFacing.java b/server/src/main/java/com/genymobile/scrcpy/video/CameraFacing.java similarity index 89% rename from server/src/main/java/com/genymobile/scrcpy/CameraFacing.java rename to server/src/main/java/com/genymobile/scrcpy/video/CameraFacing.java index b7e8daa5..f818e665 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CameraFacing.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/CameraFacing.java @@ -1,4 +1,4 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.video; import android.annotation.SuppressLint; import android.hardware.camera2.CameraCharacteristics; @@ -21,7 +21,7 @@ public enum CameraFacing { return value; } - static CameraFacing findByName(String name) { + public static CameraFacing findByName(String name) { for (CameraFacing facing : CameraFacing.values()) { if (name.equals(facing.name)) { return facing; diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java similarity index 96% rename from server/src/main/java/com/genymobile/scrcpy/ScreenCapture.java rename to server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java index 090c96f0..fbeca2af 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java @@ -1,5 +1,8 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.video; +import com.genymobile.scrcpy.device.Device; +import com.genymobile.scrcpy.util.Ln; +import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.wrappers.ServiceManager; import com.genymobile.scrcpy.wrappers.SurfaceControl; diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java similarity index 96% rename from server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java rename to server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java index 8e5b401f..ba537b17 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java @@ -1,4 +1,9 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.video; + +import com.genymobile.scrcpy.BuildConfig; +import com.genymobile.scrcpy.device.Device; +import com.genymobile.scrcpy.util.Ln; +import com.genymobile.scrcpy.device.Size; import android.graphics.Rect; diff --git a/server/src/main/java/com/genymobile/scrcpy/SurfaceCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java similarity index 95% rename from server/src/main/java/com/genymobile/scrcpy/SurfaceCapture.java rename to server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java index e300e4d6..3118ddc8 100644 --- a/server/src/main/java/com/genymobile/scrcpy/SurfaceCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java @@ -1,4 +1,6 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.video; + +import com.genymobile.scrcpy.device.Size; import android.view.Surface; diff --git a/server/src/main/java/com/genymobile/scrcpy/SurfaceEncoder.java b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java similarity index 95% rename from server/src/main/java/com/genymobile/scrcpy/SurfaceEncoder.java rename to server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java index 4a0fdf4e..8fe0b227 100644 --- a/server/src/main/java/com/genymobile/scrcpy/SurfaceEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java @@ -1,4 +1,15 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.video; + +import com.genymobile.scrcpy.AsyncProcessor; +import com.genymobile.scrcpy.util.Codec; +import com.genymobile.scrcpy.util.CodecOption; +import com.genymobile.scrcpy.util.CodecUtils; +import com.genymobile.scrcpy.device.ConfigurationException; +import com.genymobile.scrcpy.util.IO; +import com.genymobile.scrcpy.util.Ln; +import com.genymobile.scrcpy.util.LogUtils; +import com.genymobile.scrcpy.device.Size; +import com.genymobile.scrcpy.device.Streamer; import android.media.MediaCodec; import android.media.MediaCodecInfo; diff --git a/server/src/main/java/com/genymobile/scrcpy/VideoCodec.java b/server/src/main/java/com/genymobile/scrcpy/video/VideoCodec.java similarity index 93% rename from server/src/main/java/com/genymobile/scrcpy/VideoCodec.java rename to server/src/main/java/com/genymobile/scrcpy/video/VideoCodec.java index fa787a99..5d528da1 100644 --- a/server/src/main/java/com/genymobile/scrcpy/VideoCodec.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/VideoCodec.java @@ -1,4 +1,6 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.video; + +import com.genymobile.scrcpy.util.Codec; import android.annotation.SuppressLint; import android.media.MediaFormat; diff --git a/server/src/main/java/com/genymobile/scrcpy/VideoSource.java b/server/src/main/java/com/genymobile/scrcpy/video/VideoSource.java similarity index 80% rename from server/src/main/java/com/genymobile/scrcpy/VideoSource.java rename to server/src/main/java/com/genymobile/scrcpy/video/VideoSource.java index b5a74fbe..53b54a52 100644 --- a/server/src/main/java/com/genymobile/scrcpy/VideoSource.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/VideoSource.java @@ -1,4 +1,4 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.video; public enum VideoSource { DISPLAY("display"), @@ -10,7 +10,7 @@ public enum VideoSource { this.name = name; } - static VideoSource findByName(String name) { + public static VideoSource findByName(String name) { for (VideoSource videoSource : VideoSource.values()) { if (name.equals(videoSource.name)) { return videoSource; diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java index d4bee165..bb1ca0d4 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java @@ -1,7 +1,7 @@ package com.genymobile.scrcpy.wrappers; import com.genymobile.scrcpy.FakeContext; -import com.genymobile.scrcpy.Ln; +import com.genymobile.scrcpy.util.Ln; import android.annotation.SuppressLint; import android.annotation.TargetApi; 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 a0e3a7e1..c5f007fe 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java @@ -1,7 +1,7 @@ package com.genymobile.scrcpy.wrappers; import com.genymobile.scrcpy.FakeContext; -import com.genymobile.scrcpy.Ln; +import com.genymobile.scrcpy.util.Ln; import android.content.ClipData; import android.content.IOnPrimaryClipChangedListener; diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java index a03f824e..7e92ac50 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java @@ -1,8 +1,8 @@ package com.genymobile.scrcpy.wrappers; import com.genymobile.scrcpy.FakeContext; -import com.genymobile.scrcpy.Ln; -import com.genymobile.scrcpy.SettingsException; +import com.genymobile.scrcpy.util.Ln; +import com.genymobile.scrcpy.util.SettingsException; import android.annotation.SuppressLint; import android.content.AttributionSource; diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayControl.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayControl.java index ba3e9ee0..cc9d5526 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayControl.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayControl.java @@ -1,6 +1,6 @@ package com.genymobile.scrcpy.wrappers; -import com.genymobile.scrcpy.Ln; +import com.genymobile.scrcpy.util.Ln; import android.annotation.SuppressLint; import android.annotation.TargetApi; 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 2ff82d04..dd92330c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java @@ -1,9 +1,9 @@ package com.genymobile.scrcpy.wrappers; -import com.genymobile.scrcpy.Command; -import com.genymobile.scrcpy.DisplayInfo; -import com.genymobile.scrcpy.Ln; -import com.genymobile.scrcpy.Size; +import com.genymobile.scrcpy.util.Command; +import com.genymobile.scrcpy.device.DisplayInfo; +import com.genymobile.scrcpy.util.Ln; +import com.genymobile.scrcpy.device.Size; import android.annotation.SuppressLint; import android.hardware.display.VirtualDisplay; 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 16ecb09f..5c5ba56c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java @@ -1,6 +1,6 @@ package com.genymobile.scrcpy.wrappers; -import com.genymobile.scrcpy.Ln; +import com.genymobile.scrcpy.util.Ln; import android.annotation.SuppressLint; import android.view.InputEvent; diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java index 36d5f1ac..0a56f347 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java @@ -1,6 +1,6 @@ package com.genymobile.scrcpy.wrappers; -import com.genymobile.scrcpy.Ln; +import com.genymobile.scrcpy.util.Ln; import android.annotation.SuppressLint; import android.os.Build; diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java index af217da2..ca80dde2 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java @@ -1,6 +1,6 @@ package com.genymobile.scrcpy.wrappers; -import com.genymobile.scrcpy.Ln; +import com.genymobile.scrcpy.util.Ln; import android.os.IInterface; 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 f0e351a2..fc18a8e2 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java @@ -1,6 +1,6 @@ package com.genymobile.scrcpy.wrappers; -import com.genymobile.scrcpy.Ln; +import com.genymobile.scrcpy.util.Ln; import android.annotation.SuppressLint; import android.graphics.Rect; 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 44394ba9..4c769e85 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java @@ -1,6 +1,6 @@ package com.genymobile.scrcpy.wrappers; -import com.genymobile.scrcpy.Ln; +import com.genymobile.scrcpy.util.Ln; import android.annotation.TargetApi; import android.os.IInterface; diff --git a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java similarity index 99% rename from server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java rename to server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java index 0c8086f7..1737730f 100644 --- a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java @@ -1,4 +1,6 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.control; + +import com.genymobile.scrcpy.device.Device; import android.view.KeyEvent; import android.view.MotionEvent; diff --git a/server/src/test/java/com/genymobile/scrcpy/DeviceMessageWriterTest.java b/server/src/test/java/com/genymobile/scrcpy/control/DeviceMessageWriterTest.java similarity index 98% rename from server/src/test/java/com/genymobile/scrcpy/DeviceMessageWriterTest.java rename to server/src/test/java/com/genymobile/scrcpy/control/DeviceMessageWriterTest.java index d7f926ba..ff1a2fbc 100644 --- a/server/src/test/java/com/genymobile/scrcpy/DeviceMessageWriterTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/control/DeviceMessageWriterTest.java @@ -1,4 +1,4 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.control; import org.junit.Assert; import org.junit.Test; diff --git a/server/src/test/java/com/genymobile/scrcpy/BinaryTest.java b/server/src/test/java/com/genymobile/scrcpy/util/BinaryTest.java similarity index 98% rename from server/src/test/java/com/genymobile/scrcpy/BinaryTest.java rename to server/src/test/java/com/genymobile/scrcpy/util/BinaryTest.java index 569a2f2c..7ee95ac5 100644 --- a/server/src/test/java/com/genymobile/scrcpy/BinaryTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/util/BinaryTest.java @@ -1,4 +1,4 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.util; import org.junit.Assert; import org.junit.Test; diff --git a/server/src/test/java/com/genymobile/scrcpy/CodecOptionsTest.java b/server/src/test/java/com/genymobile/scrcpy/util/CodecOptionsTest.java similarity index 99% rename from server/src/test/java/com/genymobile/scrcpy/CodecOptionsTest.java rename to server/src/test/java/com/genymobile/scrcpy/util/CodecOptionsTest.java index ad802258..ffd8e32e 100644 --- a/server/src/test/java/com/genymobile/scrcpy/CodecOptionsTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/util/CodecOptionsTest.java @@ -1,4 +1,4 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.util; import org.junit.Assert; import org.junit.Test; diff --git a/server/src/test/java/com/genymobile/scrcpy/CommandParserTest.java b/server/src/test/java/com/genymobile/scrcpy/util/CommandParserTest.java similarity index 99% rename from server/src/test/java/com/genymobile/scrcpy/CommandParserTest.java rename to server/src/test/java/com/genymobile/scrcpy/util/CommandParserTest.java index de996a07..7e1d55b5 100644 --- a/server/src/test/java/com/genymobile/scrcpy/CommandParserTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/util/CommandParserTest.java @@ -1,5 +1,6 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.util; +import com.genymobile.scrcpy.device.DisplayInfo; import com.genymobile.scrcpy.wrappers.DisplayManager; import android.view.Display; diff --git a/server/src/test/java/com/genymobile/scrcpy/StringUtilsTest.java b/server/src/test/java/com/genymobile/scrcpy/util/StringUtilsTest.java similarity index 97% rename from server/src/test/java/com/genymobile/scrcpy/StringUtilsTest.java rename to server/src/test/java/com/genymobile/scrcpy/util/StringUtilsTest.java index 89799c5e..c72b112a 100644 --- a/server/src/test/java/com/genymobile/scrcpy/StringUtilsTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/util/StringUtilsTest.java @@ -1,4 +1,4 @@ -package com.genymobile.scrcpy; +package com.genymobile.scrcpy.util; import org.junit.Assert; import org.junit.Test; From c57a0512ba5b49534c0c10142998e2aa4172ce01 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 16 Jul 2024 19:52:47 +0200 Subject: [PATCH 1870/2244] Add assertions Passing an unknown enum value to convert them to string would return NULL without any error, possibly causing undefined behavior later. Add assertions to catch such programming errors early. --- app/src/server.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/server.c b/app/src/server.c index 4d55e994..721c91df 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -147,7 +147,7 @@ log_level_to_server_string(enum sc_log_level level) { return "error"; default: assert(!"unexpected log level"); - return "(unknown)"; + return NULL; } } @@ -183,6 +183,7 @@ sc_server_get_codec_name(enum sc_codec codec) { case SC_CODEC_RAW: return "raw"; default: + assert(!"unexpected codec"); return NULL; } } @@ -197,6 +198,7 @@ sc_server_get_camera_facing_name(enum sc_camera_facing camera_facing) { case SC_CAMERA_FACING_EXTERNAL: return "external"; default: + assert(!"unexpected camera facing"); return NULL; } } From bbcd7636121d03db2b7f46c0c2109082c5ae5632 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 17 Jul 2024 18:00:27 +0200 Subject: [PATCH 1871/2244] Exclude install-release tags from git describe The install_release.sh script is updated one commit after the release tag, which may be confusing. For convenience, new lightweight tags have been added (for example v2.5-install-release) to point to the commit where install_release.sh is updated. But these tags interfere with "git describe" to generate pretty filenames when executing ./release.sh on a development branch, so ignore them. Before: release-v2.5-install-release-17-gc57a0512b After: release-v2.5-18-gc57a0512b Refs #4098 comment --- release.mk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release.mk b/release.mk index 89f3da21..dd544bae 100644 --- a/release.mk +++ b/release.mk @@ -24,7 +24,7 @@ SERVER_BUILD_DIR := build-server WIN32_BUILD_DIR := build-win32 WIN64_BUILD_DIR := build-win64 -VERSION := $(shell git describe --tags --always) +VERSION := $(shell git describe --tags --exclude='*install-release' --always) DIST := dist WIN32_TARGET_DIR := scrcpy-win32-$(VERSION) From e0cdc2ace32c63892d8fa6f1c22cbbdae5622bea Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 17 Jul 2024 18:02:26 +0200 Subject: [PATCH 1872/2244] Fix method name The method indicates whether GetPhysicalDisplayIds() exists. The "Get" was missing. --- server/src/main/java/com/genymobile/scrcpy/device/Device.java | 2 +- .../java/com/genymobile/scrcpy/wrappers/SurfaceControl.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Device.java b/server/src/main/java/com/genymobile/scrcpy/device/Device.java index ae4f50e5..46657a05 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Device.java @@ -326,7 +326,7 @@ public final class Device { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { // On Android 14, these internal methods have been moved to DisplayControl boolean useDisplayControl = - Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && !SurfaceControl.hasPhysicalDisplayIdsMethod(); + Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && !SurfaceControl.hasGetPhysicalDisplayIdsMethod(); // Change the power mode for all physical displays long[] physicalDisplayIds = useDisplayControl ? DisplayControl.getPhysicalDisplayIds() : SurfaceControl.getPhysicalDisplayIds(); 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 fc18a8e2..2f24f2d2 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java @@ -134,7 +134,7 @@ public final class SurfaceControl { return getPhysicalDisplayIdsMethod; } - public static boolean hasPhysicalDisplayIdsMethod() { + public static boolean hasGetPhysicalDisplayIdsMethod() { try { getGetPhysicalDisplayIdsMethod(); return true; From 9d1d79b004ed7171f481225993d54c3b100411a7 Mon Sep 17 00:00:00 2001 From: Kaiming Hu Date: Wed, 17 Jul 2024 20:20:13 +0800 Subject: [PATCH 1873/2244] Fix "turn screen off" for Honor Android 14 devices Fixes #4823 PR #5109 Signed-off-by: Romain Vimont --- .../java/com/genymobile/scrcpy/device/Device.java | 14 +++++++++++++- .../genymobile/scrcpy/wrappers/SurfaceControl.java | 9 +++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Device.java b/server/src/main/java/com/genymobile/scrcpy/device/Device.java index 46657a05..5a1083fd 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Device.java @@ -323,7 +323,19 @@ public final class Device { * @param mode one of the {@code POWER_MODE_*} constants */ public static boolean setScreenPowerMode(int mode) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + boolean applyToMultiPhysicalDisplays = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q; + + if (applyToMultiPhysicalDisplays + && Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE + && Build.BRAND.equalsIgnoreCase("honor") + && SurfaceControl.hasGetBuildInDisplayMethod()) { + // Workaround for Honor devices with Android 14: + // - + // - + applyToMultiPhysicalDisplays = false; + } + + if (applyToMultiPhysicalDisplays) { // On Android 14, these internal methods have been moved to DisplayControl boolean useDisplayControl = Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && !SurfaceControl.hasGetPhysicalDisplayIdsMethod(); 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 2f24f2d2..038e7ca0 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java @@ -94,6 +94,15 @@ public final class SurfaceControl { return getBuiltInDisplayMethod; } + public static boolean hasGetBuildInDisplayMethod() { + try { + getGetBuiltInDisplayMethod(); + return true; + } catch (NoSuchMethodException e) { + return false; + } + } + public static IBinder getBuiltInDisplay() { try { Method method = getGetBuiltInDisplayMethod(); From 39132ff2dd7f85af7aa03d68df3d42e5b6646b97 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 16 Jul 2024 19:55:53 +0200 Subject: [PATCH 1874/2244] Make encode() method private It is only used from AudioEncoder. PR #5102 --- .../src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java index 78b6de55..e45284af 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java @@ -176,7 +176,7 @@ public final class AudioEncoder implements AsyncProcessor { } @TargetApi(Build.VERSION_CODES.M) - public void encode() throws IOException, ConfigurationException, AudioCaptureForegroundException { + private void encode() throws IOException, ConfigurationException, AudioCaptureForegroundException { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { Ln.w("Audio disabled: it is not supported before Android 11"); streamer.writeDisableStream(false); From 3b8ec0c38db43a5dd5ed46bc416743ba8e169408 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 14 Jul 2024 23:08:45 +0200 Subject: [PATCH 1875/2244] Rename audio capture exception The AudioCaptureForegroundException was very specific. Rename it to AudioCaptureException to support other capture failures. PR #5102 --- .../com/genymobile/scrcpy/audio/AudioCapture.java | 6 +++--- .../scrcpy/audio/AudioCaptureException.java | 12 ++++++++++++ .../audio/AudioCaptureForegroundException.java | 7 ------- .../com/genymobile/scrcpy/audio/AudioEncoder.java | 4 ++-- .../genymobile/scrcpy/audio/AudioRawRecorder.java | 4 ++-- 5 files changed, 19 insertions(+), 14 deletions(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/audio/AudioCaptureException.java delete mode 100644 server/src/main/java/com/genymobile/scrcpy/audio/AudioCaptureForegroundException.java diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java index 414bfa5d..1da2221e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java @@ -89,7 +89,7 @@ public final class AudioCapture { ServiceManager.getActivityManager().forceStopPackage(FakeContext.PACKAGE_NAME); } - private void tryStartRecording(int attempts, int delayMs) throws AudioCaptureForegroundException { + private void tryStartRecording(int attempts, int delayMs) throws AudioCaptureException { while (attempts-- > 0) { // Wait for activity to start SystemClock.sleep(delayMs); @@ -101,7 +101,7 @@ public final class AudioCapture { Ln.e("Failed to start audio capture"); Ln.e("On Android 11, audio capture must be started in the foreground, make sure that the device is unlocked when starting " + "scrcpy."); - throw new AudioCaptureForegroundException(); + throw new AudioCaptureException(); } else { Ln.d("Failed to start audio capture, retrying..."); } @@ -121,7 +121,7 @@ public final class AudioCapture { recorder.startRecording(); } - public void start() throws AudioCaptureForegroundException { + public void start() throws AudioCaptureException { if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { startWorkaroundAndroid11(); try { diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioCaptureException.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioCaptureException.java new file mode 100644 index 00000000..4b0b7e83 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioCaptureException.java @@ -0,0 +1,12 @@ +package com.genymobile.scrcpy.audio; + +/** + * Exception for any audio capture issue. + *

+ * This includes the case where audio capture failed on Android 11 specifically because the running App (Shell) was not in foreground. + *

+ * Its purpose is to disable audio without errors (that's why the exception is empty, any error message must be printed by the caller before + * throwing the exception). + */ +public class AudioCaptureException extends Exception { +} diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioCaptureForegroundException.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioCaptureForegroundException.java deleted file mode 100644 index 49cfe70f..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioCaptureForegroundException.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.genymobile.scrcpy.audio; - -/** - * Exception thrown if audio capture failed on Android 11 specifically because the running App (shell) was not in foreground. - */ -public class AudioCaptureForegroundException extends Exception { -} diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java index e45284af..219e2c0c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java @@ -132,7 +132,7 @@ public final class AudioEncoder implements AsyncProcessor { } catch (ConfigurationException e) { // Do not print stack trace, a user-friendly error-message has already been logged fatalError = true; - } catch (AudioCaptureForegroundException e) { + } catch (AudioCaptureException e) { // Do not print stack trace, a user-friendly error-message has already been logged } catch (IOException e) { Ln.e("Audio encoding error", e); @@ -176,7 +176,7 @@ public final class AudioEncoder implements AsyncProcessor { } @TargetApi(Build.VERSION_CODES.M) - private void encode() throws IOException, ConfigurationException, AudioCaptureForegroundException { + private void encode() throws IOException, ConfigurationException, AudioCaptureException { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { Ln.w("Audio disabled: it is not supported before Android 11"); streamer.writeDisableStream(false); diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioRawRecorder.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioRawRecorder.java index 72527600..c7279a3a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioRawRecorder.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioRawRecorder.java @@ -23,7 +23,7 @@ public final class AudioRawRecorder implements AsyncProcessor { this.streamer = streamer; } - private void record() throws IOException, AudioCaptureForegroundException { + private void record() throws IOException, AudioCaptureException { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { Ln.w("Audio disabled: it is not supported before Android 11"); streamer.writeDisableStream(false); @@ -69,7 +69,7 @@ public final class AudioRawRecorder implements AsyncProcessor { boolean fatalError = false; try { record(); - } catch (AudioCaptureForegroundException e) { + } catch (AudioCaptureException e) { // Do not print stack trace, a user-friendly error-message has already been logged } catch (Throwable t) { Ln.e("Audio recording error", t); From cf09e78323775f97516e09c17c1201b4f39be940 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 16 Jul 2024 20:00:46 +0200 Subject: [PATCH 1876/2244] Throw AudioCaptureException on workaround error Replace a RuntimeException by a specific AudioCaptureException. PR #5102 --- .../src/main/java/com/genymobile/scrcpy/Workarounds.java | 8 +++++--- .../java/com/genymobile/scrcpy/audio/AudioCapture.java | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java index 8b8b4233..6a4a57e1 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java +++ b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java @@ -1,5 +1,6 @@ package com.genymobile.scrcpy; +import com.genymobile.scrcpy.audio.AudioCaptureException; import com.genymobile.scrcpy.util.Ln; import android.annotation.SuppressLint; @@ -195,7 +196,8 @@ public final class Workarounds { @TargetApi(Build.VERSION_CODES.R) @SuppressLint("WrongConstant,MissingPermission") - public static AudioRecord createAudioRecord(int source, int sampleRate, int channelConfig, int channels, int channelMask, int encoding) { + public static AudioRecord createAudioRecord(int source, int sampleRate, int channelConfig, int channels, int channelMask, int encoding) throws + AudioCaptureException { // Vivo (and maybe some other third-party ROMs) modified `AudioRecord`'s constructor, requiring `Context`s from real App environment. // // This method invokes the `AudioRecord(long nativeRecordInJavaObj)` constructor to create an empty `AudioRecord` instance, then uses @@ -336,8 +338,8 @@ public final class Workarounds { return audioRecord; } catch (Exception e) { - Ln.e("Failed to invoke AudioRecord..", e); - throw new RuntimeException("Cannot create AudioRecord"); + Ln.e("Cannot create AudioRecord", e); + throw new AudioCaptureException(); } } } diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java index 1da2221e..609ea4c2 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java @@ -109,7 +109,7 @@ public final class AudioCapture { } } - private void startRecording() { + private void startRecording() throws AudioCaptureException { try { recorder = createAudioRecord(audioSource); } catch (NullPointerException e) { From 5e605b9b8f8a31b53a41695a831696edad29327e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 16 Jul 2024 13:02:07 +0200 Subject: [PATCH 1877/2244] Move audio compatibility check The compatibility depends on the capture constraints, not the encoding. This will allow to add a new capture implementation with different constraints. PR #5102 --- .../java/com/genymobile/scrcpy/audio/AudioCapture.java | 7 +++++++ .../java/com/genymobile/scrcpy/audio/AudioEncoder.java | 2 ++ 2 files changed, 9 insertions(+) diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java index 609ea4c2..27ea1ec1 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java @@ -121,6 +121,13 @@ public final class AudioCapture { recorder.startRecording(); } + public void checkCompatibility() throws AudioCaptureException { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + Ln.w("Audio disabled: it is not supported before Android 11"); + throw new AudioCaptureException(); + } + } + public void start() throws AudioCaptureException { if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { startWorkaroundAndroid11(); diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java index 219e2c0c..3eadf51a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java @@ -187,6 +187,8 @@ public final class AudioEncoder implements AsyncProcessor { boolean mediaCodecStarted = false; try { + capture.checkCompatibility(); // throws an AudioCaptureException on error + Codec codec = streamer.getCodec(); mediaCodec = createMediaCodec(codec, encoderName); From a2f3a5cf1887261ff526245a9e9e9caa3fb87385 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 14 Jul 2024 23:10:07 +0200 Subject: [PATCH 1878/2244] Move hardcoded audio configuration to AudioConfig This will allow to use these constants from different classes not directly related to AudioCapture. PR #5102 --- .../genymobile/scrcpy/audio/AudioCapture.java | 17 +++++++-------- .../genymobile/scrcpy/audio/AudioConfig.java | 21 +++++++++++++++++++ .../genymobile/scrcpy/audio/AudioEncoder.java | 4 ++-- .../scrcpy/audio/AudioRawRecorder.java | 2 +- 4 files changed, 31 insertions(+), 13 deletions(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/audio/AudioConfig.java diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java index 27ea1ec1..4b7fb8c6 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java @@ -20,17 +20,14 @@ import java.nio.ByteBuffer; public final class AudioCapture { - public static final int SAMPLE_RATE = 48000; - public static final int CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_STEREO; - public static final int CHANNELS = 2; - public static final int CHANNEL_MASK = AudioFormat.CHANNEL_IN_LEFT | AudioFormat.CHANNEL_IN_RIGHT; - public static final int ENCODING = AudioFormat.ENCODING_PCM_16BIT; - public static final int BYTES_PER_SAMPLE = 2; + private static final int SAMPLE_RATE = AudioConfig.SAMPLE_RATE; + private static final int CHANNEL_CONFIG = AudioConfig.CHANNEL_CONFIG; + private static final int CHANNELS = AudioConfig.CHANNELS; + private static final int CHANNEL_MASK = AudioConfig.CHANNEL_MASK; + private static final int ENCODING = AudioConfig.ENCODING; + private static final int BYTES_PER_SAMPLE = AudioConfig.BYTES_PER_SAMPLE; - // Never read more than 1024 samples, even if the buffer is bigger (that would increase latency). - // A lower value is useless, since the system captures audio samples by blocks of 1024 (so for example if we read by blocks of 256 samples, we - // receive 4 successive blocks without waiting, then we wait for the 4 next ones). - public static final int MAX_READ_SIZE = 1024 * CHANNELS * BYTES_PER_SAMPLE; + private static final int MAX_READ_SIZE = AudioConfig.MAX_READ_SIZE; private static final long ONE_SAMPLE_US = (1000000 + SAMPLE_RATE - 1) / SAMPLE_RATE; // 1 sample in microseconds (used for fixing PTS) diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioConfig.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioConfig.java new file mode 100644 index 00000000..b4d79774 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioConfig.java @@ -0,0 +1,21 @@ +package com.genymobile.scrcpy.audio; + +import android.media.AudioFormat; + +public final class AudioConfig { + public static final int SAMPLE_RATE = 48000; + public static final int CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_STEREO; + public static final int CHANNELS = 2; + public static final int CHANNEL_MASK = AudioFormat.CHANNEL_IN_LEFT | AudioFormat.CHANNEL_IN_RIGHT; + public static final int ENCODING = AudioFormat.ENCODING_PCM_16BIT; + public static final int BYTES_PER_SAMPLE = 2; + + // Never read more than 1024 samples, even if the buffer is bigger (that would increase latency). + // A lower value is useless, since the system captures audio samples by blocks of 1024 (so for example if we read by blocks of 256 samples, we + // receive 4 successive blocks without waiting, then we wait for the 4 next ones). + public static final int MAX_READ_SIZE = 1024 * CHANNELS * BYTES_PER_SAMPLE; + + private AudioConfig() { + // Not instantiable + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java index 3eadf51a..8230e054 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java @@ -44,8 +44,8 @@ public final class AudioEncoder implements AsyncProcessor { } } - private static final int SAMPLE_RATE = AudioCapture.SAMPLE_RATE; - private static final int CHANNELS = AudioCapture.CHANNELS; + private static final int SAMPLE_RATE = AudioConfig.SAMPLE_RATE; + private static final int CHANNELS = AudioConfig.CHANNELS; private final AudioCapture capture; private final Streamer streamer; diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioRawRecorder.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioRawRecorder.java index c7279a3a..323caae4 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioRawRecorder.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioRawRecorder.java @@ -30,7 +30,7 @@ public final class AudioRawRecorder implements AsyncProcessor { return; } - final ByteBuffer buffer = ByteBuffer.allocateDirect(AudioCapture.MAX_READ_SIZE); + final ByteBuffer buffer = ByteBuffer.allocateDirect(AudioConfig.MAX_READ_SIZE); final MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); try { From 414ce4c7545d62c0f1e34b2978ace5745d199fd4 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 14 Jul 2024 23:14:48 +0200 Subject: [PATCH 1879/2244] Move createAudioFormat() to AudioConfig This will allow to reuse this method. PR #5102 --- .../com/genymobile/scrcpy/audio/AudioCapture.java | 11 +---------- .../java/com/genymobile/scrcpy/audio/AudioConfig.java | 8 ++++++++ 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java index 4b7fb8c6..c1b19dac 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java @@ -9,7 +9,6 @@ import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.content.ComponentName; import android.content.Intent; -import android.media.AudioFormat; import android.media.AudioRecord; import android.media.AudioTimestamp; import android.media.MediaCodec; @@ -44,14 +43,6 @@ public final class AudioCapture { this.audioSource = audioSource.value(); } - private static AudioFormat createAudioFormat() { - AudioFormat.Builder builder = new AudioFormat.Builder(); - builder.setEncoding(ENCODING); - builder.setSampleRate(SAMPLE_RATE); - builder.setChannelMask(CHANNEL_CONFIG); - return builder.build(); - } - @TargetApi(Build.VERSION_CODES.M) @SuppressLint({"WrongConstant", "MissingPermission"}) private static AudioRecord createAudioRecord(int audioSource) { @@ -61,7 +52,7 @@ public final class AudioCapture { builder.setContext(FakeContext.get()); } builder.setAudioSource(audioSource); - builder.setAudioFormat(createAudioFormat()); + builder.setAudioFormat(AudioConfig.createAudioFormat()); int minBufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, ENCODING); // This buffer size does not impact latency builder.setBufferSizeInBytes(8 * minBufferSize); diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioConfig.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioConfig.java index b4d79774..c77165a7 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioConfig.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioConfig.java @@ -18,4 +18,12 @@ public final class AudioConfig { private AudioConfig() { // Not instantiable } + + public static AudioFormat createAudioFormat() { + AudioFormat.Builder builder = new AudioFormat.Builder(); + builder.setEncoding(ENCODING); + builder.setSampleRate(SAMPLE_RATE); + builder.setChannelMask(CHANNEL_CONFIG); + return builder.build(); + } } From 053bf83f581be543815cf859cedcb496fbb0bc75 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 14 Jul 2024 23:28:13 +0200 Subject: [PATCH 1880/2244] Extract AudioRecordReader Move the logic to read from an AudioRecord and handle all corner cases for PTS. This simplifies AudioCapture. PR #5102 --- .../genymobile/scrcpy/audio/AudioCapture.java | 51 ++------------ .../scrcpy/audio/AudioRecordReader.java | 67 +++++++++++++++++++ 2 files changed, 71 insertions(+), 47 deletions(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/audio/AudioRecordReader.java diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java index c1b19dac..cfc3455e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java @@ -10,7 +10,6 @@ import android.annotation.TargetApi; import android.content.ComponentName; import android.content.Intent; import android.media.AudioRecord; -import android.media.AudioTimestamp; import android.media.MediaCodec; import android.os.Build; import android.os.SystemClock; @@ -24,20 +23,11 @@ public final class AudioCapture { private static final int CHANNELS = AudioConfig.CHANNELS; private static final int CHANNEL_MASK = AudioConfig.CHANNEL_MASK; private static final int ENCODING = AudioConfig.ENCODING; - private static final int BYTES_PER_SAMPLE = AudioConfig.BYTES_PER_SAMPLE; - - private static final int MAX_READ_SIZE = AudioConfig.MAX_READ_SIZE; - - private static final long ONE_SAMPLE_US = (1000000 + SAMPLE_RATE - 1) / SAMPLE_RATE; // 1 sample in microseconds (used for fixing PTS) private final int audioSource; private AudioRecord recorder; - - private final AudioTimestamp timestamp = new AudioTimestamp(); - private long previousRecorderTimestamp = -1; - private long previousPts = 0; - private long nextPts = 0; + private AudioRecordReader reader; public AudioCapture(AudioSource audioSource) { this.audioSource = audioSource.value(); @@ -107,6 +97,7 @@ public final class AudioCapture { recorder = Workarounds.createAudioRecord(audioSource, SAMPLE_RATE, CHANNEL_CONFIG, CHANNELS, CHANNEL_MASK, ENCODING); } recorder.startRecording(); + reader = new AudioRecordReader(recorder); } public void checkCompatibility() throws AudioCaptureException { @@ -137,41 +128,7 @@ public final class AudioCapture { } @TargetApi(Build.VERSION_CODES.N) - public int read(ByteBuffer directBuffer, MediaCodec.BufferInfo outBufferInfo) { - int r = recorder.read(directBuffer, MAX_READ_SIZE); - if (r <= 0) { - return r; - } - - long pts; - - int ret = recorder.getTimestamp(timestamp, AudioTimestamp.TIMEBASE_MONOTONIC); - if (ret == AudioRecord.SUCCESS && timestamp.nanoTime != previousRecorderTimestamp) { - pts = timestamp.nanoTime / 1000; - previousRecorderTimestamp = timestamp.nanoTime; - } else { - if (nextPts == 0) { - Ln.w("Could not get initial audio timestamp"); - nextPts = System.nanoTime() / 1000; - } - // compute from previous timestamp and packet size - pts = nextPts; - } - - long durationUs = r * 1000000L / (CHANNELS * BYTES_PER_SAMPLE * SAMPLE_RATE); - nextPts = pts + durationUs; - - if (previousPts != 0 && pts < previousPts + ONE_SAMPLE_US) { - // Audio PTS may come from two sources: - // - recorder.getTimestamp() if the call works; - // - an estimation from the previous PTS and the packet size as a fallback. - // - // Therefore, the property that PTS are monotonically increasing is no guaranteed in corner cases, so enforce it. - pts = previousPts + ONE_SAMPLE_US; - } - previousPts = pts; - - outBufferInfo.set(0, r, pts, 0); - return r; + public int read(ByteBuffer outDirectBuffer, MediaCodec.BufferInfo outBufferInfo) { + return reader.read(outDirectBuffer, outBufferInfo); } } diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioRecordReader.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioRecordReader.java new file mode 100644 index 00000000..80286831 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioRecordReader.java @@ -0,0 +1,67 @@ +package com.genymobile.scrcpy.audio; + +import com.genymobile.scrcpy.util.Ln; + +import android.annotation.TargetApi; +import android.media.AudioRecord; +import android.media.AudioTimestamp; +import android.media.MediaCodec; +import android.os.Build; + +import java.nio.ByteBuffer; + +public class AudioRecordReader { + + private static final long ONE_SAMPLE_US = + (1000000 + AudioConfig.SAMPLE_RATE - 1) / AudioConfig.SAMPLE_RATE; // 1 sample in microseconds (used for fixing PTS) + + private final AudioRecord recorder; + + private final AudioTimestamp timestamp = new AudioTimestamp(); + private long previousRecorderTimestamp = -1; + private long previousPts = 0; + private long nextPts = 0; + + public AudioRecordReader(AudioRecord recorder) { + this.recorder = recorder; + } + + @TargetApi(Build.VERSION_CODES.N) + public int read(ByteBuffer outDirectBuffer, MediaCodec.BufferInfo outBufferInfo) { + int r = recorder.read(outDirectBuffer, AudioConfig.MAX_READ_SIZE); + if (r <= 0) { + return r; + } + + long pts; + + int ret = recorder.getTimestamp(timestamp, AudioTimestamp.TIMEBASE_MONOTONIC); + if (ret == AudioRecord.SUCCESS && timestamp.nanoTime != previousRecorderTimestamp) { + pts = timestamp.nanoTime / 1000; + previousRecorderTimestamp = timestamp.nanoTime; + } else { + if (nextPts == 0) { + Ln.w("Could not get initial audio timestamp"); + nextPts = System.nanoTime() / 1000; + } + // compute from previous timestamp and packet size + pts = nextPts; + } + + long durationUs = r * 1000000L / (AudioConfig.CHANNELS * AudioConfig.BYTES_PER_SAMPLE * AudioConfig.SAMPLE_RATE); + nextPts = pts + durationUs; + + if (previousPts != 0 && pts < previousPts + ONE_SAMPLE_US) { + // Audio PTS may come from two sources: + // - recorder.getTimestamp() if the call works; + // - an estimation from the previous PTS and the packet size as a fallback. + // + // Therefore, the property that PTS are monotonically increasing is no guaranteed in corner cases, so enforce it. + pts = previousPts + ONE_SAMPLE_US; + } + previousPts = pts; + + outBufferInfo.set(0, r, pts, 0); + return r; + } +} From 0f076083e88cdd313ab98c028598999f38b5a02f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 14 Jul 2024 23:42:22 +0200 Subject: [PATCH 1881/2244] Extract AudioCapture interface Move the implementation to AudioDirectCapture and extract an AudioCapture interface. This will allow to provide another AudioCapture implementation. PR #5102 --- .../java/com/genymobile/scrcpy/Server.java | 3 +- .../genymobile/scrcpy/audio/AudioCapture.java | 138 ++---------------- .../scrcpy/audio/AudioDirectCapture.java | 138 ++++++++++++++++++ 3 files changed, 152 insertions(+), 127 deletions(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/audio/AudioDirectCapture.java diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 263c784d..8a88e276 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -2,6 +2,7 @@ package com.genymobile.scrcpy; import com.genymobile.scrcpy.audio.AudioCapture; import com.genymobile.scrcpy.audio.AudioCodec; +import com.genymobile.scrcpy.audio.AudioDirectCapture; import com.genymobile.scrcpy.audio.AudioEncoder; import com.genymobile.scrcpy.audio.AudioRawRecorder; import com.genymobile.scrcpy.control.ControlChannel; @@ -163,7 +164,7 @@ public final class Server { if (audio) { AudioCodec audioCodec = options.getAudioCodec(); - AudioCapture audioCapture = new AudioCapture(options.getAudioSource()); + AudioCapture audioCapture = new AudioDirectCapture(options.getAudioSource()); Streamer audioStreamer = new Streamer(connection.getAudioFd(), audioCodec, options.getSendCodecMeta(), options.getSendFrameMeta()); AsyncProcessor audioRecorder; if (audioCodec == AudioCodec.RAW) { diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java index cfc3455e..62903f83 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioCapture.java @@ -1,134 +1,20 @@ package com.genymobile.scrcpy.audio; -import com.genymobile.scrcpy.FakeContext; -import com.genymobile.scrcpy.util.Ln; -import com.genymobile.scrcpy.Workarounds; -import com.genymobile.scrcpy.wrappers.ServiceManager; - -import android.annotation.SuppressLint; -import android.annotation.TargetApi; -import android.content.ComponentName; -import android.content.Intent; -import android.media.AudioRecord; import android.media.MediaCodec; -import android.os.Build; -import android.os.SystemClock; import java.nio.ByteBuffer; -public final class AudioCapture { +public interface AudioCapture { + void checkCompatibility() throws AudioCaptureException; + void start() throws AudioCaptureException; + void stop(); - private static final int SAMPLE_RATE = AudioConfig.SAMPLE_RATE; - private static final int CHANNEL_CONFIG = AudioConfig.CHANNEL_CONFIG; - private static final int CHANNELS = AudioConfig.CHANNELS; - private static final int CHANNEL_MASK = AudioConfig.CHANNEL_MASK; - private static final int ENCODING = AudioConfig.ENCODING; - - private final int audioSource; - - private AudioRecord recorder; - private AudioRecordReader reader; - - public AudioCapture(AudioSource audioSource) { - this.audioSource = audioSource.value(); - } - - @TargetApi(Build.VERSION_CODES.M) - @SuppressLint({"WrongConstant", "MissingPermission"}) - private static AudioRecord createAudioRecord(int audioSource) { - AudioRecord.Builder builder = new AudioRecord.Builder(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - // On older APIs, Workarounds.fillAppInfo() must be called beforehand - builder.setContext(FakeContext.get()); - } - builder.setAudioSource(audioSource); - builder.setAudioFormat(AudioConfig.createAudioFormat()); - int minBufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, ENCODING); - // This buffer size does not impact latency - builder.setBufferSizeInBytes(8 * minBufferSize); - return builder.build(); - } - - private static void startWorkaroundAndroid11() { - // Android 11 requires Apps to be at foreground to record audio. - // Normally, each App has its own user ID, so Android checks whether the requesting App has the user ID that's at the foreground. - // But scrcpy server is NOT an App, it's a Java application started from Android shell, so it has the same user ID (2000) with Android - // shell ("com.android.shell"). - // If there is an Activity from Android shell running at foreground, then the permission system will believe scrcpy is also in the - // foreground. - Intent intent = new Intent(Intent.ACTION_MAIN); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.addCategory(Intent.CATEGORY_LAUNCHER); - intent.setComponent(new ComponentName(FakeContext.PACKAGE_NAME, "com.android.shell.HeapDumpActivity")); - ServiceManager.getActivityManager().startActivity(intent); - } - - private static void stopWorkaroundAndroid11() { - ServiceManager.getActivityManager().forceStopPackage(FakeContext.PACKAGE_NAME); - } - - private void tryStartRecording(int attempts, int delayMs) throws AudioCaptureException { - while (attempts-- > 0) { - // Wait for activity to start - SystemClock.sleep(delayMs); - try { - startRecording(); - return; // it worked - } catch (UnsupportedOperationException e) { - if (attempts == 0) { - Ln.e("Failed to start audio capture"); - Ln.e("On Android 11, audio capture must be started in the foreground, make sure that the device is unlocked when starting " - + "scrcpy."); - throw new AudioCaptureException(); - } else { - Ln.d("Failed to start audio capture, retrying..."); - } - } - } - } - - private void startRecording() throws AudioCaptureException { - try { - recorder = createAudioRecord(audioSource); - } catch (NullPointerException e) { - // Creating an AudioRecord using an AudioRecord.Builder does not work on Vivo phones: - // - - // - - recorder = Workarounds.createAudioRecord(audioSource, SAMPLE_RATE, CHANNEL_CONFIG, CHANNELS, CHANNEL_MASK, ENCODING); - } - recorder.startRecording(); - reader = new AudioRecordReader(recorder); - } - - public void checkCompatibility() throws AudioCaptureException { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { - Ln.w("Audio disabled: it is not supported before Android 11"); - throw new AudioCaptureException(); - } - } - - public void start() throws AudioCaptureException { - if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { - startWorkaroundAndroid11(); - try { - tryStartRecording(5, 100); - } finally { - stopWorkaroundAndroid11(); - } - } else { - startRecording(); - } - } - - public void stop() { - if (recorder != null) { - // Will call .stop() if necessary, without throwing an IllegalStateException - recorder.release(); - } - } - - @TargetApi(Build.VERSION_CODES.N) - public int read(ByteBuffer outDirectBuffer, MediaCodec.BufferInfo outBufferInfo) { - return reader.read(outDirectBuffer, outBufferInfo); - } + /** + * Read a chunk of {@link AudioConfig#MAX_READ_SIZE} samples. + * + * @param outDirectBuffer The target buffer + * @param outBufferInfo The info to provide to MediaCodec + * @return the number of bytes actually read. + */ + int read(ByteBuffer outDirectBuffer, MediaCodec.BufferInfo outBufferInfo); } diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioDirectCapture.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioDirectCapture.java new file mode 100644 index 00000000..c0331467 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioDirectCapture.java @@ -0,0 +1,138 @@ +package com.genymobile.scrcpy.audio; + +import com.genymobile.scrcpy.FakeContext; +import com.genymobile.scrcpy.Workarounds; +import com.genymobile.scrcpy.util.Ln; +import com.genymobile.scrcpy.wrappers.ServiceManager; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.content.ComponentName; +import android.content.Intent; +import android.media.AudioRecord; +import android.media.MediaCodec; +import android.os.Build; +import android.os.SystemClock; + +import java.nio.ByteBuffer; + +public class AudioDirectCapture implements AudioCapture { + + private static final int SAMPLE_RATE = AudioConfig.SAMPLE_RATE; + private static final int CHANNEL_CONFIG = AudioConfig.CHANNEL_CONFIG; + private static final int CHANNELS = AudioConfig.CHANNELS; + private static final int CHANNEL_MASK = AudioConfig.CHANNEL_MASK; + private static final int ENCODING = AudioConfig.ENCODING; + + private final int audioSource; + + private AudioRecord recorder; + private AudioRecordReader reader; + + public AudioDirectCapture(AudioSource audioSource) { + this.audioSource = audioSource.value(); + } + + @TargetApi(Build.VERSION_CODES.M) + @SuppressLint({"WrongConstant", "MissingPermission"}) + private static AudioRecord createAudioRecord(int audioSource) { + AudioRecord.Builder builder = new AudioRecord.Builder(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + // On older APIs, Workarounds.fillAppInfo() must be called beforehand + builder.setContext(FakeContext.get()); + } + builder.setAudioSource(audioSource); + builder.setAudioFormat(AudioConfig.createAudioFormat()); + int minBufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, ENCODING); + // This buffer size does not impact latency + builder.setBufferSizeInBytes(8 * minBufferSize); + return builder.build(); + } + + private static void startWorkaroundAndroid11() { + // Android 11 requires Apps to be at foreground to record audio. + // Normally, each App has its own user ID, so Android checks whether the requesting App has the user ID that's at the foreground. + // But scrcpy server is NOT an App, it's a Java application started from Android shell, so it has the same user ID (2000) with Android + // shell ("com.android.shell"). + // If there is an Activity from Android shell running at foreground, then the permission system will believe scrcpy is also in the + // foreground. + Intent intent = new Intent(Intent.ACTION_MAIN); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.addCategory(Intent.CATEGORY_LAUNCHER); + intent.setComponent(new ComponentName(FakeContext.PACKAGE_NAME, "com.android.shell.HeapDumpActivity")); + ServiceManager.getActivityManager().startActivity(intent); + } + + private static void stopWorkaroundAndroid11() { + ServiceManager.getActivityManager().forceStopPackage(FakeContext.PACKAGE_NAME); + } + + private void tryStartRecording(int attempts, int delayMs) throws AudioCaptureException { + while (attempts-- > 0) { + // Wait for activity to start + SystemClock.sleep(delayMs); + try { + startRecording(); + return; // it worked + } catch (UnsupportedOperationException e) { + if (attempts == 0) { + Ln.e("Failed to start audio capture"); + Ln.e("On Android 11, audio capture must be started in the foreground, make sure that the device is unlocked when starting " + + "scrcpy."); + throw new AudioCaptureException(); + } else { + Ln.d("Failed to start audio capture, retrying..."); + } + } + } + } + + private void startRecording() throws AudioCaptureException { + try { + recorder = createAudioRecord(audioSource); + } catch (NullPointerException e) { + // Creating an AudioRecord using an AudioRecord.Builder does not work on Vivo phones: + // - + // - + recorder = Workarounds.createAudioRecord(audioSource, SAMPLE_RATE, CHANNEL_CONFIG, CHANNELS, CHANNEL_MASK, ENCODING); + } + recorder.startRecording(); + reader = new AudioRecordReader(recorder); + } + + @Override + public void checkCompatibility() throws AudioCaptureException { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + Ln.w("Audio disabled: it is not supported before Android 11"); + throw new AudioCaptureException(); + } + } + + @Override + public void start() throws AudioCaptureException { + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { + startWorkaroundAndroid11(); + try { + tryStartRecording(5, 100); + } finally { + stopWorkaroundAndroid11(); + } + } else { + startRecording(); + } + } + + @Override + public void stop() { + if (recorder != null) { + // Will call .stop() if necessary, without throwing an IllegalStateException + recorder.release(); + } + } + + @Override + @TargetApi(Build.VERSION_CODES.N) + public int read(ByteBuffer outDirectBuffer, MediaCodec.BufferInfo outBufferInfo) { + return reader.read(outDirectBuffer, outBufferInfo); + } +} From 53c6eb66ea8fc07433ffe686e9994c8d7c104b7a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 15 Jul 2024 10:54:43 +0200 Subject: [PATCH 1882/2244] Move audio source value The MediaRecorder constant should not belong to the AudioSource enum. This will allow to add a new AudioSource which has no meaningful MediaRecorder audio source value. PR #5102 --- .../scrcpy/audio/AudioDirectCapture.java | 14 +++++++++++++- .../com/genymobile/scrcpy/audio/AudioSource.java | 14 +++----------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioDirectCapture.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioDirectCapture.java index c0331467..361c7bac 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioDirectCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioDirectCapture.java @@ -11,6 +11,7 @@ import android.content.ComponentName; import android.content.Intent; import android.media.AudioRecord; import android.media.MediaCodec; +import android.media.MediaRecorder; import android.os.Build; import android.os.SystemClock; @@ -30,7 +31,18 @@ public class AudioDirectCapture implements AudioCapture { private AudioRecordReader reader; public AudioDirectCapture(AudioSource audioSource) { - this.audioSource = audioSource.value(); + this.audioSource = getAudioSourceValue(audioSource); + } + + private static int getAudioSourceValue(AudioSource audioSource) { + switch (audioSource) { + case OUTPUT: + return MediaRecorder.AudioSource.REMOTE_SUBMIX; + case MIC: + return MediaRecorder.AudioSource.MIC; + default: + throw new IllegalArgumentException("Unsupported audio source: " + audioSource); + } } @TargetApi(Build.VERSION_CODES.M) diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioSource.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioSource.java index 2324f1a4..7201dd39 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioSource.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioSource.java @@ -1,21 +1,13 @@ package com.genymobile.scrcpy.audio; -import android.media.MediaRecorder; - public enum AudioSource { - OUTPUT("output", MediaRecorder.AudioSource.REMOTE_SUBMIX), - MIC("mic", MediaRecorder.AudioSource.MIC); + OUTPUT("output"), + MIC("mic"); private final String name; - private final int value; - AudioSource(String name, int value) { + AudioSource(String name) { this.name = name; - this.value = value; - } - - int value() { - return value; } public static AudioSource findByName(String name) { From a10f8cd798023f858796b023cb846fa2184ad2c7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 15 Jul 2024 10:57:46 +0200 Subject: [PATCH 1883/2244] Add audio playback capture method Add a new method to capture audio playback. It requires Android 13 (where the Shell app has MODIFY_AUDIO_ROUTING permission). The main benefit is that it supports keeping audio playing on the device (implemented in a further commit). Fixes #4380 PR #5102 Co-authored-by: Simon Chan <1330321+yume-chan@users.noreply.github.com> --- app/data/bash-completion/scrcpy | 2 +- app/data/zsh-completion/_scrcpy | 2 +- app/scrcpy.1 | 8 +- app/src/cli.c | 16 ++- app/src/options.h | 1 + app/src/server.c | 20 ++- .../java/com/genymobile/scrcpy/Server.java | 5 +- .../scrcpy/audio/AudioPlaybackCapture.java | 130 ++++++++++++++++++ .../genymobile/scrcpy/audio/AudioSource.java | 7 +- 9 files changed, 182 insertions(+), 9 deletions(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/audio/AudioPlaybackCapture.java diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index b35ea5e4..d5f129d0 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -111,7 +111,7 @@ _scrcpy() { return ;; --audio-source) - COMPREPLY=($(compgen -W 'output mic' -- "$cur")) + COMPREPLY=($(compgen -W 'output mic playback' -- "$cur")) return ;; --camera-facing) diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 5afca977..c49c24eb 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -14,7 +14,7 @@ arguments=( '--audio-codec=[Select the audio codec]:codec:(opus aac flac raw)' '--audio-codec-options=[Set a list of comma-separated key\:type=value options for the device audio encoder]' '--audio-encoder=[Use a specific MediaCodec audio encoder]' - '--audio-source=[Select the audio source]:source:(output mic)' + '--audio-source=[Select the audio source]:source:(output mic playback)' '--audio-output-buffer=[Configure the size of the SDL audio output buffer (in milliseconds)]' {-b,--video-bit-rate=}'[Encode the video at the given bit-rate]' '--camera-ar=[Select the camera size by its aspect ratio]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 1c0c0f7a..19b4ab6b 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -57,7 +57,13 @@ The available encoders can be listed by \fB\-\-list\-encoders\fR. .TP .BI "\-\-audio\-source " source -Select the audio source (output or mic). +Select the audio source (output, mic or playback). + +The "output" source forwards the whole audio output, and disables playback on the device. + +The "playback" source captures the audio playback (Android apps can opt-out, so the whole output is not necessarily captured). + +The "mic" source captures the microphone. Default is output. diff --git a/app/src/cli.c b/app/src/cli.c index 9dd49538..1699e46d 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -189,7 +189,13 @@ static const struct sc_option options[] = { .longopt_id = OPT_AUDIO_SOURCE, .longopt = "audio-source", .argdesc = "source", - .text = "Select the audio source (output or mic).\n" + .text = "Select the audio source (output, mic or playback).\n" + "The \"output\" source forwards the whole audio output, and " + "disables playback on the device.\n" + "The \"playback\" source captures the audio playback (Android " + "apps can opt-out, so the whole output is not necessarily " + "captured).\n" + "The \"mic\" source captures the microphone.\n" "Default is output.", }, { @@ -1931,7 +1937,13 @@ parse_audio_source(const char *optarg, enum sc_audio_source *source) { return true; } - LOGE("Unsupported audio source: %s (expected output or mic)", optarg); + if (!strcmp(optarg, "playback")) { + *source = SC_AUDIO_SOURCE_PLAYBACK; + return true; + } + + LOGE("Unsupported audio source: %s (expected output, mic or playback)", + optarg); return false; } diff --git a/app/src/options.h b/app/src/options.h index 5ec809f0..403685e3 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -59,6 +59,7 @@ enum sc_audio_source { SC_AUDIO_SOURCE_AUTO, // OUTPUT for video DISPLAY, MIC for video CAMERA SC_AUDIO_SOURCE_OUTPUT, SC_AUDIO_SOURCE_MIC, + SC_AUDIO_SOURCE_PLAYBACK, }; enum sc_camera_facing { diff --git a/app/src/server.c b/app/src/server.c index 721c91df..e32aa556 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -203,6 +203,21 @@ sc_server_get_camera_facing_name(enum sc_camera_facing camera_facing) { } } +static const char * +sc_server_get_audio_source_name(enum sc_audio_source audio_source) { + switch (audio_source) { + case SC_AUDIO_SOURCE_OUTPUT: + return "output"; + case SC_AUDIO_SOURCE_MIC: + return "mic"; + case SC_AUDIO_SOURCE_PLAYBACK: + return "playback"; + default: + assert(!"unexpected audio source"); + return NULL; + } +} + static sc_pid execute_server(struct sc_server *server, const struct sc_server_params *params) { @@ -273,8 +288,9 @@ execute_server(struct sc_server *server, assert(params->video_source == SC_VIDEO_SOURCE_CAMERA); ADD_PARAM("video_source=camera"); } - if (params->audio_source == SC_AUDIO_SOURCE_MIC) { - ADD_PARAM("audio_source=mic"); + if (params->audio_source != SC_AUDIO_SOURCE_OUTPUT) { + ADD_PARAM("audio_source=%s", + sc_server_get_audio_source_name(params->audio_source)); } if (params->max_size) { ADD_PARAM("max_size=%" PRIu16, params->max_size); diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 8a88e276..75d9ea15 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -4,7 +4,9 @@ import com.genymobile.scrcpy.audio.AudioCapture; import com.genymobile.scrcpy.audio.AudioCodec; import com.genymobile.scrcpy.audio.AudioDirectCapture; import com.genymobile.scrcpy.audio.AudioEncoder; +import com.genymobile.scrcpy.audio.AudioPlaybackCapture; import com.genymobile.scrcpy.audio.AudioRawRecorder; +import com.genymobile.scrcpy.audio.AudioSource; import com.genymobile.scrcpy.control.ControlChannel; import com.genymobile.scrcpy.control.Controller; import com.genymobile.scrcpy.control.DeviceMessage; @@ -164,7 +166,8 @@ public final class Server { if (audio) { AudioCodec audioCodec = options.getAudioCodec(); - AudioCapture audioCapture = new AudioDirectCapture(options.getAudioSource()); + AudioSource audioSource = options.getAudioSource(); + AudioCapture audioCapture = audioSource.isDirect() ? new AudioDirectCapture(audioSource) : new AudioPlaybackCapture(); Streamer audioStreamer = new Streamer(connection.getAudioFd(), audioCodec, options.getSendCodecMeta(), options.getSendFrameMeta()); AsyncProcessor audioRecorder; if (audioCodec == AudioCodec.RAW) { diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioPlaybackCapture.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioPlaybackCapture.java new file mode 100644 index 00000000..2a0d23fb --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioPlaybackCapture.java @@ -0,0 +1,130 @@ +package com.genymobile.scrcpy.audio; + +import com.genymobile.scrcpy.FakeContext; +import com.genymobile.scrcpy.util.Ln; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.content.Context; +import android.media.AudioAttributes; +import android.media.AudioFormat; +import android.media.AudioManager; +import android.media.AudioRecord; +import android.media.MediaCodec; +import android.os.Build; + +import java.lang.reflect.Method; +import java.nio.ByteBuffer; + +public final class AudioPlaybackCapture implements AudioCapture { + + private AudioRecord recorder; + private AudioRecordReader reader; + + @SuppressLint("PrivateApi") + private AudioRecord createAudioRecord() throws AudioCaptureException { + // See + try { + Class audioMixingRuleClass = Class.forName("android.media.audiopolicy.AudioMixingRule"); + Class audioMixingRuleBuilderClass = Class.forName("android.media.audiopolicy.AudioMixingRule$Builder"); + + // AudioMixingRule.Builder audioMixingRuleBuilder = new AudioMixingRule.Builder(); + Object audioMixingRuleBuilder = audioMixingRuleBuilderClass.getConstructor().newInstance(); + + // audioMixingRuleBuilder.setTargetMixRole(AudioMixingRule.MIX_ROLE_PLAYERS); + int mixRolePlayersConstant = audioMixingRuleClass.getField("MIX_ROLE_PLAYERS").getInt(null); + Method setTargetMixRoleMethod = audioMixingRuleBuilderClass.getMethod("setTargetMixRole", int.class); + setTargetMixRoleMethod.invoke(audioMixingRuleBuilder, mixRolePlayersConstant); + + AudioAttributes attributes = new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA).build(); + + // audioMixingRuleBuilder.addMixRule(AudioMixingRule.RULE_MATCH_ATTRIBUTE_USAGE, attributes); + int ruleMatchAttributeUsageConstant = audioMixingRuleClass.getField("RULE_MATCH_ATTRIBUTE_USAGE").getInt(null); + Method addMixRuleMethod = audioMixingRuleBuilderClass.getMethod("addMixRule", int.class, Object.class); + addMixRuleMethod.invoke(audioMixingRuleBuilder, ruleMatchAttributeUsageConstant, attributes); + + // AudioMixingRule audioMixingRule = builder.build(); + Object audioMixingRule = audioMixingRuleBuilderClass.getMethod("build").invoke(audioMixingRuleBuilder); + + // audioMixingRuleBuilder.voiceCommunicationCaptureAllowed(true); + Method voiceCommunicationCaptureAllowedMethod = audioMixingRuleBuilderClass.getMethod("voiceCommunicationCaptureAllowed", boolean.class); + voiceCommunicationCaptureAllowedMethod.invoke(audioMixingRuleBuilder, true); + + Class audioMixClass = Class.forName("android.media.audiopolicy.AudioMix"); + Class audioMixBuilderClass = Class.forName("android.media.audiopolicy.AudioMix$Builder"); + + // AudioMix.Builder audioMixBuilder = new AudioMix.Builder(audioMixingRule); + Object audioMixBuilder = audioMixBuilderClass.getConstructor(audioMixingRuleClass).newInstance(audioMixingRule); + + // audioMixBuilder.setFormat(createAudioFormat()); + Method setFormat = audioMixBuilder.getClass().getMethod("setFormat", AudioFormat.class); + setFormat.invoke(audioMixBuilder, AudioConfig.createAudioFormat()); + + int routeFlags = audioMixClass.getField("ROUTE_FLAG_LOOP_BACK").getInt(null); + + // audioMixBuilder.setRouteFlags(routeFlag); + Method setRouteFlags = audioMixBuilder.getClass().getMethod("setRouteFlags", int.class); + setRouteFlags.invoke(audioMixBuilder, routeFlags); + + // AudioMix audioMix = audioMixBuilder.build(); + Object audioMix = audioMixBuilderClass.getMethod("build").invoke(audioMixBuilder); + + Class audioPolicyClass = Class.forName("android.media.audiopolicy.AudioPolicy"); + Class audioPolicyBuilderClass = Class.forName("android.media.audiopolicy.AudioPolicy$Builder"); + + // AudioPolicy.Builder audioPolicyBuilder = new AudioPolicy.Builder(); + Object audioPolicyBuilder = audioPolicyBuilderClass.getConstructor(Context.class).newInstance(FakeContext.get()); + + // audioPolicyBuilder.addMix(audioMix); + Method addMixMethod = audioPolicyBuilderClass.getMethod("addMix", audioMixClass); + addMixMethod.invoke(audioPolicyBuilder, audioMix); + + // AudioPolicy audioPolicy = audioPolicyBuilder.build(); + Object audioPolicy = audioPolicyBuilderClass.getMethod("build").invoke(audioPolicyBuilder); + + // AudioManager.registerAudioPolicyStatic(audioPolicy); + Method registerAudioPolicyStaticMethod = AudioManager.class.getDeclaredMethod("registerAudioPolicyStatic", audioPolicyClass); + registerAudioPolicyStaticMethod.setAccessible(true); + int result = (int) registerAudioPolicyStaticMethod.invoke(null, audioPolicy); + if (result != 0) { + throw new RuntimeException("registerAudioPolicy() returned " + result); + } + + // audioPolicy.createAudioRecordSink(audioPolicy); + Method createAudioRecordSinkClass = audioPolicyClass.getMethod("createAudioRecordSink", audioMixClass); + return (AudioRecord) createAudioRecordSinkClass.invoke(audioPolicy, audioMix); + } catch (Exception e) { + Ln.e("Could not capture audio playback", e); + throw new AudioCaptureException(); + } + } + + @Override + public void checkCompatibility() throws AudioCaptureException { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + Ln.w("Audio disabled: audio playback capture source not supported before Android 13"); + throw new AudioCaptureException(); + } + } + + @Override + public void start() throws AudioCaptureException { + recorder = createAudioRecord(); + recorder.startRecording(); + reader = new AudioRecordReader(recorder); + } + + @Override + public void stop() { + if (recorder != null) { + // Will call .stop() if necessary, without throwing an IllegalStateException + recorder.release(); + } + } + + @Override + @TargetApi(Build.VERSION_CODES.N) + public int read(ByteBuffer outDirectBuffer, MediaCodec.BufferInfo outBufferInfo) { + return reader.read(outDirectBuffer, outBufferInfo); + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioSource.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioSource.java index 7201dd39..6082f20e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioSource.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioSource.java @@ -2,7 +2,8 @@ package com.genymobile.scrcpy.audio; public enum AudioSource { OUTPUT("output"), - MIC("mic"); + MIC("mic"), + PLAYBACK("playback"); private final String name; @@ -10,6 +11,10 @@ public enum AudioSource { this.name = name; } + public boolean isDirect() { + return this != PLAYBACK; + } + public static AudioSource findByName(String name) { for (AudioSource audioSource : AudioSource.values()) { if (name.equals(audioSource.name)) { From 31116a60d7b03f8489a4f73d81ab0c0689a67d9b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 16 Jul 2024 20:56:18 +0200 Subject: [PATCH 1884/2244] Add --audio-dup Add an option to duplicate audio on the device, compatible with the new audio playback capture (--audio-source=playback). Fixes #3875 Fixes #4380 PR #5102 Co-authored-by: Simon Chan <1330321+yume-chan@users.noreply.github.com> --- app/data/bash-completion/scrcpy | 1 + app/data/zsh-completion/_scrcpy | 1 + app/scrcpy.1 | 6 +++++ app/src/cli.c | 23 +++++++++++++++++++ app/src/options.c | 1 + app/src/options.h | 1 + app/src/scrcpy.c | 1 + app/src/server.c | 3 +++ app/src/server.h | 1 + .../java/com/genymobile/scrcpy/Options.java | 8 +++++++ .../java/com/genymobile/scrcpy/Server.java | 8 ++++++- .../scrcpy/audio/AudioPlaybackCapture.java | 9 +++++++- 12 files changed, 61 insertions(+), 2 deletions(-) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index d5f129d0..e0928cbd 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -6,6 +6,7 @@ _scrcpy() { --audio-buffer= --audio-codec= --audio-codec-options= + --audio-dup --audio-encoder= --audio-source= --audio-output-buffer= diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index c49c24eb..0f06ba4b 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -13,6 +13,7 @@ arguments=( '--audio-buffer=[Configure the audio buffering delay (in milliseconds)]' '--audio-codec=[Select the audio codec]:codec:(opus aac flac raw)' '--audio-codec-options=[Set a list of comma-separated key\:type=value options for the device audio encoder]' + '--audio-dup=[Duplicate audio]' '--audio-encoder=[Use a specific MediaCodec audio encoder]' '--audio-source=[Select the audio source]:source:(output mic playback)' '--audio-output-buffer=[Configure the size of the SDL audio output buffer (in milliseconds)]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 19b4ab6b..de2b8ac6 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -49,6 +49,12 @@ The list of possible codec options is available in the Android documentation: +.TP +.B \-\-audio\-dup +Duplicate audio (capture and keep playing on the device). + +This feature is only available with --audio-source=playback. + .TP .BI "\-\-audio\-encoder " name Use a specific MediaCodec audio encoder (depending on the codec provided by \fB\-\-audio\-codec\fR). diff --git a/app/src/cli.c b/app/src/cli.c index 1699e46d..1792384e 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -100,6 +100,7 @@ enum { OPT_NO_WINDOW, OPT_MOUSE_BIND, OPT_NO_MOUSE_HOVER, + OPT_AUDIO_DUP, }; struct sc_option { @@ -177,6 +178,13 @@ static const struct sc_option options[] = { "Android documentation: " "", }, + { + .longopt_id = OPT_AUDIO_DUP, + .longopt = "audio-dup", + .text = "Duplicate audio (capture and keep playing on the device).\n" + "This feature is only available with --audio-source=playback." + + }, { .longopt_id = OPT_AUDIO_ENCODER, .longopt = "audio-encoder", @@ -2615,6 +2623,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], case OPT_NO_WINDOW: opts->window = false; break; + case OPT_AUDIO_DUP: + opts->audio_dup = true; + break; default: // getopt prints the error message on stderr return false; @@ -2891,6 +2902,18 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } } + if (opts->audio_dup) { + if (!opts->audio) { + LOGE("--audio-dup not supported if audio is disabled"); + return false; + } + + if (opts->audio_source != SC_AUDIO_SOURCE_PLAYBACK) { + LOGE("--audio-dup is specific to --audio-source=playback"); + return false; + } + } + if (opts->record_format && !opts->record_filename) { LOGE("Record format specified without recording"); return false; diff --git a/app/src/options.c b/app/src/options.c index 5eec6427..6fca6ad5 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -101,6 +101,7 @@ const struct scrcpy_options scrcpy_options_default = { .list = 0, .window = true, .mouse_hover = true, + .audio_dup = false, }; enum sc_orientation diff --git a/app/src/options.h b/app/src/options.h index 403685e3..140d12b1 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -297,6 +297,7 @@ struct scrcpy_options { uint8_t list; bool window; bool mouse_hover; + bool audio_dup; }; extern const struct scrcpy_options scrcpy_options_default; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 376d5839..43864661 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -394,6 +394,7 @@ scrcpy(struct scrcpy_options *options) { .display_id = options->display_id, .video = options->video, .audio = options->audio, + .audio_dup = options->audio_dup, .show_touches = options->show_touches, .stay_awake = options->stay_awake, .video_codec_options = options->video_codec_options, diff --git a/app/src/server.c b/app/src/server.c index e32aa556..0db29183 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -292,6 +292,9 @@ execute_server(struct sc_server *server, ADD_PARAM("audio_source=%s", sc_server_get_audio_source_name(params->audio_source)); } + if (params->audio_dup) { + ADD_PARAM("audio_dup=true"); + } if (params->max_size) { ADD_PARAM("max_size=%" PRIu16, params->max_size); } diff --git a/app/src/server.h b/app/src/server.h index 062af0a9..cffa510e 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -50,6 +50,7 @@ struct sc_server_params { uint32_t display_id; bool video; bool audio; + bool audio_dup; bool show_touches; bool stay_awake; bool force_adb_forward; diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 143fbb9a..2f86d8ce 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -26,6 +26,7 @@ public class Options { private AudioCodec audioCodec = AudioCodec.OPUS; private VideoSource videoSource = VideoSource.DISPLAY; private AudioSource audioSource = AudioSource.OUTPUT; + private boolean audioDup; private int videoBitRate = 8000000; private int audioBitRate = 128000; private int maxFps; @@ -100,6 +101,10 @@ public class Options { return audioSource; } + public boolean getAudioDup() { + return audioDup; + } + public int getVideoBitRate() { return videoBitRate; } @@ -303,6 +308,9 @@ public class Options { } options.audioSource = audioSource; break; + case "audio_dup": + options.audioDup = Boolean.parseBoolean(value); + break; case "max_size": options.maxSize = Integer.parseInt(value) & ~7; // multiple of 8 break; diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 75d9ea15..11429e6c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -167,7 +167,13 @@ public final class Server { if (audio) { AudioCodec audioCodec = options.getAudioCodec(); AudioSource audioSource = options.getAudioSource(); - AudioCapture audioCapture = audioSource.isDirect() ? new AudioDirectCapture(audioSource) : new AudioPlaybackCapture(); + AudioCapture audioCapture; + if (audioSource.isDirect()) { + audioCapture = new AudioDirectCapture(audioSource); + } else { + audioCapture = new AudioPlaybackCapture(options.getAudioDup()); + } + Streamer audioStreamer = new Streamer(connection.getAudioFd(), audioCodec, options.getSendCodecMeta(), options.getSendFrameMeta()); AsyncProcessor audioRecorder; if (audioCodec == AudioCodec.RAW) { diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioPlaybackCapture.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioPlaybackCapture.java index 2a0d23fb..e38493f2 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioPlaybackCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioPlaybackCapture.java @@ -18,9 +18,15 @@ import java.nio.ByteBuffer; public final class AudioPlaybackCapture implements AudioCapture { + private final boolean keepPlayingOnDevice; + private AudioRecord recorder; private AudioRecordReader reader; + public AudioPlaybackCapture(boolean keepPlayingOnDevice) { + this.keepPlayingOnDevice = keepPlayingOnDevice; + } + @SuppressLint("PrivateApi") private AudioRecord createAudioRecord() throws AudioCaptureException { // See @@ -60,7 +66,8 @@ public final class AudioPlaybackCapture implements AudioCapture { Method setFormat = audioMixBuilder.getClass().getMethod("setFormat", AudioFormat.class); setFormat.invoke(audioMixBuilder, AudioConfig.createAudioFormat()); - int routeFlags = audioMixClass.getField("ROUTE_FLAG_LOOP_BACK").getInt(null); + String routeFlagName = keepPlayingOnDevice ? "ROUTE_FLAG_LOOP_BACK_RENDER" : "ROUTE_FLAG_LOOP_BACK"; + int routeFlags = audioMixClass.getField(routeFlagName).getInt(null); // audioMixBuilder.setRouteFlags(routeFlag); Method setRouteFlags = audioMixBuilder.getClass().getMethod("setRouteFlags", int.class); From 127a271d34816f4f06cb7dbcba2b939f24c1ca6a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 16 Jul 2024 20:59:39 +0200 Subject: [PATCH 1885/2244] Switch audio source if audio-dup is set Automatically switch implicit audio source to "playback" if --audio-dup is passed. This allows to run: scrcpy --audio-dup without specifying explicitly: scrcpy --audio-source=playback --audio-dup PR #5102 --- app/src/cli.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/src/cli.c b/app/src/cli.c index 1792384e..dd1b6799 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -2895,7 +2895,13 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], if (opts->audio && opts->audio_source == SC_AUDIO_SOURCE_AUTO) { // Select the audio source according to the video source if (opts->video_source == SC_VIDEO_SOURCE_DISPLAY) { - opts->audio_source = SC_AUDIO_SOURCE_OUTPUT; + if (opts->audio_dup) { + LOGI("Audio duplication enabled: audio source switched to " + "\"playback\""); + opts->audio_source = SC_AUDIO_SOURCE_PLAYBACK; + } else { + opts->audio_source = SC_AUDIO_SOURCE_OUTPUT; + } } else { opts->audio_source = SC_AUDIO_SOURCE_MIC; LOGI("Camera video source: microphone audio source selected"); From ed4066902d08e4abb91b4b6c7d828f190ab65bfb Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 16 Jul 2024 21:15:03 +0200 Subject: [PATCH 1886/2244] Update documentation for audio playback capture PR #5102 --- doc/audio.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/doc/audio.md b/doc/audio.md index 0c0409a9..750163e0 100644 --- a/doc/audio.md +++ b/doc/audio.md @@ -66,6 +66,30 @@ the computer: scrcpy --audio-source=mic --no-video --no-playback --record=file.opus ``` +### Duplication + +An alternative device audio capture method is also available (only for Android +13 and above): + +``` +scrcpy --audio-source=playback +``` + +This audio source supports keeping the audio playing on the device while +mirroring, with `--audio-dup`: + +```bash +scrcpy --audio-source=playback --audio-dup +# or simply: +scrcpy --audio-dup # --audio-source=playback is implied +``` + +However, it requires Android 13, and Android apps can opt-out (so they are not +captured). + + +See [#4380](https://github.com/Genymobile/scrcpy/issues/4380). + ## Codec From 65bd6bd8d4cd16baf44a80911b67afcfa8615106 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 19 Jul 2024 17:51:50 +0200 Subject: [PATCH 1887/2244] Explicitly accept issues for general questions Add an empty question template, and reword the "Contact" section in the README. Refs #5117 --- .github/ISSUE_TEMPLATE/question.md | 8 ++++++++ README.md | 7 +++++-- 2 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/question.md diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md new file mode 100644 index 00000000..14dc373a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.md @@ -0,0 +1,8 @@ +--- +name: Question +about: Ask a question about scrcpy +title: '' +labels: '' +assignees: '' + +--- diff --git a/README.md b/README.md index 3185652b..36c8c272 100644 --- a/README.md +++ b/README.md @@ -148,11 +148,14 @@ documented in the following pages: ## Contact -If you encounter a bug, please read the [FAQ](FAQ.md) first, then open an [issue]. +You can open an [issue] for bug reports, feature requests or general questions. + +For bug reports, please read the [FAQ](FAQ.md) first, you might find a solution +to your problem immediately. [issue]: https://github.com/Genymobile/scrcpy/issues -For general questions or discussions, you can also use: +You can also use: - Reddit: [`r/scrcpy`](https://www.reddit.com/r/scrcpy) - Twitter: [`@scrcpy_app`](https://twitter.com/scrcpy_app) From bbfac9ae1fba08a045557abe9612703ace8a3890 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 19 Jul 2024 17:56:26 +0200 Subject: [PATCH 1888/2244] Add FUNDING.yml The donation links were already in the README. Also add them in the format expected by GitHub in FUNDING.yml. --- .github/FUNDING.yml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..b567129a --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +github: [rom1v] +liberapay: rom1v +custom: ["https://paypal.me/rom2v"] From 071d459ad7d32fb6465216a0f5df7696db3adc02 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 29 Jul 2024 19:58:40 +0200 Subject: [PATCH 1889/2244] Fix --no-audio By default, the audio source is initialized to SC_AUDIO_SOURCE_AUTO, and is "resolved" only if audio is enabled. But the server arguments were built assuming that the audio source was never SC_AUDIO_SOURCE_AUTO (even with audio disabled), causing a crash. Regression introduced by a10f8cd798023f858796b023cb846fa2184ad2c7. --- app/src/server.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/server.c b/app/src/server.c index 0db29183..41517f18 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -288,7 +288,9 @@ execute_server(struct sc_server *server, assert(params->video_source == SC_VIDEO_SOURCE_CAMERA); ADD_PARAM("video_source=camera"); } - if (params->audio_source != SC_AUDIO_SOURCE_OUTPUT) { + // If audio is enabled, an "auto" audio source must have been resolved + assert(params->audio_source != SC_AUDIO_SOURCE_AUTO || !params->audio); + if (params->audio_source != SC_AUDIO_SOURCE_OUTPUT && params->audio) { ADD_PARAM("audio_source=%s", sc_server_get_audio_source_name(params->audio_source)); } From f691ebb1b4fde44789292d1e8ed41adaee8e6854 Mon Sep 17 00:00:00 2001 From: Al Grimes Date: Sun, 28 Jul 2024 13:01:06 +0100 Subject: [PATCH 1890/2244] Add workaround for TCL Android 12 Smart TVs Fixes #5140 PR #5148 Signed-off-by: Romain Vimont --- server/src/main/java/com/genymobile/scrcpy/Workarounds.java | 6 ++++-- 1 file changed, 4 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 6a4a57e1..fa09d88d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java +++ b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java @@ -66,11 +66,13 @@ public final class Workarounds { // - // - mustFillAppInfo = true; - } else if (Build.BRAND.equalsIgnoreCase("honor") || Build.MANUFACTURER.equalsIgnoreCase("skyworth")) { + } else if (Build.BRAND.equalsIgnoreCase("honor") || Build.MANUFACTURER.equalsIgnoreCase("skyworth") || Build.BRAND.equalsIgnoreCase("tcl")) { // More workarounds must be applied for Honor devices: // - - // and Skyworth devices: + // for Skyworth devices: // - + // and for TCL devices: + // - // // The system context must not be set for all devices, because it would cause other problems: // - From 2b6089cbfc29c41643e0c0e8049bda3ede777b61 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 31 Jul 2024 00:43:03 +0200 Subject: [PATCH 1891/2244] Enable workarounds by default Workarounds were disabled by default, and only enabled for some devices or under specific conditions. But it seems they are needed for more and more devices, so enable them by default. They could be disabled for specific devices if necessary in the future. In the past, these workarounds caused a (harmless) exception to be printed on some Xiaomi devices [1]. But this is not a problem anymore since commit b8c5853aa6ac9cfbe3fb4e46bf10978b3fa212e3. They also caused problems for audio on Vivo devices [2], but it seems this is not the case anymore [3]. They might also impact an old Nvidia Shield [4], but hopefully this is fixed now. [1]: [2]: [3]: [4]: PR #5154 --- .../java/com/genymobile/scrcpy/Server.java | 4 +- .../com/genymobile/scrcpy/Workarounds.java | 58 ++----------------- 2 files changed, 7 insertions(+), 55 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 11429e6c..7817fdf5 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -144,7 +144,7 @@ public final class Server { final Device device = camera ? null : new Device(options); - Workarounds.apply(audio, camera); + Workarounds.apply(); List asyncProcessors = new ArrayList<>(); @@ -279,7 +279,7 @@ public final class Server { Ln.i(LogUtils.buildDisplayListMessage()); } if (options.getListCameras() || options.getListCameraSizes()) { - Workarounds.apply(false, true); + Workarounds.apply(); Ln.i(LogUtils.buildCameraListMessage(options.getListCameraSizes())); } // Just print the requested data, do not mirror diff --git a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java index fa09d88d..8fc38555 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java +++ b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java @@ -51,66 +51,18 @@ public final class Workarounds { // not instantiable } - public static void apply(boolean audio, boolean camera) { - boolean mustFillConfigurationController = false; - boolean mustFillAppInfo = false; - boolean mustFillAppContext = false; - - if (Build.BRAND.equalsIgnoreCase("meizu")) { - // Workarounds must be applied for Meizu phones: - // - - // - - // - - // - // But only apply when strictly necessary, since workarounds can cause other issues: - // - - // - - mustFillAppInfo = true; - } else if (Build.BRAND.equalsIgnoreCase("honor") || Build.MANUFACTURER.equalsIgnoreCase("skyworth") || Build.BRAND.equalsIgnoreCase("tcl")) { - // More workarounds must be applied for Honor devices: - // - - // for Skyworth devices: - // - - // and for TCL devices: - // - - // - // The system context must not be set for all devices, because it would cause other problems: - // - - // - - mustFillAppInfo = true; - mustFillAppContext = true; - } - - if (audio && Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { - // Before Android 11, audio is not supported. - // Since Android 12, we can properly set a context on the AudioRecord. - // Only on Android 11 we must fill the application context for the AudioRecord to work. - mustFillAppContext = true; - } - - if (camera) { - mustFillAppInfo = true; - mustFillAppContext = true; - } - + public static void apply() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { // On some Samsung devices, DisplayManagerGlobal.getDisplayInfoLocked() calls ActivityThread.currentActivityThread().getConfiguration(), // which requires a non-null ConfigurationController. // ConfigurationController was introduced in Android 12, so do not attempt to set it on lower versions. // - mustFillConfigurationController = true; - } - - if (mustFillConfigurationController) { - // Must be call before fillAppContext() because it is necessary to get a valid system context + // Must be called before fillAppContext() because it is necessary to get a valid system context. fillConfigurationController(); } - if (mustFillAppInfo) { - fillAppInfo(); - } - if (mustFillAppContext) { - fillAppContext(); - } + + fillAppInfo(); + fillAppContext(); } @SuppressWarnings("deprecation") From 5d2441d1983d2b20854817d55b45a565af16694f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 31 Jul 2024 15:21:49 +0200 Subject: [PATCH 1892/2244] Upgrade SDL (2.30.5) for Windows --- app/deps/sdl.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/deps/sdl.sh b/app/deps/sdl.sh index 589f93e5..0a42bc1f 100755 --- a/app/deps/sdl.sh +++ b/app/deps/sdl.sh @@ -4,10 +4,10 @@ DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) cd "$DEPS_DIR" . common -VERSION=2.30.4 +VERSION=2.30.5 FILENAME=SDL-$VERSION.tar.gz PROJECT_DIR=SDL-release-$VERSION -SHA256SUM=dcc2c8c9c3e9e1a7c8d61d9522f1cba4e9b740feb560dcb15234030984610ee2 +SHA256SUM=be3ca88f8c362704627a0bc5406edb2cd6cc6ba463596d81ebb7c2f18763d3bf cd "$SOURCES_DIR" From 52136268ef931e37f93cb05350479e386bb51fbf Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 1 Aug 2024 18:15:59 +0200 Subject: [PATCH 1893/2244] Bump version to 2.6 --- app/scrcpy-windows.rc | 2 +- meson.build | 2 +- server/build.gradle | 4 ++-- server/build_without_gradle.sh | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/scrcpy-windows.rc b/app/scrcpy-windows.rc index 717d9cb2..926dd655 100644 --- a/app/scrcpy-windows.rc +++ b/app/scrcpy-windows.rc @@ -13,7 +13,7 @@ BEGIN VALUE "LegalCopyright", "Romain Vimont, Genymobile" VALUE "OriginalFilename", "scrcpy.exe" VALUE "ProductName", "scrcpy" - VALUE "ProductVersion", "2.5" + VALUE "ProductVersion", "2.6" END END BLOCK "VarFileInfo" diff --git a/meson.build b/meson.build index 1d11e574..d4a1d6a0 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('scrcpy', 'c', - version: '2.5', + version: '2.6', meson_version: '>= 0.48', default_options: [ 'c_std=c11', diff --git a/server/build.gradle b/server/build.gradle index d17ffcb2..177f74ab 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -7,8 +7,8 @@ android { applicationId "com.genymobile.scrcpy" minSdkVersion 21 targetSdkVersion 34 - versionCode 20500 - versionName "2.5" + versionCode 20600 + versionName "2.6" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index 6dc547d9..f8eb0510 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -12,7 +12,7 @@ set -e SCRCPY_DEBUG=false -SCRCPY_VERSION_NAME=2.5 +SCRCPY_VERSION_NAME=2.6 PLATFORM=${ANDROID_PLATFORM:-34} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-34.0.0} From 67f93350f679e40ba979198753f4c657fe589c87 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 1 Aug 2024 18:46:10 +0200 Subject: [PATCH 1894/2244] Update links to 2.6 --- README.md | 2 +- doc/build.md | 6 +++--- doc/windows.md | 12 ++++++------ install_release.sh | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 36c8c272..25eeb96e 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ source for the project. Do not download releases from random websites, even if their name contains `scrcpy`.** -# scrcpy (v2.5) +# scrcpy (v2.6) scrcpy diff --git a/doc/build.md b/doc/build.md index a35910f8..707b9064 100644 --- a/doc/build.md +++ b/doc/build.md @@ -233,10 +233,10 @@ install` must be run as root)._ #### Option 2: Use prebuilt server - - [`scrcpy-server-v2.5`][direct-scrcpy-server] - SHA-256: `1488b1105d6aff534873a26bf610cd2aea06ee867dd7a4d9c6bb2c091396eb15` + - [`scrcpy-server-v2.6`][direct-scrcpy-server] + SHA-256: `7b723ff79a27f14e6ebaaaae7ef9548c40651c94e64d178612b13adf7158eb2e` -[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v2.5/scrcpy-server-v2.5 +[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v2.6/scrcpy-server-v2.6 Download the prebuilt server somewhere, and specify its path during the Meson configuration: diff --git a/doc/windows.md b/doc/windows.md index 139c3419..6842edc6 100644 --- a/doc/windows.md +++ b/doc/windows.md @@ -4,14 +4,14 @@ Download the [latest release]: - - [`scrcpy-win64-v2.5.zip`][direct-win64] (64-bit) - SHA-256: `345cf04a66a9144281dce72ca4e82adfd2c3092463196e586051df4c69e1507b` - - [`scrcpy-win32-v2.5.zip`][direct-win32] (32-bit) - SHA-256: `d56312a92471565fa4f3a6b94e8eb07717c4c90f2c0f05b03ba444e1001806ec` + - [`scrcpy-win64-v2.6.zip`][direct-win64] (64-bit) + SHA-256: `3d490a72997af950aec0540e28627ada35c8226bc9774500014c9697d9b53194` + - [`scrcpy-win32-v2.6.zip`][direct-win32] (32-bit) + SHA-256: `6c68f6b31ddef5ed61a7546f423bd4fc99d568eb4c4e3409e0df496187eb3783` [latest release]: https://github.com/Genymobile/scrcpy/releases/latest -[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v2.5/scrcpy-win64-v2.5.zip -[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v2.5/scrcpy-win32-v2.5.zip +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v2.6/scrcpy-win64-v2.6.zip +[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v2.6/scrcpy-win32-v2.6.zip and extract it. diff --git a/install_release.sh b/install_release.sh index 2bd6d7e6..9dbf3f73 100755 --- a/install_release.sh +++ b/install_release.sh @@ -2,8 +2,8 @@ set -e BUILDDIR=build-auto -PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v2.5/scrcpy-server-v2.5 -PREBUILT_SERVER_SHA256=1488b1105d6aff534873a26bf610cd2aea06ee867dd7a4d9c6bb2c091396eb15 +PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v2.6/scrcpy-server-v2.6 +PREBUILT_SERVER_SHA256=7b723ff79a27f14e6ebaaaae7ef9548c40651c94e64d178612b13adf7158eb2e echo "[scrcpy] Downloading prebuilt server..." wget "$PREBUILT_SERVER_URL" -O scrcpy-server From 992b4922fe32fd5bf96d0df204300f7c35f120fe Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 2 Aug 2024 18:40:07 +0200 Subject: [PATCH 1895/2244] Document INJECT_EVENTS permission issue on Xiaomi Make explicit in the prerequisites the exact error message when "USB debugging (Security Settings)" is not set. --- README.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 25eeb96e..d4844739 100644 --- a/README.md +++ b/README.md @@ -53,10 +53,16 @@ Make sure you [enabled USB debugging][enable-adb] on your device(s). [enable-adb]: https://developer.android.com/studio/debug/dev-options#enable -On some devices, you also need to enable [an additional option][control] `USB -debugging (Security Settings)` (this is an item different from `USB debugging`) -to control it using a keyboard and mouse. Rebooting the device is necessary once -this option is set. +On some devices (especially Xiaomi), you might get the following error: + +``` +java.lang.SecurityException: Injecting input events requires the caller (or the source of the instrumentation, if any) to have the INJECT_EVENTS permission. +``` + +In that case, you need to enable [an additional option][control] `USB debugging +(Security Settings)` (this is an item different from `USB debugging`) to control +it using a keyboard and mouse. Rebooting the device is necessary once this +option is set. [control]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323 From 773c23fda298fd3dace4ded5a89e8716d3c0fc76 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 2 Aug 2024 20:20:12 +0200 Subject: [PATCH 1896/2244] Inject finger input whenever possible Even if the pointer is a mouse, inject it as a finger unless it is required to be a mouse, that is: - when it is a HOVER_MOUSE event, or - when a secondary button is pressed. Some apps/games only accept events from a finger/touchscreen, so using a mouse by default does not work for them. For simplicity, make this change on the server side just before event injection (so that the client does not need to know about this hacky behavior). Refs 6808288823239b0f3a76f9be377e4de82e91b35a Refs c7b1d0ea9af8bb9603ec8f713f1a5fbf3f628b6a Fixes #5162 Fixes #5163 --- .../main/java/com/genymobile/scrcpy/control/Controller.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index 85425113..1494c10a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -278,8 +278,9 @@ public class Controller implements AsyncProcessor { pointer.setPressure(pressure); int source; - if (pointerId == POINTER_ID_MOUSE) { - // real mouse event + boolean activeSecondaryButtons = ((actionButton | buttons) & ~MotionEvent.BUTTON_PRIMARY) != 0; + if (pointerId == POINTER_ID_MOUSE && (action == MotionEvent.ACTION_HOVER_MOVE || activeSecondaryButtons)) { + // real mouse event, or event incompatible with a finger pointerProperties[pointerIndex].toolType = MotionEvent.TOOL_TYPE_MOUSE; source = InputDevice.SOURCE_MOUSE; pointer.setUp(buttons == 0); From cc41115625553d2f9ed8e96302174f189f2f40c7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 2 Aug 2024 22:32:04 +0200 Subject: [PATCH 1897/2244] Bump version to 2.6.1 --- app/scrcpy-windows.rc | 2 +- meson.build | 2 +- server/build.gradle | 4 ++-- server/build_without_gradle.sh | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/scrcpy-windows.rc b/app/scrcpy-windows.rc index 926dd655..9e0d90c2 100644 --- a/app/scrcpy-windows.rc +++ b/app/scrcpy-windows.rc @@ -13,7 +13,7 @@ BEGIN VALUE "LegalCopyright", "Romain Vimont, Genymobile" VALUE "OriginalFilename", "scrcpy.exe" VALUE "ProductName", "scrcpy" - VALUE "ProductVersion", "2.6" + VALUE "ProductVersion", "2.6.1" END END BLOCK "VarFileInfo" diff --git a/meson.build b/meson.build index d4a1d6a0..b532006a 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('scrcpy', 'c', - version: '2.6', + version: '2.6.1', meson_version: '>= 0.48', default_options: [ 'c_std=c11', diff --git a/server/build.gradle b/server/build.gradle index 177f74ab..decacd3f 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -7,8 +7,8 @@ android { applicationId "com.genymobile.scrcpy" minSdkVersion 21 targetSdkVersion 34 - versionCode 20600 - versionName "2.6" + versionCode 20601 + versionName "2.6.1" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index f8eb0510..5ee7af30 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -12,7 +12,7 @@ set -e SCRCPY_DEBUG=false -SCRCPY_VERSION_NAME=2.6 +SCRCPY_VERSION_NAME=2.6.1 PLATFORM=${ANDROID_PLATFORM:-34} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-34.0.0} From 44b3fd82b1831f4aa436268870adf32bddb81924 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 2 Aug 2024 22:58:09 +0200 Subject: [PATCH 1898/2244] Update links to 2.6.1 --- README.md | 2 +- doc/build.md | 6 +++--- doc/windows.md | 12 ++++++------ install_release.sh | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index d4844739..67fdf364 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ source for the project. Do not download releases from random websites, even if their name contains `scrcpy`.** -# scrcpy (v2.6) +# scrcpy (v2.6.1) scrcpy diff --git a/doc/build.md b/doc/build.md index 707b9064..15e0ffff 100644 --- a/doc/build.md +++ b/doc/build.md @@ -233,10 +233,10 @@ install` must be run as root)._ #### Option 2: Use prebuilt server - - [`scrcpy-server-v2.6`][direct-scrcpy-server] - SHA-256: `7b723ff79a27f14e6ebaaaae7ef9548c40651c94e64d178612b13adf7158eb2e` + - [`scrcpy-server-v2.6.1`][direct-scrcpy-server] + SHA-256: `ca7ab50b2e25a0e5af7599c30383e365983fa5b808e65ce2e1c1bba5bfe8dc3b` -[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v2.6/scrcpy-server-v2.6 +[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v2.6.1/scrcpy-server-v2.6.1 Download the prebuilt server somewhere, and specify its path during the Meson configuration: diff --git a/doc/windows.md b/doc/windows.md index 6842edc6..65ec2b45 100644 --- a/doc/windows.md +++ b/doc/windows.md @@ -4,14 +4,14 @@ Download the [latest release]: - - [`scrcpy-win64-v2.6.zip`][direct-win64] (64-bit) - SHA-256: `3d490a72997af950aec0540e28627ada35c8226bc9774500014c9697d9b53194` - - [`scrcpy-win32-v2.6.zip`][direct-win32] (32-bit) - SHA-256: `6c68f6b31ddef5ed61a7546f423bd4fc99d568eb4c4e3409e0df496187eb3783` + - [`scrcpy-win64-v2.6.1.zip`][direct-win64] (64-bit) + SHA-256: `041fc3abf8578ddcead5a8c4a8be8960b7c4d45b21d3370ee2683605e86a728c` + - [`scrcpy-win32-v2.6.1.zip`][direct-win32] (32-bit) + SHA-256: `17a5d4d17230b4c90fad45af6395efda9aea287a03c04e6b4ecc9ceb8134ea04` [latest release]: https://github.com/Genymobile/scrcpy/releases/latest -[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v2.6/scrcpy-win64-v2.6.zip -[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v2.6/scrcpy-win32-v2.6.zip +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v2.6.1/scrcpy-win64-v2.6.1.zip +[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v2.6.1/scrcpy-win32-v2.6.1.zip and extract it. diff --git a/install_release.sh b/install_release.sh index 9dbf3f73..2aad8cdc 100755 --- a/install_release.sh +++ b/install_release.sh @@ -2,8 +2,8 @@ set -e BUILDDIR=build-auto -PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v2.6/scrcpy-server-v2.6 -PREBUILT_SERVER_SHA256=7b723ff79a27f14e6ebaaaae7ef9548c40651c94e64d178612b13adf7158eb2e +PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v2.6.1/scrcpy-server-v2.6.1 +PREBUILT_SERVER_SHA256=ca7ab50b2e25a0e5af7599c30383e365983fa5b808e65ce2e1c1bba5bfe8dc3b echo "[scrcpy] Downloading prebuilt server..." wget "$PREBUILT_SERVER_URL" -O scrcpy-server From dd47cefa47f6cbd2e453c14df8e52de592ea6661 Mon Sep 17 00:00:00 2001 From: yangfl Date: Mon, 5 Aug 2024 22:02:12 +0800 Subject: [PATCH 1899/2244] Fix typos Refs PR #5171 Signed-off-by: Romain Vimont --- app/scrcpy.1 | 4 ++-- app/src/android/keycodes.h | 2 +- app/src/cli.c | 10 +++++----- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index de2b8ac6..b115e7ff 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -29,7 +29,7 @@ Default is 128K (128000). .BI "\-\-audio\-buffer " ms Configure the audio buffering delay (in milliseconds). -Lower values decrease the latency, but increase the likelyhood of buffer underrun (causing audio glitches). +Lower values decrease the latency, but increase the likelihood of buffer underrun (causing audio glitches). Default is 50. @@ -379,7 +379,7 @@ Default is 27183:27199. .TP \fB\-\-pause\-on\-exit\fR[=\fImode\fR] -Configure pause on exit. Possible values are "true" (always pause on exit), "false" (never pause on exit) and "if-error" (pause only if an error occured). +Configure pause on exit. Possible values are "true" (always pause on exit), "false" (never pause on exit) and "if-error" (pause only if an error occurred). This is useful to prevent the terminal window from automatically closing, so that error messages can be read. diff --git a/app/src/android/keycodes.h b/app/src/android/keycodes.h index 60465a18..03ebb9c8 100644 --- a/app/src/android/keycodes.h +++ b/app/src/android/keycodes.h @@ -633,7 +633,7 @@ enum android_keycode { * Toggles between BS and CS digital satellite services. */ AKEYCODE_TV_SATELLITE_SERVICE = 240, /** Toggle Network key. - * Toggles selecting broacast services. */ + * Toggles selecting broadcast services. */ AKEYCODE_TV_NETWORK = 241, /** Antenna/Cable key. * Toggles broadcast input source between antenna and cable. */ diff --git a/app/src/cli.c b/app/src/cli.c index dd1b6799..a7d85a92 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -156,7 +156,7 @@ static const struct sc_option options[] = { .argdesc = "ms", .text = "Configure the audio buffering delay (in milliseconds).\n" "Lower values decrease the latency, but increase the " - "likelyhood of buffer underrun (causing audio glitches).\n" + "likelihood of buffer underrun (causing audio glitches).\n" "Default is 50.", }, { @@ -654,7 +654,7 @@ static const struct sc_option options[] = { .optional_arg = true, .text = "Configure pause on exit. Possible values are \"true\" (always " "pause on exit), \"false\" (never pause on exit) and " - "\"if-error\" (pause only if an error occured).\n" + "\"if-error\" (pause only if an error occurred).\n" "This is useful to prevent the terminal window from " "automatically closing, so that error messages can be read.\n" "Default is \"false\".\n" @@ -1349,7 +1349,7 @@ print_exit_status(const struct sc_exit_status *status, unsigned cols) { return; } - assert(strlen(text) >= 9); // Contains at least the initial identation + assert(strlen(text) >= 9); // Contains at least the initial indentation // text + 9 to remove the initial indentation printf(" %3d %s\n", status->value, text + 9); @@ -2760,7 +2760,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } } - // If mouse bindings are not explictly set, configure default bindings + // If mouse bindings are not explicitly set, configure default bindings if (opts->mouse_bindings.pri.right_click == SC_MOUSE_BINDING_AUTO) { assert(opts->mouse_bindings.pri.middle_click == SC_MOUSE_BINDING_AUTO); assert(opts->mouse_bindings.pri.click4 == SC_MOUSE_BINDING_AUTO); @@ -3101,7 +3101,7 @@ sc_get_pause_on_exit(int argc, char *argv[]) { if (!strcmp(value, "if-error")) { return SC_PAUSE_ON_EXIT_IF_ERROR; } - // Set to false, inclusing when the value is invalid + // Set to false, including when the value is invalid return SC_PAUSE_ON_EXIT_FALSE; } } From 523f9395326356024f7098107af25c54eab0a6c9 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 8 Aug 2024 20:29:13 +0200 Subject: [PATCH 1900/2244] Do not create UHID thread if not used The HandlerThread is used only via the looper queue. --- .../main/java/com/genymobile/scrcpy/control/UhidManager.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java b/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java index b1e6a9b9..a7d55b7e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java @@ -33,13 +33,13 @@ public final class UhidManager { private final ByteBuffer buffer = ByteBuffer.allocate(SIZE_OF_UHID_EVENT).order(ByteOrder.nativeOrder()); private final DeviceMessageSender sender; - private final HandlerThread thread = new HandlerThread("UHidManager"); private final MessageQueue queue; public UhidManager(DeviceMessageSender sender) { this.sender = sender; - thread.start(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + HandlerThread thread = new HandlerThread("UHidManager"); + thread.start(); queue = thread.getLooper().getQueue(); } else { queue = null; From 0c95794463ee21317725f6934dc55bd8dc83e0c0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 10 Aug 2024 14:27:22 +0200 Subject: [PATCH 1901/2244] Do not apply all workarounds for ONYX devices Calling fillAppInfo() breaks video mirroring on ONYX devices. Fixes #5182 Refs 2b6089cbfc29c41643e0c0e8049bda3ede777b61 --- .../src/main/java/com/genymobile/scrcpy/Workarounds.java | 9 ++++++++- 1 file changed, 8 insertions(+), 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 8fc38555..7de98b72 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java +++ b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java @@ -61,7 +61,14 @@ public final class Workarounds { fillConfigurationController(); } - fillAppInfo(); + // On ONYX devices, fillAppInfo() breaks video mirroring: + // + boolean mustFillAppInfo = !Build.BRAND.equalsIgnoreCase("ONYX"); + + if (mustFillAppInfo) { + fillAppInfo(); + } + fillAppContext(); } From 21e2e2606e683326d9171afb106a5473f16d8bb9 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 29 Aug 2024 14:44:20 +0200 Subject: [PATCH 1902/2244] Fix markdown formatting in documentation --- doc/otg.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/otg.md b/doc/otg.md index 5f42ac9c..c9107e11 100644 --- a/doc/otg.md +++ b/doc/otg.md @@ -11,7 +11,7 @@ device (see [keyboard](keyboard.md) and [mouse](mouse.md)). A special mode (OTG) allows to control the device using AOA [keyboard](keyboard.md#aoa) and [mouse](mouse.md#aoa), without using _adb_ at all (so USB debugging is not necessary). In this mode, video and audio are -disabled, and `--keyboard=aoa and `--mouse=aoa` are implicitly set. +disabled, and `--keyboard=aoa` and `--mouse=aoa` are implicitly set. Therefore, it is possible to run _scrcpy_ with only physical keyboard and mouse simulation, as if the computer keyboard and mouse were plugged directly to the From 3b241af3f684fed977149e47270dfdb5c2d63d39 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:07:15 +0200 Subject: [PATCH 1903/2244] Allow to pass an explicit version name on release To build with a specific version name: VERSION=pr1234 ./release.sh If not set, it will use the result of "git describe" (as before). --- release.mk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release.mk b/release.mk index dd544bae..7f082144 100644 --- a/release.mk +++ b/release.mk @@ -24,7 +24,7 @@ SERVER_BUILD_DIR := build-server WIN32_BUILD_DIR := build-win32 WIN64_BUILD_DIR := build-win64 -VERSION := $(shell git describe --tags --exclude='*install-release' --always) +VERSION ?= $(shell git describe --tags --exclude='*install-release' --always) DIST := dist WIN32_TARGET_DIR := scrcpy-win32-$(VERSION) From 21b412cd9879b850e44fc0cf52012e8f8078bc60 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 7 Sep 2024 14:24:25 +0200 Subject: [PATCH 1904/2244] Simplify messages reader/writer In Java, control messages were parsed using manual buffering, which was convoluted and error-prone. Instead, read the socket directly through a DataInputStream and a BufferedInputStream. Symmetrically, use a DataOutputStream and a BufferedOutputStream to write messages. --- .../scrcpy/control/ControlChannel.java | 21 +- .../scrcpy/control/ControlMessageReader.java | 231 +++++------------- .../control/ControlProtocolException.java | 9 + .../scrcpy/control/DeviceMessageWriter.java | 38 +-- .../control/ControlMessageReaderTest.java | 220 ++++++++--------- .../control/DeviceMessageWriterTest.java | 27 +- 6 files changed, 210 insertions(+), 336 deletions(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/control/ControlProtocolException.java diff --git a/server/src/main/java/com/genymobile/scrcpy/control/ControlChannel.java b/server/src/main/java/com/genymobile/scrcpy/control/ControlChannel.java index f24ca117..2f12cdb3 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/ControlChannel.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlChannel.java @@ -3,31 +3,22 @@ package com.genymobile.scrcpy.control; import android.net.LocalSocket; import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; public final class ControlChannel { - private final InputStream inputStream; - private final OutputStream outputStream; - private final ControlMessageReader reader = new ControlMessageReader(); - private final DeviceMessageWriter writer = new DeviceMessageWriter(); + private final ControlMessageReader reader; + private final DeviceMessageWriter writer; public ControlChannel(LocalSocket controlSocket) throws IOException { - this.inputStream = controlSocket.getInputStream(); - this.outputStream = controlSocket.getOutputStream(); + reader = new ControlMessageReader(controlSocket.getInputStream()); + writer = new DeviceMessageWriter(controlSocket.getOutputStream()); } public ControlMessage recv() throws IOException { - ControlMessage msg = reader.next(); - while (msg == null) { - reader.readFrom(inputStream); - msg = reader.next(); - } - return msg; + return reader.read(); } public void send(DeviceMessage msg) throws IOException { - writer.writeTo(msg, outputStream); + writer.write(msg); } } diff --git a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java index f5cfee75..f2e89da2 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java @@ -1,259 +1,152 @@ package com.genymobile.scrcpy.control; import com.genymobile.scrcpy.util.Binary; -import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.device.Position; -import java.io.EOFException; +import java.io.BufferedInputStream; +import java.io.DataInputStream; import java.io.IOException; import java.io.InputStream; -import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; public class ControlMessageReader { - static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 13; - static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 31; - static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20; - static final int BACK_OR_SCREEN_ON_LENGTH = 1; - static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1; - static final int GET_CLIPBOARD_LENGTH = 1; - static final int SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH = 9; - static final int UHID_CREATE_FIXED_PAYLOAD_LENGTH = 4; - static final int UHID_INPUT_FIXED_PAYLOAD_LENGTH = 4; - private static final int MESSAGE_MAX_SIZE = 1 << 18; // 256k public static final int CLIPBOARD_TEXT_MAX_LENGTH = MESSAGE_MAX_SIZE - 14; // type: 1 byte; sequence: 8 bytes; paste flag: 1 byte; length: 4 bytes public static final int INJECT_TEXT_MAX_LENGTH = 300; - private final byte[] rawBuffer = new byte[MESSAGE_MAX_SIZE]; - private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer); + private final DataInputStream dis; - public ControlMessageReader() { - // invariant: the buffer is always in "get" mode - buffer.limit(0); + public ControlMessageReader(InputStream rawInputStream) { + dis = new DataInputStream(new BufferedInputStream(rawInputStream)); } - public boolean isFull() { - return buffer.remaining() == rawBuffer.length; - } - - public void readFrom(InputStream input) throws IOException { - if (isFull()) { - throw new IllegalStateException("Buffer full, call next() to consume"); - } - buffer.compact(); - int head = buffer.position(); - int r = input.read(rawBuffer, head, rawBuffer.length - head); - if (r == -1) { - throw new EOFException("Controller socket closed"); - } - buffer.position(head + r); - buffer.flip(); - } - - public ControlMessage next() { - if (!buffer.hasRemaining()) { - return null; - } - int savedPosition = buffer.position(); - - int type = buffer.get(); - ControlMessage msg; + public ControlMessage read() throws IOException { + int type = dis.readUnsignedByte(); switch (type) { case ControlMessage.TYPE_INJECT_KEYCODE: - msg = parseInjectKeycode(); - break; + return parseInjectKeycode(); case ControlMessage.TYPE_INJECT_TEXT: - msg = parseInjectText(); - break; + return parseInjectText(); case ControlMessage.TYPE_INJECT_TOUCH_EVENT: - msg = parseInjectTouchEvent(); - break; + return parseInjectTouchEvent(); case ControlMessage.TYPE_INJECT_SCROLL_EVENT: - msg = parseInjectScrollEvent(); - break; + return parseInjectScrollEvent(); case ControlMessage.TYPE_BACK_OR_SCREEN_ON: - msg = parseBackOrScreenOnEvent(); - break; + return parseBackOrScreenOnEvent(); case ControlMessage.TYPE_GET_CLIPBOARD: - msg = parseGetClipboard(); - break; + return parseGetClipboard(); case ControlMessage.TYPE_SET_CLIPBOARD: - msg = parseSetClipboard(); - break; + return parseSetClipboard(); case ControlMessage.TYPE_SET_SCREEN_POWER_MODE: - msg = parseSetScreenPowerMode(); - break; + return parseSetScreenPowerMode(); case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL: case ControlMessage.TYPE_EXPAND_SETTINGS_PANEL: case ControlMessage.TYPE_COLLAPSE_PANELS: case ControlMessage.TYPE_ROTATE_DEVICE: case ControlMessage.TYPE_OPEN_HARD_KEYBOARD_SETTINGS: - msg = ControlMessage.createEmpty(type); - break; + return ControlMessage.createEmpty(type); case ControlMessage.TYPE_UHID_CREATE: - msg = parseUhidCreate(); - break; + return parseUhidCreate(); case ControlMessage.TYPE_UHID_INPUT: - msg = parseUhidInput(); - break; + return parseUhidInput(); default: - Ln.w("Unknown event type: " + type); - msg = null; - break; + throw new ControlProtocolException("Unknown event type: " + type); } - - if (msg == null) { - // failure, reset savedPosition - buffer.position(savedPosition); - } - return msg; } - private ControlMessage parseInjectKeycode() { - if (buffer.remaining() < INJECT_KEYCODE_PAYLOAD_LENGTH) { - return null; - } - int action = Binary.toUnsigned(buffer.get()); - int keycode = buffer.getInt(); - int repeat = buffer.getInt(); - int metaState = buffer.getInt(); + private ControlMessage parseInjectKeycode() throws IOException { + int action = dis.readUnsignedByte(); + int keycode = dis.readInt(); + int repeat = dis.readInt(); + int metaState = dis.readInt(); return ControlMessage.createInjectKeycode(action, keycode, repeat, metaState); } - private int parseBufferLength(int sizeBytes) { + private int parseBufferLength(int sizeBytes) throws IOException { assert sizeBytes > 0 && sizeBytes <= 4; - if (buffer.remaining() < sizeBytes) { - return -1; - } int value = 0; for (int i = 0; i < sizeBytes; ++i) { - value = (value << 8) | (buffer.get() & 0xFF); + value = (value << 8) | dis.readUnsignedByte(); } return value; } - private String parseString() { - int len = parseBufferLength(4); - if (len == -1 || buffer.remaining() < len) { - return null; - } - int position = buffer.position(); - // Move the buffer position to consume the text - buffer.position(position + len); - return new String(rawBuffer, position, len, StandardCharsets.UTF_8); + private String parseString() throws IOException { + byte[] data = parseByteArray(4); + return new String(data, StandardCharsets.UTF_8); } - private byte[] parseByteArray(int sizeBytes) { + private byte[] parseByteArray(int sizeBytes) throws IOException { int len = parseBufferLength(sizeBytes); - if (len == -1 || buffer.remaining() < len) { - return null; - } byte[] data = new byte[len]; - buffer.get(data); + dis.readFully(data); return data; } - private ControlMessage parseInjectText() { + private ControlMessage parseInjectText() throws IOException { String text = parseString(); - if (text == null) { - return null; - } return ControlMessage.createInjectText(text); } - private ControlMessage parseInjectTouchEvent() { - if (buffer.remaining() < INJECT_TOUCH_EVENT_PAYLOAD_LENGTH) { - return null; - } - int action = Binary.toUnsigned(buffer.get()); - long pointerId = buffer.getLong(); - Position position = readPosition(buffer); - float pressure = Binary.u16FixedPointToFloat(buffer.getShort()); - int actionButton = buffer.getInt(); - int buttons = buffer.getInt(); + private ControlMessage parseInjectTouchEvent() throws IOException { + int action = dis.readUnsignedByte(); + long pointerId = dis.readLong(); + Position position = parsePosition(); + float pressure = Binary.u16FixedPointToFloat(dis.readShort()); + int actionButton = dis.readInt(); + int buttons = dis.readInt(); return ControlMessage.createInjectTouchEvent(action, pointerId, position, pressure, actionButton, buttons); } - private ControlMessage parseInjectScrollEvent() { - if (buffer.remaining() < INJECT_SCROLL_EVENT_PAYLOAD_LENGTH) { - return null; - } - Position position = readPosition(buffer); - float hScroll = Binary.i16FixedPointToFloat(buffer.getShort()); - float vScroll = Binary.i16FixedPointToFloat(buffer.getShort()); - int buttons = buffer.getInt(); + private ControlMessage parseInjectScrollEvent() throws IOException { + Position position = parsePosition(); + float hScroll = Binary.i16FixedPointToFloat(dis.readShort()); + float vScroll = Binary.i16FixedPointToFloat(dis.readShort()); + int buttons = dis.readInt(); return ControlMessage.createInjectScrollEvent(position, hScroll, vScroll, buttons); } - private ControlMessage parseBackOrScreenOnEvent() { - if (buffer.remaining() < BACK_OR_SCREEN_ON_LENGTH) { - return null; - } - int action = Binary.toUnsigned(buffer.get()); + private ControlMessage parseBackOrScreenOnEvent() throws IOException { + int action = dis.readUnsignedByte(); return ControlMessage.createBackOrScreenOn(action); } - private ControlMessage parseGetClipboard() { - if (buffer.remaining() < GET_CLIPBOARD_LENGTH) { - return null; - } - int copyKey = Binary.toUnsigned(buffer.get()); + private ControlMessage parseGetClipboard() throws IOException { + int copyKey = dis.readUnsignedByte(); return ControlMessage.createGetClipboard(copyKey); } - private ControlMessage parseSetClipboard() { - if (buffer.remaining() < SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH) { - return null; - } - long sequence = buffer.getLong(); - boolean paste = buffer.get() != 0; + private ControlMessage parseSetClipboard() throws IOException { + long sequence = dis.readLong(); + boolean paste = dis.readByte() != 0; String text = parseString(); - if (text == null) { - return null; - } return ControlMessage.createSetClipboard(sequence, text, paste); } - private ControlMessage parseSetScreenPowerMode() { - if (buffer.remaining() < SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH) { - return null; - } - int mode = buffer.get(); + private ControlMessage parseSetScreenPowerMode() throws IOException { + int mode = dis.readUnsignedByte(); return ControlMessage.createSetScreenPowerMode(mode); } - private ControlMessage parseUhidCreate() { - if (buffer.remaining() < UHID_CREATE_FIXED_PAYLOAD_LENGTH) { - return null; - } - int id = buffer.getShort(); + private ControlMessage parseUhidCreate() throws IOException { + int id = dis.readUnsignedShort(); byte[] data = parseByteArray(2); - if (data == null) { - return null; - } return ControlMessage.createUhidCreate(id, data); } - private ControlMessage parseUhidInput() { - if (buffer.remaining() < UHID_INPUT_FIXED_PAYLOAD_LENGTH) { - return null; - } - int id = buffer.getShort(); + private ControlMessage parseUhidInput() throws IOException { + int id = dis.readUnsignedShort(); byte[] data = parseByteArray(2); - if (data == null) { - return null; - } return ControlMessage.createUhidInput(id, data); } - private static Position readPosition(ByteBuffer buffer) { - int x = buffer.getInt(); - int y = buffer.getInt(); - int screenWidth = Binary.toUnsigned(buffer.getShort()); - int screenHeight = Binary.toUnsigned(buffer.getShort()); + private Position parsePosition() throws IOException { + int x = dis.readInt(); + int y = dis.readInt(); + int screenWidth = dis.readUnsignedShort(); + int screenHeight = dis.readUnsignedShort(); return new Position(x, y, screenWidth, screenHeight); } } diff --git a/server/src/main/java/com/genymobile/scrcpy/control/ControlProtocolException.java b/server/src/main/java/com/genymobile/scrcpy/control/ControlProtocolException.java new file mode 100644 index 00000000..cabf63ee --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlProtocolException.java @@ -0,0 +1,9 @@ +package com.genymobile.scrcpy.control; + +import java.io.IOException; + +public class ControlProtocolException extends IOException { + public ControlProtocolException(String message) { + super(message); + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/control/DeviceMessageWriter.java b/server/src/main/java/com/genymobile/scrcpy/control/DeviceMessageWriter.java index 6bf53bed..a18a2e5d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/DeviceMessageWriter.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/DeviceMessageWriter.java @@ -1,11 +1,11 @@ package com.genymobile.scrcpy.control; -import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.StringUtils; +import java.io.BufferedOutputStream; +import java.io.DataOutputStream; import java.io.IOException; import java.io.OutputStream; -import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; public class DeviceMessageWriter { @@ -13,35 +13,35 @@ public class DeviceMessageWriter { private static final int MESSAGE_MAX_SIZE = 1 << 18; // 256k public static final int CLIPBOARD_TEXT_MAX_LENGTH = MESSAGE_MAX_SIZE - 5; // type: 1 byte; length: 4 bytes - private final byte[] rawBuffer = new byte[MESSAGE_MAX_SIZE]; - private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer); + private final DataOutputStream dos; - public void writeTo(DeviceMessage msg, OutputStream output) throws IOException { - buffer.clear(); - buffer.put((byte) msg.getType()); - switch (msg.getType()) { + public DeviceMessageWriter(OutputStream rawOutputStream) { + dos = new DataOutputStream(new BufferedOutputStream(rawOutputStream)); + } + + public void write(DeviceMessage msg) throws IOException { + int type = msg.getType(); + dos.writeByte(type); + switch (type) { case DeviceMessage.TYPE_CLIPBOARD: String text = msg.getText(); byte[] raw = text.getBytes(StandardCharsets.UTF_8); int len = StringUtils.getUtf8TruncationIndex(raw, CLIPBOARD_TEXT_MAX_LENGTH); - buffer.putInt(len); - buffer.put(raw, 0, len); - output.write(rawBuffer, 0, buffer.position()); + dos.writeInt(len); + dos.write(raw, 0, len); break; case DeviceMessage.TYPE_ACK_CLIPBOARD: - buffer.putLong(msg.getSequence()); - output.write(rawBuffer, 0, buffer.position()); + dos.writeLong(msg.getSequence()); break; case DeviceMessage.TYPE_UHID_OUTPUT: - buffer.putShort((short) msg.getId()); + dos.writeShort(msg.getId()); byte[] data = msg.getData(); - buffer.putShort((short) data.length); - buffer.put(data); - output.write(rawBuffer, 0, buffer.position()); + dos.writeShort(data.length); + dos.write(data); break; default: - Ln.w("Unknown device message: " + msg.getType()); - break; + throw new ControlProtocolException("Unknown event type: " + type); } + dos.flush(); } } diff --git a/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java index 1737730f..ae18154d 100644 --- a/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java @@ -10,6 +10,7 @@ import org.junit.Test; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataOutputStream; +import java.io.EOFException; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.Arrays; @@ -18,8 +19,6 @@ public class ControlMessageReaderTest { @Test public void testParseKeycodeEvent() throws IOException { - ControlMessageReader reader = new ControlMessageReader(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE); @@ -29,23 +28,21 @@ public class ControlMessageReaderTest { dos.writeInt(KeyEvent.META_CTRL_ON); byte[] packet = bos.toByteArray(); - // The message type (1 byte) does not count - Assert.assertEquals(ControlMessageReader.INJECT_KEYCODE_PAYLOAD_LENGTH, packet.length - 1); - - reader.readFrom(new ByteArrayInputStream(packet)); - ControlMessage event = reader.next(); + ByteArrayInputStream bis = new ByteArrayInputStream(packet); + ControlMessageReader reader = new ControlMessageReader(bis); + ControlMessage event = reader.read(); Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType()); Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction()); Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode()); Assert.assertEquals(5, event.getRepeat()); Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); + + Assert.assertEquals(-1, bis.read()); // EOS } @Test public void testParseTextEvent() throws IOException { - ControlMessageReader reader = new ControlMessageReader(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_INJECT_TEXT); @@ -54,17 +51,18 @@ public class ControlMessageReaderTest { dos.write(text); byte[] packet = bos.toByteArray(); - reader.readFrom(new ByteArrayInputStream(packet)); - ControlMessage event = reader.next(); + ByteArrayInputStream bis = new ByteArrayInputStream(packet); + ControlMessageReader reader = new ControlMessageReader(bis); + ControlMessage event = reader.read(); Assert.assertEquals(ControlMessage.TYPE_INJECT_TEXT, event.getType()); Assert.assertEquals("testé", event.getText()); + + Assert.assertEquals(-1, bis.read()); // EOS } @Test public void testParseLongTextEvent() throws IOException { - ControlMessageReader reader = new ControlMessageReader(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_INJECT_TEXT); @@ -74,17 +72,18 @@ public class ControlMessageReaderTest { dos.write(text); byte[] packet = bos.toByteArray(); - reader.readFrom(new ByteArrayInputStream(packet)); - ControlMessage event = reader.next(); + ByteArrayInputStream bis = new ByteArrayInputStream(packet); + ControlMessageReader reader = new ControlMessageReader(bis); + ControlMessage event = reader.read(); Assert.assertEquals(ControlMessage.TYPE_INJECT_TEXT, event.getType()); Assert.assertEquals(new String(text, StandardCharsets.US_ASCII), event.getText()); + + Assert.assertEquals(-1, bis.read()); // EOS } @Test public void testParseTouchEvent() throws IOException { - ControlMessageReader reader = new ControlMessageReader(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_INJECT_TOUCH_EVENT); @@ -100,12 +99,10 @@ public class ControlMessageReaderTest { byte[] packet = bos.toByteArray(); - // The message type (1 byte) does not count - Assert.assertEquals(ControlMessageReader.INJECT_TOUCH_EVENT_PAYLOAD_LENGTH, packet.length - 1); - - reader.readFrom(new ByteArrayInputStream(packet)); - ControlMessage event = reader.next(); + ByteArrayInputStream bis = new ByteArrayInputStream(packet); + ControlMessageReader reader = new ControlMessageReader(bis); + ControlMessage event = reader.read(); Assert.assertEquals(ControlMessage.TYPE_INJECT_TOUCH_EVENT, event.getType()); Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction()); Assert.assertEquals(-42, event.getPointerId()); @@ -116,12 +113,12 @@ public class ControlMessageReaderTest { Assert.assertEquals(1f, event.getPressure(), 0f); // must be exact Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getActionButton()); Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getButtons()); + + Assert.assertEquals(-1, bis.read()); // EOS } @Test public void testParseScrollEvent() throws IOException { - ControlMessageReader reader = new ControlMessageReader(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_INJECT_SCROLL_EVENT); @@ -132,15 +129,12 @@ public class ControlMessageReaderTest { dos.writeShort(0); // 0.0f encoded as i16 dos.writeShort(0x8000); // -1.0f encoded as i16 dos.writeInt(1); - byte[] packet = bos.toByteArray(); - // The message type (1 byte) does not count - Assert.assertEquals(ControlMessageReader.INJECT_SCROLL_EVENT_PAYLOAD_LENGTH, packet.length - 1); - - reader.readFrom(new ByteArrayInputStream(packet)); - ControlMessage event = reader.next(); + ByteArrayInputStream bis = new ByteArrayInputStream(packet); + ControlMessageReader reader = new ControlMessageReader(bis); + ControlMessage event = reader.read(); Assert.assertEquals(ControlMessage.TYPE_INJECT_SCROLL_EVENT, event.getType()); Assert.assertEquals(260, event.getPosition().getPoint().getX()); Assert.assertEquals(1026, event.getPosition().getPoint().getY()); @@ -149,96 +143,96 @@ public class ControlMessageReaderTest { Assert.assertEquals(0f, event.getHScroll(), 0f); Assert.assertEquals(-1f, event.getVScroll(), 0f); Assert.assertEquals(1, event.getButtons()); + + Assert.assertEquals(-1, bis.read()); // EOS } @Test public void testParseBackOrScreenOnEvent() throws IOException { - ControlMessageReader reader = new ControlMessageReader(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_BACK_OR_SCREEN_ON); dos.writeByte(KeyEvent.ACTION_UP); - byte[] packet = bos.toByteArray(); - reader.readFrom(new ByteArrayInputStream(packet)); - ControlMessage event = reader.next(); + ByteArrayInputStream bis = new ByteArrayInputStream(packet); + ControlMessageReader reader = new ControlMessageReader(bis); + ControlMessage event = reader.read(); Assert.assertEquals(ControlMessage.TYPE_BACK_OR_SCREEN_ON, event.getType()); Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction()); + + Assert.assertEquals(-1, bis.read()); // EOS } @Test public void testParseExpandNotificationPanelEvent() throws IOException { - ControlMessageReader reader = new ControlMessageReader(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL); - byte[] packet = bos.toByteArray(); - reader.readFrom(new ByteArrayInputStream(packet)); - ControlMessage event = reader.next(); + ByteArrayInputStream bis = new ByteArrayInputStream(packet); + ControlMessageReader reader = new ControlMessageReader(bis); + ControlMessage event = reader.read(); Assert.assertEquals(ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL, event.getType()); + + Assert.assertEquals(-1, bis.read()); // EOS } @Test public void testParseExpandSettingsPanelEvent() throws IOException { - ControlMessageReader reader = new ControlMessageReader(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_EXPAND_SETTINGS_PANEL); - byte[] packet = bos.toByteArray(); - reader.readFrom(new ByteArrayInputStream(packet)); - ControlMessage event = reader.next(); + ByteArrayInputStream bis = new ByteArrayInputStream(packet); + ControlMessageReader reader = new ControlMessageReader(bis); + ControlMessage event = reader.read(); Assert.assertEquals(ControlMessage.TYPE_EXPAND_SETTINGS_PANEL, event.getType()); + + Assert.assertEquals(-1, bis.read()); // EOS } @Test public void testParseCollapsePanelsEvent() throws IOException { - ControlMessageReader reader = new ControlMessageReader(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_COLLAPSE_PANELS); - byte[] packet = bos.toByteArray(); - reader.readFrom(new ByteArrayInputStream(packet)); - ControlMessage event = reader.next(); + ByteArrayInputStream bis = new ByteArrayInputStream(packet); + ControlMessageReader reader = new ControlMessageReader(bis); + ControlMessage event = reader.read(); Assert.assertEquals(ControlMessage.TYPE_COLLAPSE_PANELS, event.getType()); + + Assert.assertEquals(-1, bis.read()); // EOS } @Test public void testParseGetClipboardEvent() throws IOException { - ControlMessageReader reader = new ControlMessageReader(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_GET_CLIPBOARD); dos.writeByte(ControlMessage.COPY_KEY_COPY); - byte[] packet = bos.toByteArray(); - reader.readFrom(new ByteArrayInputStream(packet)); - ControlMessage event = reader.next(); + ByteArrayInputStream bis = new ByteArrayInputStream(packet); + ControlMessageReader reader = new ControlMessageReader(bis); + ControlMessage event = reader.read(); Assert.assertEquals(ControlMessage.TYPE_GET_CLIPBOARD, event.getType()); Assert.assertEquals(ControlMessage.COPY_KEY_COPY, event.getCopyKey()); + + Assert.assertEquals(-1, bis.read()); // EOS } @Test public void testParseSetClipboardEvent() throws IOException { - ControlMessageReader reader = new ControlMessageReader(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_SET_CLIPBOARD); @@ -247,22 +241,22 @@ public class ControlMessageReaderTest { byte[] text = "testé".getBytes(StandardCharsets.UTF_8); dos.writeInt(text.length); dos.write(text); - byte[] packet = bos.toByteArray(); - reader.readFrom(new ByteArrayInputStream(packet)); - ControlMessage event = reader.next(); + ByteArrayInputStream bis = new ByteArrayInputStream(packet); + ControlMessageReader reader = new ControlMessageReader(bis); + ControlMessage event = reader.read(); Assert.assertEquals(ControlMessage.TYPE_SET_CLIPBOARD, event.getType()); Assert.assertEquals(0x0102030405060708L, event.getSequence()); Assert.assertEquals("testé", event.getText()); Assert.assertTrue(event.getPaste()); + + Assert.assertEquals(-1, bis.read()); // EOS } @Test public void testParseBigSetClipboardEvent() throws IOException { - ControlMessageReader reader = new ControlMessageReader(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_SET_CLIPBOARD); @@ -278,56 +272,54 @@ public class ControlMessageReaderTest { byte[] packet = bos.toByteArray(); - reader.readFrom(new ByteArrayInputStream(packet)); - ControlMessage event = reader.next(); + ByteArrayInputStream bis = new ByteArrayInputStream(packet); + ControlMessageReader reader = new ControlMessageReader(bis); + ControlMessage event = reader.read(); Assert.assertEquals(ControlMessage.TYPE_SET_CLIPBOARD, event.getType()); Assert.assertEquals(0x0807060504030201L, event.getSequence()); Assert.assertEquals(text, event.getText()); Assert.assertTrue(event.getPaste()); + + Assert.assertEquals(-1, bis.read()); // EOS } @Test public void testParseSetScreenPowerMode() throws IOException { - ControlMessageReader reader = new ControlMessageReader(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_SET_SCREEN_POWER_MODE); dos.writeByte(Device.POWER_MODE_NORMAL); - byte[] packet = bos.toByteArray(); - // The message type (1 byte) does not count - Assert.assertEquals(ControlMessageReader.SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH, packet.length - 1); - - reader.readFrom(new ByteArrayInputStream(packet)); - ControlMessage event = reader.next(); + ByteArrayInputStream bis = new ByteArrayInputStream(packet); + ControlMessageReader reader = new ControlMessageReader(bis); + ControlMessage event = reader.read(); Assert.assertEquals(ControlMessage.TYPE_SET_SCREEN_POWER_MODE, event.getType()); Assert.assertEquals(Device.POWER_MODE_NORMAL, event.getAction()); + + Assert.assertEquals(-1, bis.read()); // EOS } @Test public void testParseRotateDevice() throws IOException { - ControlMessageReader reader = new ControlMessageReader(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_ROTATE_DEVICE); - byte[] packet = bos.toByteArray(); - reader.readFrom(new ByteArrayInputStream(packet)); - ControlMessage event = reader.next(); + ByteArrayInputStream bis = new ByteArrayInputStream(packet); + ControlMessageReader reader = new ControlMessageReader(bis); + ControlMessage event = reader.read(); Assert.assertEquals(ControlMessage.TYPE_ROTATE_DEVICE, event.getType()); + + Assert.assertEquals(-1, bis.read()); // EOS } @Test public void testParseUhidCreate() throws IOException { - ControlMessageReader reader = new ControlMessageReader(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_UHID_CREATE); @@ -335,21 +327,21 @@ public class ControlMessageReaderTest { byte[] data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}; dos.writeShort(data.length); // size dos.write(data); - byte[] packet = bos.toByteArray(); - reader.readFrom(new ByteArrayInputStream(packet)); - ControlMessage event = reader.next(); + ByteArrayInputStream bis = new ByteArrayInputStream(packet); + ControlMessageReader reader = new ControlMessageReader(bis); + ControlMessage event = reader.read(); Assert.assertEquals(ControlMessage.TYPE_UHID_CREATE, event.getType()); Assert.assertEquals(42, event.getId()); Assert.assertArrayEquals(data, event.getData()); + + Assert.assertEquals(-1, bis.read()); // EOS } @Test public void testParseUhidInput() throws IOException { - ControlMessageReader reader = new ControlMessageReader(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_UHID_INPUT); @@ -357,37 +349,37 @@ public class ControlMessageReaderTest { byte[] data = {1, 2, 3, 4, 5}; dos.writeShort(data.length); // size dos.write(data); - byte[] packet = bos.toByteArray(); - reader.readFrom(new ByteArrayInputStream(packet)); - ControlMessage event = reader.next(); + ByteArrayInputStream bis = new ByteArrayInputStream(packet); + ControlMessageReader reader = new ControlMessageReader(bis); + ControlMessage event = reader.read(); Assert.assertEquals(ControlMessage.TYPE_UHID_INPUT, event.getType()); Assert.assertEquals(42, event.getId()); Assert.assertArrayEquals(data, event.getData()); + + Assert.assertEquals(-1, bis.read()); // EOS } @Test public void testParseOpenHardKeyboardSettings() throws IOException { - ControlMessageReader reader = new ControlMessageReader(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_OPEN_HARD_KEYBOARD_SETTINGS); - byte[] packet = bos.toByteArray(); - reader.readFrom(new ByteArrayInputStream(packet)); - ControlMessage event = reader.next(); + ByteArrayInputStream bis = new ByteArrayInputStream(packet); + ControlMessageReader reader = new ControlMessageReader(bis); + ControlMessage event = reader.read(); Assert.assertEquals(ControlMessage.TYPE_OPEN_HARD_KEYBOARD_SETTINGS, event.getType()); + + Assert.assertEquals(-1, bis.read()); // EOS } @Test public void testMultiEvents() throws IOException { - ControlMessageReader reader = new ControlMessageReader(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); @@ -404,27 +396,29 @@ public class ControlMessageReaderTest { dos.writeInt(KeyEvent.META_CTRL_ON); byte[] packet = bos.toByteArray(); - reader.readFrom(new ByteArrayInputStream(packet)); - ControlMessage event = reader.next(); + ByteArrayInputStream bis = new ByteArrayInputStream(packet); + ControlMessageReader reader = new ControlMessageReader(bis); + + ControlMessage event = reader.read(); Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType()); Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction()); Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode()); Assert.assertEquals(0, event.getRepeat()); Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); - event = reader.next(); + event = reader.read(); Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType()); Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction()); Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getKeycode()); Assert.assertEquals(1, event.getRepeat()); Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); + + Assert.assertEquals(-1, bis.read()); // EOS } @Test public void testPartialEvents() throws IOException { - ControlMessageReader reader = new ControlMessageReader(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); @@ -438,31 +432,21 @@ public class ControlMessageReaderTest { dos.writeByte(MotionEvent.ACTION_DOWN); byte[] packet = bos.toByteArray(); - reader.readFrom(new ByteArrayInputStream(packet)); + ByteArrayInputStream bis = new ByteArrayInputStream(packet); + ControlMessageReader reader = new ControlMessageReader(bis); - ControlMessage event = reader.next(); + ControlMessage event = reader.read(); Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType()); Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction()); Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode()); Assert.assertEquals(4, event.getRepeat()); Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); - event = reader.next(); - Assert.assertNull(event); // the event is not complete - - bos.reset(); - dos.writeInt(MotionEvent.BUTTON_PRIMARY); - dos.writeInt(5); // repeat - dos.writeInt(KeyEvent.META_CTRL_ON); - packet = bos.toByteArray(); - reader.readFrom(new ByteArrayInputStream(packet)); - - // the event is now complete - event = reader.next(); - Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType()); - Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction()); - Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getKeycode()); - Assert.assertEquals(5, event.getRepeat()); - Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); + try { + event = reader.read(); + Assert.fail("Reader did not reach EOF"); + } catch (EOFException e) { + // expected + } } } diff --git a/server/src/test/java/com/genymobile/scrcpy/control/DeviceMessageWriterTest.java b/server/src/test/java/com/genymobile/scrcpy/control/DeviceMessageWriterTest.java index ff1a2fbc..4e4717fd 100644 --- a/server/src/test/java/com/genymobile/scrcpy/control/DeviceMessageWriterTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/control/DeviceMessageWriterTest.java @@ -12,8 +12,6 @@ public class DeviceMessageWriterTest { @Test public void testSerializeClipboard() throws IOException { - DeviceMessageWriter writer = new DeviceMessageWriter(); - String text = "aéûoç"; byte[] data = text.getBytes(StandardCharsets.UTF_8); ByteArrayOutputStream bos = new ByteArrayOutputStream(); @@ -21,12 +19,13 @@ public class DeviceMessageWriterTest { dos.writeByte(DeviceMessage.TYPE_CLIPBOARD); dos.writeInt(data.length); dos.write(data); - byte[] expected = bos.toByteArray(); - DeviceMessage msg = DeviceMessage.createClipboard(text); bos = new ByteArrayOutputStream(); - writer.writeTo(msg, bos); + DeviceMessageWriter writer = new DeviceMessageWriter(bos); + + DeviceMessage msg = DeviceMessage.createClipboard(text); + writer.write(msg); byte[] actual = bos.toByteArray(); @@ -35,18 +34,17 @@ public class DeviceMessageWriterTest { @Test public void testSerializeAckSetClipboard() throws IOException { - DeviceMessageWriter writer = new DeviceMessageWriter(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(DeviceMessage.TYPE_ACK_CLIPBOARD); dos.writeLong(0x0102030405060708L); - byte[] expected = bos.toByteArray(); - DeviceMessage msg = DeviceMessage.createAckClipboard(0x0102030405060708L); bos = new ByteArrayOutputStream(); - writer.writeTo(msg, bos); + DeviceMessageWriter writer = new DeviceMessageWriter(bos); + + DeviceMessage msg = DeviceMessage.createAckClipboard(0x0102030405060708L); + writer.write(msg); byte[] actual = bos.toByteArray(); @@ -55,8 +53,6 @@ public class DeviceMessageWriterTest { @Test public void testSerializeUhidOutput() throws IOException { - DeviceMessageWriter writer = new DeviceMessageWriter(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(DeviceMessage.TYPE_UHID_OUTPUT); @@ -64,12 +60,13 @@ public class DeviceMessageWriterTest { byte[] data = {1, 2, 3, 4, 5}; dos.writeShort(data.length); dos.write(data); - byte[] expected = bos.toByteArray(); - DeviceMessage msg = DeviceMessage.createUhidOutput(42, data); bos = new ByteArrayOutputStream(); - writer.writeTo(msg, bos); + DeviceMessageWriter writer = new DeviceMessageWriter(bos); + + DeviceMessage msg = DeviceMessage.createUhidOutput(42, data); + writer.write(msg); byte[] actual = bos.toByteArray(); From 903a5aaaf599bf7c9cb9bba0fefbc04aa5ec50bd Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 9 Sep 2024 08:24:52 +0200 Subject: [PATCH 1905/2244] Replace "could not" by "cannot" where appropriate "Could not" implies that the system tried to disable the option but encountered an issue or failure. "Cannot" indicates a rule or restriction, meaning it's not possible to perform the action at all. --- app/src/cli.c | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index a7d85a92..4f4aa551 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -2816,7 +2816,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], if (kmode == SC_KEYBOARD_INPUT_MODE_DISABLED && mmode == SC_MOUSE_INPUT_MODE_DISABLED) { - LOGE("Could not disable both keyboard and mouse in OTG mode."); + LOGE("Cannot disable both keyboard and mouse in OTG mode."); return false; } } @@ -2857,18 +2857,18 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } if (opts->camera_id && opts->camera_facing != SC_CAMERA_FACING_ANY) { - LOGE("Could not specify both --camera-id and --camera-facing"); + LOGE("Cannot specify both --camera-id and --camera-facing"); return false; } if (opts->camera_size) { if (opts->max_size) { - LOGE("Could not specify both --camera-size and -m/--max-size"); + LOGE("Cannot specify both --camera-size and -m/--max-size"); return false; } if (opts->camera_ar) { - LOGE("Could not specify both --camera-size and --camera-ar"); + LOGE("Cannot specify both --camera-size and --camera-ar"); return false; } } @@ -3009,19 +3009,19 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], if (!opts->control) { if (opts->turn_screen_off) { - LOGE("Could not request to turn screen off if control is disabled"); + LOGE("Cannot request to turn screen off if control is disabled"); return false; } if (opts->stay_awake) { - LOGE("Could not request to stay awake if control is disabled"); + LOGE("Cannot request to stay awake if control is disabled"); return false; } if (opts->show_touches) { - LOGE("Could not request to show touches if control is disabled"); + LOGE("Cannot request to show touches if control is disabled"); return false; } if (opts->power_off_on_close) { - LOGE("Could not request power off on close if control is disabled"); + LOGE("Cannot request power off on close if control is disabled"); return false; } } @@ -3046,7 +3046,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], // OTG mode is compatible with only very few options. // Only report obvious errors. if (opts->record_filename) { - LOGE("OTG mode: could not record"); + LOGE("OTG mode: cannot record"); return false; } if (opts->turn_screen_off) { From 33a8c39beb970db5efa6449d4e3058134f3be8e5 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 11 Sep 2024 11:26:07 +0200 Subject: [PATCH 1906/2244] Fix local NDEBUG define The struct definition and the implementation did not use the same NDEBUG constant. --- app/src/delay_buffer.c | 2 -- app/src/delay_buffer.h | 4 +++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/delay_buffer.c b/app/src/delay_buffer.c index f6141b35..b27d4939 100644 --- a/app/src/delay_buffer.c +++ b/app/src/delay_buffer.c @@ -8,8 +8,6 @@ #include "util/log.h" -#define SC_BUFFERING_NDEBUG // comment to debug - /** Downcast frame_sink to sc_delay_buffer */ #define DOWNCAST(SINK) container_of(SINK, struct sc_delay_buffer, frame_sink) diff --git a/app/src/delay_buffer.h b/app/src/delay_buffer.h index 53592372..f0fe97d3 100644 --- a/app/src/delay_buffer.h +++ b/app/src/delay_buffer.h @@ -12,12 +12,14 @@ #include "util/tick.h" #include "util/vecdeque.h" +#define SC_BUFFERING_NDEBUG // comment to debug + // forward declarations typedef struct AVFrame AVFrame; struct sc_delayed_frame { AVFrame *frame; -#ifndef NDEBUG +#ifndef SC_BUFFERING_NDEBUG sc_tick push_date; #endif }; From 63ced79842656d0ea6447a53b267c20e3253b538 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 11 Sep 2024 11:29:00 +0200 Subject: [PATCH 1907/2244] Reverse NDEBUG conditions By default, these specific debug logs are disabled. Make the ifdef condition less confusing. --- app/src/audio_player.c | 10 +++++----- app/src/clock.c | 4 ++-- app/src/delay_buffer.c | 4 ++-- app/src/delay_buffer.h | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/app/src/audio_player.c b/app/src/audio_player.c index dac85bf9..274b6948 100644 --- a/app/src/audio_player.c +++ b/app/src/audio_player.c @@ -5,7 +5,7 @@ #include "util/log.h" -#define SC_AUDIO_PLAYER_NDEBUG // comment to debug +//#define SC_AUDIO_PLAYER_DEBUG // uncomment to debug /** * Real-time audio player with configurable latency @@ -72,7 +72,7 @@ sc_audio_player_sdl_callback(void *userdata, uint8_t *stream, int len_int) { size_t len = len_int; uint32_t count = TO_SAMPLES(len); -#ifndef SC_AUDIO_PLAYER_NDEBUG +#ifdef SC_AUDIO_PLAYER_DEBUG LOGD("[Audio] SDL callback requests %" PRIu32 " samples", count); #endif @@ -162,7 +162,7 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink, // swr_convert() returns the number of samples which would have been // written if the buffer was big enough. uint32_t samples = MIN(ret, dst_nb_samples); -#ifndef SC_AUDIO_PLAYER_NDEBUG +#ifdef SC_AUDIO_PLAYER_DEBUG LOGD("[Audio] %" PRIu32 " samples written to buffer", samples); #endif @@ -244,7 +244,7 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink, if (played) { LOGD("[Audio] Buffering threshold exceeded, skipping %" PRIu32 " samples", skip_samples); -#ifndef SC_AUDIO_PLAYER_NDEBUG +#ifdef SC_AUDIO_PLAYER_DEBUG } else { LOGD("[Audio] Playback not started, skipping %" PRIu32 " samples", skip_samples); @@ -282,7 +282,7 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink, // However, the buffering level must be smoothed sc_average_push(&ap->avg_buffering, can_read); -#ifndef SC_AUDIO_PLAYER_NDEBUG +#ifdef SC_AUDIO_PLAYER_DEBUG LOGD("[Audio] can_read=%" PRIu32 " avg_buffering=%f", can_read, sc_average_get(&ap->avg_buffering)); #endif diff --git a/app/src/clock.c b/app/src/clock.c index 92989bfe..e9c9ac35 100644 --- a/app/src/clock.c +++ b/app/src/clock.c @@ -4,7 +4,7 @@ #include "util/log.h" -#define SC_CLOCK_NDEBUG // comment to debug +//#define SC_CLOCK_DEBUG // uncomment to debug #define SC_CLOCK_RANGE 32 @@ -24,7 +24,7 @@ sc_clock_update(struct sc_clock *clock, sc_tick system, sc_tick stream) { clock->offset = ((clock->range - 1) * clock->offset + offset) / clock->range; -#ifndef SC_CLOCK_NDEBUG +#ifdef SC_CLOCK_DEBUG LOGD("Clock estimation: pts + %" PRItick, clock->offset); #endif } diff --git a/app/src/delay_buffer.c b/app/src/delay_buffer.c index b27d4939..a9dc9a35 100644 --- a/app/src/delay_buffer.c +++ b/app/src/delay_buffer.c @@ -78,7 +78,7 @@ run_buffering(void *data) { goto stopped; } -#ifndef SC_BUFFERING_NDEBUG +#ifdef SC_BUFFERING_DEBUG LOGD("Buffering: %" PRItick ";%" PRItick ";%" PRItick, pts, dframe.push_date, sc_tick_now()); #endif @@ -204,7 +204,7 @@ sc_delay_buffer_frame_sink_push(struct sc_frame_sink *sink, return false; } -#ifndef SC_BUFFERING_NDEBUG +#ifdef SC_BUFFERING_DEBUG dframe.push_date = sc_tick_now(); #endif diff --git a/app/src/delay_buffer.h b/app/src/delay_buffer.h index f0fe97d3..18c1ce94 100644 --- a/app/src/delay_buffer.h +++ b/app/src/delay_buffer.h @@ -12,14 +12,14 @@ #include "util/tick.h" #include "util/vecdeque.h" -#define SC_BUFFERING_NDEBUG // comment to debug +//#define SC_BUFFERING_DEBUG // uncomment to debug // forward declarations typedef struct AVFrame AVFrame; struct sc_delayed_frame { AVFrame *frame; -#ifndef SC_BUFFERING_NDEBUG +#ifdef SC_BUFFERING_DEBUG sc_tick push_date; #endif }; From f089ea67e17784303f46fe0dc1a4abfccbdd9d26 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 11 Sep 2024 15:36:51 +0200 Subject: [PATCH 1908/2244] Add missing flag initialization The delay buffer `stopped` field was not initialized. Since it practice the unique instance of sc_delay_buffer is initialized in static memory, the flag was initialized to false as a side effect. But with commit fd0f432e877153d83ed435474fb7b04e41de4269, in debug mode only, the delay buffer was broken. --- app/src/delay_buffer.c | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/delay_buffer.c b/app/src/delay_buffer.c index a9dc9a35..e89a2092 100644 --- a/app/src/delay_buffer.c +++ b/app/src/delay_buffer.c @@ -132,6 +132,7 @@ sc_delay_buffer_frame_sink_open(struct sc_frame_sink *sink, sc_clock_init(&db->clock); sc_vecdeque_init(&db->queue); + db->stopped = false; if (!sc_frame_source_sinks_open(&db->frame_source, ctx)) { goto error_destroy_wait_cond; From a7cae595780de4bde22447fb22a25953426a09fb Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 12 Sep 2024 10:53:44 +0200 Subject: [PATCH 1909/2244] Improve delay buffer startup The delay buffer clock estimates the clock offset between the PTS and the frame decoded date using an "Exponentially Weighted Moving Average" (EWMA). But for the first frames, the clock have less than SC_CLOCK_RANGE points to average. Since the timing for the first frames are typically the worst ones, give more weight to the last point for the estimation. Once SC_CLOCK_RANGE points are available (i.e. when SC_CLOCK_RANGE == clock->range), the new estimation is equivalent to the previous version. --- app/src/clock.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/clock.c b/app/src/clock.c index e9c9ac35..8a77e514 100644 --- a/app/src/clock.c +++ b/app/src/clock.c @@ -21,8 +21,10 @@ sc_clock_update(struct sc_clock *clock, sc_tick system, sc_tick stream) { } sc_tick offset = system - stream; - clock->offset = ((clock->range - 1) * clock->offset + offset) - / clock->range; + unsigned clock_weight = clock->range - 1; + unsigned value_weight = SC_CLOCK_RANGE - clock->range + 1; + clock->offset = (clock->offset * clock_weight + offset * value_weight) + / SC_CLOCK_RANGE; #ifdef SC_CLOCK_DEBUG LOGD("Clock estimation: pts + %" PRItick, clock->offset); From dea1fe3386b8fdb1b8bb1762ce5c9da4c4590419 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 13 Sep 2024 19:48:44 +0200 Subject: [PATCH 1910/2244] Validate crop and video size A video width or height of 0 triggered an assert. Fail explicitly instead: the server may actually send this size in practice (for example on cropping with small dimensions, even if the requested crop size is not 0). --- app/src/screen.c | 6 ++++++ server/src/main/java/com/genymobile/scrcpy/Options.java | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/app/src/screen.c b/app/src/screen.c index 55a06ab3..dc61e835 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -299,6 +299,12 @@ sc_screen_frame_sink_open(struct sc_frame_sink *sink, struct sc_screen *screen = DOWNCAST(sink); + if (ctx->width <= 0 || ctx->width > 0xFFFF + || ctx->height <= 0 || ctx->height > 0xFFFF) { + LOGE("Invalid video size: %dx%d", ctx->width, ctx->height); + return false; + } + assert(ctx->width > 0 && ctx->width <= 0xFFFF); assert(ctx->height > 0 && ctx->height <= 0xFFFF); // screen->frame_size is never used before the event is pushed, and the diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 2f86d8ce..d07828eb 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -456,8 +456,14 @@ public class Options { } int width = Integer.parseInt(tokens[0]); int height = Integer.parseInt(tokens[1]); + if (width <= 0 || height <= 0) { + throw new IllegalArgumentException("Invalid crop size: " + width + "x" + height); + } int x = Integer.parseInt(tokens[2]); int y = Integer.parseInt(tokens[3]); + if (x < 0 || y < 0) { + throw new IllegalArgumentException("Invalid crop offset: " + x + ":" + y); + } return new Rect(x, y, x + width, y + height); } From bec3321fff4c6dc3b3dbc61fdc6fd98913988a78 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 13 Sep 2024 19:53:05 +0200 Subject: [PATCH 1911/2244] Validate server arguments Some command line arguments are passed as is to "adb shell". Therefore, they must not contain special shell characters. --- app/src/server.c | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/app/src/server.c b/app/src/server.c index 41517f18..b67cb8b2 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -218,6 +218,21 @@ sc_server_get_audio_source_name(enum sc_audio_source audio_source) { } } +static bool +validate_string(const char *s) { + // The parameters values are passed as command line arguments to adb, so + // they must either be properly escaped, or they must not contain any + // special shell characters. + // Since they are not properly escaped on Windows anyway (see + // sys/win/process.c), just forbid special shell characters. + if (strpbrk(s, " ;'\"*$?&`#\\|<>[]{}()!~")) { + LOGE("Invalid server param: [%s]", s); + return false; + } + + return true; +} + static sc_pid execute_server(struct sc_server *server, const struct sc_server_params *params) { @@ -260,6 +275,11 @@ execute_server(struct sc_server *server, } \ cmd[count++] = p; \ } while(0) +#define VALIDATE_STRING(s) do { \ + if (!validate_string(s)) { \ + goto end; \ + } \ + } while(0) ADD_PARAM("scid=%08x", params->scid); ADD_PARAM("log_level=%s", log_level_to_server_string(params->log_level)); @@ -311,6 +331,7 @@ execute_server(struct sc_server *server, ADD_PARAM("tunnel_forward=true"); } if (params->crop) { + VALIDATE_STRING(params->crop); ADD_PARAM("crop=%s", params->crop); } if (!params->control) { @@ -321,9 +342,11 @@ execute_server(struct sc_server *server, ADD_PARAM("display_id=%" PRIu32, params->display_id); } if (params->camera_id) { + VALIDATE_STRING(params->camera_id); ADD_PARAM("camera_id=%s", params->camera_id); } if (params->camera_size) { + VALIDATE_STRING(params->camera_size); ADD_PARAM("camera_size=%s", params->camera_size); } if (params->camera_facing != SC_CAMERA_FACING_ANY) { @@ -331,6 +354,7 @@ execute_server(struct sc_server *server, sc_server_get_camera_facing_name(params->camera_facing)); } if (params->camera_ar) { + VALIDATE_STRING(params->camera_ar); ADD_PARAM("camera_ar=%s", params->camera_ar); } if (params->camera_fps) { @@ -346,15 +370,19 @@ execute_server(struct sc_server *server, ADD_PARAM("stay_awake=true"); } if (params->video_codec_options) { + VALIDATE_STRING(params->video_codec_options); ADD_PARAM("video_codec_options=%s", params->video_codec_options); } if (params->audio_codec_options) { + VALIDATE_STRING(params->audio_codec_options); ADD_PARAM("audio_codec_options=%s", params->audio_codec_options); } if (params->video_encoder) { + VALIDATE_STRING(params->video_encoder); ADD_PARAM("video_encoder=%s", params->video_encoder); } if (params->audio_encoder) { + VALIDATE_STRING(params->audio_encoder); ADD_PARAM("audio_encoder=%s", params->audio_encoder); } if (params->power_off_on_close) { From 6451ad271a0a2a869ceb80836b01e3c35d60cb3d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 13 Sep 2024 20:03:17 +0200 Subject: [PATCH 1912/2244] Ignore minBufferSize on error A negative return value from AudioRecord.getMinBufferSize() represents an error. Only consider positive values (0 would be invalid). Refs #5228 --- .../com/genymobile/scrcpy/audio/AudioDirectCapture.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioDirectCapture.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioDirectCapture.java index 361c7bac..8d4a4c2d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioDirectCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioDirectCapture.java @@ -56,8 +56,11 @@ public class AudioDirectCapture implements AudioCapture { builder.setAudioSource(audioSource); builder.setAudioFormat(AudioConfig.createAudioFormat()); int minBufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, ENCODING); - // This buffer size does not impact latency - builder.setBufferSizeInBytes(8 * minBufferSize); + if (minBufferSize > 0) { + // This buffer size does not impact latency + builder.setBufferSizeInBytes(8 * minBufferSize); + } + return builder.build(); } From 265a15e0b19f5bfe339ccdae01f30a50457ceecf Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 13 Sep 2024 20:03:50 +0200 Subject: [PATCH 1913/2244] Accept float values for --max-fps Android accepts a float value, there is no reason to limit the option to be an integer. In particular, it allows to capture at a rate lower than 1 fps. For example, to capture 1 frame every 5 seconds: scrcpy --video-source=camera --max-fps=0.2 It was already possible to pass a float manually: scrcpy --video-source=camera \ --video-codec-options=max-fps-to-encoder:float=0.2 But accepting a float directly for --max-fps is more convenient. Refs --- app/src/cli.c | 28 ++++++++++++++++--- app/src/options.h | 2 +- app/src/server.c | 2 +- app/src/server.h | 2 +- app/src/util/str.c | 19 +++++++++++++ app/src/util/str.h | 8 ++++++ .../java/com/genymobile/scrcpy/Options.java | 14 ++++++++-- .../scrcpy/video/SurfaceEncoder.java | 8 +++--- 8 files changed, 69 insertions(+), 14 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index 4f4aa551..e2ae4804 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -1447,6 +1447,26 @@ parse_integers_arg(const char *s, const char sep, size_t max_items, long *out, return count; } +static bool +parse_float_arg(const char *s, float *out, float min, float max, + const char *name) { + float value; + bool ok = sc_str_parse_float(s, &value); + if (!ok) { + LOGE("Could not parse %s: %s", name, s); + return false; + } + + if (value < min || value > max) { + LOGE("Could not parse %s: value (%f) out-of-range (%f; %f)", + name, value, min, max); + return false; + } + + *out = value; + return true; +} + static bool parse_bit_rate(const char *s, uint32_t *bit_rate) { long value; @@ -1474,14 +1494,14 @@ parse_max_size(const char *s, uint16_t *max_size) { } static bool -parse_max_fps(const char *s, uint16_t *max_fps) { - long value; - bool ok = parse_integer_arg(s, &value, false, 0, 0xFFFF, "max fps"); +parse_max_fps(const char *s, float *max_fps) { + float value; + bool ok = parse_float_arg(s, &value, 0, (float) (1 << 16), "max fps"); if (!ok) { return false; } - *max_fps = (uint16_t) value; + *max_fps = value; return true; } diff --git a/app/src/options.h b/app/src/options.h index 140d12b1..ee0be00a 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -240,7 +240,7 @@ struct scrcpy_options { uint16_t max_size; uint32_t video_bit_rate; uint32_t audio_bit_rate; - uint16_t max_fps; + float max_fps; enum sc_lock_video_orientation lock_video_orientation; enum sc_orientation display_orientation; enum sc_orientation record_orientation; diff --git a/app/src/server.c b/app/src/server.c index b67cb8b2..320062a4 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -321,7 +321,7 @@ execute_server(struct sc_server *server, ADD_PARAM("max_size=%" PRIu16, params->max_size); } if (params->max_fps) { - ADD_PARAM("max_fps=%" PRIu16, params->max_fps); + ADD_PARAM("max_fps=%f" , params->max_fps); } if (params->lock_video_orientation != SC_LOCK_VIDEO_ORIENTATION_UNLOCKED) { ADD_PARAM("lock_video_orientation=%" PRIi8, diff --git a/app/src/server.h b/app/src/server.h index cffa510e..81e8e05b 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -44,7 +44,7 @@ struct sc_server_params { uint16_t max_size; uint32_t video_bit_rate; uint32_t audio_bit_rate; - uint16_t max_fps; + float max_fps; int8_t lock_video_orientation; bool control; uint32_t display_id; diff --git a/app/src/util/str.c b/app/src/util/str.c index 755369d8..7ca880d7 100644 --- a/app/src/util/str.c +++ b/app/src/util/str.c @@ -147,6 +147,25 @@ sc_str_parse_integer_with_suffix(const char *s, long *out) { return true; } +bool +sc_str_parse_float(const char *s, float *out) { + char *endptr; + if (*s == '\0') { + return false; + } + errno = 0; + float value = strtof(s, &endptr); + if (errno == ERANGE) { + return false; + } + if (*endptr != '\0') { + return false; + } + + *out = value; + return true; +} + bool sc_str_list_contains(const char *list, char sep, const char *s) { char *p; diff --git a/app/src/util/str.h b/app/src/util/str.h index 20da26f0..98cb1d74 100644 --- a/app/src/util/str.h +++ b/app/src/util/str.h @@ -66,6 +66,14 @@ sc_str_parse_integers(const char *s, const char sep, size_t max_items, bool sc_str_parse_integer_with_suffix(const char *s, long *out); +/** + * `Parse `s` as a float into `out` + * + * Return true if the conversion succeeded, false otherwise. + */ +bool +sc_str_parse_float(const char *s, float *out); + /** * Search `s` in the list separated by `sep` * diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index d07828eb..51daeced 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -29,7 +29,7 @@ public class Options { private boolean audioDup; private int videoBitRate = 8000000; private int audioBitRate = 128000; - private int maxFps; + private float maxFps; private int lockVideoOrientation = -1; private boolean tunnelForward; private Rect crop; @@ -113,7 +113,7 @@ public class Options { return audioBitRate; } - public int getMaxFps() { + public float getMaxFps() { return maxFps; } @@ -321,7 +321,7 @@ public class Options { options.audioBitRate = Integer.parseInt(value); break; case "max_fps": - options.maxFps = Integer.parseInt(value); + options.maxFps = parseFloat("max_fps", value); break; case "lock_video_orientation": options.lockVideoOrientation = Integer.parseInt(value); @@ -493,4 +493,12 @@ public class Options { float floatAr = Float.parseFloat(tokens[0]); return CameraAspectRatio.fromFloat(floatAr); } + + private static float parseFloat(String key, String value) { + try { + return Float.parseFloat(value); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid float value for " + key + ": \"" + value + "\""); + } + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java index 8fe0b227..a5f2d1e9 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java @@ -39,7 +39,7 @@ public class SurfaceEncoder implements AsyncProcessor { private final String encoderName; private final List codecOptions; private final int videoBitRate; - private final int maxFps; + private final float maxFps; private final boolean downsizeOnError; private boolean firstFrameSent; @@ -48,8 +48,8 @@ public class SurfaceEncoder implements AsyncProcessor { private Thread thread; private final AtomicBoolean stopped = new AtomicBoolean(); - public SurfaceEncoder(SurfaceCapture capture, Streamer streamer, int videoBitRate, int maxFps, List codecOptions, String encoderName, - boolean downsizeOnError) { + public SurfaceEncoder(SurfaceCapture capture, Streamer streamer, int videoBitRate, float maxFps, List codecOptions, + String encoderName, boolean downsizeOnError) { this.capture = capture; this.streamer = streamer; this.videoBitRate = videoBitRate; @@ -225,7 +225,7 @@ public class SurfaceEncoder implements AsyncProcessor { } } - private static MediaFormat createFormat(String videoMimeType, int bitRate, int maxFps, List codecOptions) { + private static MediaFormat createFormat(String videoMimeType, int bitRate, float maxFps, List codecOptions) { MediaFormat format = new MediaFormat(); format.setString(MediaFormat.KEY_MIME, videoMimeType); format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate); From 1d713d759850f4b5de8525cfb73c338be8fef94e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 14 Sep 2024 14:32:32 +0200 Subject: [PATCH 1914/2244] Do not parse --max-fps float in the client Many parsing and formatting C functions like strtof() and asprintf() are locale-dependent. Forcing a C locale just for the conversions in a way that works on all platforms is a mess. In practice, this is not a problem, scrcpy always uses the C locale, because it never calls: setlocale(LC_ALL, ""); But the max-fps option should not depend on the locale configuration anyway. Since the value is parsed by the client in Java anyway, just forward the string value as is. --- app/src/cli.c | 36 +----------------------------------- app/src/options.c | 2 +- app/src/options.h | 2 +- app/src/server.c | 3 ++- app/src/server.h | 2 +- app/src/util/str.c | 19 ------------------- app/src/util/str.h | 8 -------- app/tests/test_cli.c | 2 +- 8 files changed, 7 insertions(+), 67 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index e2ae4804..e34987f3 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -1447,26 +1447,6 @@ parse_integers_arg(const char *s, const char sep, size_t max_items, long *out, return count; } -static bool -parse_float_arg(const char *s, float *out, float min, float max, - const char *name) { - float value; - bool ok = sc_str_parse_float(s, &value); - if (!ok) { - LOGE("Could not parse %s: %s", name, s); - return false; - } - - if (value < min || value > max) { - LOGE("Could not parse %s: value (%f) out-of-range (%f; %f)", - name, value, min, max); - return false; - } - - *out = value; - return true; -} - static bool parse_bit_rate(const char *s, uint32_t *bit_rate) { long value; @@ -1493,18 +1473,6 @@ parse_max_size(const char *s, uint16_t *max_size) { return true; } -static bool -parse_max_fps(const char *s, float *max_fps) { - float value; - bool ok = parse_float_arg(s, &value, 0, (float) (1 << 16), "max fps"); - if (!ok) { - return false; - } - - *max_fps = value; - return true; -} - static bool parse_buffering_time(const char *s, sc_tick *tick) { long value; @@ -2252,9 +2220,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], "--keyboard=uhid instead."); return false; case OPT_MAX_FPS: - if (!parse_max_fps(optarg, &opts->max_fps)) { - return false; - } + opts->max_fps = optarg; break; case 'm': if (!parse_max_size(optarg, &opts->max_size)) { diff --git a/app/src/options.c b/app/src/options.c index 6fca6ad5..b876b660 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -48,7 +48,7 @@ const struct scrcpy_options scrcpy_options_default = { .max_size = 0, .video_bit_rate = 0, .audio_bit_rate = 0, - .max_fps = 0, + .max_fps = NULL, .lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_UNLOCKED, .display_orientation = SC_ORIENTATION_0, .record_orientation = SC_ORIENTATION_0, diff --git a/app/src/options.h b/app/src/options.h index ee0be00a..6e77c175 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -240,7 +240,7 @@ struct scrcpy_options { uint16_t max_size; uint32_t video_bit_rate; uint32_t audio_bit_rate; - float max_fps; + const char *max_fps; // float to be parsed by the server enum sc_lock_video_orientation lock_video_orientation; enum sc_orientation display_orientation; enum sc_orientation record_orientation; diff --git a/app/src/server.c b/app/src/server.c index 320062a4..2dc00144 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -321,7 +321,8 @@ execute_server(struct sc_server *server, ADD_PARAM("max_size=%" PRIu16, params->max_size); } if (params->max_fps) { - ADD_PARAM("max_fps=%f" , params->max_fps); + VALIDATE_STRING(params->max_fps); + ADD_PARAM("max_fps=%s", params->max_fps); } if (params->lock_video_orientation != SC_LOCK_VIDEO_ORIENTATION_UNLOCKED) { ADD_PARAM("lock_video_orientation=%" PRIi8, diff --git a/app/src/server.h b/app/src/server.h index 81e8e05b..d9d42582 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -44,7 +44,7 @@ struct sc_server_params { uint16_t max_size; uint32_t video_bit_rate; uint32_t audio_bit_rate; - float max_fps; + const char *max_fps; // float to be parsed by the server int8_t lock_video_orientation; bool control; uint32_t display_id; diff --git a/app/src/util/str.c b/app/src/util/str.c index 7ca880d7..755369d8 100644 --- a/app/src/util/str.c +++ b/app/src/util/str.c @@ -147,25 +147,6 @@ sc_str_parse_integer_with_suffix(const char *s, long *out) { return true; } -bool -sc_str_parse_float(const char *s, float *out) { - char *endptr; - if (*s == '\0') { - return false; - } - errno = 0; - float value = strtof(s, &endptr); - if (errno == ERANGE) { - return false; - } - if (*endptr != '\0') { - return false; - } - - *out = value; - return true; -} - bool sc_str_list_contains(const char *list, char sep, const char *s) { char *p; diff --git a/app/src/util/str.h b/app/src/util/str.h index 98cb1d74..20da26f0 100644 --- a/app/src/util/str.h +++ b/app/src/util/str.h @@ -66,14 +66,6 @@ sc_str_parse_integers(const char *s, const char sep, size_t max_items, bool sc_str_parse_integer_with_suffix(const char *s, long *out); -/** - * `Parse `s` as a float into `out` - * - * Return true if the conversion succeeded, false otherwise. - */ -bool -sc_str_parse_float(const char *s, float *out); - /** * Search `s` in the list separated by `sep` * diff --git a/app/tests/test_cli.c b/app/tests/test_cli.c index cef8df3e..14765792 100644 --- a/app/tests/test_cli.c +++ b/app/tests/test_cli.c @@ -78,7 +78,7 @@ static void test_options(void) { assert(opts->video_bit_rate == 5000000); assert(!strcmp(opts->crop, "100:200:300:400")); assert(opts->fullscreen); - assert(opts->max_fps == 30); + assert(!strcmp(opts->max_fps, "30")); assert(opts->max_size == 1024); assert(opts->lock_video_orientation == 2); assert(opts->port_range.first == 1234); From 145a9468fdf64196dc42f24b5935ce7d40ea2543 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 14 Sep 2024 14:37:13 +0200 Subject: [PATCH 1915/2244] Fix ifdef _WIN32 We use _WIN32 across the code base, not __WIN32. --- app/src/compat.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/compat.h b/app/src/compat.h index fd610c02..1995d384 100644 --- a/app/src/compat.h +++ b/app/src/compat.h @@ -8,7 +8,7 @@ #include #include -#ifndef __WIN32 +#ifndef _WIN32 # define PRIu64_ PRIu64 # define SC_PRIsizet "zu" #else From 8453e3ba7d1b4f8ec55825edef2d4357d35ce8c5 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 14 Sep 2024 19:40:43 +0200 Subject: [PATCH 1916/2244] Enable TCP_NODELAY for the control socket It is better to disable Nagle's algorithm to avoid unnecessary latency for control messages. (I'm not sure this has any impact for a local TCP socket though.) --- app/src/server.c | 8 ++++++++ app/src/util/net.c | 17 +++++++++++++++++ app/src/util/net.h | 4 ++++ 3 files changed, 29 insertions(+) diff --git a/app/src/server.c b/app/src/server.c index 2dc00144..e94fcce8 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -659,6 +659,14 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) { } } + if (control_socket != SC_SOCKET_NONE) { + // Disable Nagle's algorithm for the control socket + // (it only impacts the sending side, so it is useless to set it + // for the other sockets) + bool ok = net_set_tcp_nodelay(control_socket, true); + (void) ok; // error already logged + } + // we don't need the adb tunnel anymore sc_adb_tunnel_close(tunnel, &server->intr, serial, server->device_socket_name); diff --git a/app/src/util/net.c b/app/src/util/net.c index 67317ead..d43d1c7a 100644 --- a/app/src/util/net.c +++ b/app/src/util/net.c @@ -15,6 +15,7 @@ # include # include # include +# include # include # include # include @@ -273,6 +274,22 @@ net_close(sc_socket socket) { #endif } +bool +net_set_tcp_nodelay(sc_socket socket, bool tcp_nodelay) { + sc_raw_socket raw_sock = unwrap(socket); + + int value = tcp_nodelay ? 1 : 0; + int ret = setsockopt(raw_sock, IPPROTO_TCP, TCP_NODELAY, + (const void *) &value, sizeof(value)); + if (ret == -1) { + net_perror("setsockopt(TCP_NODELAY)"); + return false; + } + + assert(ret == 0); + return true; +} + bool net_parse_ipv4(const char *s, uint32_t *ipv4) { struct in_addr addr; diff --git a/app/src/util/net.h b/app/src/util/net.h index 21396882..ea54b793 100644 --- a/app/src/util/net.h +++ b/app/src/util/net.h @@ -67,6 +67,10 @@ net_interrupt(sc_socket socket); bool net_close(sc_socket socket); +// Disable Nagle's algorithm (if tcp_nodelay is true) +bool +net_set_tcp_nodelay(sc_socket socket, bool tcp_nodelay); + /** * Parse `ip` "xxx.xxx.xxx.xxx" to an IPv4 host representation */ From e03888d5877e04cb1f4575099eda54d9c009ca3a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 14 Sep 2024 21:21:48 +0200 Subject: [PATCH 1917/2244] Reject arguments containing new line characters Refs bec3321fff4c6dc3b3dbc61fdc6fd98913988a78 --- 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 e94fcce8..90a0ac5d 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -225,7 +225,7 @@ validate_string(const char *s) { // special shell characters. // Since they are not properly escaped on Windows anyway (see // sys/win/process.c), just forbid special shell characters. - if (strpbrk(s, " ;'\"*$?&`#\\|<>[]{}()!~")) { + if (strpbrk(s, " ;'\"*$?&`#\\|<>[]{}()!~\r\n")) { LOGE("Invalid server param: [%s]", s); return false; } From 90ee0062cb84b618882f78165f057f58d97b6939 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 13 Sep 2024 22:00:43 +0200 Subject: [PATCH 1918/2244] Fix compilation with -Dusb=false UHID does not depend on USB support, so the struct sc_uhid_devices must always be defined. --- 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 43864661..aafe108c 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -63,8 +63,8 @@ struct scrcpy { struct sc_aoa aoa; // sequence/ack helper to synchronize clipboard and Ctrl+v via HID struct sc_acksync acksync; - struct sc_uhid_devices uhid_devices; #endif + struct sc_uhid_devices uhid_devices; union { struct sc_keyboard_sdk keyboard_sdk; struct sc_keyboard_uhid keyboard_uhid; From 4a6b335f7df04599cbea488f40848c96dc2e7541 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 7 Sep 2024 18:35:16 +0200 Subject: [PATCH 1919/2244] Do not send uninitialized HID event If the function returns false, then there is nothing to send. --- app/src/uhid/keyboard_uhid.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/uhid/keyboard_uhid.c b/app/src/uhid/keyboard_uhid.c index 515a3fd9..4199547a 100644 --- a/app/src/uhid/keyboard_uhid.c +++ b/app/src/uhid/keyboard_uhid.c @@ -43,7 +43,9 @@ sc_keyboard_uhid_synchronize_mod(struct sc_keyboard_uhid *kb) { atomic_store_explicit(&kb->device_mod, mod, memory_order_relaxed); struct sc_hid_event hid_event; - sc_hid_keyboard_event_from_mods(&hid_event, diff); + if (!sc_hid_keyboard_event_from_mods(&hid_event, diff)) { + return; + } LOGV("HID keyboard state synchronized"); From e8f02685e94608ea8d352d4fc33f0ad77f94ab61 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:08:08 +0200 Subject: [PATCH 1920/2244] Fix deprecated references in scrcpy manpage The options --hid-keyboard and --hid-mouse do not exist anymore. They have been replaced by --keyboard=XXX and --mouse=XXX. --- app/scrcpy.1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index b115e7ff..9cbb6fcb 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -369,7 +369,7 @@ If any of \fB\-\-hid\-keyboard\fR or \fB\-\-hid\-mouse\fR is set, only enable ke It may only work over USB. -See \fB\-\-hid\-keyboard\fR and \fB\-\-hid\-mouse\fR. +See \fB\-\-keyboard\fR and \fB\-\-mouse\fR. .TP .BI "\-p, \-\-port " port\fR[:\fIport\fR] From ce4e1fc4204d08b57a3fc63a9d39ad5bd9984812 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:08:08 +0200 Subject: [PATCH 1921/2244] Store events numbers in an enum This avoids to manually set an explicit value for each item. PR #5270 --- app/src/events.h | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/app/src/events.h b/app/src/events.h index 3cf2b1dd..f1aa22aa 100644 --- a/app/src/events.h +++ b/app/src/events.h @@ -1,10 +1,21 @@ -#define SC_EVENT_NEW_FRAME SDL_USEREVENT -#define SC_EVENT_DEVICE_DISCONNECTED (SDL_USEREVENT + 1) -#define SC_EVENT_SERVER_CONNECTION_FAILED (SDL_USEREVENT + 2) -#define SC_EVENT_SERVER_CONNECTED (SDL_USEREVENT + 3) -#define SC_EVENT_USB_DEVICE_DISCONNECTED (SDL_USEREVENT + 4) -#define SC_EVENT_DEMUXER_ERROR (SDL_USEREVENT + 5) -#define SC_EVENT_RECORDER_ERROR (SDL_USEREVENT + 6) -#define SC_EVENT_SCREEN_INIT_SIZE (SDL_USEREVENT + 7) -#define SC_EVENT_TIME_LIMIT_REACHED (SDL_USEREVENT + 8) -#define SC_EVENT_CONTROLLER_ERROR (SDL_USEREVENT + 9) +#ifndef SC_EVENTS_H +#define SC_EVENTS_H + +#include "common.h" + +#include + +enum { + SC_EVENT_NEW_FRAME = SDL_USEREVENT, + SC_EVENT_DEVICE_DISCONNECTED, + SC_EVENT_SERVER_CONNECTION_FAILED, + SC_EVENT_SERVER_CONNECTED, + SC_EVENT_USB_DEVICE_DISCONNECTED, + SC_EVENT_DEMUXER_ERROR, + SC_EVENT_RECORDER_ERROR, + SC_EVENT_SCREEN_INIT_SIZE, + SC_EVENT_TIME_LIMIT_REACHED, + SC_EVENT_CONTROLLER_ERROR, +}; + +#endif From e9b32d8a52b418eed8fb00b84de89f6c7ac9e5a0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:08:08 +0200 Subject: [PATCH 1922/2244] Extract sc_push_event() Expose a convenience function to push an event without args to the main thread. PR #5270 --- app/meson.build | 1 + app/src/events.c | 19 +++++++++++++++++++ app/src/events.h | 7 +++++++ app/src/scrcpy.c | 34 +++++++++++----------------------- app/src/screen.c | 18 ++++-------------- app/src/usb/scrcpy_otg.c | 7 +------ 6 files changed, 43 insertions(+), 43 deletions(-) create mode 100644 app/src/events.c diff --git a/app/meson.build b/app/meson.build index b0a6aadb..fc6b85e2 100644 --- a/app/meson.build +++ b/app/meson.build @@ -15,6 +15,7 @@ src = [ 'src/demuxer.c', 'src/device_msg.c', 'src/display.c', + 'src/events.c', 'src/icon.c', 'src/file_pusher.c', 'src/fps_counter.c', diff --git a/app/src/events.c b/app/src/events.c new file mode 100644 index 00000000..4e256be2 --- /dev/null +++ b/app/src/events.c @@ -0,0 +1,19 @@ +#include "events.h" + +#include "util/log.h" + +bool +sc_push_event_impl(uint32_t type, const char *name) { + SDL_Event event; + event.type = type; + int ret = SDL_PushEvent(&event); + // ret < 0: error (queue full) + // ret == 0: event was filtered + // ret == 1: success + if (ret != 1) { + LOGE("Could not post %s event: %s", name, SDL_GetError()); + return false; + } + + return true; +} diff --git a/app/src/events.h b/app/src/events.h index f1aa22aa..d803fb68 100644 --- a/app/src/events.h +++ b/app/src/events.h @@ -3,6 +3,8 @@ #include "common.h" +#include +#include #include enum { @@ -18,4 +20,9 @@ enum { SC_EVENT_CONTROLLER_ERROR, }; +bool +sc_push_event_impl(uint32_t type, const char *name); + +#define sc_push_event(TYPE) sc_push_event_impl(TYPE, # TYPE) + #endif diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index aafe108c..625a53a9 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -82,22 +82,10 @@ struct scrcpy { struct sc_timeout timeout; }; -static inline void -push_event(uint32_t type, const char *name) { - SDL_Event event; - event.type = type; - int ret = SDL_PushEvent(&event); - if (ret < 0) { - LOGE("Could not post %s event: %s", name, SDL_GetError()); - // What could we do? - } -} -#define PUSH_EVENT(TYPE) push_event(TYPE, # TYPE) - #ifdef _WIN32 static BOOL WINAPI windows_ctrl_handler(DWORD ctrl_type) { if (ctrl_type == CTRL_C_EVENT) { - PUSH_EVENT(SDL_QUIT); + sc_push_event(SDL_QUIT); return TRUE; } return FALSE; @@ -230,7 +218,7 @@ sc_recorder_on_ended(struct sc_recorder *recorder, bool success, (void) userdata; if (!success) { - PUSH_EVENT(SC_EVENT_RECORDER_ERROR); + sc_push_event(SC_EVENT_RECORDER_ERROR); } } @@ -244,9 +232,9 @@ sc_video_demuxer_on_ended(struct sc_demuxer *demuxer, assert(status != SC_DEMUXER_STATUS_DISABLED); if (status == SC_DEMUXER_STATUS_EOS) { - PUSH_EVENT(SC_EVENT_DEVICE_DISCONNECTED); + sc_push_event(SC_EVENT_DEVICE_DISCONNECTED); } else { - PUSH_EVENT(SC_EVENT_DEMUXER_ERROR); + sc_push_event(SC_EVENT_DEMUXER_ERROR); } } @@ -260,11 +248,11 @@ sc_audio_demuxer_on_ended(struct sc_demuxer *demuxer, // Contrary to the video demuxer, keep mirroring if only the audio fails // (unless --require-audio is set). if (status == SC_DEMUXER_STATUS_EOS) { - PUSH_EVENT(SC_EVENT_DEVICE_DISCONNECTED); + sc_push_event(SC_EVENT_DEVICE_DISCONNECTED); } else if (status == SC_DEMUXER_STATUS_ERROR || (status == SC_DEMUXER_STATUS_DISABLED && options->require_audio)) { - PUSH_EVENT(SC_EVENT_DEMUXER_ERROR); + sc_push_event(SC_EVENT_DEMUXER_ERROR); } } @@ -277,9 +265,9 @@ sc_controller_on_ended(struct sc_controller *controller, bool error, (void) userdata; if (error) { - PUSH_EVENT(SC_EVENT_CONTROLLER_ERROR); + sc_push_event(SC_EVENT_CONTROLLER_ERROR); } else { - PUSH_EVENT(SC_EVENT_DEVICE_DISCONNECTED); + sc_push_event(SC_EVENT_DEVICE_DISCONNECTED); } } @@ -288,7 +276,7 @@ sc_server_on_connection_failed(struct sc_server *server, void *userdata) { (void) server; (void) userdata; - PUSH_EVENT(SC_EVENT_SERVER_CONNECTION_FAILED); + sc_push_event(SC_EVENT_SERVER_CONNECTION_FAILED); } static void @@ -296,7 +284,7 @@ sc_server_on_connected(struct sc_server *server, void *userdata) { (void) server; (void) userdata; - PUSH_EVENT(SC_EVENT_SERVER_CONNECTED); + sc_push_event(SC_EVENT_SERVER_CONNECTED); } static void @@ -314,7 +302,7 @@ sc_timeout_on_timeout(struct sc_timeout *timeout, void *userdata) { (void) timeout; (void) userdata; - PUSH_EVENT(SC_EVENT_TIME_LIMIT_REACHED); + sc_push_event(SC_EVENT_TIME_LIMIT_REACHED); } // Generate a scrcpy id to differentiate multiple running scrcpy instances diff --git a/app/src/screen.c b/app/src/screen.c index dc61e835..42be554a 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -312,14 +312,9 @@ sc_screen_frame_sink_open(struct sc_frame_sink *sink, screen->frame_size.width = ctx->width; screen->frame_size.height = ctx->height; - static SDL_Event event = { - .type = SC_EVENT_SCREEN_INIT_SIZE, - }; - // Post the event on the UI thread (the texture must be created from there) - int ret = SDL_PushEvent(&event); - if (ret < 0) { - LOGW("Could not post init size event: %s", SDL_GetError()); + bool ok = sc_push_event(SC_EVENT_SCREEN_INIT_SIZE); + if (!ok) { return false; } @@ -358,14 +353,9 @@ sc_screen_frame_sink_push(struct sc_frame_sink *sink, const AVFrame *frame) { // The SC_EVENT_NEW_FRAME triggered for the previous frame will consume // this new frame instead } else { - static SDL_Event new_frame_event = { - .type = SC_EVENT_NEW_FRAME, - }; - // Post the event on the UI thread - int ret = SDL_PushEvent(&new_frame_event); - if (ret < 0) { - LOGW("Could not post new frame event: %s", SDL_GetError()); + bool ok = sc_push_event(SC_EVENT_NEW_FRAME); + if (!ok) { return false; } } diff --git a/app/src/usb/scrcpy_otg.c b/app/src/usb/scrcpy_otg.c index c1d38da3..715f690a 100644 --- a/app/src/usb/scrcpy_otg.c +++ b/app/src/usb/scrcpy_otg.c @@ -21,12 +21,7 @@ sc_usb_on_disconnected(struct sc_usb *usb, void *userdata) { (void) usb; (void) userdata; - SDL_Event event; - event.type = SC_EVENT_USB_DEVICE_DISCONNECTED; - int ret = SDL_PushEvent(&event); - if (ret < 0) { - LOGE("Could not post USB disconnection event: %s", SDL_GetError()); - } + sc_push_event(SC_EVENT_USB_DEVICE_DISCONNECTED); } static enum scrcpy_exit_code From e9240f6804764b2b4d7163566cd6d5cc0226f865 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:08:08 +0200 Subject: [PATCH 1923/2244] Expose main thread id This will allow to assert that a function is called from the main thread. PR #5270 --- app/src/main.c | 4 ++++ app/src/util/thread.c | 2 ++ app/src/util/thread.h | 2 ++ 3 files changed, 8 insertions(+) diff --git a/app/src/main.c b/app/src/main.c index 6050de11..8bbd074f 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -16,6 +16,7 @@ #include "usb/scrcpy_otg.h" #include "util/log.h" #include "util/net.h" +#include "util/thread.h" #include "version.h" #ifdef _WIN32 @@ -67,6 +68,9 @@ main_scrcpy(int argc, char *argv[]) { goto end; } + // The current thread is the main thread + SC_MAIN_THREAD_ID = sc_thread_get_id(); + #ifdef SCRCPY_LAVF_REQUIRES_REGISTER_ALL av_register_all(); #endif diff --git a/app/src/util/thread.c b/app/src/util/thread.c index 94921fb7..9679dfff 100644 --- a/app/src/util/thread.c +++ b/app/src/util/thread.c @@ -6,6 +6,8 @@ #include "log.h" +sc_thread_id SC_MAIN_THREAD_ID; + bool sc_thread_create(sc_thread *thread, sc_thread_fn fn, const char *name, void *userdata) { diff --git a/app/src/util/thread.h b/app/src/util/thread.h index 4183adac..3d544046 100644 --- a/app/src/util/thread.h +++ b/app/src/util/thread.h @@ -39,6 +39,8 @@ typedef struct sc_cond { SDL_cond *cond; } sc_cond; +extern sc_thread_id SC_MAIN_THREAD_ID; + bool sc_thread_create(sc_thread *thread, sc_thread_fn fn, const char *name, void *userdata); From 8620d06741ceb05ef2ad34e1a811c452216092a5 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:08:08 +0200 Subject: [PATCH 1924/2244] Add mechanism to execute code on the main thread This allows to schedule a runnable to be executed on the main thread, until the event loop is explicitly terminated. It is guaranteed that all accepted runnables will be executed (this avoids possible memory leaks if a runnable owns resources). PR #5270 --- app/src/events.c | 47 +++++++++++++++++++++++++++++++++++++++++++++++ app/src/events.h | 9 +++++++++ app/src/scrcpy.c | 22 ++++++++++++++++++++++ 3 files changed, 78 insertions(+) diff --git a/app/src/events.c b/app/src/events.c index 4e256be2..ce885241 100644 --- a/app/src/events.c +++ b/app/src/events.c @@ -1,6 +1,7 @@ #include "events.h" #include "util/log.h" +#include "util/thread.h" bool sc_push_event_impl(uint32_t type, const char *name) { @@ -17,3 +18,49 @@ sc_push_event_impl(uint32_t type, const char *name) { return true; } + +bool +sc_post_to_main_thread(sc_runnable_fn run, void *userdata) { + SDL_Event event = { + .user = { + .type = SC_EVENT_RUN_ON_MAIN_THREAD, + .data1 = run, + .data2 = userdata, + }, + }; + int ret = SDL_PushEvent(&event); + // ret < 0: error (queue full) + // ret == 0: event was filtered + // ret == 1: success + if (ret != 1) { + if (ret == 0) { + // if ret == 0, this is expected on exit, log in debug mode + LOGD("Could not post runnable to main thread (filtered)"); + } else { + assert(ret < 0); + LOGW("Could not post runnable to main thread: %s", SDL_GetError()); + } + return false; + } + + return true; +} + +static int SDLCALL +task_event_filter(void *userdata, SDL_Event *event) { + (void) userdata; + + if (event->type == SC_EVENT_RUN_ON_MAIN_THREAD) { + // Reject this event type from now on + return 0; + } + + return 1; +} + +void +sc_reject_new_runnables(void) { + assert(sc_thread_get_id() == SC_MAIN_THREAD_ID); + + SDL_SetEventFilter(task_event_filter, NULL); +} diff --git a/app/src/events.h b/app/src/events.h index d803fb68..3f15087a 100644 --- a/app/src/events.h +++ b/app/src/events.h @@ -9,6 +9,7 @@ enum { SC_EVENT_NEW_FRAME = SDL_USEREVENT, + SC_EVENT_RUN_ON_MAIN_THREAD, SC_EVENT_DEVICE_DISCONNECTED, SC_EVENT_SERVER_CONNECTION_FAILED, SC_EVENT_SERVER_CONNECTED, @@ -25,4 +26,12 @@ sc_push_event_impl(uint32_t type, const char *name); #define sc_push_event(TYPE) sc_push_event_impl(TYPE, # TYPE) +typedef void (*sc_runnable_fn)(void *userdata); + +bool +sc_post_to_main_thread(sc_runnable_fn run, void *userdata); + +void +sc_reject_new_runnables(void); + #endif diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 625a53a9..efad5891 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -174,6 +174,12 @@ event_loop(struct scrcpy *s) { case SDL_QUIT: LOGD("User requested to quit"); return SCRCPY_EXIT_SUCCESS; + case SC_EVENT_RUN_ON_MAIN_THREAD: { + sc_runnable_fn run = event.user.data1; + void *userdata = event.user.data2; + run(userdata); + break; + } default: if (!sc_screen_handle_event(&s->screen, &event)) { return SCRCPY_EXIT_FAILURE; @@ -184,6 +190,21 @@ event_loop(struct scrcpy *s) { return SCRCPY_EXIT_FAILURE; } +static void +terminate_event_loop(void) { + sc_reject_new_runnables(); + + SDL_Event event; + while (SDL_PollEvent(&event)) { + if (event.type == SC_EVENT_RUN_ON_MAIN_THREAD) { + // Make sure all posted runnables are run, to avoid memory leaks + sc_runnable_fn run = event.user.data1; + void *userdata = event.user.data2; + run(userdata); + } + } +} + // Return true on success, false on error static bool await_for_server(bool *connected) { @@ -819,6 +840,7 @@ scrcpy(struct scrcpy_options *options) { } ret = event_loop(s); + terminate_event_loop(); LOGD("quit..."); if (options->video_playback) { From 72ee195693a030990e9965ac93922c9aae32988d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:08:08 +0200 Subject: [PATCH 1925/2244] Set clipboard from the main thread The clipboard changes from the device are received from a separate thread, but they must be handled from the main thread. PR #5270 --- app/src/receiver.c | 41 +++++++++++++++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/app/src/receiver.c b/app/src/receiver.c index 3e572067..2911f8b9 100644 --- a/app/src/receiver.c +++ b/app/src/receiver.c @@ -6,8 +6,10 @@ #include #include "device_msg.h" +#include "events.h" #include "util/log.h" #include "util/str.h" +#include "util/thread.h" bool sc_receiver_init(struct sc_receiver *receiver, sc_socket control_socket, @@ -33,20 +35,39 @@ sc_receiver_destroy(struct sc_receiver *receiver) { sc_mutex_destroy(&receiver->mutex); } +static void +task_set_clipboard(void *userdata) { + assert(sc_thread_get_id() == SC_MAIN_THREAD_ID); + + char *text = userdata; + + char *current = SDL_GetClipboardText(); + bool same = current && !strcmp(current, text); + SDL_free(current); + if (same) { + LOGD("Computer clipboard unchanged"); + } else { + LOGI("Device clipboard copied"); + SDL_SetClipboardText(text); + } + + free(text); +} + static void process_msg(struct sc_receiver *receiver, struct sc_device_msg *msg) { switch (msg->type) { case DEVICE_MSG_TYPE_CLIPBOARD: { - char *current = SDL_GetClipboardText(); - bool same = current && !strcmp(current, msg->clipboard.text); - SDL_free(current); - if (same) { - LOGD("Computer clipboard unchanged"); + // Take ownership of the text (do not destroy the msg) + char *text = msg->clipboard.text; + + bool ok = sc_post_to_main_thread(task_set_clipboard, text); + if (!ok) { + LOGW("Could not post clipboard to main thread"); + free(text); return; } - LOGI("Device clipboard copied"); - SDL_SetClipboardText(msg->clipboard.text); break; } case DEVICE_MSG_TYPE_ACK_CLIPBOARD: @@ -64,6 +85,7 @@ process_msg(struct sc_receiver *receiver, struct sc_device_msg *msg) { } sc_acksync_ack(receiver->acksync, msg->ack_clipboard.sequence); + // No allocation to free in the msg break; case DEVICE_MSG_TYPE_UHID_OUTPUT: if (sc_get_log_level() <= SC_LOG_LEVEL_VERBOSE) { @@ -86,6 +108,7 @@ process_msg(struct sc_receiver *receiver, struct sc_device_msg *msg) { // Also check at runtime (do not trust the server) if (!receiver->uhid_devices) { LOGE("Received unexpected HID output message"); + sc_device_msg_destroy(msg); return; } @@ -99,6 +122,8 @@ process_msg(struct sc_receiver *receiver, struct sc_device_msg *msg) { } else { LOGW("No UHID receiver for id %" PRIu16, msg->uhid_output.id); } + + sc_device_msg_destroy(msg); break; } } @@ -117,7 +142,7 @@ process_msgs(struct sc_receiver *receiver, const uint8_t *buf, size_t len) { } process_msg(receiver, &msg); - sc_device_msg_destroy(&msg); + // the device msg must be destroyed by process_msg() head += r; assert(head <= len); From cbf5db85c1931d07e9adad32d788fdf84366b74d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:08:08 +0200 Subject: [PATCH 1926/2244] Process UHID outputs events from the main thread This will guarantee that the callbacks of UHID devices implementations will always be called from the same (main) thread. PR #5270 --- app/src/receiver.c | 57 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 47 insertions(+), 10 deletions(-) diff --git a/app/src/receiver.c b/app/src/receiver.c index 2911f8b9..42682cb4 100644 --- a/app/src/receiver.c +++ b/app/src/receiver.c @@ -11,6 +11,13 @@ #include "util/str.h" #include "util/thread.h" +struct sc_uhid_output_task_data { + struct sc_uhid_devices *uhid_devices; + uint16_t id; + uint16_t size; + uint8_t *data; +}; + bool sc_receiver_init(struct sc_receiver *receiver, sc_socket control_socket, const struct sc_receiver_callbacks *cbs, void *cbs_userdata) { @@ -54,6 +61,25 @@ task_set_clipboard(void *userdata) { free(text); } +static void +task_uhid_output(void *userdata) { + assert(sc_thread_get_id() == SC_MAIN_THREAD_ID); + + struct sc_uhid_output_task_data *data = userdata; + + struct sc_uhid_receiver *uhid_receiver = + sc_uhid_devices_get_receiver(data->uhid_devices, data->id); + if (uhid_receiver) { + uhid_receiver->ops->process_output(uhid_receiver, data->data, + data->size); + } else { + LOGW("No UHID receiver for id %" PRIu16, data->id); + } + + free(data->data); + free(data); +} + static void process_msg(struct sc_receiver *receiver, struct sc_device_msg *msg) { switch (msg->type) { @@ -112,18 +138,29 @@ process_msg(struct sc_receiver *receiver, struct sc_device_msg *msg) { return; } - struct sc_uhid_receiver *uhid_receiver = - sc_uhid_devices_get_receiver(receiver->uhid_devices, - msg->uhid_output.id); - if (uhid_receiver) { - uhid_receiver->ops->process_output(uhid_receiver, - msg->uhid_output.data, - msg->uhid_output.size); - } else { - LOGW("No UHID receiver for id %" PRIu16, msg->uhid_output.id); + struct sc_uhid_output_task_data *data = malloc(sizeof(*data)); + if (!data) { + LOG_OOM(); + return; + } + + // It is guaranteed that these pointers will still be valid when + // the main thread will process them (the main thread will stop + // processing SC_EVENT_RUN_ON_MAIN_THREAD on exit, when everything + // gets deinitialized) + data->uhid_devices = receiver->uhid_devices; + data->id = msg->uhid_output.id; + data->data = msg->uhid_output.data; // take ownership + data->size = msg->uhid_output.size; + + bool ok = sc_post_to_main_thread(task_uhid_output, data); + if (!ok) { + LOGW("Could not post UHID output to main thread"); + free(data->data); + free(data); + return; } - sc_device_msg_destroy(msg); break; } } From a84b0dfd0c3abe7a91e21d8fcd4b3ec33a36964a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:08:08 +0200 Subject: [PATCH 1927/2244] Remove atomics from keyboard_uhid The UHID output callback is now called from the same (main) thread as the process_key() function. PR #5270 --- app/src/uhid/keyboard_uhid.c | 21 +++++++++------------ app/src/uhid/keyboard_uhid.h | 2 +- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/app/src/uhid/keyboard_uhid.c b/app/src/uhid/keyboard_uhid.c index 4199547a..d63d0ab0 100644 --- a/app/src/uhid/keyboard_uhid.c +++ b/app/src/uhid/keyboard_uhid.c @@ -31,16 +31,13 @@ static void sc_keyboard_uhid_synchronize_mod(struct sc_keyboard_uhid *kb) { SDL_Keymod sdl_mod = SDL_GetModState(); uint16_t mod = sc_mods_state_from_sdl(sdl_mod) & (SC_MOD_CAPS | SC_MOD_NUM); - - uint16_t device_mod = - atomic_load_explicit(&kb->device_mod, memory_order_relaxed); - uint16_t diff = mod ^ device_mod; + uint16_t diff = mod ^ kb->device_mod; if (diff) { // Inherently racy (the HID output reports arrive asynchronously in // response to key presses), but will re-synchronize on next key press // or HID output anyway - atomic_store_explicit(&kb->device_mod, mod, memory_order_relaxed); + kb->device_mod = mod; struct sc_hid_event hid_event; if (!sc_hid_keyboard_event_from_mods(&hid_event, diff)) { @@ -59,6 +56,8 @@ sc_key_processor_process_key(struct sc_key_processor *kp, uint64_t ack_to_wait) { (void) ack_to_wait; + assert(sc_thread_get_id() == SC_MAIN_THREAD_ID); + if (event->repeat) { // In USB HID protocol, key repeat is handled by the host (Android), so // just ignore key repeat here. @@ -72,11 +71,9 @@ sc_key_processor_process_key(struct sc_key_processor *kp, // Not all keys are supported, just ignore unsupported keys if (sc_hid_keyboard_event_from_key(&kb->hid, &hid_event, event)) { if (event->scancode == SC_SCANCODE_CAPSLOCK) { - atomic_fetch_xor_explicit(&kb->device_mod, SC_MOD_CAPS, - memory_order_relaxed); + kb->device_mod ^= SC_MOD_CAPS; } else if (event->scancode == SC_SCANCODE_NUMLOCK) { - atomic_fetch_xor_explicit(&kb->device_mod, SC_MOD_NUM, - memory_order_relaxed); + kb->device_mod ^= SC_MOD_NUM; } else { // Synchronize modifiers (only if the scancode itself does not // change the modifiers) @@ -103,7 +100,7 @@ sc_keyboard_uhid_to_sc_mod(uint8_t hid_led) { static void sc_uhid_receiver_process_output(struct sc_uhid_receiver *receiver, const uint8_t *data, size_t len) { - // Called from the thread receiving device messages + assert(sc_thread_get_id() == SC_MAIN_THREAD_ID); assert(len); @@ -117,7 +114,7 @@ sc_uhid_receiver_process_output(struct sc_uhid_receiver *receiver, uint8_t hid_led = data[0]; uint16_t device_mod = sc_keyboard_uhid_to_sc_mod(hid_led); - atomic_store_explicit(&kb->device_mod, device_mod, memory_order_relaxed); + kb->device_mod = device_mod; } bool @@ -127,7 +124,7 @@ sc_keyboard_uhid_init(struct sc_keyboard_uhid *kb, sc_hid_keyboard_init(&kb->hid); kb->controller = controller; - atomic_init(&kb->device_mod, 0); + kb->device_mod = 0; static const struct sc_key_processor_ops ops = { .process_key = sc_key_processor_process_key, diff --git a/app/src/uhid/keyboard_uhid.h b/app/src/uhid/keyboard_uhid.h index 5e1be70c..639a3384 100644 --- a/app/src/uhid/keyboard_uhid.h +++ b/app/src/uhid/keyboard_uhid.h @@ -16,7 +16,7 @@ struct sc_keyboard_uhid { struct sc_hid_keyboard hid; struct sc_controller *controller; - atomic_uint_least16_t device_mod; + uint16_t device_mod; }; bool From 49c8ca34fd96548bd40862fc30a69dd54a68d2b9 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:08:08 +0200 Subject: [PATCH 1928/2244] Introduce non-droppable control messages Control messages are queued from the main thread and sent to the device from a separate thread. When the queue is full, messages are just dropped. This avoids to accumulate too much delay between the client and the device in case of network issue. However, some messages should not be dropped: for example, dropping a UHID_CREATE message would make all further UHID_INPUT messages invalid. Therefore, mark these messages as non-droppable. A non-droppable event is queued anyway (resizing the queue if necessary, unless the allocation fails). PR #5270 --- app/src/control_msg.c | 7 +++++++ app/src/control_msg.h | 5 +++++ app/src/controller.c | 26 ++++++++++++++++++++------ 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/app/src/control_msg.c b/app/src/control_msg.c index 9b0fab67..daa3bde7 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -278,6 +278,13 @@ sc_control_msg_log(const struct sc_control_msg *msg) { } } +bool +sc_control_msg_is_droppable(const struct sc_control_msg *msg) { + // Cannot drop UHID_CREATE messages, because it would cause all further + // UHID_INPUT messages for this device to be invalid + return msg->type != SC_CONTROL_MSG_TYPE_UHID_CREATE; +} + void sc_control_msg_destroy(struct sc_control_msg *msg) { switch (msg->type) { diff --git a/app/src/control_msg.h b/app/src/control_msg.h index 80714096..63670705 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -116,6 +116,11 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf); void sc_control_msg_log(const struct sc_control_msg *msg); +// Even when the buffer is "full", some messages must absolutely not be dropped +// to avoid inconsistencies. +bool +sc_control_msg_is_droppable(const struct sc_control_msg *msg); + void sc_control_msg_destroy(struct sc_control_msg *msg); diff --git a/app/src/controller.c b/app/src/controller.c index d50e1921..749de0a5 100644 --- a/app/src/controller.c +++ b/app/src/controller.c @@ -4,7 +4,8 @@ #include "util/log.h" -#define SC_CONTROL_MSG_QUEUE_MAX 64 +// Drop droppable events above this limit +#define SC_CONTROL_MSG_QUEUE_LIMIT 60 static void sc_controller_receiver_on_ended(struct sc_receiver *receiver, bool error, @@ -22,7 +23,9 @@ sc_controller_init(struct sc_controller *controller, sc_socket control_socket, void *cbs_userdata) { sc_vecdeque_init(&controller->queue); - bool ok = sc_vecdeque_reserve(&controller->queue, SC_CONTROL_MSG_QUEUE_MAX); + // Add 4 to support 4 non-droppable events without re-allocation + bool ok = sc_vecdeque_reserve(&controller->queue, + SC_CONTROL_MSG_QUEUE_LIMIT + 4); if (!ok) { return false; } @@ -93,20 +96,31 @@ sc_controller_push_msg(struct sc_controller *controller, sc_control_msg_log(msg); } + bool pushed = false; + sc_mutex_lock(&controller->mutex); - bool full = sc_vecdeque_is_full(&controller->queue); - if (!full) { + size_t size = sc_vecdeque_size(&controller->queue); + if (size < SC_CONTROL_MSG_QUEUE_LIMIT) { bool was_empty = sc_vecdeque_is_empty(&controller->queue); sc_vecdeque_push_noresize(&controller->queue, *msg); + pushed = true; if (was_empty) { sc_cond_signal(&controller->msg_cond); } + } else if (!sc_control_msg_is_droppable(msg)) { + bool ok = sc_vecdeque_push(&controller->queue, *msg); + if (ok) { + pushed = true; + } else { + // A non-droppable event must be dropped anyway + LOG_OOM(); + } } - // Otherwise (if the queue is full), the msg is discarded + // Otherwise, the msg is discarded sc_mutex_unlock(&controller->mutex); - return !full; + return pushed; } static bool From 08da2e068e7f535464b6e801dad2f74e11370b9a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:08:08 +0200 Subject: [PATCH 1929/2244] Fail on AOA keyboard/mouse initialization error If the AOA keyboard or the AOA mouse fails to be initialized, this is a fatal error. PR #5270 --- app/src/scrcpy.c | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index efad5891..529a3fc2 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -641,12 +641,15 @@ scrcpy(struct scrcpy_options *options) { goto end; } + bool aoa_fail = false; if (use_keyboard_aoa) { if (sc_keyboard_aoa_init(&s->keyboard_aoa, &s->aoa)) { keyboard_aoa_initialized = true; kp = &s->keyboard_aoa.key_processor; } else { LOGE("Could not initialize HID keyboard"); + aoa_fail = true; + goto aoa_complete; } } @@ -656,12 +659,13 @@ scrcpy(struct scrcpy_options *options) { mp = &s->mouse_aoa.mouse_processor; } else { LOGE("Could not initialized HID mouse"); + aoa_fail = true; + goto aoa_complete; } } - bool need_aoa = keyboard_aoa_initialized || mouse_aoa_initialized; - - if (!need_aoa || !sc_aoa_start(&s->aoa)) { +aoa_complete: + if (aoa_fail || !sc_aoa_start(&s->aoa)) { sc_acksync_destroy(&s->acksync); sc_usb_disconnect(&s->usb); sc_usb_destroy(&s->usb); From 785099b74d4883ee124a89f15d84e84221434032 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:08:08 +0200 Subject: [PATCH 1930/2244] Remove duplicate definition SC_HID_MAX_SIZE This constant is defined in hid_event.h. PR #5270 --- app/src/usb/aoa_hid.h | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/usb/aoa_hid.h b/app/src/usb/aoa_hid.h index 33a1f136..4d77ea3d 100644 --- a/app/src/usb/aoa_hid.h +++ b/app/src/usb/aoa_hid.h @@ -13,8 +13,6 @@ #include "util/tick.h" #include "util/vecdeque.h" -#define SC_HID_MAX_SIZE 8 - struct sc_aoa_event { struct sc_hid_event hid; uint16_t accessory_id; From 1afc8ca36889f668b0fd635d45f63a0113dbb093 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:08:08 +0200 Subject: [PATCH 1931/2244] Add missing SC_ prefix for HID mouse event size PR #5270 --- app/src/hid/hid_mouse.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/hid/hid_mouse.c b/app/src/hid/hid_mouse.c index 9d814448..1e831fb7 100644 --- a/app/src/hid/hid_mouse.c +++ b/app/src/hid/hid_mouse.c @@ -2,7 +2,7 @@ // 1 byte for buttons + padding, 1 byte for X position, 1 byte for Y position, // 1 byte for wheel motion -#define HID_MOUSE_EVENT_SIZE 4 +#define SC_HID_MOUSE_EVENT_SIZE 4 /** * Mouse descriptor from the specification: @@ -126,7 +126,7 @@ const size_t SC_HID_MOUSE_REPORT_DESC_LEN = static void sc_hid_mouse_event_init(struct sc_hid_event *hid_event) { - hid_event->size = HID_MOUSE_EVENT_SIZE; + hid_event->size = SC_HID_MOUSE_EVENT_SIZE; // Leave hid_event->data uninitialized, it will be fully initialized by // callers } From dad04bf1389ae5e4432b868c562713b127bbc2d3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:08:08 +0200 Subject: [PATCH 1932/2244] Fix HID mouse header guard PR #5270 --- app/src/hid/hid_mouse.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/hid/hid_mouse.h b/app/src/hid/hid_mouse.h index e514d7d9..0ebf0ee1 100644 --- a/app/src/hid/hid_mouse.h +++ b/app/src/hid/hid_mouse.h @@ -1,8 +1,6 @@ #ifndef SC_HID_MOUSE_H #define SC_HID_MOUSE_H -#endif - #include "common.h" #include @@ -24,3 +22,5 @@ sc_hid_mouse_event_from_click(struct sc_hid_event *hid_event, void sc_hid_mouse_event_from_scroll(struct sc_hid_event *hid_event, const struct sc_mouse_scroll_event *event); + +#endif From 2dd02ebb80cc8d64f7a3da53ccbb9d78c2506914 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:08:08 +0200 Subject: [PATCH 1933/2244] Move HID ids to common HID code The HID ids (accessory ids or UHID ids) were defined by the keyboard and mouse implementations. Instead, define them in the common HID part, and make that id part of the sc_hid_event. This prepares the introduction of gamepad support, which will handle several gamepads (and ids) in the common HID gamepad code. PR #5270 --- app/src/hid/hid_event.h | 1 + app/src/hid/hid_keyboard.c | 1 + app/src/hid/hid_keyboard.h | 2 ++ app/src/hid/hid_mouse.c | 1 + app/src/hid/hid_mouse.h | 2 ++ app/src/uhid/keyboard_uhid.c | 8 +++----- app/src/uhid/mouse_uhid.c | 6 ++---- app/src/usb/aoa_hid.c | 16 +++++++--------- app/src/usb/aoa_hid.h | 7 ++----- app/src/usb/keyboard_aoa.c | 13 ++++--------- app/src/usb/mouse_aoa.c | 15 +++++---------- 11 files changed, 30 insertions(+), 42 deletions(-) diff --git a/app/src/hid/hid_event.h b/app/src/hid/hid_event.h index e17f8569..80b65a87 100644 --- a/app/src/hid/hid_event.h +++ b/app/src/hid/hid_event.h @@ -8,6 +8,7 @@ #define SC_HID_MAX_SIZE 8 struct sc_hid_event { + uint16_t hid_id; uint8_t data[SC_HID_MAX_SIZE]; uint8_t size; }; diff --git a/app/src/hid/hid_keyboard.c b/app/src/hid/hid_keyboard.c index f3001df4..f828d014 100644 --- a/app/src/hid/hid_keyboard.c +++ b/app/src/hid/hid_keyboard.c @@ -200,6 +200,7 @@ const size_t SC_HID_KEYBOARD_REPORT_DESC_LEN = static void sc_hid_keyboard_event_init(struct sc_hid_event *hid_event) { + hid_event->hid_id = SC_HID_ID_KEYBOARD; hid_event->size = SC_HID_KEYBOARD_EVENT_SIZE; uint8_t *data = hid_event->data; diff --git a/app/src/hid/hid_keyboard.h b/app/src/hid/hid_keyboard.h index ddd2cc91..24d64b15 100644 --- a/app/src/hid/hid_keyboard.h +++ b/app/src/hid/hid_keyboard.h @@ -14,6 +14,8 @@ // 0x65 is Application, typically AT-101 Keyboard ends here. #define SC_HID_KEYBOARD_KEYS 0x66 +#define SC_HID_ID_KEYBOARD 1 + extern const uint8_t SC_HID_KEYBOARD_REPORT_DESC[]; extern const size_t SC_HID_KEYBOARD_REPORT_DESC_LEN; diff --git a/app/src/hid/hid_mouse.c b/app/src/hid/hid_mouse.c index 1e831fb7..cc1862bc 100644 --- a/app/src/hid/hid_mouse.c +++ b/app/src/hid/hid_mouse.c @@ -126,6 +126,7 @@ const size_t SC_HID_MOUSE_REPORT_DESC_LEN = static void sc_hid_mouse_event_init(struct sc_hid_event *hid_event) { + hid_event->hid_id = SC_HID_ID_MOUSE; hid_event->size = SC_HID_MOUSE_EVENT_SIZE; // Leave hid_event->data uninitialized, it will be fully initialized by // callers diff --git a/app/src/hid/hid_mouse.h b/app/src/hid/hid_mouse.h index 0ebf0ee1..91337de5 100644 --- a/app/src/hid/hid_mouse.h +++ b/app/src/hid/hid_mouse.h @@ -8,6 +8,8 @@ #include "hid/hid_event.h" #include "input_events.h" +#define SC_HID_ID_MOUSE 2 + extern const uint8_t SC_HID_MOUSE_REPORT_DESC[]; extern const size_t SC_HID_MOUSE_REPORT_DESC_LEN; diff --git a/app/src/uhid/keyboard_uhid.c b/app/src/uhid/keyboard_uhid.c index d63d0ab0..7d5c6493 100644 --- a/app/src/uhid/keyboard_uhid.c +++ b/app/src/uhid/keyboard_uhid.c @@ -9,14 +9,12 @@ #define DOWNCAST_RECEIVER(UR) \ container_of(UR, struct sc_keyboard_uhid, uhid_receiver) -#define UHID_KEYBOARD_ID 1 - static void sc_keyboard_uhid_send_input(struct sc_keyboard_uhid *kb, const struct sc_hid_event *event) { struct sc_control_msg msg; msg.type = SC_CONTROL_MSG_TYPE_UHID_INPUT; - msg.uhid_input.id = UHID_KEYBOARD_ID; + msg.uhid_input.id = event->hid_id; assert(event->size <= SC_HID_MAX_SIZE); memcpy(msg.uhid_input.data, event->data, event->size); @@ -143,13 +141,13 @@ sc_keyboard_uhid_init(struct sc_keyboard_uhid *kb, .process_output = sc_uhid_receiver_process_output, }; - kb->uhid_receiver.id = UHID_KEYBOARD_ID; + kb->uhid_receiver.id = SC_HID_ID_KEYBOARD; kb->uhid_receiver.ops = &uhid_receiver_ops; sc_uhid_devices_add_receiver(uhid_devices, &kb->uhid_receiver); struct sc_control_msg msg; msg.type = SC_CONTROL_MSG_TYPE_UHID_CREATE; - msg.uhid_create.id = UHID_KEYBOARD_ID; + msg.uhid_create.id = SC_HID_ID_KEYBOARD; msg.uhid_create.report_desc = SC_HID_KEYBOARD_REPORT_DESC; msg.uhid_create.report_desc_size = SC_HID_KEYBOARD_REPORT_DESC_LEN; if (!sc_controller_push_msg(controller, &msg)) { diff --git a/app/src/uhid/mouse_uhid.c b/app/src/uhid/mouse_uhid.c index 77446f9e..21dc018a 100644 --- a/app/src/uhid/mouse_uhid.c +++ b/app/src/uhid/mouse_uhid.c @@ -7,14 +7,12 @@ /** Downcast mouse processor to mouse_uhid */ #define DOWNCAST(MP) container_of(MP, struct sc_mouse_uhid, mouse_processor) -#define UHID_MOUSE_ID 2 - static void sc_mouse_uhid_send_input(struct sc_mouse_uhid *mouse, const struct sc_hid_event *event, const char *name) { struct sc_control_msg msg; msg.type = SC_CONTROL_MSG_TYPE_UHID_INPUT; - msg.uhid_input.id = UHID_MOUSE_ID; + msg.uhid_input.id = event->hid_id; assert(event->size <= SC_HID_MAX_SIZE); memcpy(msg.uhid_input.data, event->data, event->size); @@ -77,7 +75,7 @@ sc_mouse_uhid_init(struct sc_mouse_uhid *mouse, struct sc_control_msg msg; msg.type = SC_CONTROL_MSG_TYPE_UHID_CREATE; - msg.uhid_create.id = UHID_MOUSE_ID; + msg.uhid_create.id = SC_HID_ID_MOUSE; msg.uhid_create.report_desc = SC_HID_MOUSE_REPORT_DESC; msg.uhid_create.report_desc_size = SC_HID_MOUSE_REPORT_DESC_LEN; if (!sc_controller_push_msg(controller, &msg)) { diff --git a/app/src/usb/aoa_hid.c b/app/src/usb/aoa_hid.c index 50bc33fe..260fbb75 100644 --- a/app/src/usb/aoa_hid.c +++ b/app/src/usb/aoa_hid.c @@ -1,6 +1,7 @@ #include "util/log.h" #include +#include #include #include "aoa_hid.h" @@ -18,14 +19,14 @@ #define SC_AOA_EVENT_QUEUE_MAX 64 static void -sc_hid_event_log(uint16_t accessory_id, const struct sc_hid_event *event) { +sc_hid_event_log(const struct sc_hid_event *event) { // HID Event: [00] FF FF FF FF... assert(event->size); char *hex = sc_str_to_hex_string(event->data, event->size); if (!hex) { return; } - LOGV("HID Event: [%d] %s", accessory_id, hex); + LOGV("HID Event: [%" PRIu16 "] %s", event->hid_id, hex); free(hex); } @@ -146,14 +147,13 @@ sc_aoa_setup_hid(struct sc_aoa *aoa, uint16_t accessory_id, } static bool -sc_aoa_send_hid_event(struct sc_aoa *aoa, uint16_t accessory_id, - const struct sc_hid_event *event) { +sc_aoa_send_hid_event(struct sc_aoa *aoa, const struct sc_hid_event *event) { uint8_t request_type = LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR; uint8_t request = ACCESSORY_SEND_HID_EVENT; // // value (arg0): accessory assigned ID for the HID device // index (arg1): 0 (unused) - uint16_t value = accessory_id; + uint16_t value = event->hid_id; uint16_t index = 0; unsigned char *data = (uint8_t *) event->data; // discard const uint16_t length = event->size; @@ -194,11 +194,10 @@ sc_aoa_unregister_hid(struct sc_aoa *aoa, uint16_t accessory_id) { bool sc_aoa_push_hid_event_with_ack_to_wait(struct sc_aoa *aoa, - uint16_t accessory_id, const struct sc_hid_event *event, uint64_t ack_to_wait) { if (sc_get_log_level() <= SC_LOG_LEVEL_VERBOSE) { - sc_hid_event_log(accessory_id, event); + sc_hid_event_log(event); } sc_mutex_lock(&aoa->mutex); @@ -209,7 +208,6 @@ sc_aoa_push_hid_event_with_ack_to_wait(struct sc_aoa *aoa, struct sc_aoa_event *aoa_event = sc_vecdeque_push_hole_noresize(&aoa->queue); aoa_event->hid = *event; - aoa_event->accessory_id = accessory_id; aoa_event->ack_to_wait = ack_to_wait; if (was_empty) { @@ -265,7 +263,7 @@ run_aoa_thread(void *data) { } } - bool ok = sc_aoa_send_hid_event(aoa, event.accessory_id, &event.hid); + bool ok = sc_aoa_send_hid_event(aoa, &event.hid); if (!ok) { LOGW("Could not send HID event to USB device"); } diff --git a/app/src/usb/aoa_hid.h b/app/src/usb/aoa_hid.h index 4d77ea3d..87f070ca 100644 --- a/app/src/usb/aoa_hid.h +++ b/app/src/usb/aoa_hid.h @@ -15,7 +15,6 @@ struct sc_aoa_event { struct sc_hid_event hid; - uint16_t accessory_id; uint64_t ack_to_wait; }; @@ -56,14 +55,12 @@ sc_aoa_unregister_hid(struct sc_aoa *aoa, uint16_t accessory_id); bool sc_aoa_push_hid_event_with_ack_to_wait(struct sc_aoa *aoa, - uint16_t accessory_id, const struct sc_hid_event *event, uint64_t ack_to_wait); static inline bool -sc_aoa_push_hid_event(struct sc_aoa *aoa, uint16_t accessory_id, - const struct sc_hid_event *event) { - return sc_aoa_push_hid_event_with_ack_to_wait(aoa, accessory_id, event, +sc_aoa_push_hid_event(struct sc_aoa *aoa, const struct sc_hid_event *event) { + return sc_aoa_push_hid_event_with_ack_to_wait(aoa, event, SC_SEQUENCE_INVALID); } diff --git a/app/src/usb/keyboard_aoa.c b/app/src/usb/keyboard_aoa.c index 736c97b0..f6bd6849 100644 --- a/app/src/usb/keyboard_aoa.c +++ b/app/src/usb/keyboard_aoa.c @@ -8,8 +8,6 @@ /** Downcast key processor to keyboard_aoa */ #define DOWNCAST(KP) container_of(KP, struct sc_keyboard_aoa, key_processor) -#define HID_KEYBOARD_ACCESSORY_ID 1 - static bool push_mod_lock_state(struct sc_keyboard_aoa *kb, uint16_t mods_state) { struct sc_hid_event hid_event; @@ -18,8 +16,7 @@ push_mod_lock_state(struct sc_keyboard_aoa *kb, uint16_t mods_state) { return true; } - if (!sc_aoa_push_hid_event(kb->aoa, HID_KEYBOARD_ACCESSORY_ID, - &hid_event)) { + if (!sc_aoa_push_hid_event(kb->aoa, &hid_event)) { LOGW("Could not request HID event (mod lock state)"); return false; } @@ -58,9 +55,7 @@ sc_key_processor_process_key(struct sc_key_processor *kp, // synchronization is acknowledged by the server, otherwise it could // paste the old clipboard content. - if (!sc_aoa_push_hid_event_with_ack_to_wait(kb->aoa, - HID_KEYBOARD_ACCESSORY_ID, - &hid_event, + if (!sc_aoa_push_hid_event_with_ack_to_wait(kb->aoa, &hid_event, ack_to_wait)) { LOGW("Could not request HID event (key)"); } @@ -71,7 +66,7 @@ bool sc_keyboard_aoa_init(struct sc_keyboard_aoa *kb, struct sc_aoa *aoa) { kb->aoa = aoa; - bool ok = sc_aoa_setup_hid(aoa, HID_KEYBOARD_ACCESSORY_ID, + bool ok = sc_aoa_setup_hid(aoa, SC_HID_ID_KEYBOARD, SC_HID_KEYBOARD_REPORT_DESC, SC_HID_KEYBOARD_REPORT_DESC_LEN); if (!ok) { @@ -103,7 +98,7 @@ sc_keyboard_aoa_init(struct sc_keyboard_aoa *kb, struct sc_aoa *aoa) { void sc_keyboard_aoa_destroy(struct sc_keyboard_aoa *kb) { // Unregister HID keyboard so the soft keyboard shows again on Android - bool ok = sc_aoa_unregister_hid(kb->aoa, HID_KEYBOARD_ACCESSORY_ID); + bool ok = sc_aoa_unregister_hid(kb->aoa, SC_HID_ID_KEYBOARD); if (!ok) { LOGW("Could not unregister HID keyboard"); } diff --git a/app/src/usb/mouse_aoa.c b/app/src/usb/mouse_aoa.c index 93b32328..896578c0 100644 --- a/app/src/usb/mouse_aoa.c +++ b/app/src/usb/mouse_aoa.c @@ -9,8 +9,6 @@ /** Downcast mouse processor to mouse_aoa */ #define DOWNCAST(MP) container_of(MP, struct sc_mouse_aoa, mouse_processor) -#define HID_MOUSE_ACCESSORY_ID 2 - static void sc_mouse_processor_process_mouse_motion(struct sc_mouse_processor *mp, const struct sc_mouse_motion_event *event) { @@ -19,8 +17,7 @@ sc_mouse_processor_process_mouse_motion(struct sc_mouse_processor *mp, struct sc_hid_event hid_event; sc_hid_mouse_event_from_motion(&hid_event, event); - if (!sc_aoa_push_hid_event(mouse->aoa, HID_MOUSE_ACCESSORY_ID, - &hid_event)) { + if (!sc_aoa_push_hid_event(mouse->aoa, &hid_event)) { LOGW("Could not request HID event (mouse motion)"); } } @@ -33,8 +30,7 @@ sc_mouse_processor_process_mouse_click(struct sc_mouse_processor *mp, struct sc_hid_event hid_event; sc_hid_mouse_event_from_click(&hid_event, event); - if (!sc_aoa_push_hid_event(mouse->aoa, HID_MOUSE_ACCESSORY_ID, - &hid_event)) { + if (!sc_aoa_push_hid_event(mouse->aoa, &hid_event)) { LOGW("Could not request HID event (mouse click)"); } } @@ -47,8 +43,7 @@ sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp, struct sc_hid_event hid_event; sc_hid_mouse_event_from_scroll(&hid_event, event); - if (!sc_aoa_push_hid_event(mouse->aoa, HID_MOUSE_ACCESSORY_ID, - &hid_event)) { + if (!sc_aoa_push_hid_event(mouse->aoa, &hid_event)) { LOGW("Could not request HID event (mouse scroll)"); } } @@ -57,7 +52,7 @@ bool sc_mouse_aoa_init(struct sc_mouse_aoa *mouse, struct sc_aoa *aoa) { mouse->aoa = aoa; - bool ok = sc_aoa_setup_hid(aoa, HID_MOUSE_ACCESSORY_ID, + bool ok = sc_aoa_setup_hid(aoa, SC_HID_ID_MOUSE, SC_HID_MOUSE_REPORT_DESC, SC_HID_MOUSE_REPORT_DESC_LEN); if (!ok) { @@ -82,7 +77,7 @@ sc_mouse_aoa_init(struct sc_mouse_aoa *mouse, struct sc_aoa *aoa) { void sc_mouse_aoa_destroy(struct sc_mouse_aoa *mouse) { - bool ok = sc_aoa_unregister_hid(mouse->aoa, HID_MOUSE_ACCESSORY_ID); + bool ok = sc_aoa_unregister_hid(mouse->aoa, SC_HID_ID_MOUSE); if (!ok) { LOGW("Could not unregister HID mouse"); } From 9af3bacdd67e2179aea7dc0f56ec487c015181e3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:08:08 +0200 Subject: [PATCH 1934/2244] Refactor AOA handling Extract event processing to a separate function. This will make the code more readable when more event types will be added. PR #5270 --- app/src/usb/aoa_hid.c | 65 ++++++++++++++++++++++++++----------------- 1 file changed, 39 insertions(+), 26 deletions(-) diff --git a/app/src/usb/aoa_hid.c b/app/src/usb/aoa_hid.c index 260fbb75..8eee4d3f 100644 --- a/app/src/usb/aoa_hid.c +++ b/app/src/usb/aoa_hid.c @@ -221,6 +221,41 @@ sc_aoa_push_hid_event_with_ack_to_wait(struct sc_aoa *aoa, return !full; } +static bool +sc_aoa_process_event(struct sc_aoa *aoa, struct sc_aoa_event *event) { + uint64_t ack_to_wait = event->ack_to_wait; + if (ack_to_wait != SC_SEQUENCE_INVALID) { + LOGD("Waiting ack from server sequence=%" PRIu64_, ack_to_wait); + + // If some events have ack_to_wait set, then sc_aoa must have been + // initialized with a non NULL acksync + assert(aoa->acksync); + + // Do not block the loop indefinitely if the ack never comes (it + // should never happen) + sc_tick deadline = sc_tick_now() + SC_TICK_FROM_MS(500); + enum sc_acksync_wait_result result = + sc_acksync_wait(aoa->acksync, ack_to_wait, deadline); + + if (result == SC_ACKSYNC_WAIT_TIMEOUT) { + LOGW("Ack not received after 500ms, discarding HID event"); + // continue to process events + return true; + } else if (result == SC_ACKSYNC_WAIT_INTR) { + // stopped + return false; + } + } + + bool ok = sc_aoa_send_hid_event(aoa, &event->hid); + if (!ok) { + LOGW("Could not send HID event to USB device"); + } + + // continue to process events + return true; +} + static int run_aoa_thread(void *data) { struct sc_aoa *aoa = data; @@ -238,34 +273,12 @@ run_aoa_thread(void *data) { assert(!sc_vecdeque_is_empty(&aoa->queue)); struct sc_aoa_event event = sc_vecdeque_pop(&aoa->queue); - uint64_t ack_to_wait = event.ack_to_wait; sc_mutex_unlock(&aoa->mutex); - if (ack_to_wait != SC_SEQUENCE_INVALID) { - LOGD("Waiting ack from server sequence=%" PRIu64_, ack_to_wait); - - // If some events have ack_to_wait set, then sc_aoa must have been - // initialized with a non NULL acksync - assert(aoa->acksync); - - // Do not block the loop indefinitely if the ack never comes (it - // should never happen) - sc_tick deadline = sc_tick_now() + SC_TICK_FROM_MS(500); - enum sc_acksync_wait_result result = - sc_acksync_wait(aoa->acksync, ack_to_wait, deadline); - - if (result == SC_ACKSYNC_WAIT_TIMEOUT) { - LOGW("Ack not received after 500ms, discarding HID event"); - continue; - } else if (result == SC_ACKSYNC_WAIT_INTR) { - // stopped - break; - } - } - - bool ok = sc_aoa_send_hid_event(aoa, &event.hid); - if (!ok) { - LOGW("Could not send HID event to USB device"); + bool cont = sc_aoa_process_event(aoa, &event); + if (!cont) { + // stopped + break; } } return 0; From 3e9c89c535e0304401da877502de6a892d1bfa10 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:08:08 +0200 Subject: [PATCH 1935/2244] Reorder AOA functions This will allow sc_aoa_setup_hid() to compile even when sc_aoa_unregister_hid() will be made static. PR #5270 --- app/src/usb/aoa_hid.c | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/app/src/usb/aoa_hid.c b/app/src/usb/aoa_hid.c index 8eee4d3f..f4ff8b8d 100644 --- a/app/src/usb/aoa_hid.c +++ b/app/src/usb/aoa_hid.c @@ -126,26 +126,6 @@ sc_aoa_set_hid_report_desc(struct sc_aoa *aoa, uint16_t accessory_id, return true; } -bool -sc_aoa_setup_hid(struct sc_aoa *aoa, uint16_t accessory_id, - const uint8_t *report_desc, uint16_t report_desc_size) { - bool ok = sc_aoa_register_hid(aoa, accessory_id, report_desc_size); - if (!ok) { - return false; - } - - ok = sc_aoa_set_hid_report_desc(aoa, accessory_id, report_desc, - report_desc_size); - if (!ok) { - if (!sc_aoa_unregister_hid(aoa, accessory_id)) { - LOGW("Could not unregister HID"); - } - return false; - } - - return true; -} - static bool sc_aoa_send_hid_event(struct sc_aoa *aoa, const struct sc_hid_event *event) { uint8_t request_type = LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR; @@ -192,6 +172,26 @@ sc_aoa_unregister_hid(struct sc_aoa *aoa, uint16_t accessory_id) { return true; } +bool +sc_aoa_setup_hid(struct sc_aoa *aoa, uint16_t accessory_id, + const uint8_t *report_desc, uint16_t report_desc_size) { + bool ok = sc_aoa_register_hid(aoa, accessory_id, report_desc_size); + if (!ok) { + return false; + } + + ok = sc_aoa_set_hid_report_desc(aoa, accessory_id, report_desc, + report_desc_size); + if (!ok) { + if (!sc_aoa_unregister_hid(aoa, accessory_id)) { + LOGW("Could not unregister HID"); + } + return false; + } + + return true; +} + bool sc_aoa_push_hid_event_with_ack_to_wait(struct sc_aoa *aoa, const struct sc_hid_event *event, From 6e9b0d7d4c4d386c84e8a3a40e442a45d1666e24 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:08:08 +0200 Subject: [PATCH 1936/2244] Make AOA open and close asynchronous For AOA keyboard and mouse, only input reports were asynchronous. Register/unregister were called from the main thread. This had the benefit to fail immediately if the AOA registration failed, but we want to open/close AOA devices dynamically in order to add gamepad support. PR #5270 --- app/src/usb/aoa_hid.c | 156 +++++++++++++++++++++++++++++-------- app/src/usb/aoa_hid.h | 38 +++++++-- app/src/usb/keyboard_aoa.c | 9 +-- app/src/usb/mouse_aoa.c | 8 +- 4 files changed, 165 insertions(+), 46 deletions(-) diff --git a/app/src/usb/aoa_hid.c b/app/src/usb/aoa_hid.c index f4ff8b8d..ff2516e5 100644 --- a/app/src/usb/aoa_hid.c +++ b/app/src/usb/aoa_hid.c @@ -16,7 +16,8 @@ #define DEFAULT_TIMEOUT 1000 -#define SC_AOA_EVENT_QUEUE_MAX 64 +// Drop droppable events above this limit +#define SC_AOA_EVENT_QUEUE_LIMIT 60 static void sc_hid_event_log(const struct sc_hid_event *event) { @@ -35,7 +36,8 @@ sc_aoa_init(struct sc_aoa *aoa, struct sc_usb *usb, struct sc_acksync *acksync) { sc_vecdeque_init(&aoa->queue); - if (!sc_vecdeque_reserve(&aoa->queue, SC_AOA_EVENT_QUEUE_MAX)) { + // Add 4 to support 4 non-droppable events without re-allocation + if (!sc_vecdeque_reserve(&aoa->queue, SC_AOA_EVENT_QUEUE_LIMIT + 4)) { return false; } @@ -149,7 +151,7 @@ sc_aoa_send_hid_event(struct sc_aoa *aoa, const struct sc_hid_event *event) { return true; } -bool +static bool sc_aoa_unregister_hid(struct sc_aoa *aoa, uint16_t accessory_id) { uint8_t request_type = LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR; uint8_t request = ACCESSORY_UNREGISTER_HID; @@ -172,7 +174,7 @@ sc_aoa_unregister_hid(struct sc_aoa *aoa, uint16_t accessory_id) { return true; } -bool +static bool sc_aoa_setup_hid(struct sc_aoa *aoa, uint16_t accessory_id, const uint8_t *report_desc, uint16_t report_desc_size) { bool ok = sc_aoa_register_hid(aoa, accessory_id, report_desc_size); @@ -201,55 +203,145 @@ sc_aoa_push_hid_event_with_ack_to_wait(struct sc_aoa *aoa, } sc_mutex_lock(&aoa->mutex); - bool full = sc_vecdeque_is_full(&aoa->queue); - if (!full) { + + bool pushed = false; + + size_t size = sc_vecdeque_size(&aoa->queue); + if (size < SC_AOA_EVENT_QUEUE_LIMIT) { bool was_empty = sc_vecdeque_is_empty(&aoa->queue); struct sc_aoa_event *aoa_event = sc_vecdeque_push_hole_noresize(&aoa->queue); - aoa_event->hid = *event; - aoa_event->ack_to_wait = ack_to_wait; + aoa_event->type = SC_AOA_EVENT_TYPE_INPUT; + aoa_event->input.hid = *event; + aoa_event->input.ack_to_wait = ack_to_wait; + pushed = true; if (was_empty) { sc_cond_signal(&aoa->event_cond); } } - // Otherwise (if the queue is full), the event is discarded + // Otherwise, the event is discarded sc_mutex_unlock(&aoa->mutex); - return !full; + return pushed; +} + +bool +sc_aoa_push_open(struct sc_aoa *aoa, uint16_t accessory_id, + const uint8_t *report_desc, uint16_t report_desc_size) { + // TODO log verbose + + sc_mutex_lock(&aoa->mutex); + bool was_empty = sc_vecdeque_is_empty(&aoa->queue); + + // an OPEN event is non-droppable, so push it to the queue even above the + // SC_AOA_EVENT_QUEUE_LIMIT + struct sc_aoa_event *aoa_event = sc_vecdeque_push_hole(&aoa->queue); + if (!aoa_event) { + LOG_OOM(); + sc_mutex_unlock(&aoa->mutex); + return false; + } + + aoa_event->type = SC_AOA_EVENT_TYPE_OPEN; + aoa_event->open.hid_id = accessory_id; + aoa_event->open.report_desc = report_desc; + aoa_event->open.report_desc_size = report_desc_size; + + if (was_empty) { + sc_cond_signal(&aoa->event_cond); + } + + sc_mutex_unlock(&aoa->mutex); + + return true; +} + +bool +sc_aoa_push_close(struct sc_aoa *aoa, uint16_t accessory_id) { + // TODO log verbose + + sc_mutex_lock(&aoa->mutex); + bool was_empty = sc_vecdeque_is_empty(&aoa->queue); + + // a CLOSE event is non-droppable, so push it to the queue even above the + // SC_AOA_EVENT_QUEUE_LIMIT + struct sc_aoa_event *aoa_event = sc_vecdeque_push_hole(&aoa->queue); + if (!aoa_event) { + LOG_OOM(); + sc_mutex_unlock(&aoa->mutex); + return false; + } + + aoa_event->type = SC_AOA_EVENT_TYPE_CLOSE; + aoa_event->close.hid_id = accessory_id; + + if (was_empty) { + sc_cond_signal(&aoa->event_cond); + } + + sc_mutex_unlock(&aoa->mutex); + + return true; } static bool sc_aoa_process_event(struct sc_aoa *aoa, struct sc_aoa_event *event) { - uint64_t ack_to_wait = event->ack_to_wait; - if (ack_to_wait != SC_SEQUENCE_INVALID) { - LOGD("Waiting ack from server sequence=%" PRIu64_, ack_to_wait); + switch (event->type) { + case SC_AOA_EVENT_TYPE_INPUT: { + uint64_t ack_to_wait = event->input.ack_to_wait; + if (ack_to_wait != SC_SEQUENCE_INVALID) { + LOGD("Waiting ack from server sequence=%" PRIu64_, ack_to_wait); - // If some events have ack_to_wait set, then sc_aoa must have been - // initialized with a non NULL acksync - assert(aoa->acksync); + // If some events have ack_to_wait set, then sc_aoa must have + // been initialized with a non NULL acksync + assert(aoa->acksync); - // Do not block the loop indefinitely if the ack never comes (it - // should never happen) - sc_tick deadline = sc_tick_now() + SC_TICK_FROM_MS(500); - enum sc_acksync_wait_result result = - sc_acksync_wait(aoa->acksync, ack_to_wait, deadline); + // Do not block the loop indefinitely if the ack never comes (it + // should never happen) + sc_tick deadline = sc_tick_now() + SC_TICK_FROM_MS(500); + enum sc_acksync_wait_result result = + sc_acksync_wait(aoa->acksync, ack_to_wait, deadline); - if (result == SC_ACKSYNC_WAIT_TIMEOUT) { - LOGW("Ack not received after 500ms, discarding HID event"); - // continue to process events - return true; - } else if (result == SC_ACKSYNC_WAIT_INTR) { - // stopped - return false; + if (result == SC_ACKSYNC_WAIT_TIMEOUT) { + LOGW("Ack not received after 500ms, discarding HID event"); + // continue to process events + return true; + } else if (result == SC_ACKSYNC_WAIT_INTR) { + // stopped + return false; + } + } + + bool ok = sc_aoa_send_hid_event(aoa, &event->input.hid); + if (!ok) { + LOGW("Could not send HID event to USB device: %" PRIu16, + event->input.hid.hid_id); + } + + break; } - } + case SC_AOA_EVENT_TYPE_OPEN: { + bool ok = sc_aoa_setup_hid(aoa, event->open.hid_id, + event->open.report_desc, + event->open.report_desc_size); + if (!ok) { + LOGW("Could not open AOA device: %" PRIu16, event->open.hid_id); + } - bool ok = sc_aoa_send_hid_event(aoa, &event->hid); - if (!ok) { - LOGW("Could not send HID event to USB device"); + break; + } + case SC_AOA_EVENT_TYPE_CLOSE: { + bool ok = sc_aoa_unregister_hid(aoa, event->close.hid_id); + if (!ok) { + LOGW("Could not close AOA device: %" PRIu16, + event->close.hid_id); + } + + break; + } } // continue to process events diff --git a/app/src/usb/aoa_hid.h b/app/src/usb/aoa_hid.h index 87f070ca..b2dc04ac 100644 --- a/app/src/usb/aoa_hid.h +++ b/app/src/usb/aoa_hid.h @@ -13,9 +13,28 @@ #include "util/tick.h" #include "util/vecdeque.h" +enum sc_aoa_event_type { + SC_AOA_EVENT_TYPE_OPEN, + SC_AOA_EVENT_TYPE_INPUT, + SC_AOA_EVENT_TYPE_CLOSE, +}; + struct sc_aoa_event { - struct sc_hid_event hid; - uint64_t ack_to_wait; + enum sc_aoa_event_type type; + union { + struct { + uint16_t hid_id; + const uint8_t *report_desc; // pointer to static memory + uint16_t report_desc_size; + } open; + struct { + uint16_t hid_id; + } close; + struct { + struct sc_hid_event hid; + uint64_t ack_to_wait; + } input; + }; }; struct sc_aoa_event_queue SC_VECDEQUE(struct sc_aoa_event); @@ -46,12 +65,21 @@ sc_aoa_stop(struct sc_aoa *aoa); void sc_aoa_join(struct sc_aoa *aoa); +//bool +//sc_aoa_setup_hid(struct sc_aoa *aoa, uint16_t accessory_id, +// const uint8_t *report_desc, uint16_t report_desc_size); +// +//bool +//sc_aoa_unregister_hid(struct sc_aoa *aoa, uint16_t accessory_id); + +// report_desc must be a pointer to static memory, accessed at any time from +// another thread bool -sc_aoa_setup_hid(struct sc_aoa *aoa, uint16_t accessory_id, - const uint8_t *report_desc, uint16_t report_desc_size); +sc_aoa_push_open(struct sc_aoa *aoa, uint16_t accessory_id, + const uint8_t *report_desc, uint16_t report_desc_size); bool -sc_aoa_unregister_hid(struct sc_aoa *aoa, uint16_t accessory_id); +sc_aoa_push_close(struct sc_aoa *aoa, uint16_t accessory_id); bool sc_aoa_push_hid_event_with_ack_to_wait(struct sc_aoa *aoa, diff --git a/app/src/usb/keyboard_aoa.c b/app/src/usb/keyboard_aoa.c index f6bd6849..0052c3d8 100644 --- a/app/src/usb/keyboard_aoa.c +++ b/app/src/usb/keyboard_aoa.c @@ -66,11 +66,11 @@ bool sc_keyboard_aoa_init(struct sc_keyboard_aoa *kb, struct sc_aoa *aoa) { kb->aoa = aoa; - bool ok = sc_aoa_setup_hid(aoa, SC_HID_ID_KEYBOARD, + bool ok = sc_aoa_push_open(aoa, SC_HID_ID_KEYBOARD, SC_HID_KEYBOARD_REPORT_DESC, SC_HID_KEYBOARD_REPORT_DESC_LEN); if (!ok) { - LOGW("Register HID keyboard failed"); + LOGW("Could not push AOA keyboard open request"); return false; } @@ -97,9 +97,8 @@ sc_keyboard_aoa_init(struct sc_keyboard_aoa *kb, struct sc_aoa *aoa) { void sc_keyboard_aoa_destroy(struct sc_keyboard_aoa *kb) { - // Unregister HID keyboard so the soft keyboard shows again on Android - bool ok = sc_aoa_unregister_hid(kb->aoa, SC_HID_ID_KEYBOARD); + bool ok = sc_aoa_push_close(kb->aoa, SC_HID_ID_KEYBOARD); if (!ok) { - LOGW("Could not unregister HID keyboard"); + LOGW("Could not push AOA keyboard close request"); } } diff --git a/app/src/usb/mouse_aoa.c b/app/src/usb/mouse_aoa.c index 896578c0..84fd8d64 100644 --- a/app/src/usb/mouse_aoa.c +++ b/app/src/usb/mouse_aoa.c @@ -52,11 +52,11 @@ bool sc_mouse_aoa_init(struct sc_mouse_aoa *mouse, struct sc_aoa *aoa) { mouse->aoa = aoa; - bool ok = sc_aoa_setup_hid(aoa, SC_HID_ID_MOUSE, + bool ok = sc_aoa_push_open(aoa, SC_HID_ID_MOUSE, SC_HID_MOUSE_REPORT_DESC, SC_HID_MOUSE_REPORT_DESC_LEN); if (!ok) { - LOGW("Register HID mouse failed"); + LOGW("Could not push AOA mouse open request"); return false; } @@ -77,8 +77,8 @@ sc_mouse_aoa_init(struct sc_mouse_aoa *mouse, struct sc_aoa *aoa) { void sc_mouse_aoa_destroy(struct sc_mouse_aoa *mouse) { - bool ok = sc_aoa_unregister_hid(mouse->aoa, SC_HID_ID_MOUSE); + bool ok = sc_aoa_push_close(mouse->aoa, SC_HID_ID_MOUSE); if (!ok) { - LOGW("Could not unregister HID mouse"); + LOGW("Could not push AOA mouse close request"); } } From f6219d26409a1ba25b4df86aa3468aa18b6a11e5 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:08:08 +0200 Subject: [PATCH 1937/2244] Rename hid_event to hid_input The sc_hid_event structure represents HID input data. Rename it so that we can add other hid event structs without confusion. PR #5270 --- app/src/hid/hid_event.h | 2 +- app/src/hid/hid_keyboard.c | 34 ++++++++++++++++----------------- app/src/hid/hid_keyboard.h | 10 +++++----- app/src/hid/hid_mouse.c | 37 ++++++++++++++++++------------------ app/src/hid/hid_mouse.h | 12 ++++++------ app/src/uhid/keyboard_uhid.c | 22 ++++++++++----------- app/src/uhid/mouse_uhid.c | 29 ++++++++++++++-------------- app/src/usb/aoa_hid.c | 29 ++++++++++++++-------------- app/src/usb/aoa_hid.h | 14 +++++++------- app/src/usb/keyboard_aoa.c | 18 +++++++++--------- app/src/usb/mouse_aoa.c | 24 +++++++++++------------ 11 files changed, 116 insertions(+), 115 deletions(-) diff --git a/app/src/hid/hid_event.h b/app/src/hid/hid_event.h index 80b65a87..9f9432e6 100644 --- a/app/src/hid/hid_event.h +++ b/app/src/hid/hid_event.h @@ -7,7 +7,7 @@ #define SC_HID_MAX_SIZE 8 -struct sc_hid_event { +struct sc_hid_input { uint16_t hid_id; uint8_t data[SC_HID_MAX_SIZE]; uint8_t size; diff --git a/app/src/hid/hid_keyboard.c b/app/src/hid/hid_keyboard.c index f828d014..9ab444f6 100644 --- a/app/src/hid/hid_keyboard.c +++ b/app/src/hid/hid_keyboard.c @@ -21,7 +21,7 @@ // keyboard support, though OS could support more keys via modifying the report // desc. 6 should be enough for scrcpy. #define SC_HID_KEYBOARD_MAX_KEYS 6 -#define SC_HID_KEYBOARD_EVENT_SIZE \ +#define SC_HID_KEYBOARD_INPUT_SIZE \ (SC_HID_KEYBOARD_INDEX_KEYS + SC_HID_KEYBOARD_MAX_KEYS) #define SC_HID_RESERVED 0x00 @@ -125,7 +125,7 @@ const size_t SC_HID_KEYBOARD_REPORT_DESC_LEN = sizeof(SC_HID_KEYBOARD_REPORT_DESC); /** - * A keyboard HID event is 8 bytes long: + * A keyboard HID input report is 8 bytes long: * * - byte 0: modifiers (1 flag per modifier key, 8 possible modifier keys) * - byte 1: reserved (always 0) @@ -199,11 +199,11 @@ const size_t SC_HID_KEYBOARD_REPORT_DESC_LEN = */ static void -sc_hid_keyboard_event_init(struct sc_hid_event *hid_event) { - hid_event->hid_id = SC_HID_ID_KEYBOARD; - hid_event->size = SC_HID_KEYBOARD_EVENT_SIZE; +sc_hid_keyboard_input_init(struct sc_hid_input *hid_input) { + hid_input->hid_id = SC_HID_ID_KEYBOARD; + hid_input->size = SC_HID_KEYBOARD_INPUT_SIZE; - uint8_t *data = hid_event->data; + uint8_t *data = hid_input->data; data[SC_HID_KEYBOARD_INDEX_MODS] = SC_HID_MOD_NONE; data[1] = SC_HID_RESERVED; @@ -251,9 +251,9 @@ scancode_is_modifier(enum sc_scancode scancode) { } bool -sc_hid_keyboard_event_from_key(struct sc_hid_keyboard *hid, - struct sc_hid_event *hid_event, - const struct sc_key_event *event) { +sc_hid_keyboard_generate_input_from_key(struct sc_hid_keyboard *hid, + struct sc_hid_input *hid_input, + const struct sc_key_event *event) { enum sc_scancode scancode = event->scancode; assert(scancode >= 0); @@ -265,7 +265,7 @@ sc_hid_keyboard_event_from_key(struct sc_hid_keyboard *hid, return false; } - sc_hid_keyboard_event_init(hid_event); + sc_hid_keyboard_input_init(hid_input); uint16_t mods = sc_hid_mod_from_sdl_keymod(event->mods_state); @@ -276,9 +276,9 @@ sc_hid_keyboard_event_from_key(struct sc_hid_keyboard *hid, hid->keys[scancode] ? "true" : "false"); } - hid_event->data[SC_HID_KEYBOARD_INDEX_MODS] = mods; + hid_input->data[SC_HID_KEYBOARD_INDEX_MODS] = mods; - uint8_t *keys_data = &hid_event->data[SC_HID_KEYBOARD_INDEX_KEYS]; + uint8_t *keys_data = &hid_input->data[SC_HID_KEYBOARD_INDEX_KEYS]; // Re-calculate pressed keys every time int keys_pressed_count = 0; for (int i = 0; i < SC_HID_KEYBOARD_KEYS; ++i) { @@ -309,8 +309,8 @@ end: } bool -sc_hid_keyboard_event_from_mods(struct sc_hid_event *event, - uint16_t mods_state) { +sc_hid_keyboard_generate_input_from_mods(struct sc_hid_input *hid_input, + uint16_t mods_state) { bool capslock = mods_state & SC_MOD_CAPS; bool numlock = mods_state & SC_MOD_NUM; if (!capslock && !numlock) { @@ -318,15 +318,15 @@ sc_hid_keyboard_event_from_mods(struct sc_hid_event *event, return false; } - sc_hid_keyboard_event_init(event); + sc_hid_keyboard_input_init(hid_input); unsigned i = 0; if (capslock) { - event->data[SC_HID_KEYBOARD_INDEX_KEYS + i] = SC_SCANCODE_CAPSLOCK; + hid_input->data[SC_HID_KEYBOARD_INDEX_KEYS + i] = SC_SCANCODE_CAPSLOCK; ++i; } if (numlock) { - event->data[SC_HID_KEYBOARD_INDEX_KEYS + i] = SC_SCANCODE_NUMLOCK; + hid_input->data[SC_HID_KEYBOARD_INDEX_KEYS + i] = SC_SCANCODE_NUMLOCK; ++i; } diff --git a/app/src/hid/hid_keyboard.h b/app/src/hid/hid_keyboard.h index 24d64b15..01495cc2 100644 --- a/app/src/hid/hid_keyboard.h +++ b/app/src/hid/hid_keyboard.h @@ -39,12 +39,12 @@ void sc_hid_keyboard_init(struct sc_hid_keyboard *hid); bool -sc_hid_keyboard_event_from_key(struct sc_hid_keyboard *hid, - struct sc_hid_event *hid_event, - const struct sc_key_event *event); +sc_hid_keyboard_generate_input_from_key(struct sc_hid_keyboard *hid, + struct sc_hid_input *hid_input, + const struct sc_key_event *event); bool -sc_hid_keyboard_event_from_mods(struct sc_hid_event *event, - uint16_t mods_state); +sc_hid_keyboard_generate_input_from_mods(struct sc_hid_input *hid_input, + uint16_t mods_state); #endif diff --git a/app/src/hid/hid_mouse.c b/app/src/hid/hid_mouse.c index cc1862bc..e26c248b 100644 --- a/app/src/hid/hid_mouse.c +++ b/app/src/hid/hid_mouse.c @@ -2,7 +2,7 @@ // 1 byte for buttons + padding, 1 byte for X position, 1 byte for Y position, // 1 byte for wheel motion -#define SC_HID_MOUSE_EVENT_SIZE 4 +#define SC_HID_MOUSE_INPUT_SIZE 4 /** * Mouse descriptor from the specification: @@ -84,7 +84,7 @@ const size_t SC_HID_MOUSE_REPORT_DESC_LEN = sizeof(SC_HID_MOUSE_REPORT_DESC); /** - * A mouse HID event is 4 bytes long: + * A mouse HID input report is 4 bytes long: * * - byte 0: buttons state * - byte 1: relative x motion (signed byte from -127 to 127) @@ -125,11 +125,10 @@ const size_t SC_HID_MOUSE_REPORT_DESC_LEN = */ static void -sc_hid_mouse_event_init(struct sc_hid_event *hid_event) { - hid_event->hid_id = SC_HID_ID_MOUSE; - hid_event->size = SC_HID_MOUSE_EVENT_SIZE; - // Leave hid_event->data uninitialized, it will be fully initialized by - // callers +sc_hid_mouse_input_init(struct sc_hid_input *hid_input) { + hid_input->hid_id = SC_HID_ID_MOUSE; + hid_input->size = SC_HID_MOUSE_INPUT_SIZE; + // Leave ->data uninitialized, it will be fully initialized by callers } static uint8_t @@ -154,11 +153,11 @@ sc_hid_buttons_from_buttons_state(uint8_t buttons_state) { } void -sc_hid_mouse_event_from_motion(struct sc_hid_event *hid_event, - const struct sc_mouse_motion_event *event) { - sc_hid_mouse_event_init(hid_event); +sc_hid_mouse_generate_input_from_motion(struct sc_hid_input *hid_input, + const struct sc_mouse_motion_event *event) { + sc_hid_mouse_input_init(hid_input); - uint8_t *data = hid_event->data; + uint8_t *data = hid_input->data; data[0] = sc_hid_buttons_from_buttons_state(event->buttons_state); data[1] = CLAMP(event->xrel, -127, 127); data[2] = CLAMP(event->yrel, -127, 127); @@ -166,11 +165,11 @@ sc_hid_mouse_event_from_motion(struct sc_hid_event *hid_event, } void -sc_hid_mouse_event_from_click(struct sc_hid_event *hid_event, - const struct sc_mouse_click_event *event) { - sc_hid_mouse_event_init(hid_event); +sc_hid_mouse_generate_input_from_click(struct sc_hid_input *hid_input, + const struct sc_mouse_click_event *event) { + sc_hid_mouse_input_init(hid_input); - uint8_t *data = hid_event->data; + uint8_t *data = hid_input->data; data[0] = sc_hid_buttons_from_buttons_state(event->buttons_state); data[1] = 0; // no x motion data[2] = 0; // no y motion @@ -178,11 +177,11 @@ sc_hid_mouse_event_from_click(struct sc_hid_event *hid_event, } void -sc_hid_mouse_event_from_scroll(struct sc_hid_event *hid_event, - const struct sc_mouse_scroll_event *event) { - sc_hid_mouse_event_init(hid_event); +sc_hid_mouse_generate_input_from_scroll(struct sc_hid_input *hid_input, + const struct sc_mouse_scroll_event *event) { + sc_hid_mouse_input_init(hid_input); - uint8_t *data = hid_event->data; + uint8_t *data = hid_input->data; data[0] = 0; // buttons state irrelevant (and unknown) data[1] = 0; // no x motion data[2] = 0; // no y motion diff --git a/app/src/hid/hid_mouse.h b/app/src/hid/hid_mouse.h index 91337de5..3b647fb3 100644 --- a/app/src/hid/hid_mouse.h +++ b/app/src/hid/hid_mouse.h @@ -14,15 +14,15 @@ extern const uint8_t SC_HID_MOUSE_REPORT_DESC[]; extern const size_t SC_HID_MOUSE_REPORT_DESC_LEN; void -sc_hid_mouse_event_from_motion(struct sc_hid_event *hid_event, - const struct sc_mouse_motion_event *event); +sc_hid_mouse_generate_input_from_motion(struct sc_hid_input *hid_input, + const struct sc_mouse_motion_event *event); void -sc_hid_mouse_event_from_click(struct sc_hid_event *hid_event, - const struct sc_mouse_click_event *event); +sc_hid_mouse_generate_input_from_click(struct sc_hid_input *hid_input, + const struct sc_mouse_click_event *event); void -sc_hid_mouse_event_from_scroll(struct sc_hid_event *hid_event, - const struct sc_mouse_scroll_event *event); +sc_hid_mouse_generate_input_from_scroll(struct sc_hid_input *hid_input, + const struct sc_mouse_scroll_event *event); #endif diff --git a/app/src/uhid/keyboard_uhid.c b/app/src/uhid/keyboard_uhid.c index 7d5c6493..c91f9539 100644 --- a/app/src/uhid/keyboard_uhid.c +++ b/app/src/uhid/keyboard_uhid.c @@ -11,14 +11,14 @@ static void sc_keyboard_uhid_send_input(struct sc_keyboard_uhid *kb, - const struct sc_hid_event *event) { + const struct sc_hid_input *hid_input) { struct sc_control_msg msg; msg.type = SC_CONTROL_MSG_TYPE_UHID_INPUT; - msg.uhid_input.id = event->hid_id; + msg.uhid_input.id = hid_input->hid_id; - assert(event->size <= SC_HID_MAX_SIZE); - memcpy(msg.uhid_input.data, event->data, event->size); - msg.uhid_input.size = event->size; + assert(hid_input->size <= SC_HID_MAX_SIZE); + memcpy(msg.uhid_input.data, hid_input->data, hid_input->size); + msg.uhid_input.size = hid_input->size; if (!sc_controller_push_msg(kb->controller, &msg)) { LOGE("Could not send UHID_INPUT message (key)"); @@ -37,14 +37,14 @@ sc_keyboard_uhid_synchronize_mod(struct sc_keyboard_uhid *kb) { // or HID output anyway kb->device_mod = mod; - struct sc_hid_event hid_event; - if (!sc_hid_keyboard_event_from_mods(&hid_event, diff)) { + struct sc_hid_input hid_input; + if (!sc_hid_keyboard_generate_input_from_mods(&hid_input, diff)) { return; } LOGV("HID keyboard state synchronized"); - sc_keyboard_uhid_send_input(kb, &hid_event); + sc_keyboard_uhid_send_input(kb, &hid_input); } } @@ -64,10 +64,10 @@ sc_key_processor_process_key(struct sc_key_processor *kp, struct sc_keyboard_uhid *kb = DOWNCAST(kp); - struct sc_hid_event hid_event; + struct sc_hid_input hid_input; // Not all keys are supported, just ignore unsupported keys - if (sc_hid_keyboard_event_from_key(&kb->hid, &hid_event, event)) { + if (sc_hid_keyboard_generate_input_from_key(&kb->hid, &hid_input, event)) { if (event->scancode == SC_SCANCODE_CAPSLOCK) { kb->device_mod ^= SC_MOD_CAPS; } else if (event->scancode == SC_SCANCODE_NUMLOCK) { @@ -77,7 +77,7 @@ sc_key_processor_process_key(struct sc_key_processor *kp, // change the modifiers) sc_keyboard_uhid_synchronize_mod(kb); } - sc_keyboard_uhid_send_input(kb, &hid_event); + sc_keyboard_uhid_send_input(kb, &hid_input); } } diff --git a/app/src/uhid/mouse_uhid.c b/app/src/uhid/mouse_uhid.c index 21dc018a..e1daa9e5 100644 --- a/app/src/uhid/mouse_uhid.c +++ b/app/src/uhid/mouse_uhid.c @@ -9,14 +9,15 @@ static void sc_mouse_uhid_send_input(struct sc_mouse_uhid *mouse, - const struct sc_hid_event *event, const char *name) { + const struct sc_hid_input *hid_input, + const char *name) { struct sc_control_msg msg; msg.type = SC_CONTROL_MSG_TYPE_UHID_INPUT; - msg.uhid_input.id = event->hid_id; + msg.uhid_input.id = hid_input->hid_id; - assert(event->size <= SC_HID_MAX_SIZE); - memcpy(msg.uhid_input.data, event->data, event->size); - msg.uhid_input.size = event->size; + assert(hid_input->size <= SC_HID_MAX_SIZE); + memcpy(msg.uhid_input.data, hid_input->data, hid_input->size); + msg.uhid_input.size = hid_input->size; if (!sc_controller_push_msg(mouse->controller, &msg)) { LOGE("Could not send UHID_INPUT message (%s)", name); @@ -28,10 +29,10 @@ sc_mouse_processor_process_mouse_motion(struct sc_mouse_processor *mp, const struct sc_mouse_motion_event *event) { struct sc_mouse_uhid *mouse = DOWNCAST(mp); - struct sc_hid_event hid_event; - sc_hid_mouse_event_from_motion(&hid_event, event); + struct sc_hid_input hid_input; + sc_hid_mouse_generate_input_from_motion(&hid_input, event); - sc_mouse_uhid_send_input(mouse, &hid_event, "mouse motion"); + sc_mouse_uhid_send_input(mouse, &hid_input, "mouse motion"); } static void @@ -39,10 +40,10 @@ sc_mouse_processor_process_mouse_click(struct sc_mouse_processor *mp, const struct sc_mouse_click_event *event) { struct sc_mouse_uhid *mouse = DOWNCAST(mp); - struct sc_hid_event hid_event; - sc_hid_mouse_event_from_click(&hid_event, event); + struct sc_hid_input hid_input; + sc_hid_mouse_generate_input_from_click(&hid_input, event); - sc_mouse_uhid_send_input(mouse, &hid_event, "mouse click"); + sc_mouse_uhid_send_input(mouse, &hid_input, "mouse click"); } static void @@ -50,10 +51,10 @@ sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp, const struct sc_mouse_scroll_event *event) { struct sc_mouse_uhid *mouse = DOWNCAST(mp); - struct sc_hid_event hid_event; - sc_hid_mouse_event_from_scroll(&hid_event, event); + struct sc_hid_input hid_input; + sc_hid_mouse_generate_input_from_scroll(&hid_input, event); - sc_mouse_uhid_send_input(mouse, &hid_event, "mouse scroll"); + sc_mouse_uhid_send_input(mouse, &hid_input, "mouse scroll"); } bool diff --git a/app/src/usb/aoa_hid.c b/app/src/usb/aoa_hid.c index ff2516e5..f9bcf8e5 100644 --- a/app/src/usb/aoa_hid.c +++ b/app/src/usb/aoa_hid.c @@ -20,14 +20,14 @@ #define SC_AOA_EVENT_QUEUE_LIMIT 60 static void -sc_hid_event_log(const struct sc_hid_event *event) { - // HID Event: [00] FF FF FF FF... - assert(event->size); - char *hex = sc_str_to_hex_string(event->data, event->size); +sc_hid_input_log(const struct sc_hid_input *hid_input) { + // HID input: [00] FF FF FF FF... + assert(hid_input->size); + char *hex = sc_str_to_hex_string(hid_input->data, hid_input->size); if (!hex) { return; } - LOGV("HID Event: [%" PRIu16 "] %s", event->hid_id, hex); + LOGV("HID input: [%" PRIu16 "] %s", hid_input->hid_id, hex); free(hex); } @@ -129,16 +129,17 @@ sc_aoa_set_hid_report_desc(struct sc_aoa *aoa, uint16_t accessory_id, } static bool -sc_aoa_send_hid_event(struct sc_aoa *aoa, const struct sc_hid_event *event) { +sc_aoa_send_hid_event(struct sc_aoa *aoa, + const struct sc_hid_input *hid_input) { uint8_t request_type = LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR; uint8_t request = ACCESSORY_SEND_HID_EVENT; // // value (arg0): accessory assigned ID for the HID device // index (arg1): 0 (unused) - uint16_t value = event->hid_id; + uint16_t value = hid_input->hid_id; uint16_t index = 0; - unsigned char *data = (uint8_t *) event->data; // discard const - uint16_t length = event->size; + unsigned char *data = (uint8_t *) hid_input->data; // discard const + uint16_t length = hid_input->size; int result = libusb_control_transfer(aoa->usb->handle, request_type, request, value, index, data, length, DEFAULT_TIMEOUT); @@ -195,11 +196,11 @@ sc_aoa_setup_hid(struct sc_aoa *aoa, uint16_t accessory_id, } bool -sc_aoa_push_hid_event_with_ack_to_wait(struct sc_aoa *aoa, - const struct sc_hid_event *event, - uint64_t ack_to_wait) { +sc_aoa_push_input_with_ack_to_wait(struct sc_aoa *aoa, + const struct sc_hid_input *hid_input, + uint64_t ack_to_wait) { if (sc_get_log_level() <= SC_LOG_LEVEL_VERBOSE) { - sc_hid_event_log(event); + sc_hid_input_log(hid_input); } sc_mutex_lock(&aoa->mutex); @@ -213,7 +214,7 @@ sc_aoa_push_hid_event_with_ack_to_wait(struct sc_aoa *aoa, struct sc_aoa_event *aoa_event = sc_vecdeque_push_hole_noresize(&aoa->queue); aoa_event->type = SC_AOA_EVENT_TYPE_INPUT; - aoa_event->input.hid = *event; + aoa_event->input.hid = *hid_input; aoa_event->input.ack_to_wait = ack_to_wait; pushed = true; diff --git a/app/src/usb/aoa_hid.h b/app/src/usb/aoa_hid.h index b2dc04ac..63a9f4de 100644 --- a/app/src/usb/aoa_hid.h +++ b/app/src/usb/aoa_hid.h @@ -31,7 +31,7 @@ struct sc_aoa_event { uint16_t hid_id; } close; struct { - struct sc_hid_event hid; + struct sc_hid_input hid; uint64_t ack_to_wait; } input; }; @@ -82,14 +82,14 @@ bool sc_aoa_push_close(struct sc_aoa *aoa, uint16_t accessory_id); bool -sc_aoa_push_hid_event_with_ack_to_wait(struct sc_aoa *aoa, - const struct sc_hid_event *event, - uint64_t ack_to_wait); +sc_aoa_push_input_with_ack_to_wait(struct sc_aoa *aoa, + const struct sc_hid_input *hid_input, + uint64_t ack_to_wait); static inline bool -sc_aoa_push_hid_event(struct sc_aoa *aoa, const struct sc_hid_event *event) { - return sc_aoa_push_hid_event_with_ack_to_wait(aoa, event, - SC_SEQUENCE_INVALID); +sc_aoa_push_input(struct sc_aoa *aoa, const struct sc_hid_input *hid_input) { + return sc_aoa_push_input_with_ack_to_wait(aoa, hid_input, + SC_SEQUENCE_INVALID); } #endif diff --git a/app/src/usb/keyboard_aoa.c b/app/src/usb/keyboard_aoa.c index 0052c3d8..33924dbf 100644 --- a/app/src/usb/keyboard_aoa.c +++ b/app/src/usb/keyboard_aoa.c @@ -10,14 +10,14 @@ static bool push_mod_lock_state(struct sc_keyboard_aoa *kb, uint16_t mods_state) { - struct sc_hid_event hid_event; - if (!sc_hid_keyboard_event_from_mods(&hid_event, mods_state)) { + struct sc_hid_input hid_input; + if (!sc_hid_keyboard_generate_input_from_mods(&hid_input, mods_state)) { // Nothing to do return true; } - if (!sc_aoa_push_hid_event(kb->aoa, &hid_event)) { - LOGW("Could not request HID event (mod lock state)"); + if (!sc_aoa_push_input(kb->aoa, &hid_input)) { + LOGW("Could not push HID input (mod lock state)"); return false; } @@ -38,10 +38,10 @@ sc_key_processor_process_key(struct sc_key_processor *kp, struct sc_keyboard_aoa *kb = DOWNCAST(kp); - struct sc_hid_event hid_event; + struct sc_hid_input hid_input; // Not all keys are supported, just ignore unsupported keys - if (sc_hid_keyboard_event_from_key(&kb->hid, &hid_event, event)) { + if (sc_hid_keyboard_generate_input_from_key(&kb->hid, &hid_input, event)) { if (!kb->mod_lock_synchronized) { // Inject CAPSLOCK and/or NUMLOCK if necessary to synchronize // keyboard state @@ -55,9 +55,9 @@ sc_key_processor_process_key(struct sc_key_processor *kp, // synchronization is acknowledged by the server, otherwise it could // paste the old clipboard content. - if (!sc_aoa_push_hid_event_with_ack_to_wait(kb->aoa, &hid_event, - ack_to_wait)) { - LOGW("Could not request HID event (key)"); + if (!sc_aoa_push_input_with_ack_to_wait(kb->aoa, &hid_input, + ack_to_wait)) { + LOGW("Could not push HID input (key)"); } } } diff --git a/app/src/usb/mouse_aoa.c b/app/src/usb/mouse_aoa.c index 84fd8d64..03d28610 100644 --- a/app/src/usb/mouse_aoa.c +++ b/app/src/usb/mouse_aoa.c @@ -14,11 +14,11 @@ sc_mouse_processor_process_mouse_motion(struct sc_mouse_processor *mp, const struct sc_mouse_motion_event *event) { struct sc_mouse_aoa *mouse = DOWNCAST(mp); - struct sc_hid_event hid_event; - sc_hid_mouse_event_from_motion(&hid_event, event); + struct sc_hid_input hid_input; + sc_hid_mouse_generate_input_from_motion(&hid_input, event); - if (!sc_aoa_push_hid_event(mouse->aoa, &hid_event)) { - LOGW("Could not request HID event (mouse motion)"); + if (!sc_aoa_push_input(mouse->aoa, &hid_input)) { + LOGW("Could not push HID input (mouse motion)"); } } @@ -27,11 +27,11 @@ sc_mouse_processor_process_mouse_click(struct sc_mouse_processor *mp, const struct sc_mouse_click_event *event) { struct sc_mouse_aoa *mouse = DOWNCAST(mp); - struct sc_hid_event hid_event; - sc_hid_mouse_event_from_click(&hid_event, event); + struct sc_hid_input hid_input; + sc_hid_mouse_generate_input_from_click(&hid_input, event); - if (!sc_aoa_push_hid_event(mouse->aoa, &hid_event)) { - LOGW("Could not request HID event (mouse click)"); + if (!sc_aoa_push_input(mouse->aoa, &hid_input)) { + LOGW("Could not push HID input (mouse click)"); } } @@ -40,11 +40,11 @@ sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp, const struct sc_mouse_scroll_event *event) { struct sc_mouse_aoa *mouse = DOWNCAST(mp); - struct sc_hid_event hid_event; - sc_hid_mouse_event_from_scroll(&hid_event, event); + struct sc_hid_input hid_input; + sc_hid_mouse_generate_input_from_scroll(&hid_input, event); - if (!sc_aoa_push_hid_event(mouse->aoa, &hid_event)) { - LOGW("Could not request HID event (mouse scroll)"); + if (!sc_aoa_push_input(mouse->aoa, &hid_input)) { + LOGW("Could not push HID input (mouse scroll)"); } } From 6f0c9eba9bb030cc8e4e6bc4c14ae2c9dd730348 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:08:08 +0200 Subject: [PATCH 1938/2244] Introduce hid_open and hid_close events This allows to handle HID open/close reports at the same place as HID input reports (in the HID layer). This will be especially useful to manage HID gamepads, to avoid implementing one part in the HID layer and another part in the gamepad processor implementation. PR #5270 --- app/src/hid/hid_event.h | 10 ++++++++++ app/src/hid/hid_keyboard.c | 15 +++++++++++---- app/src/hid/hid_keyboard.h | 9 ++++++--- app/src/hid/hid_mouse.c | 15 +++++++++++---- app/src/hid/hid_mouse.h | 7 +++++-- app/src/uhid/keyboard_uhid.c | 8 ++++++-- app/src/uhid/mouse_uhid.c | 8 ++++++-- app/src/usb/aoa_hid.c | 31 +++++++++++++++---------------- app/src/usb/aoa_hid.h | 11 ++++------- app/src/usb/keyboard_aoa.c | 12 ++++++++---- app/src/usb/mouse_aoa.c | 12 ++++++++---- 11 files changed, 90 insertions(+), 48 deletions(-) diff --git a/app/src/hid/hid_event.h b/app/src/hid/hid_event.h index 9f9432e6..9171004e 100644 --- a/app/src/hid/hid_event.h +++ b/app/src/hid/hid_event.h @@ -13,4 +13,14 @@ struct sc_hid_input { uint8_t size; }; +struct sc_hid_open { + uint16_t hid_id; + const uint8_t *report_desc; // pointer to static memory + size_t report_desc_size; +}; + +struct sc_hid_close { + uint16_t hid_id; +}; + #endif diff --git a/app/src/hid/hid_keyboard.c b/app/src/hid/hid_keyboard.c index 9ab444f6..64dffe80 100644 --- a/app/src/hid/hid_keyboard.c +++ b/app/src/hid/hid_keyboard.c @@ -47,7 +47,7 @@ * * (change vid:pid' to your device's vendor ID and product ID). */ -const uint8_t SC_HID_KEYBOARD_REPORT_DESC[] = { +static const uint8_t SC_HID_KEYBOARD_REPORT_DESC[] = { // Usage Page (Generic Desktop) 0x05, 0x01, // Usage (Keyboard) @@ -121,9 +121,6 @@ const uint8_t SC_HID_KEYBOARD_REPORT_DESC[] = { 0xC0 }; -const size_t SC_HID_KEYBOARD_REPORT_DESC_LEN = - sizeof(SC_HID_KEYBOARD_REPORT_DESC); - /** * A keyboard HID input report is 8 bytes long: * @@ -332,3 +329,13 @@ sc_hid_keyboard_generate_input_from_mods(struct sc_hid_input *hid_input, return true; } + +void sc_hid_keyboard_generate_open(struct sc_hid_open *hid_open) { + hid_open->hid_id = SC_HID_ID_KEYBOARD; + hid_open->report_desc = SC_HID_KEYBOARD_REPORT_DESC; + hid_open->report_desc_size = sizeof(SC_HID_KEYBOARD_REPORT_DESC); +} + +void sc_hid_keyboard_generate_close(struct sc_hid_close *hid_close) { + hid_close->hid_id = SC_HID_ID_KEYBOARD; +} diff --git a/app/src/hid/hid_keyboard.h b/app/src/hid/hid_keyboard.h index 01495cc2..cde1ac52 100644 --- a/app/src/hid/hid_keyboard.h +++ b/app/src/hid/hid_keyboard.h @@ -16,9 +16,6 @@ #define SC_HID_ID_KEYBOARD 1 -extern const uint8_t SC_HID_KEYBOARD_REPORT_DESC[]; -extern const size_t SC_HID_KEYBOARD_REPORT_DESC_LEN; - /** * HID keyboard events are sequence-based, every time keyboard state changes * it sends an array of currently pressed keys, the host is responsible for @@ -38,6 +35,12 @@ struct sc_hid_keyboard { void sc_hid_keyboard_init(struct sc_hid_keyboard *hid); +void +sc_hid_keyboard_generate_open(struct sc_hid_open *hid_open); + +void +sc_hid_keyboard_generate_close(struct sc_hid_close *hid_close); + bool sc_hid_keyboard_generate_input_from_key(struct sc_hid_keyboard *hid, struct sc_hid_input *hid_input, diff --git a/app/src/hid/hid_mouse.c b/app/src/hid/hid_mouse.c index e26c248b..d1aae83a 100644 --- a/app/src/hid/hid_mouse.c +++ b/app/src/hid/hid_mouse.c @@ -14,7 +14,7 @@ * * §4 Generic Desktop Page (0x01) (p26) */ -const uint8_t SC_HID_MOUSE_REPORT_DESC[] = { +static const uint8_t SC_HID_MOUSE_REPORT_DESC[] = { // Usage Page (Generic Desktop) 0x05, 0x01, // Usage (Mouse) @@ -80,9 +80,6 @@ const uint8_t SC_HID_MOUSE_REPORT_DESC[] = { 0xC0, }; -const size_t SC_HID_MOUSE_REPORT_DESC_LEN = - sizeof(SC_HID_MOUSE_REPORT_DESC); - /** * A mouse HID input report is 4 bytes long: * @@ -190,3 +187,13 @@ sc_hid_mouse_generate_input_from_scroll(struct sc_hid_input *hid_input, data[3] = CLAMP(event->vscroll, -127, 127); // Horizontal scrolling ignored } + +void sc_hid_mouse_generate_open(struct sc_hid_open *hid_open) { + hid_open->hid_id = SC_HID_ID_MOUSE; + hid_open->report_desc = SC_HID_MOUSE_REPORT_DESC; + hid_open->report_desc_size = sizeof(SC_HID_MOUSE_REPORT_DESC); +} + +void sc_hid_mouse_generate_close(struct sc_hid_close *hid_close) { + hid_close->hid_id = SC_HID_ID_MOUSE; +} diff --git a/app/src/hid/hid_mouse.h b/app/src/hid/hid_mouse.h index 3b647fb3..a9a54718 100644 --- a/app/src/hid/hid_mouse.h +++ b/app/src/hid/hid_mouse.h @@ -10,8 +10,11 @@ #define SC_HID_ID_MOUSE 2 -extern const uint8_t SC_HID_MOUSE_REPORT_DESC[]; -extern const size_t SC_HID_MOUSE_REPORT_DESC_LEN; +void +sc_hid_mouse_generate_open(struct sc_hid_open *hid_open); + +void +sc_hid_mouse_generate_close(struct sc_hid_close *hid_close); void sc_hid_mouse_generate_input_from_motion(struct sc_hid_input *hid_input, diff --git a/app/src/uhid/keyboard_uhid.c b/app/src/uhid/keyboard_uhid.c index c91f9539..e7a0e33a 100644 --- a/app/src/uhid/keyboard_uhid.c +++ b/app/src/uhid/keyboard_uhid.c @@ -145,11 +145,15 @@ sc_keyboard_uhid_init(struct sc_keyboard_uhid *kb, kb->uhid_receiver.ops = &uhid_receiver_ops; sc_uhid_devices_add_receiver(uhid_devices, &kb->uhid_receiver); + struct sc_hid_open hid_open; + sc_hid_keyboard_generate_open(&hid_open); + assert(hid_open.hid_id == SC_HID_ID_KEYBOARD); + struct sc_control_msg msg; msg.type = SC_CONTROL_MSG_TYPE_UHID_CREATE; msg.uhid_create.id = SC_HID_ID_KEYBOARD; - msg.uhid_create.report_desc = SC_HID_KEYBOARD_REPORT_DESC; - msg.uhid_create.report_desc_size = SC_HID_KEYBOARD_REPORT_DESC_LEN; + msg.uhid_create.report_desc = hid_open.report_desc; + msg.uhid_create.report_desc_size = hid_open.report_desc_size; if (!sc_controller_push_msg(controller, &msg)) { LOGE("Could not send UHID_CREATE message (keyboard)"); return false; diff --git a/app/src/uhid/mouse_uhid.c b/app/src/uhid/mouse_uhid.c index e1daa9e5..c379f3ad 100644 --- a/app/src/uhid/mouse_uhid.c +++ b/app/src/uhid/mouse_uhid.c @@ -74,11 +74,15 @@ sc_mouse_uhid_init(struct sc_mouse_uhid *mouse, mouse->mouse_processor.relative_mode = true; + struct sc_hid_open hid_open; + sc_hid_mouse_generate_open(&hid_open); + assert(hid_open.hid_id == SC_HID_ID_MOUSE); + struct sc_control_msg msg; msg.type = SC_CONTROL_MSG_TYPE_UHID_CREATE; msg.uhid_create.id = SC_HID_ID_MOUSE; - msg.uhid_create.report_desc = SC_HID_MOUSE_REPORT_DESC; - msg.uhid_create.report_desc_size = SC_HID_MOUSE_REPORT_DESC_LEN; + msg.uhid_create.report_desc = hid_open.report_desc; + msg.uhid_create.report_desc_size = hid_open.report_desc_size; if (!sc_controller_push_msg(controller, &msg)) { LOGE("Could not send UHID_CREATE message (mouse)"); return false; diff --git a/app/src/usb/aoa_hid.c b/app/src/usb/aoa_hid.c index f9bcf8e5..c44c80f6 100644 --- a/app/src/usb/aoa_hid.c +++ b/app/src/usb/aoa_hid.c @@ -230,8 +230,7 @@ sc_aoa_push_input_with_ack_to_wait(struct sc_aoa *aoa, } bool -sc_aoa_push_open(struct sc_aoa *aoa, uint16_t accessory_id, - const uint8_t *report_desc, uint16_t report_desc_size) { +sc_aoa_push_open(struct sc_aoa *aoa, const struct sc_hid_open *hid_open) { // TODO log verbose sc_mutex_lock(&aoa->mutex); @@ -247,9 +246,7 @@ sc_aoa_push_open(struct sc_aoa *aoa, uint16_t accessory_id, } aoa_event->type = SC_AOA_EVENT_TYPE_OPEN; - aoa_event->open.hid_id = accessory_id; - aoa_event->open.report_desc = report_desc; - aoa_event->open.report_desc_size = report_desc_size; + aoa_event->open.hid = *hid_open; if (was_empty) { sc_cond_signal(&aoa->event_cond); @@ -261,7 +258,7 @@ sc_aoa_push_open(struct sc_aoa *aoa, uint16_t accessory_id, } bool -sc_aoa_push_close(struct sc_aoa *aoa, uint16_t accessory_id) { +sc_aoa_push_close(struct sc_aoa *aoa, const struct sc_hid_close *hid_close) { // TODO log verbose sc_mutex_lock(&aoa->mutex); @@ -277,7 +274,7 @@ sc_aoa_push_close(struct sc_aoa *aoa, uint16_t accessory_id) { } aoa_event->type = SC_AOA_EVENT_TYPE_CLOSE; - aoa_event->close.hid_id = accessory_id; + aoa_event->close.hid = *hid_close; if (was_empty) { sc_cond_signal(&aoa->event_cond); @@ -316,29 +313,31 @@ sc_aoa_process_event(struct sc_aoa *aoa, struct sc_aoa_event *event) { } } - bool ok = sc_aoa_send_hid_event(aoa, &event->input.hid); + struct sc_hid_input *hid_input = &event->input.hid; + bool ok = sc_aoa_send_hid_event(aoa, hid_input); if (!ok) { LOGW("Could not send HID event to USB device: %" PRIu16, - event->input.hid.hid_id); + hid_input->hid_id); } break; } case SC_AOA_EVENT_TYPE_OPEN: { - bool ok = sc_aoa_setup_hid(aoa, event->open.hid_id, - event->open.report_desc, - event->open.report_desc_size); + struct sc_hid_open *hid_open = &event->open.hid; + bool ok = sc_aoa_setup_hid(aoa, hid_open->hid_id, + hid_open->report_desc, + hid_open->report_desc_size); if (!ok) { - LOGW("Could not open AOA device: %" PRIu16, event->open.hid_id); + LOGW("Could not open AOA device: %" PRIu16, hid_open->hid_id); } break; } case SC_AOA_EVENT_TYPE_CLOSE: { - bool ok = sc_aoa_unregister_hid(aoa, event->close.hid_id); + struct sc_hid_close *hid_close = &event->close.hid; + bool ok = sc_aoa_unregister_hid(aoa, hid_close->hid_id); if (!ok) { - LOGW("Could not close AOA device: %" PRIu16, - event->close.hid_id); + LOGW("Could not close AOA device: %" PRIu16, hid_close->hid_id); } break; diff --git a/app/src/usb/aoa_hid.h b/app/src/usb/aoa_hid.h index 63a9f4de..010b3742 100644 --- a/app/src/usb/aoa_hid.h +++ b/app/src/usb/aoa_hid.h @@ -23,12 +23,10 @@ struct sc_aoa_event { enum sc_aoa_event_type type; union { struct { - uint16_t hid_id; - const uint8_t *report_desc; // pointer to static memory - uint16_t report_desc_size; + struct sc_hid_open hid; } open; struct { - uint16_t hid_id; + struct sc_hid_close hid; } close; struct { struct sc_hid_input hid; @@ -75,11 +73,10 @@ sc_aoa_join(struct sc_aoa *aoa); // report_desc must be a pointer to static memory, accessed at any time from // another thread bool -sc_aoa_push_open(struct sc_aoa *aoa, uint16_t accessory_id, - const uint8_t *report_desc, uint16_t report_desc_size); +sc_aoa_push_open(struct sc_aoa *aoa, const struct sc_hid_open *hid_open); bool -sc_aoa_push_close(struct sc_aoa *aoa, uint16_t accessory_id); +sc_aoa_push_close(struct sc_aoa *aoa, const struct sc_hid_close *hid_close); bool sc_aoa_push_input_with_ack_to_wait(struct sc_aoa *aoa, diff --git a/app/src/usb/keyboard_aoa.c b/app/src/usb/keyboard_aoa.c index 33924dbf..6c4aaed7 100644 --- a/app/src/usb/keyboard_aoa.c +++ b/app/src/usb/keyboard_aoa.c @@ -66,9 +66,10 @@ bool sc_keyboard_aoa_init(struct sc_keyboard_aoa *kb, struct sc_aoa *aoa) { kb->aoa = aoa; - bool ok = sc_aoa_push_open(aoa, SC_HID_ID_KEYBOARD, - SC_HID_KEYBOARD_REPORT_DESC, - SC_HID_KEYBOARD_REPORT_DESC_LEN); + struct sc_hid_open hid_open; + sc_hid_keyboard_generate_open(&hid_open); + + bool ok = sc_aoa_push_open(aoa, &hid_open); if (!ok) { LOGW("Could not push AOA keyboard open request"); return false; @@ -97,7 +98,10 @@ sc_keyboard_aoa_init(struct sc_keyboard_aoa *kb, struct sc_aoa *aoa) { void sc_keyboard_aoa_destroy(struct sc_keyboard_aoa *kb) { - bool ok = sc_aoa_push_close(kb->aoa, SC_HID_ID_KEYBOARD); + struct sc_hid_close hid_close; + sc_hid_keyboard_generate_close(&hid_close); + + bool ok = sc_aoa_push_close(kb->aoa, &hid_close); if (!ok) { LOGW("Could not push AOA keyboard close request"); } diff --git a/app/src/usb/mouse_aoa.c b/app/src/usb/mouse_aoa.c index 03d28610..3c4e3693 100644 --- a/app/src/usb/mouse_aoa.c +++ b/app/src/usb/mouse_aoa.c @@ -52,9 +52,10 @@ bool sc_mouse_aoa_init(struct sc_mouse_aoa *mouse, struct sc_aoa *aoa) { mouse->aoa = aoa; - bool ok = sc_aoa_push_open(aoa, SC_HID_ID_MOUSE, - SC_HID_MOUSE_REPORT_DESC, - SC_HID_MOUSE_REPORT_DESC_LEN); + struct sc_hid_open hid_open; + sc_hid_mouse_generate_open(&hid_open); + + bool ok = sc_aoa_push_open(aoa, &hid_open); if (!ok) { LOGW("Could not push AOA mouse open request"); return false; @@ -77,7 +78,10 @@ sc_mouse_aoa_init(struct sc_mouse_aoa *mouse, struct sc_aoa *aoa) { void sc_mouse_aoa_destroy(struct sc_mouse_aoa *mouse) { - bool ok = sc_aoa_push_close(mouse->aoa, SC_HID_ID_MOUSE); + struct sc_hid_close hid_close; + sc_hid_mouse_generate_close(&hid_close); + + bool ok = sc_aoa_push_close(mouse->aoa, &hid_close); if (!ok) { LOGW("Could not push AOA mouse close request"); } From d748ac75e651ff446df74896a39deb6bd427fe67 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:08:08 +0200 Subject: [PATCH 1939/2244] Add AOA open/close verbose logs PR #5270 --- app/src/usb/aoa_hid.c | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/app/src/usb/aoa_hid.c b/app/src/usb/aoa_hid.c index c44c80f6..ef10e460 100644 --- a/app/src/usb/aoa_hid.c +++ b/app/src/usb/aoa_hid.c @@ -31,6 +31,25 @@ sc_hid_input_log(const struct sc_hid_input *hid_input) { free(hex); } +static void +sc_hid_open_log(const struct sc_hid_open *hid_open) { + // HID open: [00] FF FF FF FF... + assert(hid_open->report_desc_size); + char *hex = sc_str_to_hex_string(hid_open->report_desc, + hid_open->report_desc_size); + if (!hex) { + return; + } + LOGV("HID open: [%" PRIu16 "] %s", hid_open->hid_id, hex); + free(hex); +} + +static void +sc_hid_close_log(const struct sc_hid_close *hid_close) { + // HID close: [00] + LOGV("HID close: [%" PRIu16 "]", hid_close->hid_id); +} + bool sc_aoa_init(struct sc_aoa *aoa, struct sc_usb *usb, struct sc_acksync *acksync) { @@ -231,7 +250,9 @@ sc_aoa_push_input_with_ack_to_wait(struct sc_aoa *aoa, bool sc_aoa_push_open(struct sc_aoa *aoa, const struct sc_hid_open *hid_open) { - // TODO log verbose + if (sc_get_log_level() <= SC_LOG_LEVEL_VERBOSE) { + sc_hid_open_log(hid_open); + } sc_mutex_lock(&aoa->mutex); bool was_empty = sc_vecdeque_is_empty(&aoa->queue); @@ -259,7 +280,9 @@ sc_aoa_push_open(struct sc_aoa *aoa, const struct sc_hid_open *hid_open) { bool sc_aoa_push_close(struct sc_aoa *aoa, const struct sc_hid_close *hid_close) { - // TODO log verbose + if (sc_get_log_level() <= SC_LOG_LEVEL_VERBOSE) { + sc_hid_close_log(hid_close); + } sc_mutex_lock(&aoa->mutex); bool was_empty = sc_vecdeque_is_empty(&aoa->queue); From 6c707ad8a359a5acf1c59fd065d50b5e1550c781 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:08:08 +0200 Subject: [PATCH 1940/2244] Make HID logs uniform PR #5270 --- app/src/uhid/keyboard_uhid.c | 2 +- app/src/uhid/mouse_uhid.c | 4 ++-- app/src/usb/keyboard_aoa.c | 8 ++++---- app/src/usb/mouse_aoa.c | 10 +++++----- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/app/src/uhid/keyboard_uhid.c b/app/src/uhid/keyboard_uhid.c index e7a0e33a..11d41e40 100644 --- a/app/src/uhid/keyboard_uhid.c +++ b/app/src/uhid/keyboard_uhid.c @@ -21,7 +21,7 @@ sc_keyboard_uhid_send_input(struct sc_keyboard_uhid *kb, msg.uhid_input.size = hid_input->size; if (!sc_controller_push_msg(kb->controller, &msg)) { - LOGE("Could not send UHID_INPUT message (key)"); + LOGE("Could not push UHID_INPUT message (key)"); } } diff --git a/app/src/uhid/mouse_uhid.c b/app/src/uhid/mouse_uhid.c index c379f3ad..9544ab0d 100644 --- a/app/src/uhid/mouse_uhid.c +++ b/app/src/uhid/mouse_uhid.c @@ -20,7 +20,7 @@ sc_mouse_uhid_send_input(struct sc_mouse_uhid *mouse, msg.uhid_input.size = hid_input->size; if (!sc_controller_push_msg(mouse->controller, &msg)) { - LOGE("Could not send UHID_INPUT message (%s)", name); + LOGE("Could not push UHID_INPUT message (%s)", name); } } @@ -84,7 +84,7 @@ sc_mouse_uhid_init(struct sc_mouse_uhid *mouse, msg.uhid_create.report_desc = hid_open.report_desc; msg.uhid_create.report_desc_size = hid_open.report_desc_size; if (!sc_controller_push_msg(controller, &msg)) { - LOGE("Could not send UHID_CREATE message (mouse)"); + LOGE("Could not push UHID_CREATE message (mouse)"); return false; } diff --git a/app/src/usb/keyboard_aoa.c b/app/src/usb/keyboard_aoa.c index 6c4aaed7..738f6875 100644 --- a/app/src/usb/keyboard_aoa.c +++ b/app/src/usb/keyboard_aoa.c @@ -17,7 +17,7 @@ push_mod_lock_state(struct sc_keyboard_aoa *kb, uint16_t mods_state) { } if (!sc_aoa_push_input(kb->aoa, &hid_input)) { - LOGW("Could not push HID input (mod lock state)"); + LOGW("Could not push AOA HID input (mod lock state)"); return false; } @@ -57,7 +57,7 @@ sc_key_processor_process_key(struct sc_key_processor *kp, if (!sc_aoa_push_input_with_ack_to_wait(kb->aoa, &hid_input, ack_to_wait)) { - LOGW("Could not push HID input (key)"); + LOGW("Could not push AOA HID input (key)"); } } } @@ -71,7 +71,7 @@ sc_keyboard_aoa_init(struct sc_keyboard_aoa *kb, struct sc_aoa *aoa) { bool ok = sc_aoa_push_open(aoa, &hid_open); if (!ok) { - LOGW("Could not push AOA keyboard open request"); + LOGW("Could not push AOA HID open (keyboard)"); return false; } @@ -103,6 +103,6 @@ sc_keyboard_aoa_destroy(struct sc_keyboard_aoa *kb) { bool ok = sc_aoa_push_close(kb->aoa, &hid_close); if (!ok) { - LOGW("Could not push AOA keyboard close request"); + LOGW("Could not push AOA HID close (keyboard)"); } } diff --git a/app/src/usb/mouse_aoa.c b/app/src/usb/mouse_aoa.c index 3c4e3693..b4eb4eb8 100644 --- a/app/src/usb/mouse_aoa.c +++ b/app/src/usb/mouse_aoa.c @@ -18,7 +18,7 @@ sc_mouse_processor_process_mouse_motion(struct sc_mouse_processor *mp, sc_hid_mouse_generate_input_from_motion(&hid_input, event); if (!sc_aoa_push_input(mouse->aoa, &hid_input)) { - LOGW("Could not push HID input (mouse motion)"); + LOGW("Could not push AOA HID input (mouse motion)"); } } @@ -31,7 +31,7 @@ sc_mouse_processor_process_mouse_click(struct sc_mouse_processor *mp, sc_hid_mouse_generate_input_from_click(&hid_input, event); if (!sc_aoa_push_input(mouse->aoa, &hid_input)) { - LOGW("Could not push HID input (mouse click)"); + LOGW("Could not push AOA HID input (mouse click)"); } } @@ -44,7 +44,7 @@ sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp, sc_hid_mouse_generate_input_from_scroll(&hid_input, event); if (!sc_aoa_push_input(mouse->aoa, &hid_input)) { - LOGW("Could not push HID input (mouse scroll)"); + LOGW("Could not push AOA HID input (mouse scroll)"); } } @@ -57,7 +57,7 @@ sc_mouse_aoa_init(struct sc_mouse_aoa *mouse, struct sc_aoa *aoa) { bool ok = sc_aoa_push_open(aoa, &hid_open); if (!ok) { - LOGW("Could not push AOA mouse open request"); + LOGW("Could not push AOA HID open (mouse)"); return false; } @@ -83,6 +83,6 @@ sc_mouse_aoa_destroy(struct sc_mouse_aoa *mouse) { bool ok = sc_aoa_push_close(mouse->aoa, &hid_close); if (!ok) { - LOGW("Could not push AOA mouse close request"); + LOGW("Could not push AOA HID close (mouse)"); } } From 222916eebed01098f3311a1096f4031be2d61197 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:08:08 +0200 Subject: [PATCH 1941/2244] Unregister all AOA devices automatically on exit Pushing a close event from the keyboard_aoa or mouse_aoa implementation was racy, because the AOA thread might be stopped before these events were processed. Instead, keep the list of open AOA devices to close them automatically from the AOA thread before exiting. PR #5270 --- app/src/usb/aoa_hid.c | 43 ++++++++++++++++++++++++++++++++++---- app/src/usb/keyboard_aoa.c | 9 ++------ app/src/usb/mouse_aoa.c | 9 ++------ 3 files changed, 43 insertions(+), 18 deletions(-) diff --git a/app/src/usb/aoa_hid.c b/app/src/usb/aoa_hid.c index ef10e460..59c8304b 100644 --- a/app/src/usb/aoa_hid.c +++ b/app/src/usb/aoa_hid.c @@ -7,6 +7,7 @@ #include "aoa_hid.h" #include "util/log.h" #include "util/str.h" +#include "util/vector.h" // See . #define ACCESSORY_REGISTER_HID 54 @@ -19,6 +20,8 @@ // Drop droppable events above this limit #define SC_AOA_EVENT_QUEUE_LIMIT 60 +struct sc_vec_hid_ids SC_VECTOR(uint16_t); + static void sc_hid_input_log(const struct sc_hid_input *hid_input) { // HID input: [00] FF FF FF FF... @@ -309,7 +312,8 @@ sc_aoa_push_close(struct sc_aoa *aoa, const struct sc_hid_close *hid_close) { } static bool -sc_aoa_process_event(struct sc_aoa *aoa, struct sc_aoa_event *event) { +sc_aoa_process_event(struct sc_aoa *aoa, struct sc_aoa_event *event, + struct sc_vec_hid_ids *vec_open) { switch (event->type) { case SC_AOA_EVENT_TYPE_INPUT: { uint64_t ack_to_wait = event->input.ack_to_wait; @@ -350,7 +354,16 @@ sc_aoa_process_event(struct sc_aoa *aoa, struct sc_aoa_event *event) { bool ok = sc_aoa_setup_hid(aoa, hid_open->hid_id, hid_open->report_desc, hid_open->report_desc_size); - if (!ok) { + if (ok) { + // The device is now open, add it to the list of devices to + // close automatically on exit + bool pushed = sc_vector_push(vec_open, hid_open->hid_id); + if (!pushed) { + LOG_OOM(); + // this is not fatal, the HID device will just not be + // explicitly unregistered + } + } else { LOGW("Could not open AOA device: %" PRIu16, hid_open->hid_id); } @@ -359,7 +372,14 @@ sc_aoa_process_event(struct sc_aoa *aoa, struct sc_aoa_event *event) { case SC_AOA_EVENT_TYPE_CLOSE: { struct sc_hid_close *hid_close = &event->close.hid; bool ok = sc_aoa_unregister_hid(aoa, hid_close->hid_id); - if (!ok) { + if (ok) { + // The device is not open anymore, remove it from the list of + // devices to close automatically on exit + ssize_t idx = sc_vector_index_of(vec_open, hid_close->hid_id); + if (idx >= 0) { + sc_vector_remove(vec_open, idx); + } + } else { LOGW("Could not close AOA device: %" PRIu16, hid_close->hid_id); } @@ -375,6 +395,9 @@ static int run_aoa_thread(void *data) { struct sc_aoa *aoa = data; + // Store the HID ids of opened devices to unregister them all before exiting + struct sc_vec_hid_ids vec_open = SC_VECTOR_INITIALIZER; + for (;;) { sc_mutex_lock(&aoa->mutex); while (!aoa->stopped && sc_vecdeque_is_empty(&aoa->queue)) { @@ -390,12 +413,24 @@ run_aoa_thread(void *data) { struct sc_aoa_event event = sc_vecdeque_pop(&aoa->queue); sc_mutex_unlock(&aoa->mutex); - bool cont = sc_aoa_process_event(aoa, &event); + bool cont = sc_aoa_process_event(aoa, &event, &vec_open); if (!cont) { // stopped break; } } + + // Explicitly unregister all registered HID ids before exiting + for (size_t i = 0; i < vec_open.size; ++i) { + uint16_t hid_id = vec_open.data[i]; + LOGD("Unregistering AOA device %" PRIu16 "...", hid_id); + bool ok = sc_aoa_unregister_hid(aoa, hid_id); + if (!ok) { + LOGW("Could not close AOA device: %" PRIu16, hid_id); + } + } + sc_vector_destroy(&vec_open); + return 0; } diff --git a/app/src/usb/keyboard_aoa.c b/app/src/usb/keyboard_aoa.c index 738f6875..b7834b0f 100644 --- a/app/src/usb/keyboard_aoa.c +++ b/app/src/usb/keyboard_aoa.c @@ -98,11 +98,6 @@ sc_keyboard_aoa_init(struct sc_keyboard_aoa *kb, struct sc_aoa *aoa) { void sc_keyboard_aoa_destroy(struct sc_keyboard_aoa *kb) { - struct sc_hid_close hid_close; - sc_hid_keyboard_generate_close(&hid_close); - - bool ok = sc_aoa_push_close(kb->aoa, &hid_close); - if (!ok) { - LOGW("Could not push AOA HID close (keyboard)"); - } + (void) kb; + // Do nothing, kb->aoa will automatically unregister all devices } diff --git a/app/src/usb/mouse_aoa.c b/app/src/usb/mouse_aoa.c index b4eb4eb8..33b777c4 100644 --- a/app/src/usb/mouse_aoa.c +++ b/app/src/usb/mouse_aoa.c @@ -78,11 +78,6 @@ sc_mouse_aoa_init(struct sc_mouse_aoa *mouse, struct sc_aoa *aoa) { void sc_mouse_aoa_destroy(struct sc_mouse_aoa *mouse) { - struct sc_hid_close hid_close; - sc_hid_mouse_generate_close(&hid_close); - - bool ok = sc_aoa_push_close(mouse->aoa, &hid_close); - if (!ok) { - LOGW("Could not push AOA HID close (mouse)"); - } + (void) mouse; + // Do nothing, mouse->aoa will automatically unregister all devices } From 1f5be743b474164f1efa691ccbc88435058d9ea7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:08:08 +0200 Subject: [PATCH 1942/2244] Make AOA keyboard/mouse open error fatal Now that the AOA open/close are asynchronous, an open error did not make scrcpy exit anymore. Add a mechanism to exit if the AOA device could not be opened asynchronously. PR #5270 --- app/src/events.h | 1 + app/src/scrcpy.c | 3 +++ app/src/usb/aoa_hid.c | 9 ++++++++- app/src/usb/aoa_hid.h | 4 +++- app/src/usb/keyboard_aoa.c | 2 +- app/src/usb/mouse_aoa.c | 2 +- app/src/usb/scrcpy_otg.c | 3 +++ 7 files changed, 20 insertions(+), 4 deletions(-) diff --git a/app/src/events.h b/app/src/events.h index 3f15087a..59c55de4 100644 --- a/app/src/events.h +++ b/app/src/events.h @@ -19,6 +19,7 @@ enum { SC_EVENT_SCREEN_INIT_SIZE, SC_EVENT_TIME_LIMIT_REACHED, SC_EVENT_CONTROLLER_ERROR, + SC_EVENT_AOA_OPEN_ERROR, }; bool diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 529a3fc2..8e8fe86e 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -168,6 +168,9 @@ event_loop(struct scrcpy *s) { case SC_EVENT_RECORDER_ERROR: LOGE("Recorder error"); return SCRCPY_EXIT_FAILURE; + case SC_EVENT_AOA_OPEN_ERROR: + LOGE("AOA open error"); + return SCRCPY_EXIT_FAILURE; case SC_EVENT_TIME_LIMIT_REACHED: LOGI("Time limit reached"); return SCRCPY_EXIT_SUCCESS; diff --git a/app/src/usb/aoa_hid.c b/app/src/usb/aoa_hid.c index 59c8304b..236a78ed 100644 --- a/app/src/usb/aoa_hid.c +++ b/app/src/usb/aoa_hid.c @@ -5,6 +5,7 @@ #include #include "aoa_hid.h" +#include "events.h" #include "util/log.h" #include "util/str.h" #include "util/vector.h" @@ -252,7 +253,8 @@ sc_aoa_push_input_with_ack_to_wait(struct sc_aoa *aoa, } bool -sc_aoa_push_open(struct sc_aoa *aoa, const struct sc_hid_open *hid_open) { +sc_aoa_push_open(struct sc_aoa *aoa, const struct sc_hid_open *hid_open, + bool exit_on_open_error) { if (sc_get_log_level() <= SC_LOG_LEVEL_VERBOSE) { sc_hid_open_log(hid_open); } @@ -271,6 +273,7 @@ sc_aoa_push_open(struct sc_aoa *aoa, const struct sc_hid_open *hid_open) { aoa_event->type = SC_AOA_EVENT_TYPE_OPEN; aoa_event->open.hid = *hid_open; + aoa_event->open.exit_on_error = exit_on_open_error; if (was_empty) { sc_cond_signal(&aoa->event_cond); @@ -365,6 +368,10 @@ sc_aoa_process_event(struct sc_aoa *aoa, struct sc_aoa_event *event, } } else { LOGW("Could not open AOA device: %" PRIu16, hid_open->hid_id); + if (event->open.exit_on_error) { + // Notify the error to the main thread, which will exit + sc_push_event(SC_EVENT_AOA_OPEN_ERROR); + } } break; diff --git a/app/src/usb/aoa_hid.h b/app/src/usb/aoa_hid.h index 010b3742..00961c28 100644 --- a/app/src/usb/aoa_hid.h +++ b/app/src/usb/aoa_hid.h @@ -24,6 +24,7 @@ struct sc_aoa_event { union { struct { struct sc_hid_open hid; + bool exit_on_error; } open; struct { struct sc_hid_close hid; @@ -73,7 +74,8 @@ sc_aoa_join(struct sc_aoa *aoa); // report_desc must be a pointer to static memory, accessed at any time from // another thread bool -sc_aoa_push_open(struct sc_aoa *aoa, const struct sc_hid_open *hid_open); +sc_aoa_push_open(struct sc_aoa *aoa, const struct sc_hid_open *hid_open, + bool exit_on_open_error); bool sc_aoa_push_close(struct sc_aoa *aoa, const struct sc_hid_close *hid_close); diff --git a/app/src/usb/keyboard_aoa.c b/app/src/usb/keyboard_aoa.c index b7834b0f..8f5cb755 100644 --- a/app/src/usb/keyboard_aoa.c +++ b/app/src/usb/keyboard_aoa.c @@ -69,7 +69,7 @@ sc_keyboard_aoa_init(struct sc_keyboard_aoa *kb, struct sc_aoa *aoa) { struct sc_hid_open hid_open; sc_hid_keyboard_generate_open(&hid_open); - bool ok = sc_aoa_push_open(aoa, &hid_open); + bool ok = sc_aoa_push_open(aoa, &hid_open, true); if (!ok) { LOGW("Could not push AOA HID open (keyboard)"); return false; diff --git a/app/src/usb/mouse_aoa.c b/app/src/usb/mouse_aoa.c index 33b777c4..cb566cc0 100644 --- a/app/src/usb/mouse_aoa.c +++ b/app/src/usb/mouse_aoa.c @@ -55,7 +55,7 @@ sc_mouse_aoa_init(struct sc_mouse_aoa *mouse, struct sc_aoa *aoa) { struct sc_hid_open hid_open; sc_hid_mouse_generate_open(&hid_open); - bool ok = sc_aoa_push_open(aoa, &hid_open); + bool ok = sc_aoa_push_open(aoa, &hid_open, true); if (!ok) { LOGW("Could not push AOA HID open (mouse)"); return false; diff --git a/app/src/usb/scrcpy_otg.c b/app/src/usb/scrcpy_otg.c index 715f690a..71d1863f 100644 --- a/app/src/usb/scrcpy_otg.c +++ b/app/src/usb/scrcpy_otg.c @@ -32,6 +32,9 @@ event_loop(struct scrcpy_otg *s) { case SC_EVENT_USB_DEVICE_DISCONNECTED: LOGW("Device disconnected"); return SCRCPY_EXIT_DISCONNECTED; + case SC_EVENT_AOA_OPEN_ERROR: + LOGE("AOA open error"); + return SCRCPY_EXIT_FAILURE; case SDL_QUIT: LOGD("User requested to quit"); return SCRCPY_EXIT_SUCCESS; From de8455400ce7e5ccef1b78e7a30407a90fcc2872 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:08:08 +0200 Subject: [PATCH 1943/2244] Fix HID comments Fix typo and reference the latest version of "HID Usage Tables" specifications. PR #5270 --- app/src/hid/hid_keyboard.c | 9 ++++++--- app/src/hid/hid_mouse.c | 12 ++++++------ 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/app/src/hid/hid_keyboard.c b/app/src/hid/hid_keyboard.c index 64dffe80..961ad790 100644 --- a/app/src/hid/hid_keyboard.c +++ b/app/src/hid/hid_keyboard.c @@ -31,13 +31,16 @@ * For HID, only report descriptor is needed. * * The specification is available here: - * + * * * In particular, read: - * - 6.2.2 Report Descriptor + * - §6.2.2 Report Descriptor * - Appendix B.1 Protocol 1 (Keyboard) * - Appendix C: Keyboard Implementation * + * The HID Usage Tables is also useful: + * + * * Normally a basic HID keyboard uses 8 bytes: * Modifier Reserved Key Key Key Key Key Key * @@ -60,7 +63,7 @@ static const uint8_t SC_HID_KEYBOARD_REPORT_DESC[] = { 0x05, 0x07, // Usage Minimum (224) 0x19, 0xE0, - // Usage Maximum (231) + // Usage Maximum (231) 0x29, 0xE7, // Logical Minimum (0) 0x15, 0x00, diff --git a/app/src/hid/hid_mouse.c b/app/src/hid/hid_mouse.c index d1aae83a..7acc413b 100644 --- a/app/src/hid/hid_mouse.c +++ b/app/src/hid/hid_mouse.c @@ -6,13 +6,13 @@ /** * Mouse descriptor from the specification: - * + * * * Appendix E (p71): §E.10 Report Descriptor (Mouse) * * The usage tags (like Wheel) are listed in "HID Usage Tables": - * - * §4 Generic Desktop Page (0x01) (p26) + * + * §4 Generic Desktop Page (0x01) (p32) */ static const uint8_t SC_HID_MOUSE_REPORT_DESC[] = { // Usage Page (Generic Desktop) @@ -34,7 +34,7 @@ static const uint8_t SC_HID_MOUSE_REPORT_DESC[] = { // Usage Minimum (1) 0x19, 0x01, - // Usage Maximum (5) + // Usage Maximum (5) 0x29, 0x05, // Logical Minimum (0) 0x15, 0x00, @@ -62,9 +62,9 @@ static const uint8_t SC_HID_MOUSE_REPORT_DESC[] = { 0x09, 0x31, // Usage (Wheel) 0x09, 0x38, - // Local Minimum (-127) + // Logical Minimum (-127) 0x15, 0x81, - // Local Maximum (127) + // Logical Maximum (127) 0x25, 0x7F, // Report Size (8) 0x75, 0x08, From c8479fe8bf71f6f46bde95f2e7034cca97f7ff1f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 13 Sep 2024 22:00:24 +0200 Subject: [PATCH 1944/2244] Discard unknown SDL events Mouse and keyboard events with unknown button/keycode/scancode cannot be handled properly. Discard them without forwarding them to the keyboard or mouse processors. This can happen for example if a more recent version of SDL introduces new enum values. PR #5270 --- app/src/input_manager.c | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index d3c94d03..00f06777 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -400,7 +400,7 @@ sc_input_manager_process_key(struct sc_input_manager *im, bool paused = im->screen->paused; bool video = im->screen->video; - SDL_Keycode keycode = event->keysym.sym; + SDL_Keycode sdl_keycode = event->keysym.sym; uint16_t mod = event->keysym.mod; bool down = event->type == SDL_KEYDOWN; bool ctrl = event->keysym.mod & KMOD_CTRL; @@ -412,21 +412,21 @@ sc_input_manager_process_key(struct sc_input_manager *im, // The second condition is necessary to ignore the release of the modifier // key (because in this case mod is 0). bool is_shortcut = is_shortcut_mod(im, mod) - || is_shortcut_key(im, keycode); + || is_shortcut_key(im, sdl_keycode); if (down && !repeat) { - if (keycode == im->last_keycode && mod == im->last_mod) { + if (sdl_keycode == im->last_keycode && mod == im->last_mod) { ++im->key_repeat; } else { im->key_repeat = 0; - im->last_keycode = keycode; + im->last_keycode = sdl_keycode; im->last_mod = mod; } } if (is_shortcut) { enum sc_action action = down ? SC_ACTION_DOWN : SC_ACTION_UP; - switch (keycode) { + switch (sdl_keycode) { case SDLK_h: if (im->kp && !shift && !repeat && !paused) { action_home(im, action); @@ -585,7 +585,7 @@ sc_input_manager_process_key(struct sc_input_manager *im, } uint64_t ack_to_wait = SC_SEQUENCE_INVALID; - bool is_ctrl_v = ctrl && !shift && keycode == SDLK_v && down && !repeat; + bool is_ctrl_v = ctrl && !shift && sdl_keycode == SDLK_v && down && !repeat; if (im->clipboard_autosync && is_ctrl_v) { if (im->legacy_paste) { // inject the text as input events @@ -613,10 +613,20 @@ sc_input_manager_process_key(struct sc_input_manager *im, } } + enum sc_keycode keycode = sc_keycode_from_sdl(sdl_keycode); + if (keycode == SC_KEYCODE_UNKNOWN) { + return; + } + + enum sc_scancode scancode = sc_scancode_from_sdl(event->keysym.scancode); + if (scancode == SC_SCANCODE_UNKNOWN) { + return; + } + struct sc_key_event evt = { .action = sc_action_from_sdl_keyboard_type(event->type), - .keycode = sc_keycode_from_sdl(event->keysym.sym), - .scancode = sc_scancode_from_sdl(event->keysym.scancode), + .keycode = keycode, + .scancode = scancode, .repeat = event->repeat, .mods_state = sc_mods_state_from_sdl(event->keysym.mod), }; @@ -739,6 +749,10 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im, bool down = event->type == SDL_MOUSEBUTTONDOWN; enum sc_mouse_button button = sc_mouse_button_from_sdl(event->button); + if (button == SC_MOUSE_BUTTON_UNKNOWN) { + return; + } + if (!down) { // Mark the button as released im->mouse_buttons_state &= ~button; @@ -827,7 +841,7 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im, struct sc_mouse_click_event evt = { .position = sc_input_manager_get_position(im, event->x, event->y), .action = sc_action_from_sdl_mousebutton_type(event->type), - .button = sc_mouse_button_from_sdl(event->button), + .button = button, .pointer_id = use_finger ? SC_POINTER_ID_GENERIC_FINGER : SC_POINTER_ID_MOUSE, .buttons_state = im->mouse_buttons_state, From 4565f36ee6993eff3b0376ad2da74a71060bc5b0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:08:08 +0200 Subject: [PATCH 1945/2244] Handle SDL gamepad events Introduce a gamepad processor trait, similar to the keyboard processor and mouse processor traits. Handle gamepad events received from SDL, convert them to scrcpy-specific gamepad events, and forward them to the gamepad processor. Further commits will provide AOA and UHID implementations of the gamepad processor trait. PR #5270 Co-authored-by: Luiz Henrique Laurini --- app/src/input_events.h | 98 +++++++++++++++++++++++++++++++ app/src/input_manager.c | 97 +++++++++++++++++++++++++++++- app/src/input_manager.h | 3 + app/src/scrcpy.c | 6 ++ app/src/screen.c | 1 + app/src/screen.h | 1 + app/src/trait/gamepad_processor.h | 50 ++++++++++++++++ 7 files changed, 255 insertions(+), 1 deletion(-) create mode 100644 app/src/trait/gamepad_processor.h diff --git a/app/src/input_events.h b/app/src/input_events.h index bbf4372f..c8966a35 100644 --- a/app/src/input_events.h +++ b/app/src/input_events.h @@ -323,6 +323,38 @@ enum sc_mouse_button { SC_MOUSE_BUTTON_X2 = SDL_BUTTON(SDL_BUTTON_X2), }; +// Use the naming from SDL3 for gamepad axis and buttons: +// + +enum sc_gamepad_axis { + SC_GAMEPAD_AXIS_UNKNOWN = -1, + SC_GAMEPAD_AXIS_LEFTX = SDL_CONTROLLER_AXIS_LEFTX, + SC_GAMEPAD_AXIS_LEFTY = SDL_CONTROLLER_AXIS_LEFTY, + SC_GAMEPAD_AXIS_RIGHTX = SDL_CONTROLLER_AXIS_RIGHTX, + SC_GAMEPAD_AXIS_RIGHTY = SDL_CONTROLLER_AXIS_RIGHTY, + SC_GAMEPAD_AXIS_LEFT_TRIGGER = SDL_CONTROLLER_AXIS_TRIGGERLEFT, + SC_GAMEPAD_AXIS_RIGHT_TRIGGER = SDL_CONTROLLER_AXIS_TRIGGERRIGHT, +}; + +enum sc_gamepad_button { + SC_GAMEPAD_BUTTON_UNKNOWN = -1, + SC_GAMEPAD_BUTTON_SOUTH = SDL_CONTROLLER_BUTTON_A, + SC_GAMEPAD_BUTTON_EAST = SDL_CONTROLLER_BUTTON_B, + SC_GAMEPAD_BUTTON_WEST = SDL_CONTROLLER_BUTTON_X, + SC_GAMEPAD_BUTTON_NORTH = SDL_CONTROLLER_BUTTON_Y, + SC_GAMEPAD_BUTTON_BACK = SDL_CONTROLLER_BUTTON_BACK, + SC_GAMEPAD_BUTTON_GUIDE = SDL_CONTROLLER_BUTTON_GUIDE, + SC_GAMEPAD_BUTTON_START = SDL_CONTROLLER_BUTTON_START, + SC_GAMEPAD_BUTTON_LEFT_STICK = SDL_CONTROLLER_BUTTON_LEFTSTICK, + SC_GAMEPAD_BUTTON_RIGHT_STICK = SDL_CONTROLLER_BUTTON_RIGHTSTICK, + SC_GAMEPAD_BUTTON_LEFT_SHOULDER = SDL_CONTROLLER_BUTTON_LEFTSHOULDER, + SC_GAMEPAD_BUTTON_RIGHT_SHOULDER = SDL_CONTROLLER_BUTTON_RIGHTSHOULDER, + SC_GAMEPAD_BUTTON_DPAD_UP = SDL_CONTROLLER_BUTTON_DPAD_UP, + SC_GAMEPAD_BUTTON_DPAD_DOWN = SDL_CONTROLLER_BUTTON_DPAD_DOWN, + SC_GAMEPAD_BUTTON_DPAD_LEFT = SDL_CONTROLLER_BUTTON_DPAD_LEFT, + SC_GAMEPAD_BUTTON_DPAD_RIGHT = SDL_CONTROLLER_BUTTON_DPAD_RIGHT, +}; + static_assert(sizeof(enum sc_mod) >= sizeof(SDL_Keymod), "SDL_Keymod must be convertible to sc_mod"); @@ -380,6 +412,33 @@ struct sc_touch_event { float pressure; }; +enum sc_gamepad_device_event_type { + SC_GAMEPAD_DEVICE_ADDED, + SC_GAMEPAD_DEVICE_REMOVED, +}; + +// As documented in : +// The ID value starts at 0 and increments from there. The value -1 is an +// invalid ID. +#define SC_GAMEPAD_ID_INVALID UINT32_C(-1) + +struct sc_gamepad_device_event { + enum sc_gamepad_device_event_type type; + uint32_t gamepad_id; +}; + +struct sc_gamepad_button_event { + uint32_t gamepad_id; + enum sc_action action; + enum sc_gamepad_button button; +}; + +struct sc_gamepad_axis_event { + uint32_t gamepad_id; + enum sc_gamepad_axis axis; + int16_t value; +}; + static inline uint16_t sc_mods_state_from_sdl(uint16_t mods_state) { return mods_state; @@ -444,4 +503,43 @@ sc_mouse_buttons_state_from_sdl(uint32_t buttons_state) { return buttons_state; } +static inline enum sc_gamepad_device_event_type +sc_gamepad_device_event_type_from_sdl_type(uint32_t type) { + assert(type == SDL_CONTROLLERDEVICEADDED + || type == SDL_CONTROLLERDEVICEREMOVED); + if (type == SDL_CONTROLLERDEVICEADDED) { + return SC_GAMEPAD_DEVICE_ADDED; + } + return SC_GAMEPAD_DEVICE_REMOVED; +} + +static inline enum sc_gamepad_axis +sc_gamepad_axis_from_sdl(uint8_t axis) { + if (axis <= SDL_CONTROLLER_AXIS_TRIGGERRIGHT) { + // SC_GAMEPAD_AXIS_* constants are initialized from + // SDL_CONTROLLER_AXIS_* + return axis; + } + return SC_GAMEPAD_AXIS_UNKNOWN; +} + +static inline enum sc_gamepad_button +sc_gamepad_button_from_sdl(uint8_t button) { + if (button <= SDL_CONTROLLER_BUTTON_DPAD_RIGHT) { + // SC_GAMEPAD_BUTTON_* constants are initialized from + // SDL_CONTROLLER_BUTTON_* + return button; + } + return SC_GAMEPAD_BUTTON_UNKNOWN; +} + +static inline enum sc_action +sc_action_from_sdl_controllerbutton_type(uint32_t type) { + assert(type == SDL_CONTROLLERBUTTONDOWN || type == SDL_CONTROLLERBUTTONUP); + if (type == SDL_CONTROLLERBUTTONDOWN) { + return SC_ACTION_DOWN; + } + return SC_ACTION_UP; +} + #endif diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 00f06777..77cb4f1d 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -56,16 +56,18 @@ void sc_input_manager_init(struct sc_input_manager *im, const struct sc_input_manager_params *params) { // A key/mouse processor may not be present if there is no controller - assert((!params->kp && !params->mp) || params->controller); + assert((!params->kp && !params->mp && !params->gp) || params->controller); // A processor must have ops initialized assert(!params->kp || params->kp->ops); assert(!params->mp || params->mp->ops); + assert(!params->gp || params->gp->ops); im->controller = params->controller; im->fp = params->fp; im->screen = params->screen; im->kp = params->kp; im->mp = params->mp; + im->gp = params->gp; im->mouse_bindings = params->mouse_bindings; im->legacy_paste = params->legacy_paste; @@ -920,6 +922,78 @@ sc_input_manager_process_mouse_wheel(struct sc_input_manager *im, im->mp->ops->process_mouse_scroll(im->mp, &evt); } +static void +sc_input_manager_process_gamepad_device(struct sc_input_manager *im, + const SDL_ControllerDeviceEvent *event) { + SDL_JoystickID id; + if (event->type == SDL_CONTROLLERDEVICEADDED) { + SDL_GameController *gc = SDL_GameControllerOpen(event->which); + if (!gc) { + LOGW("Could not open game controller"); + return; + } + + SDL_Joystick *joystick = SDL_GameControllerGetJoystick(gc); + if (!joystick) { + LOGW("Could not get controller joystick"); + SDL_GameControllerClose(gc); + return; + } + + id = SDL_JoystickInstanceID(joystick); + } else if (event->type == SDL_CONTROLLERDEVICEREMOVED) { + id = event->which; + + SDL_GameController *gc = SDL_GameControllerFromInstanceID(id); + if (gc) { + SDL_GameControllerClose(gc); + } else { + LOGW("Unknown gamepad device removed"); + } + } else { + // Nothing to do + return; + } + + struct sc_gamepad_device_event evt = { + .type = sc_gamepad_device_event_type_from_sdl_type(event->type), + .gamepad_id = id, + }; + im->gp->ops->process_gamepad_device(im->gp, &evt); +} + +static void +sc_input_manager_process_gamepad_axis(struct sc_input_manager *im, + const SDL_ControllerAxisEvent *event) { + enum sc_gamepad_axis axis = sc_gamepad_axis_from_sdl(event->axis); + if (axis == SC_GAMEPAD_AXIS_UNKNOWN) { + return; + } + + struct sc_gamepad_axis_event evt = { + .gamepad_id = event->which, + .axis = axis, + .value = event->value, + }; + im->gp->ops->process_gamepad_axis(im->gp, &evt); +} + +static void +sc_input_manager_process_gamepad_button(struct sc_input_manager *im, + const SDL_ControllerButtonEvent *event) { + enum sc_gamepad_button button = sc_gamepad_button_from_sdl(event->button); + if (button == SC_GAMEPAD_BUTTON_UNKNOWN) { + return; + } + + struct sc_gamepad_button_event evt = { + .gamepad_id = event->which, + .action = sc_action_from_sdl_controllerbutton_type(event->type), + .button = button, + }; + im->gp->ops->process_gamepad_button(im->gp, &evt); +} + static bool is_apk(const char *file) { const char *ext = strrchr(file, '.'); @@ -992,6 +1066,27 @@ sc_input_manager_handle_event(struct sc_input_manager *im, } sc_input_manager_process_touch(im, &event->tfinger); break; + case SDL_CONTROLLERDEVICEADDED: + case SDL_CONTROLLERDEVICEREMOVED: + // Handle device added or removed even if paused + if (!im->gp) { + break; + } + sc_input_manager_process_gamepad_device(im, &event->cdevice); + break; + case SDL_CONTROLLERAXISMOTION: + if (!im->gp || paused) { + break; + } + sc_input_manager_process_gamepad_axis(im, &event->caxis); + break; + case SDL_CONTROLLERBUTTONDOWN: + case SDL_CONTROLLERBUTTONUP: + if (!im->gp || paused) { + break; + } + sc_input_manager_process_gamepad_button(im, &event->cbutton); + break; case SDL_DROPFILE: { if (!control) { break; diff --git a/app/src/input_manager.h b/app/src/input_manager.h index 88558549..8efd0153 100644 --- a/app/src/input_manager.h +++ b/app/src/input_manager.h @@ -11,6 +11,7 @@ #include "file_pusher.h" #include "fps_counter.h" #include "options.h" +#include "trait/gamepad_processor.h" #include "trait/key_processor.h" #include "trait/mouse_processor.h" @@ -21,6 +22,7 @@ struct sc_input_manager { struct sc_key_processor *kp; struct sc_mouse_processor *mp; + struct sc_gamepad_processor *gp; struct sc_mouse_bindings mouse_bindings; bool legacy_paste; @@ -50,6 +52,7 @@ struct sc_input_manager_params { struct sc_screen *screen; struct sc_key_processor *kp; struct sc_mouse_processor *mp; + struct sc_gamepad_processor *gp; struct sc_mouse_bindings mouse_bindings; bool legacy_paste; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 8e8fe86e..24738876 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -485,6 +485,11 @@ scrcpy(struct scrcpy_options *options) { } } + if (SDL_Init(SDL_INIT_GAMECONTROLLER)) { + LOGE("Could not initialize SDL gamepad: %s", SDL_GetError()); + goto end; + } + sdl_configure(options->video_playback, options->disable_screensaver); // Await for server without blocking Ctrl+C handling @@ -735,6 +740,7 @@ aoa_complete: .fp = fp, .kp = kp, .mp = mp, + .gp = NULL, .mouse_bindings = options->mouse_bindings, .legacy_paste = options->legacy_paste, .clipboard_autosync = options->clipboard_autosync, diff --git a/app/src/screen.c b/app/src/screen.c index 42be554a..cb455cb1 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -477,6 +477,7 @@ sc_screen_init(struct sc_screen *screen, .screen = screen, .kp = params->kp, .mp = params->mp, + .gp = params->gp, .mouse_bindings = params->mouse_bindings, .legacy_paste = params->legacy_paste, .clipboard_autosync = params->clipboard_autosync, diff --git a/app/src/screen.h b/app/src/screen.h index 079d4fbb..7e1f7e6e 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -78,6 +78,7 @@ struct sc_screen_params { struct sc_file_pusher *fp; struct sc_key_processor *kp; struct sc_mouse_processor *mp; + struct sc_gamepad_processor *gp; struct sc_mouse_bindings mouse_bindings; bool legacy_paste; diff --git a/app/src/trait/gamepad_processor.h b/app/src/trait/gamepad_processor.h new file mode 100644 index 00000000..72479783 --- /dev/null +++ b/app/src/trait/gamepad_processor.h @@ -0,0 +1,50 @@ +#ifndef SC_GAMEPAD_PROCESSOR_H +#define SC_GAMEPAD_PROCESSOR_H + +#include "common.h" + +#include +#include + +#include "input_events.h" + +/** + * Gamepad processor trait. + * + * Component able to handle gamepads devices and inject buttons and axis events. + */ +struct sc_gamepad_processor { + const struct sc_gamepad_processor_ops *ops; +}; + +struct sc_gamepad_processor_ops { + + /** + * Process a gamepad device added or removed + * + * This function is mandatory. + */ + void + (*process_gamepad_device)(struct sc_gamepad_processor *gp, + const struct sc_gamepad_device_event *event); + + /** + * Process a gamepad axis event + * + * This function is mandatory. + */ + void + (*process_gamepad_axis)(struct sc_gamepad_processor *gp, + const struct sc_gamepad_axis_event *event); + + /** + * Process a gamepad button event + * + * This function is mandatory. + */ + void + (*process_gamepad_button)(struct sc_gamepad_processor *gp, + const struct sc_gamepad_button_event *event); +}; + +#endif From f4d1e49ad91586ea3590eefc992bdfed558f7e06 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:08:08 +0200 Subject: [PATCH 1946/2244] Add util functions to write in little-endian This will be helpful for writing HID values. PR #5270 --- app/src/util/binary.h | 20 ++++++++++++++++++++ app/tests/test_binary.c | 42 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/app/src/util/binary.h b/app/src/util/binary.h index 6dc1b58e..7de9b505 100644 --- a/app/src/util/binary.h +++ b/app/src/util/binary.h @@ -13,6 +13,12 @@ sc_write16be(uint8_t *buf, uint16_t value) { buf[1] = value; } +static inline void +sc_write16le(uint8_t *buf, uint16_t value) { + buf[0] = value; + buf[1] = value >> 8; +} + static inline void sc_write32be(uint8_t *buf, uint32_t value) { buf[0] = value >> 24; @@ -21,12 +27,26 @@ sc_write32be(uint8_t *buf, uint32_t value) { buf[3] = value; } +static inline void +sc_write32le(uint8_t *buf, uint32_t value) { + buf[0] = value; + buf[1] = value >> 8; + buf[2] = value >> 16; + buf[3] = value >> 24; +} + static inline void sc_write64be(uint8_t *buf, uint64_t value) { sc_write32be(buf, value >> 32); sc_write32be(&buf[4], (uint32_t) value); } +static inline void +sc_write64le(uint8_t *buf, uint64_t value) { + sc_write32le(buf, (uint32_t) value); + sc_write32le(&buf[4], value >> 32); +} + static inline uint16_t sc_read16be(const uint8_t *buf) { return (buf[0] << 8) | buf[1]; diff --git a/app/tests/test_binary.c b/app/tests/test_binary.c index 82a9c1e0..bce74ce2 100644 --- a/app/tests/test_binary.c +++ b/app/tests/test_binary.c @@ -42,6 +42,44 @@ static void test_write64be(void) { assert(buf[7] == 0xEF); } +static void test_write16le(void) { + uint16_t val = 0xABCD; + uint8_t buf[2]; + + sc_write16le(buf, val); + + assert(buf[0] == 0xCD); + assert(buf[1] == 0xAB); +} + +static void test_write32le(void) { + uint32_t val = 0xABCD1234; + uint8_t buf[4]; + + sc_write32le(buf, val); + + assert(buf[0] == 0x34); + assert(buf[1] == 0x12); + assert(buf[2] == 0xCD); + assert(buf[3] == 0xAB); +} + +static void test_write64le(void) { + uint64_t val = 0xABCD1234567890EF; + uint8_t buf[8]; + + sc_write64le(buf, val); + + assert(buf[0] == 0xEF); + assert(buf[1] == 0x90); + assert(buf[2] == 0x78); + assert(buf[3] == 0x56); + assert(buf[4] == 0x34); + assert(buf[5] == 0x12); + assert(buf[6] == 0xCD); + assert(buf[7] == 0xAB); +} + static void test_read16be(void) { uint8_t buf[2] = {0xAB, 0xCD}; @@ -108,6 +146,10 @@ int main(int argc, char *argv[]) { test_read32be(); test_read64be(); + test_write16le(); + test_write32le(); + test_write64le(); + test_float_to_u16fp(); test_float_to_i16fp(); return 0; From a59c6df4b7659adabd5cd98a95f1dbf330a50c14 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:08:08 +0200 Subject: [PATCH 1947/2244] Implement HID gamepad Implement the HID protocol for gamepads, that will be used in further commits by the AOA and UHID gamepad processor implementations. PR #5270 --- app/meson.build | 1 + app/src/hid/hid_event.h | 2 +- app/src/hid/hid_gamepad.c | 451 ++++++++++++++++++++++++++++++++++++++ app/src/hid/hid_gamepad.h | 53 +++++ 4 files changed, 506 insertions(+), 1 deletion(-) create mode 100644 app/src/hid/hid_gamepad.c create mode 100644 app/src/hid/hid_gamepad.h diff --git a/app/meson.build b/app/meson.build index fc6b85e2..a4880420 100644 --- a/app/meson.build +++ b/app/meson.build @@ -32,6 +32,7 @@ src = [ 'src/screen.c', 'src/server.c', 'src/version.c', + 'src/hid/hid_gamepad.c', 'src/hid/hid_keyboard.c', 'src/hid/hid_mouse.c', 'src/trait/frame_source.c', diff --git a/app/src/hid/hid_event.h b/app/src/hid/hid_event.h index 9171004e..d6818e30 100644 --- a/app/src/hid/hid_event.h +++ b/app/src/hid/hid_event.h @@ -5,7 +5,7 @@ #include -#define SC_HID_MAX_SIZE 8 +#define SC_HID_MAX_SIZE 15 struct sc_hid_input { uint16_t hid_id; diff --git a/app/src/hid/hid_gamepad.c b/app/src/hid/hid_gamepad.c new file mode 100644 index 00000000..cd009d15 --- /dev/null +++ b/app/src/hid/hid_gamepad.c @@ -0,0 +1,451 @@ +#include "hid_gamepad.h" + +#include +#include + +#include "util/binary.h" +#include "util/log.h" + +// 2x2 bytes for left stick (X, Y) +// 2x2 bytes for right stick (Z, Rz) +// 2x2 bytes for L2/R2 triggers +// 2 bytes for buttons + padding, +// 1 byte for hat switch (dpad) + padding +#define SC_HID_GAMEPAD_EVENT_SIZE 15 + +// The ->buttons field stores the state for all buttons, but only some of them +// (the 16 LSB) must be transmitted "as is". The DPAD (hat switch) buttons are +// stored locally in the MSB of this field, but not transmitted as is: they are +// transformed to generate another specific byte. +#define SC_HID_BUTTONS_MASK 0xFFFF + +// outside SC_HID_BUTTONS_MASK +#define SC_GAMEPAD_BUTTONS_BIT_DPAD_UP UINT32_C(0x10000) +#define SC_GAMEPAD_BUTTONS_BIT_DPAD_DOWN UINT32_C(0x20000) +#define SC_GAMEPAD_BUTTONS_BIT_DPAD_LEFT UINT32_C(0x40000) +#define SC_GAMEPAD_BUTTONS_BIT_DPAD_RIGHT UINT32_C(0x80000) + +/** + * Gamepad descriptor manually crafted to transmit the input reports. + * + * The HID specification is available here: + * + * + * The HID Usage Tables is also useful: + * + */ +static const uint8_t SC_HID_GAMEPAD_REPORT_DESC[] = { + // Usage Page (Generic Desktop) + 0x05, 0x01, + // Usage (Gamepad) + 0x09, 0x05, + + // Collection (Application) + 0xA1, 0x01, + + // Collection (Physical) + 0xA1, 0x00, + + // Usage Page (Generic Desktop) + 0x05, 0x01, + // Usage (X) Left stick x + 0x09, 0x30, + // Usage (Y) Left stick y + 0x09, 0x31, + // Usage (Z) Right stick x + 0x09, 0x32, + // Usage (Rz) Right stick y + 0x09, 0x35, + // Logical Minimum (0) + 0x15, 0x00, + // Logical Maximum (65535) + // Cannot use 26 FF FF because 0xFFFF is interpreted as signed 16-bit + 0x27, 0xFF, 0xFF, 0x00, 0x00, // little-endian + // Report Size (16) + 0x75, 0x10, + // Report Count (4) + 0x95, 0x04, + // Input (Data, Variable, Absolute): 4 bytes (X, Y, Z, Rz) + 0x81, 0x02, + + // Usage Page (Simulation Controls) + 0x05, 0x02, + // Usage (Brake) + 0x09, 0xC5, + // Usage (Accelerator) + 0x09, 0xC4, + // Logical Minimum (0) + 0x15, 0x00, + // Logical Maximum (32767) + 0x26, 0xFF, 0x7F, + // Report Size (16) + 0x75, 0x10, + // Report Count (2) + 0x95, 0x02, + // Input (Data, Variable, Absolute): 2 bytes (L2, R2) + 0x81, 0x02, + + // Usage Page (Buttons) + 0x05, 0x09, + // Usage Minimum (1) + 0x19, 0x01, + // Usage Maximum (16) + 0x29, 0x10, + // Logical Minimum (0) + 0x15, 0x00, + // Logical Maximum (1) + 0x25, 0x01, + // Report Count (16) + 0x95, 0x10, + // Report Size (1) + 0x75, 0x01, + // Input (Data, Variable, Absolute): 16 buttons bits + 0x81, 0x02, + + // Usage Page (Generic Desktop) + 0x05, 0x01, + // Usage (Hat switch) + 0x09, 0x39, + // Logical Minimum (1) + 0x15, 0x01, + // Logical Maximum (8) + 0x25, 0x08, + // Report Size (4) + 0x75, 0x04, + // Report Count (1) + 0x95, 0x01, + // Input (Data, Variable, Null State): 4-bit value + 0x81, 0x42, + + // End Collection + 0xC0, + + // End Collection + 0xC0, +}; + +/** + * A gamepad HID input report is 15 bytes long: + * - bytes 0-3: left stick state + * - bytes 4-7: right stick state + * - bytes 8-11: L2/R2 triggers state + * - bytes 12-13: buttons state + * - bytes 14: hat switch position (dpad) + * + * +---------------+ + * byte 0: |. . . . . . . .| + * | | left stick x (0-65535, little-endian) + * byte 1: |. . . . . . . .| + * +---------------+ + * byte 2: |. . . . . . . .| + * | | left stick y (0-65535, little-endian) + * byte 3: |. . . . . . . .| + * +---------------+ + * byte 4: |. . . . . . . .| + * | | right stick x (0-65535, little-endian) + * byte 5: |. . . . . . . .| + * +---------------+ + * byte 6: |. . . . . . . .| + * | | right stick y (0-65535, little-endian) + * byte 7: |. . . . . . . .| + * +---------------+ + * byte 8: |. . . . . . . .| + * | | L2 trigger (0-32767, little-endian) + * byte 9: |0 . . . . . . .| + * +---------------+ + * byte 10: |. . . . . . . .| + * | | R2 trigger (0-32767, little-endian) + * byte 11: |0 . . . . . . .| + * +---------------+ + * + * ,--------------- SC_GAMEPAD_BUTTON_RIGHT_SHOULDER + * | ,------------- SC_GAMEPAD_BUTTON_LEFT_SHOULDER + * | | + * | | ,--------- SC_GAMEPAD_BUTTON_NORTH + * | | | ,------- SC_GAMEPAD_BUTTON_WEST + * | | | | + * | | | | ,--- SC_GAMEPAD_BUTTON_EAST + * | | | | | ,- SC_GAMEPAD_BUTTON_SOUTH + * v v v v v v + * +---------------+ + * byte 12: |. . 0 . . 0 . .| + * | | Buttons (16-bit little-endian) + * byte 13: |0 . . . . . 0 0| + * +---------------+ + * ^ ^ ^ ^ ^ + * | | | | | + * | | | | | + * | | | | `----- SC_GAMEPAD_BUTTON_BACK + * | | | `------- SC_GAMEPAD_BUTTON_START + * | | `--------- SC_GAMEPAD_BUTTON_GUIDE + * | `----------- SC_GAMEPAD_BUTTON_LEFT_STICK + * `------------- SC_GAMEPAD_BUTTON_RIGHT_STICK + * + * +---------------+ + * byte 14: |0 0 0 . . . . .| hat switch (dpad) position (0-8) + * +---------------+ + * 9 possible positions and their values: + * 8 1 2 + * 7 0 3 + * 6 5 4 + * (8 is top-left, 1 is top, 2 is top-right, etc.) + */ + +static void +sc_hid_gamepad_slot_init(struct sc_hid_gamepad_slot *slot, + uint32_t gamepad_id) { + assert(gamepad_id != SC_GAMEPAD_ID_INVALID); + slot->gamepad_id = gamepad_id; + slot->buttons = 0; + slot->axis_left_x = 0; + slot->axis_left_y = 0; + slot->axis_right_x = 0; + slot->axis_right_y = 0; + slot->axis_left_trigger = 0; + slot->axis_right_trigger = 0; +} + +static ssize_t +sc_hid_gamepad_slot_find(struct sc_hid_gamepad *hid, uint32_t gamepad_id) { + for (size_t i = 0; i < SC_MAX_GAMEPADS; ++i) { + if (gamepad_id == hid->slots[i].gamepad_id) { + // found + return i; + } + } + + return -1; +} + +void +sc_hid_gamepad_init(struct sc_hid_gamepad *hid) { + for (size_t i = 0; i < SC_MAX_GAMEPADS; ++i) { + hid->slots[i].gamepad_id = SC_GAMEPAD_ID_INVALID; + } +} + +static inline uint16_t +sc_hid_gamepad_slot_get_id(size_t slot_idx) { + assert(slot_idx < SC_MAX_GAMEPADS); + return SC_HID_ID_GAMEPAD_FIRST + slot_idx; +} + +bool +sc_hid_gamepad_generate_open(struct sc_hid_gamepad *hid, + struct sc_hid_open *hid_open, + uint32_t gamepad_id) { + assert(gamepad_id != SC_GAMEPAD_ID_INVALID); + ssize_t slot_idx = sc_hid_gamepad_slot_find(hid, SC_GAMEPAD_ID_INVALID); + if (slot_idx == -1) { + LOGW("No gamepad slot available for new gamepad %" PRIu32, gamepad_id); + return false; + } + + sc_hid_gamepad_slot_init(&hid->slots[slot_idx], gamepad_id); + + uint16_t hid_id = sc_hid_gamepad_slot_get_id(slot_idx); + hid_open->hid_id = hid_id; + hid_open->report_desc = SC_HID_GAMEPAD_REPORT_DESC; + hid_open->report_desc_size = sizeof(SC_HID_GAMEPAD_REPORT_DESC); + + return true; +} + +bool +sc_hid_gamepad_generate_close(struct sc_hid_gamepad *hid, + struct sc_hid_close *hid_close, + uint32_t gamepad_id) { + assert(gamepad_id != SC_GAMEPAD_ID_INVALID); + ssize_t slot_idx = sc_hid_gamepad_slot_find(hid, gamepad_id); + if (slot_idx == -1) { + LOGW("Unknown gamepad removed %" PRIu32, gamepad_id); + return false; + } + + hid->slots[slot_idx].gamepad_id = SC_GAMEPAD_ID_INVALID; + + uint16_t hid_id = sc_hid_gamepad_slot_get_id(slot_idx); + hid_close->hid_id = hid_id; + + return true; +} + +static uint8_t +sc_hid_gamepad_get_dpad_value(uint32_t buttons) { + // Value depending on direction: + // 8 1 2 + // 7 0 3 + // 6 5 4 + if (buttons & SC_GAMEPAD_BUTTONS_BIT_DPAD_UP) { + if (buttons & SC_GAMEPAD_BUTTONS_BIT_DPAD_LEFT) { + return 8; + } + if (buttons & SC_GAMEPAD_BUTTONS_BIT_DPAD_RIGHT) { + return 2; + } + return 1; + } + if (buttons & SC_GAMEPAD_BUTTONS_BIT_DPAD_DOWN) { + if (buttons & SC_GAMEPAD_BUTTONS_BIT_DPAD_LEFT) { + return 6; + } + if (buttons & SC_GAMEPAD_BUTTONS_BIT_DPAD_RIGHT) { + return 4; + } + return 5; + } + if (buttons & SC_GAMEPAD_BUTTONS_BIT_DPAD_LEFT) { + return 7; + } + if (buttons & SC_GAMEPAD_BUTTONS_BIT_DPAD_RIGHT) { + return 3; + } + + return 0; +} + +static void +sc_hid_gamepad_event_from_slot(uint16_t hid_id, + const struct sc_hid_gamepad_slot *slot, + struct sc_hid_input *hid_input) { + hid_input->hid_id = hid_id; + hid_input->size = SC_HID_GAMEPAD_EVENT_SIZE; + + uint8_t *data = hid_input->data; + // Values must be written in little-endian + sc_write16le(data, slot->axis_left_x); + sc_write16le(data + 2, slot->axis_left_y); + sc_write16le(data + 4, slot->axis_right_x); + sc_write16le(data + 6, slot->axis_right_y); + sc_write16le(data + 8, slot->axis_left_trigger); + sc_write16le(data + 10, slot->axis_right_trigger); + sc_write16le(data + 12, slot->buttons & SC_HID_BUTTONS_MASK); + data[14] = sc_hid_gamepad_get_dpad_value(slot->buttons); +} + +static uint32_t +sc_hid_gamepad_get_button_id(enum sc_gamepad_button button) { + switch (button) { + case SC_GAMEPAD_BUTTON_SOUTH: + return 0x0001; + case SC_GAMEPAD_BUTTON_EAST: + return 0x0002; + case SC_GAMEPAD_BUTTON_WEST: + return 0x0008; + case SC_GAMEPAD_BUTTON_NORTH: + return 0x0010; + case SC_GAMEPAD_BUTTON_BACK: + return 0x0400; + case SC_GAMEPAD_BUTTON_GUIDE: + return 0x1000; + case SC_GAMEPAD_BUTTON_START: + return 0x0800; + case SC_GAMEPAD_BUTTON_LEFT_STICK: + return 0x2000; + case SC_GAMEPAD_BUTTON_RIGHT_STICK: + return 0x4000; + case SC_GAMEPAD_BUTTON_LEFT_SHOULDER: + return 0x0040; + case SC_GAMEPAD_BUTTON_RIGHT_SHOULDER: + return 0x0080; + case SC_GAMEPAD_BUTTON_DPAD_UP: + return SC_GAMEPAD_BUTTONS_BIT_DPAD_UP; + case SC_GAMEPAD_BUTTON_DPAD_DOWN: + return SC_GAMEPAD_BUTTONS_BIT_DPAD_DOWN; + case SC_GAMEPAD_BUTTON_DPAD_LEFT: + return SC_GAMEPAD_BUTTONS_BIT_DPAD_LEFT; + case SC_GAMEPAD_BUTTON_DPAD_RIGHT: + return SC_GAMEPAD_BUTTONS_BIT_DPAD_RIGHT; + default: + // unknown button, ignore + return 0; + } +} + +bool +sc_hid_gamepad_generate_input_from_button(struct sc_hid_gamepad *hid, + struct sc_hid_input *hid_input, + const struct sc_gamepad_button_event *event) { + if ((event->button < 0) || (event->button > 15)) { + return false; + } + + uint32_t gamepad_id = event->gamepad_id; + + ssize_t slot_idx = sc_hid_gamepad_slot_find(hid, gamepad_id); + if (slot_idx == -1) { + LOGW("Axis event for unknown gamepad %" PRIu32, gamepad_id); + return false; + } + + assert(slot_idx < SC_MAX_GAMEPADS); + + struct sc_hid_gamepad_slot *slot = &hid->slots[slot_idx]; + + uint32_t button = sc_hid_gamepad_get_button_id(event->button); + if (!button) { + // unknown button, ignore + return false; + } + + if (event->action == SC_ACTION_DOWN) { + slot->buttons |= button; + } else { + assert(event->action == SC_ACTION_UP); + slot->buttons &= ~button; + } + + uint16_t hid_id = sc_hid_gamepad_slot_get_id(slot_idx); + sc_hid_gamepad_event_from_slot(hid_id, slot, hid_input); + + return true; +} + +bool +sc_hid_gamepad_generate_input_from_axis(struct sc_hid_gamepad *hid, + struct sc_hid_input *hid_input, + const struct sc_gamepad_axis_event *event) { + uint32_t gamepad_id = event->gamepad_id; + + ssize_t slot_idx = sc_hid_gamepad_slot_find(hid, gamepad_id); + if (slot_idx == -1) { + LOGW("Button event for unknown gamepad %" PRIu32, gamepad_id); + return false; + } + + assert(slot_idx < SC_MAX_GAMEPADS); + + struct sc_hid_gamepad_slot *slot = &hid->slots[slot_idx]; + +// [-32768 to 32767] -> [0 to 65535] +#define AXIS_RESCALE(V) (uint16_t) (((int32_t) V) + 0x8000) + switch (event->axis) { + case SC_GAMEPAD_AXIS_LEFTX: + slot->axis_left_x = AXIS_RESCALE(event->value); + break; + case SC_GAMEPAD_AXIS_LEFTY: + slot->axis_left_y = AXIS_RESCALE(event->value); + break; + case SC_GAMEPAD_AXIS_RIGHTX: + slot->axis_right_x = AXIS_RESCALE(event->value); + break; + case SC_GAMEPAD_AXIS_RIGHTY: + slot->axis_right_y = AXIS_RESCALE(event->value); + break; + case SC_GAMEPAD_AXIS_LEFT_TRIGGER: + // Trigger is always positive between 0 and 32767 + slot->axis_left_trigger = MAX(0, event->value); + break; + case SC_GAMEPAD_AXIS_RIGHT_TRIGGER: + // Trigger is always positive between 0 and 32767 + slot->axis_right_trigger = MAX(0, event->value); + break; + default: + return false; + } + + uint16_t hid_id = sc_hid_gamepad_slot_get_id(slot_idx); + sc_hid_gamepad_event_from_slot(hid_id, slot, hid_input); + + return true; +} diff --git a/app/src/hid/hid_gamepad.h b/app/src/hid/hid_gamepad.h new file mode 100644 index 00000000..b532a703 --- /dev/null +++ b/app/src/hid/hid_gamepad.h @@ -0,0 +1,53 @@ +#ifndef SC_HID_GAMEPAD_H +#define SC_HID_GAMEPAD_H + +#include "common.h" + +#include + +#include "hid/hid_event.h" +#include "input_events.h" + +#define SC_MAX_GAMEPADS 8 +#define SC_HID_ID_GAMEPAD_FIRST 3 +#define SC_HID_ID_GAMEPAD_LAST (SC_HID_ID_GAMEPAD_FIRST + SC_MAX_GAMEPADS - 1) + +struct sc_hid_gamepad_slot { + uint32_t gamepad_id; + uint32_t buttons; + uint16_t axis_left_x; + uint16_t axis_left_y; + uint16_t axis_right_x; + uint16_t axis_right_y; + uint16_t axis_left_trigger; + uint16_t axis_right_trigger; +}; + +struct sc_hid_gamepad { + struct sc_hid_gamepad_slot slots[SC_MAX_GAMEPADS]; +}; + +void +sc_hid_gamepad_init(struct sc_hid_gamepad *hid); + +bool +sc_hid_gamepad_generate_open(struct sc_hid_gamepad *hid, + struct sc_hid_open *hid_open, + uint32_t gamepad_id); + +bool +sc_hid_gamepad_generate_close(struct sc_hid_gamepad *hid, + struct sc_hid_close *hid_close, + uint32_t gamepad_id); + +bool +sc_hid_gamepad_generate_input_from_button(struct sc_hid_gamepad *hid, + struct sc_hid_input *hid_input, + const struct sc_gamepad_button_event *event); + +bool +sc_hid_gamepad_generate_input_from_axis(struct sc_hid_gamepad *hid, + struct sc_hid_input *hid_input, + const struct sc_gamepad_axis_event *event); + +#endif From a34a62ca4b4cc5aa946b2c6c2a53d18815160abd Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:08:08 +0200 Subject: [PATCH 1948/2244] Add AOA gamepad support Similar to AOA keyboard and mouse, but for gamepads. Can be enabled with --gamepad=aoa. PR #5270 --- app/data/bash-completion/scrcpy | 5 ++ app/data/zsh-completion/_scrcpy | 1 + app/meson.build | 1 + app/scrcpy.1 | 16 ++++-- app/src/cli.c | 44 ++++++++++++++-- app/src/options.c | 1 + app/src/options.h | 6 +++ app/src/scrcpy.c | 29 +++++++++-- app/src/usb/gamepad_aoa.c | 91 +++++++++++++++++++++++++++++++++ app/src/usb/gamepad_aoa.h | 25 +++++++++ 10 files changed, 208 insertions(+), 11 deletions(-) create mode 100644 app/src/usb/gamepad_aoa.c create mode 100644 app/src/usb/gamepad_aoa.h diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index e0928cbd..bcfff85e 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -26,6 +26,7 @@ _scrcpy() { -e --select-tcpip -f --fullscreen --force-adb-forward + --gamepad= -h --help -K --keyboard= @@ -127,6 +128,10 @@ _scrcpy() { COMPREPLY=($(compgen -W 'disabled sdk uhid aoa' -- "$cur")) return ;; + --gamepad) + COMPREPLY=($(compgen -W 'disabled aoa' -- "$cur")) + return + ;; --orientation|--display-orientation) COMPREPLY=($(compgen -W '0 90 180 270 flip0 flip90 flip180 flip270' -- "$cur")) return diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 0f06ba4b..5cbfd84b 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -33,6 +33,7 @@ arguments=( {-e,--select-tcpip}'[Use TCP/IP device]' {-f,--fullscreen}'[Start in fullscreen]' '--force-adb-forward[Do not attempt to use \"adb reverse\" to connect to the device]' + '--gamepad=[Set the gamepad input mode]:mode:(disabled aoa)' {-h,--help}'[Print the help]' '-K[Use UHID keyboard (same as --keyboard=uhid)]' '--keyboard=[Set the keyboard input mode]:mode:(disabled sdk uhid aoa)' diff --git a/app/meson.build b/app/meson.build index a4880420..e3a7501a 100644 --- a/app/meson.build +++ b/app/meson.build @@ -95,6 +95,7 @@ usb_support = get_option('usb') if usb_support src += [ 'src/usb/aoa_hid.c', + 'src/usb/gamepad_aoa.c', 'src/usb/keyboard_aoa.c', 'src/usb/mouse_aoa.c', 'src/usb/scrcpy_otg.c', diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 9cbb6fcb..2e3522af 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -175,6 +175,16 @@ Start in fullscreen. .B \-\-force\-adb\-forward Do not attempt to use "adb reverse" to connect to the device. +.TP +.BI "\-\-gamepad " mode +Select how to send gamepad inputs to the device. + +Possible values are "disabled" and "aoa": + + - "disabled" does not send gamepad inputs to the device. + - "aoa" simulates physical HID gamepads using the AOAv2 protocol. It may only work over USB. + +Also see \fB\-\-keyboard\f and R\fB\-\-mouse\fR. .TP .B \-h, \-\-help Print this help. @@ -200,7 +210,7 @@ For "uhid" and "aoa", the keyboard layout must be configured (once and for all) This option is only available when the HID keyboard is enabled (or a physical keyboard is connected). -Also see \fB\-\-mouse\fR. +Also see \fB\-\-mouse\fR and \fB\-\-gamepad\fR. .TP .B \-\-kill\-adb\-on\-close @@ -267,7 +277,7 @@ In "uhid" and "aoa" modes, the computer mouse is captured to control the device LAlt, LSuper or RSuper toggle the capture mode, to give control of the mouse back to the computer. -Also see \fB\-\-keyboard\fR. +Also see \fB\-\-keyboard\fR and \fB\-\-gamepad\fR. .TP .BI "\-\-mouse\-bind " xxxx[:xxxx] @@ -369,7 +379,7 @@ If any of \fB\-\-hid\-keyboard\fR or \fB\-\-hid\-mouse\fR is set, only enable ke It may only work over USB. -See \fB\-\-keyboard\fR and \fB\-\-mouse\fR. +See \fB\-\-keyboard\fR, \fB\-\-mouse\fR and \fB\-\-gamepad\fR. .TP .BI "\-p, \-\-port " port\fR[:\fIport\fR] diff --git a/app/src/cli.c b/app/src/cli.c index e34987f3..96877a51 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -101,6 +101,7 @@ enum { OPT_MOUSE_BIND, OPT_NO_MOUSE_HOVER, OPT_AUDIO_DUP, + OPT_GAMEPAD, }; struct sc_option { @@ -372,6 +373,17 @@ static const struct sc_option options[] = { .longopt_id = OPT_FORWARD_ALL_CLICKS, .longopt = "forward-all-clicks", }, + { + .longopt_id = OPT_GAMEPAD, + .longopt = "gamepad", + .argdesc = "mode", + .text = "Select how to send gamepad inputs to the device.\n" + "Possible values are \"disabled\" and \"aoa\".\n" + "\"disabled\" does not send gamepad inputs to the device.\n" + "\"aoa\" simulates physical gamepads using the AOAv2 protocol." + "It may only work over USB.\n" + "Also see --keyboard and --mouse.", + }, { .shortopt = 'h', .longopt = "help", @@ -403,7 +415,7 @@ static const struct sc_option options[] = { "start -a android.settings.HARD_KEYBOARD_SETTINGS`.\n" "This option is only available when a HID keyboard is enabled " "(or a physical keyboard is connected).\n" - "Also see --mouse.", + "Also see --mouse and --gamepad.", }, { .longopt_id = OPT_KILL_ADB_ON_CLOSE, @@ -502,7 +514,7 @@ static const struct sc_option options[] = { "to control the device directly (relative mouse mode).\n" "LAlt, LSuper or RSuper toggle the capture mode, to give " "control of the mouse back to the computer.\n" - "Also see --keyboard.", + "Also see --keyboard and --gamepad.", }, { .longopt_id = OPT_MOUSE_BIND, @@ -637,7 +649,7 @@ static const struct sc_option options[] = { "Keyboard and mouse may be disabled separately using" "--keyboard=disabled and --mouse=disabled.\n" "It may only work over USB.\n" - "See --keyboard and --mouse.", + "See --keyboard, --mouse and --gamepad.", }, { .shortopt = 'p', @@ -2046,6 +2058,27 @@ parse_mouse(const char *optarg, enum sc_mouse_input_mode *mode) { return false; } +static bool +parse_gamepad(const char *optarg, enum sc_gamepad_input_mode *mode) { + if (!strcmp(optarg, "disabled")) { + *mode = SC_GAMEPAD_INPUT_MODE_DISABLED; + return true; + } + + if (!strcmp(optarg, "aoa")) { +#ifdef HAVE_USB + *mode = SC_GAMEPAD_INPUT_MODE_AOA; + return true; +#else + LOGE("--gamepad=aoa is disabled."); + return false; +#endif + } + + LOGE("Unsupported gamepad: %s (expected disabled or aoa)", optarg); + return false; +} + static bool parse_time_limit(const char *s, sc_tick *tick) { long value; @@ -2612,6 +2645,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], case OPT_AUDIO_DUP: opts->audio_dup = true; break; + case OPT_GAMEPAD: + if (!parse_gamepad(optarg, &opts->gamepad_input_mode)) { + return false; + } + break; default: // getopt prints the error message on stderr return false; diff --git a/app/src/options.c b/app/src/options.c index b876b660..f8448792 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -23,6 +23,7 @@ const struct scrcpy_options scrcpy_options_default = { .record_format = SC_RECORD_FORMAT_AUTO, .keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_AUTO, .mouse_input_mode = SC_MOUSE_INPUT_MODE_AUTO, + .gamepad_input_mode = SC_GAMEPAD_INPUT_MODE_DISABLED, .mouse_bindings = { .pri = { .right_click = SC_MOUSE_BINDING_AUTO, diff --git a/app/src/options.h b/app/src/options.h index 6e77c175..a7b96bb6 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -156,6 +156,11 @@ enum sc_mouse_input_mode { SC_MOUSE_INPUT_MODE_AOA, }; +enum sc_gamepad_input_mode { + SC_GAMEPAD_INPUT_MODE_DISABLED, + SC_GAMEPAD_INPUT_MODE_AOA, +}; + enum sc_mouse_binding { SC_MOUSE_BINDING_AUTO, SC_MOUSE_BINDING_DISABLED, @@ -231,6 +236,7 @@ struct scrcpy_options { enum sc_record_format record_format; enum sc_keyboard_input_mode keyboard_input_mode; enum sc_mouse_input_mode mouse_input_mode; + enum sc_gamepad_input_mode gamepad_input_mode; struct sc_mouse_bindings mouse_bindings; enum sc_camera_facing camera_facing; struct sc_port_range port_range; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 24738876..bd706cc1 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -29,6 +29,7 @@ #include "uhid/mouse_uhid.h" #ifdef HAVE_USB # include "usb/aoa_hid.h" +# include "usb/gamepad_aoa.h" # include "usb/keyboard_aoa.h" # include "usb/mouse_aoa.h" # include "usb/usb.h" @@ -79,6 +80,9 @@ struct scrcpy { struct sc_mouse_aoa mouse_aoa; #endif }; +#ifdef HAVE_USB + struct sc_gamepad_aoa gamepad_aoa; +#endif struct sc_timeout timeout; }; @@ -370,6 +374,7 @@ scrcpy(struct scrcpy_options *options) { bool aoa_hid_initialized = false; bool keyboard_aoa_initialized = false; bool mouse_aoa_initialized = false; + bool gamepad_aoa_initialized = false; #endif bool controller_initialized = false; bool controller_started = false; @@ -485,9 +490,11 @@ scrcpy(struct scrcpy_options *options) { } } - if (SDL_Init(SDL_INIT_GAMECONTROLLER)) { - LOGE("Could not initialize SDL gamepad: %s", SDL_GetError()); - goto end; + if (options->gamepad_input_mode != SC_GAMEPAD_INPUT_MODE_DISABLED) { + if (SDL_Init(SDL_INIT_GAMECONTROLLER)) { + LOGE("Could not initialize SDL gamepad: %s", SDL_GetError()); + goto end; + } } sdl_configure(options->video_playback, options->disable_screensaver); @@ -587,6 +594,7 @@ scrcpy(struct scrcpy_options *options) { struct sc_controller *controller = NULL; struct sc_key_processor *kp = NULL; struct sc_mouse_processor *mp = NULL; + struct sc_gamepad_processor *gp = NULL; if (options->control) { static const struct sc_controller_callbacks controller_cbs = { @@ -606,7 +614,9 @@ scrcpy(struct scrcpy_options *options) { options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_AOA; bool use_mouse_aoa = options->mouse_input_mode == SC_MOUSE_INPUT_MODE_AOA; - if (use_keyboard_aoa || use_mouse_aoa) { + bool use_gamepad_aoa = + options->gamepad_input_mode == SC_GAMEPAD_INPUT_MODE_AOA; + if (use_keyboard_aoa || use_mouse_aoa || use_gamepad_aoa) { bool ok = sc_acksync_init(&s->acksync); if (!ok) { goto end; @@ -672,6 +682,12 @@ scrcpy(struct scrcpy_options *options) { } } + if (use_gamepad_aoa) { + sc_gamepad_aoa_init(&s->gamepad_aoa, &s->aoa); + gp = &s->gamepad_aoa.gamepad_processor; + gamepad_aoa_initialized = true; + } + aoa_complete: if (aoa_fail || !sc_aoa_start(&s->aoa)) { sc_acksync_destroy(&s->acksync); @@ -740,7 +756,7 @@ aoa_complete: .fp = fp, .kp = kp, .mp = mp, - .gp = NULL, + .gp = gp, .mouse_bindings = options->mouse_bindings, .legacy_paste = options->legacy_paste, .clipboard_autosync = options->clipboard_autosync, @@ -878,6 +894,9 @@ end: if (mouse_aoa_initialized) { sc_mouse_aoa_destroy(&s->mouse_aoa); } + if (gamepad_aoa_initialized) { + sc_gamepad_aoa_destroy(&s->gamepad_aoa); + } sc_aoa_stop(&s->aoa); sc_usb_stop(&s->usb); } diff --git a/app/src/usb/gamepad_aoa.c b/app/src/usb/gamepad_aoa.c new file mode 100644 index 00000000..37587532 --- /dev/null +++ b/app/src/usb/gamepad_aoa.c @@ -0,0 +1,91 @@ +#include "gamepad_aoa.h" + +#include "input_events.h" +#include "util/log.h" + +/** Downcast gamepad processor to gamepad_aoa */ +#define DOWNCAST(GP) container_of(GP, struct sc_gamepad_aoa, gamepad_processor) + +static void +sc_gamepad_processor_process_gamepad_device(struct sc_gamepad_processor *gp, + const struct sc_gamepad_device_event *event) { + struct sc_gamepad_aoa *gamepad = DOWNCAST(gp); + + if (event->type == SC_GAMEPAD_DEVICE_ADDED) { + struct sc_hid_open hid_open; + if (!sc_hid_gamepad_generate_open(&gamepad->hid, &hid_open, + event->gamepad_id)) { + return; + } + + // exit_on_error: false (a gamepad open failure should not exit scrcpy) + if (!sc_aoa_push_open(gamepad->aoa, &hid_open, false)) { + LOGW("Could not push AOA HID open (gamepad)"); + } + } else { + assert(event->type == SC_GAMEPAD_DEVICE_REMOVED); + + struct sc_hid_close hid_close; + if (!sc_hid_gamepad_generate_close(&gamepad->hid, &hid_close, + event->gamepad_id)) { + return; + } + + if (!sc_aoa_push_close(gamepad->aoa, &hid_close)) { + LOGW("Could not push AOA HID close (gamepad)"); + } + } +} + +static void +sc_gamepad_processor_process_gamepad_axis(struct sc_gamepad_processor *gp, + const struct sc_gamepad_axis_event *event) { + struct sc_gamepad_aoa *gamepad = DOWNCAST(gp); + + struct sc_hid_input hid_input; + if (!sc_hid_gamepad_generate_input_from_axis(&gamepad->hid, &hid_input, + event)) { + return; + } + + if (!sc_aoa_push_input(gamepad->aoa, &hid_input)) { + LOGW("Could not push AOA HID input (gamepad axis)"); + } +} + +static void +sc_gamepad_processor_process_gamepad_button(struct sc_gamepad_processor *gp, + const struct sc_gamepad_button_event *event) { + struct sc_gamepad_aoa *gamepad = DOWNCAST(gp); + + struct sc_hid_input hid_input; + if (!sc_hid_gamepad_generate_input_from_button(&gamepad->hid, &hid_input, + event)) { + return; + } + + if (!sc_aoa_push_input(gamepad->aoa, &hid_input)) { + LOGW("Could not push AOA HID input (gamepad button)"); + } +} + +void +sc_gamepad_aoa_init(struct sc_gamepad_aoa *gamepad, struct sc_aoa *aoa) { + gamepad->aoa = aoa; + + sc_hid_gamepad_init(&gamepad->hid); + + static const struct sc_gamepad_processor_ops ops = { + .process_gamepad_device = sc_gamepad_processor_process_gamepad_device, + .process_gamepad_axis = sc_gamepad_processor_process_gamepad_axis, + .process_gamepad_button = sc_gamepad_processor_process_gamepad_button, + }; + + gamepad->gamepad_processor.ops = &ops; +} + +void +sc_gamepad_aoa_destroy(struct sc_gamepad_aoa *gamepad) { + (void) gamepad; + // Do nothing, gamepad->aoa will automatically unregister all devices +} diff --git a/app/src/usb/gamepad_aoa.h b/app/src/usb/gamepad_aoa.h new file mode 100644 index 00000000..b2dfbe5e --- /dev/null +++ b/app/src/usb/gamepad_aoa.h @@ -0,0 +1,25 @@ +#ifndef SC_GAMEPAD_AOA_H +#define SC_GAMEPAD_AOA_H + +#include "common.h" + +#include + +#include "aoa_hid.h" +#include "hid/hid_gamepad.h" +#include "trait/gamepad_processor.h" + +struct sc_gamepad_aoa { + struct sc_gamepad_processor gamepad_processor; // gamepad processor trait + + struct sc_hid_gamepad hid; + struct sc_aoa *aoa; +}; + +void +sc_gamepad_aoa_init(struct sc_gamepad_aoa *gamepad, struct sc_aoa *aoa); + +void +sc_gamepad_aoa_destroy(struct sc_gamepad_aoa *gamepad); + +#endif From 3e68244dd3f410da5ff5cce3e6bb1c40743a7c51 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:08:08 +0200 Subject: [PATCH 1949/2244] Add connected gamepads on start Trigger SDL_CONTROLLERDEVICEADDED for all gamepads already connected when scrcpy starts. We want to handle both the gamepads initially connected and the gamepads connected while scrcpy is running. This is not racy, because this event may not be trigged automatically until SDL events are "pumped" (SDL_PumpEvents/SDL_WaitEvent). PR #5270 --- app/src/scrcpy.c | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index bd706cc1..fbd00db7 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -342,6 +342,21 @@ scrcpy_generate_scid(void) { return sc_rand_u32(&rand) & 0x7FFFFFFF; } +static void +init_sdl_gamepads(void) { + // Trigger a SDL_CONTROLLERDEVICEADDED event for all gamepads already + // connected + int num_joysticks = SDL_NumJoysticks(); + for (int i = 0; i < num_joysticks; ++i) { + if (SDL_IsGameController(i)) { + SDL_Event event; + event.cdevice.type = SDL_CONTROLLERDEVICEADDED; + event.cdevice.which = i; + SDL_PushEvent(&event); + } + } +} + enum scrcpy_exit_code scrcpy(struct scrcpy_options *options) { static struct scrcpy scrcpy; @@ -868,6 +883,11 @@ aoa_complete: timeout_started = true; } + if (options->control + && options->gamepad_input_mode != SC_GAMEPAD_INPUT_MODE_DISABLED) { + init_sdl_gamepads(); + } + ret = event_loop(s); terminate_event_loop(); LOGD("quit..."); From 5fe884276b099f6313db886312fe691cc0bf69a5 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:08:08 +0200 Subject: [PATCH 1950/2244] Add gamepad support in OTG mode Implement gamepad support for OTG. PR #5270 --- app/src/cli.c | 12 ++++- app/src/usb/scrcpy_otg.c | 22 +++++++++ app/src/usb/screen_otg.c | 100 +++++++++++++++++++++++++++++++++++++++ app/src/usb/screen_otg.h | 3 ++ 4 files changed, 135 insertions(+), 2 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index 96877a51..6fb8cdcb 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -2838,9 +2838,17 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; } + enum sc_gamepad_input_mode gmode = opts->gamepad_input_mode; + if (gmode != SC_GAMEPAD_INPUT_MODE_AOA + && gmode != SC_GAMEPAD_INPUT_MODE_DISABLED) { + LOGE("In OTG mode, --gamepad only supports aoa or disabled."); + return false; + } + if (kmode == SC_KEYBOARD_INPUT_MODE_DISABLED - && mmode == SC_MOUSE_INPUT_MODE_DISABLED) { - LOGE("Cannot disable both keyboard and mouse in OTG mode."); + && mmode == SC_MOUSE_INPUT_MODE_DISABLED + && gmode == SC_GAMEPAD_INPUT_MODE_DISABLED) { + LOGE("Cannot not disable all inputs in OTG mode."); return false; } } diff --git a/app/src/usb/scrcpy_otg.c b/app/src/usb/scrcpy_otg.c index 71d1863f..47afd9d0 100644 --- a/app/src/usb/scrcpy_otg.c +++ b/app/src/usb/scrcpy_otg.c @@ -12,6 +12,7 @@ struct scrcpy_otg { struct sc_aoa aoa; struct sc_keyboard_aoa keyboard; struct sc_mouse_aoa mouse; + struct sc_gamepad_aoa gamepad; struct sc_screen_otg screen_otg; }; @@ -63,6 +64,13 @@ scrcpy_otg(struct scrcpy_options *options) { return SCRCPY_EXIT_FAILURE; } + if (options->gamepad_input_mode != SC_GAMEPAD_INPUT_MODE_DISABLED) { + if (SDL_Init(SDL_INIT_GAMECONTROLLER)) { + LOGE("Could not initialize SDL controller: %s", SDL_GetError()); + // Not fatal, keyboard/mouse should still work + } + } + atexit(SDL_Quit); if (!SDL_SetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1")) { @@ -73,6 +81,7 @@ scrcpy_otg(struct scrcpy_options *options) { struct sc_keyboard_aoa *keyboard = NULL; struct sc_mouse_aoa *mouse = NULL; + struct sc_gamepad_aoa *gamepad = NULL; bool usb_device_initialized = false; bool usb_connected = false; bool aoa_started = false; @@ -119,11 +128,15 @@ scrcpy_otg(struct scrcpy_options *options) { || options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_DISABLED); assert(options->mouse_input_mode == SC_MOUSE_INPUT_MODE_AOA || options->mouse_input_mode == SC_MOUSE_INPUT_MODE_DISABLED); + assert(options->gamepad_input_mode == SC_GAMEPAD_INPUT_MODE_AOA + || options->gamepad_input_mode == SC_GAMEPAD_INPUT_MODE_DISABLED); bool enable_keyboard = options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_AOA; bool enable_mouse = options->mouse_input_mode == SC_MOUSE_INPUT_MODE_AOA; + bool enable_gamepad = + options->gamepad_input_mode == SC_GAMEPAD_INPUT_MODE_AOA; if (enable_keyboard) { ok = sc_keyboard_aoa_init(&s->keyboard, &s->aoa); @@ -141,6 +154,11 @@ scrcpy_otg(struct scrcpy_options *options) { mouse = &s->mouse; } + if (enable_gamepad) { + sc_gamepad_aoa_init(&s->gamepad, &s->aoa); + gamepad = &s->gamepad; + } + ok = sc_aoa_start(&s->aoa); if (!ok) { goto end; @@ -155,6 +173,7 @@ scrcpy_otg(struct scrcpy_options *options) { struct sc_screen_otg_params params = { .keyboard = keyboard, .mouse = mouse, + .gamepad = gamepad, .window_title = window_title, .always_on_top = options->always_on_top, .window_x = options->window_x, @@ -188,6 +207,9 @@ end: if (keyboard) { sc_keyboard_aoa_destroy(&s->keyboard); } + if (gamepad) { + sc_gamepad_aoa_destroy(&s->gamepad); + } if (aoa_initialized) { sc_aoa_join(&s->aoa); diff --git a/app/src/usb/screen_otg.c b/app/src/usb/screen_otg.c index 5c4f97f0..b13f8d04 100644 --- a/app/src/usb/screen_otg.c +++ b/app/src/usb/screen_otg.c @@ -59,6 +59,7 @@ sc_screen_otg_init(struct sc_screen_otg *screen, const struct sc_screen_otg_params *params) { screen->keyboard = params->keyboard; screen->mouse = params->mouse; + screen->gamepad = params->gamepad; screen->mouse_capture_key_pressed = 0; @@ -214,6 +215,87 @@ sc_screen_otg_process_mouse_wheel(struct sc_screen_otg *screen, mp->ops->process_mouse_scroll(mp, &evt); } +static void +sc_screen_otg_process_gamepad_device(struct sc_screen_otg *screen, + const SDL_ControllerDeviceEvent *event) { + assert(screen->gamepad); + struct sc_gamepad_processor *gp = &screen->gamepad->gamepad_processor; + + SDL_JoystickID id; + if (event->type == SDL_CONTROLLERDEVICEADDED) { + SDL_GameController *gc = SDL_GameControllerOpen(event->which); + if (!gc) { + LOGW("Could not open game controller"); + return; + } + + SDL_Joystick *joystick = SDL_GameControllerGetJoystick(gc); + if (!joystick) { + LOGW("Could not get controller joystick"); + SDL_GameControllerClose(gc); + return; + } + + id = SDL_JoystickInstanceID(joystick); + } else if (event->type == SDL_CONTROLLERDEVICEREMOVED) { + id = event->which; + + SDL_GameController *gc = SDL_GameControllerFromInstanceID(id); + if (gc) { + SDL_GameControllerClose(gc); + } else { + LOGW("Unknown gamepad device removed"); + } + } else { + // Nothing to do + return; + } + + struct sc_gamepad_device_event evt = { + .type = sc_gamepad_device_event_type_from_sdl_type(event->type), + .gamepad_id = id, + }; + gp->ops->process_gamepad_device(gp, &evt); +} + +static void +sc_screen_otg_process_gamepad_axis(struct sc_screen_otg *screen, + const SDL_ControllerAxisEvent *event) { + assert(screen->gamepad); + struct sc_gamepad_processor *gp = &screen->gamepad->gamepad_processor; + + enum sc_gamepad_axis axis = sc_gamepad_axis_from_sdl(event->axis); + if (axis == SC_GAMEPAD_AXIS_UNKNOWN) { + return; + } + + struct sc_gamepad_axis_event evt = { + .gamepad_id = event->which, + .axis = axis, + .value = event->value, + }; + gp->ops->process_gamepad_axis(gp, &evt); +} + +static void +sc_screen_otg_process_gamepad_button(struct sc_screen_otg *screen, + const SDL_ControllerButtonEvent *event) { + assert(screen->gamepad); + struct sc_gamepad_processor *gp = &screen->gamepad->gamepad_processor; + + enum sc_gamepad_button button = sc_gamepad_button_from_sdl(event->button); + if (button == SC_GAMEPAD_BUTTON_UNKNOWN) { + return; + } + + struct sc_gamepad_button_event evt = { + .gamepad_id = event->which, + .action = sc_action_from_sdl_controllerbutton_type(event->type), + .button = button, + }; + gp->ops->process_gamepad_button(gp, &evt); +} + void sc_screen_otg_handle_event(struct sc_screen_otg *screen, SDL_Event *event) { switch (event->type) { @@ -293,5 +375,23 @@ sc_screen_otg_handle_event(struct sc_screen_otg *screen, SDL_Event *event) { sc_screen_otg_process_mouse_wheel(screen, &event->wheel); } break; + case SDL_CONTROLLERDEVICEADDED: + case SDL_CONTROLLERDEVICEREMOVED: + // Handle device added or removed even if paused + if (screen->gamepad) { + sc_screen_otg_process_gamepad_device(screen, &event->cdevice); + } + break; + case SDL_CONTROLLERAXISMOTION: + if (screen->gamepad) { + sc_screen_otg_process_gamepad_axis(screen, &event->caxis); + } + break; + case SDL_CONTROLLERBUTTONDOWN: + case SDL_CONTROLLERBUTTONUP: + if (screen->gamepad) { + sc_screen_otg_process_gamepad_button(screen, &event->cbutton); + } + break; } } diff --git a/app/src/usb/screen_otg.h b/app/src/usb/screen_otg.h index c4e03b87..2ea76eda 100644 --- a/app/src/usb/screen_otg.h +++ b/app/src/usb/screen_otg.h @@ -8,10 +8,12 @@ #include "keyboard_aoa.h" #include "mouse_aoa.h" +#include "gamepad_aoa.h" struct sc_screen_otg { struct sc_keyboard_aoa *keyboard; struct sc_mouse_aoa *mouse; + struct sc_gamepad_aoa *gamepad; SDL_Window *window; SDL_Renderer *renderer; @@ -24,6 +26,7 @@ struct sc_screen_otg { struct sc_screen_otg_params { struct sc_keyboard_aoa *keyboard; struct sc_mouse_aoa *mouse; + struct sc_gamepad_aoa *gamepad; const char *window_title; bool always_on_top; From 64a25f6e9dfc12d25fa54126afbbef34b27f56f9 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:08:08 +0200 Subject: [PATCH 1951/2244] Add UHID_DESTROY control message This message will be sent on gamepad disconnection. Contrary to keyboard and mouse devices, which are registered once and unregistered when scrcpy exists, each physical gamepad is mapped with its own HID id, and they can be plugged and unplugged dynamically. PR #5270 --- app/src/control_msg.c | 13 ++++++++++-- app/src/control_msg.h | 4 ++++ app/tests/test_control_msg_serialize.c | 20 +++++++++++++++++++ .../scrcpy/control/ControlMessage.java | 10 +++++++++- .../scrcpy/control/ControlMessageReader.java | 7 +++++++ .../control/ControlMessageReaderTest.java | 18 +++++++++++++++++ 6 files changed, 69 insertions(+), 3 deletions(-) diff --git a/app/src/control_msg.c b/app/src/control_msg.c index daa3bde7..b9d50222 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -155,6 +155,9 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) { sc_write16be(&buf[3], msg->uhid_input.size); memcpy(&buf[5], msg->uhid_input.data, msg->uhid_input.size); return 5 + msg->uhid_input.size; + case SC_CONTROL_MSG_TYPE_UHID_DESTROY: + sc_write16be(&buf[1], msg->uhid_destroy.id); + return 3; case SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL: case SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL: case SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS: @@ -269,6 +272,9 @@ sc_control_msg_log(const struct sc_control_msg *msg) { } break; } + case SC_CONTROL_MSG_TYPE_UHID_DESTROY: + LOG_CMSG("UHID destroy [%" PRIu16 "]", msg->uhid_destroy.id); + break; case SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS: LOG_CMSG("open hard keyboard settings"); break; @@ -281,8 +287,11 @@ sc_control_msg_log(const struct sc_control_msg *msg) { bool sc_control_msg_is_droppable(const struct sc_control_msg *msg) { // Cannot drop UHID_CREATE messages, because it would cause all further - // UHID_INPUT messages for this device to be invalid - return msg->type != SC_CONTROL_MSG_TYPE_UHID_CREATE; + // UHID_INPUT messages for this device to be invalid. + // Cannot drop UHID_DESTROY messages either, because a further UHID_CREATE + // with the same id may fail. + return msg->type != SC_CONTROL_MSG_TYPE_UHID_CREATE + && msg->type != SC_CONTROL_MSG_TYPE_UHID_DESTROY; } void diff --git a/app/src/control_msg.h b/app/src/control_msg.h index 63670705..b48d91af 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -39,6 +39,7 @@ enum sc_control_msg_type { SC_CONTROL_MSG_TYPE_ROTATE_DEVICE, SC_CONTROL_MSG_TYPE_UHID_CREATE, SC_CONTROL_MSG_TYPE_UHID_INPUT, + SC_CONTROL_MSG_TYPE_UHID_DESTROY, SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS, }; @@ -105,6 +106,9 @@ struct sc_control_msg { uint16_t size; uint8_t data[SC_HID_MAX_SIZE]; } uhid_input; + struct { + uint16_t id; + } uhid_destroy; }; }; diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c index 7a978f2b..f88048d8 100644 --- a/app/tests/test_control_msg_serialize.c +++ b/app/tests/test_control_msg_serialize.c @@ -370,6 +370,25 @@ static void test_serialize_uhid_input(void) { assert(!memcmp(buf, expected, sizeof(expected))); } +static void test_serialize_uhid_destroy(void) { + struct sc_control_msg msg = { + .type = SC_CONTROL_MSG_TYPE_UHID_DESTROY, + .uhid_destroy = { + .id = 42, + }, + }; + + uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; + size_t size = sc_control_msg_serialize(&msg, buf); + assert(size == 3); + + const uint8_t expected[] = { + SC_CONTROL_MSG_TYPE_UHID_DESTROY, + 0, 42, // id + }; + assert(!memcmp(buf, expected, sizeof(expected))); +} + static void test_serialize_open_hard_keyboard(void) { struct sc_control_msg msg = { .type = SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS, @@ -405,6 +424,7 @@ int main(int argc, char *argv[]) { test_serialize_rotate_device(); test_serialize_uhid_create(); test_serialize_uhid_input(); + test_serialize_uhid_destroy(); test_serialize_open_hard_keyboard(); return 0; } diff --git a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java index c414f2a5..ef71353a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java @@ -21,7 +21,8 @@ public final class ControlMessage { public static final int TYPE_ROTATE_DEVICE = 11; public static final int TYPE_UHID_CREATE = 12; public static final int TYPE_UHID_INPUT = 13; - public static final int TYPE_OPEN_HARD_KEYBOARD_SETTINGS = 14; + public static final int TYPE_UHID_DESTROY = 14; + public static final int TYPE_OPEN_HARD_KEYBOARD_SETTINGS = 15; public static final long SEQUENCE_INVALID = 0; @@ -146,6 +147,13 @@ public final class ControlMessage { return msg; } + public static ControlMessage createUhidDestroy(int id) { + ControlMessage msg = new ControlMessage(); + msg.type = TYPE_UHID_DESTROY; + msg.id = id; + return msg; + } + public int getType() { return type; } diff --git a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java index f2e89da2..ef7877f1 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java @@ -51,6 +51,8 @@ public class ControlMessageReader { return parseUhidCreate(); case ControlMessage.TYPE_UHID_INPUT: return parseUhidInput(); + case ControlMessage.TYPE_UHID_DESTROY: + return parseUhidDestroy(); default: throw new ControlProtocolException("Unknown event type: " + type); } @@ -142,6 +144,11 @@ public class ControlMessageReader { return ControlMessage.createUhidInput(id, data); } + private ControlMessage parseUhidDestroy() throws IOException { + int id = dis.readUnsignedShort(); + return ControlMessage.createUhidDestroy(id); + } + private Position parsePosition() throws IOException { int x = dis.readInt(); int y = dis.readInt(); diff --git a/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java index ae18154d..0fd5f0ac 100644 --- a/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java @@ -362,6 +362,24 @@ public class ControlMessageReaderTest { Assert.assertEquals(-1, bis.read()); // EOS } + @Test + public void testParseUhidDestroy() throws IOException { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(ControlMessage.TYPE_UHID_DESTROY); + dos.writeShort(42); // id + byte[] packet = bos.toByteArray(); + + ByteArrayInputStream bis = new ByteArrayInputStream(packet); + ControlMessageReader reader = new ControlMessageReader(bis); + + ControlMessage event = reader.read(); + Assert.assertEquals(ControlMessage.TYPE_UHID_DESTROY, event.getType()); + Assert.assertEquals(42, event.getId()); + + Assert.assertEquals(-1, bis.read()); // EOS + } + @Test public void testParseOpenHardKeyboardSettings() throws IOException { ByteArrayOutputStream bos = new ByteArrayOutputStream(); From f9d1a333a0807bc354a7ba65a9fd2a9dbd12aa4b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:08:08 +0200 Subject: [PATCH 1952/2244] Add UHID gamepad support Similar to UHID keyboard and mouse, but for gamepads. Can be enabled with --gamepad=uhid or -G. It is not enabled by default because not all devices support UHID (there is a permission error on old Android versions). PR #5270 --- app/data/bash-completion/scrcpy | 3 +- app/data/zsh-completion/_scrcpy | 3 +- app/meson.build | 1 + app/scrcpy.1 | 7 +- app/src/cli.c | 16 ++- app/src/options.h | 1 + app/src/scrcpy.c | 11 +- app/src/uhid/gamepad_uhid.c | 122 ++++++++++++++++++ app/src/uhid/gamepad_uhid.h | 23 ++++ .../genymobile/scrcpy/control/Controller.java | 3 + .../scrcpy/control/UhidManager.java | 18 ++- 11 files changed, 200 insertions(+), 8 deletions(-) create mode 100644 app/src/uhid/gamepad_uhid.c create mode 100644 app/src/uhid/gamepad_uhid.h diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index bcfff85e..db825ecc 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -26,6 +26,7 @@ _scrcpy() { -e --select-tcpip -f --fullscreen --force-adb-forward + -G --gamepad= -h --help -K @@ -129,7 +130,7 @@ _scrcpy() { return ;; --gamepad) - COMPREPLY=($(compgen -W 'disabled aoa' -- "$cur")) + COMPREPLY=($(compgen -W 'disabled uhid aoa' -- "$cur")) return ;; --orientation|--display-orientation) diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 5cbfd84b..b5ceda3a 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -33,7 +33,8 @@ arguments=( {-e,--select-tcpip}'[Use TCP/IP device]' {-f,--fullscreen}'[Start in fullscreen]' '--force-adb-forward[Do not attempt to use \"adb reverse\" to connect to the device]' - '--gamepad=[Set the gamepad input mode]:mode:(disabled aoa)' + '-G[Use UHID gamepad (same as --gamepad=uhid)]' + '--gamepad=[Set the gamepad input mode]:mode:(disabled uhid aoa)' {-h,--help}'[Print the help]' '-K[Use UHID keyboard (same as --keyboard=uhid)]' '--keyboard=[Set the keyboard input mode]:mode:(disabled sdk uhid aoa)' diff --git a/app/meson.build b/app/meson.build index e3a7501a..fc752e86 100644 --- a/app/meson.build +++ b/app/meson.build @@ -37,6 +37,7 @@ src = [ 'src/hid/hid_mouse.c', 'src/trait/frame_source.c', 'src/trait/packet_source.c', + 'src/uhid/gamepad_uhid.c', 'src/uhid/keyboard_uhid.c', 'src/uhid/mouse_uhid.c', 'src/uhid/uhid_output.c', diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 2e3522af..e4295feb 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -175,13 +175,18 @@ Start in fullscreen. .B \-\-force\-adb\-forward Do not attempt to use "adb reverse" to connect to the device. +.TP +.B \-K +Same as \fB\-\-gamepad=uhid\fR. + .TP .BI "\-\-gamepad " mode Select how to send gamepad inputs to the device. -Possible values are "disabled" and "aoa": +Possible values are "disabled", "uhid" and "aoa": - "disabled" does not send gamepad inputs to the device. + - "uhid" simulates physical HID gamepads using the Linux HID kernel module on the device. - "aoa" simulates physical HID gamepads using the AOAv2 protocol. It may only work over USB. Also see \fB\-\-keyboard\f and R\fB\-\-mouse\fR. diff --git a/app/src/cli.c b/app/src/cli.c index 6fb8cdcb..7fe0bc70 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -373,13 +373,19 @@ static const struct sc_option options[] = { .longopt_id = OPT_FORWARD_ALL_CLICKS, .longopt = "forward-all-clicks", }, + { + .shortopt = 'G', + .text = "Same as --gamepad=uhid.", + }, { .longopt_id = OPT_GAMEPAD, .longopt = "gamepad", .argdesc = "mode", .text = "Select how to send gamepad inputs to the device.\n" - "Possible values are \"disabled\" and \"aoa\".\n" + "Possible values are \"disabled\", \"uhid\" and \"aoa\".\n" "\"disabled\" does not send gamepad inputs to the device.\n" + "\"uhid\" simulates physical HID gamepads using the Linux UHID " + "kernel module on the device.\n" "\"aoa\" simulates physical gamepads using the AOAv2 protocol." "It may only work over USB.\n" "Also see --keyboard and --mouse.", @@ -2065,6 +2071,11 @@ parse_gamepad(const char *optarg, enum sc_gamepad_input_mode *mode) { return true; } + if (!strcmp(optarg, "uhid")) { + *mode = SC_GAMEPAD_INPUT_MODE_UHID; + return true; + } + if (!strcmp(optarg, "aoa")) { #ifdef HAVE_USB *mode = SC_GAMEPAD_INPUT_MODE_AOA; @@ -2645,6 +2656,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], case OPT_AUDIO_DUP: opts->audio_dup = true; break; + case 'G': + opts->gamepad_input_mode = SC_GAMEPAD_INPUT_MODE_UHID; + break; case OPT_GAMEPAD: if (!parse_gamepad(optarg, &opts->gamepad_input_mode)) { return false; diff --git a/app/src/options.h b/app/src/options.h index a7b96bb6..e2652773 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -158,6 +158,7 @@ enum sc_mouse_input_mode { enum sc_gamepad_input_mode { SC_GAMEPAD_INPUT_MODE_DISABLED, + SC_GAMEPAD_INPUT_MODE_UHID, SC_GAMEPAD_INPUT_MODE_AOA, }; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index fbd00db7..71e64344 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -25,6 +25,7 @@ #include "recorder.h" #include "screen.h" #include "server.h" +#include "uhid/gamepad_uhid.h" #include "uhid/keyboard_uhid.h" #include "uhid/mouse_uhid.h" #ifdef HAVE_USB @@ -80,9 +81,12 @@ struct scrcpy { struct sc_mouse_aoa mouse_aoa; #endif }; + union { + struct sc_gamepad_uhid gamepad_uhid; #ifdef HAVE_USB - struct sc_gamepad_aoa gamepad_aoa; + struct sc_gamepad_aoa gamepad_aoa; #endif + }; struct sc_timeout timeout; }; @@ -750,6 +754,11 @@ aoa_complete: mp = &s->mouse_uhid.mouse_processor; } + if (options->gamepad_input_mode == SC_GAMEPAD_INPUT_MODE_UHID) { + sc_gamepad_uhid_init(&s->gamepad_uhid, &s->controller); + gp = &s->gamepad_uhid.gamepad_processor; + } + sc_controller_configure(&s->controller, acksync, uhid_devices); if (!sc_controller_start(&s->controller)) { diff --git a/app/src/uhid/gamepad_uhid.c b/app/src/uhid/gamepad_uhid.c new file mode 100644 index 00000000..3c8d3643 --- /dev/null +++ b/app/src/uhid/gamepad_uhid.c @@ -0,0 +1,122 @@ +#include "gamepad_uhid.h" + +#include "hid/hid_gamepad.h" +#include "input_events.h" +#include "util/log.h" + +/** Downcast gamepad processor to sc_gamepad_uhid */ +#define DOWNCAST(GP) container_of(GP, struct sc_gamepad_uhid, gamepad_processor) + +static void +sc_gamepad_uhid_send_input(struct sc_gamepad_uhid *gamepad, + const struct sc_hid_input *hid_input, + const char *name) { + struct sc_control_msg msg; + msg.type = SC_CONTROL_MSG_TYPE_UHID_INPUT; + msg.uhid_input.id = hid_input->hid_id; + + assert(hid_input->size <= SC_HID_MAX_SIZE); + memcpy(msg.uhid_input.data, hid_input->data, hid_input->size); + msg.uhid_input.size = hid_input->size; + + if (!sc_controller_push_msg(gamepad->controller, &msg)) { + LOGE("Could not push UHID_INPUT message (%s)", name); + } +} + +static void +sc_gamepad_uhid_send_open(struct sc_gamepad_uhid *gamepad, + const struct sc_hid_open *hid_open) { + struct sc_control_msg msg; + msg.type = SC_CONTROL_MSG_TYPE_UHID_CREATE; + msg.uhid_create.id = hid_open->hid_id; + msg.uhid_create.report_desc = hid_open->report_desc; + msg.uhid_create.report_desc_size = hid_open->report_desc_size; + + if (!sc_controller_push_msg(gamepad->controller, &msg)) { + LOGE("Could not push UHID_CREATE message (gamepad)"); + } +} + +static void +sc_gamepad_uhid_send_close(struct sc_gamepad_uhid *gamepad, + const struct sc_hid_close *hid_close) { + struct sc_control_msg msg; + msg.type = SC_CONTROL_MSG_TYPE_UHID_DESTROY; + msg.uhid_create.id = hid_close->hid_id; + + if (!sc_controller_push_msg(gamepad->controller, &msg)) { + LOGE("Could not push UHID_DESTROY message (gamepad)"); + } +} + +static void +sc_gamepad_processor_process_gamepad_device(struct sc_gamepad_processor *gp, + const struct sc_gamepad_device_event *event) { + struct sc_gamepad_uhid *gamepad = DOWNCAST(gp); + + if (event->type == SC_GAMEPAD_DEVICE_ADDED) { + struct sc_hid_open hid_open; + if (!sc_hid_gamepad_generate_open(&gamepad->hid, &hid_open, + event->gamepad_id)) { + return; + } + + sc_gamepad_uhid_send_open(gamepad, &hid_open); + } else { + assert(event->type == SC_GAMEPAD_DEVICE_REMOVED); + + struct sc_hid_close hid_close; + if (!sc_hid_gamepad_generate_close(&gamepad->hid, &hid_close, + event->gamepad_id)) { + return; + } + + sc_gamepad_uhid_send_close(gamepad, &hid_close); + } +} + +static void +sc_gamepad_processor_process_gamepad_axis(struct sc_gamepad_processor *gp, + const struct sc_gamepad_axis_event *event) { + struct sc_gamepad_uhid *gamepad = DOWNCAST(gp); + + struct sc_hid_input hid_input; + if (!sc_hid_gamepad_generate_input_from_axis(&gamepad->hid, &hid_input, + event)) { + return; + } + + sc_gamepad_uhid_send_input(gamepad, &hid_input, "gamepad axis"); +} + +static void +sc_gamepad_processor_process_gamepad_button(struct sc_gamepad_processor *gp, + const struct sc_gamepad_button_event *event) { + struct sc_gamepad_uhid *gamepad = DOWNCAST(gp); + + struct sc_hid_input hid_input; + if (!sc_hid_gamepad_generate_input_from_button(&gamepad->hid, &hid_input, + event)) { + return; + } + + sc_gamepad_uhid_send_input(gamepad, &hid_input, "gamepad button"); + +} + +void +sc_gamepad_uhid_init(struct sc_gamepad_uhid *gamepad, + struct sc_controller *controller) { + sc_hid_gamepad_init(&gamepad->hid); + + gamepad->controller = controller; + + static const struct sc_gamepad_processor_ops ops = { + .process_gamepad_device = sc_gamepad_processor_process_gamepad_device, + .process_gamepad_axis = sc_gamepad_processor_process_gamepad_axis, + .process_gamepad_button = sc_gamepad_processor_process_gamepad_button, + }; + + gamepad->gamepad_processor.ops = &ops; +} diff --git a/app/src/uhid/gamepad_uhid.h b/app/src/uhid/gamepad_uhid.h new file mode 100644 index 00000000..07d03099 --- /dev/null +++ b/app/src/uhid/gamepad_uhid.h @@ -0,0 +1,23 @@ +#ifndef SC_GAMEPAD_UHID_H +#define SC_GAMEPAD_UHID_H + +#include "common.h" + +#include + +#include "controller.h" +#include "hid/hid_gamepad.h" +#include "trait/gamepad_processor.h" + +struct sc_gamepad_uhid { + struct sc_gamepad_processor gamepad_processor; // gamepad processor trait + + struct sc_hid_gamepad hid; + struct sc_controller *controller; +}; + +void +sc_gamepad_uhid_init(struct sc_gamepad_uhid *mouse, + struct sc_controller *controller); + +#endif diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index 1494c10a..e656cbb6 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -215,6 +215,9 @@ public class Controller implements AsyncProcessor { case ControlMessage.TYPE_UHID_INPUT: getUhidManager().writeInput(msg.getId(), msg.getData()); break; + case ControlMessage.TYPE_UHID_DESTROY: + getUhidManager().close(msg.getId()); + break; case ControlMessage.TYPE_OPEN_HARD_KEYBOARD_SETTINGS: openHardKeyboardSettings(); break; diff --git a/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java b/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java index a7d55b7e..408dbf5d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java @@ -95,6 +95,12 @@ public final class UhidManager { } } + private void unregisterUhidListener(FileDescriptor fd) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + queue.removeOnFileDescriptorEventListener(fd); + } + } + private static byte[] extractHidOutputData(ByteBuffer buffer) { /* * #define UHID_DATA_MAX 4096 @@ -199,9 +205,15 @@ public final class UhidManager { } public void close(int id) { - FileDescriptor fd = fds.get(id); - assert fd != null; - close(fd); + // Linux: Documentation/hid/uhid.rst + // If you close() the fd, the device is automatically unregistered and destroyed internally. + FileDescriptor fd = fds.remove(id); + if (fd != null) { + unregisterUhidListener(fd); + close(fd); + } else { + Ln.w("Closing unknown UHID device: " + id); + } } public void closeAll() { From c4febd55ebb75e9033fe7af072cf4af54182ce45 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 7 Sep 2024 23:06:16 +0200 Subject: [PATCH 1953/2244] Make -K -M and -G use AOA in OTG mode For convenience, short options were added to select UHID input modes: - -K for --keyboard=uhid - -M for --mouse=uhid - -G for --gamepad=uhid In OTG mode, UHID is not available, so the short options should select AOA instead. PR #5270 --- app/data/zsh-completion/_scrcpy | 6 +++--- app/scrcpy.1 | 8 ++++---- app/src/cli.c | 24 ++++++++++++++++++------ app/src/options.h | 3 +++ 4 files changed, 28 insertions(+), 13 deletions(-) diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index b5ceda3a..fa0fa84f 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -33,10 +33,10 @@ arguments=( {-e,--select-tcpip}'[Use TCP/IP device]' {-f,--fullscreen}'[Start in fullscreen]' '--force-adb-forward[Do not attempt to use \"adb reverse\" to connect to the device]' - '-G[Use UHID gamepad (same as --gamepad=uhid)]' + '-G[Use UHID/AOA gamepad (same as --gamepad=uhid or --gamepad=aoa, depending on OTG mode)]' '--gamepad=[Set the gamepad input mode]:mode:(disabled uhid aoa)' {-h,--help}'[Print the help]' - '-K[Use UHID keyboard (same as --keyboard=uhid)]' + '-K[Use UHID/AOA keyboard (same as --keyboard=uhid or --keyboard=aoa, depending on OTG mode)]' '--keyboard=[Set the keyboard input mode]:mode:(disabled sdk uhid aoa)' '--kill-adb-on-close[Kill adb when scrcpy terminates]' '--legacy-paste[Inject computer clipboard text as a sequence of key events on Ctrl+v]' @@ -46,7 +46,7 @@ arguments=( '--list-encoders[List video and audio encoders available on the device]' '--lock-video-orientation=[Lock video orientation]:orientation:(unlocked initial 0 90 180 270)' {-m,--max-size=}'[Limit both the width and height of the video to value]' - '-M[Use UHID mouse (same as --mouse=uhid)]' + '-M[Use UHID/AOA mouse (same as --mouse=uhid or --mouse=aoa, depending on OTG mode)]' '--max-fps=[Limit the frame rate of screen capture]' '--mouse=[Set the mouse input mode]:mode:(disabled sdk uhid aoa)' '--mouse-bind=[Configure bindings of secondary clicks]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index e4295feb..a256c40e 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -176,8 +176,8 @@ Start in fullscreen. Do not attempt to use "adb reverse" to connect to the device. .TP -.B \-K -Same as \fB\-\-gamepad=uhid\fR. +.B \-G +Same as \fB\-\-gamepad=uhid\fR, or \fB\-\-keyboard=aoa\fR if \fB\-\-otg\fR is set. .TP .BI "\-\-gamepad " mode @@ -196,7 +196,7 @@ Print this help. .TP .B \-K -Same as \fB\-\-keyboard=uhid\fR. +Same as \fB\-\-keyboard=uhid\fR, or \fB\-\-keyboard=aoa\fR if \fB\-\-otg\fR is set. .TP .BI "\-\-keyboard " mode @@ -261,7 +261,7 @@ Default is 0 (unlimited). .TP .B \-M -Same as \fB\-\-mouse=uhid\fR. +Same as \fB\-\-mouse=uhid\fR, or \fB\-\-mouse=aoa\fR if \fB\-\-otg\fR is set. .TP .BI "\-\-max\-fps " value diff --git a/app/src/cli.c b/app/src/cli.c index 7fe0bc70..3c1f9a1b 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -375,7 +375,7 @@ static const struct sc_option options[] = { }, { .shortopt = 'G', - .text = "Same as --gamepad=uhid.", + .text = "Same as --gamepad=uhid, or --gamepad=aoa if --otg is set.", }, { .longopt_id = OPT_GAMEPAD, @@ -397,7 +397,7 @@ static const struct sc_option options[] = { }, { .shortopt = 'K', - .text = "Same as --keyboard=uhid.", + .text = "Same as --keyboard=uhid, or --keyboard=aoa if --otg is set.", }, { .longopt_id = OPT_KEYBOARD, @@ -493,7 +493,7 @@ static const struct sc_option options[] = { }, { .shortopt = 'M', - .text = "Same as --mouse=uhid.", + .text = "Same as --mouse=uhid, or --mouse=aoa if --otg is set.", }, { .longopt_id = OPT_MAX_FPS, @@ -2252,7 +2252,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], args->help = true; break; case 'K': - opts->keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_UHID; + opts->keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_UHID_OR_AOA; break; case OPT_KEYBOARD: if (!parse_keyboard(optarg, &opts->keyboard_input_mode)) { @@ -2272,7 +2272,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } break; case 'M': - opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_UHID; + opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_UHID_OR_AOA; break; case OPT_MOUSE: if (!parse_mouse(optarg, &opts->mouse_input_mode)) { @@ -2657,7 +2657,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], opts->audio_dup = true; break; case 'G': - opts->gamepad_input_mode = SC_GAMEPAD_INPUT_MODE_UHID; + opts->gamepad_input_mode = SC_GAMEPAD_INPUT_MODE_UHID_OR_AOA; break; case OPT_GAMEPAD: if (!parse_gamepad(optarg, &opts->gamepad_input_mode)) { @@ -2781,7 +2781,12 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], if (opts->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_AUTO) { opts->keyboard_input_mode = otg ? SC_KEYBOARD_INPUT_MODE_AOA : SC_KEYBOARD_INPUT_MODE_SDK; + } else if (opts->keyboard_input_mode + == SC_KEYBOARD_INPUT_MODE_UHID_OR_AOA) { + opts->keyboard_input_mode = otg ? SC_KEYBOARD_INPUT_MODE_AOA + : SC_KEYBOARD_INPUT_MODE_UHID; } + if (opts->mouse_input_mode == SC_MOUSE_INPUT_MODE_AUTO) { if (otg) { opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_AOA; @@ -2791,11 +2796,18 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } else { opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_SDK; } + } else if (opts->mouse_input_mode == SC_MOUSE_INPUT_MODE_UHID_OR_AOA) { + opts->mouse_input_mode = otg ? SC_MOUSE_INPUT_MODE_AOA + : SC_MOUSE_INPUT_MODE_UHID; } else if (opts->mouse_input_mode == SC_MOUSE_INPUT_MODE_SDK && !opts->video_playback) { LOGE("SDK mouse mode requires video playback. Try --mouse=uhid."); return false; } + if (opts->gamepad_input_mode == SC_GAMEPAD_INPUT_MODE_UHID_OR_AOA) { + opts->gamepad_input_mode = otg ? SC_GAMEPAD_INPUT_MODE_AOA + : SC_GAMEPAD_INPUT_MODE_UHID; + } } // If mouse bindings are not explicitly set, configure default bindings diff --git a/app/src/options.h b/app/src/options.h index e2652773..5f6726e0 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -142,6 +142,7 @@ enum sc_lock_video_orientation { enum sc_keyboard_input_mode { SC_KEYBOARD_INPUT_MODE_AUTO, + SC_KEYBOARD_INPUT_MODE_UHID_OR_AOA, // normal vs otg mode SC_KEYBOARD_INPUT_MODE_DISABLED, SC_KEYBOARD_INPUT_MODE_SDK, SC_KEYBOARD_INPUT_MODE_UHID, @@ -150,6 +151,7 @@ enum sc_keyboard_input_mode { enum sc_mouse_input_mode { SC_MOUSE_INPUT_MODE_AUTO, + SC_MOUSE_INPUT_MODE_UHID_OR_AOA, // normal vs otg mode SC_MOUSE_INPUT_MODE_DISABLED, SC_MOUSE_INPUT_MODE_SDK, SC_MOUSE_INPUT_MODE_UHID, @@ -158,6 +160,7 @@ enum sc_mouse_input_mode { enum sc_gamepad_input_mode { SC_GAMEPAD_INPUT_MODE_DISABLED, + SC_GAMEPAD_INPUT_MODE_UHID_OR_AOA, // normal vs otg mode SC_GAMEPAD_INPUT_MODE_UHID, SC_GAMEPAD_INPUT_MODE_AOA, }; From 68e27c7357fbf0c3a3a5d75fb40611b809c13282 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 8 Sep 2024 20:01:28 +0200 Subject: [PATCH 1954/2244] Reorder function parameters for consistency Make the local function write_string() accept the output buffer as a first parameter, like the other similar functions. PR #5270 --- app/src/control_msg.c | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/app/src/control_msg.c b/app/src/control_msg.c index b9d50222..bef7ab05 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -85,7 +85,7 @@ write_position(uint8_t *buf, const struct sc_position *position) { // write length (4 bytes) + string (non null-terminated) static size_t -write_string(const char *utf8, size_t max_len, uint8_t *buf) { +write_string(uint8_t *buf, const char *utf8, size_t max_len) { size_t len = sc_str_utf8_truncation_index(utf8, max_len); sc_write32be(buf, len); memcpy(&buf[4], utf8, len); @@ -103,9 +103,8 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) { sc_write32be(&buf[10], msg->inject_keycode.metastate); return 14; case SC_CONTROL_MSG_TYPE_INJECT_TEXT: { - size_t len = - write_string(msg->inject_text.text, - SC_CONTROL_MSG_INJECT_TEXT_MAX_LENGTH, &buf[1]); + size_t len = write_string(&buf[1], msg->inject_text.text, + SC_CONTROL_MSG_INJECT_TEXT_MAX_LENGTH); return 1 + len; } case SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT: @@ -137,9 +136,8 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) { case SC_CONTROL_MSG_TYPE_SET_CLIPBOARD: sc_write64be(&buf[1], msg->set_clipboard.sequence); buf[9] = !!msg->set_clipboard.paste; - size_t len = write_string(msg->set_clipboard.text, - SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH, - &buf[10]); + size_t len = write_string(&buf[10], msg->set_clipboard.text, + SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH); return 10 + len; case SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE: buf[1] = msg->set_screen_power_mode.mode; From 7f250dd66923018e145f90789d53c4c7a1e30962 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 8 Sep 2024 19:57:14 +0200 Subject: [PATCH 1955/2244] Mention physical gamepad names for UHID devices Initialize UHID devices with a custom name: - "scrcpy: $GAMEPAD_NAME" for gamepads - "scrcpy" for keyboard and mouse (or if no gamepad name is available) The name may appear in Android apps. PR #5270 --- app/src/control_msg.c | 52 +++++++++++++++---- app/src/control_msg.h | 1 + app/src/hid/hid_event.h | 1 + app/src/hid/hid_gamepad.c | 6 +++ app/src/hid/hid_keyboard.c | 1 + app/src/hid/hid_mouse.c | 1 + app/src/uhid/gamepad_uhid.c | 1 + app/src/uhid/keyboard_uhid.c | 1 + app/src/uhid/mouse_uhid.c | 1 + app/tests/test_control_msg_serialize.c | 7 ++- .../scrcpy/control/ControlMessage.java | 3 +- .../scrcpy/control/ControlMessageReader.java | 12 +++-- .../genymobile/scrcpy/control/Controller.java | 2 +- .../scrcpy/control/UhidManager.java | 17 ++++-- .../control/ControlMessageReaderTest.java | 5 +- 15 files changed, 88 insertions(+), 23 deletions(-) diff --git a/app/src/control_msg.c b/app/src/control_msg.c index bef7ab05..d599b62d 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -83,15 +83,34 @@ write_position(uint8_t *buf, const struct sc_position *position) { sc_write16be(&buf[10], position->screen_size.height); } -// write length (4 bytes) + string (non null-terminated) +// Write truncated string, and return the size +static size_t +write_string_payload(uint8_t *payload, const char *utf8, size_t max_len) { + if (!utf8) { + return 0; + } + size_t len = sc_str_utf8_truncation_index(utf8, max_len); + memcpy(payload, utf8, len); + return len; +} + +// Write length (4 bytes) + string (non null-terminated) static size_t write_string(uint8_t *buf, const char *utf8, size_t max_len) { - size_t len = sc_str_utf8_truncation_index(utf8, max_len); + size_t len = write_string_payload(buf + 4, utf8, max_len); sc_write32be(buf, len); - memcpy(&buf[4], utf8, len); return 4 + len; } +// Write length (1 byte) + string (non null-terminated) +static size_t +write_string_tiny(uint8_t *buf, const char *utf8, size_t max_len) { + assert(max_len <= 0xFF); + size_t len = write_string_payload(buf + 1, utf8, max_len); + buf[0] = len; + return 1 + len; +} + size_t sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) { buf[0] = msg->type; @@ -144,10 +163,18 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) { return 2; case SC_CONTROL_MSG_TYPE_UHID_CREATE: sc_write16be(&buf[1], msg->uhid_create.id); - sc_write16be(&buf[3], msg->uhid_create.report_desc_size); - memcpy(&buf[5], msg->uhid_create.report_desc, - msg->uhid_create.report_desc_size); - return 5 + msg->uhid_create.report_desc_size; + + size_t index = 3; + index += write_string_tiny(&buf[index], msg->uhid_create.name, 127); + + sc_write16be(&buf[index], msg->uhid_create.report_desc_size); + index += 2; + + memcpy(&buf[index], msg->uhid_create.report_desc, + msg->uhid_create.report_desc_size); + index += msg->uhid_create.report_desc_size; + + return index; case SC_CONTROL_MSG_TYPE_UHID_INPUT: sc_write16be(&buf[1], msg->uhid_input.id); sc_write16be(&buf[3], msg->uhid_input.size); @@ -253,10 +280,15 @@ sc_control_msg_log(const struct sc_control_msg *msg) { case SC_CONTROL_MSG_TYPE_ROTATE_DEVICE: LOG_CMSG("rotate device"); break; - case SC_CONTROL_MSG_TYPE_UHID_CREATE: - LOG_CMSG("UHID create [%" PRIu16 "] report_desc_size=%" PRIu16, - msg->uhid_create.id, msg->uhid_create.report_desc_size); + case SC_CONTROL_MSG_TYPE_UHID_CREATE: { + // Quote only if name is not null + const char *name = msg->uhid_create.name; + const char *quote = name ? "\"" : ""; + LOG_CMSG("UHID create [%" PRIu16 "] name=%s%s%s " + "report_desc_size=%" PRIu16, msg->uhid_create.id, + quote, name, quote, msg->uhid_create.report_desc_size); break; + } case SC_CONTROL_MSG_TYPE_UHID_INPUT: { char *hex = sc_str_to_hex_string(msg->uhid_input.data, msg->uhid_input.size); diff --git a/app/src/control_msg.h b/app/src/control_msg.h index b48d91af..1ae8cae4 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -98,6 +98,7 @@ struct sc_control_msg { } set_screen_power_mode; struct { uint16_t id; + const char *name; // pointer to static data uint16_t report_desc_size; const uint8_t *report_desc; // pointer to static data } uhid_create; diff --git a/app/src/hid/hid_event.h b/app/src/hid/hid_event.h index d6818e30..37c3611b 100644 --- a/app/src/hid/hid_event.h +++ b/app/src/hid/hid_event.h @@ -15,6 +15,7 @@ struct sc_hid_input { struct sc_hid_open { uint16_t hid_id; + const char *name; // pointer to static memory const uint8_t *report_desc; // pointer to static memory size_t report_desc_size; }; diff --git a/app/src/hid/hid_gamepad.c b/app/src/hid/hid_gamepad.c index cd009d15..e2bf0616 100644 --- a/app/src/hid/hid_gamepad.c +++ b/app/src/hid/hid_gamepad.c @@ -243,8 +243,14 @@ sc_hid_gamepad_generate_open(struct sc_hid_gamepad *hid, sc_hid_gamepad_slot_init(&hid->slots[slot_idx], gamepad_id); + SDL_GameController* game_controller = + SDL_GameControllerFromInstanceID(gamepad_id); + assert(game_controller); + const char *name = SDL_GameControllerName(game_controller); + uint16_t hid_id = sc_hid_gamepad_slot_get_id(slot_idx); hid_open->hid_id = hid_id; + hid_open->name = name; hid_open->report_desc = SC_HID_GAMEPAD_REPORT_DESC; hid_open->report_desc_size = sizeof(SC_HID_GAMEPAD_REPORT_DESC); diff --git a/app/src/hid/hid_keyboard.c b/app/src/hid/hid_keyboard.c index 961ad790..2109224a 100644 --- a/app/src/hid/hid_keyboard.c +++ b/app/src/hid/hid_keyboard.c @@ -335,6 +335,7 @@ sc_hid_keyboard_generate_input_from_mods(struct sc_hid_input *hid_input, void sc_hid_keyboard_generate_open(struct sc_hid_open *hid_open) { hid_open->hid_id = SC_HID_ID_KEYBOARD; + hid_open->name = NULL; // No name specified after "scrcpy" hid_open->report_desc = SC_HID_KEYBOARD_REPORT_DESC; hid_open->report_desc_size = sizeof(SC_HID_KEYBOARD_REPORT_DESC); } diff --git a/app/src/hid/hid_mouse.c b/app/src/hid/hid_mouse.c index 7acc413b..ac215165 100644 --- a/app/src/hid/hid_mouse.c +++ b/app/src/hid/hid_mouse.c @@ -190,6 +190,7 @@ sc_hid_mouse_generate_input_from_scroll(struct sc_hid_input *hid_input, void sc_hid_mouse_generate_open(struct sc_hid_open *hid_open) { hid_open->hid_id = SC_HID_ID_MOUSE; + hid_open->name = NULL; // No name specified after "scrcpy" hid_open->report_desc = SC_HID_MOUSE_REPORT_DESC; hid_open->report_desc_size = sizeof(SC_HID_MOUSE_REPORT_DESC); } diff --git a/app/src/uhid/gamepad_uhid.c b/app/src/uhid/gamepad_uhid.c index 3c8d3643..62b0f653 100644 --- a/app/src/uhid/gamepad_uhid.c +++ b/app/src/uhid/gamepad_uhid.c @@ -30,6 +30,7 @@ sc_gamepad_uhid_send_open(struct sc_gamepad_uhid *gamepad, struct sc_control_msg msg; msg.type = SC_CONTROL_MSG_TYPE_UHID_CREATE; msg.uhid_create.id = hid_open->hid_id; + msg.uhid_create.name = hid_open->name; msg.uhid_create.report_desc = hid_open->report_desc; msg.uhid_create.report_desc_size = hid_open->report_desc_size; diff --git a/app/src/uhid/keyboard_uhid.c b/app/src/uhid/keyboard_uhid.c index 11d41e40..9fdf4def 100644 --- a/app/src/uhid/keyboard_uhid.c +++ b/app/src/uhid/keyboard_uhid.c @@ -152,6 +152,7 @@ sc_keyboard_uhid_init(struct sc_keyboard_uhid *kb, struct sc_control_msg msg; msg.type = SC_CONTROL_MSG_TYPE_UHID_CREATE; msg.uhid_create.id = SC_HID_ID_KEYBOARD; + msg.uhid_create.name = hid_open.name; msg.uhid_create.report_desc = hid_open.report_desc; msg.uhid_create.report_desc_size = hid_open.report_desc_size; if (!sc_controller_push_msg(controller, &msg)) { diff --git a/app/src/uhid/mouse_uhid.c b/app/src/uhid/mouse_uhid.c index 9544ab0d..1dc02777 100644 --- a/app/src/uhid/mouse_uhid.c +++ b/app/src/uhid/mouse_uhid.c @@ -81,6 +81,7 @@ sc_mouse_uhid_init(struct sc_mouse_uhid *mouse, struct sc_control_msg msg; msg.type = SC_CONTROL_MSG_TYPE_UHID_CREATE; msg.uhid_create.id = SC_HID_ID_MOUSE; + msg.uhid_create.name = hid_open.name; msg.uhid_create.report_desc = hid_open.report_desc; msg.uhid_create.report_desc_size = hid_open.report_desc_size; if (!sc_controller_push_msg(controller, &msg)) { diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c index f88048d8..72ec61ee 100644 --- a/app/tests/test_control_msg_serialize.c +++ b/app/tests/test_control_msg_serialize.c @@ -329,6 +329,7 @@ static void test_serialize_uhid_create(void) { .type = SC_CONTROL_MSG_TYPE_UHID_CREATE, .uhid_create = { .id = 42, + .name = "ABC", .report_desc_size = sizeof(report_desc), .report_desc = report_desc, }, @@ -336,12 +337,14 @@ static void test_serialize_uhid_create(void) { uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; size_t size = sc_control_msg_serialize(&msg, buf); - assert(size == 16); + assert(size == 20); const uint8_t expected[] = { SC_CONTROL_MSG_TYPE_UHID_CREATE, 0, 42, // id - 0, 11, // size + 3, // name size + 65, 66, 67, // "ABC" + 0, 11, // report desc size 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, }; assert(!memcmp(buf, expected, sizeof(expected))); diff --git a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java index ef71353a..d1406ed0 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java @@ -131,10 +131,11 @@ public final class ControlMessage { return msg; } - public static ControlMessage createUhidCreate(int id, byte[] reportDesc) { + public static ControlMessage createUhidCreate(int id, String name, byte[] reportDesc) { ControlMessage msg = new ControlMessage(); msg.type = TYPE_UHID_CREATE; msg.id = id; + msg.text = name; msg.data = reportDesc; return msg; } diff --git a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java index ef7877f1..45116935 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java @@ -75,11 +75,16 @@ public class ControlMessageReader { return value; } - private String parseString() throws IOException { - byte[] data = parseByteArray(4); + private String parseString(int sizeBytes) throws IOException { + assert sizeBytes > 0 && sizeBytes <= 4; + byte[] data = parseByteArray(sizeBytes); return new String(data, StandardCharsets.UTF_8); } + private String parseString() throws IOException { + return parseString(4); + } + private byte[] parseByteArray(int sizeBytes) throws IOException { int len = parseBufferLength(sizeBytes); byte[] data = new byte[len]; @@ -134,8 +139,9 @@ public class ControlMessageReader { private ControlMessage parseUhidCreate() throws IOException { int id = dis.readUnsignedShort(); + String name = parseString(1); byte[] data = parseByteArray(2); - return ControlMessage.createUhidCreate(id, data); + return ControlMessage.createUhidCreate(id, name, data); } private ControlMessage parseUhidInput() throws IOException { diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index e656cbb6..38251655 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -210,7 +210,7 @@ public class Controller implements AsyncProcessor { device.rotateDevice(); break; case ControlMessage.TYPE_UHID_CREATE: - getUhidManager().open(msg.getId(), msg.getData()); + getUhidManager().open(msg.getId(), msg.getText(), msg.getData()); break; case ControlMessage.TYPE_UHID_INPUT: getUhidManager().writeInput(msg.getId(), msg.getData()); diff --git a/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java b/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java index 408dbf5d..d8cfd81f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java @@ -1,6 +1,7 @@ package com.genymobile.scrcpy.control; import com.genymobile.scrcpy.util.Ln; +import com.genymobile.scrcpy.util.StringUtils; import android.os.Build; import android.os.HandlerThread; @@ -46,7 +47,7 @@ public final class UhidManager { } } - public void open(int id, byte[] reportDesc) throws IOException { + public void open(int id, String name, byte[] reportDesc) throws IOException { try { FileDescriptor fd = Os.open("/dev/uhid", OsConstants.O_RDWR, 0); try { @@ -56,7 +57,7 @@ public final class UhidManager { close(old); } - byte[] req = buildUhidCreate2Req(reportDesc); + byte[] req = buildUhidCreate2Req(name, reportDesc); Os.write(fd, req, 0, req.length); registerUhidListener(id, fd); @@ -146,7 +147,7 @@ public final class UhidManager { } } - private static byte[] buildUhidCreate2Req(byte[] reportDesc) { + private static byte[] buildUhidCreate2Req(String name, byte[] reportDesc) { /* * struct uhid_event { * uint32_t type; @@ -171,8 +172,14 @@ public final class UhidManager { byte[] empty = new byte[256]; ByteBuffer buf = ByteBuffer.allocate(280 + reportDesc.length).order(ByteOrder.nativeOrder()); buf.putInt(UHID_CREATE2); - buf.put("scrcpy".getBytes(StandardCharsets.US_ASCII)); - buf.put(empty, 0, 256 - "scrcpy".length()); + + String actualName = name.isEmpty() ? "scrcpy" : "scrcpy: " + name; + byte[] utf8Name = actualName.getBytes(StandardCharsets.UTF_8); + int len = StringUtils.getUtf8TruncationIndex(utf8Name, 127); + assert len <= 127; + buf.put(utf8Name, 0, len); + buf.put(empty, 0, 256 - len); + buf.putShort((short) reportDesc.length); buf.putShort(BUS_VIRTUAL); buf.putInt(0); // vendor id diff --git a/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java index 0fd5f0ac..f29be2f4 100644 --- a/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java @@ -324,8 +324,10 @@ public class ControlMessageReaderTest { DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_UHID_CREATE); dos.writeShort(42); // id + dos.writeByte(3); // name size + dos.write("ABC".getBytes(StandardCharsets.US_ASCII)); byte[] data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}; - dos.writeShort(data.length); // size + dos.writeShort(data.length); // report desc size dos.write(data); byte[] packet = bos.toByteArray(); @@ -335,6 +337,7 @@ public class ControlMessageReaderTest { ControlMessage event = reader.read(); Assert.assertEquals(ControlMessage.TYPE_UHID_CREATE, event.getType()); Assert.assertEquals(42, event.getId()); + Assert.assertEquals("ABC", event.getText()); Assert.assertArrayEquals(data, event.getData()); Assert.assertEquals(-1, bis.read()); // EOS From bf2b679e705ed0864e765b1de5f238799a09ece7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Sep 2024 23:08:08 +0200 Subject: [PATCH 1956/2244] Simplify UHID outputs routing There was a registration mechanism to listen to HID outputs with a specific HID id. However, the UHID gamepad processor handles several ids, so it cannot work. We could complexify the registration mechanism, but instead, directly dispatch to the expected processor based on the UHID id. Concretely, instead of passing a sc_uhid_devices instance to construct a sc_keyboard_uhid, so that it can register itself, construct the sc_uhid_devices with all the UHID instances (currently only sc_keyboard_uhid) so that it can dispatch HID outputs directly. PR #5270 --- app/src/receiver.c | 10 ++-------- app/src/scrcpy.c | 15 ++++++++++----- app/src/uhid/keyboard_uhid.c | 23 ++++++----------------- app/src/uhid/keyboard_uhid.h | 9 +++++---- app/src/uhid/uhid_output.c | 30 ++++++++++++++++-------------- app/src/uhid/uhid_output.h | 30 ++++++------------------------ 6 files changed, 45 insertions(+), 72 deletions(-) diff --git a/app/src/receiver.c b/app/src/receiver.c index 42682cb4..15cd05df 100644 --- a/app/src/receiver.c +++ b/app/src/receiver.c @@ -67,14 +67,8 @@ task_uhid_output(void *userdata) { struct sc_uhid_output_task_data *data = userdata; - struct sc_uhid_receiver *uhid_receiver = - sc_uhid_devices_get_receiver(data->uhid_devices, data->id); - if (uhid_receiver) { - uhid_receiver->ops->process_output(uhid_receiver, data->data, - data->size); - } else { - LOGW("No UHID receiver for id %" PRIu16, data->id); - } + sc_uhid_devices_process_hid_output(data->uhid_devices, data->id, data->data, + data->size); free(data->data); free(data); diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 71e64344..687a9f34 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -402,7 +402,6 @@ scrcpy(struct scrcpy_options *options) { bool timeout_started = false; struct sc_acksync *acksync = NULL; - struct sc_uhid_devices *uhid_devices = NULL; uint32_t scid = scrcpy_generate_scid(); @@ -725,6 +724,8 @@ aoa_complete: assert(options->mouse_input_mode != SC_MOUSE_INPUT_MODE_AOA); #endif + struct sc_keyboard_uhid *uhid_keyboard = NULL; + if (options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_SDK) { sc_keyboard_sdk_init(&s->keyboard_sdk, &s->controller, options->key_inject_mode, @@ -732,14 +733,12 @@ aoa_complete: kp = &s->keyboard_sdk.key_processor; } else if (options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_UHID) { - sc_uhid_devices_init(&s->uhid_devices); - bool ok = sc_keyboard_uhid_init(&s->keyboard_uhid, &s->controller, - &s->uhid_devices); + bool ok = sc_keyboard_uhid_init(&s->keyboard_uhid, &s->controller); if (!ok) { goto end; } - uhid_devices = &s->uhid_devices; kp = &s->keyboard_uhid.key_processor; + uhid_keyboard = &s->keyboard_uhid; } if (options->mouse_input_mode == SC_MOUSE_INPUT_MODE_SDK) { @@ -759,6 +758,12 @@ aoa_complete: gp = &s->gamepad_uhid.gamepad_processor; } + struct sc_uhid_devices *uhid_devices = NULL; + if (uhid_keyboard) { + sc_uhid_devices_init(&s->uhid_devices, uhid_keyboard); + uhid_devices = &s->uhid_devices; + } + sc_controller_configure(&s->controller, acksync, uhid_devices); if (!sc_controller_start(&s->controller)) { diff --git a/app/src/uhid/keyboard_uhid.c b/app/src/uhid/keyboard_uhid.c index 9fdf4def..496da23d 100644 --- a/app/src/uhid/keyboard_uhid.c +++ b/app/src/uhid/keyboard_uhid.c @@ -95,21 +95,19 @@ sc_keyboard_uhid_to_sc_mod(uint8_t hid_led) { return mod; } -static void -sc_uhid_receiver_process_output(struct sc_uhid_receiver *receiver, - const uint8_t *data, size_t len) { +void +sc_keyboard_uhid_process_hid_output(struct sc_keyboard_uhid *kb, + const uint8_t *data, size_t size) { assert(sc_thread_get_id() == SC_MAIN_THREAD_ID); - assert(len); + assert(size); // Also check at runtime (do not trust the server) - if (!len) { + if (!size) { LOGE("Unexpected empty HID output message"); return; } - struct sc_keyboard_uhid *kb = DOWNCAST_RECEIVER(receiver); - uint8_t hid_led = data[0]; uint16_t device_mod = sc_keyboard_uhid_to_sc_mod(hid_led); kb->device_mod = device_mod; @@ -117,8 +115,7 @@ sc_uhid_receiver_process_output(struct sc_uhid_receiver *receiver, bool sc_keyboard_uhid_init(struct sc_keyboard_uhid *kb, - struct sc_controller *controller, - struct sc_uhid_devices *uhid_devices) { + struct sc_controller *controller) { sc_hid_keyboard_init(&kb->hid); kb->controller = controller; @@ -137,14 +134,6 @@ sc_keyboard_uhid_init(struct sc_keyboard_uhid *kb, kb->key_processor.hid = true; kb->key_processor.ops = &ops; - static const struct sc_uhid_receiver_ops uhid_receiver_ops = { - .process_output = sc_uhid_receiver_process_output, - }; - - kb->uhid_receiver.id = SC_HID_ID_KEYBOARD; - kb->uhid_receiver.ops = &uhid_receiver_ops; - sc_uhid_devices_add_receiver(uhid_devices, &kb->uhid_receiver); - struct sc_hid_open hid_open; sc_hid_keyboard_generate_open(&hid_open); assert(hid_open.hid_id == SC_HID_ID_KEYBOARD); diff --git a/app/src/uhid/keyboard_uhid.h b/app/src/uhid/keyboard_uhid.h index 639a3384..1628a678 100644 --- a/app/src/uhid/keyboard_uhid.h +++ b/app/src/uhid/keyboard_uhid.h @@ -7,12 +7,10 @@ #include "controller.h" #include "hid/hid_keyboard.h" -#include "uhid/uhid_output.h" #include "trait/key_processor.h" struct sc_keyboard_uhid { struct sc_key_processor key_processor; // key processor trait - struct sc_uhid_receiver uhid_receiver; struct sc_hid_keyboard hid; struct sc_controller *controller; @@ -21,7 +19,10 @@ struct sc_keyboard_uhid { bool sc_keyboard_uhid_init(struct sc_keyboard_uhid *kb, - struct sc_controller *controller, - struct sc_uhid_devices *uhid_devices); + struct sc_controller *controller); + +void +sc_keyboard_uhid_process_hid_output(struct sc_keyboard_uhid *kb, + const uint8_t *data, size_t size); #endif diff --git a/app/src/uhid/uhid_output.c b/app/src/uhid/uhid_output.c index 3b095faf..05e691da 100644 --- a/app/src/uhid/uhid_output.c +++ b/app/src/uhid/uhid_output.c @@ -1,25 +1,27 @@ #include "uhid_output.h" #include +#include + +#include "uhid/keyboard_uhid.h" +#include "util/log.h" void -sc_uhid_devices_init(struct sc_uhid_devices *devices) { - devices->count = 0; +sc_uhid_devices_init(struct sc_uhid_devices *devices, + struct sc_keyboard_uhid *keyboard) { + devices->keyboard = keyboard; } void -sc_uhid_devices_add_receiver(struct sc_uhid_devices *devices, - struct sc_uhid_receiver *receiver) { - assert(devices->count < SC_UHID_MAX_RECEIVERS); - devices->receivers[devices->count++] = receiver; -} - -struct sc_uhid_receiver * -sc_uhid_devices_get_receiver(struct sc_uhid_devices *devices, uint16_t id) { - for (size_t i = 0; i < devices->count; ++i) { - if (devices->receivers[i]->id == id) { - return devices->receivers[i]; +sc_uhid_devices_process_hid_output(struct sc_uhid_devices *devices, uint16_t id, + const uint8_t *data, size_t size) { + if (id == SC_HID_ID_KEYBOARD) { + if (devices->keyboard) { + sc_keyboard_uhid_process_hid_output(devices->keyboard, data, size); + } else { + LOGW("Unexpected keyboard HID output without UHID keyboard"); } + } else { + LOGW("HID output ignored for id %" PRIu16, id); } - return NULL; } diff --git a/app/src/uhid/uhid_output.h b/app/src/uhid/uhid_output.h index e13eed87..cd6a800f 100644 --- a/app/src/uhid/uhid_output.h +++ b/app/src/uhid/uhid_output.h @@ -9,37 +9,19 @@ /** * The communication with UHID devices is bidirectional. * - * This component manages the registration of receivers to handle UHID output - * messages (sent from the device to the computer). + * This component dispatches HID outputs to the expected processor. */ -struct sc_uhid_receiver { - uint16_t id; - - const struct sc_uhid_receiver_ops *ops; -}; - -struct sc_uhid_receiver_ops { - void - (*process_output)(struct sc_uhid_receiver *receiver, - const uint8_t *data, size_t len); -}; - -#define SC_UHID_MAX_RECEIVERS 1 - struct sc_uhid_devices { - struct sc_uhid_receiver *receivers[SC_UHID_MAX_RECEIVERS]; - unsigned count; + struct sc_keyboard_uhid *keyboard; }; void -sc_uhid_devices_init(struct sc_uhid_devices *devices); +sc_uhid_devices_init(struct sc_uhid_devices *devices, + struct sc_keyboard_uhid *keyboard); void -sc_uhid_devices_add_receiver(struct sc_uhid_devices *devices, - struct sc_uhid_receiver *receiver); - -struct sc_uhid_receiver * -sc_uhid_devices_get_receiver(struct sc_uhid_devices *devices, uint16_t id); +sc_uhid_devices_process_hid_output(struct sc_uhid_devices *devices, uint16_t id, + const uint8_t *data, size_t size); #endif From 9f3d51106d026e9f234477d7600fcb6162edf4bc Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 13 Sep 2024 21:58:11 +0200 Subject: [PATCH 1957/2244] Remove fragile assert() The sc_uhid_devices instance is initialized only when there is a UHID keyboard. The device message receiver assumed that it could not receive HID output reports without a sc_uhid_devices instance (i.e. without a UHID keyboard), but in practice, a UHID driver implementation on the device may decide to send UHID output reports for mouse or for gamepads (and we must just ignore them). So remove the assert(). PR #5270 --- app/src/receiver.c | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/src/receiver.c b/app/src/receiver.c index 15cd05df..b89b0c6e 100644 --- a/app/src/receiver.c +++ b/app/src/receiver.c @@ -121,11 +121,6 @@ process_msg(struct sc_receiver *receiver, struct sc_device_msg *msg) { } } - // This is a programming error to receive this message if there is - // no uhid_devices instance - assert(receiver->uhid_devices); - - // Also check at runtime (do not trust the server) if (!receiver->uhid_devices) { LOGE("Received unexpected HID output message"); sc_device_msg_destroy(msg); From 91d40c75483592eae78c5702c4e7278370cd06c6 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 9 Sep 2024 18:24:29 +0200 Subject: [PATCH 1958/2244] Fix link in OTG documentation PR #5270 --- doc/otg.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/otg.md b/doc/otg.md index 5f42ac9c..93002f14 100644 --- a/doc/otg.md +++ b/doc/otg.md @@ -6,7 +6,7 @@ was a [physical keyboard] and/or a [physical mouse] connected to the Android device (see [keyboard](keyboard.md) and [mouse](mouse.md)). [physical keyboard]: keyboard.md#physical-keyboard-simulation -[physical mouse]: physical-keyboard-simulation +[physical mouse]: mouse.md#physical-mouse-simulation A special mode (OTG) allows to control the device using AOA [keyboard](keyboard.md#aoa) and [mouse](mouse.md#aoa), without using _adb_ at From 0ba430a462f778d1dc9de1a82b527f1570cc8cf3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 9 Sep 2024 18:25:50 +0200 Subject: [PATCH 1959/2244] Add gamepad user documentation Mainly copied and adapted from HID keyboard and mouse documentation. PR #5270 --- README.md | 9 +++++++++ doc/gamepad.md | 53 ++++++++++++++++++++++++++++++++++++++++++++++++++ doc/otg.md | 27 ++++++++++++++++--------- 3 files changed, 80 insertions(+), 9 deletions(-) create mode 100644 doc/gamepad.md diff --git a/README.md b/README.md index 67fdf364..0d44228e 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ Its features include: - [camera mirroring](doc/camera.md) (Android 12+) - [mirroring as a webcam (V4L2)](doc/v4l2.md) (Linux-only) - physical [keyboard][hid-keyboard] and [mouse][hid-mouse] simulation (HID) + - [gamepad](doc/gamepad.md) support - [OTG mode](doc/otg.md) - and more… @@ -111,6 +112,13 @@ Here are just some common examples. scrcpy --otg ``` + - Control the device using gamepad controllers plugged into the computer: + + ```bash + scrcpy --gamepad=uhid + scrcpy -G # short version + ``` + ## User documentation The application provides a lot of features and configuration options. They are @@ -122,6 +130,7 @@ documented in the following pages: - [Control](doc/control.md) - [Keyboard](doc/keyboard.md) - [Mouse](doc/mouse.md) + - [Gamepad](doc/gamepad.md) - [Device](doc/device.md) - [Window](doc/window.md) - [Recording](doc/recording.md) diff --git a/doc/gamepad.md b/doc/gamepad.md new file mode 100644 index 00000000..f78fb828 --- /dev/null +++ b/doc/gamepad.md @@ -0,0 +1,53 @@ +# Gamepad + +Several gamepad input modes are available: + + - `--gamepad=disabled` (default) + - `--gamepad=uhid` (or `-G`): simulates physical HID gamepads using the UHID + kernel module on the device + - `--gamepad=aoa`: simulates physical HID gamepads using the AOAv2 protocol + + +## Physical gamepad simulation + +Two modes allow to simulate physical HID gamepads on the device, one for each +physical gamepad plugged into the computer. + + +### UHID + +This mode simulates physical HID gamepads using the [UHID] kernel module on the +device. + +[UHID]: https://kernel.org/doc/Documentation/hid/uhid.txt + +To enable UHID gamepads, use: + +```bash +scrcpy --gamepad=uhid +scrcpy -G # short version +``` + + +### AOA + +This mode simulates physical HID gamepads using the [AOAv2] protocol. + +[AOAv2]: https://source.android.com/devices/accessories/aoa2#hid-support + +To enable AOA gamepads, use: + +```bash +scrcpy --gamepad=aoa +``` + +Contrary to the other mode, it works at the USB level directly (so it only works +over USB). + +It does not use the scrcpy server, and does not require `adb` (USB debugging). +Therefore, it is possible to control the device (but not mirror) even with USB +debugging disabled (see [OTG](otg.md)). + +Note: On Windows, it may only work in [OTG mode](otg.md), not while mirroring +(it is not possible to open a USB device if it is already open by another +process like the _adb daemon_). diff --git a/doc/otg.md b/doc/otg.md index 93002f14..7d31c0a7 100644 --- a/doc/otg.md +++ b/doc/otg.md @@ -9,13 +9,15 @@ device (see [keyboard](keyboard.md) and [mouse](mouse.md)). [physical mouse]: mouse.md#physical-mouse-simulation A special mode (OTG) allows to control the device using AOA -[keyboard](keyboard.md#aoa) and [mouse](mouse.md#aoa), without using _adb_ at -all (so USB debugging is not necessary). In this mode, video and audio are -disabled, and `--keyboard=aoa and `--mouse=aoa` are implicitly set. +[keyboard](keyboard.md#aoa), [mouse](mouse.md#aoa) and +[gamepad](gamepad.md#aoa), without using _adb_ at all (so USB debugging is not +necessary). In this mode, video and audio are disabled, and `--keyboard=aoa` and +`--mouse=aoa` are implicitly set. However, gamepads are disabled by default, so +`--gamepad=aoa` (or `-G` in OTG mode) must be explicitly set. -Therefore, it is possible to run _scrcpy_ with only physical keyboard and mouse -simulation, as if the computer keyboard and mouse were plugged directly to the -device via an OTG cable. +Therefore, it is possible to run _scrcpy_ with only physical keyboard, mouse and +gamepad simulation, as if the computer keyboard, mouse and gamepads were plugged +directly to the device via an OTG cable. To enable OTG mode: @@ -32,6 +34,13 @@ scrcpy --otg --keyboard=disabled scrcpy --otg --mouse=disabled ``` +and to enable gamepads: + +```bash +scrcpy --otg --gamepad=aoa +scrcpy --otg -G # short version +``` + It only works if the device is connected over USB. ## OTG issues on Windows @@ -50,9 +59,9 @@ is enabled, then OTG mode is not necessary. Instead, disable video and audio, and select UHID (or AOA): ```bash -scrcpy --no-video --no-audio --keyboard=uhid --mouse=uhid -scrcpy --no-video --no-audio -KM # short version -scrcpy --no-video --no-audio --keyboard=aoa --mouse=aoa +scrcpy --no-video --no-audio --keyboard=uhid --mouse=uhid --gamepad=uhid +scrcpy --no-video --no-audio -KMG # short version +scrcpy --no-video --no-audio --keyboard=aoa --mouse=aoa --gamepad=aoa ``` One benefit of UHID is that it also works wirelessly. From f01a622eadebb66e2fe0da22e24e71679e20bcb5 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 10 Sep 2024 09:16:05 +0200 Subject: [PATCH 1960/2244] Enable joystick events in background Capture the gamepads even when the window is not focused. Note: In theory, with this flag set, we could capture gamepad events even without a window (--no-window). In practice, scrcpy still requires a window, because --no-window implies --no-control, and the input manager is owned by the sc_screen instance, which does not exist if there is no window. Supporting this use case would require a lot of refactors. Refs PR #5270 Suggested-by: Luiz Henrique Laurini --- app/src/scrcpy.c | 4 ++++ app/src/usb/scrcpy_otg.c | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 687a9f34..854657fb 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -136,6 +136,10 @@ sdl_set_hints(const char *render_driver) { if (!SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "0")) { LOGW("Could not disable minimize on focus loss"); } + + if (!SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1")) { + LOGW("Could not allow joystick background events"); + } } static void diff --git a/app/src/usb/scrcpy_otg.c b/app/src/usb/scrcpy_otg.c index 47afd9d0..9595face 100644 --- a/app/src/usb/scrcpy_otg.c +++ b/app/src/usb/scrcpy_otg.c @@ -58,6 +58,10 @@ scrcpy_otg(struct scrcpy_options *options) { LOGW("Could not enable linear filtering"); } + if (!SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1")) { + LOGW("Could not allow joystick background events"); + } + // Minimal SDL initialization if (SDL_Init(SDL_INIT_EVENTS)) { LOGE("Could not initialize SDL: %s", SDL_GetError()); From befc0fac5b0f3c819ba798fc979f1a946a21976c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 15 Sep 2024 18:32:10 +0200 Subject: [PATCH 1961/2244] Mention UHID permission errors UHID may not work on old Android versions due to permission errors. Mention it in UHID mouse and gamepad documentation (it was already mentioned for UHID keyboard). Refs #4473 comment PR #5270 --- doc/gamepad.md | 2 ++ doc/mouse.md | 2 ++ 2 files changed, 4 insertions(+) diff --git a/doc/gamepad.md b/doc/gamepad.md index f78fb828..607bb935 100644 --- a/doc/gamepad.md +++ b/doc/gamepad.md @@ -28,6 +28,8 @@ scrcpy --gamepad=uhid scrcpy -G # short version ``` +Note: UHID may not work on old Android versions due to permission errors. + ### AOA diff --git a/doc/mouse.md b/doc/mouse.md index ec4aea63..ae7c6834 100644 --- a/doc/mouse.md +++ b/doc/mouse.md @@ -53,6 +53,8 @@ scrcpy --mouse=uhid scrcpy -M # short version ``` +Note: UHID may not work on old Android versions due to permission errors. + ### AOA From 4cc4abdcc8b3f0d76a362f450db4cefe96edca5d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 15 Sep 2024 18:35:22 +0200 Subject: [PATCH 1962/2244] Mention issue with AOA and multiple gamepads Android does not support multiple HID gamepads properly over AOA. PR #5270 --- doc/gamepad.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/gamepad.md b/doc/gamepad.md index 607bb935..d3d27b51 100644 --- a/doc/gamepad.md +++ b/doc/gamepad.md @@ -50,6 +50,9 @@ It does not use the scrcpy server, and does not require `adb` (USB debugging). Therefore, it is possible to control the device (but not mirror) even with USB debugging disabled (see [OTG](otg.md)). +Note: For some reason, in this mode, Android detects multiple physical gamepads +as a single misbehaving one. Use UHID if you need multiple gamepads. + Note: On Windows, it may only work in [OTG mode](otg.md), not while mirroring (it is not possible to open a USB device if it is already open by another process like the _adb daemon_). From 337901368e7bd143c68052d4a92e809fecdfe5a7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 15 Sep 2024 14:40:52 +0200 Subject: [PATCH 1963/2244] Upgrade SDL (2.30.7) for Windows --- app/deps/sdl.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/deps/sdl.sh b/app/deps/sdl.sh index 0a42bc1f..c8b62746 100755 --- a/app/deps/sdl.sh +++ b/app/deps/sdl.sh @@ -4,10 +4,10 @@ DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) cd "$DEPS_DIR" . common -VERSION=2.30.5 +VERSION=2.30.7 FILENAME=SDL-$VERSION.tar.gz PROJECT_DIR=SDL-release-$VERSION -SHA256SUM=be3ca88f8c362704627a0bc5406edb2cd6cc6ba463596d81ebb7c2f18763d3bf +SHA256SUM=1578c96f62c9ae36b64e431b2aa0e0b0fd07c275dedbc694afc38e19056688f5 cd "$SOURCES_DIR" From 6d23a389cab2d8678627d6743223506095729ff2 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 15 Sep 2024 18:54:37 +0200 Subject: [PATCH 1964/2244] Upgrade FFmpeg (7.0.2) for Windows --- app/deps/ffmpeg.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/deps/ffmpeg.sh b/app/deps/ffmpeg.sh index ef92d4a5..89431542 100755 --- a/app/deps/ffmpeg.sh +++ b/app/deps/ffmpeg.sh @@ -4,10 +4,10 @@ DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) cd "$DEPS_DIR" . common -VERSION=7.0.1 +VERSION=7.0.2 FILENAME=ffmpeg-$VERSION.tar.xz PROJECT_DIR=ffmpeg-$VERSION -SHA256SUM=bce9eeb0f17ef8982390b1f37711a61b4290dc8c2a0c1a37b5857e85bfb0e4ff +SHA256SUM=8646515b638a3ad303e23af6a3587734447cb8fc0a0c064ecdb8e95c4fd8b389 cd "$SOURCES_DIR" From 292adf294dd654e79b42215fb6bc2c6d2d44165b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 15 Sep 2024 18:59:27 +0200 Subject: [PATCH 1965/2244] Bump version to 2.7 --- app/scrcpy-windows.rc | 2 +- meson.build | 2 +- server/build.gradle | 4 ++-- server/build_without_gradle.sh | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/scrcpy-windows.rc b/app/scrcpy-windows.rc index 9e0d90c2..6454c88e 100644 --- a/app/scrcpy-windows.rc +++ b/app/scrcpy-windows.rc @@ -13,7 +13,7 @@ BEGIN VALUE "LegalCopyright", "Romain Vimont, Genymobile" VALUE "OriginalFilename", "scrcpy.exe" VALUE "ProductName", "scrcpy" - VALUE "ProductVersion", "2.6.1" + VALUE "ProductVersion", "2.7" END END BLOCK "VarFileInfo" diff --git a/meson.build b/meson.build index b532006a..f76d5ecf 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('scrcpy', 'c', - version: '2.6.1', + version: '2.7', meson_version: '>= 0.48', default_options: [ 'c_std=c11', diff --git a/server/build.gradle b/server/build.gradle index decacd3f..655298a9 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -7,8 +7,8 @@ android { applicationId "com.genymobile.scrcpy" minSdkVersion 21 targetSdkVersion 34 - versionCode 20601 - versionName "2.6.1" + versionCode 20700 + versionName "2.7" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index 5ee7af30..ab6c821d 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -12,7 +12,7 @@ set -e SCRCPY_DEBUG=false -SCRCPY_VERSION_NAME=2.6.1 +SCRCPY_VERSION_NAME=2.7 PLATFORM=${ANDROID_PLATFORM:-34} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-34.0.0} From 665ccb32f5306ebd866dc0d99f4d08ed2aeb91c3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 15 Sep 2024 21:15:26 +0200 Subject: [PATCH 1966/2244] Update links to 2.7 --- README.md | 2 +- doc/build.md | 6 +++--- doc/windows.md | 12 ++++++------ install_release.sh | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 0d44228e..44f3d740 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ source for the project. Do not download releases from random websites, even if their name contains `scrcpy`.** -# scrcpy (v2.6.1) +# scrcpy (v2.7) scrcpy diff --git a/doc/build.md b/doc/build.md index 15e0ffff..63bd7ca7 100644 --- a/doc/build.md +++ b/doc/build.md @@ -233,10 +233,10 @@ install` must be run as root)._ #### Option 2: Use prebuilt server - - [`scrcpy-server-v2.6.1`][direct-scrcpy-server] - SHA-256: `ca7ab50b2e25a0e5af7599c30383e365983fa5b808e65ce2e1c1bba5bfe8dc3b` + - [`scrcpy-server-v2.7`][direct-scrcpy-server] + SHA-256: `a23c5659f36c260f105c022d27bcb3eafffa26070e7baa9eda66d01377a1adba` -[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v2.6.1/scrcpy-server-v2.6.1 +[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v2.7/scrcpy-server-v2.7 Download the prebuilt server somewhere, and specify its path during the Meson configuration: diff --git a/doc/windows.md b/doc/windows.md index 65ec2b45..36e59178 100644 --- a/doc/windows.md +++ b/doc/windows.md @@ -4,14 +4,14 @@ Download the [latest release]: - - [`scrcpy-win64-v2.6.1.zip`][direct-win64] (64-bit) - SHA-256: `041fc3abf8578ddcead5a8c4a8be8960b7c4d45b21d3370ee2683605e86a728c` - - [`scrcpy-win32-v2.6.1.zip`][direct-win32] (32-bit) - SHA-256: `17a5d4d17230b4c90fad45af6395efda9aea287a03c04e6b4ecc9ceb8134ea04` + - [`scrcpy-win64-v2.7.zip`][direct-win64] (64-bit) + SHA-256: `5910bc18d5a16f42d84185ddc7e16a4cee6a6f5f33451559c1a1d6d0099bd5f5` + - [`scrcpy-win32-v2.7.zip`][direct-win32] (32-bit) + SHA-256: `ef4daf89d500f33d78b830625536ecb18481429dd94433e7634c824292059d06` [latest release]: https://github.com/Genymobile/scrcpy/releases/latest -[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v2.6.1/scrcpy-win64-v2.6.1.zip -[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v2.6.1/scrcpy-win32-v2.6.1.zip +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v2.7/scrcpy-win64-v2.7.zip +[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v2.7/scrcpy-win32-v2.7.zip and extract it. diff --git a/install_release.sh b/install_release.sh index 2aad8cdc..3cf3490c 100755 --- a/install_release.sh +++ b/install_release.sh @@ -2,8 +2,8 @@ set -e BUILDDIR=build-auto -PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v2.6.1/scrcpy-server-v2.6.1 -PREBUILT_SERVER_SHA256=ca7ab50b2e25a0e5af7599c30383e365983fa5b808e65ce2e1c1bba5bfe8dc3b +PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v2.7/scrcpy-server-v2.7 +PREBUILT_SERVER_SHA256=a23c5659f36c260f105c022d27bcb3eafffa26070e7baa9eda66d01377a1adba echo "[scrcpy] Downloading prebuilt server..." wget "$PREBUILT_SERVER_URL" -O scrcpy-server From f69ac405340efb0b53849b96a32f389cf1c1d54e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 20 Sep 2024 08:43:42 +0200 Subject: [PATCH 1967/2244] Reorganize server imports Moving classes into subpackages changed the expected imports order. Reorganize them all at once automatically to avoid spurious changes in future commits. --- .../main/java/com/genymobile/scrcpy/audio/AudioEncoder.java | 4 ++-- .../java/com/genymobile/scrcpy/audio/AudioRawRecorder.java | 2 +- .../com/genymobile/scrcpy/control/ControlMessageReader.java | 2 +- .../main/java/com/genymobile/scrcpy/control/Controller.java | 2 +- .../java/com/genymobile/scrcpy/video/CameraCapture.java | 2 +- .../java/com/genymobile/scrcpy/video/ScreenCapture.java | 2 +- .../main/java/com/genymobile/scrcpy/video/ScreenInfo.java | 2 +- .../java/com/genymobile/scrcpy/video/SurfaceEncoder.java | 6 +++--- .../java/com/genymobile/scrcpy/wrappers/DisplayManager.java | 4 ++-- 9 files changed, 13 insertions(+), 13 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java index 8230e054..672403b8 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java @@ -1,14 +1,14 @@ package com.genymobile.scrcpy.audio; import com.genymobile.scrcpy.AsyncProcessor; +import com.genymobile.scrcpy.device.ConfigurationException; +import com.genymobile.scrcpy.device.Streamer; import com.genymobile.scrcpy.util.Codec; import com.genymobile.scrcpy.util.CodecOption; import com.genymobile.scrcpy.util.CodecUtils; -import com.genymobile.scrcpy.device.ConfigurationException; import com.genymobile.scrcpy.util.IO; import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.LogUtils; -import com.genymobile.scrcpy.device.Streamer; import android.annotation.TargetApi; import android.media.MediaCodec; diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioRawRecorder.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioRawRecorder.java index 323caae4..3924c205 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioRawRecorder.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioRawRecorder.java @@ -1,9 +1,9 @@ package com.genymobile.scrcpy.audio; import com.genymobile.scrcpy.AsyncProcessor; +import com.genymobile.scrcpy.device.Streamer; import com.genymobile.scrcpy.util.IO; import com.genymobile.scrcpy.util.Ln; -import com.genymobile.scrcpy.device.Streamer; import android.media.MediaCodec; import android.os.Build; diff --git a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java index 45116935..17e121c2 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java @@ -1,7 +1,7 @@ package com.genymobile.scrcpy.control; -import com.genymobile.scrcpy.util.Binary; import com.genymobile.scrcpy.device.Position; +import com.genymobile.scrcpy.util.Binary; import java.io.BufferedInputStream; import java.io.DataInputStream; diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index 38251655..b445427d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -3,9 +3,9 @@ package com.genymobile.scrcpy.control; import com.genymobile.scrcpy.AsyncProcessor; import com.genymobile.scrcpy.CleanUp; import com.genymobile.scrcpy.device.Device; -import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.device.Point; import com.genymobile.scrcpy.device.Position; +import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.wrappers.InputManager; import com.genymobile.scrcpy.wrappers.ServiceManager; diff --git a/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java index 7d2e2055..3b8fc59b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java @@ -1,8 +1,8 @@ package com.genymobile.scrcpy.video; +import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.util.HandlerExecutor; import com.genymobile.scrcpy.util.Ln; -import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.wrappers.ServiceManager; import android.annotation.SuppressLint; diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java index fbeca2af..62afb263 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java @@ -1,8 +1,8 @@ package com.genymobile.scrcpy.video; import com.genymobile.scrcpy.device.Device; -import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.device.Size; +import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.wrappers.ServiceManager; import com.genymobile.scrcpy.wrappers.SurfaceControl; diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java index ba537b17..bd0a3b62 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java @@ -2,8 +2,8 @@ package com.genymobile.scrcpy.video; import com.genymobile.scrcpy.BuildConfig; import com.genymobile.scrcpy.device.Device; -import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.device.Size; +import com.genymobile.scrcpy.util.Ln; import android.graphics.Rect; diff --git a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java index a5f2d1e9..7800e4bb 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java @@ -1,15 +1,15 @@ package com.genymobile.scrcpy.video; import com.genymobile.scrcpy.AsyncProcessor; +import com.genymobile.scrcpy.device.ConfigurationException; +import com.genymobile.scrcpy.device.Size; +import com.genymobile.scrcpy.device.Streamer; import com.genymobile.scrcpy.util.Codec; import com.genymobile.scrcpy.util.CodecOption; import com.genymobile.scrcpy.util.CodecUtils; -import com.genymobile.scrcpy.device.ConfigurationException; import com.genymobile.scrcpy.util.IO; import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.LogUtils; -import com.genymobile.scrcpy.device.Size; -import com.genymobile.scrcpy.device.Streamer; import android.media.MediaCodec; import android.media.MediaCodecInfo; 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 dd92330c..00a39274 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java @@ -1,9 +1,9 @@ package com.genymobile.scrcpy.wrappers; -import com.genymobile.scrcpy.util.Command; import com.genymobile.scrcpy.device.DisplayInfo; -import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.device.Size; +import com.genymobile.scrcpy.util.Command; +import com.genymobile.scrcpy.util.Ln; import android.annotation.SuppressLint; import android.hardware.display.VirtualDisplay; From 0cc6f6aa09f0fe5913ec66276e7ea3681fa81cd7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 20 Sep 2024 08:17:48 +0200 Subject: [PATCH 1968/2244] Detect codec/encoder mismatch Fail with an explicit error when the requested encoder does not match the requested codec. Refs #5066 --- .../java/com/genymobile/scrcpy/audio/AudioEncoder.java | 8 +++++++- .../src/main/java/com/genymobile/scrcpy/util/Codec.java | 7 +++++++ .../java/com/genymobile/scrcpy/video/SurfaceEncoder.java | 8 +++++++- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java index 672403b8..f462431a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java @@ -287,7 +287,13 @@ public final class AudioEncoder implements AsyncProcessor { if (encoderName != null) { Ln.d("Creating audio encoder by name: '" + encoderName + "'"); try { - return MediaCodec.createByCodecName(encoderName); + MediaCodec mediaCodec = MediaCodec.createByCodecName(encoderName); + String mimeType = Codec.getMimeType(mediaCodec); + if (!codec.getMimeType().equals(mimeType)) { + Ln.e("Audio encoder type for \"" + encoderName + "\" (" + mimeType + ") does not match codec type (" + codec.getMimeType() + ")"); + throw new ConfigurationException("Incorrect encoder type: " + encoderName); + } + return mediaCodec; } catch (IllegalArgumentException e) { Ln.e("Audio encoder '" + encoderName + "' for " + codec.getName() + " not found\n" + LogUtils.buildAudioEncoderListMessage()); throw new ConfigurationException("Unknown encoder: " + encoderName); diff --git a/server/src/main/java/com/genymobile/scrcpy/util/Codec.java b/server/src/main/java/com/genymobile/scrcpy/util/Codec.java index a363bd8b..b350409b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/util/Codec.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/Codec.java @@ -1,5 +1,7 @@ package com.genymobile.scrcpy.util; +import android.media.MediaCodec; + public interface Codec { enum Type { @@ -14,4 +16,9 @@ public interface Codec { String getName(); String getMimeType(); + + static String getMimeType(MediaCodec codec) { + String[] types = codec.getCodecInfo().getSupportedTypes(); + return types.length > 0 ? types[0] : null; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java index 7800e4bb..41c38642 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java @@ -205,7 +205,13 @@ public class SurfaceEncoder implements AsyncProcessor { if (encoderName != null) { Ln.d("Creating encoder by name: '" + encoderName + "'"); try { - return MediaCodec.createByCodecName(encoderName); + MediaCodec mediaCodec = MediaCodec.createByCodecName(encoderName); + String mimeType = Codec.getMimeType(mediaCodec); + if (!codec.getMimeType().equals(mimeType)) { + Ln.e("Video encoder type for \"" + encoderName + "\" (" + mimeType + ") does not match codec type (" + codec.getMimeType() + ")"); + throw new ConfigurationException("Incorrect encoder type: " + encoderName); + } + return mediaCodec; } catch (IllegalArgumentException e) { Ln.e("Video encoder '" + encoderName + "' for " + codec.getName() + " not found\n" + LogUtils.buildVideoEncoderListMessage()); throw new ConfigurationException("Unknown encoder: " + encoderName); From a7e61fb8712316e0375bf156f3243d7ced333e10 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 23 Sep 2024 22:28:04 +0200 Subject: [PATCH 1969/2244] Remove unused audio player callbacks The callbacks were never used: the player can report errors directly from sc_audio_player_frame_sink_push(). --- app/src/audio_player.h | 7 ------- 1 file changed, 7 deletions(-) diff --git a/app/src/audio_player.h b/app/src/audio_player.h index 0c677363..3d468999 100644 --- a/app/src/audio_player.h +++ b/app/src/audio_player.h @@ -68,13 +68,6 @@ struct sc_audio_player { // Set to true the first time the SDL callback is called atomic_bool played; - - const struct sc_audio_player_callbacks *cbs; - void *cbs_userdata; -}; - -struct sc_audio_player_callbacks { - void (*on_ended)(struct sc_audio_player *ap, bool success, void *userdata); }; void From 2e7a15a9987615b10460e7717f3a0dd31774936e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 23 Sep 2024 22:32:02 +0200 Subject: [PATCH 1970/2244] Remove unused audio player fields They are only used locally. --- app/src/audio_player.c | 4 +--- app/src/audio_player.h | 4 ---- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/app/src/audio_player.c b/app/src/audio_player.c index 274b6948..24144483 100644 --- a/app/src/audio_player.c +++ b/app/src/audio_player.c @@ -351,8 +351,6 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink, assert(out_bytes_per_sample > 0); ap->sample_rate = ctx->sample_rate; - ap->nb_channels = nb_channels; - ap->out_bytes_per_sample = out_bytes_per_sample; ap->target_buffering = ap->target_buffering_delay * ap->sample_rate / SC_TICK_FREQ; @@ -413,7 +411,7 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink, // without locking. uint32_t audiobuf_samples = ap->target_buffering + ap->sample_rate; - size_t sample_size = ap->nb_channels * ap->out_bytes_per_sample; + size_t sample_size = nb_channels * out_bytes_per_sample; bool ok = sc_audiobuf_init(&ap->buf, sample_size, audiobuf_samples); if (!ok) { goto error_free_swr_ctx; diff --git a/app/src/audio_player.h b/app/src/audio_player.h index 3d468999..e638e601 100644 --- a/app/src/audio_player.h +++ b/app/src/audio_player.h @@ -41,10 +41,6 @@ struct sc_audio_player { // The sample rate is the same for input and output unsigned sample_rate; - // The number of channels is the same for input and output - unsigned nb_channels; - // The number of bytes per sample for a single channel - size_t out_bytes_per_sample; // Target buffer for resampling (only used by the receiver thread) uint8_t *swr_buf; From 42fb947780e1054a72de7b4baf02f1a73beda3c6 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 23 Sep 2024 22:39:45 +0200 Subject: [PATCH 1971/2244] Use local mutex for audio player Replace SDL_LockAudioDevice() by a local mutex, to minimize the lock section and to make the code independent of SDL. --- app/src/audio_player.c | 29 +++++++++++++++++++++-------- app/src/audio_player.h | 2 ++ 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/app/src/audio_player.c b/app/src/audio_player.c index 24144483..fe007832 100644 --- a/app/src/audio_player.c +++ b/app/src/audio_player.c @@ -66,8 +66,6 @@ static void SDLCALL sc_audio_player_sdl_callback(void *userdata, uint8_t *stream, int len_int) { struct sc_audio_player *ap = userdata; - // This callback is called with the lock used by SDL_LockAudioDevice() - assert(len_int > 0); size_t len = len_int; uint32_t count = TO_SAMPLES(len); @@ -76,6 +74,10 @@ sc_audio_player_sdl_callback(void *userdata, uint8_t *stream, int len_int) { LOGD("[Audio] SDL callback requests %" PRIu32 " samples", count); #endif + // A lock is necessary in the rare case where the producer needs to drop + // samples already pushed (when the buffer is full) + sc_mutex_lock(&ap->mutex); + bool played = atomic_load_explicit(&ap->played, memory_order_relaxed); if (!played) { uint32_t buffered_samples = sc_audiobuf_can_read(&ap->buf); @@ -88,12 +90,15 @@ sc_audio_player_sdl_callback(void *userdata, uint8_t *stream, int len_int) { // whole buffer with silence (len is small compared to the // arbitrary margin value). memset(stream, 0, len); + sc_mutex_unlock(&ap->mutex); return; } } uint32_t read = sc_audiobuf_read(&ap->buf, stream, count); + sc_mutex_unlock(&ap->mutex); + if (read < count) { uint32_t silence = count - read; // Insert silence. In theory, the inserted silent samples replace the @@ -183,7 +188,7 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink, // All samples that could be written without locking have been written, // now we need to lock to drop/consume old samples - SDL_LockAudioDevice(ap->device); + sc_mutex_lock(&ap->mutex); // Retry with the lock written += sc_audiobuf_write(&ap->buf, @@ -196,7 +201,7 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink, assert(skipped_samples == remaining); } - SDL_UnlockAudioDevice(ap->device); + sc_mutex_unlock(&ap->mutex); if (written < samples) { // Now there is enough space @@ -229,7 +234,7 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink, if (can_read > max_buffered_samples) { uint32_t skip_samples = 0; - SDL_LockAudioDevice(ap->device); + sc_mutex_lock(&ap->mutex); can_read = sc_audiobuf_can_read(&ap->buf); if (can_read > max_buffered_samples) { skip_samples = can_read - max_buffered_samples; @@ -238,7 +243,7 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink, (void) r; skipped_samples += skip_samples; } - SDL_UnlockAudioDevice(ap->device); + sc_mutex_unlock(&ap->mutex); if (skip_samples) { if (played) { @@ -411,12 +416,17 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink, // without locking. uint32_t audiobuf_samples = ap->target_buffering + ap->sample_rate; - size_t sample_size = nb_channels * out_bytes_per_sample; - bool ok = sc_audiobuf_init(&ap->buf, sample_size, audiobuf_samples); + bool ok = sc_mutex_init(&ap->mutex); if (!ok) { goto error_free_swr_ctx; } + size_t sample_size = nb_channels * out_bytes_per_sample; + ok = sc_audiobuf_init(&ap->buf, sample_size, audiobuf_samples); + if (!ok) { + goto error_destroy_mutex; + } + size_t initial_swr_buf_size = TO_BYTES(4096); ap->swr_buf = malloc(initial_swr_buf_size); if (!ap->swr_buf) { @@ -450,6 +460,8 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink, error_destroy_audiobuf: sc_audiobuf_destroy(&ap->buf); +error_destroy_mutex: + sc_mutex_destroy(&ap->mutex); error_free_swr_ctx: swr_free(&ap->swr_ctx); error_close_audio_device: @@ -468,6 +480,7 @@ sc_audio_player_frame_sink_close(struct sc_frame_sink *sink) { free(ap->swr_buf); sc_audiobuf_destroy(&ap->buf); + sc_mutex_destroy(&ap->mutex); swr_free(&ap->swr_ctx); } diff --git a/app/src/audio_player.h b/app/src/audio_player.h index e638e601..7ebb43db 100644 --- a/app/src/audio_player.h +++ b/app/src/audio_player.h @@ -20,6 +20,8 @@ struct sc_audio_player { SDL_AudioDeviceID device; + sc_mutex mutex; + // The target buffering between the producer and the consumer. This value // is directly use for compensation. // Since audio capture and/or encoding on the device typically produce From 10f60054aca70047c128fc801d103dac4d5d7896 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 23 Sep 2024 23:11:03 +0200 Subject: [PATCH 1972/2244] Use exact-width integer types --- app/src/audio_player.c | 8 ++++---- app/src/audio_player.h | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/audio_player.c b/app/src/audio_player.c index fe007832..d72dac25 100644 --- a/app/src/audio_player.c +++ b/app/src/audio_player.c @@ -342,12 +342,12 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink, const AVCodecContext *ctx) { struct sc_audio_player *ap = DOWNCAST(sink); #ifdef SCRCPY_LAVU_HAS_CHLAYOUT - assert(ctx->ch_layout.nb_channels > 0); - unsigned nb_channels = ctx->ch_layout.nb_channels; + assert(ctx->ch_layout.nb_channels > 0 && ctx->ch_layout.nb_channels < 256); + uint8_t nb_channels = ctx->ch_layout.nb_channels; #else int tmp = av_get_channel_layout_nb_channels(ctx->channel_layout); - assert(tmp > 0); - unsigned nb_channels = tmp; + assert(tmp > 0 && tmp < 256); + uint8_t nb_channels = tmp; #endif assert(ctx->sample_rate > 0); diff --git a/app/src/audio_player.h b/app/src/audio_player.h index 7ebb43db..c02a0d20 100644 --- a/app/src/audio_player.h +++ b/app/src/audio_player.h @@ -42,7 +42,7 @@ struct sc_audio_player { struct SwrContext *swr_ctx; // The sample rate is the same for input and output - unsigned sample_rate; + uint32_t sample_rate; // Target buffer for resampling (only used by the receiver thread) uint8_t *swr_buf; From 62776fb2617a3c23190d3549b673c44b65fe12ce Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 23 Sep 2024 23:16:05 +0200 Subject: [PATCH 1973/2244] Make audio buffering independant of output buffer This will allow to extract the "audio regulator" part from the audio player. --- app/src/audio_player.c | 9 ++++----- app/src/audio_player.h | 1 - 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/app/src/audio_player.c b/app/src/audio_player.c index d72dac25..9e856181 100644 --- a/app/src/audio_player.c +++ b/app/src/audio_player.c @@ -220,14 +220,14 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink, underflow = atomic_exchange_explicit(&ap->underflow, 0, memory_order_relaxed); - max_buffered_samples = ap->target_buffering - + 12 * ap->output_buffer - + ap->target_buffering / 10; + max_buffered_samples = ap->target_buffering * 11 / 10 + + 60 * ap->sample_rate / 1000 /* 60 ms */; } else { // SDL playback not started yet, do not accumulate more than // max_initial_buffering samples, this would cause unnecessary delay // (and glitches to compensate) on start. - max_buffered_samples = ap->target_buffering + 2 * ap->output_buffer; + max_buffered_samples = ap->target_buffering + + 10 * ap->sample_rate / 1000 /* 10 ms */; } uint32_t can_read = sc_audiobuf_can_read(&ap->buf); @@ -363,7 +363,6 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink, uint64_t aout_samples = ap->output_buffer_duration * ap->sample_rate / SC_TICK_FREQ; assert(aout_samples <= 0xFFFF); - ap->output_buffer = (uint16_t) aout_samples; SDL_AudioSpec desired = { .freq = ctx->sample_rate, diff --git a/app/src/audio_player.h b/app/src/audio_player.h index c02a0d20..4ad40306 100644 --- a/app/src/audio_player.h +++ b/app/src/audio_player.h @@ -32,7 +32,6 @@ struct sc_audio_player { // SDL audio output buffer size. sc_tick output_buffer_duration; - uint16_t output_buffer; // Audio buffer to communicate between the receiver and the SDL audio // callback From 0bb3955b958752e1ec220a1045e5185c027fc56b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 23 Sep 2024 23:58:02 +0200 Subject: [PATCH 1974/2244] Split audio player The audio player had 2 roles: - handle the SDL audio output device; - resample input samples to maintain a target latency. Extract the latter to a separate component (an "audio regulator"), independent of SDL. --- app/meson.build | 1 + app/src/audio_player.c | 413 ++----------------------------------- app/src/audio_player.h | 47 +---- app/src/audio_regulator.c | 415 ++++++++++++++++++++++++++++++++++++++ app/src/audio_regulator.h | 71 +++++++ 5 files changed, 507 insertions(+), 440 deletions(-) create mode 100644 app/src/audio_regulator.c create mode 100644 app/src/audio_regulator.h diff --git a/app/meson.build b/app/meson.build index fc752e86..99e7e3a2 100644 --- a/app/meson.build +++ b/app/meson.build @@ -5,6 +5,7 @@ src = [ 'src/adb/adb_parser.c', 'src/adb/adb_tunnel.c', 'src/audio_player.c', + 'src/audio_regulator.c', 'src/cli.c', 'src/clock.c', 'src/compat.c', diff --git a/app/src/audio_player.c b/app/src/audio_player.c index 9e856181..9413c2ea 100644 --- a/app/src/audio_player.c +++ b/app/src/audio_player.c @@ -1,143 +1,23 @@ #include "audio_player.h" -#include -#include - #include "util/log.h" -//#define SC_AUDIO_PLAYER_DEBUG // uncomment to debug - -/** - * Real-time audio player with configurable latency - * - * As input, the player regularly receives AVFrames of decoded audio samples. - * As output, an SDL callback regularly requests audio samples to be played. - * In the middle, an audio buffer stores the samples produced but not consumed - * yet. - * - * The goal of the player is to feed the audio output with a latency as low as - * possible while avoiding buffer underrun (i.e. not being able to provide - * samples when requested). - * - * The player aims to feed the audio output with as little latency as possible - * while avoiding buffer underrun. To achieve this, it attempts to maintain the - * average buffering (the number of samples present in the buffer) around a - * target value. If this target buffering is too low, then buffer underrun will - * occur frequently. If it is too high, then latency will become unacceptable. - * This target value is configured using the scrcpy option --audio-buffer. - * - * The player cannot adjust the sample input rate (it receives samples produced - * in real-time) or the sample output rate (it must provide samples as - * requested by the audio output callback). Therefore, it may only apply - * compensation by resampling (converting _m_ input samples to _n_ output - * samples). - * - * The compensation itself is applied by libswresample (FFmpeg). It is - * configured using swr_set_compensation(). An important work for the player - * is to estimate the compensation value regularly and apply it. - * - * The estimated buffering level is the result of averaging the "natural" - * buffering (samples are produced and consumed by blocks, so it must be - * smoothed), and making instant adjustments resulting of its own actions - * (explicit compensation and silence insertion on underflow), which are not - * smoothed. - * - * Buffer underflow events can occur when packets arrive too late. In that case, - * the player inserts silence. Once the packets finally arrive (late), one - * strategy could be to drop the samples that were replaced by silence, in - * order to keep a minimal latency. However, dropping samples in case of buffer - * underflow is inadvisable, as it would temporarily increase the underflow - * even more and cause very noticeable audio glitches. - * - * Therefore, the player doesn't drop any sample on underflow. The compensation - * mechanism will absorb the delay introduced by the inserted silence. - */ - /** Downcast frame_sink to sc_audio_player */ #define DOWNCAST(SINK) container_of(SINK, struct sc_audio_player, frame_sink) -#define SC_AV_SAMPLE_FMT AV_SAMPLE_FMT_FLT #define SC_SDL_SAMPLE_FMT AUDIO_F32 -#define TO_BYTES(SAMPLES) sc_audiobuf_to_bytes(&ap->buf, (SAMPLES)) -#define TO_SAMPLES(BYTES) sc_audiobuf_to_samples(&ap->buf, (BYTES)) - static void SDLCALL sc_audio_player_sdl_callback(void *userdata, uint8_t *stream, int len_int) { struct sc_audio_player *ap = userdata; assert(len_int > 0); size_t len = len_int; - uint32_t count = TO_SAMPLES(len); -#ifdef SC_AUDIO_PLAYER_DEBUG - LOGD("[Audio] SDL callback requests %" PRIu32 " samples", count); -#endif + assert(len % ap->audioreg.sample_size == 0); + uint32_t out_samples = len / ap->audioreg.sample_size; - // A lock is necessary in the rare case where the producer needs to drop - // samples already pushed (when the buffer is full) - sc_mutex_lock(&ap->mutex); - - bool played = atomic_load_explicit(&ap->played, memory_order_relaxed); - if (!played) { - uint32_t buffered_samples = sc_audiobuf_can_read(&ap->buf); - // Wait until the buffer is filled up to at least target_buffering - // before playing - if (buffered_samples < ap->target_buffering) { - LOGV("[Audio] Inserting initial buffering silence: %" PRIu32 - " samples", count); - // Delay playback starting to reach the target buffering. Fill the - // whole buffer with silence (len is small compared to the - // arbitrary margin value). - memset(stream, 0, len); - sc_mutex_unlock(&ap->mutex); - return; - } - } - - uint32_t read = sc_audiobuf_read(&ap->buf, stream, count); - - sc_mutex_unlock(&ap->mutex); - - if (read < count) { - uint32_t silence = count - read; - // Insert silence. In theory, the inserted silent samples replace the - // missing real samples, which will arrive later, so they should be - // dropped to keep the latency minimal. However, this would cause very - // audible glitches, so let the clock compensation restore the target - // latency. - LOGD("[Audio] Buffer underflow, inserting silence: %" PRIu32 " samples", - silence); - memset(stream + TO_BYTES(read), 0, TO_BYTES(silence)); - - bool received = atomic_load_explicit(&ap->received, - memory_order_relaxed); - if (received) { - // Inserting additional samples immediately increases buffering - atomic_fetch_add_explicit(&ap->underflow, silence, - memory_order_relaxed); - } - } - - atomic_store_explicit(&ap->played, true, memory_order_relaxed); -} - -static uint8_t * -sc_audio_player_get_swr_buf(struct sc_audio_player *ap, uint32_t min_samples) { - size_t min_buf_size = TO_BYTES(min_samples); - if (min_buf_size > ap->swr_buf_alloc_size) { - size_t new_size = min_buf_size + 4096; - uint8_t *buf = realloc(ap->swr_buf, new_size); - if (!buf) { - LOG_OOM(); - // Could not realloc to the requested size - return NULL; - } - ap->swr_buf = buf; - ap->swr_buf_alloc_size = new_size; - } - - return ap->swr_buf; + sc_audio_regulator_pull(&ap->audioreg, stream, out_samples); } static bool @@ -145,202 +25,14 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink, const AVFrame *frame) { struct sc_audio_player *ap = DOWNCAST(sink); - SwrContext *swr_ctx = ap->swr_ctx; - - int64_t swr_delay = swr_get_delay(swr_ctx, ap->sample_rate); - // No need to av_rescale_rnd(), input and output sample rates are the same. - // Add more space (256) for clock compensation. - int dst_nb_samples = swr_delay + frame->nb_samples + 256; - - uint8_t *swr_buf = sc_audio_player_get_swr_buf(ap, dst_nb_samples); - if (!swr_buf) { - return false; - } - - int ret = swr_convert(swr_ctx, &swr_buf, dst_nb_samples, - (const uint8_t **) frame->data, frame->nb_samples); - if (ret < 0) { - LOGE("Resampling failed: %d", ret); - return false; - } - - // swr_convert() returns the number of samples which would have been - // written if the buffer was big enough. - uint32_t samples = MIN(ret, dst_nb_samples); -#ifdef SC_AUDIO_PLAYER_DEBUG - LOGD("[Audio] %" PRIu32 " samples written to buffer", samples); -#endif - - uint32_t cap = sc_audiobuf_capacity(&ap->buf); - if (samples > cap) { - // Very very unlikely: a single resampled frame should never - // exceed the audio buffer size (or something is very wrong). - // Ignore the first bytes in swr_buf to avoid memory corruption anyway. - swr_buf += TO_BYTES(samples - cap); - samples = cap; - } - - uint32_t skipped_samples = 0; - - uint32_t written = sc_audiobuf_write(&ap->buf, swr_buf, samples); - if (written < samples) { - uint32_t remaining = samples - written; - - // All samples that could be written without locking have been written, - // now we need to lock to drop/consume old samples - sc_mutex_lock(&ap->mutex); - - // Retry with the lock - written += sc_audiobuf_write(&ap->buf, - swr_buf + TO_BYTES(written), - remaining); - if (written < samples) { - remaining = samples - written; - // Still insufficient, drop old samples to make space - skipped_samples = sc_audiobuf_read(&ap->buf, NULL, remaining); - assert(skipped_samples == remaining); - } - - sc_mutex_unlock(&ap->mutex); - - if (written < samples) { - // Now there is enough space - uint32_t w = sc_audiobuf_write(&ap->buf, - swr_buf + TO_BYTES(written), - remaining); - assert(w == remaining); - (void) w; - } - } - - uint32_t underflow = 0; - uint32_t max_buffered_samples; - bool played = atomic_load_explicit(&ap->played, memory_order_relaxed); - if (played) { - underflow = atomic_exchange_explicit(&ap->underflow, 0, - memory_order_relaxed); - - max_buffered_samples = ap->target_buffering * 11 / 10 - + 60 * ap->sample_rate / 1000 /* 60 ms */; - } else { - // SDL playback not started yet, do not accumulate more than - // max_initial_buffering samples, this would cause unnecessary delay - // (and glitches to compensate) on start. - max_buffered_samples = ap->target_buffering - + 10 * ap->sample_rate / 1000 /* 10 ms */; - } - - uint32_t can_read = sc_audiobuf_can_read(&ap->buf); - if (can_read > max_buffered_samples) { - uint32_t skip_samples = 0; - - sc_mutex_lock(&ap->mutex); - can_read = sc_audiobuf_can_read(&ap->buf); - if (can_read > max_buffered_samples) { - skip_samples = can_read - max_buffered_samples; - uint32_t r = sc_audiobuf_read(&ap->buf, NULL, skip_samples); - assert(r == skip_samples); - (void) r; - skipped_samples += skip_samples; - } - sc_mutex_unlock(&ap->mutex); - - if (skip_samples) { - if (played) { - LOGD("[Audio] Buffering threshold exceeded, skipping %" PRIu32 - " samples", skip_samples); -#ifdef SC_AUDIO_PLAYER_DEBUG - } else { - LOGD("[Audio] Playback not started, skipping %" PRIu32 - " samples", skip_samples); -#endif - } - } - } - - atomic_store_explicit(&ap->received, true, memory_order_relaxed); - if (!played) { - // Nothing more to do - return true; - } - - // Number of samples added (or removed, if negative) for compensation - int32_t instant_compensation = (int32_t) written - frame->nb_samples; - // Inserting silence instantly increases buffering - int32_t inserted_silence = (int32_t) underflow; - // Dropping input samples instantly decreases buffering - int32_t dropped = (int32_t) skipped_samples; - - // The compensation must apply instantly, it must not be smoothed - ap->avg_buffering.avg += instant_compensation + inserted_silence - dropped; - if (ap->avg_buffering.avg < 0) { - // Since dropping samples instantly reduces buffering, the difference - // is applied immediately to the average value, assuming that the delay - // between the producer and the consumer will be caught up. - // - // However, when this assumption is not valid, the average buffering - // may decrease indefinitely. Prevent it to become negative to limit - // the consequences. - ap->avg_buffering.avg = 0; - } - - // However, the buffering level must be smoothed - sc_average_push(&ap->avg_buffering, can_read); - -#ifdef SC_AUDIO_PLAYER_DEBUG - LOGD("[Audio] can_read=%" PRIu32 " avg_buffering=%f", - can_read, sc_average_get(&ap->avg_buffering)); -#endif - - ap->samples_since_resync += written; - if (ap->samples_since_resync >= ap->sample_rate) { - // Recompute compensation every second - ap->samples_since_resync = 0; - - float avg = sc_average_get(&ap->avg_buffering); - int diff = ap->target_buffering - avg; - - // Enable compensation when the difference exceeds +/- 4ms. - // Disable compensation when the difference is lower than +/- 1ms. - int threshold = ap->compensation != 0 - ? ap->sample_rate / 1000 /* 1ms */ - : ap->sample_rate * 4 / 1000; /* 4ms */ - - if (abs(diff) < threshold) { - // Do not compensate for small values, the error is just noise - diff = 0; - } else if (diff < 0 && can_read < ap->target_buffering) { - // Do not accelerate if the instant buffering level is below the - // target, this would increase underflow - diff = 0; - } - // Compensate the diff over 4 seconds (but will be recomputed after 1 - // second) - int distance = 4 * ap->sample_rate; - // Limit compensation rate to 2% - int abs_max_diff = distance / 50; - diff = CLAMP(diff, -abs_max_diff, abs_max_diff); - LOGV("[Audio] Buffering: target=%" PRIu32 " avg=%f cur=%" PRIu32 - " compensation=%d", ap->target_buffering, avg, can_read, diff); - - if (diff != ap->compensation) { - int ret = swr_set_compensation(swr_ctx, diff, distance); - if (ret < 0) { - LOGW("Resampling compensation failed: %d", ret); - // not fatal - } else { - ap->compensation = diff; - } - } - } - - return true; + return sc_audio_regulator_push(&ap->audioreg, frame); } static bool sc_audio_player_frame_sink_open(struct sc_frame_sink *sink, const AVCodecContext *ctx) { struct sc_audio_player *ap = DOWNCAST(sink); + #ifdef SCRCPY_LAVU_HAS_CHLAYOUT assert(ctx->ch_layout.nb_channels > 0 && ctx->ch_layout.nb_channels < 256); uint8_t nb_channels = ctx->ch_layout.nb_channels; @@ -355,12 +47,17 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink, int out_bytes_per_sample = av_get_bytes_per_sample(SC_AV_SAMPLE_FMT); assert(out_bytes_per_sample > 0); - ap->sample_rate = ctx->sample_rate; + uint32_t target_buffering_samples = + ap->target_buffering_delay * ctx->sample_rate / SC_TICK_FREQ; - ap->target_buffering = ap->target_buffering_delay * ap->sample_rate - / SC_TICK_FREQ; + size_t sample_size = nb_channels * out_bytes_per_sample; + bool ok = sc_audio_regulator_init(&ap->audioreg, sample_size, ctx, + target_buffering_samples); + if (!ok) { + return false; + } - uint64_t aout_samples = ap->output_buffer_duration * ap->sample_rate + uint64_t aout_samples = ap->output_buffer_duration * ctx->sample_rate / SC_TICK_FREQ; assert(aout_samples <= 0xFFFF); @@ -377,74 +74,10 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink, ap->device = SDL_OpenAudioDevice(NULL, 0, &desired, &obtained, 0); if (!ap->device) { LOGE("Could not open audio device: %s", SDL_GetError()); + sc_audio_regulator_destroy(&ap->audioreg); return false; } - SwrContext *swr_ctx = swr_alloc(); - if (!swr_ctx) { - LOG_OOM(); - goto error_close_audio_device; - } - ap->swr_ctx = swr_ctx; - -#ifdef SCRCPY_LAVU_HAS_CHLAYOUT - av_opt_set_chlayout(swr_ctx, "in_chlayout", &ctx->ch_layout, 0); - av_opt_set_chlayout(swr_ctx, "out_chlayout", &ctx->ch_layout, 0); -#else - av_opt_set_channel_layout(swr_ctx, "in_channel_layout", - ctx->channel_layout, 0); - av_opt_set_channel_layout(swr_ctx, "out_channel_layout", - ctx->channel_layout, 0); -#endif - - av_opt_set_int(swr_ctx, "in_sample_rate", ctx->sample_rate, 0); - av_opt_set_int(swr_ctx, "out_sample_rate", ctx->sample_rate, 0); - - av_opt_set_sample_fmt(swr_ctx, "in_sample_fmt", ctx->sample_fmt, 0); - av_opt_set_sample_fmt(swr_ctx, "out_sample_fmt", SC_AV_SAMPLE_FMT, 0); - - int ret = swr_init(swr_ctx); - if (ret) { - LOGE("Failed to initialize the resampling context"); - goto error_free_swr_ctx; - } - - // Use a ring-buffer of the target buffering size plus 1 second between the - // producer and the consumer. It's too big on purpose, to guarantee that - // the producer and the consumer will be able to access it in parallel - // without locking. - uint32_t audiobuf_samples = ap->target_buffering + ap->sample_rate; - - bool ok = sc_mutex_init(&ap->mutex); - if (!ok) { - goto error_free_swr_ctx; - } - - size_t sample_size = nb_channels * out_bytes_per_sample; - ok = sc_audiobuf_init(&ap->buf, sample_size, audiobuf_samples); - if (!ok) { - goto error_destroy_mutex; - } - - size_t initial_swr_buf_size = TO_BYTES(4096); - ap->swr_buf = malloc(initial_swr_buf_size); - if (!ap->swr_buf) { - LOG_OOM(); - goto error_destroy_audiobuf; - } - ap->swr_buf_alloc_size = initial_swr_buf_size; - - // Samples are produced and consumed by blocks, so the buffering must be - // smoothed to get a relatively stable value. - sc_average_init(&ap->avg_buffering, 128); - ap->samples_since_resync = 0; - - ap->received = false; - atomic_init(&ap->played, false); - atomic_init(&ap->received, false); - atomic_init(&ap->underflow, 0); - ap->compensation = 0; - // The thread calling open() is the thread calling push(), which fills the // audio buffer consumed by the SDL audio thread. ok = sc_thread_set_priority(SC_THREAD_PRIORITY_TIME_CRITICAL); @@ -456,17 +89,6 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink, SDL_PauseAudioDevice(ap->device, 0); return true; - -error_destroy_audiobuf: - sc_audiobuf_destroy(&ap->buf); -error_destroy_mutex: - sc_mutex_destroy(&ap->mutex); -error_free_swr_ctx: - swr_free(&ap->swr_ctx); -error_close_audio_device: - SDL_CloseAudioDevice(ap->device); - - return false; } static void @@ -477,10 +99,7 @@ sc_audio_player_frame_sink_close(struct sc_frame_sink *sink) { SDL_PauseAudioDevice(ap->device, 1); SDL_CloseAudioDevice(ap->device); - free(ap->swr_buf); - sc_audiobuf_destroy(&ap->buf); - sc_mutex_destroy(&ap->mutex); - swr_free(&ap->swr_ctx); + sc_audio_regulator_destroy(&ap->audioreg); } void diff --git a/app/src/audio_player.h b/app/src/audio_player.h index 4ad40306..9133c24a 100644 --- a/app/src/audio_player.h +++ b/app/src/audio_player.h @@ -5,66 +5,27 @@ #include #include -#include -#include #include +#include "audio_regulator.h" #include "trait/frame_sink.h" -#include "util/audiobuf.h" -#include "util/average.h" -#include "util/thread.h" #include "util/tick.h" struct sc_audio_player { struct sc_frame_sink frame_sink; - SDL_AudioDeviceID device; - - sc_mutex mutex; - // The target buffering between the producer and the consumer. This value // is directly use for compensation. // Since audio capture and/or encoding on the device typically produce // blocks of 960 samples (20ms) or 1024 samples (~21.3ms), this target // value should be higher. sc_tick target_buffering_delay; - uint32_t target_buffering; // in samples - // SDL audio output buffer size. + // SDL audio output buffer size sc_tick output_buffer_duration; - // Audio buffer to communicate between the receiver and the SDL audio - // callback - struct sc_audiobuf buf; - - // Resampler (only used from the receiver thread) - struct SwrContext *swr_ctx; - - // The sample rate is the same for input and output - uint32_t sample_rate; - - // Target buffer for resampling (only used by the receiver thread) - uint8_t *swr_buf; - size_t swr_buf_alloc_size; - - // Number of buffered samples (may be negative on underflow) (only used by - // the receiver thread) - struct sc_average avg_buffering; - // Count the number of samples to trigger a compensation update regularly - // (only used by the receiver thread) - uint32_t samples_since_resync; - - // Number of silence samples inserted since the last received packet - atomic_uint_least32_t underflow; - - // Current applied compensation value (only used by the receiver thread) - int compensation; - - // Set to true the first time a sample is received - atomic_bool received; - - // Set to true the first time the SDL callback is called - atomic_bool played; + SDL_AudioDeviceID device; + struct sc_audio_regulator audioreg; }; void diff --git a/app/src/audio_regulator.c b/app/src/audio_regulator.c new file mode 100644 index 00000000..911b2bfa --- /dev/null +++ b/app/src/audio_regulator.c @@ -0,0 +1,415 @@ +#include "audio_regulator.h" + +#include +#include + +#include "util/log.h" + +//#define SC_AUDIO_REGULATOR_DEBUG // uncomment to debug + +/** + * Real-time audio regulator with configurable latency + * + * As input, the regulator regularly receives AVFrames of decoded audio samples. + * As output, the audio player regularly requests audio samples to be played. + * In the middle, an audio buffer stores the samples produced but not consumed + * yet. + * + * The goal of the regulator is to feed the audio player with a latency as low + * as possible while avoiding buffer underrun (i.e. not being able to provide + * samples when requested). + * + * To achieve this, it attempts to maintain the average buffering (the number + * of samples present in the buffer) around a target value. If this target + * buffering is too low, then buffer underrun will occur frequently. If it is + * too high, then latency will become unacceptable. This target value is + * configured using the scrcpy option --audio-buffer. + * + * The regulator cannot adjust the sample input rate (it receives samples + * produced in real-time) or the sample output rate (it must provide samples as + * requested by the audio player). Therefore, it may only apply compensation by + * resampling (converting _m_ input samples to _n_ output samples). + * + * The compensation itself is applied by libswresample (FFmpeg). It is + * configured using swr_set_compensation(). An important work for the regulator + * is to estimate the compensation value regularly and apply it. + * + * The estimated buffering level is the result of averaging the "natural" + * buffering (samples are produced and consumed by blocks, so it must be + * smoothed), and making instant adjustments resulting of its own actions + * (explicit compensation and silence insertion on underflow), which are not + * smoothed. + * + * Buffer underflow events can occur when packets arrive too late. In that case, + * the regulator inserts silence. Once the packets finally arrive (late), one + * strategy could be to drop the samples that were replaced by silence, in + * order to keep a minimal latency. However, dropping samples in case of buffer + * underflow is inadvisable, as it would temporarily increase the underflow + * even more and cause very noticeable audio glitches. + * + * Therefore, the regulator doesn't drop any sample on underflow. The + * compensation mechanism will absorb the delay introduced by the inserted + * silence. + */ + +#define TO_BYTES(SAMPLES) sc_audiobuf_to_bytes(&ar->buf, (SAMPLES)) +#define TO_SAMPLES(BYTES) sc_audiobuf_to_samples(&ar->buf, (BYTES)) + +void +sc_audio_regulator_pull(struct sc_audio_regulator *ar, uint8_t *out, + uint32_t out_samples) { +#ifdef SC_AUDIO_REGULATOR_DEBUG + LOGD("[Audio] Audio regulator pulls %" PRIu32 " samples", out_samples); +#endif + + // A lock is necessary in the rare case where the producer needs to drop + // samples already pushed (when the buffer is full) + sc_mutex_lock(&ar->mutex); + + bool played = atomic_load_explicit(&ar->played, memory_order_relaxed); + if (!played) { + uint32_t buffered_samples = sc_audiobuf_can_read(&ar->buf); + // Wait until the buffer is filled up to at least target_buffering + // before playing + if (buffered_samples < ar->target_buffering) { + LOGV("[Audio] Inserting initial buffering silence: %" PRIu32 + " samples", out_samples); + // Delay playback starting to reach the target buffering. Fill the + // whole buffer with silence (len is small compared to the + // arbitrary margin value). + memset(out, 0, out_samples * ar->sample_size); + sc_mutex_unlock(&ar->mutex); + return; + } + } + + uint32_t read = sc_audiobuf_read(&ar->buf, out, out_samples); + + sc_mutex_unlock(&ar->mutex); + + if (read < out_samples) { + uint32_t silence = out_samples - read; + // Insert silence. In theory, the inserted silent samples replace the + // missing real samples, which will arrive later, so they should be + // dropped to keep the latency minimal. However, this would cause very + // audible glitches, so let the clock compensation restore the target + // latency. + LOGD("[Audio] Buffer underflow, inserting silence: %" PRIu32 " samples", + silence); + memset(out + TO_BYTES(read), 0, TO_BYTES(silence)); + + bool received = atomic_load_explicit(&ar->received, + memory_order_relaxed); + if (received) { + // Inserting additional samples immediately increases buffering + atomic_fetch_add_explicit(&ar->underflow, silence, + memory_order_relaxed); + } + } + + atomic_store_explicit(&ar->played, true, memory_order_relaxed); +} + +static uint8_t * +sc_audio_regulator_get_swr_buf(struct sc_audio_regulator *ar, + uint32_t min_samples) { + size_t min_buf_size = TO_BYTES(min_samples); + if (min_buf_size > ar->swr_buf_alloc_size) { + size_t new_size = min_buf_size + 4096; + uint8_t *buf = realloc(ar->swr_buf, new_size); + if (!buf) { + LOG_OOM(); + // Could not realloc to the requested size + return NULL; + } + ar->swr_buf = buf; + ar->swr_buf_alloc_size = new_size; + } + + return ar->swr_buf; +} + +bool +sc_audio_regulator_push(struct sc_audio_regulator *ar, const AVFrame *frame) { + SwrContext *swr_ctx = ar->swr_ctx; + + int64_t swr_delay = swr_get_delay(swr_ctx, ar->sample_rate); + // No need to av_rescale_rnd(), input and output sample rates are the same. + // Add more space (256) for clock compensation. + int dst_nb_samples = swr_delay + frame->nb_samples + 256; + + uint8_t *swr_buf = sc_audio_regulator_get_swr_buf(ar, dst_nb_samples); + if (!swr_buf) { + return false; + } + + int ret = swr_convert(swr_ctx, &swr_buf, dst_nb_samples, + (const uint8_t **) frame->data, frame->nb_samples); + if (ret < 0) { + LOGE("Resampling failed: %d", ret); + return false; + } + + // swr_convert() returns the number of samples which would have been + // written if the buffer was big enough. + uint32_t samples = MIN(ret, dst_nb_samples); +#ifdef SC_AUDIO_REGULATOR_DEBUG + LOGD("[Audio] %" PRIu32 " samples written to buffer", samples); +#endif + + uint32_t cap = sc_audiobuf_capacity(&ar->buf); + if (samples > cap) { + // Very very unlikely: a single resampled frame should never + // exceed the audio buffer size (or something is very wrong). + // Ignore the first bytes in swr_buf to avoid memory corruption anyway. + swr_buf += TO_BYTES(samples - cap); + samples = cap; + } + + uint32_t skipped_samples = 0; + + uint32_t written = sc_audiobuf_write(&ar->buf, swr_buf, samples); + if (written < samples) { + uint32_t remaining = samples - written; + + // All samples that could be written without locking have been written, + // now we need to lock to drop/consume old samples + sc_mutex_lock(&ar->mutex); + + // Retry with the lock + written += sc_audiobuf_write(&ar->buf, + swr_buf + TO_BYTES(written), + remaining); + if (written < samples) { + remaining = samples - written; + // Still insufficient, drop old samples to make space + skipped_samples = sc_audiobuf_read(&ar->buf, NULL, remaining); + assert(skipped_samples == remaining); + } + + sc_mutex_unlock(&ar->mutex); + + if (written < samples) { + // Now there is enough space + uint32_t w = sc_audiobuf_write(&ar->buf, + swr_buf + TO_BYTES(written), + remaining); + assert(w == remaining); + (void) w; + } + } + + uint32_t underflow = 0; + uint32_t max_buffered_samples; + bool played = atomic_load_explicit(&ar->played, memory_order_relaxed); + if (played) { + underflow = atomic_exchange_explicit(&ar->underflow, 0, + memory_order_relaxed); + + max_buffered_samples = ar->target_buffering * 11 / 10 + + 60 * ar->sample_rate / 1000 /* 60 ms */; + } else { + // Playback not started yet, do not accumulate more than + // max_initial_buffering samples, this would cause unnecessary delay + // (and glitches to compensate) on start. + max_buffered_samples = ar->target_buffering + + 10 * ar->sample_rate / 1000 /* 10 ms */; + } + + uint32_t can_read = sc_audiobuf_can_read(&ar->buf); + if (can_read > max_buffered_samples) { + uint32_t skip_samples = 0; + + sc_mutex_lock(&ar->mutex); + can_read = sc_audiobuf_can_read(&ar->buf); + if (can_read > max_buffered_samples) { + skip_samples = can_read - max_buffered_samples; + uint32_t r = sc_audiobuf_read(&ar->buf, NULL, skip_samples); + assert(r == skip_samples); + (void) r; + skipped_samples += skip_samples; + } + sc_mutex_unlock(&ar->mutex); + + if (skip_samples) { + if (played) { + LOGD("[Audio] Buffering threshold exceeded, skipping %" PRIu32 + " samples", skip_samples); +#ifdef SC_AUDIO_REGULATOR_DEBUG + } else { + LOGD("[Audio] Playback not started, skipping %" PRIu32 + " samples", skip_samples); +#endif + } + } + } + + atomic_store_explicit(&ar->received, true, memory_order_relaxed); + if (!played) { + // Nothing more to do + return true; + } + + // Number of samples added (or removed, if negative) for compensation + int32_t instant_compensation = (int32_t) written - frame->nb_samples; + // Inserting silence instantly increases buffering + int32_t inserted_silence = (int32_t) underflow; + // Dropping input samples instantly decreases buffering + int32_t dropped = (int32_t) skipped_samples; + + // The compensation must apply instantly, it must not be smoothed + ar->avg_buffering.avg += instant_compensation + inserted_silence - dropped; + if (ar->avg_buffering.avg < 0) { + // Since dropping samples instantly reduces buffering, the difference + // is applied immediately to the average value, assuming that the delay + // between the producer and the consumer will be caught up. + // + // However, when this assumption is not valid, the average buffering + // may decrease indefinitely. Prevent it to become negative to limit + // the consequences. + ar->avg_buffering.avg = 0; + } + + // However, the buffering level must be smoothed + sc_average_push(&ar->avg_buffering, can_read); + +#ifdef SC_AUDIO_REGULATOR_DEBUG + LOGD("[Audio] can_read=%" PRIu32 " avg_buffering=%f", + can_read, sc_average_get(&ar->avg_buffering)); +#endif + + ar->samples_since_resync += written; + if (ar->samples_since_resync >= ar->sample_rate) { + // Recompute compensation every second + ar->samples_since_resync = 0; + + float avg = sc_average_get(&ar->avg_buffering); + int diff = ar->target_buffering - avg; + + // Enable compensation when the difference exceeds +/- 4ms. + // Disable compensation when the difference is lower than +/- 1ms. + int threshold = ar->compensation != 0 + ? ar->sample_rate / 1000 /* 1ms */ + : ar->sample_rate * 4 / 1000; /* 4ms */ + + if (abs(diff) < threshold) { + // Do not compensate for small values, the error is just noise + diff = 0; + } else if (diff < 0 && can_read < ar->target_buffering) { + // Do not accelerate if the instant buffering level is below the + // target, this would increase underflow + diff = 0; + } + // Compensate the diff over 4 seconds (but will be recomputed after 1 + // second) + int distance = 4 * ar->sample_rate; + // Limit compensation rate to 2% + int abs_max_diff = distance / 50; + diff = CLAMP(diff, -abs_max_diff, abs_max_diff); + LOGV("[Audio] Buffering: target=%" PRIu32 " avg=%f cur=%" PRIu32 + " compensation=%d", ar->target_buffering, avg, can_read, diff); + + if (diff != ar->compensation) { + int ret = swr_set_compensation(swr_ctx, diff, distance); + if (ret < 0) { + LOGW("Resampling compensation failed: %d", ret); + // not fatal + } else { + ar->compensation = diff; + } + } + } + + return true; +} + +bool +sc_audio_regulator_init(struct sc_audio_regulator *ar, size_t sample_size, + const AVCodecContext *ctx, uint32_t target_buffering) { + SwrContext *swr_ctx = swr_alloc(); + if (!swr_ctx) { + LOG_OOM(); + return false; + } + ar->swr_ctx = swr_ctx; + +#ifdef SCRCPY_LAVU_HAS_CHLAYOUT + av_opt_set_chlayout(swr_ctx, "in_chlayout", &ctx->ch_layout, 0); + av_opt_set_chlayout(swr_ctx, "out_chlayout", &ctx->ch_layout, 0); +#else + av_opt_set_channel_layout(swr_ctx, "in_channel_layout", + ctx->channel_layout, 0); + av_opt_set_channel_layout(swr_ctx, "out_channel_layout", + ctx->channel_layout, 0); +#endif + + av_opt_set_int(swr_ctx, "in_sample_rate", ctx->sample_rate, 0); + av_opt_set_int(swr_ctx, "out_sample_rate", ctx->sample_rate, 0); + + av_opt_set_sample_fmt(swr_ctx, "in_sample_fmt", ctx->sample_fmt, 0); + av_opt_set_sample_fmt(swr_ctx, "out_sample_fmt", SC_AV_SAMPLE_FMT, 0); + + int ret = swr_init(swr_ctx); + if (ret) { + LOGE("Failed to initialize the resampling context"); + goto error_free_swr_ctx; + } + + bool ok = sc_mutex_init(&ar->mutex); + if (!ok) { + goto error_free_swr_ctx; + } + + ar->target_buffering = target_buffering; + ar->sample_size = sample_size; + ar->sample_rate = ctx->sample_rate; + + // Use a ring-buffer of the target buffering size plus 1 second between the + // producer and the consumer. It's too big on purpose, to guarantee that + // the producer and the consumer will be able to access it in parallel + // without locking. + uint32_t audiobuf_samples = target_buffering + ar->sample_rate; + + ok = sc_audiobuf_init(&ar->buf, sample_size, audiobuf_samples); + if (!ok) { + goto error_destroy_mutex; + } + + size_t initial_swr_buf_size = TO_BYTES(4096); + ar->swr_buf = malloc(initial_swr_buf_size); + if (!ar->swr_buf) { + LOG_OOM(); + goto error_destroy_audiobuf; + } + ar->swr_buf_alloc_size = initial_swr_buf_size; + + // Samples are produced and consumed by blocks, so the buffering must be + // smoothed to get a relatively stable value. + sc_average_init(&ar->avg_buffering, 128); + ar->samples_since_resync = 0; + + ar->received = false; + atomic_init(&ar->played, false); + atomic_init(&ar->received, false); + atomic_init(&ar->underflow, 0); + ar->compensation = 0; + + return true; + +error_destroy_audiobuf: + sc_audiobuf_destroy(&ar->buf); +error_destroy_mutex: + sc_mutex_destroy(&ar->mutex); +error_free_swr_ctx: + swr_free(&ar->swr_ctx); + + return false; +} + +void +sc_audio_regulator_destroy(struct sc_audio_regulator *ar) { + free(ar->swr_buf); + sc_audiobuf_destroy(&ar->buf); + sc_mutex_destroy(&ar->mutex); + swr_free(&ar->swr_ctx); +} diff --git a/app/src/audio_regulator.h b/app/src/audio_regulator.h new file mode 100644 index 00000000..7daa1b05 --- /dev/null +++ b/app/src/audio_regulator.h @@ -0,0 +1,71 @@ +#ifndef SC_AUDIO_REGULATOR_H +#define SC_AUDIO_REGULATOR_H + +#include "common.h" + +#include +#include +#include +#include +#include "util/audiobuf.h" +#include "util/average.h" +#include "util/thread.h" + +#define SC_AV_SAMPLE_FMT AV_SAMPLE_FMT_FLT + +struct sc_audio_regulator { + sc_mutex mutex; + + // Target buffering between the producer and the consumer (in samples) + uint32_t target_buffering; + + // Audio buffer to communicate between the receiver and the player + struct sc_audiobuf buf; + + // Resampler (only used from the receiver thread) + struct SwrContext *swr_ctx; + + // The sample rate is the same for input and output + uint32_t sample_rate; + // The number of bytes per sample (for all channels) + size_t sample_size; + + // Target buffer for resampling (only used by the receiver thread) + uint8_t *swr_buf; + size_t swr_buf_alloc_size; + + // Number of buffered samples (may be negative on underflow) (only used by + // the receiver thread) + struct sc_average avg_buffering; + // Count the number of samples to trigger a compensation update regularly + // (only used by the receiver thread) + uint32_t samples_since_resync; + + // Number of silence samples inserted since the last received packet + atomic_uint_least32_t underflow; + + // Current applied compensation value (only used by the receiver thread) + int compensation; + + // Set to true the first time a sample is received + atomic_bool received; + + // Set to true the first time samples are pulled by the player + atomic_bool played; +}; + +bool +sc_audio_regulator_init(struct sc_audio_regulator *ar, size_t sample_size, + const AVCodecContext *ctx, uint32_t target_buffering); + +void +sc_audio_regulator_destroy(struct sc_audio_regulator *ar); + +bool +sc_audio_regulator_push(struct sc_audio_regulator *ar, const AVFrame *frame); + +void +sc_audio_regulator_pull(struct sc_audio_regulator *ar, uint8_t *out, + uint32_t samples); + +#endif From d92b7a60243f1e08141d8d9bfbc94dadd1b19ac8 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 25 Sep 2024 19:59:49 +0200 Subject: [PATCH 1975/2244] Rename switch_fullscreen() to toggle_fullscreen() Toggle means to switch between two states. --- app/src/input_manager.c | 2 +- app/src/screen.c | 4 ++-- app/src/screen.h | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 77cb4f1d..b1d7e9b9 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -536,7 +536,7 @@ sc_input_manager_process_key(struct sc_input_manager *im, return; case SDLK_f: if (video && !shift && !repeat && down) { - sc_screen_switch_fullscreen(im->screen); + sc_screen_toggle_fullscreen(im->screen); } return; case SDLK_w: diff --git a/app/src/screen.c b/app/src/screen.c index cb455cb1..ce730f19 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -538,7 +538,7 @@ sc_screen_show_initial_window(struct sc_screen *screen) { SDL_SetWindowPosition(screen->window, x, y); if (screen->req.fullscreen) { - sc_screen_switch_fullscreen(screen); + sc_screen_toggle_fullscreen(screen); } if (screen->req.start_fps_counter) { @@ -774,7 +774,7 @@ sc_screen_set_paused(struct sc_screen *screen, bool paused) { } void -sc_screen_switch_fullscreen(struct sc_screen *screen) { +sc_screen_toggle_fullscreen(struct sc_screen *screen) { assert(screen->video); uint32_t new_mode = screen->fullscreen ? 0 : SDL_WINDOW_FULLSCREEN_DESKTOP; diff --git a/app/src/screen.h b/app/src/screen.h index 7e1f7e6e..6d5964bd 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -126,9 +126,9 @@ sc_screen_destroy(struct sc_screen *screen); void sc_screen_hide_window(struct sc_screen *screen); -// switch the fullscreen mode +// toggle the fullscreen mode void -sc_screen_switch_fullscreen(struct sc_screen *screen); +sc_screen_toggle_fullscreen(struct sc_screen *screen); // resize window to optimal size (remove black borders) void From 7a9ea5c66fedbb5b3b1d02f51695aa4ab259dfe3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 25 Sep 2024 21:38:09 +0200 Subject: [PATCH 1976/2244] Add shortcut for horizontal tilt Use Ctrl+Shift for horizontal tilt. Refs #4529 comment Fixes #5317 --- app/scrcpy.1 | 6 +++++- app/src/cli.c | 6 +++++- app/src/input_manager.c | 20 ++++++++++++++++---- doc/control.md | 8 ++++++-- doc/shortcuts.md | 3 ++- 5 files changed, 34 insertions(+), 9 deletions(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index a256c40e..3fd3eb29 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -727,7 +727,11 @@ Pinch-to-zoom and rotate from the center of the screen .TP .B Shift+click-and-move -Tilt (slide vertically with two fingers) +Tilt vertically (slide with 2 fingers) + +.TP +.B Ctrl+Shift+click-and-move +Tilt horizontally (slide with 2 fingers) .TP .B Drag & drop APK file diff --git a/app/src/cli.c b/app/src/cli.c index 3c1f9a1b..4fc3c534 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -1072,7 +1072,11 @@ static const struct sc_shortcut shortcuts[] = { }, { .shortcuts = { "Shift+click-and-move" }, - .text = "Tilt (slide vertically with two fingers)", + .text = "Tilt vertically (slide with 2 fingers)", + }, + { + .shortcuts = { "Ctrl+Shift+click-and-move" }, + .text = "Tilt horizontally (slide with 2 fingers)", }, { .shortcuts = { "Drag & drop APK file" }, diff --git a/app/src/input_manager.c b/app/src/input_manager.c index b1d7e9b9..444a5f16 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -836,7 +836,7 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im, } bool change_vfinger = event->button == SDL_BUTTON_LEFT && - ((down && !im->vfinger_down && (ctrl_pressed ^ shift_pressed)) || + ((down && !im->vfinger_down && (ctrl_pressed || shift_pressed)) || (!down && im->vfinger_down)); bool use_finger = im->vfinger_down || change_vfinger; @@ -868,16 +868,28 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im, // In other words, the center of the rotation/scaling is the center of the // screen. // - // To simulate a tilt gesture (a vertical slide with two fingers), Shift - // can be used instead of Ctrl. The "virtual finger" has a position + // To simulate a vertical tilt gesture (a vertical slide with two fingers), + // Shift can be used instead of Ctrl. The "virtual finger" has a position // inverted with respect to the vertical axis of symmetry in the middle of // the screen. + // + // To simulate a horizontal tilt gesture (a horizontal slide with two + // fingers), Ctrl+Shift can be used. The "virtual finger" has a position + // inverted with respect to the horizontal axis of symmetry in the middle + // of the screen. It is expected to be less frequently used, that's why the + // one-mod shortcuts are assigned to rotation and vertical tilt. if (change_vfinger) { struct sc_point mouse = sc_screen_convert_window_to_frame_coords(im->screen, event->x, event->y); if (down) { - im->vfinger_invert_x = ctrl_pressed || shift_pressed; + // Ctrl Shift invert_x invert_y + // ---- ----- ==> -------- -------- + // 0 0 0 0 - + // 0 1 1 0 vertical tilt + // 1 0 1 1 rotate + // 1 1 0 1 horizontal tilt + im->vfinger_invert_x = ctrl_pressed ^ shift_pressed; im->vfinger_invert_y = ctrl_pressed; } struct sc_point vfinger = inverse_point(mouse, im->screen->frame_size, diff --git a/doc/control.md b/doc/control.md index 34eb7a6a..26805346 100644 --- a/doc/control.md +++ b/doc/control.md @@ -94,14 +94,18 @@ the content (if supported by the app) relative to the center of the screen. https://github.com/Genymobile/scrcpy/assets/543275/26c4a920-9805-43f1-8d4c-608752d04767 -To simulate a tilt gesture: Shift+_click-and-move-up-or-down_. +To simulate a vertical tilt gesture: Shift+_click-and-move-up-or-down_. https://github.com/Genymobile/scrcpy/assets/543275/1e252341-4a90-4b29-9d11-9153b324669f +Similarly, to simulate a horizontal tilt gesture: +Ctrl+Shift+_click-and-move-left-or-right_. + Technically, _scrcpy_ generates additional touch events from a "virtual finger" at a location inverted through the center of the screen. When pressing Ctrl the _x_ and _y_ coordinates are inverted. Using Shift -only inverts _x_. +only inverts _x_, whereas using Ctrl+Shift only inverts +_y_. This only works for the default mouse mode (`--mouse=sdk`). diff --git a/doc/shortcuts.md b/doc/shortcuts.md index 841ceaa6..4ea37257 100644 --- a/doc/shortcuts.md +++ b/doc/shortcuts.md @@ -53,7 +53,8 @@ _[Super] is typically the Windows or Cmd key._ | Open keyboard settings (HID keyboard only) | MOD+k | Enable/disable FPS counter (on stdout) | MOD+i | Pinch-to-zoom/rotate | Ctrl+_click-and-move_ - | Tilt (slide vertically with 2 fingers) | Shift+_click-and-move_ + | Tilt vertically (slide with 2 fingers) | Shift+_click-and-move_ + | Tilt horizontally (slide with 2 fingers) | Ctrl+Shift+_click-and-move_ | Drag & drop APK file | Install APK from computer | Drag & drop non-APK file | [Push file to device](control.md#push-file-to-device) From ec602a0334357982d75b374f7ac753c5bef1216a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 30 Sep 2024 08:12:08 +0200 Subject: [PATCH 1977/2244] Suggest command line arguments without quotes Replace argument suggestion: --video-encoder='c2.android.avc.encoder' by: --video-encoder=c2.android.avc.encoder On Linux, the quotes are interpreted by the shell, but on Windows they are passed as is. This was harmless, because even transmitted as is, they were interpreted by the shell on the device. However, special characters are now validated since commit bec3321fff4c6dc3b3dbc61fdc6fd98913988a78, making the command fail. Fixes #5329 --- server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java b/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java index aee1594a..45ab4eba 100644 --- a/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java @@ -31,7 +31,7 @@ public final class LogUtils { } else { for (CodecUtils.DeviceEncoder encoder : videoEncoders) { builder.append("\n --video-codec=").append(encoder.getCodec().getName()); - builder.append(" --video-encoder='").append(encoder.getInfo().getName()).append("'"); + builder.append(" --video-encoder=").append(encoder.getInfo().getName()); } } return builder.toString(); @@ -45,7 +45,7 @@ public final class LogUtils { } else { for (CodecUtils.DeviceEncoder encoder : audioEncoders) { builder.append("\n --audio-codec=").append(encoder.getCodec().getName()); - builder.append(" --audio-encoder='").append(encoder.getInfo().getName()).append("'"); + builder.append(" --audio-encoder=").append(encoder.getInfo().getName()); } } return builder.toString(); From c0a6432967c54d739cb0f01e87c834c3927f84f2 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 1 Oct 2024 22:39:06 +0200 Subject: [PATCH 1978/2244] Extract EINTR handling for Os.write() Expose a function which retries automatically on EINTR, and throws an IOException on other errors. --- .../java/com/genymobile/scrcpy/util/IO.java | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/util/IO.java b/server/src/main/java/com/genymobile/scrcpy/util/IO.java index ab3fa59f..5c558c1b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/util/IO.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/IO.java @@ -17,24 +17,30 @@ public final class IO { // not instantiable } + private static int write(FileDescriptor fd, ByteBuffer from) throws IOException { + while (true) { + try { + return Os.write(fd, from); + } catch (ErrnoException e) { + if (e.errno != OsConstants.EINTR) { + throw new IOException(e); + } + } + } + } + public static void writeFully(FileDescriptor fd, ByteBuffer from) throws IOException { // ByteBuffer position is not updated as expected by Os.write() on old Android versions, so // count the remaining bytes manually. // See . int remaining = from.remaining(); while (remaining > 0) { - try { - int w = Os.write(fd, from); - if (BuildConfig.DEBUG && w < 0) { - // w should not be negative, since an exception is thrown on error - throw new AssertionError("Os.write() returned a negative value (" + w + ")"); - } - remaining -= w; - } catch (ErrnoException e) { - if (e.errno != OsConstants.EINTR) { - throw new IOException(e); - } + int w = write(fd, from); + if (BuildConfig.DEBUG && w < 0) { + // w should not be negative, since an exception is thrown on error + throw new AssertionError("Os.write() returned a negative value (" + w + ")"); } + remaining -= w; } } From 79014143b9cc958ed4b36b8e9a49676243ca68b7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 1 Oct 2024 22:49:55 +0200 Subject: [PATCH 1979/2244] Fix IO.writeFully() on Android 5 Os.write() did not update the ByteBuffer position before Android 6. A workaround was added by commit b882322f7371b16acd53677c4a3adbaaed0aef77, which fixed part of the problem, but the position was still not updated across calls, causing the wrong chunk to be written. Refs --- server/src/main/java/com/genymobile/scrcpy/util/IO.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/util/IO.java b/server/src/main/java/com/genymobile/scrcpy/util/IO.java index 5c558c1b..8ef1500d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/util/IO.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/IO.java @@ -31,8 +31,9 @@ public final class IO { public static void writeFully(FileDescriptor fd, ByteBuffer from) throws IOException { // ByteBuffer position is not updated as expected by Os.write() on old Android versions, so - // count the remaining bytes manually. + // handle the position and the remaining bytes manually. // See . + int position = from.position(); int remaining = from.remaining(); while (remaining > 0) { int w = write(fd, from); @@ -41,6 +42,8 @@ public final class IO { throw new AssertionError("Os.write() returned a negative value (" + w + ")"); } remaining -= w; + position += w; + from.position(position); } } From e724ff43490661d9b1c7f92632303a4f08768f03 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 1 Oct 2024 22:50:34 +0200 Subject: [PATCH 1980/2244] Simplify IO.writeFully() for Android >= 6 Do not handle buffer properties manually for Android >= 6 (where it is already handled by Os.write()). Refs --- .../java/com/genymobile/scrcpy/util/IO.java | 33 +++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/util/IO.java b/server/src/main/java/com/genymobile/scrcpy/util/IO.java index 8ef1500d..d9247a98 100644 --- a/server/src/main/java/com/genymobile/scrcpy/util/IO.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/IO.java @@ -2,6 +2,7 @@ package com.genymobile.scrcpy.util; import com.genymobile.scrcpy.BuildConfig; +import android.os.Build; import android.system.ErrnoException; import android.system.Os; import android.system.OsConstants; @@ -30,20 +31,26 @@ public final class IO { } public static void writeFully(FileDescriptor fd, ByteBuffer from) throws IOException { - // ByteBuffer position is not updated as expected by Os.write() on old Android versions, so - // handle the position and the remaining bytes manually. - // See . - int position = from.position(); - int remaining = from.remaining(); - while (remaining > 0) { - int w = write(fd, from); - if (BuildConfig.DEBUG && w < 0) { - // w should not be negative, since an exception is thrown on error - throw new AssertionError("Os.write() returned a negative value (" + w + ")"); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + while (from.hasRemaining()) { + write(fd, from); + } + } else { + // ByteBuffer position is not updated as expected by Os.write() on old Android versions, so + // handle the position and the remaining bytes manually. + // See . + int position = from.position(); + int remaining = from.remaining(); + while (remaining > 0) { + int w = write(fd, from); + if (BuildConfig.DEBUG && w < 0) { + // w should not be negative, since an exception is thrown on error + throw new AssertionError("Os.write() returned a negative value (" + w + ")"); + } + remaining -= w; + position += w; + from.position(position); } - remaining -= w; - position += w; - from.position(position); } } From a6f74d72f52c96fa20ccd49e216383321e4a9efa Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 25 Sep 2024 22:26:07 +0200 Subject: [PATCH 1981/2244] Forward Alt and Super with SDK keyboard Alt and Super (also named Meta) modifier keys are captured for shortcuts by default (cf --shortcut-mod). However, when shortcut modifiers are changed, Alt and Super should be forwarded to the device. This is the case for AOA and UHID keyboards, but it was not the case for SDK keyboard. Fixes #5318 PR #5322 --- app/src/keyboard_sdk.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/keyboard_sdk.c b/app/src/keyboard_sdk.c index 00b7f92a..2d9ca85b 100644 --- a/app/src/keyboard_sdk.c +++ b/app/src/keyboard_sdk.c @@ -45,6 +45,10 @@ convert_keycode(enum sc_keycode from, enum android_keycode *to, uint16_t mod, {SC_KEYCODE_RCTRL, AKEYCODE_CTRL_RIGHT}, {SC_KEYCODE_LSHIFT, AKEYCODE_SHIFT_LEFT}, {SC_KEYCODE_RSHIFT, AKEYCODE_SHIFT_RIGHT}, + {SC_KEYCODE_LALT, AKEYCODE_ALT_LEFT}, + {SC_KEYCODE_RALT, AKEYCODE_ALT_RIGHT}, + {SC_KEYCODE_LGUI, AKEYCODE_META_LEFT}, + {SC_KEYCODE_RGUI, AKEYCODE_META_RIGHT}, }; // Numpad navigation keys. @@ -166,11 +170,7 @@ convert_keycode(enum sc_keycode from, enum android_keycode *to, uint16_t mod, return false; } - if (mod & (SC_MOD_LALT | SC_MOD_RALT | SC_MOD_LGUI | SC_MOD_RGUI)) { - return false; - } - - // if ALT and META are not pressed, also handle letters and space + // Handle letters and space entry = SC_INTMAP_FIND_ENTRY(alphaspace_keys, from); if (entry) { *to = entry->value; From 65fc53eace19392426631ba2f5bcbd9aec88d796 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 25 Sep 2024 22:39:22 +0200 Subject: [PATCH 1982/2244] Simplify (and inline) is_shortcut_mod() Masking was unnecessary (im->sdl_shortcut_mods is implicitly masked). PR #5322 --- 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 444a5f16..0f121da9 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -33,10 +33,10 @@ to_sdl_mod(uint8_t shortcut_mod) { return sdl_mod; } -static bool +static inline bool is_shortcut_mod(struct sc_input_manager *im, uint16_t sdl_mod) { - // keep only the relevant modifier keys - sdl_mod &= SC_SDL_SHORTCUT_MODS_MASK; + // im->sdl_shortcut_mods is within the mask + assert(!(im->sdl_shortcut_mods & ~SC_SDL_SHORTCUT_MODS_MASK)); // at least one shortcut mod pressed? return sdl_mod & im->sdl_shortcut_mods; From 281fcc705254653edc4b418ab68951c6fd069622 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 25 Sep 2024 22:50:44 +0200 Subject: [PATCH 1983/2244] Extract mouse capture Factorize mouse capture for relative mouse mode to reduce code duplication between normal and OTG modes. PR #5322 --- app/meson.build | 1 + app/src/mouse_capture.c | 120 ++++++++++++++++++++++++++++++++++++++ app/src/mouse_capture.h | 35 +++++++++++ app/src/screen.c | 123 ++++----------------------------------- app/src/screen.h | 6 +- app/src/usb/screen_otg.c | 104 ++++----------------------------- app/src/usb/screen_otg.h | 4 +- 7 files changed, 183 insertions(+), 210 deletions(-) create mode 100644 app/src/mouse_capture.c create mode 100644 app/src/mouse_capture.h diff --git a/app/meson.build b/app/meson.build index 99e7e3a2..9d179101 100644 --- a/app/meson.build +++ b/app/meson.build @@ -23,6 +23,7 @@ src = [ 'src/frame_buffer.c', 'src/input_manager.c', 'src/keyboard_sdk.c', + 'src/mouse_capture.c', 'src/mouse_sdk.c', 'src/opengl.c', 'src/options.c', diff --git a/app/src/mouse_capture.c b/app/src/mouse_capture.c new file mode 100644 index 00000000..1420bad6 --- /dev/null +++ b/app/src/mouse_capture.c @@ -0,0 +1,120 @@ +#include "mouse_capture.h" + +#include "util/log.h" + +void +sc_mouse_capture_init(struct sc_mouse_capture *mc, SDL_Window *window) { + mc->window = window; + mc->mouse_capture_key_pressed = SDLK_UNKNOWN; +} + +static inline bool +sc_mouse_capture_is_capture_key(SDL_Keycode key) { + return key == SDLK_LALT || key == SDLK_LGUI || key == SDLK_RGUI; +} + +bool +sc_mouse_capture_handle_event(struct sc_mouse_capture *mc, + const SDL_Event *event) { + switch (event->type) { + case SDL_WINDOWEVENT: + if (event->window.event == SDL_WINDOWEVENT_FOCUS_LOST) { + sc_mouse_capture_set_active(mc, false); + return true; + } + break; + case SDL_KEYDOWN: { + SDL_Keycode key = event->key.keysym.sym; + if (sc_mouse_capture_is_capture_key(key)) { + if (!mc->mouse_capture_key_pressed) { + mc->mouse_capture_key_pressed = key; + } else { + // Another mouse capture key has been pressed, cancel + // mouse (un)capture + mc->mouse_capture_key_pressed = 0; + } + // Mouse capture keys are never forwarded to the device + return true; + } + break; + } + case SDL_KEYUP: { + SDL_Keycode key = event->key.keysym.sym; + SDL_Keycode cap = mc->mouse_capture_key_pressed; + mc->mouse_capture_key_pressed = 0; + if (sc_mouse_capture_is_capture_key(key)) { + if (key == cap) { + // A mouse capture key has been pressed then released: + // toggle the capture mouse mode + sc_mouse_capture_toggle(mc); + } + // Mouse capture keys are never forwarded to the device + return true; + } + break; + } + case SDL_MOUSEWHEEL: + case SDL_MOUSEMOTION: + case SDL_MOUSEBUTTONDOWN: + if (!sc_mouse_capture_is_active(mc)) { + // The mouse will be captured on SDL_MOUSEBUTTONUP, so consume + // the event + return true; + } + break; + case SDL_MOUSEBUTTONUP: + if (!sc_mouse_capture_is_active(mc)) { + sc_mouse_capture_set_active(mc, true); + return true; + } + break; + case SDL_FINGERMOTION: + case SDL_FINGERDOWN: + case SDL_FINGERUP: + // Touch events are not compatible with relative mode + // (coordinates are not relative), so consume the event + return true; + } + + return false; +} + +void +sc_mouse_capture_set_active(struct sc_mouse_capture *mc, bool capture) { +#ifdef __APPLE__ + // Workaround for SDL bug on macOS: + // + if (capture) { + int mouse_x, mouse_y; + SDL_GetGlobalMouseState(&mouse_x, &mouse_y); + + int x, y, w, h; + SDL_GetWindowPosition(window, &x, &y); + SDL_GetWindowSize(window, &w, &h); + + bool outside_window = mouse_x < x || mouse_x >= x + w + || mouse_y < y || mouse_y >= y + h; + if (outside_window) { + SDL_WarpMouseInWindow(mc->window, w / 2, h / 2); + } + } +#else + (void) mc; +#endif + if (SDL_SetRelativeMouseMode(capture)) { + LOGE("Could not set relative mouse mode to %s: %s", + capture ? "true" : "false", SDL_GetError()); + } +} + +bool +sc_mouse_capture_is_active(struct sc_mouse_capture *mc) { + (void) mc; + return SDL_GetRelativeMouseMode(); +} + +void +sc_mouse_capture_toggle(struct sc_mouse_capture *mc) { + bool new_value = !sc_mouse_capture_is_active(mc); + sc_mouse_capture_set_active(mc, new_value); +} diff --git a/app/src/mouse_capture.h b/app/src/mouse_capture.h new file mode 100644 index 00000000..53018c19 --- /dev/null +++ b/app/src/mouse_capture.h @@ -0,0 +1,35 @@ +#ifndef SC_MOUSE_CAPTURE_H +#define SC_MOUSE_CAPTURE_H + +#include "common.h" + +#include + +#include + +struct sc_mouse_capture { + SDL_Window *window; + + // To enable/disable mouse capture, a mouse capture key (LALT, LGUI or + // RGUI) must be pressed. This variable tracks the pressed capture key. + SDL_Keycode mouse_capture_key_pressed; +}; + +void +sc_mouse_capture_init(struct sc_mouse_capture *mc, SDL_Window *window); + +void +sc_mouse_capture_set_active(struct sc_mouse_capture *mc, bool capture); + +bool +sc_mouse_capture_is_active(struct sc_mouse_capture *mc); + +void +sc_mouse_capture_toggle(struct sc_mouse_capture *mc); + +// Return true if it consumed the event +bool +sc_mouse_capture_handle_event(struct sc_mouse_capture *mc, + const SDL_Event *event); + +#endif diff --git a/app/src/screen.c b/app/src/screen.c index ce730f19..146f10a5 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -162,47 +162,6 @@ sc_screen_is_relative_mode(struct sc_screen *screen) { return screen->im.mp && screen->im.mp->relative_mode; } -static void -sc_screen_set_mouse_capture(struct sc_screen *screen, bool capture) { -#ifdef __APPLE__ - // Workaround for SDL bug on macOS: - // - if (capture) { - int mouse_x, mouse_y; - SDL_GetGlobalMouseState(&mouse_x, &mouse_y); - - int x, y, w, h; - SDL_GetWindowPosition(screen->window, &x, &y); - SDL_GetWindowSize(screen->window, &w, &h); - - bool outside_window = mouse_x < x || mouse_x >= x + w - || mouse_y < y || mouse_y >= y + h; - if (outside_window) { - SDL_WarpMouseInWindow(screen->window, w / 2, h / 2); - } - } -#else - (void) screen; -#endif - if (SDL_SetRelativeMouseMode(capture)) { - LOGE("Could not set relative mouse mode to %s: %s", - capture ? "true" : "false", SDL_GetError()); - } -} - -static inline bool -sc_screen_get_mouse_capture(struct sc_screen *screen) { - (void) screen; - return SDL_GetRelativeMouseMode(); -} - -static inline void -sc_screen_toggle_mouse_capture(struct sc_screen *screen) { - (void) screen; - bool new_value = !sc_screen_get_mouse_capture(screen); - sc_screen_set_mouse_capture(screen, new_value); -} - static void sc_screen_update_content_rect(struct sc_screen *screen) { assert(screen->video); @@ -371,7 +330,6 @@ sc_screen_init(struct sc_screen *screen, screen->fullscreen = false; screen->maximized = false; screen->minimized = false; - screen->mouse_capture_key_pressed = 0; screen->paused = false; screen->resume_frame = NULL; screen->orientation = SC_ORIENTATION_0; @@ -486,6 +444,9 @@ sc_screen_init(struct sc_screen *screen, sc_input_manager_init(&screen->im, &im_params); + // Initialize even if not used for simplicity + sc_mouse_capture_init(&screen->mc, screen->window); + #ifdef CONTINUOUS_RESIZING_WORKAROUND if (screen->video) { SDL_AddEventWatch(event_watcher, screen); @@ -506,7 +467,7 @@ sc_screen_init(struct sc_screen *screen, if (!screen->video && sc_screen_is_relative_mode(screen)) { // Capture mouse immediately if video mirroring is disabled - sc_screen_set_mouse_capture(screen, true); + sc_mouse_capture_set_active(&screen->mc, true); } return true; @@ -713,7 +674,7 @@ sc_screen_apply_frame(struct sc_screen *screen) { if (sc_screen_is_relative_mode(screen)) { // Capture mouse on start - sc_screen_set_mouse_capture(screen, true); + sc_mouse_capture_set_active(&screen->mc, true); } } @@ -837,15 +798,8 @@ sc_screen_resize_to_pixel_perfect(struct sc_screen *screen) { content_size.height); } -static inline bool -sc_screen_is_mouse_capture_key(SDL_Keycode key) { - return key == SDLK_LALT || key == SDLK_LGUI || key == SDLK_RGUI; -} - bool sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) { - bool relative_mode = sc_screen_is_relative_mode(screen); - switch (event->type) { case SC_EVENT_SCREEN_INIT_SIZE: { // The initial size is passed via screen->frame_size @@ -903,69 +857,14 @@ sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) { apply_pending_resize(screen); sc_screen_render(screen, true); break; - case SDL_WINDOWEVENT_FOCUS_LOST: - if (relative_mode) { - sc_screen_set_mouse_capture(screen, false); - } - break; } return true; - case SDL_KEYDOWN: - if (relative_mode) { - SDL_Keycode key = event->key.keysym.sym; - if (sc_screen_is_mouse_capture_key(key)) { - if (!screen->mouse_capture_key_pressed) { - screen->mouse_capture_key_pressed = key; - } else { - // Another mouse capture key has been pressed, cancel - // mouse (un)capture - screen->mouse_capture_key_pressed = 0; - } - // Mouse capture keys are never forwarded to the device - return true; - } - } - break; - case SDL_KEYUP: - if (relative_mode) { - SDL_Keycode key = event->key.keysym.sym; - SDL_Keycode cap = screen->mouse_capture_key_pressed; - screen->mouse_capture_key_pressed = 0; - if (sc_screen_is_mouse_capture_key(key)) { - if (key == cap) { - // A mouse capture key has been pressed then released: - // toggle the capture mouse mode - sc_screen_toggle_mouse_capture(screen); - } - // Mouse capture keys are never forwarded to the device - return true; - } - } - break; - case SDL_MOUSEWHEEL: - case SDL_MOUSEMOTION: - case SDL_MOUSEBUTTONDOWN: - if (relative_mode && !sc_screen_get_mouse_capture(screen)) { - // Do not forward to input manager, the mouse will be captured - // on SDL_MOUSEBUTTONUP - return true; - } - break; - case SDL_FINGERMOTION: - case SDL_FINGERDOWN: - case SDL_FINGERUP: - if (relative_mode) { - // Touch events are not compatible with relative mode - // (coordinates are not relative) - return true; - } - break; - case SDL_MOUSEBUTTONUP: - if (relative_mode && !sc_screen_get_mouse_capture(screen)) { - sc_screen_set_mouse_capture(screen, true); - return true; - } - break; + } + + if (sc_screen_is_relative_mode(screen) + && sc_mouse_capture_handle_event(&screen->mc, event)) { + // The mouse capture handler consumed the event + return true; } sc_input_manager_handle_event(&screen->im, event); diff --git a/app/src/screen.h b/app/src/screen.h index 6d5964bd..c716c399 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -13,6 +13,7 @@ #include "fps_counter.h" #include "frame_buffer.h" #include "input_manager.h" +#include "mouse_capture.h" #include "opengl.h" #include "options.h" #include "trait/key_processor.h" @@ -30,6 +31,7 @@ struct sc_screen { struct sc_display display; struct sc_input_manager im; + struct sc_mouse_capture mc; // only used in mouse relative mode struct sc_frame_buffer fb; struct sc_fps_counter fps_counter; @@ -61,10 +63,6 @@ struct sc_screen { bool maximized; bool minimized; - // To enable/disable mouse capture, a mouse capture key (LALT, LGUI or - // RGUI) must be pressed. This variable tracks the pressed capture key. - SDL_Keycode mouse_capture_key_pressed; - AVFrame *frame; bool paused; diff --git a/app/src/usb/screen_otg.c b/app/src/usb/screen_otg.c index b13f8d04..aabb8a7f 100644 --- a/app/src/usb/screen_otg.c +++ b/app/src/usb/screen_otg.c @@ -4,47 +4,6 @@ #include "options.h" #include "util/log.h" -static void -sc_screen_otg_set_mouse_capture(struct sc_screen_otg *screen, bool capture) { -#ifdef __APPLE__ - // Workaround for SDL bug on macOS: - // - if (capture) { - int mouse_x, mouse_y; - SDL_GetGlobalMouseState(&mouse_x, &mouse_y); - - int x, y, w, h; - SDL_GetWindowPosition(screen->window, &x, &y); - SDL_GetWindowSize(screen->window, &w, &h); - - bool outside_window = mouse_x < x || mouse_x >= x + w - || mouse_y < y || mouse_y >= y + h; - if (outside_window) { - SDL_WarpMouseInWindow(screen->window, w / 2, h / 2); - } - } -#else - (void) screen; -#endif - if (SDL_SetRelativeMouseMode(capture)) { - LOGE("Could not set relative mouse mode to %s: %s", - capture ? "true" : "false", SDL_GetError()); - } -} - -static inline bool -sc_screen_otg_get_mouse_capture(struct sc_screen_otg *screen) { - (void) screen; - return SDL_GetRelativeMouseMode(); -} - -static inline void -sc_screen_otg_toggle_mouse_capture(struct sc_screen_otg *screen) { - (void) screen; - bool new_value = !sc_screen_otg_get_mouse_capture(screen); - sc_screen_otg_set_mouse_capture(screen, new_value); -} - static void sc_screen_otg_render(struct sc_screen_otg *screen) { SDL_RenderClear(screen->renderer); @@ -61,8 +20,6 @@ sc_screen_otg_init(struct sc_screen_otg *screen, screen->mouse = params->mouse; screen->gamepad = params->gamepad; - screen->mouse_capture_key_pressed = 0; - const char *title = params->window_title; assert(title); @@ -113,9 +70,11 @@ sc_screen_otg_init(struct sc_screen_otg *screen, LOGW("Could not load icon"); } + sc_mouse_capture_init(&screen->mc, screen->window); + if (screen->mouse) { // Capture mouse on start - sc_screen_otg_set_mouse_capture(screen, true); + sc_mouse_capture_set_active(&screen->mc, true); } return true; @@ -137,11 +96,6 @@ sc_screen_otg_destroy(struct sc_screen_otg *screen) { SDL_DestroyWindow(screen->window); } -static inline bool -sc_screen_otg_is_mouse_capture_key(SDL_Keycode key) { - return key == SDLK_LALT || key == SDLK_LGUI || key == SDLK_RGUI; -} - static void sc_screen_otg_process_key(struct sc_screen_otg *screen, const SDL_KeyboardEvent *event) { @@ -298,80 +252,46 @@ sc_screen_otg_process_gamepad_button(struct sc_screen_otg *screen, void sc_screen_otg_handle_event(struct sc_screen_otg *screen, SDL_Event *event) { + if (sc_mouse_capture_handle_event(&screen->mc, event)) { + // The mouse capture handler consumed the event + return; + } + switch (event->type) { case SDL_WINDOWEVENT: switch (event->window.event) { case SDL_WINDOWEVENT_EXPOSED: sc_screen_otg_render(screen); break; - case SDL_WINDOWEVENT_FOCUS_LOST: - if (screen->mouse) { - sc_screen_otg_set_mouse_capture(screen, false); - } - break; } return; case SDL_KEYDOWN: - if (screen->mouse) { - SDL_Keycode key = event->key.keysym.sym; - if (sc_screen_otg_is_mouse_capture_key(key)) { - if (!screen->mouse_capture_key_pressed) { - screen->mouse_capture_key_pressed = key; - } else { - // Another mouse capture key has been pressed, cancel - // mouse (un)capture - screen->mouse_capture_key_pressed = 0; - } - // Mouse capture keys are never forwarded to the device - return; - } - } - if (screen->keyboard) { sc_screen_otg_process_key(screen, &event->key); } break; case SDL_KEYUP: - if (screen->mouse) { - SDL_Keycode key = event->key.keysym.sym; - SDL_Keycode cap = screen->mouse_capture_key_pressed; - screen->mouse_capture_key_pressed = 0; - if (sc_screen_otg_is_mouse_capture_key(key)) { - if (key == cap) { - // A mouse capture key has been pressed then released: - // toggle the capture mouse mode - sc_screen_otg_toggle_mouse_capture(screen); - } - // Mouse capture keys are never forwarded to the device - return; - } - } - if (screen->keyboard) { sc_screen_otg_process_key(screen, &event->key); } break; case SDL_MOUSEMOTION: - if (screen->mouse && sc_screen_otg_get_mouse_capture(screen)) { + if (screen->mouse) { sc_screen_otg_process_mouse_motion(screen, &event->motion); } break; case SDL_MOUSEBUTTONDOWN: - if (screen->mouse && sc_screen_otg_get_mouse_capture(screen)) { + if (screen->mouse) { sc_screen_otg_process_mouse_button(screen, &event->button); } break; case SDL_MOUSEBUTTONUP: if (screen->mouse) { - if (sc_screen_otg_get_mouse_capture(screen)) { - sc_screen_otg_process_mouse_button(screen, &event->button); - } else { - sc_screen_otg_set_mouse_capture(screen, true); - } + sc_screen_otg_process_mouse_button(screen, &event->button); } break; case SDL_MOUSEWHEEL: - if (screen->mouse && sc_screen_otg_get_mouse_capture(screen)) { + if (screen->mouse) { sc_screen_otg_process_mouse_wheel(screen, &event->wheel); } break; diff --git a/app/src/usb/screen_otg.h b/app/src/usb/screen_otg.h index 2ea76eda..850a6ae5 100644 --- a/app/src/usb/screen_otg.h +++ b/app/src/usb/screen_otg.h @@ -8,6 +8,7 @@ #include "keyboard_aoa.h" #include "mouse_aoa.h" +#include "mouse_capture.h" #include "gamepad_aoa.h" struct sc_screen_otg { @@ -19,8 +20,7 @@ struct sc_screen_otg { SDL_Renderer *renderer; SDL_Texture *texture; - // See equivalent mechanism in screen.h - SDL_Keycode mouse_capture_key_pressed; + struct sc_mouse_capture mc; }; struct sc_screen_otg_params { From a36de26969791d054cfd3729e1c29923fec61b32 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 27 Sep 2024 18:24:21 +0200 Subject: [PATCH 1984/2244] Move shortcut mod functions to a separate header This will allow to reuse it for mouse capture keys, which are handled by a component separate from the input manager. PR #5322 --- app/src/input_manager.c | 56 +++++--------------------------------- app/src/shortcut_mod.h | 60 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 49 deletions(-) create mode 100644 app/src/shortcut_mod.h diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 0f121da9..969196e3 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -5,53 +5,9 @@ #include "input_events.h" #include "screen.h" +#include "shortcut_mod.h" #include "util/log.h" -#define SC_SDL_SHORTCUT_MODS_MASK (KMOD_CTRL | KMOD_ALT | KMOD_GUI) - -static inline uint16_t -to_sdl_mod(uint8_t shortcut_mod) { - uint16_t sdl_mod = 0; - if (shortcut_mod & SC_SHORTCUT_MOD_LCTRL) { - sdl_mod |= KMOD_LCTRL; - } - if (shortcut_mod & SC_SHORTCUT_MOD_RCTRL) { - sdl_mod |= KMOD_RCTRL; - } - if (shortcut_mod & SC_SHORTCUT_MOD_LALT) { - sdl_mod |= KMOD_LALT; - } - if (shortcut_mod & SC_SHORTCUT_MOD_RALT) { - sdl_mod |= KMOD_RALT; - } - if (shortcut_mod & SC_SHORTCUT_MOD_LSUPER) { - sdl_mod |= KMOD_LGUI; - } - if (shortcut_mod & SC_SHORTCUT_MOD_RSUPER) { - sdl_mod |= KMOD_RGUI; - } - return sdl_mod; -} - -static inline bool -is_shortcut_mod(struct sc_input_manager *im, uint16_t sdl_mod) { - // im->sdl_shortcut_mods is within the mask - assert(!(im->sdl_shortcut_mods & ~SC_SDL_SHORTCUT_MODS_MASK)); - - // at least one shortcut mod pressed? - return sdl_mod & im->sdl_shortcut_mods; -} - -static bool -is_shortcut_key(struct sc_input_manager *im, SDL_Keycode keycode) { - return (im->sdl_shortcut_mods & KMOD_LCTRL && keycode == SDLK_LCTRL) - || (im->sdl_shortcut_mods & KMOD_RCTRL && keycode == SDLK_RCTRL) - || (im->sdl_shortcut_mods & KMOD_LALT && keycode == SDLK_LALT) - || (im->sdl_shortcut_mods & KMOD_RALT && keycode == SDLK_RALT) - || (im->sdl_shortcut_mods & KMOD_LGUI && keycode == SDLK_LGUI) - || (im->sdl_shortcut_mods & KMOD_RGUI && keycode == SDLK_RGUI); -} - void sc_input_manager_init(struct sc_input_manager *im, const struct sc_input_manager_params *params) { @@ -73,7 +29,7 @@ sc_input_manager_init(struct sc_input_manager *im, im->legacy_paste = params->legacy_paste; im->clipboard_autosync = params->clipboard_autosync; - im->sdl_shortcut_mods = to_sdl_mod(params->shortcut_mods); + im->sdl_shortcut_mods = sc_shortcut_mods_to_sdl(params->shortcut_mods); im->vfinger_down = false; im->vfinger_invert_x = false; @@ -346,7 +302,8 @@ sc_input_manager_process_text_input(struct sc_input_manager *im, return; } - if (is_shortcut_mod(im, SDL_GetModState())) { + if (sc_shortcut_mods_is_shortcut_mod(im->sdl_shortcut_mods, + SDL_GetModState())) { // A shortcut must never generate text events return; } @@ -413,8 +370,9 @@ sc_input_manager_process_key(struct sc_input_manager *im, // press/release is a modifier key. // The second condition is necessary to ignore the release of the modifier // key (because in this case mod is 0). - bool is_shortcut = is_shortcut_mod(im, mod) - || is_shortcut_key(im, sdl_keycode); + uint16_t mods = im->sdl_shortcut_mods; + bool is_shortcut = sc_shortcut_mods_is_shortcut_mod(mods, mod) + || sc_shortcut_mods_is_shortcut_key(mods, sdl_keycode); if (down && !repeat) { if (sdl_keycode == im->last_keycode && mod == im->last_mod) { diff --git a/app/src/shortcut_mod.h b/app/src/shortcut_mod.h new file mode 100644 index 00000000..b685e987 --- /dev/null +++ b/app/src/shortcut_mod.h @@ -0,0 +1,60 @@ +#ifndef SC_SHORTCUT_MOD_H +#define SC_SHORTCUT_MOD_H + +#include "common.h" + +#include +#include +#include + +#include "options.h" + +#define SC_SDL_SHORTCUT_MODS_MASK (KMOD_CTRL | KMOD_ALT | KMOD_GUI) + +// input: OR of enum sc_shortcut_mod +// output: OR of SDL_Keymod +static inline uint16_t +sc_shortcut_mods_to_sdl(uint8_t shortcut_mods) { + uint16_t sdl_mod = 0; + if (shortcut_mods & SC_SHORTCUT_MOD_LCTRL) { + sdl_mod |= KMOD_LCTRL; + } + if (shortcut_mods & SC_SHORTCUT_MOD_RCTRL) { + sdl_mod |= KMOD_RCTRL; + } + if (shortcut_mods & SC_SHORTCUT_MOD_LALT) { + sdl_mod |= KMOD_LALT; + } + if (shortcut_mods & SC_SHORTCUT_MOD_RALT) { + sdl_mod |= KMOD_RALT; + } + if (shortcut_mods & SC_SHORTCUT_MOD_LSUPER) { + sdl_mod |= KMOD_LGUI; + } + if (shortcut_mods & SC_SHORTCUT_MOD_RSUPER) { + sdl_mod |= KMOD_RGUI; + } + return sdl_mod; +} + +static inline bool +sc_shortcut_mods_is_shortcut_mod(uint16_t sdl_shortcut_mods, uint16_t sdl_mod) { + // sdl_shortcut_mods must be within the mask + assert(!(sdl_shortcut_mods & ~SC_SDL_SHORTCUT_MODS_MASK)); + + // at least one shortcut mod pressed? + return sdl_mod & sdl_shortcut_mods; +} + +static inline bool +sc_shortcut_mods_is_shortcut_key(uint16_t sdl_shortcut_mods, + SDL_Keycode keycode) { + return (sdl_shortcut_mods & KMOD_LCTRL && keycode == SDLK_LCTRL) + || (sdl_shortcut_mods & KMOD_RCTRL && keycode == SDLK_RCTRL) + || (sdl_shortcut_mods & KMOD_LALT && keycode == SDLK_LALT) + || (sdl_shortcut_mods & KMOD_RALT && keycode == SDLK_RALT) + || (sdl_shortcut_mods & KMOD_LGUI && keycode == SDLK_LGUI) + || (sdl_shortcut_mods & KMOD_RGUI && keycode == SDLK_RGUI); +} + +#endif From ff9fb5994dbe555be8835a5f8d06b03e9f3b1b27 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 27 Sep 2024 18:32:39 +0200 Subject: [PATCH 1985/2244] Use shortcut mods as mouse capture keys Instead of using separate hardcoded keys for mouse capture/uncapture, use the shortcut mods. By changing the shortcut mods (for example --shortcut-mod=rctrl), it allows to forward Alt and Super to the device. Fixes #5318 PR #5322 --- app/src/mouse_capture.c | 13 ++++++++----- app/src/mouse_capture.h | 5 ++++- app/src/screen.c | 2 +- app/src/usb/scrcpy_otg.c | 1 + app/src/usb/screen_otg.c | 2 +- app/src/usb/screen_otg.h | 1 + 6 files changed, 16 insertions(+), 8 deletions(-) diff --git a/app/src/mouse_capture.c b/app/src/mouse_capture.c index 1420bad6..ee96ae60 100644 --- a/app/src/mouse_capture.c +++ b/app/src/mouse_capture.c @@ -1,16 +1,19 @@ #include "mouse_capture.h" +#include "shortcut_mod.h" #include "util/log.h" void -sc_mouse_capture_init(struct sc_mouse_capture *mc, SDL_Window *window) { +sc_mouse_capture_init(struct sc_mouse_capture *mc, SDL_Window *window, + uint8_t shortcut_mods) { mc->window = window; + mc->sdl_mouse_capture_keys = sc_shortcut_mods_to_sdl(shortcut_mods); mc->mouse_capture_key_pressed = SDLK_UNKNOWN; } static inline bool -sc_mouse_capture_is_capture_key(SDL_Keycode key) { - return key == SDLK_LALT || key == SDLK_LGUI || key == SDLK_RGUI; +sc_mouse_capture_is_capture_key(struct sc_mouse_capture *mc, SDL_Keycode key) { + return sc_shortcut_mods_is_shortcut_key(mc->sdl_mouse_capture_keys, key); } bool @@ -25,7 +28,7 @@ sc_mouse_capture_handle_event(struct sc_mouse_capture *mc, break; case SDL_KEYDOWN: { SDL_Keycode key = event->key.keysym.sym; - if (sc_mouse_capture_is_capture_key(key)) { + if (sc_mouse_capture_is_capture_key(mc, key)) { if (!mc->mouse_capture_key_pressed) { mc->mouse_capture_key_pressed = key; } else { @@ -42,7 +45,7 @@ sc_mouse_capture_handle_event(struct sc_mouse_capture *mc, SDL_Keycode key = event->key.keysym.sym; SDL_Keycode cap = mc->mouse_capture_key_pressed; mc->mouse_capture_key_pressed = 0; - if (sc_mouse_capture_is_capture_key(key)) { + if (sc_mouse_capture_is_capture_key(mc, key)) { if (key == cap) { // A mouse capture key has been pressed then released: // toggle the capture mouse mode diff --git a/app/src/mouse_capture.h b/app/src/mouse_capture.h index 53018c19..f352cc13 100644 --- a/app/src/mouse_capture.h +++ b/app/src/mouse_capture.h @@ -9,14 +9,17 @@ struct sc_mouse_capture { SDL_Window *window; + uint16_t sdl_mouse_capture_keys; // To enable/disable mouse capture, a mouse capture key (LALT, LGUI or // RGUI) must be pressed. This variable tracks the pressed capture key. SDL_Keycode mouse_capture_key_pressed; + }; void -sc_mouse_capture_init(struct sc_mouse_capture *mc, SDL_Window *window); +sc_mouse_capture_init(struct sc_mouse_capture *mc, SDL_Window *window, + uint8_t shortcut_mods); void sc_mouse_capture_set_active(struct sc_mouse_capture *mc, bool capture); diff --git a/app/src/screen.c b/app/src/screen.c index 146f10a5..1d694f12 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -445,7 +445,7 @@ sc_screen_init(struct sc_screen *screen, sc_input_manager_init(&screen->im, &im_params); // Initialize even if not used for simplicity - sc_mouse_capture_init(&screen->mc, screen->window); + sc_mouse_capture_init(&screen->mc, screen->window, params->shortcut_mods); #ifdef CONTINUOUS_RESIZING_WORKAROUND if (screen->video) { diff --git a/app/src/usb/scrcpy_otg.c b/app/src/usb/scrcpy_otg.c index 9595face..1a7e9544 100644 --- a/app/src/usb/scrcpy_otg.c +++ b/app/src/usb/scrcpy_otg.c @@ -185,6 +185,7 @@ scrcpy_otg(struct scrcpy_options *options) { .window_width = options->window_width, .window_height = options->window_height, .window_borderless = options->window_borderless, + .shortcut_mods = options->shortcut_mods, }; ok = sc_screen_otg_init(&s->screen_otg, ¶ms); diff --git a/app/src/usb/screen_otg.c b/app/src/usb/screen_otg.c index aabb8a7f..18377074 100644 --- a/app/src/usb/screen_otg.c +++ b/app/src/usb/screen_otg.c @@ -70,7 +70,7 @@ sc_screen_otg_init(struct sc_screen_otg *screen, LOGW("Could not load icon"); } - sc_mouse_capture_init(&screen->mc, screen->window); + sc_mouse_capture_init(&screen->mc, screen->window, params->shortcut_mods); if (screen->mouse) { // Capture mouse on start diff --git a/app/src/usb/screen_otg.h b/app/src/usb/screen_otg.h index 850a6ae5..427723ad 100644 --- a/app/src/usb/screen_otg.h +++ b/app/src/usb/screen_otg.h @@ -35,6 +35,7 @@ struct sc_screen_otg_params { uint16_t window_width; uint16_t window_height; bool window_borderless; + uint8_t shortcut_mods; // OR of enum sc_shortcut_mod values }; bool From 064670ab4c7d740ba6a02a1242d24edd267d2b76 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 29 Sep 2024 17:20:35 +0200 Subject: [PATCH 1986/2244] Add missing include common.h --- app/src/usb/aoa_hid.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/usb/aoa_hid.h b/app/src/usb/aoa_hid.h index 00961c28..9cc6355e 100644 --- a/app/src/usb/aoa_hid.h +++ b/app/src/usb/aoa_hid.h @@ -1,6 +1,8 @@ #ifndef SC_AOA_HID_H #define SC_AOA_HID_H +#include "common.h" + #include #include From 0d8014be5260b64daf3d7b3516d37a0dfafca7e1 Mon Sep 17 00:00:00 2001 From: Yan Date: Mon, 7 Oct 2024 16:43:01 +0200 Subject: [PATCH 1987/2244] Fix build error on macOS Fix window access typo for macOS. PR #5348 Signed-off-by: Romain Vimont --- app/src/mouse_capture.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/mouse_capture.c b/app/src/mouse_capture.c index ee96ae60..25345faa 100644 --- a/app/src/mouse_capture.c +++ b/app/src/mouse_capture.c @@ -92,8 +92,8 @@ sc_mouse_capture_set_active(struct sc_mouse_capture *mc, bool capture) { SDL_GetGlobalMouseState(&mouse_x, &mouse_y); int x, y, w, h; - SDL_GetWindowPosition(window, &x, &y); - SDL_GetWindowSize(window, &w, &h); + SDL_GetWindowPosition(mc->window, &x, &y); + SDL_GetWindowSize(mc->window, &w, &h); bool outside_window = mouse_x < x || mouse_x >= x + w || mouse_y < y || mouse_y >= y + h; From 5b10650f22b218b2fb42415aefe4bd6fec6c7e9e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 8 Oct 2024 18:12:55 +0200 Subject: [PATCH 1988/2244] Fix time-limit early interruption If a value for --time-limit was set, then the thread was not interrupted on stop (the condvar was not signaled). --- app/src/util/timeout.c | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/util/timeout.c b/app/src/util/timeout.c index a1665373..159a4681 100644 --- a/app/src/util/timeout.c +++ b/app/src/util/timeout.c @@ -62,6 +62,7 @@ void sc_timeout_stop(struct sc_timeout *timeout) { sc_mutex_lock(&timeout->mutex); timeout->stopped = true; + sc_cond_signal(&timeout->cond); sc_mutex_unlock(&timeout->mutex); } From afbaf59abba79a79ab6f4c659ff3d832e02a8e7f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 8 Oct 2024 18:18:05 +0200 Subject: [PATCH 1989/2244] Cast to sc_tick type in conversion macros With the old macros definitions, the type of the result depended on the type of `sec`. In particular, if sec is a 32-bit type, sec * 1000000 was likely to overflow (even if the result was assigned to a sc_tick by the caller of the macro). This was the case on Windows, where the long type is a 32-bit signed integer: the --time-limit argument, expressed in seconds, was first parsed to a long value, then multiplied by 1000000 by the SC_TICK_FROM_SEC() macro, causing an overflow when the value was greater than 2147 (2^31 / 1000000). Fixes #5355 --- app/src/util/tick.h | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/src/util/tick.h b/app/src/util/tick.h index 2d941f23..b037734b 100644 --- a/app/src/util/tick.h +++ b/app/src/util/tick.h @@ -10,14 +10,14 @@ typedef int64_t sc_tick; #define SC_TICK_FREQ 1000000 // microsecond // To be adapted if SC_TICK_FREQ changes -#define SC_TICK_TO_NS(tick) ((tick) * 1000) -#define SC_TICK_TO_US(tick) (tick) -#define SC_TICK_TO_MS(tick) ((tick) / 1000) -#define SC_TICK_TO_SEC(tick) ((tick) / 1000000) -#define SC_TICK_FROM_NS(ns) ((ns) / 1000) -#define SC_TICK_FROM_US(us) (us) -#define SC_TICK_FROM_MS(ms) ((ms) * 1000) -#define SC_TICK_FROM_SEC(sec) ((sec) * 1000000) +#define SC_TICK_TO_NS(tick) ((sc_tick) (tick) * 1000) +#define SC_TICK_TO_US(tick) ((sc_tick) tick) +#define SC_TICK_TO_MS(tick) ((sc_tick) (tick) / 1000) +#define SC_TICK_TO_SEC(tick) ((sc_tick) (tick) / 1000000) +#define SC_TICK_FROM_NS(ns) ((sc_tick) (ns) / 1000) +#define SC_TICK_FROM_US(us) ((sc_tick) us) +#define SC_TICK_FROM_MS(ms) ((sc_tick) (ms) * 1000) +#define SC_TICK_FROM_SEC(sec) ((sc_tick) (sec) * 1000000) sc_tick sc_tick_now(void); From 09741bc8051fc0d131c00690088390b8b36dd672 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 11 Oct 2024 22:42:39 +0200 Subject: [PATCH 1990/2244] Do not duplicate server string params The server params were passed from the main thread to the server thread, so a deep copy was performed in case the caller instance was destroyed. But in practice, it only contains memory that lives until the end of the program (command line arguments), so simply reference it. Several copies of string fields were missing anyway. --- app/src/server.c | 64 +++--------------------------------------------- 1 file changed, 4 insertions(+), 60 deletions(-) diff --git a/app/src/server.c b/app/src/server.c index 90a0ac5d..b7f3b56d 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -66,56 +66,6 @@ get_server_path(void) { return server_path; } -static void -sc_server_params_destroy(struct sc_server_params *params) { - // The server stores a copy of the params provided by the user - free((char *) params->req_serial); - free((char *) params->crop); - free((char *) params->video_codec_options); - free((char *) params->audio_codec_options); - free((char *) params->video_encoder); - free((char *) params->audio_encoder); - free((char *) params->tcpip_dst); - free((char *) params->camera_id); - free((char *) params->camera_ar); -} - -static bool -sc_server_params_copy(struct sc_server_params *dst, - const struct sc_server_params *src) { - *dst = *src; - - // The params reference user-allocated memory, so we must copy them to - // handle them from another thread - -#define COPY(FIELD) do { \ - dst->FIELD = NULL; \ - if (src->FIELD) { \ - dst->FIELD = strdup(src->FIELD); \ - if (!dst->FIELD) { \ - goto error; \ - } \ - } \ -} while(0) - - COPY(req_serial); - COPY(crop); - COPY(video_codec_options); - COPY(audio_codec_options); - COPY(video_encoder); - COPY(audio_encoder); - COPY(tcpip_dst); - COPY(camera_id); - COPY(camera_ar); -#undef COPY - - return true; - -error: - sc_server_params_destroy(dst); - return false; -} - static bool push_server(struct sc_intr *intr, const char *serial) { char *server_path = get_server_path(); @@ -499,22 +449,18 @@ connect_to_server(struct sc_server *server, unsigned attempts, sc_tick delay, bool sc_server_init(struct sc_server *server, const struct sc_server_params *params, const struct sc_server_callbacks *cbs, void *cbs_userdata) { - bool ok = sc_server_params_copy(&server->params, params); - if (!ok) { - LOG_OOM(); - return false; - } + // The allocated data in params (const char *) must remain valid until the + // end of the program + server->params = *params; - ok = sc_mutex_init(&server->mutex); + bool ok = sc_mutex_init(&server->mutex); if (!ok) { - sc_server_params_destroy(&server->params); return false; } ok = sc_cond_init(&server->cond_stopped); if (!ok) { sc_mutex_destroy(&server->mutex); - sc_server_params_destroy(&server->params); return false; } @@ -522,7 +468,6 @@ sc_server_init(struct sc_server *server, const struct sc_server_params *params, if (!ok) { sc_cond_destroy(&server->cond_stopped); sc_mutex_destroy(&server->mutex); - sc_server_params_destroy(&server->params); return false; } @@ -1161,7 +1106,6 @@ sc_server_destroy(struct sc_server *server) { free(server->serial); free(server->device_socket_name); - sc_server_params_destroy(&server->params); sc_intr_destroy(&server->intr); sc_cond_destroy(&server->cond_stopped); sc_mutex_destroy(&server->mutex); From c15df01171793dca7074a012f4703a185d99169d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 11 Oct 2024 22:51:15 +0200 Subject: [PATCH 1991/2244] Reject non-positive camera sizes early Throw an exception on parsing if the camera size dimensions are not both positive. --- server/src/main/java/com/genymobile/scrcpy/Options.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 51daeced..9eab1d90 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -475,6 +475,9 @@ public class Options { } int width = Integer.parseInt(tokens[0]); int height = Integer.parseInt(tokens[1]); + if (width <= 0 || height <= 0) { + throw new IllegalArgumentException("Invalid non-positive size dimension: \"" + size + "\""); + } return new Size(width, height); } From e33be3d288fb6ec764a07c56eb4299b44c4793ac Mon Sep 17 00:00:00 2001 From: dillonfrederica Date: Sat, 12 Oct 2024 18:12:46 +0800 Subject: [PATCH 1992/2244] Fix SDL_events.h include All SDL includes must be prefixed by "SDL2/". Fixed #5372 Signed-off-by: Romain Vimont --- app/src/events.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/events.h b/app/src/events.h index 59c55de4..2fe4d3a7 100644 --- a/app/src/events.h +++ b/app/src/events.h @@ -5,7 +5,7 @@ #include #include -#include +#include enum { SC_EVENT_NEW_FRAME = SDL_USEREVENT, From 3acffaae57238ee47e05f97f8e762a04550fdad8 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 20 Oct 2024 13:01:52 +0200 Subject: [PATCH 1993/2244] Use explicit constants for Android versions Who remembers code names? This avoids to check the mapping every time. --- .../genymobile/scrcpy/AndroidVersions.java | 32 +++++++++++++++++++ .../com/genymobile/scrcpy/FakeContext.java | 3 +- .../java/com/genymobile/scrcpy/Server.java | 2 +- .../com/genymobile/scrcpy/Workarounds.java | 8 ++--- .../scrcpy/audio/AudioDirectCapture.java | 11 ++++--- .../genymobile/scrcpy/audio/AudioEncoder.java | 9 +++--- .../scrcpy/audio/AudioPlaybackCapture.java | 5 +-- .../scrcpy/audio/AudioRawRecorder.java | 3 +- .../scrcpy/audio/AudioRecordReader.java | 4 +-- .../genymobile/scrcpy/control/Controller.java | 7 ++-- .../scrcpy/control/UhidManager.java | 7 ++-- .../com/genymobile/scrcpy/device/Device.java | 15 +++++---- .../java/com/genymobile/scrcpy/util/IO.java | 3 +- .../com/genymobile/scrcpy/util/Settings.java | 7 ++-- .../scrcpy/video/CameraCapture.java | 10 +++--- .../scrcpy/video/ScreenCapture.java | 5 +-- .../scrcpy/video/SurfaceEncoder.java | 3 +- .../scrcpy/wrappers/ActivityManager.java | 4 +-- .../scrcpy/wrappers/ClipboardManager.java | 13 ++++---- .../scrcpy/wrappers/ContentProvider.java | 5 +-- .../scrcpy/wrappers/DisplayControl.java | 4 +-- .../scrcpy/wrappers/PowerManager.java | 3 +- .../scrcpy/wrappers/SurfaceControl.java | 7 ++-- 23 files changed, 108 insertions(+), 62 deletions(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/AndroidVersions.java diff --git a/server/src/main/java/com/genymobile/scrcpy/AndroidVersions.java b/server/src/main/java/com/genymobile/scrcpy/AndroidVersions.java new file mode 100644 index 00000000..8acad7ee --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/AndroidVersions.java @@ -0,0 +1,32 @@ +package com.genymobile.scrcpy; + +import android.os.Build; + +/** + * Android version code constants, done right. + *

+ * API levels + */ +public final class AndroidVersions { + + private AndroidVersions() { + // not instantiable + } + + public static final int API_20_ANDROID_4_4 = Build.VERSION_CODES.KITKAT_WATCH; + public static final int API_21_ANDROID_5_0 = Build.VERSION_CODES.LOLLIPOP; + public static final int API_22_ANDROID_5_1 = Build.VERSION_CODES.LOLLIPOP_MR1; + public static final int API_23_ANDROID_6_0 = Build.VERSION_CODES.M; + public static final int API_24_ANDROID_7_0 = Build.VERSION_CODES.N; + public static final int API_25_ANDROID_7_1 = Build.VERSION_CODES.N_MR1; + public static final int API_26_ANDROID_8_0 = Build.VERSION_CODES.O; + public static final int API_27_ANDROID_8_1 = Build.VERSION_CODES.O_MR1; + public static final int API_28_ANDROID_9 = Build.VERSION_CODES.P; + public static final int API_29_ANDROID_10 = Build.VERSION_CODES.Q; + public static final int API_30_ANDROID_11 = Build.VERSION_CODES.R; + public static final int API_31_ANDROID_12 = Build.VERSION_CODES.S; + public static final int API_32_ANDROID_12L = Build.VERSION_CODES.S_V2; + public static final int API_33_ANDROID_13 = Build.VERSION_CODES.TIRAMISU; + public static final int API_34_ANDROID_14 = Build.VERSION_CODES.UPSIDE_DOWN_CAKE; + +} diff --git a/server/src/main/java/com/genymobile/scrcpy/FakeContext.java b/server/src/main/java/com/genymobile/scrcpy/FakeContext.java index 2ea7bf4a..0b086cc5 100644 --- a/server/src/main/java/com/genymobile/scrcpy/FakeContext.java +++ b/server/src/main/java/com/genymobile/scrcpy/FakeContext.java @@ -4,7 +4,6 @@ import android.annotation.TargetApi; import android.content.AttributionSource; import android.content.Context; import android.content.ContextWrapper; -import android.os.Build; import android.os.Process; public final class FakeContext extends ContextWrapper { @@ -32,7 +31,7 @@ public final class FakeContext extends ContextWrapper { return PACKAGE_NAME; } - @TargetApi(Build.VERSION_CODES.S) + @TargetApi(AndroidVersions.API_31_ANDROID_12) @Override public AttributionSource getAttributionSource() { AttributionSource.Builder builder = new AttributionSource.Builder(Process.SHELL_UID); diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 7817fdf5..555cf97a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -121,7 +121,7 @@ public final class Server { } private static void scrcpy(Options options) throws IOException, ConfigurationException { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S && options.getVideoSource() == VideoSource.CAMERA) { + if (Build.VERSION.SDK_INT < AndroidVersions.API_31_ANDROID_12 && options.getVideoSource() == VideoSource.CAMERA) { Ln.e("Camera mirroring is not supported before Android 12"); throw new ConfigurationException("Camera mirroring is not supported"); } diff --git a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java index 7de98b72..eec00a04 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java +++ b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java @@ -52,7 +52,7 @@ public final class Workarounds { } public static void apply() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + if (Build.VERSION.SDK_INT >= AndroidVersions.API_31_ANDROID_12) { // On some Samsung devices, DisplayManagerGlobal.getDisplayInfoLocked() calls ActivityThread.currentActivityThread().getConfiguration(), // which requires a non-null ConfigurationController. // ConfigurationController was introduced in Android 12, so do not attempt to set it on lower versions. @@ -155,7 +155,7 @@ public final class Workarounds { } } - @TargetApi(Build.VERSION_CODES.R) + @TargetApi(AndroidVersions.API_30_ANDROID_11) @SuppressLint("WrongConstant,MissingPermission") public static AudioRecord createAudioRecord(int source, int sampleRate, int channelConfig, int channels, int channelMask, int encoding) throws AudioCaptureException { @@ -226,7 +226,7 @@ public final class Workarounds { int[] session = new int[]{AudioManager.AUDIO_SESSION_ID_GENERATE}; int initResult; - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { + if (Build.VERSION.SDK_INT < AndroidVersions.API_31_ANDROID_12) { // private native final int native_setup(Object audiorecord_this, // Object /*AudioAttributes*/ attributes, // int[] sampleRate, int channelMask, int channelIndexMask, int audioFormat, @@ -252,7 +252,7 @@ public final class Workarounds { Method getParcelMethod = attributionSourceState.getClass().getDeclaredMethod("getParcel"); Parcel attributionSourceParcel = (Parcel) getParcelMethod.invoke(attributionSourceState); - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + if (Build.VERSION.SDK_INT < AndroidVersions.API_34_ANDROID_14) { // private native int native_setup(Object audiorecordThis, // Object /*AudioAttributes*/ attributes, // int[] sampleRate, int channelMask, int channelIndexMask, int audioFormat, diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioDirectCapture.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioDirectCapture.java index 8d4a4c2d..5c859738 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioDirectCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioDirectCapture.java @@ -1,5 +1,6 @@ package com.genymobile.scrcpy.audio; +import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.FakeContext; import com.genymobile.scrcpy.Workarounds; import com.genymobile.scrcpy.util.Ln; @@ -45,11 +46,11 @@ public class AudioDirectCapture implements AudioCapture { } } - @TargetApi(Build.VERSION_CODES.M) + @TargetApi(AndroidVersions.API_23_ANDROID_6_0) @SuppressLint({"WrongConstant", "MissingPermission"}) private static AudioRecord createAudioRecord(int audioSource) { AudioRecord.Builder builder = new AudioRecord.Builder(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + if (Build.VERSION.SDK_INT >= AndroidVersions.API_31_ANDROID_12) { // On older APIs, Workarounds.fillAppInfo() must be called beforehand builder.setContext(FakeContext.get()); } @@ -117,7 +118,7 @@ public class AudioDirectCapture implements AudioCapture { @Override public void checkCompatibility() throws AudioCaptureException { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + if (Build.VERSION.SDK_INT < AndroidVersions.API_30_ANDROID_11) { Ln.w("Audio disabled: it is not supported before Android 11"); throw new AudioCaptureException(); } @@ -125,7 +126,7 @@ public class AudioDirectCapture implements AudioCapture { @Override public void start() throws AudioCaptureException { - if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { + if (Build.VERSION.SDK_INT == AndroidVersions.API_30_ANDROID_11) { startWorkaroundAndroid11(); try { tryStartRecording(5, 100); @@ -146,7 +147,7 @@ public class AudioDirectCapture implements AudioCapture { } @Override - @TargetApi(Build.VERSION_CODES.N) + @TargetApi(AndroidVersions.API_24_ANDROID_7_0) public int read(ByteBuffer outDirectBuffer, MediaCodec.BufferInfo outBufferInfo) { return reader.read(outDirectBuffer, outBufferInfo); } diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java index f462431a..fcc0c52f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java @@ -1,5 +1,6 @@ package com.genymobile.scrcpy.audio; +import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.AsyncProcessor; import com.genymobile.scrcpy.device.ConfigurationException; import com.genymobile.scrcpy.device.Streamer; @@ -93,7 +94,7 @@ public final class AudioEncoder implements AsyncProcessor { return format; } - @TargetApi(Build.VERSION_CODES.N) + @TargetApi(AndroidVersions.API_24_ANDROID_7_0) private void inputThread(MediaCodec mediaCodec, AudioCapture capture) throws IOException, InterruptedException { final MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); @@ -175,9 +176,9 @@ public final class AudioEncoder implements AsyncProcessor { } } - @TargetApi(Build.VERSION_CODES.M) + @TargetApi(AndroidVersions.API_23_ANDROID_6_0) private void encode() throws IOException, ConfigurationException, AudioCaptureException { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + if (Build.VERSION.SDK_INT < AndroidVersions.API_30_ANDROID_11) { Ln.w("Audio disabled: it is not supported before Android 11"); streamer.writeDisableStream(false); return; @@ -314,7 +315,7 @@ public final class AudioEncoder implements AsyncProcessor { } private final class EncoderCallback extends MediaCodec.Callback { - @TargetApi(Build.VERSION_CODES.N) + @TargetApi(AndroidVersions.API_24_ANDROID_7_0) @Override public void onInputBufferAvailable(MediaCodec codec, int index) { try { diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioPlaybackCapture.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioPlaybackCapture.java index e38493f2..009a239a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioPlaybackCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioPlaybackCapture.java @@ -1,5 +1,6 @@ package com.genymobile.scrcpy.audio; +import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.FakeContext; import com.genymobile.scrcpy.util.Ln; @@ -108,7 +109,7 @@ public final class AudioPlaybackCapture implements AudioCapture { @Override public void checkCompatibility() throws AudioCaptureException { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + if (Build.VERSION.SDK_INT < AndroidVersions.API_33_ANDROID_13) { Ln.w("Audio disabled: audio playback capture source not supported before Android 13"); throw new AudioCaptureException(); } @@ -130,7 +131,7 @@ public final class AudioPlaybackCapture implements AudioCapture { } @Override - @TargetApi(Build.VERSION_CODES.N) + @TargetApi(AndroidVersions.API_24_ANDROID_7_0) public int read(ByteBuffer outDirectBuffer, MediaCodec.BufferInfo outBufferInfo) { return reader.read(outDirectBuffer, outBufferInfo); } diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioRawRecorder.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioRawRecorder.java index 3924c205..9645bbbd 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioRawRecorder.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioRawRecorder.java @@ -1,5 +1,6 @@ package com.genymobile.scrcpy.audio; +import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.AsyncProcessor; import com.genymobile.scrcpy.device.Streamer; import com.genymobile.scrcpy.util.IO; @@ -24,7 +25,7 @@ public final class AudioRawRecorder implements AsyncProcessor { } private void record() throws IOException, AudioCaptureException { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + if (Build.VERSION.SDK_INT < AndroidVersions.API_30_ANDROID_11) { Ln.w("Audio disabled: it is not supported before Android 11"); streamer.writeDisableStream(false); return; diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioRecordReader.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioRecordReader.java index 80286831..32b42257 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioRecordReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioRecordReader.java @@ -1,12 +1,12 @@ package com.genymobile.scrcpy.audio; +import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.util.Ln; import android.annotation.TargetApi; import android.media.AudioRecord; import android.media.AudioTimestamp; import android.media.MediaCodec; -import android.os.Build; import java.nio.ByteBuffer; @@ -26,7 +26,7 @@ public class AudioRecordReader { this.recorder = recorder; } - @TargetApi(Build.VERSION_CODES.N) + @TargetApi(AndroidVersions.API_24_ANDROID_7_0) public int read(ByteBuffer outDirectBuffer, MediaCodec.BufferInfo outBufferInfo) { int r = recorder.read(outDirectBuffer, AudioConfig.MAX_READ_SIZE); if (r <= 0) { diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index b445427d..8fa27e81 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -1,5 +1,6 @@ package com.genymobile.scrcpy.control; +import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.AsyncProcessor; import com.genymobile.scrcpy.CleanUp; import com.genymobile.scrcpy.device.Device; @@ -318,7 +319,7 @@ public class Controller implements AsyncProcessor { * * Otherwise, Chrome does not work properly: */ - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && source == InputDevice.SOURCE_MOUSE) { + if (Build.VERSION.SDK_INT >= AndroidVersions.API_23_ANDROID_6_0 && source == InputDevice.SOURCE_MOUSE) { if (action == MotionEvent.ACTION_DOWN) { if (actionButton == buttons) { // First button pressed: ACTION_DOWN @@ -423,7 +424,7 @@ public class Controller implements AsyncProcessor { private void getClipboard(int copyKey) { // On Android >= 7, press the COPY or CUT key if requested - if (copyKey != ControlMessage.COPY_KEY_NONE && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && device.supportsInputEvents()) { + if (copyKey != ControlMessage.COPY_KEY_NONE && Build.VERSION.SDK_INT >= AndroidVersions.API_24_ANDROID_7_0 && device.supportsInputEvents()) { int key = copyKey == ControlMessage.COPY_KEY_COPY ? KeyEvent.KEYCODE_COPY : KeyEvent.KEYCODE_CUT; // Wait until the event is finished, to ensure that the clipboard text we read just after is the correct one device.pressReleaseKeycode(key, Device.INJECT_MODE_WAIT_FOR_FINISH); @@ -448,7 +449,7 @@ public class Controller implements AsyncProcessor { } // On Android >= 7, also press the PASTE key if requested - if (paste && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && device.supportsInputEvents()) { + if (paste && Build.VERSION.SDK_INT >= AndroidVersions.API_24_ANDROID_7_0 && device.supportsInputEvents()) { device.pressReleaseKeycode(KeyEvent.KEYCODE_PASTE, Device.INJECT_MODE_ASYNC); } diff --git a/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java b/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java index d8cfd81f..8121adfc 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java @@ -1,5 +1,6 @@ package com.genymobile.scrcpy.control; +import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.StringUtils; @@ -38,7 +39,7 @@ public final class UhidManager { public UhidManager(DeviceMessageSender sender) { this.sender = sender; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (Build.VERSION.SDK_INT >= AndroidVersions.API_23_ANDROID_6_0) { HandlerThread thread = new HandlerThread("UHidManager"); thread.start(); queue = thread.getLooper().getQueue(); @@ -71,7 +72,7 @@ public final class UhidManager { } private void registerUhidListener(int id, FileDescriptor fd) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (Build.VERSION.SDK_INT >= AndroidVersions.API_23_ANDROID_6_0) { queue.addOnFileDescriptorEventListener(fd, MessageQueue.OnFileDescriptorEventListener.EVENT_INPUT, (fd2, events) -> { try { buffer.clear(); @@ -97,7 +98,7 @@ public final class UhidManager { } private void unregisterUhidListener(FileDescriptor fd) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (Build.VERSION.SDK_INT >= AndroidVersions.API_23_ANDROID_6_0) { queue.removeOnFileDescriptorEventListener(fd); } } diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Device.java b/server/src/main/java/com/genymobile/scrcpy/device/Device.java index 5a1083fd..1f375942 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Device.java @@ -1,5 +1,6 @@ package com.genymobile.scrcpy.device; +import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.Options; import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.LogUtils; @@ -104,7 +105,7 @@ public final class Device { } }, displayId); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + if (Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10) { ServiceManager.getWindowManager().registerDisplayFoldListener(new IDisplayFoldListener.Stub() { @Override public void onDisplayFoldChanged(int displayId, boolean folded) { @@ -161,8 +162,8 @@ public final class Device { Ln.w("Display doesn't have FLAG_SUPPORTS_PROTECTED_BUFFERS flag, mirroring can be restricted"); } - // main display or any display on Android >= Q - supportsInputEvents = displayId == 0 || Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q; + // main display or any display on Android >= 10 + supportsInputEvents = displayId == 0 || Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10; if (!supportsInputEvents) { Ln.w("Input events are not supported for secondary displays before Android 10"); } @@ -215,7 +216,7 @@ public final class Device { } public static boolean supportsInputEvents(int displayId) { - return displayId == 0 || Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q; + return displayId == 0 || Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10; } public boolean supportsInputEvents() { @@ -323,10 +324,10 @@ public final class Device { * @param mode one of the {@code POWER_MODE_*} constants */ public static boolean setScreenPowerMode(int mode) { - boolean applyToMultiPhysicalDisplays = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q; + boolean applyToMultiPhysicalDisplays = Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10; if (applyToMultiPhysicalDisplays - && Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE + && Build.VERSION.SDK_INT >= AndroidVersions.API_34_ANDROID_14 && Build.BRAND.equalsIgnoreCase("honor") && SurfaceControl.hasGetBuildInDisplayMethod()) { // Workaround for Honor devices with Android 14: @@ -338,7 +339,7 @@ public final class Device { if (applyToMultiPhysicalDisplays) { // On Android 14, these internal methods have been moved to DisplayControl boolean useDisplayControl = - Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && !SurfaceControl.hasGetPhysicalDisplayIdsMethod(); + Build.VERSION.SDK_INT >= AndroidVersions.API_34_ANDROID_14 && !SurfaceControl.hasGetPhysicalDisplayIdsMethod(); // Change the power mode for all physical displays long[] physicalDisplayIds = useDisplayControl ? DisplayControl.getPhysicalDisplayIds() : SurfaceControl.getPhysicalDisplayIds(); diff --git a/server/src/main/java/com/genymobile/scrcpy/util/IO.java b/server/src/main/java/com/genymobile/scrcpy/util/IO.java index d9247a98..b953f290 100644 --- a/server/src/main/java/com/genymobile/scrcpy/util/IO.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/IO.java @@ -1,5 +1,6 @@ package com.genymobile.scrcpy.util; +import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.BuildConfig; import android.os.Build; @@ -31,7 +32,7 @@ public final class IO { } public static void writeFully(FileDescriptor fd, ByteBuffer from) throws IOException { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (Build.VERSION.SDK_INT >= AndroidVersions.API_23_ANDROID_6_0) { while (from.hasRemaining()) { write(fd, from); } diff --git a/server/src/main/java/com/genymobile/scrcpy/util/Settings.java b/server/src/main/java/com/genymobile/scrcpy/util/Settings.java index d9e82d62..e6465525 100644 --- a/server/src/main/java/com/genymobile/scrcpy/util/Settings.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/Settings.java @@ -1,5 +1,6 @@ package com.genymobile.scrcpy.util; +import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.wrappers.ContentProvider; import com.genymobile.scrcpy.wrappers.ServiceManager; @@ -34,7 +35,7 @@ public final class Settings { } public static String getValue(String table, String key) throws SettingsException { - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { + if (Build.VERSION.SDK_INT <= AndroidVersions.API_30_ANDROID_11) { // on Android >= 12, it always fails: try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) { return provider.getValue(table, key); @@ -47,7 +48,7 @@ public final class Settings { } public static void putValue(String table, String key, String value) throws SettingsException { - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { + if (Build.VERSION.SDK_INT <= AndroidVersions.API_30_ANDROID_11) { // on Android >= 12, it always fails: try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) { provider.putValue(table, key, value); @@ -60,7 +61,7 @@ public final class Settings { } public static String getAndPutValue(String table, String key, String value) throws SettingsException { - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { + if (Build.VERSION.SDK_INT <= AndroidVersions.API_30_ANDROID_11) { // on Android >= 12, it always fails: try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) { String oldValue = provider.getValue(table, key); diff --git a/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java index 3b8fc59b..a5fa4b06 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java @@ -1,5 +1,6 @@ package com.genymobile.scrcpy.video; +import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.util.HandlerExecutor; import com.genymobile.scrcpy.util.Ln; @@ -20,7 +21,6 @@ import android.hardware.camera2.params.OutputConfiguration; import android.hardware.camera2.params.SessionConfiguration; import android.hardware.camera2.params.StreamConfigurationMap; import android.media.MediaCodec; -import android.os.Build; import android.os.Handler; import android.os.HandlerThread; import android.util.Range; @@ -118,7 +118,7 @@ public class CameraCapture extends SurfaceCapture { return null; } - @TargetApi(Build.VERSION_CODES.N) + @TargetApi(AndroidVersions.API_24_ANDROID_7_0) private static Size selectSize(String cameraId, Size explicitSize, int maxSize, CameraAspectRatio aspectRatio, boolean highSpeed) throws CameraAccessException { if (explicitSize != null) { @@ -242,7 +242,7 @@ public class CameraCapture extends SurfaceCapture { } @SuppressLint("MissingPermission") - @TargetApi(Build.VERSION_CODES.S) + @TargetApi(AndroidVersions.API_31_ANDROID_12) private CameraDevice openCamera(String id) throws CameraAccessException, InterruptedException { CompletableFuture future = new CompletableFuture<>(); ServiceManager.getCameraManager().openCamera(id, new CameraDevice.StateCallback() { @@ -289,7 +289,7 @@ public class CameraCapture extends SurfaceCapture { } } - @TargetApi(Build.VERSION_CODES.S) + @TargetApi(AndroidVersions.API_31_ANDROID_12) private CameraCaptureSession createCaptureSession(CameraDevice camera, Surface surface) throws CameraAccessException, InterruptedException { CompletableFuture future = new CompletableFuture<>(); OutputConfiguration outputConfig = new OutputConfiguration(surface); @@ -328,7 +328,7 @@ public class CameraCapture extends SurfaceCapture { return requestBuilder.build(); } - @TargetApi(Build.VERSION_CODES.S) + @TargetApi(AndroidVersions.API_31_ANDROID_12) private void setRepeatingRequest(CameraCaptureSession session, CaptureRequest request) throws CameraAccessException, InterruptedException { CameraCaptureSession.CaptureCallback callback = new CameraCaptureSession.CaptureCallback() { @Override diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java index 62afb263..e6357410 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java @@ -1,5 +1,6 @@ package com.genymobile.scrcpy.video; +import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.device.Device; import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.util.Ln; @@ -103,8 +104,8 @@ public class ScreenCapture extends SurfaceCapture implements Device.RotationList private static IBinder createDisplay() throws Exception { // Since Android 12 (preview), secure displays could not be created with shell permissions anymore. // On Android 12 preview, SDK_INT is still R (not S), but CODENAME is "S". - boolean secure = Build.VERSION.SDK_INT < Build.VERSION_CODES.R || (Build.VERSION.SDK_INT == Build.VERSION_CODES.R && !"S".equals( - Build.VERSION.CODENAME)); + boolean secure = Build.VERSION.SDK_INT < AndroidVersions.API_30_ANDROID_11 || (Build.VERSION.SDK_INT == AndroidVersions.API_30_ANDROID_11 + && !"S".equals(Build.VERSION.CODENAME)); return SurfaceControl.createDisplay("scrcpy", secure); } diff --git a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java index 41c38642..5a9417da 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java @@ -1,5 +1,6 @@ package com.genymobile.scrcpy.video; +import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.AsyncProcessor; import com.genymobile.scrcpy.device.ConfigurationException; import com.genymobile.scrcpy.device.Size; @@ -238,7 +239,7 @@ public class SurfaceEncoder implements AsyncProcessor { // must be present to configure the encoder, but does not impact the actual frame rate, which is variable format.setInteger(MediaFormat.KEY_FRAME_RATE, 60); format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + if (Build.VERSION.SDK_INT >= AndroidVersions.API_24_ANDROID_7_0) { format.setInteger(MediaFormat.KEY_COLOR_RANGE, MediaFormat.COLOR_RANGE_LIMITED); } format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, DEFAULT_I_FRAME_INTERVAL); diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java index bb1ca0d4..c907e12f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java @@ -1,5 +1,6 @@ package com.genymobile.scrcpy.wrappers; +import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.FakeContext; import com.genymobile.scrcpy.util.Ln; @@ -7,7 +8,6 @@ import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.content.Intent; import android.os.Binder; -import android.os.Build; import android.os.Bundle; import android.os.IBinder; import android.os.IInterface; @@ -63,7 +63,7 @@ public final class ActivityManager { return removeContentProviderExternalMethod; } - @TargetApi(Build.VERSION_CODES.Q) + @TargetApi(AndroidVersions.API_29_ANDROID_10) private ContentProvider getContentProviderExternal(String name, IBinder token) { try { Method method = getGetContentProviderExternalMethod(); 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 c5f007fe..791df0f8 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java @@ -1,5 +1,6 @@ package com.genymobile.scrcpy.wrappers; +import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.FakeContext; import com.genymobile.scrcpy.util.Ln; @@ -36,7 +37,7 @@ public final class ClipboardManager { private Method getGetPrimaryClipMethod() throws NoSuchMethodException { if (getPrimaryClipMethod == null) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) { getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class); return getPrimaryClipMethod; } @@ -99,7 +100,7 @@ public final class ClipboardManager { private Method getSetPrimaryClipMethod() throws NoSuchMethodException { if (setPrimaryClipMethod == null) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) { setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class); return setPrimaryClipMethod; } @@ -137,7 +138,7 @@ public final class ClipboardManager { } private static ClipData getPrimaryClip(Method method, int methodVersion, IInterface manager) throws ReflectiveOperationException { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) { return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME); } @@ -161,7 +162,7 @@ public final class ClipboardManager { } private static void setPrimaryClip(Method method, int methodVersion, IInterface manager, ClipData clipData) throws ReflectiveOperationException { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) { method.invoke(manager, clipData, FakeContext.PACKAGE_NAME); return; } @@ -210,7 +211,7 @@ public final class ClipboardManager { private static void addPrimaryClipChangedListener(Method method, int methodVersion, IInterface manager, IOnPrimaryClipChangedListener listener) throws ReflectiveOperationException { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) { method.invoke(manager, listener, FakeContext.PACKAGE_NAME); return; } @@ -230,7 +231,7 @@ public final class ClipboardManager { private Method getAddPrimaryClipChangedListener() throws NoSuchMethodException { if (addPrimaryClipChangedListener == null) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) { addPrimaryClipChangedListener = manager.getClass() .getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class); } else { diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java index 7e92ac50..f625b398 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java @@ -1,5 +1,6 @@ package com.genymobile.scrcpy.wrappers; +import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.FakeContext; import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.SettingsException; @@ -51,7 +52,7 @@ public final class ContentProvider implements Closeable { @SuppressLint("PrivateApi") private Method getCallMethod() throws NoSuchMethodException { if (callMethod == null) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + if (Build.VERSION.SDK_INT >= AndroidVersions.API_31_ANDROID_12) { callMethod = provider.getClass().getMethod("call", AttributionSource.class, String.class, String.class, String.class, Bundle.class); callMethodVersion = 0; } else { @@ -79,7 +80,7 @@ public final class ContentProvider implements Closeable { Method method = getCallMethod(); Object[] args; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && callMethodVersion == 0) { + if (Build.VERSION.SDK_INT >= AndroidVersions.API_31_ANDROID_12 && callMethodVersion == 0) { args = new Object[]{FakeContext.get().getAttributionSource(), "settings", callMethod, arg, extras}; } else { switch (callMethodVersion) { diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayControl.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayControl.java index cc9d5526..a57f7948 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayControl.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayControl.java @@ -1,16 +1,16 @@ package com.genymobile.scrcpy.wrappers; +import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.util.Ln; import android.annotation.SuppressLint; import android.annotation.TargetApi; -import android.os.Build; import android.os.IBinder; import java.lang.reflect.Method; @SuppressLint({"PrivateApi", "SoonBlockedPrivateApi", "BlockedPrivateApi"}) -@TargetApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) +@TargetApi(AndroidVersions.API_34_ANDROID_14) public final class DisplayControl { private static final Class CLASS; diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java index 0a56f347..615ceb42 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java @@ -1,5 +1,6 @@ package com.genymobile.scrcpy.wrappers; +import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.util.Ln; import android.annotation.SuppressLint; @@ -24,7 +25,7 @@ public final class PowerManager { private Method getIsScreenOnMethod() throws NoSuchMethodException { if (isScreenOnMethod == null) { @SuppressLint("ObsoleteSdkInt") // we may lower minSdkVersion in the future - String methodName = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH ? "isInteractive" : "isScreenOn"; + String methodName = Build.VERSION.SDK_INT >= AndroidVersions.API_20_ANDROID_4_4 ? "isInteractive" : "isScreenOn"; isScreenOnMethod = manager.getClass().getMethod(methodName); } return isScreenOnMethod; 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 038e7ca0..3bae4a37 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java @@ -1,5 +1,6 @@ package com.genymobile.scrcpy.wrappers; +import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.util.Ln; import android.annotation.SuppressLint; @@ -83,9 +84,9 @@ public final class SurfaceControl { private static Method getGetBuiltInDisplayMethod() throws NoSuchMethodException { if (getBuiltInDisplayMethod == null) { - // the method signature has changed in Android Q + // the method signature has changed in Android 10 // - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) { getBuiltInDisplayMethod = CLASS.getMethod("getBuiltInDisplay", int.class); } else { getBuiltInDisplayMethod = CLASS.getMethod("getInternalDisplayToken"); @@ -106,7 +107,7 @@ public final class SurfaceControl { public static IBinder getBuiltInDisplay() { try { Method method = getGetBuiltInDisplayMethod(); - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) { // call getBuiltInDisplay(0) return (IBinder) method.invoke(null, 0); } From a46150f753c47d0fb180040448629d229ae74581 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 20 Oct 2024 15:06:54 +0200 Subject: [PATCH 1994/2244] Upgrade Android SDK to 35 Also adapt the call to build-tools/35.0.0/aidl, which now requires an import path (-I. for the current directory). Otherwise, it fails with: ERROR: android/view/IRotationWatcher.aidl:23.1-10: directory ./ is not found in any of the import paths: - Also upgrade AGP (8.7.1) and Gradle (8.9), required for SDK 35. --- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 4 +++- server/build.gradle | 4 ++-- server/build_without_gradle.sh | 10 +++++----- .../java/com/genymobile/scrcpy/AndroidVersions.java | 1 + 5 files changed, 12 insertions(+), 9 deletions(-) diff --git a/build.gradle b/build.gradle index f81f7d27..81c91d37 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:8.3.0' + classpath 'com.android.tools.build:gradle:8.7.1' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e411586a..b34b7096 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip +# https://gradle.org/release-checksums/ +distributionSha256Sum=d725d707bfabd4dfdc958c624003b3c80accc03f7037b5122c4b1d0ef15cecab zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/server/build.gradle b/server/build.gradle index 655298a9..2781a2db 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -2,11 +2,11 @@ apply plugin: 'com.android.application' android { namespace 'com.genymobile.scrcpy' - compileSdk 34 + compileSdk 35 defaultConfig { applicationId "com.genymobile.scrcpy" minSdkVersion 21 - targetSdkVersion 34 + targetSdkVersion 35 versionCode 20700 versionName "2.7" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index ab6c821d..14534700 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -14,8 +14,8 @@ set -e SCRCPY_DEBUG=false SCRCPY_VERSION_NAME=2.7 -PLATFORM=${ANDROID_PLATFORM:-34} -BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-34.0.0} +PLATFORM=${ANDROID_PLATFORM:-35} +BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-35.0.0} BUILD_TOOLS_DIR="$ANDROID_HOME/build-tools/$BUILD_TOOLS" BUILD_DIR="$(realpath ${BUILD_DIR:-build_manual})" @@ -45,10 +45,10 @@ EOF echo "Generating java from aidl..." cd "$SERVER_DIR/src/main/aidl" -"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" android/view/IRotationWatcher.aidl -"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" \ +"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" -I. android/view/IRotationWatcher.aidl +"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" -I. \ android/content/IOnPrimaryClipChangedListener.aidl -"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" android/view/IDisplayFoldListener.aidl +"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" -I. android/view/IDisplayFoldListener.aidl SRC=( \ com/genymobile/scrcpy/*.java \ diff --git a/server/src/main/java/com/genymobile/scrcpy/AndroidVersions.java b/server/src/main/java/com/genymobile/scrcpy/AndroidVersions.java index 8acad7ee..98fa6dc3 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AndroidVersions.java +++ b/server/src/main/java/com/genymobile/scrcpy/AndroidVersions.java @@ -28,5 +28,6 @@ public final class AndroidVersions { public static final int API_32_ANDROID_12L = Build.VERSION_CODES.S_V2; public static final int API_33_ANDROID_13 = Build.VERSION_CODES.TIRAMISU; public static final int API_34_ANDROID_14 = Build.VERSION_CODES.UPSIDE_DOWN_CAKE; + public static final int API_35_ANDROID_15 = Build.VERSION_CODES.VANILLA_ICE_CREAM; } From 7b3dd595b493449b4e93281205c375b1472d3b87 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 20 Oct 2024 22:53:32 +0200 Subject: [PATCH 1995/2244] Remove useless version check Scrcpy requires Android 5.0+, so there is no point testing for older versions. Btw, there were two mistakes: - the constant name in AndroidVersions should have been API_20_ANDROID_4_4W (Android 4.4 without 'W' is API 19) - the method isInteractive() was introduced in Android 5.0, not 4.4W: --- .../main/java/com/genymobile/scrcpy/AndroidVersions.java | 1 - .../java/com/genymobile/scrcpy/wrappers/PowerManager.java | 7 +------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/AndroidVersions.java b/server/src/main/java/com/genymobile/scrcpy/AndroidVersions.java index 98fa6dc3..5303924a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AndroidVersions.java +++ b/server/src/main/java/com/genymobile/scrcpy/AndroidVersions.java @@ -13,7 +13,6 @@ public final class AndroidVersions { // not instantiable } - public static final int API_20_ANDROID_4_4 = Build.VERSION_CODES.KITKAT_WATCH; public static final int API_21_ANDROID_5_0 = Build.VERSION_CODES.LOLLIPOP; public static final int API_22_ANDROID_5_1 = Build.VERSION_CODES.LOLLIPOP_MR1; public static final int API_23_ANDROID_6_0 = Build.VERSION_CODES.M; diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java index 615ceb42..f62e5b8e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java @@ -1,10 +1,7 @@ package com.genymobile.scrcpy.wrappers; -import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.util.Ln; -import android.annotation.SuppressLint; -import android.os.Build; import android.os.IInterface; import java.lang.reflect.Method; @@ -24,9 +21,7 @@ public final class PowerManager { private Method getIsScreenOnMethod() throws NoSuchMethodException { if (isScreenOnMethod == null) { - @SuppressLint("ObsoleteSdkInt") // we may lower minSdkVersion in the future - String methodName = Build.VERSION.SDK_INT >= AndroidVersions.API_20_ANDROID_4_4 ? "isInteractive" : "isScreenOn"; - isScreenOnMethod = manager.getClass().getMethod(methodName); + isScreenOnMethod = manager.getClass().getMethod("isInteractive"); } return isScreenOnMethod; } From 9578aae34eca49f448d0312838c2b4011e977810 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 22 Oct 2024 19:47:56 +0200 Subject: [PATCH 1996/2244] Use explicit constant for @TargetApi --- .../java/com/genymobile/scrcpy/wrappers/WindowManager.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 4c769e85..5894b836 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java @@ -1,5 +1,6 @@ package com.genymobile.scrcpy.wrappers; +import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.util.Ln; import android.annotation.TargetApi; @@ -200,7 +201,7 @@ public final class WindowManager { } } - @TargetApi(29) + @TargetApi(AndroidVersions.API_29_ANDROID_10) public void registerDisplayFoldListener(IDisplayFoldListener foldListener) { try { Class cls = manager.getClass(); From 67d4dfb5ffcfeaddf3363e73fdf3b2c500fd5842 Mon Sep 17 00:00:00 2001 From: Anwar Fuadi Date: Sun, 28 Jul 2024 14:24:51 +0700 Subject: [PATCH 1997/2244] Add missing client build dependency in Fedora PR #5147 Signed-off-by: Romain Vimont --- doc/build.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/build.md b/doc/build.md index 63bd7ca7..0c70f598 100644 --- a/doc/build.md +++ b/doc/build.md @@ -77,7 +77,7 @@ pip3 install meson sudo dnf install https://download1.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm # client build dependencies -sudo dnf install SDL2-devel ffms2-devel libusb1-devel meson gcc make +sudo dnf install SDL2-devel ffms2-devel libusb1-devel libavdevice-free-devel meson gcc make # server build dependencies sudo dnf install java-devel From 538a32a53973d5ab1e393a36286de094a97aaf9a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 18 Sep 2024 22:32:28 +0200 Subject: [PATCH 1998/2244] Fix .PHONY in release.mk The prepare-deps recipe does not exist anymore. It has been split into prepare-deps-win32 and prepare-deps-win64. PR #5306 --- release.mk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release.mk b/release.mk index 7f082144..099db85e 100644 --- a/release.mk +++ b/release.mk @@ -11,7 +11,7 @@ .PHONY: default clean \ test \ build-server \ - prepare-deps \ + prepare-deps-win32 prepare-deps-win64 \ build-win32 build-win64 \ dist-win32 dist-win64 \ zip-win32 zip-win64 \ From 02ef3d57ce9542493ea7efb4ace66c4300d472fb Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 18 Sep 2024 23:39:45 +0200 Subject: [PATCH 1999/2244] Split client and server tests in release.mk This will allow to run server tests separately on the CI. PR #5306 --- release.mk | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/release.mk b/release.mk index 099db85e..91a8f41d 100644 --- a/release.mk +++ b/release.mk @@ -9,7 +9,7 @@ # the server to the device. .PHONY: default clean \ - test \ + test test-client test-server \ build-server \ prepare-deps-win32 prepare-deps-win64 \ build-win32 build-win64 \ @@ -51,12 +51,16 @@ clean: rm -rf "$(DIST)" "$(TEST_BUILD_DIR)" "$(SERVER_BUILD_DIR)" \ "$(WIN32_BUILD_DIR)" "$(WIN64_BUILD_DIR)" -test: +test-client: [ -d "$(TEST_BUILD_DIR)" ] || ( mkdir "$(TEST_BUILD_DIR)" && \ meson setup "$(TEST_BUILD_DIR)" -Db_sanitize=address ) ninja -C "$(TEST_BUILD_DIR)" + +test-server: $(GRADLE) -p server check +test: test-client test-server + build-server: [ -d "$(SERVER_BUILD_DIR)" ] || ( mkdir "$(SERVER_BUILD_DIR)" && \ meson setup "$(SERVER_BUILD_DIR)" --buildtype release -Dcompile_app=false ) From 9c0a32849897d98e17bf0f8d7603454e558746cd Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 18 Sep 2024 23:40:27 +0200 Subject: [PATCH 2000/2244] Build server without meson in release.mk This avoids to install meson/ninja to build scrcpy-server on the CI. PR #5306 --- release.mk | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/release.mk b/release.mk index 91a8f41d..f99eaf4f 100644 --- a/release.mk +++ b/release.mk @@ -62,9 +62,10 @@ test-server: test: test-client test-server build-server: - [ -d "$(SERVER_BUILD_DIR)" ] || ( mkdir "$(SERVER_BUILD_DIR)" && \ - meson setup "$(SERVER_BUILD_DIR)" --buildtype release -Dcompile_app=false ) - ninja -C "$(SERVER_BUILD_DIR)" + $(GRADLE) -p server assembleRelease + mkdir -p "$(SERVER_BUILD_DIR)/server" + cp server/build/outputs/apk/release/server-release-unsigned.apk \ + "$(SERVER_BUILD_DIR)/server/scrcpy-server" prepare-deps-win32: @app/deps/adb.sh win32 From 2687d202809dfaafe8f40f613aec131ad9501433 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 18 Sep 2024 23:51:52 +0200 Subject: [PATCH 2001/2244] Rework release.mk for CI Make it possible to build scrcpy-server and Windows binaries in parallel from different GitHub Actions workflows, and to package everything as a final step. PR #5306 --- release.mk | 87 +++++++++++++++++++++++++++++------------------------- 1 file changed, 46 insertions(+), 41 deletions(-) diff --git a/release.mk b/release.mk index f99eaf4f..61145002 100644 --- a/release.mk +++ b/release.mk @@ -13,9 +13,8 @@ build-server \ prepare-deps-win32 prepare-deps-win64 \ build-win32 build-win64 \ - dist-win32 dist-win64 \ zip-win32 zip-win64 \ - release + package release GRADLE ?= ./gradlew @@ -26,7 +25,7 @@ WIN64_BUILD_DIR := build-win64 VERSION ?= $(shell git describe --tags --exclude='*install-release' --always) -DIST := dist +ZIP := zip WIN32_TARGET_DIR := scrcpy-win32-$(VERSION) WIN64_TARGET_DIR := scrcpy-win64-$(VERSION) WIN32_TARGET := $(WIN32_TARGET_DIR).zip @@ -34,21 +33,11 @@ WIN64_TARGET := $(WIN64_TARGET_DIR).zip RELEASE_DIR := release-$(VERSION) -release: clean test build-server zip-win32 zip-win64 - mkdir -p "$(RELEASE_DIR)" - cp "$(SERVER_BUILD_DIR)/server/scrcpy-server" \ - "$(RELEASE_DIR)/scrcpy-server-$(VERSION)" - cp "$(DIST)/$(WIN32_TARGET)" "$(RELEASE_DIR)" - cp "$(DIST)/$(WIN64_TARGET)" "$(RELEASE_DIR)" - cd "$(RELEASE_DIR)" && \ - sha256sum "scrcpy-server-$(VERSION)" \ - "scrcpy-win32-$(VERSION).zip" \ - "scrcpy-win64-$(VERSION).zip" > SHA256SUMS.txt - @echo "Release generated in $(RELEASE_DIR)/" +release: clean test build-server build-win32 build-win64 package clean: $(GRADLE) clean - rm -rf "$(DIST)" "$(TEST_BUILD_DIR)" "$(SERVER_BUILD_DIR)" \ + rm -rf "$(ZIP)" "$(TEST_BUILD_DIR)" "$(SERVER_BUILD_DIR)" \ "$(WIN32_BUILD_DIR)" "$(WIN64_BUILD_DIR)" test-client: @@ -91,6 +80,15 @@ build-win32: prepare-deps-win32 -Dcompile_server=false \ -Dportable=true ninja -C "$(WIN32_BUILD_DIR)" + # Group intermediate outputs into a 'dist' directory + mkdir -p "$(WIN32_BUILD_DIR)/dist" + cp "$(WIN32_BUILD_DIR)"/app/scrcpy.exe "$(WIN32_BUILD_DIR)/dist/" + cp app/data/scrcpy-console.bat "$(WIN32_BUILD_DIR)/dist/" + cp app/data/scrcpy-noconsole.vbs "$(WIN32_BUILD_DIR)/dist/" + cp app/data/icon.png "$(WIN32_BUILD_DIR)/dist/" + cp app/data/open_a_terminal_here.bat "$(WIN32_BUILD_DIR)/dist/" + cp app/deps/work/install/win32/bin/*.dll "$(WIN32_BUILD_DIR)/dist/" + cp app/deps/work/install/win32/bin/adb.exe "$(WIN32_BUILD_DIR)/dist/" build-win64: prepare-deps-win64 rm -rf "$(WIN64_BUILD_DIR)" @@ -104,33 +102,40 @@ build-win64: prepare-deps-win64 -Dcompile_server=false \ -Dportable=true ninja -C "$(WIN64_BUILD_DIR)" + # Group intermediate outputs into a 'dist' directory + mkdir -p "$(WIN64_BUILD_DIR)/dist" + cp "$(WIN64_BUILD_DIR)"/app/scrcpy.exe "$(WIN64_BUILD_DIR)/dist/" + cp app/data/scrcpy-console.bat "$(WIN64_BUILD_DIR)/dist/" + cp app/data/scrcpy-noconsole.vbs "$(WIN64_BUILD_DIR)/dist/" + cp app/data/icon.png "$(WIN64_BUILD_DIR)/dist/" + cp app/data/open_a_terminal_here.bat "$(WIN64_BUILD_DIR)/dist/" + cp app/deps/work/install/win64/bin/*.dll "$(WIN64_BUILD_DIR)/dist/" + cp app/deps/work/install/win64/bin/adb.exe "$(WIN64_BUILD_DIR)/dist/" -dist-win32: build-server build-win32 - mkdir -p "$(DIST)/$(WIN32_TARGET_DIR)" - cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(DIST)/$(WIN32_TARGET_DIR)/" - cp "$(WIN32_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/data/scrcpy-console.bat "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/data/icon.png "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/deps/work/install/win32/bin/*.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/deps/work/install/win32/bin/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/" - -dist-win64: build-server build-win64 - mkdir -p "$(DIST)/$(WIN64_TARGET_DIR)" - cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(DIST)/$(WIN64_TARGET_DIR)/" - cp "$(WIN64_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/data/scrcpy-console.bat "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/data/icon.png "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/deps/work/install/win64/bin/*.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/deps/work/install/win64/bin/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/" - -zip-win32: dist-win32 - cd "$(DIST)"; \ +zip-win32: + mkdir -p "$(ZIP)/$(WIN32_TARGET_DIR)" + cp -r "$(WIN32_BUILD_DIR)/dist/." "$(ZIP)/$(WIN32_TARGET_DIR)/" + cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(ZIP)/$(WIN32_TARGET_DIR)/" + cd "$(ZIP)"; \ zip -r "$(WIN32_TARGET)" "$(WIN32_TARGET_DIR)" + rm -rf "$(ZIP)/$(WIN32_TARGET_DIR)" -zip-win64: dist-win64 - cd "$(DIST)"; \ +zip-win64: + mkdir -p "$(ZIP)/$(WIN64_TARGET_DIR)" + cp -r "$(WIN64_BUILD_DIR)/dist/." "$(ZIP)/$(WIN64_TARGET_DIR)/" + cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(ZIP)/$(WIN64_TARGET_DIR)/" + cd "$(ZIP)"; \ zip -r "$(WIN64_TARGET)" "$(WIN64_TARGET_DIR)" + rm -rf "$(ZIP)/$(WIN64_TARGET_DIR)" + +package: zip-win32 zip-win64 + mkdir -p "$(RELEASE_DIR)" + cp "$(SERVER_BUILD_DIR)/server/scrcpy-server" \ + "$(RELEASE_DIR)/scrcpy-server-$(VERSION)" + cp "$(ZIP)/$(WIN32_TARGET)" "$(RELEASE_DIR)" + cp "$(ZIP)/$(WIN64_TARGET)" "$(RELEASE_DIR)" + cd "$(RELEASE_DIR)" && \ + sha256sum "scrcpy-server-$(VERSION)" \ + "scrcpy-win32-$(VERSION).zip" \ + "scrcpy-win64-$(VERSION).zip" > SHA256SUMS.txt + @echo "Release generated in $(RELEASE_DIR)/" From a5844e198e9bc53653c8658d53b7b9df4e122cc0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 18 Sep 2024 23:58:42 +0200 Subject: [PATCH 2002/2244] Add GitHub Actions release workflow Fixes #4490 PR #5306 --- .github/workflows/release.yml | 147 ++++++++++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..e67c1c21 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,147 @@ +name: Build + +on: + workflow_dispatch: + inputs: + name: + description: 'Version name (default is ref name)' + +jobs: + build-scrcpy-server: + runs-on: ubuntu-latest + env: + GRADLE: gradle # use native gradle instead of ./gradlew in release.mk + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup JDK + uses: actions/setup-java@v4 + with: + distribution: 'zulu' + java-version: '17' + + - name: Test scrcpy-server + run: make -f release.mk test-server + + - name: Build scrcpy-server + run: make -f release.mk build-server + + - name: Upload scrcpy-server artifact + uses: actions/upload-artifact@v4 + with: + name: scrcpy-server + path: build-server/server/scrcpy-server + + test-client: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt update + sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \ + libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \ + libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev + + - name: Build + run: | + meson setup d -Db_sanitize=address,undefined + + - name: Test + run: | + meson test -Cd + + build-win32: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt update + sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \ + libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \ + libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \ + mingw-w64 mingw-w64-tools libz-mingw-w64-dev + + - name: Workaround for old meson version run by Github Actions + run: sed -i 's/^pkg-config/pkgconfig/' cross_win32.txt + + - name: Build scrcpy win32 + run: make -f release.mk build-win32 + + - name: Upload build-win32 artifact + uses: actions/upload-artifact@v4 + with: + name: build-win32-intermediate + path: build-win32/dist/ + + build-win64: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt update + sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \ + libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \ + libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \ + mingw-w64 mingw-w64-tools libz-mingw-w64-dev + + - name: Workaround for old meson version run by Github Actions + run: sed -i 's/^pkg-config/pkgconfig/' cross_win64.txt + + - name: Build scrcpy win64 + run: make -f release.mk build-win64 + + - name: Upload build-win64 artifact + uses: actions/upload-artifact@v4 + with: + name: build-win64-intermediate + path: build-win64/dist/ + + package: + needs: + - build-scrcpy-server + - build-win32 + - build-win64 + runs-on: ubuntu-latest + env: + # $VERSION is used by release.mk + VERSION: ${{ github.event.inputs.name || github.ref_name }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Download scrcpy-server + uses: actions/download-artifact@v4 + with: + name: scrcpy-server + path: build-server/server/ + + - name: Download build-win32 + uses: actions/download-artifact@v4 + with: + name: build-win32-intermediate + path: build-win32/dist/ + + - name: Download build-win64 + uses: actions/download-artifact@v4 + with: + name: build-win64-intermediate + path: build-win64/dist/ + + - name: Package + run: make -f release.mk package + + - name: Upload release artifact + uses: actions/upload-artifact@v4 + with: + name: scrcpy-release-${{ env.VERSION }} + path: release-${{ env.VERSION }} From 14e5439dee5486f870bda95add4102eaba39971c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 28 Oct 2024 14:52:05 +0100 Subject: [PATCH 2003/2244] Update mouse documentation about capture key The mouse capture keys are not hardcoded anymore, they use the configured shortcut modifiers. Refs ff9fb5994dbe555be8835a5f8d06b03e9f3b1b27 --- doc/mouse.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/mouse.md b/doc/mouse.md index ae7c6834..3607a92c 100644 --- a/doc/mouse.md +++ b/doc/mouse.md @@ -34,9 +34,9 @@ Two modes allow to simulate a physical HID mouse on the device. In these modes, the computer mouse is "captured": the mouse pointer disappears from the computer and appears on the Android device instead. -Special capture keys, either Alt or Super, toggle -(disable or enable) the mouse capture. Use one of them to give the control of -the mouse back to the computer. +The [shortcut mod](shortcuts.md) (either Alt or Super by +default) toggle (disable or enable) the mouse capture. Use one of them to give +the control of the mouse back to the computer. ### UHID From 874eaec487369f7fcaa9ed8c5f85569659565d4f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 12 Oct 2024 09:23:31 +0200 Subject: [PATCH 2004/2244] Move screen-related features out of Device.java Move the code related to screen size and rotation/fold to ScreenCapture. For now, keep the ScreenInfo instance in the Device class to communicate with the Controller, but it will be removed by further commits. PR #5370 --- .../java/com/genymobile/scrcpy/Server.java | 3 +- .../com/genymobile/scrcpy/device/Device.java | 127 ++---------------- .../scrcpy/video/ScreenCapture.java | 123 +++++++++++++---- .../scrcpy/video/SurfaceCapture.java | 3 +- .../scrcpy/wrappers/WindowManager.java | 20 ++- 5 files changed, 135 insertions(+), 141 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 555cf97a..9802e0f5 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -190,7 +190,8 @@ public final class Server { options.getSendFrameMeta()); SurfaceCapture surfaceCapture; if (options.getVideoSource() == VideoSource.DISPLAY) { - surfaceCapture = new ScreenCapture(device); + surfaceCapture = new ScreenCapture(device, options.getDisplayId(), options.getMaxSize(), options.getCrop(), + options.getLockVideoOrientation()); } else { surfaceCapture = new CameraCapture(options.getCameraId(), options.getCameraFacing(), options.getCameraSize(), options.getMaxSize(), options.getCameraAspectRatio(), options.getCameraFps(), options.getCameraHighSpeed()); diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Device.java b/server/src/main/java/com/genymobile/scrcpy/device/Device.java index 1f375942..63e33988 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Device.java @@ -3,7 +3,6 @@ package com.genymobile.scrcpy.device; import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.Options; import com.genymobile.scrcpy.util.Ln; -import com.genymobile.scrcpy.util.LogUtils; import com.genymobile.scrcpy.video.ScreenInfo; import com.genymobile.scrcpy.wrappers.ClipboardManager; import com.genymobile.scrcpy.wrappers.DisplayControl; @@ -17,14 +16,13 @@ import android.graphics.Rect; import android.os.Build; import android.os.IBinder; import android.os.SystemClock; -import android.view.IDisplayFoldListener; -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; +import java.util.concurrent.atomic.AtomicReference; public final class Device { @@ -38,26 +36,10 @@ public final class Device { public static final int LOCK_VIDEO_ORIENTATION_UNLOCKED = -1; public static final int LOCK_VIDEO_ORIENTATION_INITIAL = -2; - public interface RotationListener { - void onRotationChanged(int rotation); - } - - public interface FoldListener { - void onFoldChanged(int displayId, boolean folded); - } - public interface ClipboardListener { void onClipboardTextChanged(String text); } - private final Rect crop; - private int maxSize; - private final int lockVideoOrientation; - - private Size deviceSize; - private ScreenInfo screenInfo; - private RotationListener rotationListener; - private FoldListener foldListener; private ClipboardListener clipboardListener; private final AtomicBoolean isSettingClipboard = new AtomicBoolean(); @@ -66,71 +48,12 @@ public final class Device { */ private final int displayId; - /** - * The surface flinger layer stack associated with this logical display - */ - private final int layerStack; - private final boolean supportsInputEvents; - public Device(Options options) throws ConfigurationException { + private final AtomicReference screenInfo = new AtomicReference<>(); // set by the ScreenCapture instance + + public Device(Options options) { displayId = options.getDisplayId(); - DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(displayId); - if (displayInfo == null) { - Ln.e("Display " + displayId + " not found\n" + LogUtils.buildDisplayListMessage()); - throw new ConfigurationException("Unknown display id: " + displayId); - } - - int displayInfoFlags = displayInfo.getFlags(); - - deviceSize = displayInfo.getSize(); - crop = options.getCrop(); - maxSize = options.getMaxSize(); - lockVideoOrientation = options.getLockVideoOrientation(); - - screenInfo = ScreenInfo.computeScreenInfo(displayInfo.getRotation(), deviceSize, crop, maxSize, lockVideoOrientation); - layerStack = displayInfo.getLayerStack(); - - ServiceManager.getWindowManager().registerRotationWatcher(new IRotationWatcher.Stub() { - @Override - public void onRotationChanged(int rotation) { - synchronized (Device.this) { - screenInfo = screenInfo.withDeviceRotation(rotation); - - // notify - if (rotationListener != null) { - rotationListener.onRotationChanged(rotation); - } - } - } - }, displayId); - - if (Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10) { - ServiceManager.getWindowManager().registerDisplayFoldListener(new IDisplayFoldListener.Stub() { - @Override - public void onDisplayFoldChanged(int displayId, boolean folded) { - if (Device.this.displayId != displayId) { - // Ignore events related to other display ids - return; - } - - synchronized (Device.this) { - DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(displayId); - if (displayInfo == null) { - Ln.e("Display " + displayId + " not found\n" + LogUtils.buildDisplayListMessage()); - return; - } - - deviceSize = displayInfo.getSize(); - screenInfo = ScreenInfo.computeScreenInfo(displayInfo.getRotation(), deviceSize, crop, maxSize, lockVideoOrientation); - // notify - if (foldListener != null) { - foldListener.onFoldChanged(displayId, folded); - } - } - } - }); - } if (options.getControl() && options.getClipboardAutosync()) { // If control and autosync are enabled, synchronize Android clipboard to the computer automatically @@ -158,38 +81,20 @@ 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"); - } - // main display or any display on Android >= 10 - supportsInputEvents = displayId == 0 || Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10; + supportsInputEvents = options.getDisplayId() == 0 || Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10; if (!supportsInputEvents) { Ln.w("Input events are not supported for secondary displays before Android 10"); } } - public int getDisplayId() { - return displayId; - } - - public synchronized void setMaxSize(int newMaxSize) { - maxSize = newMaxSize; - screenInfo = ScreenInfo.computeScreenInfo(screenInfo.getReverseVideoRotation(), deviceSize, crop, newMaxSize, lockVideoOrientation); - } - - 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 + // it hides the field on purpose, to read it with atomic access @SuppressWarnings("checkstyle:HiddenField") - ScreenInfo screenInfo = getScreenInfo(); // read with synchronization + ScreenInfo screenInfo = this.screenInfo.get(); + if (screenInfo == null) { + return null; + } // ignore the locked video orientation, the events will apply in coordinates considered in the physical device orientation Size unlockedVideoSize = screenInfo.getUnlockedVideoSize(); @@ -223,6 +128,10 @@ public final class Device { return supportsInputEvents; } + public void setScreenInfo(ScreenInfo screenInfo) { + this.screenInfo.set(screenInfo); + } + public static boolean injectEvent(InputEvent inputEvent, int displayId, int injectMode) { if (!supportsInputEvents(displayId)) { throw new AssertionError("Could not inject input event if !supportsInputEvents()"); @@ -263,14 +172,6 @@ public final class Device { return ServiceManager.getPowerManager().isScreenOn(); } - public synchronized void setRotationListener(RotationListener rotationListener) { - this.rotationListener = rotationListener; - } - - public synchronized void setFoldListener(FoldListener foldlistener) { - this.foldListener = foldlistener; - } - public synchronized void setClipboardListener(ClipboardListener clipboardListener) { this.clipboardListener = clipboardListener; } diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java index e6357410..af9a9283 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java @@ -1,9 +1,12 @@ package com.genymobile.scrcpy.video; import com.genymobile.scrcpy.AndroidVersions; +import com.genymobile.scrcpy.device.ConfigurationException; import com.genymobile.scrcpy.device.Device; +import com.genymobile.scrcpy.device.DisplayInfo; import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.util.Ln; +import com.genymobile.scrcpy.util.LogUtils; import com.genymobile.scrcpy.wrappers.ServiceManager; import com.genymobile.scrcpy.wrappers.SurfaceControl; @@ -11,33 +14,104 @@ import android.graphics.Rect; import android.hardware.display.VirtualDisplay; import android.os.Build; import android.os.IBinder; +import android.view.IDisplayFoldListener; +import android.view.IRotationWatcher; import android.view.Surface; -public class ScreenCapture extends SurfaceCapture implements Device.RotationListener, Device.FoldListener { +public class ScreenCapture extends SurfaceCapture { private final Device device; + + private final int displayId; + private int maxSize; + private final Rect crop; + private final int lockVideoOrientation; + private int layerStack; + + private Size deviceSize; + private ScreenInfo screenInfo; + private IBinder display; private VirtualDisplay virtualDisplay; - public ScreenCapture(Device device) { + private IRotationWatcher rotationWatcher; + private IDisplayFoldListener displayFoldListener; + + public ScreenCapture(Device device, int displayId, int maxSize, Rect crop, int lockVideoOrientation) { this.device = device; + this.displayId = displayId; + this.maxSize = maxSize; + this.crop = crop; + this.lockVideoOrientation = lockVideoOrientation; } @Override - public void init() { - device.setRotationListener(this); - device.setFoldListener(this); + public void init() throws ConfigurationException { + DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(displayId); + if (displayInfo == null) { + Ln.e("Display " + displayId + " not found\n" + LogUtils.buildDisplayListMessage()); + throw new ConfigurationException("Unknown display id: " + displayId); + } + + deviceSize = displayInfo.getSize(); + ScreenInfo si = ScreenInfo.computeScreenInfo(displayInfo.getRotation(), deviceSize, crop, maxSize, lockVideoOrientation); + setScreenInfo(si); + layerStack = displayInfo.getLayerStack(); + + if (displayId == 0) { + rotationWatcher = new IRotationWatcher.Stub() { + @Override + public void onRotationChanged(int rotation) { + synchronized (ScreenCapture.this) { + ScreenInfo si = screenInfo.withDeviceRotation(rotation); + setScreenInfo(si); + } + + requestReset(); + } + }; + ServiceManager.getWindowManager().registerRotationWatcher(rotationWatcher, displayId); + } + + if (Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10) { + displayFoldListener = new IDisplayFoldListener.Stub() { + @Override + public void onDisplayFoldChanged(int displayId, boolean folded) { + if (ScreenCapture.this.displayId != displayId) { + // Ignore events related to other display ids + return; + } + + synchronized (ScreenCapture.this) { + DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(displayId); + if (displayInfo == null) { + Ln.e("Display " + displayId + " not found\n" + LogUtils.buildDisplayListMessage()); + return; + } + + deviceSize = displayInfo.getSize(); + ScreenInfo si = ScreenInfo.computeScreenInfo(displayInfo.getRotation(), deviceSize, crop, maxSize, lockVideoOrientation); + setScreenInfo(si); + } + + requestReset(); + } + }; + ServiceManager.getWindowManager().registerDisplayFoldListener(displayFoldListener); + } + + if ((displayInfo.getFlags() & DisplayInfo.FLAG_SUPPORTS_PROTECTED_BUFFERS) == 0) { + Ln.w("Display doesn't have FLAG_SUPPORTS_PROTECTED_BUFFERS flag, mirroring can be restricted"); + } } @Override public void start(Surface surface) { - ScreenInfo screenInfo = device.getScreenInfo(); Rect contentRect = screenInfo.getContentRect(); // does not include the locked video orientation Rect unlockedVideoRect = screenInfo.getUnlockedVideoSize().toRect(); int videoRotation = screenInfo.getVideoRotation(); - int layerStack = device.getLayerStack(); if (display != null) { SurfaceControl.destroyDisplay(display); @@ -51,7 +125,7 @@ public class ScreenCapture extends SurfaceCapture implements Device.RotationList try { Rect videoRect = screenInfo.getVideoSize().toRect(); virtualDisplay = ServiceManager.getDisplayManager() - .createVirtualDisplay("scrcpy", videoRect.width(), videoRect.height(), device.getDisplayId(), surface); + .createVirtualDisplay("scrcpy", videoRect.width(), videoRect.height(), displayId, surface); Ln.d("Display: using DisplayManager API"); } catch (Exception displayManagerException) { try { @@ -68,8 +142,12 @@ public class ScreenCapture extends SurfaceCapture implements Device.RotationList @Override public void release() { - device.setRotationListener(null); - device.setFoldListener(null); + if (rotationWatcher != null) { + ServiceManager.getWindowManager().unregisterRotationWatcher(rotationWatcher); + } + if (Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10) { + ServiceManager.getWindowManager().unregisterDisplayFoldListener(displayFoldListener); + } if (display != null) { SurfaceControl.destroyDisplay(display); display = null; @@ -81,26 +159,18 @@ public class ScreenCapture extends SurfaceCapture implements Device.RotationList } @Override - public Size getSize() { - return device.getScreenInfo().getVideoSize(); + public synchronized Size getSize() { + return screenInfo.getVideoSize(); } @Override - public boolean setMaxSize(int maxSize) { - device.setMaxSize(maxSize); + public synchronized boolean setMaxSize(int newMaxSize) { + maxSize = newMaxSize; + ScreenInfo si = ScreenInfo.computeScreenInfo(screenInfo.getReverseVideoRotation(), deviceSize, crop, newMaxSize, lockVideoOrientation); + setScreenInfo(si); return true; } - @Override - public void onFoldChanged(int displayId, boolean folded) { - requestReset(); - } - - @Override - public void onRotationChanged(int rotation) { - requestReset(); - } - private static IBinder createDisplay() throws Exception { // Since Android 12 (preview), secure displays could not be created with shell permissions anymore. // On Android 12 preview, SDK_INT is still R (not S), but CODENAME is "S". @@ -119,4 +189,9 @@ public class ScreenCapture extends SurfaceCapture implements Device.RotationList SurfaceControl.closeTransaction(); } } + + private void setScreenInfo(ScreenInfo si) { + screenInfo = si; + device.setScreenInfo(si); + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java index 3118ddc8..fe679beb 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java @@ -1,5 +1,6 @@ package com.genymobile.scrcpy.video; +import com.genymobile.scrcpy.device.ConfigurationException; import com.genymobile.scrcpy.device.Size; import android.view.Surface; @@ -34,7 +35,7 @@ public abstract class SurfaceCapture { /** * Called once before the capture starts. */ - public abstract void init() throws IOException; + public abstract void init() throws ConfigurationException, IOException; /** * Called after the capture ends (if and only if {@link #init()} has been called). 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 5894b836..ee36139a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java @@ -201,13 +201,29 @@ public final class WindowManager { } } + public void unregisterRotationWatcher(IRotationWatcher rotationWatcher) { + try { + manager.getClass().getMethod("removeRotationWatcher", IRotationWatcher.class).invoke(manager, rotationWatcher); + } catch (Exception e) { + Ln.e("Could not unregister rotation watcher", e); + } + } + @TargetApi(AndroidVersions.API_29_ANDROID_10) public void registerDisplayFoldListener(IDisplayFoldListener foldListener) { try { - Class cls = manager.getClass(); - cls.getMethod("registerDisplayFoldListener", IDisplayFoldListener.class).invoke(manager, foldListener); + manager.getClass().getMethod("registerDisplayFoldListener", IDisplayFoldListener.class).invoke(manager, foldListener); } catch (Exception e) { Ln.e("Could not register display fold listener", e); } } + + @TargetApi(AndroidVersions.API_29_ANDROID_10) + public void unregisterDisplayFoldListener(IDisplayFoldListener foldListener) { + try { + manager.getClass().getMethod("unregisterDisplayFoldListener", IDisplayFoldListener.class).invoke(manager, foldListener); + } catch (Exception e) { + Ln.e("Could not unregister display fold listener", e); + } + } } From 5f0480c0398bb3370d3a68182a0a4f950c56d824 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 12 Oct 2024 09:23:31 +0200 Subject: [PATCH 2005/2244] Ignore first displayFoldChanged event An event is posted on registration to signal the initial state. This had no impact when the listener was registered from Device (before it was moved to ScreenCapture), because this first initial event was already triggered when ScreenCapture started listening. But now, it causes the first encoding to be reset immediately. To avoid that, ignore the first event. Refs PR #5370 --- .../java/com/genymobile/scrcpy/video/ScreenCapture.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java index af9a9283..df7cd8f2 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java @@ -75,8 +75,17 @@ public class ScreenCapture extends SurfaceCapture { if (Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10) { displayFoldListener = new IDisplayFoldListener.Stub() { + + private boolean first = true; + @Override public void onDisplayFoldChanged(int displayId, boolean folded) { + if (first) { + // An event is posted on registration to signal the initial state. Ignore it to avoid restarting encoding. + first = false; + return; + } + if (ScreenCapture.this.displayId != displayId) { // Ignore events related to other display ids return; From 68e54d9b0b393b53539218679b21a105fba4ef8f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 12 Oct 2024 09:23:31 +0200 Subject: [PATCH 2006/2244] Refactor to call getSize() only once Avoid to call capture.getSize() (provided by the SurfaceCapture implementation) twice. PR #5370 --- .../java/com/genymobile/scrcpy/video/SurfaceEncoder.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java index 5a9417da..4da1454d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java @@ -68,12 +68,16 @@ public class SurfaceEncoder implements AsyncProcessor { capture.init(); try { - streamer.writeVideoHeader(capture.getSize()); - boolean alive; + boolean headerWritten = false; do { Size size = capture.getSize(); + if (!headerWritten) { + streamer.writeVideoHeader(size); + headerWritten = true; + } + format.setInteger(MediaFormat.KEY_WIDTH, size.getWidth()); format.setInteger(MediaFormat.KEY_HEIGHT, size.getHeight()); From 12d5ca4d5ee870ed6112b5fa396924a090b206b2 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 12 Oct 2024 09:23:31 +0200 Subject: [PATCH 2007/2244] Move local variables in ScreenCapture Do not initialize variables when they are not used. PR #5370 --- .../com/genymobile/scrcpy/video/ScreenCapture.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java index df7cd8f2..e279f569 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java @@ -116,12 +116,6 @@ public class ScreenCapture extends SurfaceCapture { @Override public void start(Surface surface) { - Rect contentRect = screenInfo.getContentRect(); - - // does not include the locked video orientation - Rect unlockedVideoRect = screenInfo.getUnlockedVideoSize().toRect(); - int videoRotation = screenInfo.getVideoRotation(); - if (display != null) { SurfaceControl.destroyDisplay(display); display = null; @@ -139,6 +133,13 @@ public class ScreenCapture extends SurfaceCapture { } catch (Exception displayManagerException) { try { display = createDisplay(); + + Rect contentRect = screenInfo.getContentRect(); + + // does not include the locked video orientation + Rect unlockedVideoRect = screenInfo.getUnlockedVideoSize().toRect(); + int videoRotation = screenInfo.getVideoRotation(); + setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack); Ln.d("Display: using SurfaceControl API"); } catch (Exception surfaceControlException) { From 5851b6258037d2b2c7d763fac17d33d131eb8922 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 12 Oct 2024 09:23:31 +0200 Subject: [PATCH 2008/2244] Simplify virtual display video size Do not use an unnecessary intermediate Rect object. PR #5370 --- .../main/java/com/genymobile/scrcpy/video/ScreenCapture.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java index e279f569..05d349da 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java @@ -126,9 +126,9 @@ public class ScreenCapture extends SurfaceCapture { } try { - Rect videoRect = screenInfo.getVideoSize().toRect(); + Size videoSize = screenInfo.getVideoSize(); virtualDisplay = ServiceManager.getDisplayManager() - .createVirtualDisplay("scrcpy", videoRect.width(), videoRect.height(), displayId, surface); + .createVirtualDisplay("scrcpy", videoSize.getWidth(), videoSize.getHeight(), displayId, surface); Ln.d("Display: using DisplayManager API"); } catch (Exception displayManagerException) { try { From b60e1747809cce58793a8c0d54b499df87a6a975 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 12 Oct 2024 09:23:31 +0200 Subject: [PATCH 2009/2244] Add capture prepare() step Add a function called before each capture starts (before getSize() is called). This allows to compute the ScreenInfo instance once exactly when needed. PR #5370 --- .../scrcpy/video/ScreenCapture.java | 58 ++++++------------- .../genymobile/scrcpy/video/ScreenInfo.java | 22 ------- .../scrcpy/video/SurfaceCapture.java | 11 +++- .../scrcpy/video/SurfaceEncoder.java | 1 + 4 files changed, 28 insertions(+), 64 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java index 05d349da..f71ff020 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java @@ -26,9 +26,8 @@ public class ScreenCapture extends SurfaceCapture { private int maxSize; private final Rect crop; private final int lockVideoOrientation; - private int layerStack; - private Size deviceSize; + private DisplayInfo displayInfo; private ScreenInfo screenInfo; private IBinder display; @@ -46,27 +45,11 @@ public class ScreenCapture extends SurfaceCapture { } @Override - public void init() throws ConfigurationException { - DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(displayId); - if (displayInfo == null) { - Ln.e("Display " + displayId + " not found\n" + LogUtils.buildDisplayListMessage()); - throw new ConfigurationException("Unknown display id: " + displayId); - } - - deviceSize = displayInfo.getSize(); - ScreenInfo si = ScreenInfo.computeScreenInfo(displayInfo.getRotation(), deviceSize, crop, maxSize, lockVideoOrientation); - setScreenInfo(si); - layerStack = displayInfo.getLayerStack(); - + public void init() { if (displayId == 0) { rotationWatcher = new IRotationWatcher.Stub() { @Override public void onRotationChanged(int rotation) { - synchronized (ScreenCapture.this) { - ScreenInfo si = screenInfo.withDeviceRotation(rotation); - setScreenInfo(si); - } - requestReset(); } }; @@ -91,27 +74,26 @@ public class ScreenCapture extends SurfaceCapture { return; } - synchronized (ScreenCapture.this) { - DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(displayId); - if (displayInfo == null) { - Ln.e("Display " + displayId + " not found\n" + LogUtils.buildDisplayListMessage()); - return; - } - - deviceSize = displayInfo.getSize(); - ScreenInfo si = ScreenInfo.computeScreenInfo(displayInfo.getRotation(), deviceSize, crop, maxSize, lockVideoOrientation); - setScreenInfo(si); - } - requestReset(); } }; ServiceManager.getWindowManager().registerDisplayFoldListener(displayFoldListener); } + } + + @Override + public void prepare() throws ConfigurationException { + displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(displayId); + if (displayInfo == null) { + Ln.e("Display " + displayId + " not found\n" + LogUtils.buildDisplayListMessage()); + throw new ConfigurationException("Unknown display id: " + displayId); + } if ((displayInfo.getFlags() & DisplayInfo.FLAG_SUPPORTS_PROTECTED_BUFFERS) == 0) { Ln.w("Display doesn't have FLAG_SUPPORTS_PROTECTED_BUFFERS flag, mirroring can be restricted"); } + + screenInfo = ScreenInfo.computeScreenInfo(displayInfo.getRotation(), displayInfo.getSize(), crop, maxSize, lockVideoOrientation); } @Override @@ -139,6 +121,7 @@ public class ScreenCapture extends SurfaceCapture { // does not include the locked video orientation Rect unlockedVideoRect = screenInfo.getUnlockedVideoSize().toRect(); int videoRotation = screenInfo.getVideoRotation(); + int layerStack = displayInfo.getLayerStack(); setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack); Ln.d("Display: using SurfaceControl API"); @@ -148,6 +131,8 @@ public class ScreenCapture extends SurfaceCapture { throw new AssertionError("Could not create display"); } } + + device.setScreenInfo(screenInfo); } @Override @@ -169,15 +154,13 @@ public class ScreenCapture extends SurfaceCapture { } @Override - public synchronized Size getSize() { + public Size getSize() { return screenInfo.getVideoSize(); } @Override - public synchronized boolean setMaxSize(int newMaxSize) { + public boolean setMaxSize(int newMaxSize) { maxSize = newMaxSize; - ScreenInfo si = ScreenInfo.computeScreenInfo(screenInfo.getReverseVideoRotation(), deviceSize, crop, newMaxSize, lockVideoOrientation); - setScreenInfo(si); return true; } @@ -199,9 +182,4 @@ public class ScreenCapture extends SurfaceCapture { SurfaceControl.closeTransaction(); } } - - private void setScreenInfo(ScreenInfo si) { - screenInfo = si; - device.setScreenInfo(si); - } } diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java index bd0a3b62..1f74ce34 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java @@ -63,28 +63,6 @@ public final class ScreenInfo { return unlockedVideoSize.rotate(); } - public int getDeviceRotation() { - return deviceRotation; - } - - public ScreenInfo withDeviceRotation(int newDeviceRotation) { - if (newDeviceRotation == deviceRotation) { - return this; - } - // true if changed between portrait and landscape - boolean orientationChanged = (deviceRotation + newDeviceRotation) % 2 != 0; - Rect newContentRect; - Size newUnlockedVideoSize; - if (orientationChanged) { - newContentRect = flipRect(contentRect); - newUnlockedVideoSize = unlockedVideoSize.rotate(); - } else { - newContentRect = contentRect; - newUnlockedVideoSize = unlockedVideoSize; - } - return new ScreenInfo(newContentRect, newUnlockedVideoSize, newDeviceRotation, lockedVideoOrientation); - } - public static ScreenInfo computeScreenInfo(int rotation, Size deviceSize, Rect crop, int maxSize, int lockedVideoOrientation) { if (lockedVideoOrientation == Device.LOCK_VIDEO_ORIENTATION_INITIAL) { // The user requested to lock the video orientation to the current orientation diff --git a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java index fe679beb..0ee93c92 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java @@ -33,15 +33,22 @@ public abstract class SurfaceCapture { } /** - * Called once before the capture starts. + * Called once before the first capture starts. */ public abstract void init() throws ConfigurationException, IOException; /** - * Called after the capture ends (if and only if {@link #init()} has been called). + * Called after the last capture ends (if and only if {@link #init()} has been called). */ public abstract void release(); + /** + * Called once before each capture starts, before {@link #getSize()}. + */ + public void prepare() throws ConfigurationException { + // empty by default + } + /** * Start the capture to the target surface. * diff --git a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java index 4da1454d..84bda1ce 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java @@ -72,6 +72,7 @@ public class SurfaceEncoder implements AsyncProcessor { boolean headerWritten = false; do { + capture.prepare(); Size size = capture.getSize(); if (!headerWritten) { streamer.writeVideoHeader(size); From 7cfefae5e110f32940f6ad35dbd45813d066f735 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 12 Oct 2024 09:23:31 +0200 Subject: [PATCH 2010/2244] Move implicit displayId to Controller Remove from Device the functions using an implicit displayId. Move them to Controller, which knows best which displayId it must use. This will allow to properly dispatch events either to the origin display or to the virtual display created for mirroring. PR #5370 --- .../java/com/genymobile/scrcpy/Server.java | 3 +- .../genymobile/scrcpy/control/Controller.java | 67 ++++++++++++------- .../com/genymobile/scrcpy/device/Device.java | 34 +--------- 3 files changed, 48 insertions(+), 56 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 9802e0f5..ed3ae669 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -156,7 +156,8 @@ public final class Server { if (control) { ControlChannel controlChannel = connection.getControlChannel(); - Controller controller = new Controller(device, controlChannel, cleanUp, options.getClipboardAutosync(), options.getPowerOn()); + Controller controller = new Controller( + device, options.getDisplayId(), controlChannel, cleanUp, options.getClipboardAutosync(), options.getPowerOn()); device.setClipboardListener(text -> { DeviceMessage msg = DeviceMessage.createClipboard(text); controller.getSender().send(msg); diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index 8fa27e81..ee2e1749 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -14,6 +14,7 @@ import android.content.Intent; import android.os.Build; import android.os.SystemClock; import android.view.InputDevice; +import android.view.InputEvent; import android.view.KeyCharacterMap; import android.view.KeyEvent; import android.view.MotionEvent; @@ -37,6 +38,8 @@ public class Controller implements AsyncProcessor { private UhidManager uhidManager; private final Device device; + private final int displayId; + private final boolean supportsInputEvents; private final ControlChannel controlChannel; private final CleanUp cleanUp; private final DeviceMessageSender sender; @@ -52,14 +55,20 @@ public class Controller implements AsyncProcessor { private boolean keepPowerModeOff; - public Controller(Device device, ControlChannel controlChannel, CleanUp cleanUp, boolean clipboardAutosync, boolean powerOn) { + public Controller(Device device, int displayId, ControlChannel controlChannel, CleanUp cleanUp, boolean clipboardAutosync, boolean powerOn) { this.device = device; + this.displayId = displayId; this.controlChannel = controlChannel; this.cleanUp = cleanUp; this.clipboardAutosync = clipboardAutosync; this.powerOn = powerOn; initPointers(); sender = new DeviceMessageSender(controlChannel); + + supportsInputEvents = Device.supportsInputEvents(displayId); + if (!supportsInputEvents) { + Ln.w("Input events are not supported for secondary displays before Android 10"); + } } private UhidManager getUhidManager() { @@ -86,7 +95,7 @@ public class Controller implements AsyncProcessor { private void control() throws IOException { // on start, power on the device if (powerOn && !Device.isScreenOn()) { - device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER, Device.INJECT_MODE_ASYNC); + Device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER, displayId, Device.INJECT_MODE_ASYNC); // dirty hack // After POWER is injected, the device is powered on asynchronously. @@ -154,27 +163,27 @@ public class Controller implements AsyncProcessor { switch (msg.getType()) { case ControlMessage.TYPE_INJECT_KEYCODE: - if (device.supportsInputEvents()) { + if (supportsInputEvents) { injectKeycode(msg.getAction(), msg.getKeycode(), msg.getRepeat(), msg.getMetaState()); } break; case ControlMessage.TYPE_INJECT_TEXT: - if (device.supportsInputEvents()) { + if (supportsInputEvents) { injectText(msg.getText()); } break; case ControlMessage.TYPE_INJECT_TOUCH_EVENT: - if (device.supportsInputEvents()) { + if (supportsInputEvents) { injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getActionButton(), msg.getButtons()); } break; case ControlMessage.TYPE_INJECT_SCROLL_EVENT: - if (device.supportsInputEvents()) { + if (supportsInputEvents) { injectScroll(msg.getPosition(), msg.getHScroll(), msg.getVScroll(), msg.getButtons()); } break; case ControlMessage.TYPE_BACK_OR_SCREEN_ON: - if (device.supportsInputEvents()) { + if (supportsInputEvents) { pressBackOrTurnScreenOn(msg.getAction()); } break; @@ -194,7 +203,7 @@ public class Controller implements AsyncProcessor { setClipboard(msg.getText(), msg.getPaste(), msg.getSequence()); break; case ControlMessage.TYPE_SET_SCREEN_POWER_MODE: - if (device.supportsInputEvents()) { + if (supportsInputEvents) { int mode = msg.getAction(); boolean setPowerModeOk = Device.setScreenPowerMode(mode); if (setPowerModeOk) { @@ -208,7 +217,7 @@ public class Controller implements AsyncProcessor { } break; case ControlMessage.TYPE_ROTATE_DEVICE: - device.rotateDevice(); + Device.rotateDevice(displayId); break; case ControlMessage.TYPE_UHID_CREATE: getUhidManager().open(msg.getId(), msg.getText(), msg.getData()); @@ -233,7 +242,7 @@ public class Controller implements AsyncProcessor { if (keepPowerModeOff && action == KeyEvent.ACTION_UP && (keycode == KeyEvent.KEYCODE_POWER || keycode == KeyEvent.KEYCODE_WAKEUP)) { schedulePowerModeOff(); } - return device.injectKeyEvent(action, keycode, repeat, metaState, Device.INJECT_MODE_ASYNC); + return injectKeyEvent(action, keycode, repeat, metaState, Device.INJECT_MODE_ASYNC); } private boolean injectChar(char c) { @@ -244,7 +253,7 @@ public class Controller implements AsyncProcessor { return false; } for (KeyEvent event : events) { - if (!device.injectEvent(event, Device.INJECT_MODE_ASYNC)) { + if (!injectEvent(event, Device.INJECT_MODE_ASYNC)) { return false; } } @@ -325,7 +334,7 @@ public class Controller implements AsyncProcessor { // First button pressed: ACTION_DOWN MotionEvent downEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_DOWN, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0); - if (!device.injectEvent(downEvent, Device.INJECT_MODE_ASYNC)) { + if (!injectEvent(downEvent, Device.INJECT_MODE_ASYNC)) { return false; } } @@ -336,7 +345,7 @@ public class Controller implements AsyncProcessor { if (!InputManager.setActionButton(pressEvent, actionButton)) { return false; } - if (!device.injectEvent(pressEvent, Device.INJECT_MODE_ASYNC)) { + if (!injectEvent(pressEvent, Device.INJECT_MODE_ASYNC)) { return false; } @@ -350,7 +359,7 @@ public class Controller implements AsyncProcessor { if (!InputManager.setActionButton(releaseEvent, actionButton)) { return false; } - if (!device.injectEvent(releaseEvent, Device.INJECT_MODE_ASYNC)) { + if (!injectEvent(releaseEvent, Device.INJECT_MODE_ASYNC)) { return false; } @@ -358,7 +367,7 @@ public class Controller implements AsyncProcessor { // Last button released: ACTION_UP MotionEvent upEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_UP, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0); - if (!device.injectEvent(upEvent, Device.INJECT_MODE_ASYNC)) { + if (!injectEvent(upEvent, Device.INJECT_MODE_ASYNC)) { return false; } } @@ -369,7 +378,7 @@ public class Controller implements AsyncProcessor { MotionEvent event = MotionEvent.obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0); - return device.injectEvent(event, Device.INJECT_MODE_ASYNC); + return injectEvent(event, Device.INJECT_MODE_ASYNC); } private boolean injectScroll(Position position, float hScroll, float vScroll, int buttons) { @@ -391,7 +400,7 @@ public class Controller implements AsyncProcessor { MotionEvent event = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, InputDevice.SOURCE_MOUSE, 0); - return device.injectEvent(event, Device.INJECT_MODE_ASYNC); + return injectEvent(event, Device.INJECT_MODE_ASYNC); } /** @@ -406,7 +415,7 @@ public class Controller implements AsyncProcessor { private boolean pressBackOrTurnScreenOn(int action) { if (Device.isScreenOn()) { - return device.injectKeyEvent(action, KeyEvent.KEYCODE_BACK, 0, 0, Device.INJECT_MODE_ASYNC); + return injectKeyEvent(action, KeyEvent.KEYCODE_BACK, 0, 0, Device.INJECT_MODE_ASYNC); } // Screen is off @@ -419,15 +428,15 @@ public class Controller implements AsyncProcessor { if (keepPowerModeOff) { schedulePowerModeOff(); } - return device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER, Device.INJECT_MODE_ASYNC); + return pressReleaseKeycode(KeyEvent.KEYCODE_POWER, Device.INJECT_MODE_ASYNC); } private void getClipboard(int copyKey) { // On Android >= 7, press the COPY or CUT key if requested - if (copyKey != ControlMessage.COPY_KEY_NONE && Build.VERSION.SDK_INT >= AndroidVersions.API_24_ANDROID_7_0 && device.supportsInputEvents()) { + if (copyKey != ControlMessage.COPY_KEY_NONE && Build.VERSION.SDK_INT >= AndroidVersions.API_24_ANDROID_7_0 && supportsInputEvents) { int key = copyKey == ControlMessage.COPY_KEY_COPY ? KeyEvent.KEYCODE_COPY : KeyEvent.KEYCODE_CUT; // Wait until the event is finished, to ensure that the clipboard text we read just after is the correct one - device.pressReleaseKeycode(key, Device.INJECT_MODE_WAIT_FOR_FINISH); + pressReleaseKeycode(key, Device.INJECT_MODE_WAIT_FOR_FINISH); } // If clipboard autosync is enabled, then the device clipboard is synchronized to the computer clipboard whenever it changes, in @@ -449,8 +458,8 @@ public class Controller implements AsyncProcessor { } // On Android >= 7, also press the PASTE key if requested - if (paste && Build.VERSION.SDK_INT >= AndroidVersions.API_24_ANDROID_7_0 && device.supportsInputEvents()) { - device.pressReleaseKeycode(KeyEvent.KEYCODE_PASTE, Device.INJECT_MODE_ASYNC); + if (paste && Build.VERSION.SDK_INT >= AndroidVersions.API_24_ANDROID_7_0 && supportsInputEvents) { + pressReleaseKeycode(KeyEvent.KEYCODE_PASTE, Device.INJECT_MODE_ASYNC); } if (sequence != ControlMessage.SEQUENCE_INVALID) { @@ -466,4 +475,16 @@ public class Controller implements AsyncProcessor { Intent intent = new Intent("android.settings.HARD_KEYBOARD_SETTINGS"); ServiceManager.getActivityManager().startActivity(intent); } + + private boolean injectEvent(InputEvent event, int injectMode) { + return Device.injectEvent(event, displayId, injectMode); + } + + private boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState, int injectMode) { + return Device.injectKeyEvent(action, keyCode, repeat, metaState, displayId, injectMode); + } + + private boolean pressReleaseKeycode(int keyCode, int injectMode) { + return Device.pressReleaseKeycode(keyCode, displayId, injectMode); + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Device.java b/server/src/main/java/com/genymobile/scrcpy/device/Device.java index 63e33988..7972d740 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Device.java @@ -43,18 +43,9 @@ public final class Device { private ClipboardListener clipboardListener; private final AtomicBoolean isSettingClipboard = new AtomicBoolean(); - /** - * Logical display identifier - */ - private final int displayId; - - private final boolean supportsInputEvents; - private final AtomicReference screenInfo = new AtomicReference<>(); // set by the ScreenCapture instance public Device(Options options) { - displayId = options.getDisplayId(); - if (options.getControl() && options.getClipboardAutosync()) { // If control and autosync are enabled, synchronize Android clipboard to the computer automatically ClipboardManager clipboardManager = ServiceManager.getClipboardManager(); @@ -80,12 +71,6 @@ public final class Device { Ln.w("No clipboard manager, copy-paste between device and computer will not work"); } } - - // main display or any display on Android >= 10 - supportsInputEvents = options.getDisplayId() == 0 || Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10; - if (!supportsInputEvents) { - Ln.w("Input events are not supported for secondary displays before Android 10"); - } } public Point getPhysicalPoint(Position position) { @@ -121,13 +106,10 @@ public final class Device { } public static boolean supportsInputEvents(int displayId) { + // main display or any display on Android >= 10 return displayId == 0 || Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10; } - public boolean supportsInputEvents() { - return supportsInputEvents; - } - public void setScreenInfo(ScreenInfo screenInfo) { this.screenInfo.set(screenInfo); } @@ -144,10 +126,6 @@ public final class Device { return ServiceManager.getInputManager().injectInputEvent(inputEvent, injectMode); } - public boolean injectEvent(InputEvent event, int injectMode) { - return injectEvent(event, displayId, injectMode); - } - public static boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState, int displayId, int injectMode) { long now = SystemClock.uptimeMillis(); KeyEvent event = new KeyEvent(now, now, action, keyCode, repeat, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0, @@ -155,19 +133,11 @@ public final class Device { return injectEvent(event, displayId, injectMode); } - public boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState, int injectMode) { - return injectKeyEvent(action, keyCode, repeat, metaState, displayId, injectMode); - } - public static boolean pressReleaseKeycode(int keyCode, int displayId, int injectMode) { return injectKeyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0, displayId, injectMode) && injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0, displayId, injectMode); } - public boolean pressReleaseKeycode(int keyCode, int injectMode) { - return pressReleaseKeycode(keyCode, displayId, injectMode); - } - public static boolean isScreenOn() { return ServiceManager.getPowerManager().isScreenOn(); } @@ -277,7 +247,7 @@ public final class Device { /** * Disable auto-rotation (if enabled), set the screen rotation and re-enable auto-rotation (if it was enabled). */ - public void rotateDevice() { + public static void rotateDevice(int displayId) { WindowManager wm = ServiceManager.getWindowManager(); boolean accelerometerRotation = !wm.isRotationFrozen(displayId); From d9164295666ef9a27b45eecc8608ca1fc0e0fbd8 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 12 Oct 2024 09:23:31 +0200 Subject: [PATCH 2011/2244] Move clipboard management to Controller Continue to declutter the global Device. PR #5370 --- .../java/com/genymobile/scrcpy/Server.java | 7 +-- .../genymobile/scrcpy/control/Controller.java | 36 ++++++++++++-- .../com/genymobile/scrcpy/device/Device.java | 49 +------------------ 3 files changed, 34 insertions(+), 58 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index ed3ae669..0b60dbdc 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -9,7 +9,6 @@ import com.genymobile.scrcpy.audio.AudioRawRecorder; import com.genymobile.scrcpy.audio.AudioSource; import com.genymobile.scrcpy.control.ControlChannel; import com.genymobile.scrcpy.control.Controller; -import com.genymobile.scrcpy.control.DeviceMessage; import com.genymobile.scrcpy.device.ConfigurationException; import com.genymobile.scrcpy.device.DesktopConnection; import com.genymobile.scrcpy.device.Device; @@ -142,7 +141,7 @@ public final class Server { boolean sendDummyByte = options.getSendDummyByte(); boolean camera = video && options.getVideoSource() == VideoSource.CAMERA; - final Device device = camera ? null : new Device(options); + final Device device = camera ? null : new Device(); Workarounds.apply(); @@ -158,10 +157,6 @@ public final class Server { ControlChannel controlChannel = connection.getControlChannel(); Controller controller = new Controller( device, options.getDisplayId(), controlChannel, cleanUp, options.getClipboardAutosync(), options.getPowerOn()); - device.setClipboardListener(text -> { - DeviceMessage msg = DeviceMessage.createClipboard(text); - controller.getSender().send(msg); - }); asyncProcessors.add(controller); } diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index ee2e1749..e6463563 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -7,9 +7,11 @@ import com.genymobile.scrcpy.device.Device; import com.genymobile.scrcpy.device.Point; import com.genymobile.scrcpy.device.Position; import com.genymobile.scrcpy.util.Ln; +import com.genymobile.scrcpy.wrappers.ClipboardManager; import com.genymobile.scrcpy.wrappers.InputManager; import com.genymobile.scrcpy.wrappers.ServiceManager; +import android.content.IOnPrimaryClipChangedListener; import android.content.Intent; import android.os.Build; import android.os.SystemClock; @@ -23,6 +25,7 @@ import java.io.IOException; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; public class Controller implements AsyncProcessor { @@ -48,6 +51,8 @@ public class Controller implements AsyncProcessor { private final KeyCharacterMap charMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD); + private final AtomicBoolean isSettingClipboard = new AtomicBoolean(); + private long lastTouchDown; private final PointersState pointersState = new PointersState(); private final MotionEvent.PointerProperties[] pointerProperties = new MotionEvent.PointerProperties[PointersState.MAX_POINTERS]; @@ -69,6 +74,29 @@ public class Controller implements AsyncProcessor { if (!supportsInputEvents) { Ln.w("Input events are not supported for secondary displays before Android 10"); } + + if (clipboardAutosync) { + // If control and autosync are enabled, synchronize Android clipboard to the computer automatically + 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; + } + String text = Device.getClipboardText(); + if (text != null) { + DeviceMessage msg = DeviceMessage.createClipboard(text); + sender.send(msg); + } + } + }); + } else { + Ln.w("No clipboard manager, copy-paste between device and computer will not work"); + } + } } private UhidManager getUhidManager() { @@ -148,10 +176,6 @@ public class Controller implements AsyncProcessor { sender.join(); } - public DeviceMessageSender getSender() { - return sender; - } - private boolean handleEvent() throws IOException { ControlMessage msg; try { @@ -452,7 +476,9 @@ public class Controller implements AsyncProcessor { } private boolean setClipboard(String text, boolean paste, long sequence) { - boolean ok = device.setClipboardText(text); + isSettingClipboard.set(true); + boolean ok = Device.setClipboardText(text); + isSettingClipboard.set(false); if (ok) { Ln.i("Device clipboard set"); } diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Device.java b/server/src/main/java/com/genymobile/scrcpy/device/Device.java index 7972d740..1765ccf2 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Device.java @@ -1,7 +1,6 @@ package com.genymobile.scrcpy.device; import com.genymobile.scrcpy.AndroidVersions; -import com.genymobile.scrcpy.Options; import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.video.ScreenInfo; import com.genymobile.scrcpy.wrappers.ClipboardManager; @@ -11,7 +10,6 @@ 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; @@ -21,7 +19,6 @@ import android.view.InputEvent; import android.view.KeyCharacterMap; import android.view.KeyEvent; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; public final class Device { @@ -36,43 +33,8 @@ public final class Device { public static final int LOCK_VIDEO_ORIENTATION_UNLOCKED = -1; public static final int LOCK_VIDEO_ORIENTATION_INITIAL = -2; - public interface ClipboardListener { - void onClipboardTextChanged(String text); - } - - private ClipboardListener clipboardListener; - private final AtomicBoolean isSettingClipboard = new AtomicBoolean(); - private final AtomicReference screenInfo = new AtomicReference<>(); // set by the ScreenCapture instance - public Device(Options options) { - if (options.getControl() && options.getClipboardAutosync()) { - // If control and autosync are enabled, synchronize Android clipboard to the computer automatically - 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"); - } - } - } - public Point getPhysicalPoint(Position position) { // it hides the field on purpose, to read it with atomic access @SuppressWarnings("checkstyle:HiddenField") @@ -142,10 +104,6 @@ public final class Device { return ServiceManager.getPowerManager().isScreenOn(); } - public synchronized void setClipboardListener(ClipboardListener clipboardListener) { - this.clipboardListener = clipboardListener; - } - public static void expandNotificationPanel() { ServiceManager.getStatusBarManager().expandNotificationsPanel(); } @@ -170,7 +128,7 @@ public final class Device { return s.toString(); } - public boolean setClipboardText(String text) { + public static boolean setClipboardText(String text) { ClipboardManager clipboardManager = ServiceManager.getClipboardManager(); if (clipboardManager == null) { return false; @@ -185,10 +143,7 @@ public final class Device { return false; } - isSettingClipboard.set(true); - boolean ok = clipboardManager.setText(text); - isSettingClipboard.set(false); - return ok; + return clipboardManager.setText(text); } /** From f1368d9a8f936ab47d51de5af30c7e64bf5285bc Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 12 Oct 2024 09:23:31 +0200 Subject: [PATCH 2012/2244] Introduce PositionMapper Extract the function that converts coordinates from video space to display space into a separate component. It only requires the specific data it uses and does not need a full ScreenInfo object (although it can be created from a ScreenInfo instance). PR #5370 --- .../scrcpy/control/PositionMapper.java | 48 +++++++++++++++++++ .../com/genymobile/scrcpy/device/Device.java | 32 +++---------- .../scrcpy/video/ScreenCapture.java | 4 +- 3 files changed, 58 insertions(+), 26 deletions(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/control/PositionMapper.java diff --git a/server/src/main/java/com/genymobile/scrcpy/control/PositionMapper.java b/server/src/main/java/com/genymobile/scrcpy/control/PositionMapper.java new file mode 100644 index 00000000..2ebb5961 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/control/PositionMapper.java @@ -0,0 +1,48 @@ +package com.genymobile.scrcpy.control; + +import com.genymobile.scrcpy.device.Point; +import com.genymobile.scrcpy.device.Position; +import com.genymobile.scrcpy.device.Size; +import com.genymobile.scrcpy.video.ScreenInfo; + +import android.graphics.Rect; + +public final class PositionMapper { + + private final Size videoSize; + private final Rect contentRect; + private final int coordsRotation; + + public PositionMapper(Size videoSize, Rect contentRect, int videoRotation) { + this.videoSize = videoSize; + this.contentRect = contentRect; + this.coordsRotation = reverseRotation(videoRotation); + } + + public static PositionMapper from(ScreenInfo screenInfo) { + // ignore the locked video orientation, the events will apply in coordinates considered in the physical device orientation + Size videoSize = screenInfo.getUnlockedVideoSize(); + return new PositionMapper(videoSize, screenInfo.getContentRect(), screenInfo.getVideoRotation()); + } + + private static int reverseRotation(int rotation) { + return (4 - rotation) % 4; + } + + public Point map(Position position) { + // reverse the video rotation to apply the events + Position devicePosition = position.rotate(coordsRotation); + + 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; + } + + 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); + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Device.java b/server/src/main/java/com/genymobile/scrcpy/device/Device.java index 1765ccf2..0977a2b7 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Device.java @@ -1,8 +1,8 @@ package com.genymobile.scrcpy.device; import com.genymobile.scrcpy.AndroidVersions; +import com.genymobile.scrcpy.control.PositionMapper; import com.genymobile.scrcpy.util.Ln; -import com.genymobile.scrcpy.video.ScreenInfo; import com.genymobile.scrcpy.wrappers.ClipboardManager; import com.genymobile.scrcpy.wrappers.DisplayControl; import com.genymobile.scrcpy.wrappers.InputManager; @@ -10,7 +10,6 @@ import com.genymobile.scrcpy.wrappers.ServiceManager; import com.genymobile.scrcpy.wrappers.SurfaceControl; import com.genymobile.scrcpy.wrappers.WindowManager; -import android.graphics.Rect; import android.os.Build; import android.os.IBinder; import android.os.SystemClock; @@ -33,34 +32,17 @@ public final class Device { public static final int LOCK_VIDEO_ORIENTATION_UNLOCKED = -1; public static final int LOCK_VIDEO_ORIENTATION_INITIAL = -2; - private final AtomicReference screenInfo = new AtomicReference<>(); // set by the ScreenCapture instance + private final AtomicReference positionMapper = new AtomicReference<>(); // set by the ScreenCapture instance public Point getPhysicalPoint(Position position) { // it hides the field on purpose, to read it with atomic access @SuppressWarnings("checkstyle:HiddenField") - ScreenInfo screenInfo = this.screenInfo.get(); - if (screenInfo == null) { + PositionMapper positionMapper = this.positionMapper.get(); + if (positionMapper == null) { return null; } - // 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 (!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() / unlockedVideoSize.getWidth(); - int convertedY = contentRect.top + point.getY() * contentRect.height() / unlockedVideoSize.getHeight(); - return new Point(convertedX, convertedY); + return positionMapper.map(position); } public static String getDeviceName() { @@ -72,8 +54,8 @@ public final class Device { return displayId == 0 || Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10; } - public void setScreenInfo(ScreenInfo screenInfo) { - this.screenInfo.set(screenInfo); + public void setPositionMapper(PositionMapper positionMapper) { + this.positionMapper.set(positionMapper); } public static boolean injectEvent(InputEvent inputEvent, int displayId, int injectMode) { diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java index f71ff020..066c9ae4 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java @@ -1,6 +1,7 @@ package com.genymobile.scrcpy.video; import com.genymobile.scrcpy.AndroidVersions; +import com.genymobile.scrcpy.control.PositionMapper; import com.genymobile.scrcpy.device.ConfigurationException; import com.genymobile.scrcpy.device.Device; import com.genymobile.scrcpy.device.DisplayInfo; @@ -132,7 +133,8 @@ public class ScreenCapture extends SurfaceCapture { } } - device.setScreenInfo(screenInfo); + PositionMapper positionMapper = PositionMapper.from(screenInfo); + device.setPositionMapper(positionMapper); } @Override From 7024d38199206e1a4e7c02e2c9016e856de4a3c0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 12 Oct 2024 09:23:31 +0200 Subject: [PATCH 2013/2244] Send PositionMapper to Controller directly When a new capture starts, send a new PositionMapper to the Controller without using the global Device as an intermediate. Now all Device methods are static. PR #5370 --- .../java/com/genymobile/scrcpy/Server.java | 10 +++--- .../genymobile/scrcpy/control/Controller.java | 32 +++++++++++++++---- .../com/genymobile/scrcpy/device/Device.java | 20 ++---------- .../scrcpy/video/ScreenCapture.java | 14 ++++---- .../scrcpy/video/VirtualDisplayListener.java | 7 ++++ 5 files changed, 45 insertions(+), 38 deletions(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/video/VirtualDisplayListener.java diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 0b60dbdc..91e7ce6c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -139,9 +139,6 @@ public final class Server { boolean video = options.getVideo(); boolean audio = options.getAudio(); boolean sendDummyByte = options.getSendDummyByte(); - boolean camera = video && options.getVideoSource() == VideoSource.CAMERA; - - final Device device = camera ? null : new Device(); Workarounds.apply(); @@ -153,10 +150,11 @@ public final class Server { connection.sendDeviceMeta(Device.getDeviceName()); } + Controller controller = null; + if (control) { ControlChannel controlChannel = connection.getControlChannel(); - Controller controller = new Controller( - device, options.getDisplayId(), controlChannel, cleanUp, options.getClipboardAutosync(), options.getPowerOn()); + controller = new Controller(options.getDisplayId(), controlChannel, cleanUp, options.getClipboardAutosync(), options.getPowerOn()); asyncProcessors.add(controller); } @@ -186,7 +184,7 @@ public final class Server { options.getSendFrameMeta()); SurfaceCapture surfaceCapture; if (options.getVideoSource() == VideoSource.DISPLAY) { - surfaceCapture = new ScreenCapture(device, options.getDisplayId(), options.getMaxSize(), options.getCrop(), + surfaceCapture = new ScreenCapture(controller, options.getDisplayId(), options.getMaxSize(), options.getCrop(), options.getLockVideoOrientation()); } else { surfaceCapture = new CameraCapture(options.getCameraId(), options.getCameraFacing(), options.getCameraSize(), diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index e6463563..ac870f27 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -7,6 +7,7 @@ import com.genymobile.scrcpy.device.Device; import com.genymobile.scrcpy.device.Point; import com.genymobile.scrcpy.device.Position; import com.genymobile.scrcpy.util.Ln; +import com.genymobile.scrcpy.video.VirtualDisplayListener; import com.genymobile.scrcpy.wrappers.ClipboardManager; import com.genymobile.scrcpy.wrappers.InputManager; import com.genymobile.scrcpy.wrappers.ServiceManager; @@ -26,8 +27,9 @@ import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; -public class Controller implements AsyncProcessor { +public class Controller implements AsyncProcessor, VirtualDisplayListener { private static final int DEFAULT_DEVICE_ID = 0; @@ -40,7 +42,6 @@ public class Controller implements AsyncProcessor { private UhidManager uhidManager; - private final Device device; private final int displayId; private final boolean supportsInputEvents; private final ControlChannel controlChannel; @@ -53,6 +54,8 @@ public class Controller implements AsyncProcessor { private final AtomicBoolean isSettingClipboard = new AtomicBoolean(); + private final AtomicReference positionMapper = new AtomicReference<>(); + private long lastTouchDown; private final PointersState pointersState = new PointersState(); private final MotionEvent.PointerProperties[] pointerProperties = new MotionEvent.PointerProperties[PointersState.MAX_POINTERS]; @@ -60,8 +63,7 @@ public class Controller implements AsyncProcessor { private boolean keepPowerModeOff; - public Controller(Device device, int displayId, ControlChannel controlChannel, CleanUp cleanUp, boolean clipboardAutosync, boolean powerOn) { - this.device = device; + public Controller(int displayId, ControlChannel controlChannel, CleanUp cleanUp, boolean clipboardAutosync, boolean powerOn) { this.displayId = displayId; this.controlChannel = controlChannel; this.cleanUp = cleanUp; @@ -99,6 +101,11 @@ public class Controller implements AsyncProcessor { } } + @Override + public void onNewVirtualDisplay(PositionMapper positionMapper) { + this.positionMapper.set(positionMapper); + } + private UhidManager getUhidManager() { if (uhidManager == null) { uhidManager = new UhidManager(sender); @@ -299,7 +306,7 @@ public class Controller implements AsyncProcessor { private boolean injectTouch(int action, long pointerId, Position position, float pressure, int actionButton, int buttons) { long now = SystemClock.uptimeMillis(); - Point point = device.getPhysicalPoint(position); + Point point = getPhysicalPoint(position); if (point == null) { Ln.w("Ignore touch event, it was generated for a different device size"); return false; @@ -407,9 +414,9 @@ public class Controller implements AsyncProcessor { private boolean injectScroll(Position position, float hScroll, float vScroll, int buttons) { long now = SystemClock.uptimeMillis(); - Point point = device.getPhysicalPoint(position); + Point point = getPhysicalPoint(position); if (point == null) { - // ignore event + Ln.w("Ignore scroll event, it was generated for a different device size"); return false; } @@ -427,6 +434,17 @@ public class Controller implements AsyncProcessor { return injectEvent(event, Device.INJECT_MODE_ASYNC); } + private Point getPhysicalPoint(Position position) { + // it hides the field on purpose, to read it with atomic access + @SuppressWarnings("checkstyle:HiddenField") + PositionMapper positionMapper = this.positionMapper.get(); + if (positionMapper == null) { + return null; + } + + return positionMapper.map(position); + } + /** * Schedule a call to set power mode to off after a small delay. */ diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Device.java b/server/src/main/java/com/genymobile/scrcpy/device/Device.java index 0977a2b7..35266d0e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Device.java @@ -1,7 +1,6 @@ package com.genymobile.scrcpy.device; import com.genymobile.scrcpy.AndroidVersions; -import com.genymobile.scrcpy.control.PositionMapper; import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.wrappers.ClipboardManager; import com.genymobile.scrcpy.wrappers.DisplayControl; @@ -18,8 +17,6 @@ import android.view.InputEvent; import android.view.KeyCharacterMap; import android.view.KeyEvent; -import java.util.concurrent.atomic.AtomicReference; - public final class Device { public static final int POWER_MODE_OFF = SurfaceControl.POWER_MODE_OFF; @@ -32,17 +29,8 @@ public final class Device { public static final int LOCK_VIDEO_ORIENTATION_UNLOCKED = -1; public static final int LOCK_VIDEO_ORIENTATION_INITIAL = -2; - private final AtomicReference positionMapper = new AtomicReference<>(); // set by the ScreenCapture instance - - public Point getPhysicalPoint(Position position) { - // it hides the field on purpose, to read it with atomic access - @SuppressWarnings("checkstyle:HiddenField") - PositionMapper positionMapper = this.positionMapper.get(); - if (positionMapper == null) { - return null; - } - - return positionMapper.map(position); + private Device() { + // not instantiable } public static String getDeviceName() { @@ -54,10 +42,6 @@ public final class Device { return displayId == 0 || Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10; } - public void setPositionMapper(PositionMapper positionMapper) { - this.positionMapper.set(positionMapper); - } - public static boolean injectEvent(InputEvent inputEvent, int displayId, int injectMode) { if (!supportsInputEvents(displayId)) { throw new AssertionError("Could not inject input event if !supportsInputEvents()"); diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java index 066c9ae4..95faaf39 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java @@ -3,7 +3,6 @@ package com.genymobile.scrcpy.video; import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.control.PositionMapper; import com.genymobile.scrcpy.device.ConfigurationException; -import com.genymobile.scrcpy.device.Device; import com.genymobile.scrcpy.device.DisplayInfo; import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.util.Ln; @@ -21,8 +20,7 @@ import android.view.Surface; public class ScreenCapture extends SurfaceCapture { - private final Device device; - + private final VirtualDisplayListener vdListener; private final int displayId; private int maxSize; private final Rect crop; @@ -37,8 +35,8 @@ public class ScreenCapture extends SurfaceCapture { private IRotationWatcher rotationWatcher; private IDisplayFoldListener displayFoldListener; - public ScreenCapture(Device device, int displayId, int maxSize, Rect crop, int lockVideoOrientation) { - this.device = device; + public ScreenCapture(VirtualDisplayListener vdListener, int displayId, int maxSize, Rect crop, int lockVideoOrientation) { + this.vdListener = vdListener; this.displayId = displayId; this.maxSize = maxSize; this.crop = crop; @@ -133,8 +131,10 @@ public class ScreenCapture extends SurfaceCapture { } } - PositionMapper positionMapper = PositionMapper.from(screenInfo); - device.setPositionMapper(positionMapper); + if (vdListener != null) { + PositionMapper positionMapper = PositionMapper.from(screenInfo); + vdListener.onNewVirtualDisplay(positionMapper); + } } @Override diff --git a/server/src/main/java/com/genymobile/scrcpy/video/VirtualDisplayListener.java b/server/src/main/java/com/genymobile/scrcpy/video/VirtualDisplayListener.java new file mode 100644 index 00000000..d978361e --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/video/VirtualDisplayListener.java @@ -0,0 +1,7 @@ +package com.genymobile.scrcpy.video; + +import com.genymobile.scrcpy.control.PositionMapper; + +public interface VirtualDisplayListener { + void onNewVirtualDisplay(PositionMapper positionMapper); +} From d19396718ee0c0ba7fb578f595a6553c0458da59 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 12 Oct 2024 09:23:31 +0200 Subject: [PATCH 2014/2244] Inject display-related events to virtual display Mouse and touch events must be sent to the virtual display id (used for mirroring), other events (like key events) must be sent to the original display id. Fixes #4598 Fixes #5137 PR #5370 Co-authored-by: nightmare --- .../genymobile/scrcpy/control/Controller.java | 74 ++++++++++++------- .../scrcpy/video/ScreenCapture.java | 11 ++- .../scrcpy/video/VirtualDisplayListener.java | 2 +- 3 files changed, 56 insertions(+), 31 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index ac870f27..5175ed5e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -17,7 +17,6 @@ import android.content.Intent; import android.os.Build; import android.os.SystemClock; import android.view.InputDevice; -import android.view.InputEvent; import android.view.KeyCharacterMap; import android.view.KeyEvent; import android.view.MotionEvent; @@ -31,6 +30,28 @@ import java.util.concurrent.atomic.AtomicReference; public class Controller implements AsyncProcessor, VirtualDisplayListener { + /* + * For event injection, there are two display ids: + * - the displayId passed to the constructor (which comes from --display-id passed by the client, 0 for the main display); + * - the virtualDisplayId used for mirroring, notified by the capture instance via the VirtualDisplayListener interface. + * + * (In case the ScreenCapture uses the "SurfaceControl API", then both ids are equals, but this is an implementation detail.) + * + * In order to make events work correctly in all cases: + * - virtualDisplayId must be used for events relative to the display (mouse and touch events with coordinates); + * - displayId must be used for other events (like key events). + */ + + private static final class DisplayData { + private final int virtualDisplayId; + private final PositionMapper positionMapper; + + private DisplayData(int virtualDisplayId, PositionMapper positionMapper) { + this.virtualDisplayId = virtualDisplayId; + this.positionMapper = positionMapper; + } + } + private static final int DEFAULT_DEVICE_ID = 0; // control_msg.h values of the pointerId field in inject_touch_event message @@ -54,7 +75,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { private final AtomicBoolean isSettingClipboard = new AtomicBoolean(); - private final AtomicReference positionMapper = new AtomicReference<>(); + private final AtomicReference displayData = new AtomicReference<>(); private long lastTouchDown; private final PointersState pointersState = new PointersState(); @@ -102,8 +123,9 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { } @Override - public void onNewVirtualDisplay(PositionMapper positionMapper) { - this.positionMapper.set(positionMapper); + public void onNewVirtualDisplay(int virtualDisplayId, PositionMapper positionMapper) { + DisplayData data = new DisplayData(virtualDisplayId, positionMapper); + this.displayData.set(data); } private UhidManager getUhidManager() { @@ -284,7 +306,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { return false; } for (KeyEvent event : events) { - if (!injectEvent(event, Device.INJECT_MODE_ASYNC)) { + if (!Device.injectEvent(event, displayId, Device.INJECT_MODE_ASYNC)) { return false; } } @@ -306,7 +328,12 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { private boolean injectTouch(int action, long pointerId, Position position, float pressure, int actionButton, int buttons) { long now = SystemClock.uptimeMillis(); - Point point = getPhysicalPoint(position); + // it hides the field on purpose, to read it with atomic access + @SuppressWarnings("checkstyle:HiddenField") + DisplayData displayData = this.displayData.get(); + assert displayData != null : "Cannot receive a touch event without a display"; + + Point point = displayData.positionMapper.map(position); if (point == null) { Ln.w("Ignore touch event, it was generated for a different device size"); return false; @@ -365,7 +392,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { // First button pressed: ACTION_DOWN MotionEvent downEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_DOWN, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0); - if (!injectEvent(downEvent, Device.INJECT_MODE_ASYNC)) { + if (!Device.injectEvent(downEvent, displayData.virtualDisplayId, Device.INJECT_MODE_ASYNC)) { return false; } } @@ -376,7 +403,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { if (!InputManager.setActionButton(pressEvent, actionButton)) { return false; } - if (!injectEvent(pressEvent, Device.INJECT_MODE_ASYNC)) { + if (!Device.injectEvent(pressEvent, displayData.virtualDisplayId, Device.INJECT_MODE_ASYNC)) { return false; } @@ -390,7 +417,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { if (!InputManager.setActionButton(releaseEvent, actionButton)) { return false; } - if (!injectEvent(releaseEvent, Device.INJECT_MODE_ASYNC)) { + if (!Device.injectEvent(releaseEvent, displayData.virtualDisplayId, Device.INJECT_MODE_ASYNC)) { return false; } @@ -398,7 +425,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { // Last button released: ACTION_UP MotionEvent upEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_UP, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0); - if (!injectEvent(upEvent, Device.INJECT_MODE_ASYNC)) { + if (!Device.injectEvent(upEvent, displayData.virtualDisplayId, Device.INJECT_MODE_ASYNC)) { return false; } } @@ -409,12 +436,18 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { MotionEvent event = MotionEvent.obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0); - return injectEvent(event, Device.INJECT_MODE_ASYNC); + return Device.injectEvent(event, displayData.virtualDisplayId, Device.INJECT_MODE_ASYNC); } private boolean injectScroll(Position position, float hScroll, float vScroll, int buttons) { long now = SystemClock.uptimeMillis(); - Point point = getPhysicalPoint(position); + + // it hides the field on purpose, to read it with atomic access + @SuppressWarnings("checkstyle:HiddenField") + DisplayData displayData = this.displayData.get(); + assert displayData != null : "Cannot receive a scroll event without a display"; + + Point point = displayData.positionMapper.map(position); if (point == null) { Ln.w("Ignore scroll event, it was generated for a different device size"); return false; @@ -431,18 +464,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { MotionEvent event = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, InputDevice.SOURCE_MOUSE, 0); - return injectEvent(event, Device.INJECT_MODE_ASYNC); - } - - private Point getPhysicalPoint(Position position) { - // it hides the field on purpose, to read it with atomic access - @SuppressWarnings("checkstyle:HiddenField") - PositionMapper positionMapper = this.positionMapper.get(); - if (positionMapper == null) { - return null; - } - - return positionMapper.map(position); + return Device.injectEvent(event, displayData.virtualDisplayId, Device.INJECT_MODE_ASYNC); } /** @@ -520,10 +542,6 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { ServiceManager.getActivityManager().startActivity(intent); } - private boolean injectEvent(InputEvent event, int injectMode) { - return Device.injectEvent(event, displayId, injectMode); - } - private boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState, int injectMode) { return Device.injectKeyEvent(action, keyCode, repeat, metaState, displayId, injectMode); } diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java index 95faaf39..7e516909 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java @@ -106,10 +106,16 @@ public class ScreenCapture extends SurfaceCapture { virtualDisplay = null; } + int virtualDisplayId; + PositionMapper positionMapper; try { Size videoSize = screenInfo.getVideoSize(); virtualDisplay = ServiceManager.getDisplayManager() .createVirtualDisplay("scrcpy", videoSize.getWidth(), videoSize.getHeight(), displayId, surface); + virtualDisplayId = virtualDisplay.getDisplay().getDisplayId(); + Rect contentRect = new Rect(0, 0, videoSize.getWidth(), videoSize.getHeight()); + // The position are relative to the virtual display, not the original display + positionMapper = new PositionMapper(videoSize, contentRect, 0); Ln.d("Display: using DisplayManager API"); } catch (Exception displayManagerException) { try { @@ -123,6 +129,8 @@ public class ScreenCapture extends SurfaceCapture { int layerStack = displayInfo.getLayerStack(); setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack); + virtualDisplayId = displayId; + positionMapper = PositionMapper.from(screenInfo); Ln.d("Display: using SurfaceControl API"); } catch (Exception surfaceControlException) { Ln.e("Could not create display using DisplayManager", displayManagerException); @@ -132,8 +140,7 @@ public class ScreenCapture extends SurfaceCapture { } if (vdListener != null) { - PositionMapper positionMapper = PositionMapper.from(screenInfo); - vdListener.onNewVirtualDisplay(positionMapper); + vdListener.onNewVirtualDisplay(virtualDisplayId, positionMapper); } } diff --git a/server/src/main/java/com/genymobile/scrcpy/video/VirtualDisplayListener.java b/server/src/main/java/com/genymobile/scrcpy/video/VirtualDisplayListener.java index d978361e..c079265e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/VirtualDisplayListener.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/VirtualDisplayListener.java @@ -3,5 +3,5 @@ package com.genymobile.scrcpy.video; import com.genymobile.scrcpy.control.PositionMapper; public interface VirtualDisplayListener { - void onNewVirtualDisplay(PositionMapper positionMapper); + void onNewVirtualDisplay(int displayId, PositionMapper positionMapper); } From 5d0e012a4c198633df8c578e626e2160d148d3d6 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 12 Oct 2024 09:23:31 +0200 Subject: [PATCH 2015/2244] Add DPI to DisplayInfo It will be useful to automatically set an appropriate DPI for new virtual displays. PR #5370 --- .../java/com/genymobile/scrcpy/device/DisplayInfo.java | 8 +++++++- .../com/genymobile/scrcpy/wrappers/DisplayManager.java | 10 ++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/device/DisplayInfo.java b/server/src/main/java/com/genymobile/scrcpy/device/DisplayInfo.java index 2973710d..cdd4bab9 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/DisplayInfo.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/DisplayInfo.java @@ -6,15 +6,17 @@ public final class DisplayInfo { private final int rotation; private final int layerStack; private final int flags; + private final int dpi; public static final int FLAG_SUPPORTS_PROTECTED_BUFFERS = 0x00000001; - public DisplayInfo(int displayId, Size size, int rotation, int layerStack, int flags) { + public DisplayInfo(int displayId, Size size, int rotation, int layerStack, int flags, int dpi) { this.displayId = displayId; this.size = size; this.rotation = rotation; this.layerStack = layerStack; this.flags = flags; + this.dpi = dpi; } public int getDisplayId() { @@ -36,5 +38,9 @@ public final class DisplayInfo { public int getFlags() { return flags; } + + public int getDpi() { + return dpi; + } } 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 00a39274..b91b7146 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java @@ -39,7 +39,7 @@ public final class DisplayManager { public static DisplayInfo parseDisplayInfo(String dumpsysDisplayOutput, int displayId) { Pattern regex = Pattern.compile( "^ mOverrideDisplayInfo=DisplayInfo\\{\".*?, displayId " + displayId + ".*?(, FLAG_.*)?, real ([0-9]+) x ([0-9]+).*?, " - + "rotation ([0-9]+).*?, layerStack ([0-9]+)", + + "rotation ([0-9]+).*?, density ([0-9]+).*?, layerStack ([0-9]+)", Pattern.MULTILINE); Matcher m = regex.matcher(dumpsysDisplayOutput); if (!m.find()) { @@ -49,9 +49,10 @@ public final class DisplayManager { int width = Integer.parseInt(m.group(2)); int height = Integer.parseInt(m.group(3)); int rotation = Integer.parseInt(m.group(4)); - int layerStack = Integer.parseInt(m.group(5)); + int density = Integer.parseInt(m.group(5)); + int layerStack = Integer.parseInt(m.group(6)); - return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags); + return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags, density); } private static DisplayInfo getDisplayInfoFromDumpsysDisplay(int displayId) { @@ -98,7 +99,8 @@ public final class DisplayManager { int rotation = cls.getDeclaredField("rotation").getInt(displayInfo); 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); + int dpi = cls.getDeclaredField("logicalDensityDpi").getInt(displayInfo); + return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags, dpi); } catch (ReflectiveOperationException e) { throw new AssertionError(e); } From 98ed5eb643553b6886babfd083931bef4fb4a5b8 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 12 Oct 2024 09:23:31 +0200 Subject: [PATCH 2016/2244] Add virtual display feature Add a feature to create a new (separate) virtual display instead of mirroring the device screen: scrcpy --new-display=1920x1080 scrcpy --new-display=1920x1080/420 # force 420 dpi scrcpy --new-display # use the main display size and density scrcpy --new-display -m1920 # scaled to fit a max size of 1920 scrcpy --new-display=/240 # use the main display size and 240 dpi Fixes #1887 PR #5370 Co-authored-by: Simon Chan <1330321+yume-chan@users.noreply.github.com> Co-authored-by: anirudhb --- app/data/bash-completion/scrcpy | 2 + app/data/zsh-completion/_scrcpy | 1 + app/scrcpy.1 | 12 ++ app/src/cli.c | 43 ++++++ app/src/options.c | 1 + app/src/options.h | 1 + app/src/scrcpy.c | 1 + app/src/server.c | 4 + app/src/server.h | 1 + .../java/com/genymobile/scrcpy/CleanUp.java | 6 +- .../java/com/genymobile/scrcpy/Options.java | 42 +++++ .../java/com/genymobile/scrcpy/Server.java | 18 ++- .../genymobile/scrcpy/control/Controller.java | 31 +++- .../com/genymobile/scrcpy/device/Device.java | 8 + .../genymobile/scrcpy/device/NewDisplay.java | 31 ++++ .../com/genymobile/scrcpy/device/Size.java | 4 + .../scrcpy/video/NewDisplayCapture.java | 146 ++++++++++++++++++ .../genymobile/scrcpy/video/ScreenInfo.java | 2 +- .../scrcpy/wrappers/DisplayManager.java | 11 ++ 19 files changed, 353 insertions(+), 12 deletions(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/device/NewDisplay.java create mode 100644 server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index db825ecc..4f40d466 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -46,6 +46,8 @@ _scrcpy() { --mouse-bind= -n --no-control -N --no-playback + --new-display + --new-display= --no-audio --no-audio-playback --no-cleanup diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index fa0fa84f..f65430e0 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -52,6 +52,7 @@ arguments=( '--mouse-bind=[Configure bindings of secondary clicks]' {-n,--no-control}'[Disable device control \(mirror the device in read only\)]' {-N,--no-playback}'[Disable video and audio playback]' + '--new-display=[Create a new display]' '--no-audio[Disable audio forwarding]' '--no-audio-playback[Disable audio playback]' '--no-cleanup[Disable device cleanup actions on exit]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 3fd3eb29..8a0d09aa 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -314,6 +314,18 @@ Disable device control (mirror the device in read\-only). .B \-N, \-\-no\-playback Disable video and audio playback on the computer (equivalent to \fB\-\-no\-video\-playback \-\-no\-audio\-playback\fR). +.TP +\fB\-\-new\-display\fR[=[\fIwidth\fRx\fIheight\fR][/\fIdpi\fR]] +Create a new display with the specified resolution and density. If not provided, they default to the main display dimensions and DPI, and \fB\-\-max\-size\fR is considered. + +Examples: + + \-\-new\-display=1920x1080 + \-\-new\-display=1920x1080/420 + \-\-new\-display # main display size and density + \-\-new\-display -m1920 # scaled to fit a max size of 1920 + \-\-new\-display=/240 # main display size and 240 dpi + .TP .B \-\-no\-audio Disable audio forwarding. diff --git a/app/src/cli.c b/app/src/cli.c index 4fc3c534..88477c00 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -102,6 +102,7 @@ enum { OPT_NO_MOUSE_HOVER, OPT_AUDIO_DUP, OPT_GAMEPAD, + OPT_NEW_DISPLAY, }; struct sc_option { @@ -557,6 +558,21 @@ static const struct sc_option options[] = { .text = "Disable video and audio playback on the computer (equivalent " "to --no-video-playback --no-audio-playback).", }, + { + .longopt_id = OPT_NEW_DISPLAY, + .longopt = "new-display", + .argdesc = "[x][/]", + .optional_arg = true, + .text = "Create a new display with the specified resolution and " + "density. If not provided, they default to the main display " + "dimensions and DPI, and --max-size is considered.\n" + "Examples:\n" + " --new-display=1920x1080\n" + " --new-display=1920x1080/420 # force 420 dpi\n" + " --new-display # main display size and density\n" + " --new-display -m1920 # scaled to fit a max size of 1920\n" + " --new-display=/240 # main display size and 240 dpi", + }, { .longopt_id = OPT_NO_AUDIO, .longopt = "no-audio", @@ -2668,6 +2684,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; } break; + case OPT_NEW_DISPLAY: + opts->new_display = optarg ? optarg : ""; + break; default: // getopt prints the error message on stderr return false; @@ -2848,6 +2867,25 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } } + if (opts->new_display) { + if (opts->video_source != SC_VIDEO_SOURCE_DISPLAY) { + LOGE("--new-display is only available with --video-source=display"); + return false; + } + + if (!opts->video) { + LOGE("--new-display is incompatible with --no-video"); + return false; + } + + if (opts->max_size && opts->new_display[0] != '\0' + && opts->new_display[0] != '/') { + // An explicit size is defined (not "" nor "/") + LOGE("Cannot specify both --new-display size and -m/--max-size"); + return false; + } + } + if (otg) { if (!opts->control) { LOGE("--no-control is not allowed in OTG mode"); @@ -2954,6 +2992,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; } + if (opts->display_id != 0 && opts->new_display) { + LOGE("Cannot specify both --display-id and --new-display"); + return false; + } + if (opts->audio && opts->audio_source == SC_AUDIO_SOURCE_AUTO) { // Select the audio source according to the video source if (opts->video_source == SC_VIDEO_SOURCE_DISPLAY) { diff --git a/app/src/options.c b/app/src/options.c index f8448792..62fcd925 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -103,6 +103,7 @@ const struct scrcpy_options scrcpy_options_default = { .window = true, .mouse_hover = true, .audio_dup = false, + .new_display = NULL, }; enum sc_orientation diff --git a/app/src/options.h b/app/src/options.h index 5f6726e0..f3d27a88 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -308,6 +308,7 @@ struct scrcpy_options { bool window; bool mouse_hover; bool audio_dup; + const char *new_display; // [x][/] parsed by the server }; extern const struct scrcpy_options scrcpy_options_default; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 854657fb..502498ad 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -431,6 +431,7 @@ scrcpy(struct scrcpy_options *options) { .lock_video_orientation = options->lock_video_orientation, .control = options->control, .display_id = options->display_id, + .new_display = options->new_display, .video = options->video, .audio = options->audio, .audio_dup = options->audio_dup, diff --git a/app/src/server.c b/app/src/server.c index b7f3b56d..26725fa0 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -355,6 +355,10 @@ execute_server(struct sc_server *server, // By default, power_on is true ADD_PARAM("power_on=false"); } + if (params->new_display) { + VALIDATE_STRING(params->new_display); + ADD_PARAM("new_display=%s", params->new_display); + } if (params->list & SC_OPTION_LIST_ENCODERS) { ADD_PARAM("list_encoders=true"); } diff --git a/app/src/server.h b/app/src/server.h index d9d42582..4ff5539d 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -48,6 +48,7 @@ struct sc_server_params { int8_t lock_video_orientation; bool control; uint32_t display_id; + const char *new_display; bool video; bool audio; bool audio_dup; diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java index 1b8d4248..a47aae90 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java +++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java @@ -139,8 +139,10 @@ public final class CleanUp { if (Device.isScreenOn()) { if (powerOffScreen) { - Ln.i("Power off screen"); - Device.powerOffScreen(displayId); + if (displayId != Device.DISPLAY_ID_NONE) { + Ln.i("Power off screen"); + Device.powerOffScreen(displayId); + } } else if (restoreNormalPowerMode) { Ln.i("Restoring normal power mode"); Device.setScreenPowerMode(Device.POWER_MODE_NORMAL); diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 9eab1d90..a4b9d28b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -2,6 +2,7 @@ package com.genymobile.scrcpy; import com.genymobile.scrcpy.audio.AudioCodec; import com.genymobile.scrcpy.audio.AudioSource; +import com.genymobile.scrcpy.device.NewDisplay; import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.util.CodecOption; import com.genymobile.scrcpy.util.Ln; @@ -54,6 +55,8 @@ public class Options { private boolean cleanup = true; private boolean powerOn = true; + private NewDisplay newDisplay; + private boolean listEncoders; private boolean listDisplays; private boolean listCameras; @@ -205,6 +208,10 @@ public class Options { return powerOn; } + public NewDisplay getNewDisplay() { + return newDisplay; + } + public boolean getList() { return listEncoders || listDisplays || listCameras || listCameraSizes; } @@ -418,6 +425,9 @@ public class Options { case "camera_high_speed": options.cameraHighSpeed = Boolean.parseBoolean(value); break; + case "new_display": + options.newDisplay = parseNewDisplay(value); + break; case "send_device_meta": options.sendDeviceMeta = Boolean.parseBoolean(value); break; @@ -504,4 +514,36 @@ public class Options { throw new IllegalArgumentException("Invalid float value for " + key + ": \"" + value + "\""); } } + + private static NewDisplay parseNewDisplay(String newDisplay) { + // Possible inputs: + // - "" (empty string) + // - "x/" + // - "x" + // - "/" + if (newDisplay.isEmpty()) { + return new NewDisplay(); + } + + String[] tokens = newDisplay.split("/"); + + Size size; + if (!tokens[0].isEmpty()) { + size = parseSize(tokens[0]); + } else { + size = null; + } + + int dpi; + if (tokens.length >= 2) { + dpi = Integer.parseInt(tokens[1]); + if (dpi <= 0) { + throw new IllegalArgumentException("Invalid non-positive dpi: " + tokens[1]); + } + } else { + dpi = 0; + } + + return new NewDisplay(size, dpi); + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 91e7ce6c..fd854e06 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -12,12 +12,14 @@ import com.genymobile.scrcpy.control.Controller; import com.genymobile.scrcpy.device.ConfigurationException; import com.genymobile.scrcpy.device.DesktopConnection; import com.genymobile.scrcpy.device.Device; +import com.genymobile.scrcpy.device.NewDisplay; import com.genymobile.scrcpy.device.Streamer; import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.LogUtils; import com.genymobile.scrcpy.util.Settings; import com.genymobile.scrcpy.util.SettingsException; import com.genymobile.scrcpy.video.CameraCapture; +import com.genymobile.scrcpy.video.NewDisplayCapture; import com.genymobile.scrcpy.video.ScreenCapture; import com.genymobile.scrcpy.video.SurfaceCapture; import com.genymobile.scrcpy.video.SurfaceEncoder; @@ -128,8 +130,11 @@ public final class Server { CleanUp cleanUp = null; Thread initThread = null; + NewDisplay newDisplay = options.getNewDisplay(); + int displayId = newDisplay == null ? options.getDisplayId() : Device.DISPLAY_ID_NONE; + if (options.getCleanup()) { - cleanUp = CleanUp.configure(options.getDisplayId()); + cleanUp = CleanUp.configure(displayId); initThread = startInitThread(options, cleanUp); } @@ -154,7 +159,7 @@ public final class Server { if (control) { ControlChannel controlChannel = connection.getControlChannel(); - controller = new Controller(options.getDisplayId(), controlChannel, cleanUp, options.getClipboardAutosync(), options.getPowerOn()); + controller = new Controller(displayId, controlChannel, cleanUp, options.getClipboardAutosync(), options.getPowerOn()); asyncProcessors.add(controller); } @@ -184,8 +189,13 @@ public final class Server { options.getSendFrameMeta()); SurfaceCapture surfaceCapture; if (options.getVideoSource() == VideoSource.DISPLAY) { - surfaceCapture = new ScreenCapture(controller, options.getDisplayId(), options.getMaxSize(), options.getCrop(), - options.getLockVideoOrientation()); + if (newDisplay != null) { + surfaceCapture = new NewDisplayCapture(controller, newDisplay, options.getMaxSize()); + } else { + assert displayId != Device.DISPLAY_ID_NONE; + surfaceCapture = new ScreenCapture(controller, displayId, options.getMaxSize(), options.getCrop(), + options.getLockVideoOrientation()); + } } else { surfaceCapture = new CameraCapture(options.getCameraId(), options.getCameraFacing(), options.getCameraSize(), options.getMaxSize(), options.getCameraAspectRatio(), options.getCameraFps(), options.getCameraHighSpeed()); diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index 5175ed5e..1bc4c692 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -40,6 +40,9 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { * In order to make events work correctly in all cases: * - virtualDisplayId must be used for events relative to the display (mouse and touch events with coordinates); * - displayId must be used for other events (like key events). + * + * If a new separate virtual display is created (using --new-display), then displayId == Device.DISPLAY_ID_NONE. In that case, all events are + * sent to the virtual display id. */ private static final class DisplayData { @@ -151,7 +154,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { private void control() throws IOException { // on start, power on the device - if (powerOn && !Device.isScreenOn()) { + if (powerOn && displayId != Device.DISPLAY_ID_NONE && !Device.isScreenOn()) { Device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER, displayId, Device.INJECT_MODE_ASYNC); // dirty hack @@ -270,7 +273,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { } break; case ControlMessage.TYPE_ROTATE_DEVICE: - Device.rotateDevice(displayId); + Device.rotateDevice(getActionDisplayId()); break; case ControlMessage.TYPE_UHID_CREATE: getUhidManager().open(msg.getId(), msg.getText(), msg.getData()); @@ -305,8 +308,10 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { if (events == null) { return false; } + + int actionDisplayId = getActionDisplayId(); for (KeyEvent event : events) { - if (!Device.injectEvent(event, displayId, Device.INJECT_MODE_ASYNC)) { + if (!Device.injectEvent(event, actionDisplayId, Device.INJECT_MODE_ASYNC)) { return false; } } @@ -543,10 +548,26 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { } private boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState, int injectMode) { - return Device.injectKeyEvent(action, keyCode, repeat, metaState, displayId, injectMode); + return Device.injectKeyEvent(action, keyCode, repeat, metaState, getActionDisplayId(), injectMode); } private boolean pressReleaseKeycode(int keyCode, int injectMode) { - return Device.pressReleaseKeycode(keyCode, displayId, injectMode); + return Device.pressReleaseKeycode(keyCode, getActionDisplayId(), injectMode); + } + + private int getActionDisplayId() { + if (displayId != Device.DISPLAY_ID_NONE) { + // Real screen mirrored, use the source display id + return displayId; + } + + // Virtual display created by --new-display, use the virtualDisplayId + DisplayData data = displayData.get(); + if (data == null) { + // If no virtual display id is initialized yet, use the main display id + return 0; + } + + return data.virtualDisplayId; } } diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Device.java b/server/src/main/java/com/genymobile/scrcpy/device/Device.java index 35266d0e..1cf9aae5 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Device.java @@ -19,6 +19,8 @@ import android.view.KeyEvent; public final class Device { + public static final int DISPLAY_ID_NONE = -1; + public static final int POWER_MODE_OFF = SurfaceControl.POWER_MODE_OFF; public static final int POWER_MODE_NORMAL = SurfaceControl.POWER_MODE_NORMAL; @@ -159,6 +161,8 @@ public final class Device { } public static boolean powerOffScreen(int displayId) { + assert displayId != DISPLAY_ID_NONE; + if (!isScreenOn()) { return true; } @@ -169,6 +173,8 @@ public final class Device { * Disable auto-rotation (if enabled), set the screen rotation and re-enable auto-rotation (if it was enabled). */ public static void rotateDevice(int displayId) { + assert displayId != DISPLAY_ID_NONE; + WindowManager wm = ServiceManager.getWindowManager(); boolean accelerometerRotation = !wm.isRotationFrozen(displayId); @@ -187,6 +193,8 @@ public final class Device { } private static int getCurrentRotation(int displayId) { + assert displayId != DISPLAY_ID_NONE; + if (displayId == 0) { return ServiceManager.getWindowManager().getRotation(); } diff --git a/server/src/main/java/com/genymobile/scrcpy/device/NewDisplay.java b/server/src/main/java/com/genymobile/scrcpy/device/NewDisplay.java new file mode 100644 index 00000000..3aa2996a --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/device/NewDisplay.java @@ -0,0 +1,31 @@ +package com.genymobile.scrcpy.device; + +public final class NewDisplay { + private Size size; + private int dpi; + + public NewDisplay() { + // Auto size and dpi + } + + public NewDisplay(Size size, int dpi) { + this.size = size; + this.dpi = dpi; + } + + public Size getSize() { + return size; + } + + public int getDpi() { + return dpi; + } + + public boolean hasExplicitSize() { + return size != null; + } + + public boolean hasExplicitDpi() { + return dpi != 0; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Size.java b/server/src/main/java/com/genymobile/scrcpy/device/Size.java index bc9dce1c..230fd29e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/Size.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Size.java @@ -21,6 +21,10 @@ public final class Size { return height; } + public int getMax() { + return Math.max(width, height); + } + public Size rotate() { return new Size(height, width); } diff --git a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java new file mode 100644 index 00000000..8f507fdf --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java @@ -0,0 +1,146 @@ +package com.genymobile.scrcpy.video; + +import com.genymobile.scrcpy.AndroidVersions; +import com.genymobile.scrcpy.control.PositionMapper; +import com.genymobile.scrcpy.device.DisplayInfo; +import com.genymobile.scrcpy.device.NewDisplay; +import com.genymobile.scrcpy.device.Size; +import com.genymobile.scrcpy.util.Ln; +import com.genymobile.scrcpy.wrappers.ServiceManager; + +import android.graphics.Rect; +import android.hardware.display.DisplayManager; +import android.hardware.display.VirtualDisplay; +import android.os.Build; +import android.view.Surface; + +public class NewDisplayCapture extends SurfaceCapture { + + // Internal fields copied from android.hardware.display.DisplayManager + private static final int VIRTUAL_DISPLAY_FLAG_SUPPORTS_TOUCH = 1 << 6; + private static final int VIRTUAL_DISPLAY_FLAG_ROTATES_WITH_CONTENT = 1 << 7; + private static final int VIRTUAL_DISPLAY_FLAG_DESTROY_CONTENT_ON_REMOVAL = 1 << 8; + private static final int VIRTUAL_DISPLAY_FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS = 1 << 9; + private static final int VIRTUAL_DISPLAY_FLAG_TRUSTED = 1 << 10; + private static final int VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP = 1 << 11; + private static final int VIRTUAL_DISPLAY_FLAG_ALWAYS_UNLOCKED = 1 << 12; + private static final int VIRTUAL_DISPLAY_FLAG_TOUCH_FEEDBACK_DISABLED = 1 << 13; + private static final int VIRTUAL_DISPLAY_FLAG_OWN_FOCUS = 1 << 14; + private static final int VIRTUAL_DISPLAY_FLAG_DEVICE_DISPLAY_GROUP = 1 << 15; + + private final VirtualDisplayListener vdListener; + private final NewDisplay newDisplay; + + private Size mainDisplaySize; + private int mainDisplayDpi; + private int maxSize; // only used if newDisplay.getSize() != null + + private VirtualDisplay virtualDisplay; + private Size size; + private int dpi; + + public NewDisplayCapture(VirtualDisplayListener vdListener, NewDisplay newDisplay, int maxSize) { + this.vdListener = vdListener; + this.newDisplay = newDisplay; + this.maxSize = maxSize; + } + + @Override + public void init() { + size = newDisplay.getSize(); + dpi = newDisplay.getDpi(); + if (size == null || dpi == 0) { + DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(0); + if (displayInfo != null) { + mainDisplaySize = displayInfo.getSize(); + mainDisplayDpi = displayInfo.getDpi(); + } else { + Ln.w("Main display not found, fallback to 1920x1080 240dpi"); + mainDisplaySize = new Size(1920, 1080); + mainDisplayDpi = 240; + } + } + } + + @Override + public void prepare() { + if (!newDisplay.hasExplicitSize()) { + size = ScreenInfo.computeVideoSize(mainDisplaySize.getWidth(), mainDisplaySize.getHeight(), maxSize); + } + if (!newDisplay.hasExplicitDpi()) { + dpi = scaleDpi(mainDisplaySize, mainDisplayDpi, size); + } + } + + @Override + public void start(Surface surface) { + if (virtualDisplay != null) { + virtualDisplay.release(); + virtualDisplay = null; + } + + int virtualDisplayId; + try { + int flags = DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC + | DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY + | VIRTUAL_DISPLAY_FLAG_SUPPORTS_TOUCH + | VIRTUAL_DISPLAY_FLAG_ROTATES_WITH_CONTENT + | VIRTUAL_DISPLAY_FLAG_DESTROY_CONTENT_ON_REMOVAL + | VIRTUAL_DISPLAY_FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS; + if (Build.VERSION.SDK_INT >= AndroidVersions.API_33_ANDROID_13) { + flags |= VIRTUAL_DISPLAY_FLAG_TRUSTED + | VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP + | VIRTUAL_DISPLAY_FLAG_ALWAYS_UNLOCKED + | VIRTUAL_DISPLAY_FLAG_TOUCH_FEEDBACK_DISABLED; + if (Build.VERSION.SDK_INT >= AndroidVersions.API_34_ANDROID_14) { + flags |= VIRTUAL_DISPLAY_FLAG_OWN_FOCUS + | VIRTUAL_DISPLAY_FLAG_DEVICE_DISPLAY_GROUP; + } + } + virtualDisplay = ServiceManager.getDisplayManager() + .createNewVirtualDisplay("scrcpy", size.getWidth(), size.getHeight(), dpi, surface, flags); + virtualDisplayId = virtualDisplay.getDisplay().getDisplayId(); + Ln.i("New display: " + size.getWidth() + "x" + size.getHeight() + "/" + dpi + " (id=" + virtualDisplayId + ")"); + } catch (Exception e) { + Ln.e("Could not create display", e); + throw new AssertionError("Could not create display"); + } + + if (vdListener != null) { + virtualDisplayId = virtualDisplay.getDisplay().getDisplayId(); + Rect contentRect = new Rect(0, 0, size.getWidth(), size.getHeight()); + PositionMapper positionMapper = new PositionMapper(size, contentRect, 0); + vdListener.onNewVirtualDisplay(virtualDisplayId, positionMapper); + } + } + + @Override + public void release() { + if (virtualDisplay != null) { + virtualDisplay.release(); + virtualDisplay = null; + } + } + + @Override + public synchronized Size getSize() { + return size; + } + + @Override + public synchronized boolean setMaxSize(int newMaxSize) { + if (newDisplay.hasExplicitSize()) { + // Cannot retry with a different size if the display size was explicitly provided + return false; + } + + maxSize = newMaxSize; + return true; + } + + private static int scaleDpi(Size initialSize, int initialDpi, Size size) { + int den = initialSize.getMax(); + int num = size.getMax(); + return initialDpi * num / den; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java index 1f74ce34..cc82a654 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java @@ -90,7 +90,7 @@ public final class ScreenInfo { return rect.width() + ":" + rect.height() + ":" + rect.left + ":" + rect.top; } - private static Size computeVideoSize(int w, int h, int maxSize) { + public 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); 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 b91b7146..c8c405bb 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java @@ -1,15 +1,18 @@ package com.genymobile.scrcpy.wrappers; +import com.genymobile.scrcpy.FakeContext; import com.genymobile.scrcpy.device.DisplayInfo; import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.util.Command; import com.genymobile.scrcpy.util.Ln; import android.annotation.SuppressLint; +import android.content.Context; import android.hardware.display.VirtualDisplay; import android.view.Display; import android.view.Surface; +import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.regex.Matcher; @@ -126,4 +129,12 @@ public final class DisplayManager { Method method = getCreateVirtualDisplayMethod(); return (VirtualDisplay) method.invoke(null, name, width, height, displayIdToMirror, surface); } + + public VirtualDisplay createNewVirtualDisplay(String name, int width, int height, int dpi, Surface surface, int flags) throws Exception { + Constructor ctor = android.hardware.display.DisplayManager.class.getDeclaredConstructor( + Context.class); + ctor.setAccessible(true); + android.hardware.display.DisplayManager dm = ctor.newInstance(FakeContext.get()); + return dm.createVirtualDisplay(name, width, height, dpi, surface, flags); + } } From 408a388fc5115483248c3444062d52565fd1f906 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 12 Oct 2024 09:23:31 +0200 Subject: [PATCH 2017/2244] Reject --new-display for Android <= 10 Fail explicitly if a new virtual display is requested on an Android version lower than 10. PR #5370 --- server/src/main/java/com/genymobile/scrcpy/Server.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index fd854e06..68f3c4ee 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -127,6 +127,11 @@ public final class Server { throw new ConfigurationException("Camera mirroring is not supported"); } + if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10 && options.getNewDisplay() != null) { + Ln.e("New virtual display is not supported before Android 10"); + throw new ConfigurationException("New virtual display is not supported"); + } + CleanUp cleanUp = null; Thread initThread = null; From 9c9d92fb1c75dcf7631ef1408dfdce7a01c29db4 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 19 Oct 2024 17:16:08 +0200 Subject: [PATCH 2018/2244] Add --list-apps Add an option to list all apps installed on the device: scrcpy --list-apps PR #5370 --- app/data/bash-completion/scrcpy | 1 + app/data/zsh-completion/_scrcpy | 1 + app/scrcpy.1 | 4 ++ app/src/cli.c | 9 ++++ app/src/options.h | 1 + app/src/server.c | 3 ++ .../java/com/genymobile/scrcpy/Options.java | 10 +++- .../java/com/genymobile/scrcpy/Server.java | 5 ++ .../com/genymobile/scrcpy/device/Device.java | 41 ++++++++++++++ .../genymobile/scrcpy/device/DeviceApp.java | 26 +++++++++ .../com/genymobile/scrcpy/util/LogUtils.java | 54 +++++++++++++++++++ 11 files changed, 154 insertions(+), 1 deletion(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/device/DeviceApp.java diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index 4f40d466..f37da13a 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -33,6 +33,7 @@ _scrcpy() { --keyboard= --kill-adb-on-close --legacy-paste + --list-apps --list-camera-sizes --list-cameras --list-displays diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index f65430e0..3f25b88d 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -40,6 +40,7 @@ arguments=( '--keyboard=[Set the keyboard input mode]:mode:(disabled sdk uhid aoa)' '--kill-adb-on-close[Kill adb when scrcpy terminates]' '--legacy-paste[Inject computer clipboard text as a sequence of key events on Ctrl+v]' + '--list-apps[List Android apps installed on the device]' '--list-camera-sizes[List the valid camera capture sizes]' '--list-cameras[List cameras available on the device]' '--list-displays[List displays available on the device]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 8a0d09aa..258c125d 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -227,6 +227,10 @@ Inject computer clipboard text as a sequence of key events on Ctrl+v (like MOD+S This is a workaround for some devices not behaving as expected when setting the device clipboard programmatically. +.TP +.B \-\-list\-apps +List Android apps installed on the device. + .TP .B \-\-list\-camera\-sizes List the valid camera capture sizes. diff --git a/app/src/cli.c b/app/src/cli.c index 88477c00..ac58364e 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -103,6 +103,7 @@ enum { OPT_AUDIO_DUP, OPT_GAMEPAD, OPT_NEW_DISPLAY, + OPT_LIST_APPS, }; struct sc_option { @@ -443,6 +444,11 @@ static const struct sc_option options[] = { "This is a workaround for some devices not behaving as " "expected when setting the device clipboard programmatically.", }, + { + .longopt_id = OPT_LIST_APPS, + .longopt = "list-apps", + .text = "List Android apps installed on the device.", + }, { .longopt_id = OPT_LIST_CAMERAS, .longopt = "list-cameras", @@ -2611,6 +2617,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], case OPT_LIST_CAMERA_SIZES: opts->list |= SC_OPTION_LIST_CAMERA_SIZES; break; + case OPT_LIST_APPS: + opts->list |= SC_OPTION_LIST_APPS; + break; case OPT_REQUIRE_AUDIO: opts->require_audio = true; break; diff --git a/app/src/options.h b/app/src/options.h index f3d27a88..7cbe2e5b 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -304,6 +304,7 @@ struct scrcpy_options { #define SC_OPTION_LIST_DISPLAYS 0x2 #define SC_OPTION_LIST_CAMERAS 0x4 #define SC_OPTION_LIST_CAMERA_SIZES 0x8 +#define SC_OPTION_LIST_APPS 0x10 uint8_t list; bool window; bool mouse_hover; diff --git a/app/src/server.c b/app/src/server.c index 26725fa0..167582e4 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -371,6 +371,9 @@ execute_server(struct sc_server *server, if (params->list & SC_OPTION_LIST_CAMERA_SIZES) { ADD_PARAM("list_camera_sizes=true"); } + if (params->list & SC_OPTION_LIST_APPS) { + ADD_PARAM("list_apps=true"); + } #undef ADD_PARAM diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index a4b9d28b..65702b42 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -61,6 +61,7 @@ public class Options { private boolean listDisplays; private boolean listCameras; private boolean listCameraSizes; + private boolean listApps; // Options not used by the scrcpy client, but useful to use scrcpy-server directly private boolean sendDeviceMeta = true; // send device name and size @@ -213,7 +214,7 @@ public class Options { } public boolean getList() { - return listEncoders || listDisplays || listCameras || listCameraSizes; + return listEncoders || listDisplays || listCameras || listCameraSizes || listApps; } public boolean getListEncoders() { @@ -232,6 +233,10 @@ public class Options { return listCameraSizes; } + public boolean getListApps() { + return listApps; + } + public boolean getSendDeviceMeta() { return sendDeviceMeta; } @@ -395,6 +400,9 @@ public class Options { case "list_camera_sizes": options.listCameraSizes = Boolean.parseBoolean(value); break; + case "list_apps": + options.listApps = Boolean.parseBoolean(value); + break; case "camera_id": if (!value.isEmpty()) { options.cameraId = value; diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 68f3c4ee..35d317f2 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -292,6 +292,11 @@ public final class Server { Workarounds.apply(); Ln.i(LogUtils.buildCameraListMessage(options.getListCameraSizes())); } + if (options.getListApps()) { + Workarounds.apply(); + Ln.i("Processing Android apps... (this may take some time)"); + Ln.i(LogUtils.buildAppListMessage()); + } // Just print the requested data, do not mirror return; } diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Device.java b/server/src/main/java/com/genymobile/scrcpy/device/Device.java index 1cf9aae5..f7931141 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Device.java @@ -1,6 +1,7 @@ package com.genymobile.scrcpy.device; import com.genymobile.scrcpy.AndroidVersions; +import com.genymobile.scrcpy.FakeContext; import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.wrappers.ClipboardManager; import com.genymobile.scrcpy.wrappers.DisplayControl; @@ -9,6 +10,10 @@ import com.genymobile.scrcpy.wrappers.ServiceManager; import com.genymobile.scrcpy.wrappers.SurfaceControl; import com.genymobile.scrcpy.wrappers.WindowManager; +import android.annotation.SuppressLint; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; import android.os.Build; import android.os.IBinder; import android.os.SystemClock; @@ -17,6 +22,9 @@ import android.view.InputEvent; import android.view.KeyCharacterMap; import android.view.KeyEvent; +import java.util.ArrayList; +import java.util.List; + public final class Device { public static final int DISPLAY_ID_NONE = -1; @@ -202,4 +210,37 @@ public final class Device { DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(displayId); return displayInfo.getRotation(); } + + public static List listApps() { + List apps = new ArrayList<>(); + PackageManager pm = FakeContext.get().getPackageManager(); + for (ApplicationInfo appInfo : getLaunchableApps(pm)) { + String name = pm.getApplicationLabel(appInfo).toString(); + boolean system = (appInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0; + apps.add(new DeviceApp(appInfo.packageName, name, system)); + } + + return apps; + } + + @SuppressLint("QueryPermissionsNeeded") + private static List getLaunchableApps(PackageManager pm) { + List result = new ArrayList<>(); + for (ApplicationInfo appInfo : pm.getInstalledApplications(PackageManager.GET_META_DATA)) { + if (appInfo.enabled && getLaunchIntent(pm, appInfo.packageName) != null) { + result.add(appInfo); + } + } + + return result; + } + + public static Intent getLaunchIntent(PackageManager pm, String packageName) { + Intent launchIntent = pm.getLaunchIntentForPackage(packageName); + if (launchIntent != null) { + return launchIntent; + } + + return pm.getLeanbackLaunchIntentForPackage(packageName); + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/device/DeviceApp.java b/server/src/main/java/com/genymobile/scrcpy/device/DeviceApp.java new file mode 100644 index 00000000..ed292efa --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/device/DeviceApp.java @@ -0,0 +1,26 @@ +package com.genymobile.scrcpy.device; + +public final class DeviceApp { + + private final String packageName; + private final String name; + private final boolean system; + + public DeviceApp(String packageName, String name, boolean system) { + this.packageName = packageName; + this.name = name; + this.system = system; + } + + public String getPackageName() { + return packageName; + } + + public String getName() { + return name; + } + + public boolean isSystem() { + return system; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java b/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java index 45ab4eba..e25f140c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java @@ -1,10 +1,13 @@ package com.genymobile.scrcpy.util; +import com.genymobile.scrcpy.device.Device; +import com.genymobile.scrcpy.device.DeviceApp; import com.genymobile.scrcpy.device.DisplayInfo; import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.wrappers.DisplayManager; import com.genymobile.scrcpy.wrappers.ServiceManager; +import android.annotation.SuppressLint; import android.graphics.Rect; import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraCharacteristics; @@ -13,7 +16,9 @@ import android.hardware.camera2.params.StreamConfigurationMap; import android.media.MediaCodec; import android.util.Range; +import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.SortedSet; import java.util.TreeSet; @@ -154,4 +159,53 @@ public final class LogUtils { } return set; } + + @SuppressLint("QueryPermissionsNeeded") + public static String buildAppListMessage() { + StringBuilder builder = new StringBuilder("List of apps:"); + + List apps = Device.listApps(); + + // Sort by: + // 1. system flag (system apps are before non-system apps) + // 2. name + // 3. package name + // Comparator.comparing() was introduced in API 24, so it cannot be used here to simplify the code + Collections.sort(apps, (thisApp, otherApp) -> { + // System apps first + int cmp = -Boolean.compare(thisApp.isSystem(), otherApp.isSystem()); + if (cmp != 0) { + return cmp; + } + + cmp = Objects.compare(thisApp.getName(), otherApp.getName(), String::compareTo); + if (cmp != 0) { + return cmp; + } + + return Objects.compare(thisApp.getPackageName(), otherApp.getPackageName(), String::compareTo); + }); + + final int column = 30; + for (DeviceApp app : apps) { + String name = app.getName(); + int padding = column - name.length(); + builder.append("\n "); + if (app.isSystem()) { + builder.append("* "); + } else { + builder.append("- "); + + } + builder.append(name); + if (padding > 0) { + builder.append(String.format("%" + padding + "s", " ")); + } else { + builder.append("\n ").append(String.format("%" + column + "s", " ")); + } + builder.append(" [").append(app.getPackageName()).append(']'); + } + + return builder.toString(); + } } From 13ce277e1f1eec6312350c5a3a3ac3be7b9be6e1 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 19 Oct 2024 18:19:10 +0200 Subject: [PATCH 2019/2244] Add --start-app Add a command line option --start-app=name to start an Android app by its package name. For example: scrcpy --start-app=org.mozilla.firefox The app will be started on the correct target display: scrcpy --new-display=1920x1080 --start-app=org.videolan.vlc PR #5370 Co-authored-by: Simon Chan <1330321+yume-chan@users.noreply.github.com> --- app/data/bash-completion/scrcpy | 1 + app/data/zsh-completion/_scrcpy | 1 + app/scrcpy.1 | 4 + app/src/cli.c | 14 ++++ app/src/control_msg.c | 10 +++ app/src/control_msg.h | 4 + app/src/options.c | 1 + app/src/options.h | 1 + app/src/scrcpy.c | 19 +++++ .../scrcpy/control/ControlMessage.java | 8 ++ .../scrcpy/control/ControlMessageReader.java | 7 ++ .../genymobile/scrcpy/control/Controller.java | 79 ++++++++++++++++++- .../com/genymobile/scrcpy/device/Device.java | 48 ++++++++++- .../com/genymobile/scrcpy/util/LogUtils.java | 10 ++- .../scrcpy/wrappers/ActivityManager.java | 8 +- .../control/ControlMessageReaderTest.java | 21 +++++ 16 files changed, 227 insertions(+), 9 deletions(-) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index f37da13a..223c5264 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -79,6 +79,7 @@ _scrcpy() { -s --serial= -S --turn-screen-off --shortcut-mod= + --start-app= -t --show-touches --tcpip --tcpip= diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 3f25b88d..8d1189c0 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -82,6 +82,7 @@ arguments=( {-s,--serial=}'[The device serial number \(mandatory for multiple devices only\)]:serial:($("${ADB-adb}" devices | awk '\''$2 == "device" {print $1}'\''))' {-S,--turn-screen-off}'[Turn the device screen off immediately]' '--shortcut-mod=[\[key1,key2+key3,...\] Specify the modifiers to use for scrcpy shortcuts]:shortcut mod:(lctrl rctrl lalt ralt lsuper rsuper)' + '--start-app=[Start an Android app]' {-t,--show-touches}'[Show physical touches]' '--tcpip[\(optional \[ip\:port\]\) Configure and connect the device over TCP/IP]' '--time-limit=[Set the maximum mirroring time, in seconds]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 258c125d..35abd0d1 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -494,6 +494,10 @@ For example, to use either LCtrl or LSuper for scrcpy shortcuts, pass "lctrl,lsu Default is "lalt,lsuper" (left-Alt or left-Super). +.TP +.BI "\-\-start\-app " name +Start an Android app, by its exact package name. + .TP .B \-t, \-\-show\-touches Enable "show touches" on start, restore the initial value on exit. diff --git a/app/src/cli.c b/app/src/cli.c index ac58364e..ba272393 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -104,6 +104,7 @@ enum { OPT_GAMEPAD, OPT_NEW_DISPLAY, OPT_LIST_APPS, + OPT_START_APP, }; struct sc_option { @@ -806,6 +807,12 @@ static const struct sc_option options[] = { "shortcuts, pass \"lctrl,lsuper\".\n" "Default is \"lalt,lsuper\" (left-Alt or left-Super).", }, + { + .longopt_id = OPT_START_APP, + .longopt = "start-app", + .argdesc = "name", + .text = "Start an Android app, by its exact package name.", + }, { .shortopt = 't', .longopt = "show-touches", @@ -2696,6 +2703,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], case OPT_NEW_DISPLAY: opts->new_display = optarg ? optarg : ""; break; + case OPT_START_APP: + opts->start_app = optarg; + break; default: // getopt prints the error message on stderr return false; @@ -3138,6 +3148,10 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], LOGE("Cannot request power off on close if control is disabled"); return false; } + if (opts->start_app) { + LOGE("Cannot start an Android app if control is disabled"); + return false; + } } # ifdef _WIN32 diff --git a/app/src/control_msg.c b/app/src/control_msg.c index d599b62d..a71bf445 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -183,6 +183,10 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) { case SC_CONTROL_MSG_TYPE_UHID_DESTROY: sc_write16be(&buf[1], msg->uhid_destroy.id); return 3; + case SC_CONTROL_MSG_TYPE_START_APP: { + size_t len = write_string_tiny(&buf[1], msg->start_app.name, 255); + return 1 + len; + } case SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL: case SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL: case SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS: @@ -308,6 +312,9 @@ sc_control_msg_log(const struct sc_control_msg *msg) { case SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS: LOG_CMSG("open hard keyboard settings"); break; + case SC_CONTROL_MSG_TYPE_START_APP: + LOG_CMSG("start app \"%s\"", msg->start_app.name); + break; default: LOG_CMSG("unknown type: %u", (unsigned) msg->type); break; @@ -333,6 +340,9 @@ sc_control_msg_destroy(struct sc_control_msg *msg) { case SC_CONTROL_MSG_TYPE_SET_CLIPBOARD: free(msg->set_clipboard.text); break; + case SC_CONTROL_MSG_TYPE_START_APP: + free(msg->start_app.name); + break; default: // do nothing break; diff --git a/app/src/control_msg.h b/app/src/control_msg.h index 1ae8cae4..a809a154 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -41,6 +41,7 @@ enum sc_control_msg_type { SC_CONTROL_MSG_TYPE_UHID_INPUT, SC_CONTROL_MSG_TYPE_UHID_DESTROY, SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS, + SC_CONTROL_MSG_TYPE_START_APP, }; enum sc_screen_power_mode { @@ -110,6 +111,9 @@ struct sc_control_msg { struct { uint16_t id; } uhid_destroy; + struct { + char *name; + } start_app; }; }; diff --git a/app/src/options.c b/app/src/options.c index 62fcd925..8106ce3d 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -104,6 +104,7 @@ const struct scrcpy_options scrcpy_options_default = { .mouse_hover = true, .audio_dup = false, .new_display = NULL, + .start_app = NULL, }; enum sc_orientation diff --git a/app/src/options.h b/app/src/options.h index 7cbe2e5b..ec5e71ea 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -310,6 +310,7 @@ struct scrcpy_options { bool mouse_hover; bool audio_dup; const char *new_display; // [x][/] parsed by the server + const char *start_app; }; extern const struct scrcpy_options scrcpy_options_default; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 502498ad..64a2fa10 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -907,6 +907,25 @@ aoa_complete: init_sdl_gamepads(); } + if (options->control && options->start_app) { + assert(controller); + + char *name = strdup(options->start_app); + if (!name) { + LOG_OOM(); + goto end; + } + + struct sc_control_msg msg; + msg.type = SC_CONTROL_MSG_TYPE_START_APP; + msg.start_app.name = name; + + if (!sc_controller_push_msg(controller, &msg)) { + LOGW("Could not request start app '%s'", name); + free(name); + } + } + ret = event_loop(s); terminate_event_loop(); LOGD("quit..."); diff --git a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java index d1406ed0..36dbd03a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java @@ -23,6 +23,7 @@ public final class ControlMessage { public static final int TYPE_UHID_INPUT = 13; public static final int TYPE_UHID_DESTROY = 14; public static final int TYPE_OPEN_HARD_KEYBOARD_SETTINGS = 15; + public static final int TYPE_START_APP = 16; public static final long SEQUENCE_INVALID = 0; @@ -155,6 +156,13 @@ public final class ControlMessage { return msg; } + public static ControlMessage createStartApp(String name) { + ControlMessage msg = new ControlMessage(); + msg.type = TYPE_START_APP; + msg.text = name; + return msg; + } + public int getType() { return type; } diff --git a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java index 17e121c2..eb5dc787 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java @@ -53,6 +53,8 @@ public class ControlMessageReader { return parseUhidInput(); case ControlMessage.TYPE_UHID_DESTROY: return parseUhidDestroy(); + case ControlMessage.TYPE_START_APP: + return parseStartApp(); default: throw new ControlProtocolException("Unknown event type: " + type); } @@ -155,6 +157,11 @@ public class ControlMessageReader { return ControlMessage.createUhidDestroy(id); } + private ControlMessage parseStartApp() throws IOException { + String name = parseString(1); + return ControlMessage.createStartApp(name); + } + private Position parsePosition() throws IOException { int x = dis.readInt(); int y = dis.readInt(); diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index 1bc4c692..ccdb85e1 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -4,6 +4,7 @@ import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.AsyncProcessor; import com.genymobile.scrcpy.CleanUp; import com.genymobile.scrcpy.device.Device; +import com.genymobile.scrcpy.device.DeviceApp; import com.genymobile.scrcpy.device.Point; import com.genymobile.scrcpy.device.Position; import com.genymobile.scrcpy.util.Ln; @@ -22,6 +23,7 @@ import android.view.KeyEvent; import android.view.MotionEvent; import java.io.IOException; +import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; @@ -61,6 +63,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { private static final int POINTER_ID_MOUSE = -1; private static final ScheduledExecutorService EXECUTOR = Executors.newSingleThreadScheduledExecutor(); + private ExecutorService startAppExecutor; private Thread thread; @@ -79,6 +82,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { private final AtomicBoolean isSettingClipboard = new AtomicBoolean(); private final AtomicReference displayData = new AtomicReference<>(); + private final Object displayDataAvailable = new Object(); // condition variable private long lastTouchDown; private final PointersState pointersState = new PointersState(); @@ -128,7 +132,13 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { @Override public void onNewVirtualDisplay(int virtualDisplayId, PositionMapper positionMapper) { DisplayData data = new DisplayData(virtualDisplayId, positionMapper); - this.displayData.set(data); + DisplayData old = this.displayData.getAndSet(data); + if (old == null) { + // The very first time the Controller is notified of a new virtual display + synchronized (displayDataAvailable) { + displayDataAvailable.notify(); + } + } } private UhidManager getUhidManager() { @@ -287,6 +297,9 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { case ControlMessage.TYPE_OPEN_HARD_KEYBOARD_SETTINGS: openHardKeyboardSettings(); break; + case ControlMessage.TYPE_START_APP: + startAppAsync(msg.getText()); + break; default: // do nothing } @@ -570,4 +583,68 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { return data.virtualDisplayId; } + + private void startAppAsync(String name) { + if (startAppExecutor == null) { + startAppExecutor = Executors.newSingleThreadExecutor(); + } + + // Listing and selecting the app may take a lot of time + startAppExecutor.submit(() -> startApp(name)); + } + + private void startApp(String name) { + DeviceApp app = Device.findByPackageName(name); + if (app == null) { + Ln.w("No app found for package \"" + name + "\""); + return; + } + + int startAppDisplayId = getStartAppDisplayId(); + if (startAppDisplayId == Device.DISPLAY_ID_NONE) { + Ln.e("No known display id to start app \"" + name + "\""); + return; + } + + Ln.i("Starting app \"" + app.getName() + "\" [" + app.getPackageName() + "] on display " + startAppDisplayId + "..."); + Device.startApp(app.getPackageName(), startAppDisplayId); + } + + private int getStartAppDisplayId() { + if (displayId != Device.DISPLAY_ID_NONE) { + return displayId; + } + + // Mirroring a new virtual display id (using --new-display-id feature) + try { + // Wait for at most 1 second until a virtual display id is known + DisplayData data = waitDisplayData(1000); + if (data != null) { + return data.virtualDisplayId; + } + } catch (InterruptedException e) { + // do nothing + } + + // No display id available + return Device.DISPLAY_ID_NONE; + } + + private DisplayData waitDisplayData(long timeoutMillis) throws InterruptedException { + long deadline = System.currentTimeMillis() + timeoutMillis; + + synchronized (displayDataAvailable) { + DisplayData data = displayData.get(); + while (data == null) { + long timeout = deadline - System.currentTimeMillis(); + if (timeout < 0) { + return null; + } + displayDataAvailable.wait(timeout); + data = displayData.get(); + } + + return data; + } + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Device.java b/server/src/main/java/com/genymobile/scrcpy/device/Device.java index f7931141..496865e4 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Device.java @@ -3,6 +3,7 @@ package com.genymobile.scrcpy.device; import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.FakeContext; import com.genymobile.scrcpy.util.Ln; +import com.genymobile.scrcpy.wrappers.ActivityManager; import com.genymobile.scrcpy.wrappers.ClipboardManager; import com.genymobile.scrcpy.wrappers.DisplayControl; import com.genymobile.scrcpy.wrappers.InputManager; @@ -12,9 +13,11 @@ import com.genymobile.scrcpy.wrappers.WindowManager; import android.annotation.SuppressLint; import android.content.Intent; +import android.app.ActivityOptions; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.os.Build; +import android.os.Bundle; import android.os.IBinder; import android.os.SystemClock; import android.view.InputDevice; @@ -215,9 +218,7 @@ public final class Device { List apps = new ArrayList<>(); PackageManager pm = FakeContext.get().getPackageManager(); for (ApplicationInfo appInfo : getLaunchableApps(pm)) { - String name = pm.getApplicationLabel(appInfo).toString(); - boolean system = (appInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0; - apps.add(new DeviceApp(appInfo.packageName, name, system)); + apps.add(toApp(pm, appInfo)); } return apps; @@ -243,4 +244,45 @@ public final class Device { return pm.getLeanbackLaunchIntentForPackage(packageName); } + + private static DeviceApp toApp(PackageManager pm, ApplicationInfo appInfo) { + String name = pm.getApplicationLabel(appInfo).toString(); + boolean system = (appInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0; + return new DeviceApp(appInfo.packageName, name, system); + } + + @SuppressLint("QueryPermissionsNeeded") + public static DeviceApp findByPackageName(String packageName) { + PackageManager pm = FakeContext.get().getPackageManager(); + // No need to filter by "launchable" apps, an error will be reported on start if the app is not launchable + for (ApplicationInfo appInfo : pm.getInstalledApplications(PackageManager.GET_META_DATA)) { + if (packageName.equals(appInfo.packageName)) { + return toApp(pm, appInfo); + } + } + + return null; + } + + public static void startApp(String packageName, int displayId) { + PackageManager pm = FakeContext.get().getPackageManager(); + + Intent launchIntent = getLaunchIntent(pm, packageName); + if (launchIntent == null) { + Ln.w("Cannot create launch intent for app " + packageName); + return; + } + + launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + + Bundle options = null; + if (Build.VERSION.SDK_INT >= AndroidVersions.API_26_ANDROID_8_0) { + ActivityOptions launchOptions = ActivityOptions.makeBasic(); + launchOptions.setLaunchDisplayId(displayId); + options = launchOptions.toBundle(); + } + + ActivityManager am = ServiceManager.getActivityManager(); + am.startActivity(launchIntent, options); + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java b/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java index e25f140c..6b813135 100644 --- a/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java @@ -160,11 +160,15 @@ public final class LogUtils { return set; } - @SuppressLint("QueryPermissionsNeeded") - public static String buildAppListMessage() { - StringBuilder builder = new StringBuilder("List of apps:"); + public static String buildAppListMessage() { List apps = Device.listApps(); + return buildAppListMessage("List of apps:", apps); + } + + @SuppressLint("QueryPermissionsNeeded") + public static String buildAppListMessage(String title, List apps) { + StringBuilder builder = new StringBuilder(title); // Sort by: // 1. system flag (system apps are before non-system apps) diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java index c907e12f..f052dee0 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java @@ -118,8 +118,12 @@ public final class ActivityManager { return startActivityAsUserMethod; } - @SuppressWarnings("ConstantConditions") public int startActivity(Intent intent) { + return startActivity(intent, null); + } + + @SuppressWarnings("ConstantConditions") + public int startActivity(Intent intent, Bundle options) { try { Method method = getStartActivityAsUserMethod(); return (int) method.invoke( @@ -133,7 +137,7 @@ public final class ActivityManager { /* requestCode */ 0, /* startFlags */ 0, /* profilerInfo */ null, - /* bOptions */ null, + /* bOptions */ options, /* userId */ /* UserHandle.USER_CURRENT */ -2); } catch (Throwable e) { Ln.e("Could not invoke method", e); diff --git a/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java index f29be2f4..d8489fc3 100644 --- a/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java @@ -399,6 +399,27 @@ public class ControlMessageReaderTest { Assert.assertEquals(-1, bis.read()); // EOS } + @Test + public void testParseStartApp() throws IOException { + byte[] name = "firefox".getBytes(StandardCharsets.UTF_8); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(ControlMessage.TYPE_START_APP); + dos.writeByte(name.length); + dos.write(name); + byte[] packet = bos.toByteArray(); + + ByteArrayInputStream bis = new ByteArrayInputStream(packet); + ControlMessageReader reader = new ControlMessageReader(bis); + + ControlMessage event = reader.read(); + Assert.assertEquals(ControlMessage.TYPE_START_APP, event.getType()); + Assert.assertEquals("firefox", event.getText()); + + Assert.assertEquals(-1, bis.read()); // EOS + } + @Test public void testMultiEvents() throws IOException { ByteArrayOutputStream bos = new ByteArrayOutputStream(); From dd20efa41cd7d1f2220e38a45e0d7413d19f5c09 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 19 Oct 2024 19:01:54 +0200 Subject: [PATCH 2020/2244] Add option to force-stop app before starting The previous commit introduced: scrcpy --start-app=name By adding a '+' prefix, the app is stopped beforehand: scrcpy --start-app=+name This may be useful to start a fresh app on a new virtual display: scrcpy --new-display --start-app=+org.mozilla.firefox PR #5370 --- app/scrcpy.1 | 4 ++++ app/src/cli.c | 4 +++- .../java/com/genymobile/scrcpy/control/Controller.java | 7 ++++++- .../src/main/java/com/genymobile/scrcpy/device/Device.java | 5 ++++- 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 35abd0d1..802dab5e 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -498,6 +498,10 @@ Default is "lalt,lsuper" (left-Alt or left-Super). .BI "\-\-start\-app " name Start an Android app, by its exact package name. +Add a '+' prefix to force-stop before starting the app: + + scrcpy --new-display --start-app=+org.mozilla.firefox + .TP .B \-t, \-\-show\-touches Enable "show touches" on start, restore the initial value on exit. diff --git a/app/src/cli.c b/app/src/cli.c index ba272393..d715a385 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -811,7 +811,9 @@ static const struct sc_option options[] = { .longopt_id = OPT_START_APP, .longopt = "start-app", .argdesc = "name", - .text = "Start an Android app, by its exact package name.", + .text = "Start an Android app, by its exact package name.\n" + "Add a '+' prefix to force-stop before starting the app:\n" + " scrcpy --new-display --start-app=+org.mozilla.firefox", }, { .shortopt = 't', diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index ccdb85e1..b3ab34c3 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -594,6 +594,11 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { } private void startApp(String name) { + boolean forceStopBeforeStart = name.startsWith("+"); + if (forceStopBeforeStart) { + name = name.substring(1); + } + DeviceApp app = Device.findByPackageName(name); if (app == null) { Ln.w("No app found for package \"" + name + "\""); @@ -607,7 +612,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { } Ln.i("Starting app \"" + app.getName() + "\" [" + app.getPackageName() + "] on display " + startAppDisplayId + "..."); - Device.startApp(app.getPackageName(), startAppDisplayId); + Device.startApp(app.getPackageName(), startAppDisplayId, forceStopBeforeStart); } private int getStartAppDisplayId() { diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Device.java b/server/src/main/java/com/genymobile/scrcpy/device/Device.java index 496865e4..f51a433e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Device.java @@ -264,7 +264,7 @@ public final class Device { return null; } - public static void startApp(String packageName, int displayId) { + public static void startApp(String packageName, int displayId, boolean forceStop) { PackageManager pm = FakeContext.get().getPackageManager(); Intent launchIntent = getLaunchIntent(pm, packageName); @@ -283,6 +283,9 @@ public final class Device { } ActivityManager am = ServiceManager.getActivityManager(); + if (forceStop) { + am.forceStopPackage(packageName); + } am.startActivity(launchIntent, options); } } From 566b5be0f69dae6b98f5df849a75d709ef3bd1c2 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 20 Oct 2024 15:49:25 +0200 Subject: [PATCH 2021/2244] Add option to start an app by its name By adding the '?' prefix, the app is searched by its name instead of its package name (retrieving app names on the device may take some time): scrcpy --start-app=?firefox An app matches if its label starts with the given name, case-insensitive. If '+' is also passed to force-stop the app before starting, then the prefixes must be in that order: scrcpy --start-app=+?firefox PR #5370 --- app/scrcpy.1 | 8 +++++ app/src/cli.c | 8 ++++- .../genymobile/scrcpy/control/Controller.java | 31 ++++++++++++++++--- .../com/genymobile/scrcpy/device/Device.java | 18 +++++++++++ 4 files changed, 60 insertions(+), 5 deletions(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 802dab5e..1b81d05e 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -498,10 +498,18 @@ Default is "lalt,lsuper" (left-Alt or left-Super). .BI "\-\-start\-app " name Start an Android app, by its exact package name. +Add a '?' prefix to select an app whose name starts with the given name, case-insensitive (retrieving app names on the device may take some time): + + scrcpy --start-app=?firefox + Add a '+' prefix to force-stop before starting the app: scrcpy --new-display --start-app=+org.mozilla.firefox +Both prefixes can be used, in that order: + + scrcpy --start-app=+?firefox + .TP .B \-t, \-\-show\-touches Enable "show touches" on start, restore the initial value on exit. diff --git a/app/src/cli.c b/app/src/cli.c index d715a385..4b9be5d8 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -812,8 +812,14 @@ static const struct sc_option options[] = { .longopt = "start-app", .argdesc = "name", .text = "Start an Android app, by its exact package name.\n" + "Add a '?' prefix to select an app whose name starts with the " + "given name, case-insensitive (retrieving app names on the " + "device may take some time):\n" + " scrcpy --start-app=?firefox\n" "Add a '+' prefix to force-stop before starting the app:\n" - " scrcpy --new-display --start-app=+org.mozilla.firefox", + " scrcpy --new-display --start-app=+org.mozilla.firefox\n" + "Both prefixes can be used, in that order:\n" + " scrcpy --start-app=+?firefox", }, { .shortopt = 't', diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index b3ab34c3..0fdb6064 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -8,6 +8,7 @@ import com.genymobile.scrcpy.device.DeviceApp; import com.genymobile.scrcpy.device.Point; import com.genymobile.scrcpy.device.Position; import com.genymobile.scrcpy.util.Ln; +import com.genymobile.scrcpy.util.LogUtils; import com.genymobile.scrcpy.video.VirtualDisplayListener; import com.genymobile.scrcpy.wrappers.ClipboardManager; import com.genymobile.scrcpy.wrappers.InputManager; @@ -23,6 +24,7 @@ import android.view.KeyEvent; import android.view.MotionEvent; import java.io.IOException; +import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; @@ -599,10 +601,31 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { name = name.substring(1); } - DeviceApp app = Device.findByPackageName(name); - if (app == null) { - Ln.w("No app found for package \"" + name + "\""); - return; + DeviceApp app; + boolean searchByName = name.startsWith("?"); + if (searchByName) { + name = name.substring(1); + + Ln.i("Processing Android apps... (this may take some time)"); + List apps = Device.findByName(name); + if (apps.isEmpty()) { + Ln.w("No app found for name \"" + name + "\""); + return; + } + + if (apps.size() > 1) { + String title = "No unique app found for name \"" + name + "\":"; + Ln.w(LogUtils.buildAppListMessage(title, apps)); + return; + } + + app = apps.get(0); + } else { + app = Device.findByPackageName(name); + if (app == null) { + Ln.w("No app found for package \"" + name + "\""); + return; + } } int startAppDisplayId = getStartAppDisplayId(); diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Device.java b/server/src/main/java/com/genymobile/scrcpy/device/Device.java index f51a433e..a2699076 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Device.java @@ -27,6 +27,7 @@ import android.view.KeyEvent; import java.util.ArrayList; import java.util.List; +import java.util.Locale; public final class Device { @@ -264,6 +265,23 @@ public final class Device { return null; } + @SuppressLint("QueryPermissionsNeeded") + public static List findByName(String searchName) { + List result = new ArrayList<>(); + searchName = searchName.toLowerCase(Locale.getDefault()); + + PackageManager pm = FakeContext.get().getPackageManager(); + for (ApplicationInfo appInfo : getLaunchableApps(pm)) { + String name = pm.getApplicationLabel(appInfo).toString(); + if (name.toLowerCase(Locale.getDefault()).startsWith(searchName)) { + boolean system = (appInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0; + result.add(new DeviceApp(appInfo.packageName, name, system)); + } + } + + return result; + } + public static void startApp(String packageName, int displayId, boolean forceStop) { PackageManager pm = FakeContext.get().getPackageManager(); From 381fe95867fb51cfd463f798e2ddbc122e4884b3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 23 Oct 2024 18:52:35 +0200 Subject: [PATCH 2022/2244] Document virtual display and "start app" features PR #5370 --- README.md | 8 ++++++++ doc/device.md | 45 ++++++++++++++++++++++++++++++++++++++++++ doc/virtual_display.md | 26 ++++++++++++++++++++++++ 3 files changed, 79 insertions(+) create mode 100644 doc/virtual_display.md diff --git a/README.md b/README.md index 44f3d740..6e4e513a 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ It focuses on: Its features include: - [audio forwarding](doc/audio.md) (Android 11+) - [recording](doc/recording.md) + - [virtual display](doc/virtual_display.md) - mirroring with [Android device screen off](doc/device.md#turn-screen-off) - [copy-paste](doc/control.md#copy-paste) in both directions - [configurable quality](doc/video.md) @@ -91,6 +92,12 @@ Here are just some common examples. scrcpy --video-codec=h265 -m1920 --max-fps=60 --no-audio -K # short version ``` + - Start VLC in a new virtual display (separate from the device display): + + ```bash + scrcpy --new-display=1920x1080 --start-app=org.videolan.vlc + ``` + - Record the device camera in H.265 at 1920x1080 (and microphone) to an MP4 file: @@ -134,6 +141,7 @@ documented in the following pages: - [Device](doc/device.md) - [Window](doc/window.md) - [Recording](doc/recording.md) + - [Virtual display](doc/virtual_displays.md) - [Tunnels](doc/tunnels.md) - [OTG](doc/otg.md) - [Camera](doc/camera.md) diff --git a/doc/device.md b/doc/device.md index 988ad417..ee86c359 100644 --- a/doc/device.md +++ b/doc/device.md @@ -78,3 +78,48 @@ By default, on start, the device is powered on. To prevent this behavior: ```bash scrcpy --no-power-on ``` + + +## Start Android app + +To list the Android apps installed on the device: + +```bash +scrcpy --list-apps +``` + +An app, selected by its package name, can be launched on start: + +``` +scrcpy --start-app=org.mozilla.firefox +``` + +This feature can be used to run an app in a [virtual +display](virtual_display.md): + +``` +scrcpy --new-display=1920x1080 --start-app=org.videolan.vlc +``` + +The app can be optionally forced-stop before being started, by adding a `+` +prefix: + +``` +scrcpy --start-app=+org.mozilla.firefox +``` + +For convenience, it is also possible to select an app by its name, by adding a +`?` prefix: + +``` +scrcpy --start-app=?firefox +``` + +But retrieving app names may take some time (sometimes several seconds), so +passing the package name is recommended. + +The `+` and `?` prefixes can be combined (in that order): + +``` +scrcpy --start-app=+?firefox +``` diff --git a/doc/virtual_display.md b/doc/virtual_display.md new file mode 100644 index 00000000..4ed5961f --- /dev/null +++ b/doc/virtual_display.md @@ -0,0 +1,26 @@ +# Virtual display + +## New display + +To mirror a new virtual display instead of the device screen: + +```bash +scrcpy --new-display=1920x1080 +scrcpy --new-display=1920x1080/420 # force 420 dpi +scrcpy --new-display # use the main display size and density +scrcpy --new-display -m1920 # ... scaled to fit a max size of 1920 +scrcpy --new-display=/240 # use the main display size and 240 dpi +``` + +## Start app + +On some devices, a launcher is available in the virtual display. + +When no launcher is available, the virtual display is empty. In that case, you +must [start an Android app](device.md#start-android-app). + +For example: + +```bash +scrcpy --new-display=1920x1080 --start-app=org.videolan.vlc +``` From ce21f515e318474ca979a38a05814fc748c2bc85 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 29 Oct 2024 18:58:54 +0100 Subject: [PATCH 2023/2244] Remove unnecessary '\n' in log --- 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 4b9be5d8..d4caaa89 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -2822,7 +2822,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } if (opts->v4l2_buffer && !opts->v4l2_device) { - LOGE("V4L2 buffer value without V4L2 sink\n"); + LOGE("V4L2 buffer value without V4L2 sink"); return false; } #endif From 2c25fd7a8082307da19645a690c31403903fbb1e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 29 Oct 2024 18:59:29 +0100 Subject: [PATCH 2024/2244] Disable mouse by default if no video playback If video playback is disabled, then SDK mouse (which uses absolute positions) could not be used, so the default mouse mode was automatically switched to UHID. But UHID does not work on all devices, so it could make the whole scrcpy session fail. Instead, disable the mouse by default. It is still possible to pass -M or --mouse=uhid to enable it explicitly. Fixes #5410 --- app/src/cli.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index d4caaa89..2437d5fc 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -2841,8 +2841,8 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], if (otg) { opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_AOA; } else if (!opts->video_playback) { - LOGI("No video mirroring, mouse mode switched to UHID"); - opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_UHID; + LOGI("No video mirroring, SDK mouse disabled"); + opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_DISABLED; } else { opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_SDK; } From 5474ae6bd62f65e6aa25a8685a7e5db9571390ac Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 24 Oct 2024 23:50:54 +0200 Subject: [PATCH 2025/2244] Factorize codec info listing Make the listing of video and audio encoders share the same code. PR #5416 --- .../genymobile/scrcpy/util/CodecUtils.java | 45 +------------------ .../com/genymobile/scrcpy/util/LogUtils.java | 38 ++++++++-------- 2 files changed, 19 insertions(+), 64 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/util/CodecUtils.java b/server/src/main/java/com/genymobile/scrcpy/util/CodecUtils.java index 5b0c95e8..3a01256a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/util/CodecUtils.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/CodecUtils.java @@ -1,8 +1,5 @@ package com.genymobile.scrcpy.util; -import com.genymobile.scrcpy.audio.AudioCodec; -import com.genymobile.scrcpy.video.VideoCodec; - import android.media.MediaCodecInfo; import android.media.MediaCodecList; import android.media.MediaFormat; @@ -13,24 +10,6 @@ import java.util.List; public final class CodecUtils { - public static final class DeviceEncoder { - private final Codec codec; - private final MediaCodecInfo info; - - DeviceEncoder(Codec codec, MediaCodecInfo info) { - this.codec = codec; - this.info = info; - } - - public Codec getCodec() { - return codec; - } - - public MediaCodecInfo getInfo() { - return info; - } - } - private CodecUtils() { // not instantiable } @@ -47,7 +26,7 @@ public final class CodecUtils { } } - private static MediaCodecInfo[] getEncoders(MediaCodecList codecs, String mimeType) { + public static MediaCodecInfo[] getEncoders(MediaCodecList codecs, String mimeType) { List result = new ArrayList<>(); for (MediaCodecInfo codecInfo : codecs.getCodecInfos()) { if (codecInfo.isEncoder() && Arrays.asList(codecInfo.getSupportedTypes()).contains(mimeType)) { @@ -56,26 +35,4 @@ public final class CodecUtils { } return result.toArray(new MediaCodecInfo[result.size()]); } - - public static List listVideoEncoders() { - List encoders = new ArrayList<>(); - MediaCodecList codecs = new MediaCodecList(MediaCodecList.REGULAR_CODECS); - for (VideoCodec codec : VideoCodec.values()) { - for (MediaCodecInfo info : getEncoders(codecs, codec.getMimeType())) { - encoders.add(new DeviceEncoder(codec, info)); - } - } - return encoders; - } - - public static List listAudioEncoders() { - List encoders = new ArrayList<>(); - MediaCodecList codecs = new MediaCodecList(MediaCodecList.REGULAR_CODECS); - for (AudioCodec codec : AudioCodec.values()) { - for (MediaCodecInfo info : getEncoders(codecs, codec.getMimeType())) { - encoders.add(new DeviceEncoder(codec, info)); - } - } - return encoders; - } } diff --git a/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java b/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java index 6b813135..f2837f40 100644 --- a/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java @@ -1,9 +1,11 @@ package com.genymobile.scrcpy.util; +import com.genymobile.scrcpy.audio.AudioCodec; import com.genymobile.scrcpy.device.Device; import com.genymobile.scrcpy.device.DeviceApp; import com.genymobile.scrcpy.device.DisplayInfo; import com.genymobile.scrcpy.device.Size; +import com.genymobile.scrcpy.video.VideoCodec; import com.genymobile.scrcpy.wrappers.DisplayManager; import com.genymobile.scrcpy.wrappers.ServiceManager; @@ -14,6 +16,8 @@ import android.hardware.camera2.CameraCharacteristics; import android.hardware.camera2.CameraManager; import android.hardware.camera2.params.StreamConfigurationMap; import android.media.MediaCodec; +import android.media.MediaCodecInfo; +import android.media.MediaCodecList; import android.util.Range; import java.util.Collections; @@ -28,32 +32,26 @@ public final class LogUtils { // not instantiable } - public static String buildVideoEncoderListMessage() { - StringBuilder builder = new StringBuilder("List of video encoders:"); - List videoEncoders = CodecUtils.listVideoEncoders(); - if (videoEncoders.isEmpty()) { - builder.append("\n (none)"); - } else { - for (CodecUtils.DeviceEncoder encoder : videoEncoders) { - builder.append("\n --video-codec=").append(encoder.getCodec().getName()); - builder.append(" --video-encoder=").append(encoder.getInfo().getName()); + private static String buildEncoderListMessage(String type, Codec[] codecs) { + StringBuilder builder = new StringBuilder("List of ").append(type).append(" encoders:"); + MediaCodecList codecList = new MediaCodecList(MediaCodecList.REGULAR_CODECS); + for (Codec codec : codecs) { + MediaCodecInfo[] encoders = CodecUtils.getEncoders(codecList, codec.getMimeType()); + for (MediaCodecInfo info : encoders) { + builder.append("\n --").append(type).append("-codec=").append(codec.getName()); + builder.append(" --").append(type).append("-encoder=").append(info.getName()); } } + return builder.toString(); } + public static String buildVideoEncoderListMessage() { + return buildEncoderListMessage("video", VideoCodec.values()); + } + public static String buildAudioEncoderListMessage() { - StringBuilder builder = new StringBuilder("List of audio encoders:"); - List audioEncoders = CodecUtils.listAudioEncoders(); - if (audioEncoders.isEmpty()) { - builder.append("\n (none)"); - } else { - for (CodecUtils.DeviceEncoder encoder : audioEncoders) { - builder.append("\n --audio-codec=").append(encoder.getCodec().getName()); - builder.append(" --audio-encoder=").append(encoder.getInfo().getName()); - } - } - return builder.toString(); + return buildEncoderListMessage("audio", AudioCodec.values()); } public static String buildDisplayListMessage() { From acff5b005ce9f2efae80adad09d2e77ba3052460 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 24 Oct 2024 23:50:54 +0200 Subject: [PATCH 2026/2244] Add more details to --list-encoders output Add more information about each codec (hw/sw, vendor, alias). Before: [server] INFO: List of video encoders: --video-codec=h264 --video-encoder=c2.exynos.h264.encoder --video-codec=h264 --video-encoder=c2.android.avc.encoder --video-codec=h264 --video-encoder=OMX.google.h264.encoder --video-codec=h265 --video-encoder=c2.exynos.hevc.encoder --video-codec=h265 --video-encoder=c2.android.hevc.encoder --video-codec=av1 --video-encoder=c2.google.av1.encoder --video-codec=av1 --video-encoder=c2.android.av1.encoder // audio encoders omitted After: [server] INFO: List of video encoders: --video-codec=h264 --video-encoder=c2.exynos.h264.encoder (hw) [vendor] --video-codec=h264 --video-encoder=c2.android.avc.encoder (sw) --video-codec=h264 --video-encoder=OMX.google.h264.encoder (sw) (alias for c2.android.avc.encoder) --video-codec=h265 --video-encoder=c2.exynos.hevc.encoder (hw) [vendor] --video-codec=h265 --video-encoder=c2.android.hevc.encoder (sw) --video-codec=av1 --video-encoder=c2.google.av1.encoder (hw) [vendor] --video-codec=av1 --video-encoder=c2.android.av1.encoder (sw) // audio encoders omitted PR #5416 --- .../com/genymobile/scrcpy/util/LogUtils.java | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java b/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java index f2837f40..2b780caf 100644 --- a/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java @@ -1,5 +1,6 @@ package com.genymobile.scrcpy.util; +import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.audio.AudioCodec; import com.genymobile.scrcpy.device.Device; import com.genymobile.scrcpy.device.DeviceApp; @@ -10,6 +11,7 @@ import com.genymobile.scrcpy.wrappers.DisplayManager; import com.genymobile.scrcpy.wrappers.ServiceManager; import android.annotation.SuppressLint; +import android.annotation.TargetApi; import android.graphics.Rect; import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraCharacteristics; @@ -18,6 +20,7 @@ import android.hardware.camera2.params.StreamConfigurationMap; import android.media.MediaCodec; import android.media.MediaCodecInfo; import android.media.MediaCodecList; +import android.os.Build; import android.util.Range; import java.util.Collections; @@ -38,8 +41,25 @@ public final class LogUtils { for (Codec codec : codecs) { MediaCodecInfo[] encoders = CodecUtils.getEncoders(codecList, codec.getMimeType()); for (MediaCodecInfo info : encoders) { + int lineStart = builder.length(); builder.append("\n --").append(type).append("-codec=").append(codec.getName()); builder.append(" --").append(type).append("-encoder=").append(info.getName()); + if (Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10) { + int lineLength = builder.length() - lineStart; + final int column = 70; + if (lineLength < column) { + int padding = column - lineLength; + builder.append(String.format("%" + padding + "s", " ")); + } + builder.append(" (").append(getHwCodecType(info)).append(')'); + if (info.isVendor()) { + builder.append(" [vendor]"); + } + if (info.isAlias()) { + builder.append(" (alias for ").append(info.getCanonicalName()).append(')'); + } + } + } } @@ -54,6 +74,17 @@ public final class LogUtils { return buildEncoderListMessage("audio", AudioCodec.values()); } + @TargetApi(AndroidVersions.API_29_ANDROID_10) + private static String getHwCodecType(MediaCodecInfo info) { + if (info.isSoftwareOnly()) { + return "sw"; + } + if (info.isHardwareAccelerated()) { + return "hw"; + } + return "hybrid"; + } + public static String buildDisplayListMessage() { StringBuilder builder = new StringBuilder("List of displays:"); DisplayManager displayManager = ServiceManager.getDisplayManager(); From 58a0fbbf2e0c7912ad487196c2f46ca6c0bd79cd Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 28 Oct 2024 23:56:17 +0100 Subject: [PATCH 2027/2244] Refactor display power mode Accept a single boolean "on" rather than a "mode" (which, in practice, could only take 2 values: NORMAL and OFF). Also rename "screen power mode" to "display power". PR #5418 --- app/src/control_msg.c | 21 +++--------- app/src/control_msg.h | 12 ++----- app/src/input_manager.c | 13 +++---- app/src/scrcpy.c | 6 ++-- app/tests/test_control_msg_serialize.c | 14 ++++---- .../java/com/genymobile/scrcpy/CleanUp.java | 18 +++++----- .../scrcpy/control/ControlMessage.java | 18 +++++----- .../scrcpy/control/ControlMessageReader.java | 10 +++--- .../genymobile/scrcpy/control/Controller.java | 34 +++++++++---------- .../com/genymobile/scrcpy/device/Device.java | 6 ++-- .../control/ControlMessageReaderTest.java | 12 +++---- 11 files changed, 71 insertions(+), 93 deletions(-) diff --git a/app/src/control_msg.c b/app/src/control_msg.c index a71bf445..e04fbd3c 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -22,9 +22,6 @@ #define MOTIONEVENT_ACTION_LABEL(value) \ ENUM_TO_LABEL(android_motionevent_action_labels, value) -#define SCREEN_POWER_MODE_LABEL(value) \ - ENUM_TO_LABEL(screen_power_mode_labels, value) - static const char *const android_keyevent_action_labels[] = { "down", "up", @@ -47,14 +44,6 @@ static const char *const android_motionevent_action_labels[] = { "btn-release", }; -static const char *const screen_power_mode_labels[] = { - "off", - "doze", - "normal", - "doze-suspend", - "suspend", -}; - static const char *const copy_key_labels[] = { "none", "copy", @@ -158,8 +147,8 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) { size_t len = write_string(&buf[10], msg->set_clipboard.text, SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH); return 10 + len; - case SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE: - buf[1] = msg->set_screen_power_mode.mode; + case SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER: + buf[1] = msg->set_display_power.on; return 2; case SC_CONTROL_MSG_TYPE_UHID_CREATE: sc_write16be(&buf[1], msg->uhid_create.id); @@ -268,9 +257,9 @@ sc_control_msg_log(const struct sc_control_msg *msg) { msg->set_clipboard.paste ? "paste" : "nopaste", msg->set_clipboard.text); break; - case SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE: - LOG_CMSG("power mode %s", - SCREEN_POWER_MODE_LABEL(msg->set_screen_power_mode.mode)); + case SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER: + LOG_CMSG("display power %s", + msg->set_display_power.on ? "on" : "off"); break; case SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL: LOG_CMSG("expand notification panel"); diff --git a/app/src/control_msg.h b/app/src/control_msg.h index a809a154..9eef7e82 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -35,7 +35,7 @@ enum sc_control_msg_type { SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS, SC_CONTROL_MSG_TYPE_GET_CLIPBOARD, SC_CONTROL_MSG_TYPE_SET_CLIPBOARD, - SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE, + SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER, SC_CONTROL_MSG_TYPE_ROTATE_DEVICE, SC_CONTROL_MSG_TYPE_UHID_CREATE, SC_CONTROL_MSG_TYPE_UHID_INPUT, @@ -44,12 +44,6 @@ enum sc_control_msg_type { SC_CONTROL_MSG_TYPE_START_APP, }; -enum sc_screen_power_mode { - // see - SC_SCREEN_POWER_MODE_OFF = 0, - SC_SCREEN_POWER_MODE_NORMAL = 2, -}; - enum sc_copy_key { SC_COPY_KEY_NONE, SC_COPY_KEY_COPY, @@ -95,8 +89,8 @@ struct sc_control_msg { bool paste; } set_clipboard; struct { - enum sc_screen_power_mode mode; - } set_screen_power_mode; + bool on; + } set_display_power; struct { uint16_t id; const char *name; // pointer to static data diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 969196e3..140b50ac 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -203,13 +203,12 @@ set_device_clipboard(struct sc_input_manager *im, bool paste, } static void -set_screen_power_mode(struct sc_input_manager *im, - enum sc_screen_power_mode mode) { +set_display_power(struct sc_input_manager *im, bool on) { assert(im->controller); struct sc_control_msg msg; - msg.type = SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE; - msg.set_screen_power_mode.mode = mode; + msg.type = SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER; + msg.set_display_power.on = on; if (!sc_controller_push_msg(im->controller, &msg)) { LOGW("Could not request 'set screen power mode'"); @@ -415,10 +414,8 @@ sc_input_manager_process_key(struct sc_input_manager *im, return; case SDLK_o: if (control && !repeat && down && !paused) { - enum sc_screen_power_mode mode = shift - ? SC_SCREEN_POWER_MODE_NORMAL - : SC_SCREEN_POWER_MODE_OFF; - set_screen_power_mode(im, mode); + bool on = shift; + set_display_power(im, on); } return; case SDLK_z: diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 64a2fa10..f0ce1959 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -873,11 +873,11 @@ aoa_complete: // everything is set up if (options->control && options->turn_screen_off) { struct sc_control_msg msg; - msg.type = SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE; - msg.set_screen_power_mode.mode = SC_SCREEN_POWER_MODE_OFF; + msg.type = SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER; + msg.set_display_power.on = false; if (!sc_controller_push_msg(&s->controller, &msg)) { - LOGW("Could not request 'set screen power mode'"); + LOGW("Could not request 'set display power'"); } } diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c index 72ec61ee..73bca901 100644 --- a/app/tests/test_control_msg_serialize.c +++ b/app/tests/test_control_msg_serialize.c @@ -289,11 +289,11 @@ static void test_serialize_set_clipboard_long(void) { assert(!memcmp(buf, expected, sizeof(expected))); } -static void test_serialize_set_screen_power_mode(void) { +static void test_serialize_set_display_power(void) { struct sc_control_msg msg = { - .type = SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE, - .set_screen_power_mode = { - .mode = SC_SCREEN_POWER_MODE_NORMAL, + .type = SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER, + .set_display_power = { + .on = true, }, }; @@ -302,8 +302,8 @@ static void test_serialize_set_screen_power_mode(void) { assert(size == 2); const uint8_t expected[] = { - SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE, - 0x02, // SC_SCREEN_POWER_MODE_NORMAL + SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER, + 0x01, // true }; assert(!memcmp(buf, expected, sizeof(expected))); } @@ -423,7 +423,7 @@ int main(int argc, char *argv[]) { test_serialize_get_clipboard(); test_serialize_set_clipboard(); test_serialize_set_clipboard_long(); - test_serialize_set_screen_power_mode(); + test_serialize_set_display_power(); test_serialize_rotate_device(); test_serialize_uhid_create(); test_serialize_uhid_input(); diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java index a47aae90..7fbb6cc6 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java +++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java @@ -19,7 +19,7 @@ public final class CleanUp { private static final int MSG_TYPE_MASK = 0b11; private static final int MSG_TYPE_RESTORE_STAY_ON = 0; private static final int MSG_TYPE_DISABLE_SHOW_TOUCHES = 1; - private static final int MSG_TYPE_RESTORE_NORMAL_POWER_MODE = 2; + private static final int MSG_TYPE_RESTORE_DISPLAY_POWER = 2; private static final int MSG_TYPE_POWER_OFF_SCREEN = 3; private static final int MSG_PARAM_SHIFT = 2; @@ -63,8 +63,8 @@ public final class CleanUp { return sendMessage(MSG_TYPE_DISABLE_SHOW_TOUCHES, disableOnExit ? 1 : 0); } - public boolean setRestoreNormalPowerMode(boolean restoreOnExit) { - return sendMessage(MSG_TYPE_RESTORE_NORMAL_POWER_MODE, restoreOnExit ? 1 : 0); + public boolean setRestoreDisplayPower(boolean restoreOnExit) { + return sendMessage(MSG_TYPE_RESTORE_DISPLAY_POWER, restoreOnExit ? 1 : 0); } public boolean setPowerOffScreen(boolean powerOffScreenOnExit) { @@ -86,7 +86,7 @@ public final class CleanUp { int restoreStayOn = -1; boolean disableShowTouches = false; - boolean restoreNormalPowerMode = false; + boolean restoreDisplayPower = false; boolean powerOffScreen = false; try { @@ -102,8 +102,8 @@ public final class CleanUp { case MSG_TYPE_DISABLE_SHOW_TOUCHES: disableShowTouches = param != 0; break; - case MSG_TYPE_RESTORE_NORMAL_POWER_MODE: - restoreNormalPowerMode = param != 0; + case MSG_TYPE_RESTORE_DISPLAY_POWER: + restoreDisplayPower = param != 0; break; case MSG_TYPE_POWER_OFF_SCREEN: powerOffScreen = param != 0; @@ -143,9 +143,9 @@ public final class CleanUp { Ln.i("Power off screen"); Device.powerOffScreen(displayId); } - } else if (restoreNormalPowerMode) { - Ln.i("Restoring normal power mode"); - Device.setScreenPowerMode(Device.POWER_MODE_NORMAL); + } else if (restoreDisplayPower) { + Ln.i("Restoring display power"); + Device.setDisplayPower(true); } } diff --git a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java index 36dbd03a..eec5f67f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java @@ -17,7 +17,7 @@ public final class ControlMessage { public static final int TYPE_COLLAPSE_PANELS = 7; public static final int TYPE_GET_CLIPBOARD = 8; public static final int TYPE_SET_CLIPBOARD = 9; - public static final int TYPE_SET_SCREEN_POWER_MODE = 10; + public static final int TYPE_SET_DISPLAY_POWER = 10; public static final int TYPE_ROTATE_DEVICE = 11; public static final int TYPE_UHID_CREATE = 12; public static final int TYPE_UHID_INPUT = 13; @@ -34,7 +34,7 @@ public final class ControlMessage { private int type; private String text; private int metaState; // KeyEvent.META_* - private int action; // KeyEvent.ACTION_* or MotionEvent.ACTION_* or POWER_MODE_* + private int action; // KeyEvent.ACTION_* or MotionEvent.ACTION_* private int keycode; // KeyEvent.KEYCODE_* private int actionButton; // MotionEvent.BUTTON_* private int buttons; // MotionEvent.BUTTON_* @@ -49,6 +49,7 @@ public final class ControlMessage { private long sequence; private int id; private byte[] data; + private boolean on; private ControlMessage() { } @@ -116,13 +117,10 @@ public final class ControlMessage { return msg; } - /** - * @param mode one of the {@code Device.SCREEN_POWER_MODE_*} constants - */ - public static ControlMessage createSetScreenPowerMode(int mode) { + public static ControlMessage createSetDisplayPower(boolean on) { ControlMessage msg = new ControlMessage(); - msg.type = TYPE_SET_SCREEN_POWER_MODE; - msg.action = mode; + msg.type = TYPE_SET_DISPLAY_POWER; + msg.on = on; return msg; } @@ -234,4 +232,8 @@ public final class ControlMessage { public byte[] getData() { return data; } + + public boolean getOn() { + return on; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java index eb5dc787..ae167690 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java @@ -39,8 +39,8 @@ public class ControlMessageReader { return parseGetClipboard(); case ControlMessage.TYPE_SET_CLIPBOARD: return parseSetClipboard(); - case ControlMessage.TYPE_SET_SCREEN_POWER_MODE: - return parseSetScreenPowerMode(); + case ControlMessage.TYPE_SET_DISPLAY_POWER: + return parseSetDisplayPower(); case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL: case ControlMessage.TYPE_EXPAND_SETTINGS_PANEL: case ControlMessage.TYPE_COLLAPSE_PANELS: @@ -134,9 +134,9 @@ public class ControlMessageReader { return ControlMessage.createSetClipboard(sequence, text, paste); } - private ControlMessage parseSetScreenPowerMode() throws IOException { - int mode = dis.readUnsignedByte(); - return ControlMessage.createSetScreenPowerMode(mode); + private ControlMessage parseSetDisplayPower() throws IOException { + boolean on = dis.readBoolean(); + return ControlMessage.createSetDisplayPower(on); } private ControlMessage parseUhidCreate() throws IOException { diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index 0fdb6064..81da1800 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -91,7 +91,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { 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; + private boolean keepDisplayPowerOff; public Controller(int displayId, ControlChannel controlChannel, CleanUp cleanUp, boolean clipboardAutosync, boolean powerOn) { this.displayId = displayId; @@ -270,16 +270,16 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { case ControlMessage.TYPE_SET_CLIPBOARD: setClipboard(msg.getText(), msg.getPaste(), msg.getSequence()); break; - case ControlMessage.TYPE_SET_SCREEN_POWER_MODE: + case ControlMessage.TYPE_SET_DISPLAY_POWER: if (supportsInputEvents) { - 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")); + boolean on = msg.getOn(); + boolean setDisplayPowerOk = Device.setDisplayPower(on); + if (setDisplayPowerOk) { + keepDisplayPowerOff = !on; + Ln.i("Device display turned " + (on ? "on" : "off")); if (cleanUp != null) { - boolean mustRestoreOnExit = mode != Device.POWER_MODE_NORMAL; - cleanUp.setRestoreNormalPowerMode(mustRestoreOnExit); + boolean mustRestoreOnExit = !on; + cleanUp.setRestoreDisplayPower(mustRestoreOnExit); } } } @@ -310,8 +310,8 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { } 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(); + if (keepDisplayPowerOff && action == KeyEvent.ACTION_UP && (keycode == KeyEvent.KEYCODE_POWER || keycode == KeyEvent.KEYCODE_WAKEUP)) { + scheduleDisplayPowerOff(); } return injectKeyEvent(action, keycode, repeat, metaState, Device.INJECT_MODE_ASYNC); } @@ -488,12 +488,12 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { } /** - * Schedule a call to set power mode to off after a small delay. + * Schedule a call to set display power to off after a small delay. */ - private static void schedulePowerModeOff() { + private static void scheduleDisplayPowerOff() { EXECUTOR.schedule(() -> { - Ln.i("Forcing screen off"); - Device.setScreenPowerMode(Device.POWER_MODE_OFF); + Ln.i("Forcing display off"); + Device.setDisplayPower(false); }, 200, TimeUnit.MILLISECONDS); } @@ -509,8 +509,8 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { return true; } - if (keepPowerModeOff) { - schedulePowerModeOff(); + if (keepDisplayPowerOff) { + scheduleDisplayPowerOff(); } return pressReleaseKeycode(KeyEvent.KEYCODE_POWER, Device.INJECT_MODE_ASYNC); } diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Device.java b/server/src/main/java/com/genymobile/scrcpy/device/Device.java index a2699076..cbc1bc81 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Device.java @@ -126,10 +126,7 @@ public final class Device { return clipboardManager.setText(text); } - /** - * @param mode one of the {@code POWER_MODE_*} constants - */ - public static boolean setScreenPowerMode(int mode) { + public static boolean setDisplayPower(boolean on) { boolean applyToMultiPhysicalDisplays = Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10; if (applyToMultiPhysicalDisplays @@ -142,6 +139,7 @@ public final class Device { applyToMultiPhysicalDisplays = false; } + int mode = on ? POWER_MODE_NORMAL : POWER_MODE_OFF; if (applyToMultiPhysicalDisplays) { // On Android 14, these internal methods have been moved to DisplayControl boolean useDisplayControl = diff --git a/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java index d8489fc3..a25507b4 100644 --- a/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java @@ -1,7 +1,5 @@ package com.genymobile.scrcpy.control; -import com.genymobile.scrcpy.device.Device; - import android.view.KeyEvent; import android.view.MotionEvent; import org.junit.Assert; @@ -285,19 +283,19 @@ public class ControlMessageReaderTest { } @Test - public void testParseSetScreenPowerMode() throws IOException { + public void testParseSetDisplayPower() throws IOException { ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); - dos.writeByte(ControlMessage.TYPE_SET_SCREEN_POWER_MODE); - dos.writeByte(Device.POWER_MODE_NORMAL); + dos.writeByte(ControlMessage.TYPE_SET_DISPLAY_POWER); + dos.writeBoolean(true); byte[] packet = bos.toByteArray(); ByteArrayInputStream bis = new ByteArrayInputStream(packet); ControlMessageReader reader = new ControlMessageReader(bis); ControlMessage event = reader.read(); - Assert.assertEquals(ControlMessage.TYPE_SET_SCREEN_POWER_MODE, event.getType()); - Assert.assertEquals(Device.POWER_MODE_NORMAL, event.getAction()); + Assert.assertEquals(ControlMessage.TYPE_SET_DISPLAY_POWER, event.getType()); + Assert.assertTrue(event.getOn()); Assert.assertEquals(-1, bis.read()); // EOS } From 569c37cec159a67593b01c52aaa4cb92d1826bab Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 29 Oct 2024 00:41:02 +0100 Subject: [PATCH 2028/2244] Disable display power for virtual displays If displayId == Device.DISPLAY_ID_NONE, then the display is virtual: its power mode cannot be changed. PR #5418 --- server/src/main/java/com/genymobile/scrcpy/CleanUp.java | 8 +++----- .../java/com/genymobile/scrcpy/control/Controller.java | 4 +++- 2 files changed, 6 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 7fbb6cc6..f561a10f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java +++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java @@ -137,12 +137,10 @@ public final class CleanUp { } } - if (Device.isScreenOn()) { + if (Device.isScreenOn() && displayId != Device.DISPLAY_ID_NONE) { if (powerOffScreen) { - if (displayId != Device.DISPLAY_ID_NONE) { - Ln.i("Power off screen"); - Device.powerOffScreen(displayId); - } + Ln.i("Power off screen"); + Device.powerOffScreen(displayId); } else if (restoreDisplayPower) { Ln.i("Restoring display power"); Device.setDisplayPower(true); diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index 81da1800..fbe0691e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -271,7 +271,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { setClipboard(msg.getText(), msg.getPaste(), msg.getSequence()); break; case ControlMessage.TYPE_SET_DISPLAY_POWER: - if (supportsInputEvents) { + if (supportsInputEvents && displayId != Device.DISPLAY_ID_NONE) { boolean on = msg.getOn(); boolean setDisplayPowerOk = Device.setDisplayPower(on); if (setDisplayPowerOk) { @@ -311,6 +311,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { private boolean injectKeycode(int action, int keycode, int repeat, int metaState) { if (keepDisplayPowerOff && action == KeyEvent.ACTION_UP && (keycode == KeyEvent.KEYCODE_POWER || keycode == KeyEvent.KEYCODE_WAKEUP)) { + assert displayId != Device.DISPLAY_ID_NONE; scheduleDisplayPowerOff(); } return injectKeyEvent(action, keycode, repeat, metaState, Device.INJECT_MODE_ASYNC); @@ -510,6 +511,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { } if (keepDisplayPowerOff) { + assert displayId != Device.DISPLAY_ID_NONE; scheduleDisplayPowerOff(); } return pressReleaseKeycode(KeyEvent.KEYCODE_POWER, Device.INJECT_MODE_ASYNC); From 58ba00fa060c9a1f439120f8869ed106e1c935f9 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 29 Oct 2024 00:45:26 +0100 Subject: [PATCH 2029/2244] Adapt "turn screen off" for Android 15 Android 15 introduced an easy way to set the display power: Refs #3927 Refs PR #5418 --- .../java/com/genymobile/scrcpy/CleanUp.java | 2 +- .../genymobile/scrcpy/control/Controller.java | 10 ++++----- .../com/genymobile/scrcpy/device/Device.java | 8 ++++++- .../scrcpy/wrappers/DisplayManager.java | 21 +++++++++++++++++++ 4 files changed, 34 insertions(+), 7 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java index f561a10f..c8ee3ef4 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java +++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java @@ -143,7 +143,7 @@ public final class CleanUp { Device.powerOffScreen(displayId); } else if (restoreDisplayPower) { Ln.i("Restoring display power"); - Device.setDisplayPower(true); + Device.setDisplayPower(displayId, true); } } diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index fbe0691e..7add4ea9 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -273,7 +273,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { case ControlMessage.TYPE_SET_DISPLAY_POWER: if (supportsInputEvents && displayId != Device.DISPLAY_ID_NONE) { boolean on = msg.getOn(); - boolean setDisplayPowerOk = Device.setDisplayPower(on); + boolean setDisplayPowerOk = Device.setDisplayPower(displayId, on); if (setDisplayPowerOk) { keepDisplayPowerOff = !on; Ln.i("Device display turned " + (on ? "on" : "off")); @@ -312,7 +312,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { private boolean injectKeycode(int action, int keycode, int repeat, int metaState) { if (keepDisplayPowerOff && action == KeyEvent.ACTION_UP && (keycode == KeyEvent.KEYCODE_POWER || keycode == KeyEvent.KEYCODE_WAKEUP)) { assert displayId != Device.DISPLAY_ID_NONE; - scheduleDisplayPowerOff(); + scheduleDisplayPowerOff(displayId); } return injectKeyEvent(action, keycode, repeat, metaState, Device.INJECT_MODE_ASYNC); } @@ -491,10 +491,10 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { /** * Schedule a call to set display power to off after a small delay. */ - private static void scheduleDisplayPowerOff() { + private static void scheduleDisplayPowerOff(int displayId) { EXECUTOR.schedule(() -> { Ln.i("Forcing display off"); - Device.setDisplayPower(false); + Device.setDisplayPower(displayId, false); }, 200, TimeUnit.MILLISECONDS); } @@ -512,7 +512,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { if (keepDisplayPowerOff) { assert displayId != Device.DISPLAY_ID_NONE; - scheduleDisplayPowerOff(); + scheduleDisplayPowerOff(displayId); } return pressReleaseKeycode(KeyEvent.KEYCODE_POWER, Device.INJECT_MODE_ASYNC); } diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Device.java b/server/src/main/java/com/genymobile/scrcpy/device/Device.java index cbc1bc81..1cf96714 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Device.java @@ -126,7 +126,13 @@ public final class Device { return clipboardManager.setText(text); } - public static boolean setDisplayPower(boolean on) { + public static boolean setDisplayPower(int displayId, boolean on) { + assert displayId != Device.DISPLAY_ID_NONE; + + if (Build.VERSION.SDK_INT >= AndroidVersions.API_35_ANDROID_15) { + return ServiceManager.getDisplayManager().requestDisplayPower(displayId, on); + } + boolean applyToMultiPhysicalDisplays = Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10; if (applyToMultiPhysicalDisplays 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 c8c405bb..37d82c33 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java @@ -1,5 +1,6 @@ package com.genymobile.scrcpy.wrappers; +import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.FakeContext; import com.genymobile.scrcpy.device.DisplayInfo; import com.genymobile.scrcpy.device.Size; @@ -7,6 +8,7 @@ import com.genymobile.scrcpy.util.Command; import com.genymobile.scrcpy.util.Ln; import android.annotation.SuppressLint; +import android.annotation.TargetApi; import android.content.Context; import android.hardware.display.VirtualDisplay; import android.view.Display; @@ -22,6 +24,7 @@ import java.util.regex.Pattern; public final class DisplayManager { private final Object manager; // instance of hidden class android.hardware.display.DisplayManagerGlobal private Method createVirtualDisplayMethod; + private Method requestDisplayPowerMethod; static DisplayManager create() { try { @@ -137,4 +140,22 @@ public final class DisplayManager { android.hardware.display.DisplayManager dm = ctor.newInstance(FakeContext.get()); return dm.createVirtualDisplay(name, width, height, dpi, surface, flags); } + + private Method getRequestDisplayPowerMethod() throws NoSuchMethodException { + if (requestDisplayPowerMethod == null) { + requestDisplayPowerMethod = manager.getClass().getMethod("requestDisplayPower", int.class, boolean.class); + } + return requestDisplayPowerMethod; + } + + @TargetApi(AndroidVersions.API_35_ANDROID_15) + public boolean requestDisplayPower(int displayId, boolean on) { + try { + Method method = getRequestDisplayPowerMethod(); + return (boolean) method.invoke(manager, displayId, on); + } catch (ReflectiveOperationException e) { + Ln.e("Could not invoke method", e); + return false; + } + } } From 1f6634ea87b36c2c6fe08af90b983c69294ee74d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 30 Oct 2024 22:23:53 +0100 Subject: [PATCH 2030/2244] Document adb shell settings commands Some scrcpy features change Android settings with `adb shell settings`. Document the commands to execute manually. --- doc/device.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/doc/device.md b/doc/device.md index ee86c359..06210e18 100644 --- a/doc/device.md +++ b/doc/device.md @@ -18,6 +18,21 @@ The initial state is restored when _scrcpy_ is closed. If the device is not plugged in (i.e. only connected over TCP/IP), `--stay-awake` has no effect (this is the Android behavior). +This changes the value of [`stay_on_while_plugged_in`], setting which can be +changed manually: + +[`stay_on_while_plugged_in`]: https://developer.android.com/reference/android/provider/Settings.Global#STAY_ON_WHILE_PLUGGED_IN + + +```bash +# get the current show_touches value +adb shell settings get global stay_on_while_plugged_in +# enable for AC/USB/wireless chargers +adb shell settings put global stay_on_while_plugged_in 7 +# disable +adb shell settings put global stay_on_while_plugged_in 0 +``` + ## Turn screen off @@ -46,6 +61,15 @@ scrcpy --turn-screen-off --stay-awake scrcpy -Sw # short version ``` +Since Android 15, it is possible to change this setting manually: + +``` +# turn screen off (0 for main display) +adb shell cmd display power-off 0 +# turn screen on +adb shell cmd display power-on 0 +``` + ## Show touches @@ -62,6 +86,16 @@ scrcpy -t # short version Note that it only shows _physical_ touches (by a finger on the device). +It is possible to change this setting manually: + +```bash +# get the current show_touches value +adb shell settings get system show_touches +# enable show_touches +adb shell settings put system show_touches 1 +# disable show_touches +adb shell settings put system show_touches 0 +``` ## Power off on close From d62fa8880e03e8823057a5d4d9659d5f19132806 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 28 Oct 2024 23:31:27 +0100 Subject: [PATCH 2031/2244] Disable broken options on Android 14 The options --lock-video-orientation and --crop are broken since Android 14. Hopefully, they will be reimplemented differently. Meanwhile, when running Android >= 14, fail with an error to prevent incorrect behavior. Refs #4011 Refs #4162 PR #5417 --- app/src/cli.c | 3 ++- app/src/options.h | 2 ++ .../java/com/genymobile/scrcpy/Options.java | 7 ++++++- .../main/java/com/genymobile/scrcpy/Server.java | 17 +++++++++++++++++ .../com/genymobile/scrcpy/device/Device.java | 2 ++ .../com/genymobile/scrcpy/video/ScreenInfo.java | 2 +- 6 files changed, 30 insertions(+), 3 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index 2437d5fc..95035836 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -2812,7 +2812,8 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], SC_LOCK_VIDEO_ORIENTATION_UNLOCKED) { LOGI("Video orientation is locked for v4l2 sink. " "See --lock-video-orientation."); - opts->lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_INITIAL; + opts->lock_video_orientation = + SC_LOCK_VIDEO_ORIENTATION_INITIAL_AUTO; } // V4L2 could not handle size change. diff --git a/app/src/options.h b/app/src/options.h index ec5e71ea..2d458ab4 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -134,6 +134,8 @@ enum sc_lock_video_orientation { SC_LOCK_VIDEO_ORIENTATION_UNLOCKED = -1, // lock the current orientation when scrcpy starts SC_LOCK_VIDEO_ORIENTATION_INITIAL = -2, + // like SC_LOCK_VIDEO_ORIENTATION_INITIAL, but set automatically + SC_LOCK_VIDEO_ORIENTATION_INITIAL_AUTO = -3, SC_LOCK_VIDEO_ORIENTATION_0 = 0, SC_LOCK_VIDEO_ORIENTATION_90 = 3, SC_LOCK_VIDEO_ORIENTATION_180 = 2, diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 65702b42..659a6948 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -2,6 +2,7 @@ package com.genymobile.scrcpy; import com.genymobile.scrcpy.audio.AudioCodec; import com.genymobile.scrcpy.audio.AudioSource; +import com.genymobile.scrcpy.device.Device; import com.genymobile.scrcpy.device.NewDisplay; import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.util.CodecOption; @@ -31,7 +32,7 @@ public class Options { private int videoBitRate = 8000000; private int audioBitRate = 128000; private float maxFps; - private int lockVideoOrientation = -1; + private int lockVideoOrientation = Device.LOCK_VIDEO_ORIENTATION_UNLOCKED; private boolean tunnelForward; private Rect crop; private boolean control = true; @@ -253,6 +254,10 @@ public class Options { return sendCodecMeta; } + public void resetLockVideoOrientation() { + this.lockVideoOrientation = Device.LOCK_VIDEO_ORIENTATION_UNLOCKED; + } + @SuppressWarnings("MethodLength") public static Options parse(String... args) { if (args.length < 1) { diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 35d317f2..a0a48806 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -132,6 +132,23 @@ public final class Server { throw new ConfigurationException("New virtual display is not supported"); } + if (Build.VERSION.SDK_INT >= AndroidVersions.API_34_ANDROID_14) { + int lockVideoOrientation = options.getLockVideoOrientation(); + if (lockVideoOrientation != Device.LOCK_VIDEO_ORIENTATION_UNLOCKED) { + if (lockVideoOrientation != Device.LOCK_VIDEO_ORIENTATION_INITIAL_AUTO) { + Ln.e("--lock-video-orientation is broken on Android >= 14: "); + throw new ConfigurationException("--lock-video-orientation is broken on Android >= 14"); + } else { + // If the flag has been set automatically (because v4l2 sink is enabled), do not fail + Ln.w("--lock-video-orientation is ignored on Android >= 14: "); + } + } + if (options.getCrop() != null) { + Ln.e("--crop is broken on Android >= 14: "); + throw new ConfigurationException("Crop is not broken on Android >= 14"); + } + } + CleanUp cleanUp = null; Thread initThread = null; diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Device.java b/server/src/main/java/com/genymobile/scrcpy/device/Device.java index 1cf96714..5cd7a52a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Device.java @@ -42,6 +42,8 @@ public final class Device { public static final int LOCK_VIDEO_ORIENTATION_UNLOCKED = -1; public static final int LOCK_VIDEO_ORIENTATION_INITIAL = -2; + // like SC_LOCK_VIDEO_ORIENTATION_INITIAL, but set automatically + public static final int LOCK_VIDEO_ORIENTATION_INITIAL_AUTO = -3; private Device() { // not instantiable diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java index cc82a654..602bd8ab 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java @@ -64,7 +64,7 @@ public final class ScreenInfo { } public static ScreenInfo computeScreenInfo(int rotation, Size deviceSize, Rect crop, int maxSize, int lockedVideoOrientation) { - if (lockedVideoOrientation == Device.LOCK_VIDEO_ORIENTATION_INITIAL) { + if (lockedVideoOrientation == Device.LOCK_VIDEO_ORIENTATION_INITIAL || lockedVideoOrientation == Device.LOCK_VIDEO_ORIENTATION_INITIAL_AUTO) { // The user requested to lock the video orientation to the current orientation lockedVideoOrientation = rotation; } From c29ecd0314352661e826d8474b5d275d5ac660f2 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 29 Oct 2024 19:09:01 +0100 Subject: [PATCH 2032/2244] Rename --display-buffer to --video-buffer For consistency with --audio-buffer, rename --display-buffer to --video-buffer. Fixes #5403 PR #5420 --- app/data/bash-completion/scrcpy | 4 ++-- app/data/zsh-completion/_scrcpy | 2 +- app/scrcpy.1 | 16 +++++++++------- app/src/cli.c | 22 +++++++++++++++++----- app/src/options.c | 2 +- app/src/options.h | 2 +- app/src/scrcpy.c | 12 ++++++------ doc/audio.md | 2 +- doc/develop.md | 6 +++--- doc/video.md | 6 +++--- 10 files changed, 44 insertions(+), 30 deletions(-) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index 223c5264..d9a7be60 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -20,7 +20,6 @@ _scrcpy() { --crop= -d --select-usb --disable-screensaver - --display-buffer= --display-id= --display-orientation= -e --select-tcpip @@ -90,6 +89,7 @@ _scrcpy() { --v4l2-sink= -v --version -V --verbosity= + --video-buffer= --video-codec= --video-codec-options= --video-encoder= @@ -191,7 +191,6 @@ _scrcpy() { |--camera-size \ |--crop \ |--display-id \ - |--display-buffer \ |--max-fps \ |-m|--max-size \ |-p|--port \ @@ -201,6 +200,7 @@ _scrcpy() { |--tunnel-port \ |--v4l2-buffer \ |--v4l2-sink \ + |--video-buffer \ |--video-codec-options \ |--video-encoder \ |--tcpip \ diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 8d1189c0..c4e0e60c 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -27,7 +27,6 @@ arguments=( '--crop=[\[width\:height\:x\:y\] Crop the device screen on the server]' {-d,--select-usb}'[Use USB device]' '--disable-screensaver[Disable screensaver while scrcpy is running]' - '--display-buffer=[Add a buffering delay \(in milliseconds\) before displaying]' '--display-id=[Specify the display id to mirror]' '--display-orientation=[Set the initial display orientation]:orientation values:(0 90 180 270 flip0 flip90 flip180 flip270)' {-e,--select-tcpip}'[Use TCP/IP device]' @@ -92,6 +91,7 @@ arguments=( '--v4l2-sink=[\[\/dev\/videoN\] Output to v4l2loopback device]' {-v,--version}'[Print the version of scrcpy]' {-V,--verbosity=}'[Set the log level]:verbosity:(verbose debug info warn error)' + '--video-buffer=[Add a buffering delay \(in milliseconds\) before displaying video frames]' '--video-codec=[Select the video codec]:codec:(h264 h265 av1)' '--video-codec-options=[Set a list of comma-separated key\:type=value options for the device video encoder]' '--video-encoder=[Use a specific MediaCodec video encoder]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 1b81d05e..f517ad4c 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -139,12 +139,6 @@ Also see \fB\-e\fR (\fB\-\-select\-tcpip\fR). .BI "\-\-disable\-screensaver" Disable screensaver while scrcpy is running. -.TP -.BI "\-\-display\-buffer " ms -Add a buffering delay (in milliseconds) before displaying. This increases latency to compensate for jitter. - -Default is 0 (no buffering). - .TP .BI "\-\-display\-id " id Specify the device display id to mirror. @@ -560,7 +554,15 @@ It requires to lock the video orientation (see \fB\-\-lock\-video\-orientation\f .BI "\-\-v4l2-buffer " ms Add a buffering delay (in milliseconds) before pushing frames. This increases latency to compensate for jitter. -This option is similar to \fB\-\-display\-buffer\fR, but specific to V4L2 sink. +This option is similar to \fB\-\-video\-buffer\fR, but specific to V4L2 sink. + +Default is 0 (no buffering). + +.TP +.BI "\-\-video\-buffer " ms +Add a buffering delay (in milliseconds) before displaying video frames. + +This increases latency to compensate for jitter. Default is 0 (no buffering). diff --git a/app/src/cli.c b/app/src/cli.c index 95035836..77747e93 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -50,6 +50,7 @@ enum { OPT_POWER_OFF_ON_CLOSE, OPT_V4L2_SINK, OPT_DISPLAY_BUFFER, + OPT_VIDEO_BUFFER, OPT_V4L2_BUFFER, OPT_TUNNEL_HOST, OPT_TUNNEL_PORT, @@ -321,12 +322,10 @@ static const struct sc_option options[] = { .argdesc = "id", }, { + // deprecated .longopt_id = OPT_DISPLAY_BUFFER, .longopt = "display-buffer", .argdesc = "ms", - .text = "Add a buffering delay (in milliseconds) before displaying. " - "This increases latency to compensate for jitter.\n" - "Default is 0 (no buffering).", }, { .longopt_id = OPT_DISPLAY_ID, @@ -898,11 +897,20 @@ static const struct sc_option options[] = { .argdesc = "ms", .text = "Add a buffering delay (in milliseconds) before pushing " "frames. This increases latency to compensate for jitter.\n" - "This option is similar to --display-buffer, but specific to " + "This option is similar to --video-buffer, but specific to " "V4L2 sink.\n" "Default is 0 (no buffering).\n" "This option is only available on Linux.", }, + { + .longopt_id = OPT_VIDEO_BUFFER, + .longopt = "video-buffer", + .argdesc = "ms", + .text = "Add a buffering delay (in milliseconds) before displaying " + "video frames.\n" + "This increases latency to compensate for jitter.\n" + "Default is 0 (no buffering).", + }, { .longopt_id = OPT_VIDEO_CODEC, .longopt = "video-codec", @@ -2549,7 +2557,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], opts->power_off_on_close = true; break; case OPT_DISPLAY_BUFFER: - if (!parse_buffering_time(optarg, &opts->display_buffer)) { + LOGW("--display-buffer is deprecated, use --video-buffer " + "instead."); + // fall through + case OPT_VIDEO_BUFFER: + if (!parse_buffering_time(optarg, &opts->video_buffer)) { return false; } break; diff --git a/app/src/options.c b/app/src/options.c index 8106ce3d..b1a3b739 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -58,7 +58,7 @@ const struct scrcpy_options scrcpy_options_default = { .window_width = 0, .window_height = 0, .display_id = 0, - .display_buffer = 0, + .video_buffer = 0, .audio_buffer = -1, // depends on the audio format, .audio_output_buffer = SC_TICK_FROM_MS(5), .time_limit = 0, diff --git a/app/src/options.h b/app/src/options.h index 2d458ab4..d37ac0a2 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -261,7 +261,7 @@ struct scrcpy_options { uint16_t window_width; uint16_t window_height; uint32_t display_id; - sc_tick display_buffer; + sc_tick video_buffer; sc_tick audio_buffer; sc_tick audio_output_buffer; sc_tick time_limit; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index f0ce1959..8d135394 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -53,7 +53,7 @@ struct scrcpy { struct sc_decoder video_decoder; struct sc_decoder audio_decoder; struct sc_recorder recorder; - struct sc_delay_buffer display_buffer; + struct sc_delay_buffer video_buffer; #ifdef HAVE_V4L2 struct sc_v4l2_sink v4l2_sink; struct sc_delay_buffer v4l2_buffer; @@ -815,11 +815,11 @@ aoa_complete: if (options->video_playback) { struct sc_frame_source *src = &s->video_decoder.frame_source; - if (options->display_buffer) { - sc_delay_buffer_init(&s->display_buffer, - options->display_buffer, true); - sc_frame_source_add_sink(src, &s->display_buffer.frame_sink); - src = &s->display_buffer.frame_source; + if (options->video_buffer) { + sc_delay_buffer_init(&s->video_buffer, + options->video_buffer, true); + sc_frame_source_add_sink(src, &s->video_buffer.frame_sink); + src = &s->video_buffer.frame_source; } sc_frame_source_add_sink(src, &s->screen.frame_sink); diff --git a/doc/audio.md b/doc/audio.md index 750163e0..85f76ac5 100644 --- a/doc/audio.md +++ b/doc/audio.md @@ -170,7 +170,7 @@ latency (for both [video](video.md#buffering) and audio) might be preferable to avoid glitches and smooth the playback: ``` -scrcpy --display-buffer=200 --audio-buffer=200 +scrcpy --video-buffer=200 --audio-buffer=200 ``` It is also possible to configure another audio buffer (the audio output buffer), diff --git a/doc/develop.md b/doc/develop.md index e5274783..a094aa32 100644 --- a/doc/develop.md +++ b/doc/develop.md @@ -21,9 +21,9 @@ the client and on the server. If video is enabled, then the server sends a raw video stream (H.264 by default) of the device screen, with some additional headers for each packet. The client decodes the video frames, and displays them as soon as possible, without -buffering (unless `--display-buffer=delay` is specified) to minimize latency. -The client is not aware of the device rotation (which is handled by the server), -it just knows the dimensions of the video frames it receives. +buffering (unless `--video-buffer=delay` is specified) to minimize latency. The +client is not aware of the device rotation (which is handled by the server), it +just knows the dimensions of the video frames it receives. Similarly, if audio is enabled, then the server sends a raw audio stream (OPUS by default) of the device audio output (or the microphone if diff --git a/doc/video.md b/doc/video.md index ed92cb22..74ec74dd 100644 --- a/doc/video.md +++ b/doc/video.md @@ -189,15 +189,15 @@ The configuration is available independently for the display, [v4l2 sinks](video.md#video4linux) and [audio](audio.md#buffering) playback. ```bash -scrcpy --display-buffer=50 # add 50ms buffering for display -scrcpy --v4l2-buffer=300 # add 300ms buffering for v4l2 sink +scrcpy --video-buffer=50 # add 50ms buffering for video playback scrcpy --audio-buffer=200 # set 200ms buffering for audio playback +scrcpy --v4l2-buffer=300 # add 300ms buffering for v4l2 sink ``` They can be applied simultaneously: ```bash -scrcpy --display-buffer=50 --v4l2-buffer=300 +scrcpy --video-buffer=50 --v4l2-buffer=300 ``` From 04a3e6fb06377fc75eef805147e55f93877b3e86 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 28 Oct 2024 18:30:52 +0100 Subject: [PATCH 2033/2244] Consume reset request on encoding start If a reset request is pending when a new encoding starts, then it is implicitly fulfilled. PR #5415 --- .../main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java | 1 + 1 file changed, 1 insertion(+) diff --git a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java index 84bda1ce..6a58d791 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java @@ -72,6 +72,7 @@ public class SurfaceEncoder implements AsyncProcessor { boolean headerWritten = false; do { + capture.consumeReset(); // If a capture reset was requested, it is implicitly fulfilled capture.prepare(); Size size = capture.getSize(); if (!headerWritten) { From e26bdb07a21493d096ea5c8cfd870fc5a3f015dc Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 21 Oct 2024 18:25:08 +0200 Subject: [PATCH 2034/2244] Listen to display changed events Replace RotationWatcher and DisplayFoldListener by a single DisplayListener, which is notified whenever the display size or dpi changes. However, the DisplayListener mechanism is broken in the first versions of Android 14 (it is fixed in android-14.0.0_r29 by commit [1]), so continue to use the old mechanism specifically for Android 14 (where DisplayListener may be broken), until we receive the first "display changed" event (which proves that it works). [1]: Fixes #161 Fixes #1918 Fixes #4152 Fixes #5362 comment Refs #4469 PR #5415 Co-authored-by: Simon Chan <1330321+yume-chan@users.noreply.github.com> --- .../com/genymobile/scrcpy/device/Size.java | 2 +- .../scrcpy/video/ScreenCapture.java | 170 ++++++++++++++---- .../scrcpy/wrappers/DisplayManager.java | 69 +++++++ 3 files changed, 206 insertions(+), 35 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Size.java b/server/src/main/java/com/genymobile/scrcpy/device/Size.java index 230fd29e..558deb00 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/Size.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Size.java @@ -52,6 +52,6 @@ public final class Size { @Override public String toString() { - return "Size{" + "width=" + width + ", height=" + height + '}'; + return "Size{" + width + 'x' + height + '}'; } } diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java index 7e516909..d190bdde 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java @@ -7,12 +7,15 @@ import com.genymobile.scrcpy.device.DisplayInfo; import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.LogUtils; +import com.genymobile.scrcpy.wrappers.DisplayManager; import com.genymobile.scrcpy.wrappers.ServiceManager; import com.genymobile.scrcpy.wrappers.SurfaceControl; import android.graphics.Rect; import android.hardware.display.VirtualDisplay; import android.os.Build; +import android.os.Handler; +import android.os.HandlerThread; import android.os.IBinder; import android.view.IDisplayFoldListener; import android.view.IRotationWatcher; @@ -29,9 +32,19 @@ public class ScreenCapture extends SurfaceCapture { private DisplayInfo displayInfo; private ScreenInfo screenInfo; + // Source display size (before resizing/crop) for the current session + private Size sessionDisplaySize; + private IBinder display; private VirtualDisplay virtualDisplay; + private DisplayManager.DisplayListenerHandle displayListenerHandle; + private HandlerThread handlerThread; + + // On Android 14, the DisplayListener may be broken (it never sends events). This is fixed in recent Android 14 upgrades, but we can't really + // detect it directly, so register a RotationWatcher and a DisplayFoldListener as a fallback, until we receive the first event from + // DisplayListener (which proves that it works). + private boolean displayListenerWorks; // only accessed from the display listener thread private IRotationWatcher rotationWatcher; private IDisplayFoldListener displayFoldListener; @@ -45,39 +58,57 @@ public class ScreenCapture extends SurfaceCapture { @Override public void init() { - if (displayId == 0) { - rotationWatcher = new IRotationWatcher.Stub() { - @Override - public void onRotationChanged(int rotation) { - requestReset(); - } - }; - ServiceManager.getWindowManager().registerRotationWatcher(rotationWatcher, displayId); + if (Build.VERSION.SDK_INT == AndroidVersions.API_34_ANDROID_14) { + registerDisplayListenerFallbacks(); } - if (Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10) { - displayFoldListener = new IDisplayFoldListener.Stub() { - - private boolean first = true; - - @Override - public void onDisplayFoldChanged(int displayId, boolean folded) { - if (first) { - // An event is posted on registration to signal the initial state. Ignore it to avoid restarting encoding. - first = false; - return; - } - - if (ScreenCapture.this.displayId != displayId) { - // Ignore events related to other display ids - return; - } - - requestReset(); + handlerThread = new HandlerThread("DisplayListener"); + handlerThread.start(); + Handler handler = new Handler(handlerThread.getLooper()); + displayListenerHandle = ServiceManager.getDisplayManager().registerDisplayListener(displayId -> { + if (Ln.isEnabled(Ln.Level.VERBOSE)) { + Ln.v("ScreenCapture: onDisplayChanged(" + displayId + ")"); + } + if (Build.VERSION.SDK_INT == AndroidVersions.API_34_ANDROID_14) { + if (!displayListenerWorks) { + // On the first display listener event, we know it works, we can unregister the fallbacks + displayListenerWorks = true; + unregisterDisplayListenerFallbacks(); } - }; - ServiceManager.getWindowManager().registerDisplayFoldListener(displayFoldListener); - } + } + if (this.displayId == displayId) { + DisplayInfo di = ServiceManager.getDisplayManager().getDisplayInfo(displayId); + if (di == null) { + Ln.w("DisplayInfo for " + displayId + " cannot be retrieved"); + // We can't compare with the current size, so reset unconditionally + if (Ln.isEnabled(Ln.Level.VERBOSE)) { + Ln.v("ScreenCapture: requestReset(): " + getSessionDisplaySize() + " -> (unknown)"); + } + setSessionDisplaySize(null); + requestReset(); + } else { + Size size = di.getSize(); + + // The field is hidden on purpose, to read it with synchronization + @SuppressWarnings("checkstyle:HiddenField") + Size sessionDisplaySize = getSessionDisplaySize(); // synchronized + + // .equals() also works if sessionDisplaySize == null + if (!size.equals(sessionDisplaySize)) { + // Reset only if the size is different + if (Ln.isEnabled(Ln.Level.VERBOSE)) { + Ln.v("ScreenCapture: requestReset(): " + sessionDisplaySize + " -> " + size); + } + // Set the new size immediately, so that a future onDisplayChanged() event called before the asynchronous prepare() + // considers that the current size is the requested size (to avoid a duplicate requestReset()) + setSessionDisplaySize(size); + requestReset(); + } else if (Ln.isEnabled(Ln.Level.VERBOSE)) { + Ln.v("ScreenCapture: Size not changed (" + size + "): do not requestReset()"); + } + } + } + }, handler); } @Override @@ -92,6 +123,7 @@ public class ScreenCapture extends SurfaceCapture { Ln.w("Display doesn't have FLAG_SUPPORTS_PROTECTED_BUFFERS flag, mirroring can be restricted"); } + setSessionDisplaySize(displayInfo.getSize()); screenInfo = ScreenInfo.computeScreenInfo(displayInfo.getRotation(), displayInfo.getSize(), crop, maxSize, lockVideoOrientation); } @@ -146,12 +178,19 @@ public class ScreenCapture extends SurfaceCapture { @Override public void release() { - if (rotationWatcher != null) { - ServiceManager.getWindowManager().unregisterRotationWatcher(rotationWatcher); + if (Build.VERSION.SDK_INT == AndroidVersions.API_34_ANDROID_14) { + unregisterDisplayListenerFallbacks(); } - if (Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10) { - ServiceManager.getWindowManager().unregisterDisplayFoldListener(displayFoldListener); + + handlerThread.quitSafely(); + handlerThread = null; + + // displayListenerHandle may be null if registration failed + if (displayListenerHandle != null) { + ServiceManager.getDisplayManager().unregisterDisplayListener(displayListenerHandle); + displayListenerHandle = null; } + if (display != null) { SurfaceControl.destroyDisplay(display); display = null; @@ -191,4 +230,67 @@ public class ScreenCapture extends SurfaceCapture { SurfaceControl.closeTransaction(); } } + + private synchronized Size getSessionDisplaySize() { + return sessionDisplaySize; + } + + private synchronized void setSessionDisplaySize(Size sessionDisplaySize) { + this.sessionDisplaySize = sessionDisplaySize; + } + + private void registerDisplayListenerFallbacks() { + if (displayId == 0) { + rotationWatcher = new IRotationWatcher.Stub() { + @Override + public void onRotationChanged(int rotation) { + if (Ln.isEnabled(Ln.Level.VERBOSE)) { + Ln.v("ScreenCapture: onRotationChanged(" + rotation + ")"); + } + requestReset(); + } + }; + ServiceManager.getWindowManager().registerRotationWatcher(rotationWatcher, displayId); + } + + // Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10 (but implied by == API_34_ANDROID 14) + displayFoldListener = new IDisplayFoldListener.Stub() { + + private boolean first = true; + + @Override + public void onDisplayFoldChanged(int displayId, boolean folded) { + if (first) { + // An event is posted on registration to signal the initial state. Ignore it to avoid restarting encoding. + first = false; + return; + } + + if (Ln.isEnabled(Ln.Level.VERBOSE)) { + Ln.v("ScreenCapture: onDisplayFoldChanged(" + displayId + ", " + folded + ")"); + } + + if (ScreenCapture.this.displayId != displayId) { + // Ignore events related to other display ids + return; + } + requestReset(); + } + }; + ServiceManager.getWindowManager().registerDisplayFoldListener(displayFoldListener); + } + + private void unregisterDisplayListenerFallbacks() { + synchronized (this) { + if (rotationWatcher != null) { + ServiceManager.getWindowManager().unregisterRotationWatcher(rotationWatcher); + rotationWatcher = null; + } + if (displayFoldListener != null) { + // Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10 (but implied by == API_34_ANDROID 14) + ServiceManager.getWindowManager().unregisterDisplayFoldListener(displayFoldListener); + displayFoldListener = null; + } + } + } } 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 37d82c33..b497e97f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java @@ -11,17 +11,40 @@ import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.content.Context; import android.hardware.display.VirtualDisplay; +import android.os.Handler; import android.view.Display; import android.view.Surface; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.lang.reflect.Proxy; import java.util.regex.Matcher; import java.util.regex.Pattern; @SuppressLint("PrivateApi,DiscouragedPrivateApi") public final class DisplayManager { + + // android.hardware.display.DisplayManager.EVENT_FLAG_DISPLAY_CHANGED + public static final long EVENT_FLAG_DISPLAY_CHANGED = 1L << 2; + + public interface DisplayListener { + /** + * Called whenever the properties of a logical {@link android.view.Display}, + * such as size and density, have changed. + * + * @param displayId The id of the logical display that changed. + */ + void onDisplayChanged(int displayId); + } + + public static final class DisplayListenerHandle { + private final Object displayListenerProxy; + private DisplayListenerHandle(Object displayListenerProxy) { + this.displayListenerProxy = displayListenerProxy; + } + } + private final Object manager; // instance of hidden class android.hardware.display.DisplayManagerGlobal private Method createVirtualDisplayMethod; private Method requestDisplayPowerMethod; @@ -158,4 +181,50 @@ public final class DisplayManager { return false; } } + + public DisplayListenerHandle registerDisplayListener(DisplayListener listener, Handler handler) { + try { + Class displayListenerClass = Class.forName("android.hardware.display.DisplayManager$DisplayListener"); + Object displayListenerProxy = Proxy.newProxyInstance( + ClassLoader.getSystemClassLoader(), + new Class[] {displayListenerClass}, + (proxy, method, args) -> { + if ("onDisplayChanged".equals(method.getName())) { + listener.onDisplayChanged((int) args[0]); + } + return null; + }); + try { + manager.getClass() + .getMethod("registerDisplayListener", displayListenerClass, Handler.class, long.class, String.class) + .invoke(manager, displayListenerProxy, handler, EVENT_FLAG_DISPLAY_CHANGED, FakeContext.PACKAGE_NAME); + } catch (NoSuchMethodException e) { + try { + manager.getClass() + .getMethod("registerDisplayListener", displayListenerClass, Handler.class, long.class) + .invoke(manager, displayListenerProxy, handler, EVENT_FLAG_DISPLAY_CHANGED); + } catch (NoSuchMethodException e2) { + manager.getClass() + .getMethod("registerDisplayListener", displayListenerClass, Handler.class) + .invoke(manager, displayListenerProxy, handler); + } + } + + return new DisplayListenerHandle(displayListenerProxy); + } catch (Exception e) { + // Rotation and screen size won't be updated, not a fatal error + Ln.e("Could not register display listener", e); + } + + return null; + } + + public void unregisterDisplayListener(DisplayListenerHandle listener) { + try { + Class displayListenerClass = Class.forName("android.hardware.display.DisplayManager$DisplayListener"); + manager.getClass().getMethod("unregisterDisplayListener", displayListenerClass).invoke(manager, listener.displayListenerProxy); + } catch (Exception e) { + Ln.e("Could not unregister display listener", e); + } + } } From c7378f4dc843d24699349812661867c07af3954d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 31 Oct 2024 20:23:11 +0100 Subject: [PATCH 2035/2244] Extract setting display power to a separate method For consistency with the other actions. --- .../genymobile/scrcpy/control/Controller.java | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index 7add4ea9..76d62fa6 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -272,16 +272,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { break; case ControlMessage.TYPE_SET_DISPLAY_POWER: if (supportsInputEvents && displayId != Device.DISPLAY_ID_NONE) { - boolean on = msg.getOn(); - boolean setDisplayPowerOk = Device.setDisplayPower(displayId, on); - if (setDisplayPowerOk) { - keepDisplayPowerOff = !on; - Ln.i("Device display turned " + (on ? "on" : "off")); - if (cleanUp != null) { - boolean mustRestoreOnExit = !on; - cleanUp.setRestoreDisplayPower(mustRestoreOnExit); - } - } + setDisplayPower(msg.getOn()); } break; case ControlMessage.TYPE_ROTATE_DEVICE: @@ -677,4 +668,16 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { return data; } } + + private void setDisplayPower(boolean on) { + boolean setDisplayPowerOk = Device.setDisplayPower(displayId, on); + if (setDisplayPowerOk) { + keepDisplayPowerOff = !on; + Ln.i("Device display turned " + (on ? "on" : "off")); + if (cleanUp != null) { + boolean mustRestoreOnExit = !on; + cleanUp.setRestoreDisplayPower(mustRestoreOnExit); + } + } + } } From 3ac4b64461716ff472e62ef13b0947dfff98cb2e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 2 Nov 2024 18:40:23 +0100 Subject: [PATCH 2036/2244] Register rotation watcher for non-main displays While moving code, commit 874eaec487369f7fcaa9ed8c5f85569659565d4f added a condition `if (displayId == 0)` to register a rotation watcher, without good reasons. This condition was kept when the rotation watcher was moved to a fallback in e26bdb07a21493d096ea5c8cfd870fc5a3f015dc. Note: use `git show -b` to show this commit ignoring whitespace changes. Refs #5428 --- .../scrcpy/video/ScreenCapture.java | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java index d190bdde..04e42800 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java @@ -240,18 +240,16 @@ public class ScreenCapture extends SurfaceCapture { } private void registerDisplayListenerFallbacks() { - if (displayId == 0) { - rotationWatcher = new IRotationWatcher.Stub() { - @Override - public void onRotationChanged(int rotation) { - if (Ln.isEnabled(Ln.Level.VERBOSE)) { - Ln.v("ScreenCapture: onRotationChanged(" + rotation + ")"); - } - requestReset(); + rotationWatcher = new IRotationWatcher.Stub() { + @Override + public void onRotationChanged(int rotation) { + if (Ln.isEnabled(Ln.Level.VERBOSE)) { + Ln.v("ScreenCapture: onRotationChanged(" + rotation + ")"); } - }; - ServiceManager.getWindowManager().registerRotationWatcher(rotationWatcher, displayId); - } + requestReset(); + } + }; + ServiceManager.getWindowManager().registerRotationWatcher(rotationWatcher, displayId); // Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10 (but implied by == API_34_ANDROID 14) displayFoldListener = new IDisplayFoldListener.Stub() { From f08a6d86c5d84aa48c1c197d8f92f322768515cb Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 31 Oct 2024 20:19:58 +0100 Subject: [PATCH 2037/2244] Power on the device only for main display Power on the device on start only if scrcpy is mirroring the main display. --- .../src/main/java/com/genymobile/scrcpy/control/Controller.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index 76d62fa6..533faa68 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -166,7 +166,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { private void control() throws IOException { // on start, power on the device - if (powerOn && displayId != Device.DISPLAY_ID_NONE && !Device.isScreenOn()) { + if (powerOn && displayId == 0 && !Device.isScreenOn()) { Device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER, displayId, Device.INJECT_MODE_ASYNC); // dirty hack From c905fbba8d383af3a128390a95f26f9ab986486e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 3 Nov 2024 19:01:39 +0100 Subject: [PATCH 2038/2244] Fix indentation --- .../java/com/genymobile/scrcpy/video/NewDisplayCapture.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java index 8f507fdf..8c47ba43 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java @@ -93,8 +93,8 @@ public class NewDisplayCapture extends SurfaceCapture { | VIRTUAL_DISPLAY_FLAG_ALWAYS_UNLOCKED | VIRTUAL_DISPLAY_FLAG_TOUCH_FEEDBACK_DISABLED; if (Build.VERSION.SDK_INT >= AndroidVersions.API_34_ANDROID_14) { - flags |= VIRTUAL_DISPLAY_FLAG_OWN_FOCUS - | VIRTUAL_DISPLAY_FLAG_DEVICE_DISPLAY_GROUP; + flags |= VIRTUAL_DISPLAY_FLAG_OWN_FOCUS + | VIRTUAL_DISPLAY_FLAG_DEVICE_DISPLAY_GROUP; } } virtualDisplay = ServiceManager.getDisplayManager() From 1270997f6beb9574dc0ce46e4961f031ca7fb7c5 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 3 Nov 2024 19:02:57 +0100 Subject: [PATCH 2039/2244] Remove useless assignment The local variable virtualDisplayId was already initialized to the exact same value. --- .../main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java | 1 - 1 file changed, 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java index 8c47ba43..5cbdb792 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java @@ -107,7 +107,6 @@ public class NewDisplayCapture extends SurfaceCapture { } if (vdListener != null) { - virtualDisplayId = virtualDisplay.getDisplay().getDisplayId(); Rect contentRect = new Rect(0, 0, size.getWidth(), size.getHeight()); PositionMapper positionMapper = new PositionMapper(size, contentRect, 0); vdListener.onNewVirtualDisplay(virtualDisplayId, positionMapper); From 790ea5e58c97a8635ba0326b2b3268ef77bdaae0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 3 Nov 2024 12:33:33 +0100 Subject: [PATCH 2040/2244] Check screen on for current displayId Since Android 14, the "screen on" state can be checked per-display. Refs PR #5442 --- .../main/java/com/genymobile/scrcpy/CleanUp.java | 2 +- .../com/genymobile/scrcpy/control/Controller.java | 4 ++-- .../java/com/genymobile/scrcpy/device/Device.java | 7 ++++--- .../genymobile/scrcpy/wrappers/PowerManager.java | 14 ++++++++++++-- 4 files changed, 19 insertions(+), 8 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java index c8ee3ef4..343d854a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java +++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java @@ -137,7 +137,7 @@ public final class CleanUp { } } - if (Device.isScreenOn() && displayId != Device.DISPLAY_ID_NONE) { + if (displayId != Device.DISPLAY_ID_NONE && Device.isScreenOn(displayId)) { if (powerOffScreen) { Ln.i("Power off screen"); Device.powerOffScreen(displayId); diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index 533faa68..67a0115d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -166,7 +166,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { private void control() throws IOException { // on start, power on the device - if (powerOn && displayId == 0 && !Device.isScreenOn()) { + if (powerOn && displayId == 0 && !Device.isScreenOn(displayId)) { Device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER, displayId, Device.INJECT_MODE_ASYNC); // dirty hack @@ -490,7 +490,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { } private boolean pressBackOrTurnScreenOn(int action) { - if (Device.isScreenOn()) { + if (displayId == Device.DISPLAY_ID_NONE || Device.isScreenOn(displayId)) { return injectKeyEvent(action, KeyEvent.KEYCODE_BACK, 0, 0, Device.INJECT_MODE_ASYNC); } diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Device.java b/server/src/main/java/com/genymobile/scrcpy/device/Device.java index 5cd7a52a..e7a743f8 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Device.java @@ -82,8 +82,9 @@ public final class Device { && injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0, displayId, injectMode); } - public static boolean isScreenOn() { - return ServiceManager.getPowerManager().isScreenOn(); + public static boolean isScreenOn(int displayId) { + assert displayId != DISPLAY_ID_NONE; + return ServiceManager.getPowerManager().isScreenOn(displayId); } public static void expandNotificationPanel() { @@ -181,7 +182,7 @@ public final class Device { public static boolean powerOffScreen(int displayId) { assert displayId != DISPLAY_ID_NONE; - if (!isScreenOn()) { + if (!isScreenOn(displayId)) { return true; } return pressReleaseKeycode(KeyEvent.KEYCODE_POWER, displayId, Device.INJECT_MODE_ASYNC); diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java index f62e5b8e..b5fefdd8 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java @@ -1,7 +1,9 @@ package com.genymobile.scrcpy.wrappers; +import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.util.Ln; +import android.os.Build; import android.os.IInterface; import java.lang.reflect.Method; @@ -21,14 +23,22 @@ public final class PowerManager { private Method getIsScreenOnMethod() throws NoSuchMethodException { if (isScreenOnMethod == null) { - isScreenOnMethod = manager.getClass().getMethod("isInteractive"); + if (Build.VERSION.SDK_INT >= AndroidVersions.API_34_ANDROID_14) { + isScreenOnMethod = manager.getClass().getMethod("isDisplayInteractive", int.class); + } else { + isScreenOnMethod = manager.getClass().getMethod("isInteractive"); + } } return isScreenOnMethod; } - public boolean isScreenOn() { + public boolean isScreenOn(int displayId) { + try { Method method = getIsScreenOnMethod(); + if (Build.VERSION.SDK_INT >= AndroidVersions.API_34_ANDROID_14) { + return (boolean) method.invoke(manager, displayId); + } return (boolean) method.invoke(manager); } catch (ReflectiveOperationException e) { Ln.e("Could not invoke method", e); From 69b836930a1d33d2aeebd17eed67c264c084c37d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 31 Oct 2024 20:43:13 +0100 Subject: [PATCH 2041/2244] Handle capture reset via listener When the capture source becomes "invalid" (because the display size changes for example), a reset request is performed to restart the encoder. The reset state was stored in SurfaceCapture. The capture implementation set the flag, and the encoder consumed it. However, this mechanism did not allow a reset request to _interrupt_ the encoder, which may be waiting on a blocking call (until a new frame is produced). To be able to interrupt the encoder, a reset request must not only set a flag, but run a callback provided by the encoder. For that purpose, introduce the CaptureListener interface, which is notified by the SurfaceCapture implementation whenever the capture is invalidated. For now, the listener implementation just set a flag as before, so the behavior is unchanged. It lays the groundwork for the next commits. PR #5432 --- .../scrcpy/video/CameraCapture.java | 4 +-- .../genymobile/scrcpy/video/CaptureReset.java | 21 +++++++++++++ .../scrcpy/video/NewDisplayCapture.java | 2 +- .../scrcpy/video/ScreenCapture.java | 8 ++--- .../scrcpy/video/SurfaceCapture.java | 31 ++++++++++--------- .../scrcpy/video/SurfaceEncoder.java | 10 +++--- 6 files changed, 50 insertions(+), 26 deletions(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/video/CaptureReset.java diff --git a/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java index a5fa4b06..92663f79 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java @@ -68,7 +68,7 @@ public class CameraCapture extends SurfaceCapture { } @Override - public void init() throws IOException { + protected void init() throws IOException { cameraThread = new HandlerThread("camera"); cameraThread.start(); cameraHandler = new Handler(cameraThread.getLooper()); @@ -256,7 +256,7 @@ public class CameraCapture extends SurfaceCapture { public void onDisconnected(CameraDevice camera) { Ln.w("Camera disconnected"); disconnected.set(true); - requestReset(); + invalidate(); } @Override diff --git a/server/src/main/java/com/genymobile/scrcpy/video/CaptureReset.java b/server/src/main/java/com/genymobile/scrcpy/video/CaptureReset.java new file mode 100644 index 00000000..20256d1e --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/video/CaptureReset.java @@ -0,0 +1,21 @@ +package com.genymobile.scrcpy.video; + +import java.util.concurrent.atomic.AtomicBoolean; + +public class CaptureReset implements SurfaceCapture.CaptureListener { + + private final AtomicBoolean reset = new AtomicBoolean(); + + public boolean consumeReset() { + return reset.getAndSet(false); + } + + public void reset() { + reset.set(true); + } + + @Override + public void onInvalidated() { + reset(); + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java index 5cbdb792..5d61c4bd 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java @@ -46,7 +46,7 @@ public class NewDisplayCapture extends SurfaceCapture { } @Override - public void init() { + protected void init() { size = newDisplay.getSize(); dpi = newDisplay.getDpi(); if (size == null || dpi == 0) { diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java index 04e42800..c0d49f60 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java @@ -85,7 +85,7 @@ public class ScreenCapture extends SurfaceCapture { Ln.v("ScreenCapture: requestReset(): " + getSessionDisplaySize() + " -> (unknown)"); } setSessionDisplaySize(null); - requestReset(); + invalidate(); } else { Size size = di.getSize(); @@ -102,7 +102,7 @@ public class ScreenCapture extends SurfaceCapture { // Set the new size immediately, so that a future onDisplayChanged() event called before the asynchronous prepare() // considers that the current size is the requested size (to avoid a duplicate requestReset()) setSessionDisplaySize(size); - requestReset(); + invalidate(); } else if (Ln.isEnabled(Ln.Level.VERBOSE)) { Ln.v("ScreenCapture: Size not changed (" + size + "): do not requestReset()"); } @@ -246,7 +246,7 @@ public class ScreenCapture extends SurfaceCapture { if (Ln.isEnabled(Ln.Level.VERBOSE)) { Ln.v("ScreenCapture: onRotationChanged(" + rotation + ")"); } - requestReset(); + invalidate(); } }; ServiceManager.getWindowManager().registerRotationWatcher(rotationWatcher, displayId); @@ -272,7 +272,7 @@ public class ScreenCapture extends SurfaceCapture { // Ignore events related to other display ids return; } - requestReset(); + invalidate(); } }; ServiceManager.getWindowManager().registerDisplayFoldListener(displayFoldListener); diff --git a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java index 0ee93c92..172bd78f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java @@ -6,36 +6,37 @@ import com.genymobile.scrcpy.device.Size; import android.view.Surface; import java.io.IOException; -import java.util.concurrent.atomic.AtomicBoolean; /** * A video source which can be rendered on a Surface for encoding. */ public abstract class SurfaceCapture { - private final AtomicBoolean resetCapture = new AtomicBoolean(); - - /** - * Request the encoding session to be restarted, for example if the capture implementation detects that the video source size has changed (on - * device rotation for example). - */ - protected void requestReset() { - resetCapture.set(true); + public interface CaptureListener { + void onInvalidated(); } + private CaptureListener listener; + /** - * Consume the reset request (intended to be called by the encoder). - * - * @return {@code true} if a reset request was pending, {@code false} otherwise. + * Notify the listener that the capture has been invalidated (for example, because its size changed). */ - public boolean consumeReset() { - return resetCapture.getAndSet(false); + protected void invalidate() { + listener.onInvalidated(); } /** * Called once before the first capture starts. */ - public abstract void init() throws ConfigurationException, IOException; + public final void init(CaptureListener listener) throws ConfigurationException, IOException { + this.listener = listener; + init(); + } + + /** + * Called once before the first capture starts. + */ + protected abstract void init() throws ConfigurationException, IOException; /** * Called after the last capture ends (if and only if {@link #init()} has been called). diff --git a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java index 6a58d791..3a1c481e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java @@ -49,6 +49,8 @@ public class SurfaceEncoder implements AsyncProcessor { private Thread thread; private final AtomicBoolean stopped = new AtomicBoolean(); + private final CaptureReset reset = new CaptureReset(); + public SurfaceEncoder(SurfaceCapture capture, Streamer streamer, int videoBitRate, float maxFps, List codecOptions, String encoderName, boolean downsizeOnError) { this.capture = capture; @@ -65,14 +67,14 @@ public class SurfaceEncoder implements AsyncProcessor { MediaCodec mediaCodec = createMediaCodec(codec, encoderName); MediaFormat format = createFormat(codec.getMimeType(), videoBitRate, maxFps, codecOptions); - capture.init(); + capture.init(reset); try { boolean alive; boolean headerWritten = false; do { - capture.consumeReset(); // If a capture reset was requested, it is implicitly fulfilled + reset.consumeReset(); // If a capture reset was requested, it is implicitly fulfilled capture.prepare(); Size size = capture.getSize(); if (!headerWritten) { @@ -168,14 +170,14 @@ public class SurfaceEncoder implements AsyncProcessor { boolean alive = true; MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); - while (!capture.consumeReset() && !eof) { + while (!reset.consumeReset() && !eof) { if (stopped.get()) { alive = false; break; } int outputBufferId = codec.dequeueOutputBuffer(bufferInfo, -1); try { - if (capture.consumeReset()) { + if (reset.consumeReset()) { // must restart encoding with new size break; } From 9958302e6f8780c7aa547186c70b493137986701 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 31 Oct 2024 21:42:58 +0100 Subject: [PATCH 2042/2244] Interrupt MediaCodec blocking call on reset When the MediaCodec input is a Surface, no EOS (end-of-stream) will never occur automatically: it may only be triggered manually by MediaCodec.signalEndOfInputStream(). Use this signal to interrupt the blocking call to dequeueOutputBuffer() immediately on reset, without waiting for the next frame to be dequeued. PR #5432 --- .../genymobile/scrcpy/video/CaptureReset.java | 14 +++++- .../scrcpy/video/SurfaceEncoder.java | 48 +++++++++---------- 2 files changed, 37 insertions(+), 25 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/video/CaptureReset.java b/server/src/main/java/com/genymobile/scrcpy/video/CaptureReset.java index 20256d1e..c11e2e80 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/CaptureReset.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/CaptureReset.java @@ -1,17 +1,29 @@ package com.genymobile.scrcpy.video; +import android.media.MediaCodec; + import java.util.concurrent.atomic.AtomicBoolean; public class CaptureReset implements SurfaceCapture.CaptureListener { private final AtomicBoolean reset = new AtomicBoolean(); + // Current instance of MediaCodec to "interrupt" on reset + private MediaCodec runningMediaCodec; + public boolean consumeReset() { return reset.getAndSet(false); } - public void reset() { + public synchronized void reset() { reset.set(true); + if (runningMediaCodec != null) { + runningMediaCodec.signalEndOfInputStream(); + } + } + + public synchronized void setRunningMediaCodec(MediaCodec runningMediaCodec) { + this.runningMediaCodec = runningMediaCodec; } @Override diff --git a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java index 3a1c481e..8fadfa7b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java @@ -94,7 +94,21 @@ public class SurfaceEncoder implements AsyncProcessor { mediaCodec.start(); - alive = encode(mediaCodec, streamer); + // Set the MediaCodec instance to "interrupt" (by signaling an EOS) on reset + reset.setRunningMediaCodec(mediaCodec); + + if (stopped.get()) { + alive = false; + } else { + boolean resetRequested = reset.consumeReset(); + if (!resetRequested) { + // If a reset is requested during encode(), it will interrupt the encoding by an EOS + encode(mediaCodec, streamer); + } + // The capture might have been closed internally (for example if the camera is disconnected) + alive = !stopped.get() && !capture.isClosed(); + } + // do not call stop() on exception, it would trigger an IllegalStateException mediaCodec.stop(); } catch (IllegalStateException | IllegalArgumentException e) { @@ -105,6 +119,7 @@ public class SurfaceEncoder implements AsyncProcessor { Ln.i("Retrying..."); alive = true; } finally { + reset.setRunningMediaCodec(null); mediaCodec.reset(); if (surface != null) { surface.release(); @@ -165,25 +180,16 @@ public class SurfaceEncoder implements AsyncProcessor { return 0; } - private boolean encode(MediaCodec codec, Streamer streamer) throws IOException { - boolean eof = false; - boolean alive = true; + private void encode(MediaCodec codec, Streamer streamer) throws IOException { MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); - while (!reset.consumeReset() && !eof) { - if (stopped.get()) { - alive = false; - break; - } + boolean eos; + do { int outputBufferId = codec.dequeueOutputBuffer(bufferInfo, -1); try { - if (reset.consumeReset()) { - // must restart encoding with new size - break; - } - - eof = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0; - if (outputBufferId >= 0) { + eos = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0; + // On EOS, there might be data or not, depending on bufferInfo.size + if (outputBufferId >= 0 && bufferInfo.size > 0) { ByteBuffer codecBuffer = codec.getOutputBuffer(outputBufferId); boolean isConfig = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0; @@ -200,14 +206,7 @@ public class SurfaceEncoder implements AsyncProcessor { codec.releaseOutputBuffer(outputBufferId, false); } } - } - - if (capture.isClosed()) { - // The capture might have been closed internally (for example if the camera is disconnected) - alive = false; - } - - return !eof && alive; + } while (!eos); } private static MediaCodec createMediaCodec(Codec codec, String encoderName) throws IOException, ConfigurationException { @@ -300,6 +299,7 @@ public class SurfaceEncoder implements AsyncProcessor { public void stop() { if (thread != null) { stopped.set(true); + reset.reset(); } } From 104195fc3bdcd69481ede5ff2808be0a1f1d6e5e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 31 Oct 2024 22:47:35 +0100 Subject: [PATCH 2043/2244] Add shortcut to reset video capture/encoding Reset video capture/encoding on MOD+Shift+r. Like on device rotation, this starts a new encoding session which produces a video stream starting by a key frame. PR #5432 --- app/scrcpy.1 | 4 ++++ app/src/cli.c | 4 ++++ app/src/control_msg.c | 4 ++++ app/src/control_msg.h | 1 + app/src/input_manager.c | 20 ++++++++++++++-- app/tests/test_control_msg_serialize.c | 16 +++++++++++++ doc/shortcuts.md | 1 + .../java/com/genymobile/scrcpy/Server.java | 4 ++++ .../scrcpy/control/ControlMessage.java | 1 + .../scrcpy/control/ControlMessageReader.java | 1 + .../genymobile/scrcpy/control/Controller.java | 18 +++++++++++++++ .../scrcpy/video/CameraCapture.java | 5 ++++ .../scrcpy/video/NewDisplayCapture.java | 23 ++++++++++++++----- .../scrcpy/video/ScreenCapture.java | 5 ++++ .../scrcpy/video/SurfaceCapture.java | 7 ++++++ 15 files changed, 106 insertions(+), 8 deletions(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index f517ad4c..76e36dcb 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -671,6 +671,10 @@ Pause or re-pause display .B MOD+Shift+z Unpause display +.TP +.B MOD+Shift+r +Reset video capture/encoding + .TP .B MOD+g Resize window to 1:1 (pixel\-perfect) diff --git a/app/src/cli.c b/app/src/cli.c index 77747e93..7cc68085 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -1022,6 +1022,10 @@ static const struct sc_shortcut shortcuts[] = { .shortcuts = { "MOD+Shift+z" }, .text = "Unpause display", }, + { + .shortcuts = { "MOD+Shift+r" }, + .text = "Reset video capture/encoding", + }, { .shortcuts = { "MOD+g" }, .text = "Resize window to 1:1 (pixel-perfect)", diff --git a/app/src/control_msg.c b/app/src/control_msg.c index e04fbd3c..0defda92 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -181,6 +181,7 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) { case SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS: case SC_CONTROL_MSG_TYPE_ROTATE_DEVICE: case SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS: + case SC_CONTROL_MSG_TYPE_RESET_VIDEO: // no additional data return 1; default: @@ -304,6 +305,9 @@ sc_control_msg_log(const struct sc_control_msg *msg) { case SC_CONTROL_MSG_TYPE_START_APP: LOG_CMSG("start app \"%s\"", msg->start_app.name); break; + case SC_CONTROL_MSG_TYPE_RESET_VIDEO: + LOG_CMSG("reset video"); + break; default: LOG_CMSG("unknown type: %u", (unsigned) msg->type); break; diff --git a/app/src/control_msg.h b/app/src/control_msg.h index 9eef7e82..f0a2e373 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -42,6 +42,7 @@ enum sc_control_msg_type { SC_CONTROL_MSG_TYPE_UHID_DESTROY, SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS, SC_CONTROL_MSG_TYPE_START_APP, + SC_CONTROL_MSG_TYPE_RESET_VIDEO, }; enum sc_copy_key { diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 140b50ac..3955c211 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -284,6 +284,18 @@ open_hard_keyboard_settings(struct sc_input_manager *im) { } } +static void +reset_video(struct sc_input_manager *im) { + assert(im->controller); + + struct sc_control_msg msg; + msg.type = SC_CONTROL_MSG_TYPE_RESET_VIDEO; + + if (!sc_controller_push_msg(im->controller, &msg)) { + LOGW("Could not request reset video"); + } +} + static void apply_orientation_transform(struct sc_input_manager *im, enum sc_orientation transform) { @@ -521,8 +533,12 @@ sc_input_manager_process_key(struct sc_input_manager *im, } return; case SDLK_r: - if (control && !shift && !repeat && down && !paused) { - rotate_device(im); + if (control && !repeat && down && !paused) { + if (shift) { + reset_video(im); + } else { + rotate_device(im); + } } return; case SDLK_k: diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c index 73bca901..9adf2a3d 100644 --- a/app/tests/test_control_msg_serialize.c +++ b/app/tests/test_control_msg_serialize.c @@ -407,6 +407,21 @@ static void test_serialize_open_hard_keyboard(void) { assert(!memcmp(buf, expected, sizeof(expected))); } +static void test_serialize_reset_video(void) { + struct sc_control_msg msg = { + .type = SC_CONTROL_MSG_TYPE_RESET_VIDEO, + }; + + uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; + size_t size = sc_control_msg_serialize(&msg, buf); + assert(size == 1); + + const uint8_t expected[] = { + SC_CONTROL_MSG_TYPE_RESET_VIDEO, + }; + assert(!memcmp(buf, expected, sizeof(expected))); +} + int main(int argc, char *argv[]) { (void) argc; (void) argv; @@ -429,5 +444,6 @@ int main(int argc, char *argv[]) { test_serialize_uhid_input(); test_serialize_uhid_destroy(); test_serialize_open_hard_keyboard(); + test_serialize_reset_video(); return 0; } diff --git a/doc/shortcuts.md b/doc/shortcuts.md index 4ea37257..d22eb473 100644 --- a/doc/shortcuts.md +++ b/doc/shortcuts.md @@ -30,6 +30,7 @@ _[Super] is typically the Windows or Cmd key._ | Flip display vertically | MOD+Shift+ _(up)_ \| MOD+Shift+ _(down)_ | Pause or re-pause display | MOD+z | Unpause display | MOD+Shift+z + | Reset video capture/encoding | MOD+Shift+r | Resize window to 1:1 (pixel-perfect) | MOD+g | Resize window to remove black borders | MOD+w \| _Double-left-click¹_ | Click on `HOME` | MOD+h \| _Middle-click_ diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index a0a48806..e0adeea0 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -225,6 +225,10 @@ public final class Server { SurfaceEncoder surfaceEncoder = new SurfaceEncoder(surfaceCapture, videoStreamer, options.getVideoBitRate(), options.getMaxFps(), options.getVideoCodecOptions(), options.getVideoEncoder(), options.getDownsizeOnError()); asyncProcessors.add(surfaceEncoder); + + if (controller != null) { + controller.setSurfaceCapture(surfaceCapture); + } } Completion completion = new Completion(asyncProcessors.size()); diff --git a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java index eec5f67f..7455cdf8 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java @@ -24,6 +24,7 @@ public final class ControlMessage { public static final int TYPE_UHID_DESTROY = 14; public static final int TYPE_OPEN_HARD_KEYBOARD_SETTINGS = 15; public static final int TYPE_START_APP = 16; + public static final int TYPE_RESET_VIDEO = 17; public static final long SEQUENCE_INVALID = 0; diff --git a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java index ae167690..b82615ed 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java @@ -46,6 +46,7 @@ public class ControlMessageReader { case ControlMessage.TYPE_COLLAPSE_PANELS: case ControlMessage.TYPE_ROTATE_DEVICE: case ControlMessage.TYPE_OPEN_HARD_KEYBOARD_SETTINGS: + case ControlMessage.TYPE_RESET_VIDEO: return ControlMessage.createEmpty(type); case ControlMessage.TYPE_UHID_CREATE: return parseUhidCreate(); diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index 67a0115d..b4ae07b6 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -9,6 +9,7 @@ import com.genymobile.scrcpy.device.Point; import com.genymobile.scrcpy.device.Position; import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.LogUtils; +import com.genymobile.scrcpy.video.SurfaceCapture; import com.genymobile.scrcpy.video.VirtualDisplayListener; import com.genymobile.scrcpy.wrappers.ClipboardManager; import com.genymobile.scrcpy.wrappers.InputManager; @@ -93,6 +94,9 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { private boolean keepDisplayPowerOff; + // Used for resetting video encoding on RESET_VIDEO message + private SurfaceCapture surfaceCapture; + public Controller(int displayId, ControlChannel controlChannel, CleanUp cleanUp, boolean clipboardAutosync, boolean powerOn) { this.displayId = displayId; this.controlChannel = controlChannel; @@ -143,6 +147,10 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { } } + public void setSurfaceCapture(SurfaceCapture surfaceCapture) { + this.surfaceCapture = surfaceCapture; + } + private UhidManager getUhidManager() { if (uhidManager == null) { uhidManager = new UhidManager(sender); @@ -293,6 +301,9 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { case ControlMessage.TYPE_START_APP: startAppAsync(msg.getText()); break; + case ControlMessage.TYPE_RESET_VIDEO: + resetVideo(); + break; default: // do nothing } @@ -680,4 +691,11 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { } } } + + private void resetVideo() { + if (surfaceCapture != null) { + Ln.i("Video capture reset"); + surfaceCapture.requestInvalidate(); + } + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java index 92663f79..7385283e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java @@ -355,4 +355,9 @@ public class CameraCapture extends SurfaceCapture { public boolean isClosed() { return disconnected.get(); } + + @Override + public void requestInvalidate() { + // do nothing (the user could not request a reset anyway for now, since there is no controller for camera mirroring) + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java index 5d61c4bd..f6561a5f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java @@ -14,6 +14,8 @@ import android.hardware.display.VirtualDisplay; import android.os.Build; import android.view.Surface; +import java.io.IOException; + public class NewDisplayCapture extends SurfaceCapture { // Internal fields copied from android.hardware.display.DisplayManager @@ -72,13 +74,8 @@ public class NewDisplayCapture extends SurfaceCapture { } } - @Override - public void start(Surface surface) { - if (virtualDisplay != null) { - virtualDisplay.release(); - virtualDisplay = null; - } + public void startNew(Surface surface) { int virtualDisplayId; try { int flags = DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC @@ -113,6 +110,15 @@ public class NewDisplayCapture extends SurfaceCapture { } } + @Override + public void start(Surface surface) throws IOException { + if (virtualDisplay == null) { + startNew(surface); + } else { + virtualDisplay.setSurface(surface); + } + } + @Override public void release() { if (virtualDisplay != null) { @@ -142,4 +148,9 @@ public class NewDisplayCapture extends SurfaceCapture { int num = size.getMax(); return initialDpi * num / den; } + + @Override + public void requestInvalidate() { + invalidate(); + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java index c0d49f60..48e594b7 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java @@ -291,4 +291,9 @@ public class ScreenCapture extends SurfaceCapture { } } } + + @Override + public void requestInvalidate() { + invalidate(); + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java index 172bd78f..de9e1b27 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java @@ -79,4 +79,11 @@ public abstract class SurfaceCapture { public boolean isClosed() { return false; } + + /** + * Manually request to invalidate (typically a user request). + *

+ * The capture implementation is free to ignore the request and do nothing. + */ + public abstract void requestInvalidate(); } From e9dd0f68adc59bae67f04bc859a8287785771ace Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 4 Nov 2024 23:06:19 +0100 Subject: [PATCH 2044/2244] Fix audio regulator compensation A call to swr_set_compensation() configures the resampler to drop or duplicate "diff" samples over an interval of "distance" samples. If the function is not called again, then after "distance" samples, no more compensation will be applied. So it must always be called, even if the new computed diff value happens to be the same as the previous one. In practice, it is unlikely that the diff value is exactly the same every second, except when it is actively clamped (to 2% of the sample rate). --- app/src/audio_regulator.c | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/app/src/audio_regulator.c b/app/src/audio_regulator.c index 911b2bfa..fb0c3758 100644 --- a/app/src/audio_regulator.c +++ b/app/src/audio_regulator.c @@ -309,14 +309,12 @@ sc_audio_regulator_push(struct sc_audio_regulator *ar, const AVFrame *frame) { LOGV("[Audio] Buffering: target=%" PRIu32 " avg=%f cur=%" PRIu32 " compensation=%d", ar->target_buffering, avg, can_read, diff); - if (diff != ar->compensation) { - int ret = swr_set_compensation(swr_ctx, diff, distance); - if (ret < 0) { - LOGW("Resampling compensation failed: %d", ret); - // not fatal - } else { - ar->compensation = diff; - } + int ret = swr_set_compensation(swr_ctx, diff, distance); + if (ret < 0) { + LOGW("Resampling compensation failed: %d", ret); + // not fatal + } else { + ar->compensation = diff; } } From 5936167ff77b543a533c7f6a5fa42d78ca5d724f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 4 Nov 2024 23:17:43 +0100 Subject: [PATCH 2045/2244] Store compensation state as a boolean We don't need to store the last compensation value anymore, we just need to know if it's non-zero. --- app/src/audio_regulator.c | 6 +++--- app/src/audio_regulator.h | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/audio_regulator.c b/app/src/audio_regulator.c index fb0c3758..3e4f78ad 100644 --- a/app/src/audio_regulator.c +++ b/app/src/audio_regulator.c @@ -288,7 +288,7 @@ sc_audio_regulator_push(struct sc_audio_regulator *ar, const AVFrame *frame) { // Enable compensation when the difference exceeds +/- 4ms. // Disable compensation when the difference is lower than +/- 1ms. - int threshold = ar->compensation != 0 + int threshold = ar->compensation_active ? ar->sample_rate / 1000 /* 1ms */ : ar->sample_rate * 4 / 1000; /* 4ms */ @@ -314,7 +314,7 @@ sc_audio_regulator_push(struct sc_audio_regulator *ar, const AVFrame *frame) { LOGW("Resampling compensation failed: %d", ret); // not fatal } else { - ar->compensation = diff; + ar->compensation_active = diff != 0; } } @@ -390,7 +390,7 @@ sc_audio_regulator_init(struct sc_audio_regulator *ar, size_t sample_size, atomic_init(&ar->played, false); atomic_init(&ar->received, false); atomic_init(&ar->underflow, 0); - ar->compensation = 0; + ar->compensation_active = false; return true; diff --git a/app/src/audio_regulator.h b/app/src/audio_regulator.h index 7daa1b05..1c0eeb9f 100644 --- a/app/src/audio_regulator.h +++ b/app/src/audio_regulator.h @@ -44,8 +44,8 @@ struct sc_audio_regulator { // Number of silence samples inserted since the last received packet atomic_uint_least32_t underflow; - // Current applied compensation value (only used by the receiver thread) - int compensation; + // Non-zero compensation applied (only used by the receiver thread) + bool compensation_active; // Set to true the first time a sample is received atomic_bool received; From d3db9c40653d4c1eaafda1ece2abe57acad94906 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 3 Nov 2024 22:34:02 +0100 Subject: [PATCH 2046/2244] Refactor clean up configuration to simplify All options were configured dynamically by sending a single byte to an output stream. But in practice, only the power mode must be changed dynamically, the others are configured once on start. For simplicity, pass the value of static options as command line arguments, and handle dynamic options in a loop only from a separate thread once the clean up process is started. This will allow to easily add cleanup options with values which do not fit in 1 byte. Also handle the clean up thread (and the loading of initial settings values) from the CleanUp class, to expose a simpler clean up API. Refs 9efa162949c2a3e3e42564862ff390700270394d PR #5447 --- .../java/com/genymobile/scrcpy/CleanUp.java | 164 +++++++++++------- .../java/com/genymobile/scrcpy/Server.java | 66 +------ 2 files changed, 106 insertions(+), 124 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java index 343d854a..a8a5784d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java +++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java @@ -5,6 +5,8 @@ import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.Settings; import com.genymobile.scrcpy.util.SettingsException; +import android.os.BatteryManager; + import java.io.File; import java.io.IOException; import java.io.OutputStream; @@ -16,59 +18,110 @@ import java.io.OutputStream; */ public final class CleanUp { - private static final int MSG_TYPE_MASK = 0b11; - private static final int MSG_TYPE_RESTORE_STAY_ON = 0; - private static final int MSG_TYPE_DISABLE_SHOW_TOUCHES = 1; - private static final int MSG_TYPE_RESTORE_DISPLAY_POWER = 2; - private static final int MSG_TYPE_POWER_OFF_SCREEN = 3; + // Dynamic options + private static final int PENDING_CHANGE_DISPLAY_POWER = 1 << 0; + private int pendingChanges; + private boolean pendingRestoreDisplayPower; - private static final int MSG_PARAM_SHIFT = 2; + private Thread thread; - private final OutputStream out; - - public CleanUp(OutputStream out) { - this.out = out; + private CleanUp(int displayId, Options options) { + thread = new Thread(() -> runCleanUp(displayId, options), "cleanup"); + thread.start(); } - public static CleanUp configure(int displayId) throws IOException { - String[] cmd = {"app_process", "/", CleanUp.class.getName(), String.valueOf(displayId)}; + public static CleanUp start(int displayId, Options options) { + return new CleanUp(displayId, options); + } + + public void interrupt() { + thread.interrupt(); + } + + public void join() throws InterruptedException { + thread.join(); + } + + private void runCleanUp(int displayId, Options options) { + boolean disableShowTouches = false; + if (options.getShowTouches()) { + try { + String oldValue = Settings.getAndPutValue(Settings.TABLE_SYSTEM, "show_touches", "1"); + // If "show touches" was disabled, it must be disabled back on clean up + disableShowTouches = !"1".equals(oldValue); + } catch (SettingsException e) { + Ln.e("Could not change \"show_touches\"", e); + } + } + + int restoreStayOn = -1; + if (options.getStayAwake()) { + int stayOn = BatteryManager.BATTERY_PLUGGED_AC | BatteryManager.BATTERY_PLUGGED_USB | BatteryManager.BATTERY_PLUGGED_WIRELESS; + try { + String oldValue = Settings.getAndPutValue(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(stayOn)); + try { + int currentStayOn = Integer.parseInt(oldValue); + // Restore only if the current value is different + if (currentStayOn != stayOn) { + restoreStayOn = currentStayOn; + } + } catch (NumberFormatException e) { + // ignore + } + } catch (SettingsException e) { + Ln.e("Could not change \"stay_on_while_plugged_in\"", e); + } + } + + boolean powerOffScreen = options.getPowerOffScreenOnClose(); + + try { + run(displayId, restoreStayOn, disableShowTouches, powerOffScreen); + } catch (InterruptedException e) { + // ignore + } catch (IOException e) { + Ln.e("Clean up I/O exception", e); + } + } + + private void run(int displayId, int restoreStayOn, boolean disableShowTouches, boolean powerOffScreen) throws IOException, InterruptedException { + String[] cmd = { + "app_process", + "/", + CleanUp.class.getName(), + String.valueOf(displayId), + String.valueOf(restoreStayOn), + String.valueOf(disableShowTouches), + String.valueOf(powerOffScreen), + }; ProcessBuilder builder = new ProcessBuilder(cmd); builder.environment().put("CLASSPATH", Server.SERVER_PATH); Process process = builder.start(); - return new CleanUp(process.getOutputStream()); - } + OutputStream out = process.getOutputStream(); - private boolean sendMessage(int type, int param) { - assert (type & ~MSG_TYPE_MASK) == 0; - int msg = type | param << MSG_PARAM_SHIFT; - try { - out.write(msg); - out.flush(); - return true; - } catch (IOException e) { - Ln.w("Could not configure cleanup (type=" + type + ", param=" + param + ")", e); - return false; + while (true) { + int localPendingChanges; + boolean localPendingRestoreDisplayPower; + synchronized (this) { + while (pendingChanges == 0) { + wait(); + } + localPendingChanges = pendingChanges; + localPendingRestoreDisplayPower = pendingRestoreDisplayPower; + pendingChanges = 0; + } + if ((localPendingChanges & PENDING_CHANGE_DISPLAY_POWER) != 0) { + out.write(localPendingRestoreDisplayPower ? 1 : 0); + out.flush(); + } } } - public boolean setRestoreStayOn(int restoreValue) { - // Restore the value (between 0 and 7), -1 to not restore - // - assert restoreValue >= -1 && restoreValue <= 7; - return sendMessage(MSG_TYPE_RESTORE_STAY_ON, restoreValue & 0b1111); - } - - public boolean setDisableShowTouches(boolean disableOnExit) { - return sendMessage(MSG_TYPE_DISABLE_SHOW_TOUCHES, disableOnExit ? 1 : 0); - } - - public boolean setRestoreDisplayPower(boolean restoreOnExit) { - return sendMessage(MSG_TYPE_RESTORE_DISPLAY_POWER, restoreOnExit ? 1 : 0); - } - - public boolean setPowerOffScreen(boolean powerOffScreenOnExit) { - return sendMessage(MSG_TYPE_POWER_OFF_SCREEN, powerOffScreenOnExit ? 1 : 0); + public synchronized void setRestoreDisplayPower(boolean restoreDisplayPower) { + pendingRestoreDisplayPower = restoreDisplayPower; + pendingChanges |= PENDING_CHANGE_DISPLAY_POWER; + notify(); } public static void unlinkSelf() { @@ -83,35 +136,20 @@ public final class CleanUp { unlinkSelf(); int displayId = Integer.parseInt(args[0]); + int restoreStayOn = Integer.parseInt(args[1]); + boolean disableShowTouches = Boolean.parseBoolean(args[2]); + boolean powerOffScreen = Boolean.parseBoolean(args[3]); - int restoreStayOn = -1; - boolean disableShowTouches = false; + // Dynamic option boolean restoreDisplayPower = false; - boolean powerOffScreen = false; try { // Wait for the server to die int msg; while ((msg = System.in.read()) != -1) { - int type = msg & MSG_TYPE_MASK; - int param = msg >> MSG_PARAM_SHIFT; - switch (type) { - case MSG_TYPE_RESTORE_STAY_ON: - restoreStayOn = param > 7 ? -1 : param; - break; - case MSG_TYPE_DISABLE_SHOW_TOUCHES: - disableShowTouches = param != 0; - break; - case MSG_TYPE_RESTORE_DISPLAY_POWER: - restoreDisplayPower = param != 0; - break; - case MSG_TYPE_POWER_OFF_SCREEN: - powerOffScreen = param != 0; - break; - default: - Ln.w("Unexpected msg type: " + type); - break; - } + // Only restore display power + assert msg == 0 || msg == 1; + restoreDisplayPower = msg != 0; } } catch (IOException e) { // Expected when the server is dead diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index e0adeea0..a093fdf0 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -16,8 +16,6 @@ import com.genymobile.scrcpy.device.NewDisplay; import com.genymobile.scrcpy.device.Streamer; import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.LogUtils; -import com.genymobile.scrcpy.util.Settings; -import com.genymobile.scrcpy.util.SettingsException; import com.genymobile.scrcpy.video.CameraCapture; import com.genymobile.scrcpy.video.NewDisplayCapture; import com.genymobile.scrcpy.video.ScreenCapture; @@ -25,7 +23,6 @@ import com.genymobile.scrcpy.video.SurfaceCapture; import com.genymobile.scrcpy.video.SurfaceEncoder; import com.genymobile.scrcpy.video.VideoSource; -import android.os.BatteryManager; import android.os.Build; import java.io.File; @@ -76,51 +73,6 @@ public final class Server { // not instantiable } - private static void initAndCleanUp(Options options, CleanUp cleanUp) { - // This method is called from its own thread, so it may only configure cleanup actions which are NOT dynamic (i.e. they are configured once - // and for all, they cannot be changed from another thread) - - if (options.getShowTouches()) { - try { - String oldValue = Settings.getAndPutValue(Settings.TABLE_SYSTEM, "show_touches", "1"); - // If "show touches" was disabled, it must be disabled back on clean up - if (!"1".equals(oldValue)) { - if (!cleanUp.setDisableShowTouches(true)) { - Ln.e("Could not disable show touch on exit"); - } - } - } catch (SettingsException e) { - Ln.e("Could not change \"show_touches\"", e); - } - } - - if (options.getStayAwake()) { - int stayOn = BatteryManager.BATTERY_PLUGGED_AC | BatteryManager.BATTERY_PLUGGED_USB | BatteryManager.BATTERY_PLUGGED_WIRELESS; - try { - String oldValue = Settings.getAndPutValue(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(stayOn)); - try { - int restoreStayOn = Integer.parseInt(oldValue); - if (restoreStayOn != stayOn) { - // Restore only if the current value is different - if (!cleanUp.setRestoreStayOn(restoreStayOn)) { - Ln.e("Could not restore stay on on exit"); - } - } - } catch (NumberFormatException e) { - // ignore - } - } catch (SettingsException e) { - Ln.e("Could not change \"stay_on_while_plugged_in\"", e); - } - } - - if (options.getPowerOffScreenOnClose()) { - if (!cleanUp.setPowerOffScreen(true)) { - Ln.e("Could not power off screen on exit"); - } - } - } - private static void scrcpy(Options options) throws IOException, ConfigurationException { if (Build.VERSION.SDK_INT < AndroidVersions.API_31_ANDROID_12 && options.getVideoSource() == VideoSource.CAMERA) { Ln.e("Camera mirroring is not supported before Android 12"); @@ -150,14 +102,12 @@ public final class Server { } CleanUp cleanUp = null; - Thread initThread = null; NewDisplay newDisplay = options.getNewDisplay(); int displayId = newDisplay == null ? options.getDisplayId() : Device.DISPLAY_ID_NONE; if (options.getCleanup()) { - cleanUp = CleanUp.configure(displayId); - initThread = startInitThread(options, cleanUp); + cleanUp = CleanUp.start(displayId, options); } int scid = options.getScid(); @@ -240,8 +190,8 @@ public final class Server { completion.await(); } finally { - if (initThread != null) { - initThread.interrupt(); + if (cleanUp != null) { + cleanUp.interrupt(); } for (AsyncProcessor asyncProcessor : asyncProcessors) { asyncProcessor.stop(); @@ -250,8 +200,8 @@ public final class Server { connection.shutdown(); try { - if (initThread != null) { - initThread.join(); + if (cleanUp != null) { + cleanUp.join(); } for (AsyncProcessor asyncProcessor : asyncProcessors) { asyncProcessor.join(); @@ -264,12 +214,6 @@ public final class Server { } } - private static Thread startInitThread(final Options options, final CleanUp cleanUp) { - Thread thread = new Thread(() -> initAndCleanUp(options, cleanUp), "init-cleanup"); - thread.start(); - return thread; - } - public static void main(String... args) { int status = 0; try { From eff5b4b219be6043a3baf51149b1d6752569a173 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 3 Nov 2024 22:46:21 +0100 Subject: [PATCH 2047/2244] Add --screen-off-timeout Change the Android "screen off timeout" (the idle delay before the screen automatically turns off) and restore the initial value on exit. PR #5447 --- app/data/bash-completion/scrcpy | 1 + app/data/zsh-completion/_scrcpy | 1 + app/src/cli.c | 28 +++++++++++++++ app/src/options.c | 1 + app/src/options.h | 1 + app/src/scrcpy.c | 1 + app/src/server.c | 5 +++ app/src/server.h | 1 + doc/device.md | 25 +++++++++++++ .../java/com/genymobile/scrcpy/CleanUp.java | 35 +++++++++++++++++-- .../java/com/genymobile/scrcpy/Options.java | 11 ++++++ 11 files changed, 108 insertions(+), 2 deletions(-) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index d9a7be60..d9ad4c8d 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -77,6 +77,7 @@ _scrcpy() { --rotation= -s --serial= -S --turn-screen-off + --screen-off-timeout= --shortcut-mod= --start-app= -t --show-touches diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index c4e0e60c..430e8000 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -80,6 +80,7 @@ arguments=( '--require-audio=[Make scrcpy fail if audio is enabled but does not work]' {-s,--serial=}'[The device serial number \(mandatory for multiple devices only\)]:serial:($("${ADB-adb}" devices | awk '\''$2 == "device" {print $1}'\''))' {-S,--turn-screen-off}'[Turn the device screen off immediately]' + '--screen-off-timeout=[Set the screen off timeout in seconds]' '--shortcut-mod=[\[key1,key2+key3,...\] Specify the modifiers to use for scrcpy shortcuts]:shortcut mod:(lctrl rctrl lalt ralt lsuper rsuper)' '--start-app=[Start an Android app]' {-t,--show-touches}'[Show physical touches]' diff --git a/app/src/cli.c b/app/src/cli.c index 7cc68085..ebf0f6f6 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -106,6 +106,7 @@ enum { OPT_NEW_DISPLAY, OPT_LIST_APPS, OPT_START_APP, + OPT_SCREEN_OFF_TIMEOUT, }; struct sc_option { @@ -793,6 +794,13 @@ static const struct sc_option options[] = { .longopt = "turn-screen-off", .text = "Turn the device screen off immediately.", }, + { + .longopt_id = OPT_SCREEN_OFF_TIMEOUT, + .longopt = "screen-off-timeout", + .argdesc = "seconds", + .text = "Set the screen off timeout while scrcpy is running (restore " + "the initial value on exit).", + }, { .longopt_id = OPT_SHORTCUT_MOD, .longopt = "shortcut-mod", @@ -2155,6 +2163,20 @@ parse_time_limit(const char *s, sc_tick *tick) { return true; } +static bool +parse_screen_off_timeout(const char *s, sc_tick *tick) { + long value; + // value in seconds, but must fit in 31 bits in milliseconds + bool ok = parse_integer_arg(s, &value, false, 0, 0x7FFFFFFF / 1000, + "screen off timeout"); + if (!ok) { + return false; + } + + *tick = SC_TICK_FROM_SEC(value); + return true; +} + static bool parse_pause_on_exit(const char *s, enum sc_pause_on_exit *pause_on_exit) { if (!s || !strcmp(s, "true")) { @@ -2730,6 +2752,12 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], case OPT_START_APP: opts->start_app = optarg; break; + case OPT_SCREEN_OFF_TIMEOUT: + if (!parse_screen_off_timeout(optarg, + &opts->screen_off_timeout)) { + return false; + } + break; default: // getopt prints the error message on stderr return false; diff --git a/app/src/options.c b/app/src/options.c index b1a3b739..3cad9d9f 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -62,6 +62,7 @@ const struct scrcpy_options scrcpy_options_default = { .audio_buffer = -1, // depends on the audio format, .audio_output_buffer = SC_TICK_FROM_MS(5), .time_limit = 0, + .screen_off_timeout = -1, #ifdef HAVE_V4L2 .v4l2_device = NULL, .v4l2_buffer = 0, diff --git a/app/src/options.h b/app/src/options.h index d37ac0a2..5662719a 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -265,6 +265,7 @@ struct scrcpy_options { sc_tick audio_buffer; sc_tick audio_output_buffer; sc_tick time_limit; + sc_tick screen_off_timeout; #ifdef HAVE_V4L2 const char *v4l2_device; sc_tick v4l2_buffer; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 8d135394..2721c0d8 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -428,6 +428,7 @@ scrcpy(struct scrcpy_options *options) { .video_bit_rate = options->video_bit_rate, .audio_bit_rate = options->audio_bit_rate, .max_fps = options->max_fps, + .screen_off_timeout = options->screen_off_timeout, .lock_video_orientation = options->lock_video_orientation, .control = options->control, .display_id = options->display_id, diff --git a/app/src/server.c b/app/src/server.c index 167582e4..41f0bf27 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -320,6 +320,11 @@ execute_server(struct sc_server *server, if (params->stay_awake) { ADD_PARAM("stay_awake=true"); } + if (params->screen_off_timeout != -1) { + assert(params->screen_off_timeout >= 0); + uint64_t ms = SC_TICK_TO_MS(params->screen_off_timeout); + ADD_PARAM("screen_off_timeout=%" PRIu64, ms); + } if (params->video_codec_options) { VALIDATE_STRING(params->video_codec_options); ADD_PARAM("video_codec_options=%s", params->video_codec_options); diff --git a/app/src/server.h b/app/src/server.h index 4ff5539d..7059be7f 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -45,6 +45,7 @@ struct sc_server_params { uint32_t video_bit_rate; uint32_t audio_bit_rate; const char *max_fps; // float to be parsed by the server + sc_tick screen_off_timeout; int8_t lock_video_orientation; bool control; uint32_t display_id; diff --git a/doc/device.md b/doc/device.md index 06210e18..42208faa 100644 --- a/doc/device.md +++ b/doc/device.md @@ -71,6 +71,31 @@ adb shell cmd display power-on 0 ``` +## Screen off timeout + +The Android screen automatically turns off after some delay. + +To change this delay while scrcpy is running: + +```bash +scrcpy --screen-off-timeout=300 # 300 seconds (5 minutes) +``` + +The initial value is restored on exit. + +It is possible to change this setting manually: + +```bash +# get the current screen_off_timeout value +adb shell settings get system screen_off_timeout +# set a new value (in milliseconds) +adb shell settings put system screen_off_timeout 30000 +``` + +Note that the Android value is in milliseconds, but the scrcpy command line +argument is in seconds. + + ## Show touches For presentations, it may be useful to show physical touches (on the physical diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java index a8a5784d..90de8c2c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java +++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java @@ -73,10 +73,29 @@ public final class CleanUp { } } + int restoreScreenOffTimeout = -1; + int screenOffTimeout = options.getScreenOffTimeout(); + if (screenOffTimeout != -1) { + try { + String oldValue = Settings.getAndPutValue(Settings.TABLE_SYSTEM, "screen_off_timeout", String.valueOf(screenOffTimeout)); + try { + int currentScreenOffTimeout = Integer.parseInt(oldValue); + // Restore only if the current value is different + if (currentScreenOffTimeout != screenOffTimeout) { + restoreScreenOffTimeout = currentScreenOffTimeout; + } + } catch (NumberFormatException e) { + // ignore + } + } catch (SettingsException e) { + Ln.e("Could not change \"screen_off_timeout\"", e); + } + } + boolean powerOffScreen = options.getPowerOffScreenOnClose(); try { - run(displayId, restoreStayOn, disableShowTouches, powerOffScreen); + run(displayId, restoreStayOn, disableShowTouches, powerOffScreen, restoreScreenOffTimeout); } catch (InterruptedException e) { // ignore } catch (IOException e) { @@ -84,7 +103,8 @@ public final class CleanUp { } } - private void run(int displayId, int restoreStayOn, boolean disableShowTouches, boolean powerOffScreen) throws IOException, InterruptedException { + private void run(int displayId, int restoreStayOn, boolean disableShowTouches, boolean powerOffScreen, int restoreScreenOffTimeout) + throws IOException, InterruptedException { String[] cmd = { "app_process", "/", @@ -93,6 +113,7 @@ public final class CleanUp { String.valueOf(restoreStayOn), String.valueOf(disableShowTouches), String.valueOf(powerOffScreen), + String.valueOf(restoreScreenOffTimeout), }; ProcessBuilder builder = new ProcessBuilder(cmd); @@ -139,6 +160,7 @@ public final class CleanUp { int restoreStayOn = Integer.parseInt(args[1]); boolean disableShowTouches = Boolean.parseBoolean(args[2]); boolean powerOffScreen = Boolean.parseBoolean(args[3]); + int restoreScreenOffTimeout = Integer.parseInt(args[4]); // Dynamic option boolean restoreDisplayPower = false; @@ -175,6 +197,15 @@ public final class CleanUp { } } + if (restoreScreenOffTimeout != -1) { + Ln.i("Restoring \"screen off timeout\""); + try { + Settings.putValue(Settings.TABLE_SYSTEM, "screen_off_timeout", String.valueOf(restoreScreenOffTimeout)); + } catch (SettingsException e) { + Ln.e("Could not restore \"screen_off_timeout\"", e); + } + } + if (displayId != Device.DISPLAY_ID_NONE && Device.isScreenOn(displayId)) { if (powerOffScreen) { Ln.i("Power off screen"); diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 659a6948..e75321e6 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -45,6 +45,7 @@ public class Options { private boolean cameraHighSpeed; private boolean showTouches; private boolean stayAwake; + private int screenOffTimeout = -1; private List videoCodecOptions; private List audioCodecOptions; @@ -174,6 +175,10 @@ public class Options { return stayAwake; } + public int getScreenOffTimeout() { + return screenOffTimeout; + } + public List getVideoCodecOptions() { return videoCodecOptions; } @@ -363,6 +368,12 @@ public class Options { case "stay_awake": options.stayAwake = Boolean.parseBoolean(value); break; + case "screen_off_timeout": + options.screenOffTimeout = Integer.parseInt(value); + if (options.screenOffTimeout < -1) { + throw new IllegalArgumentException("Invalid screen off timeout: " + options.screenOffTimeout); + } + break; case "video_codec_options": options.videoCodecOptions = CodecOption.parse(value); break; From c0e2e27cf9eb38a95b324ba9aeeb34dab6d2e72f Mon Sep 17 00:00:00 2001 From: Simon Chan <1330321+yume-chan@users.noreply.github.com> Date: Mon, 11 Nov 2024 12:16:18 +0800 Subject: [PATCH 2048/2244] Force javac to use UTF-8 The source files are encoded in UTF-8. Refs Signed-off-by: Romain Vimont --- server/build_without_gradle.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index 14534700..ddb98b21 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -68,7 +68,7 @@ done echo "Compiling java sources..." cd ../java -javac -bootclasspath "$ANDROID_JAR" \ +javac -encoding UTF-8 -bootclasspath "$ANDROID_JAR" \ -cp "$LAMBDA_JAR:$GEN_DIR" \ -d "$CLASSES_DIR" \ -source 1.8 -target 1.8 \ From 762816cac62b39a58bebf129fb6e00bd24a4594b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 13 Nov 2024 12:54:25 +0100 Subject: [PATCH 2049/2244] Remove quotes for --video-encoder in documentation Refs ec602a0334357982d75b374f7ac753c5bef1216a --- doc/video.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/video.md b/doc/video.md index ed92cb22..844fa516 100644 --- a/doc/video.md +++ b/doc/video.md @@ -93,7 +93,7 @@ Sometimes, the default encoder may have issues or even crash, so it is useful to try another one: ```bash -scrcpy --video-codec=h264 --video-encoder='OMX.qcom.video.encoder.avc' +scrcpy --video-codec=h264 --video-encoder=OMX.qcom.video.encoder.avc ``` From 04dd72b5944dbe8de2a2fa438f920e88c03e4ff8 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 13 Nov 2024 12:56:35 +0100 Subject: [PATCH 2050/2244] Add "how to run" link for Windows Reference the documentation explaining how to run scrcpy on Windows directly in the main README. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 44f3d740..7b29f3a4 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ Note that USB debugging is not required to run scrcpy in [OTG mode](doc/otg.md). ## Get the app - [Linux](doc/linux.md) - - [Windows](doc/windows.md) + - [Windows](doc/windows.md) (read [how to run](doc/windows.md#run)) - [macOS](doc/macos.md) From 91373d906b100349de959f49172d4605f66f64b2 Mon Sep 17 00:00:00 2001 From: Simon Chan <1330321+yume-chan@users.noreply.github.com> Date: Sat, 9 Nov 2024 14:34:55 +0800 Subject: [PATCH 2051/2244] Add FakeContext.getContentResolver() This avoids the following error on some devices: Given calling package android does not match caller's uid 2000 Refs #4639 comment Fixes #4639 PR #5476 Signed-off-by: Romain Vimont --- server/build_without_gradle.sh | 6 +++ .../android/content/IContentProvider.java | 5 +++ .../com/genymobile/scrcpy/FakeContext.java | 42 +++++++++++++++++++ .../scrcpy/wrappers/ActivityManager.java | 16 +++---- 4 files changed, 62 insertions(+), 7 deletions(-) create mode 100644 server/src/main/java/android/content/IContentProvider.java diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index ddb98b21..e0fc3a95 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -50,6 +50,11 @@ cd "$SERVER_DIR/src/main/aidl" android/content/IOnPrimaryClipChangedListener.aidl "$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" -I. android/view/IDisplayFoldListener.aidl +# Fake sources to expose hidden Android types to the project +FAKE_SRC=( \ + android/content/*java \ +) + SRC=( \ com/genymobile/scrcpy/*.java \ com/genymobile/scrcpy/audio/*.java \ @@ -72,6 +77,7 @@ javac -encoding UTF-8 -bootclasspath "$ANDROID_JAR" \ -cp "$LAMBDA_JAR:$GEN_DIR" \ -d "$CLASSES_DIR" \ -source 1.8 -target 1.8 \ + ${FAKE_SRC[@]} \ ${SRC[@]} echo "Dexing..." diff --git a/server/src/main/java/android/content/IContentProvider.java b/server/src/main/java/android/content/IContentProvider.java new file mode 100644 index 00000000..bb907dd3 --- /dev/null +++ b/server/src/main/java/android/content/IContentProvider.java @@ -0,0 +1,5 @@ +package android.content; + +public interface IContentProvider { + // android.content.IContentProvider is hidden, this is a fake one to expose the type to the project +} diff --git a/server/src/main/java/com/genymobile/scrcpy/FakeContext.java b/server/src/main/java/com/genymobile/scrcpy/FakeContext.java index 0b086cc5..2b83e397 100644 --- a/server/src/main/java/com/genymobile/scrcpy/FakeContext.java +++ b/server/src/main/java/com/genymobile/scrcpy/FakeContext.java @@ -1,9 +1,14 @@ package com.genymobile.scrcpy; +import com.genymobile.scrcpy.wrappers.ServiceManager; + import android.annotation.TargetApi; import android.content.AttributionSource; +import android.content.ContentResolver; import android.content.Context; import android.content.ContextWrapper; +import android.content.IContentProvider; +import android.os.Binder; import android.os.Process; public final class FakeContext extends ContextWrapper { @@ -17,6 +22,38 @@ public final class FakeContext extends ContextWrapper { return INSTANCE; } + private final ContentResolver contentResolver = new ContentResolver(this) { + @SuppressWarnings({"unused", "ProtectedMemberInFinalClass"}) + // @Override (but super-class method not visible) + protected IContentProvider acquireProvider(Context c, String name) { + return ServiceManager.getActivityManager().getContentProviderExternal(name, new Binder()); + } + + @SuppressWarnings("unused") + // @Override (but super-class method not visible) + public boolean releaseProvider(IContentProvider icp) { + return false; + } + + @SuppressWarnings({"unused", "ProtectedMemberInFinalClass"}) + // @Override (but super-class method not visible) + protected IContentProvider acquireUnstableProvider(Context c, String name) { + return null; + } + + @SuppressWarnings("unused") + // @Override (but super-class method not visible) + public boolean releaseUnstableProvider(IContentProvider icp) { + return false; + } + + @SuppressWarnings("unused") + // @Override (but super-class method not visible) + public void unstableProviderDied(IContentProvider icp) { + // ignore + } + }; + private FakeContext() { super(Workarounds.getSystemContext()); } @@ -49,4 +86,9 @@ public final class FakeContext extends ContextWrapper { public Context getApplicationContext() { return this; } + + @Override + public ContentResolver getContentResolver() { + return contentResolver; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java index f052dee0..255483c6 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java @@ -6,6 +6,7 @@ import com.genymobile.scrcpy.util.Ln; import android.annotation.SuppressLint; import android.annotation.TargetApi; +import android.content.IContentProvider; import android.content.Intent; import android.os.Binder; import android.os.Bundle; @@ -64,7 +65,7 @@ public final class ActivityManager { } @TargetApi(AndroidVersions.API_29_ANDROID_10) - private ContentProvider getContentProviderExternal(String name, IBinder token) { + public IContentProvider getContentProviderExternal(String name, IBinder token) { try { Method method = getGetContentProviderExternalMethod(); Object[] args; @@ -83,11 +84,7 @@ public final class ActivityManager { // 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); + return (IContentProvider) providerField.get(providerHolder); } catch (ReflectiveOperationException e) { Ln.e("Could not invoke method", e); return null; @@ -104,7 +101,12 @@ public final class ActivityManager { } public ContentProvider createSettingsProvider() { - return getContentProviderExternal("settings", new Binder()); + IBinder token = new Binder(); + IContentProvider provider = getContentProviderExternal("settings", token); + if (provider == null) { + return null; + } + return new ContentProvider(this, provider, "settings", token); } private Method getStartActivityAsUserMethod() throws NoSuchMethodException, ClassNotFoundException { From df74cceb6fe115bd39e862612a14a1e1483b1529 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 10 Nov 2024 18:46:51 +0100 Subject: [PATCH 2052/2244] Use camera prepare() step For consistency with screen capture. Refs b60e1747809cce58793a8c0d54b499df87a6a975 --- .../scrcpy/video/CameraCapture.java | 23 ++++++++++--------- .../scrcpy/video/SurfaceCapture.java | 2 +- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java index 7385283e..0ec404eb 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java @@ -80,14 +80,21 @@ public class CameraCapture extends SurfaceCapture { throw new IOException("No matching camera found"); } + Ln.i("Using camera '" + cameraId + "'"); + cameraDevice = openCamera(cameraId); + } catch (CameraAccessException | InterruptedException e) { + throw new IOException(e); + } + } + + @Override + public void prepare() throws IOException { + try { size = selectSize(cameraId, explicitSize, maxSize, aspectRatio, highSpeed); if (size == null) { throw new IOException("Could not select camera size"); } - - Ln.i("Using camera '" + cameraId + "'"); - cameraDevice = openCamera(cameraId); - } catch (CameraAccessException | InterruptedException e) { + } catch (CameraAccessException e) { throw new IOException(e); } } @@ -232,13 +239,7 @@ public class CameraCapture extends SurfaceCapture { } this.maxSize = maxSize; - try { - size = selectSize(cameraId, null, maxSize, aspectRatio, highSpeed); - return size != null; - } catch (CameraAccessException e) { - Ln.w("Could not select camera size", e); - return false; - } + return true; } @SuppressLint("MissingPermission") diff --git a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java index de9e1b27..d0d93f54 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java @@ -46,7 +46,7 @@ public abstract class SurfaceCapture { /** * Called once before each capture starts, before {@link #getSize()}. */ - public void prepare() throws ConfigurationException { + public void prepare() throws ConfigurationException, IOException { // empty by default } From 2337f524d167e210a2985d691e5695e5b7e3249f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 10 Nov 2024 19:17:04 +0100 Subject: [PATCH 2053/2244] Improve error message on unknown camera id If the camera id is explicitly provided (via --camera-id), report a user-friendly error if no camera with this id is found. --- .../scrcpy/video/CameraCapture.java | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java index 0ec404eb..01afad7b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java @@ -1,9 +1,11 @@ package com.genymobile.scrcpy.video; import com.genymobile.scrcpy.AndroidVersions; +import com.genymobile.scrcpy.device.ConfigurationException; import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.util.HandlerExecutor; import com.genymobile.scrcpy.util.Ln; +import com.genymobile.scrcpy.util.LogUtils; import com.genymobile.scrcpy.wrappers.ServiceManager; import android.annotation.SuppressLint; @@ -68,7 +70,7 @@ public class CameraCapture extends SurfaceCapture { } @Override - protected void init() throws IOException { + protected void init() throws ConfigurationException, IOException { cameraThread = new HandlerThread("camera"); cameraThread.start(); cameraHandler = new Handler(cameraThread.getLooper()); @@ -77,7 +79,7 @@ public class CameraCapture extends SurfaceCapture { try { cameraId = selectCamera(explicitCameraId, cameraFacing); if (cameraId == null) { - throw new IOException("No matching camera found"); + throw new ConfigurationException("No matching camera found"); } Ln.i("Using camera '" + cameraId + "'"); @@ -99,14 +101,18 @@ public class CameraCapture extends SurfaceCapture { } } - private static String selectCamera(String explicitCameraId, CameraFacing cameraFacing) throws CameraAccessException { - if (explicitCameraId != null) { - return explicitCameraId; - } - + private static String selectCamera(String explicitCameraId, CameraFacing cameraFacing) throws CameraAccessException, ConfigurationException { CameraManager cameraManager = ServiceManager.getCameraManager(); String[] cameraIds = cameraManager.getCameraIdList(); + if (explicitCameraId != null) { + if (!Arrays.asList(cameraIds).contains(explicitCameraId)) { + Ln.e("Camera with id " + explicitCameraId + " not found\n" + LogUtils.buildCameraListMessage(false)); + throw new ConfigurationException("Camera id not found"); + } + return explicitCameraId; + } + if (cameraFacing == null) { // Use the first one return cameraIds.length > 0 ? cameraIds[0] : null; From 0e399b65bd2f32f93a0372ed0d64cdc2ba223d86 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 10 Nov 2024 17:03:39 +0100 Subject: [PATCH 2054/2244] Remove [] around app package names This simplifies copy-pasting from the result of: scrcpy --list-apps --- server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java b/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java index 2b780caf..088be7e7 100644 --- a/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java @@ -236,7 +236,7 @@ public final class LogUtils { } else { builder.append("\n ").append(String.format("%" + column + "s", " ")); } - builder.append(" [").append(app.getPackageName()).append(']'); + builder.append(" ").append(app.getPackageName()); } return builder.toString(); From 5e10c37f02cb41054257dc895a5aae7d9df7dd89 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 10 Nov 2024 11:53:12 +0100 Subject: [PATCH 2055/2244] Define all DisplayManager flags locally For consistency. --- .../com/genymobile/scrcpy/video/NewDisplayCapture.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java index f6561a5f..3dc05ce7 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java @@ -19,6 +19,8 @@ import java.io.IOException; public class NewDisplayCapture extends SurfaceCapture { // Internal fields copied from android.hardware.display.DisplayManager + private static final int VIRTUAL_DISPLAY_FLAG_PUBLIC = DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC; + private static final int VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY = DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY; private static final int VIRTUAL_DISPLAY_FLAG_SUPPORTS_TOUCH = 1 << 6; private static final int VIRTUAL_DISPLAY_FLAG_ROTATES_WITH_CONTENT = 1 << 7; private static final int VIRTUAL_DISPLAY_FLAG_DESTROY_CONTENT_ON_REMOVAL = 1 << 8; @@ -74,12 +76,11 @@ public class NewDisplayCapture extends SurfaceCapture { } } - public void startNew(Surface surface) { int virtualDisplayId; try { - int flags = DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC - | DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY + int flags = VIRTUAL_DISPLAY_FLAG_PUBLIC + | VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY | VIRTUAL_DISPLAY_FLAG_SUPPORTS_TOUCH | VIRTUAL_DISPLAY_FLAG_ROTATES_WITH_CONTENT | VIRTUAL_DISPLAY_FLAG_DESTROY_CONTENT_ON_REMOVAL From 794595e3f0db8208a2a1df30c1c5e9442deb4112 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 14 Nov 2024 20:20:10 +0100 Subject: [PATCH 2056/2244] Set displayId to NONE in Options on new display If a new display is set, force options.getDisplayId() to return Device.DISPLAY_ID_NONE, to avoid any confusion between a local displayId and options.getDisplayId(). --- .../src/main/java/com/genymobile/scrcpy/CleanUp.java | 11 ++++++----- .../src/main/java/com/genymobile/scrcpy/Options.java | 5 +++++ .../src/main/java/com/genymobile/scrcpy/Server.java | 12 +++++------- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java index 90de8c2c..352f7c6b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java +++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java @@ -25,13 +25,13 @@ public final class CleanUp { private Thread thread; - private CleanUp(int displayId, Options options) { - thread = new Thread(() -> runCleanUp(displayId, options), "cleanup"); + private CleanUp(Options options) { + thread = new Thread(() -> runCleanUp(options), "cleanup"); thread.start(); } - public static CleanUp start(int displayId, Options options) { - return new CleanUp(displayId, options); + public static CleanUp start(Options options) { + return new CleanUp(options); } public void interrupt() { @@ -42,7 +42,7 @@ public final class CleanUp { thread.join(); } - private void runCleanUp(int displayId, Options options) { + private void runCleanUp(Options options) { boolean disableShowTouches = false; if (options.getShowTouches()) { try { @@ -93,6 +93,7 @@ public final class CleanUp { } boolean powerOffScreen = options.getPowerOffScreenOnClose(); + int displayId = options.getDisplayId(); try { run(displayId, restoreStayOn, disableShowTouches, powerOffScreen, restoreScreenOffTimeout); diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index e75321e6..54888827 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -479,6 +479,11 @@ public class Options { } } + if (options.newDisplay != null) { + assert options.displayId == 0 : "Must not set both displayId and newDisplay"; + options.displayId = Device.DISPLAY_ID_NONE; + } + return options; } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index a093fdf0..d0a340da 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -103,11 +103,8 @@ public final class Server { CleanUp cleanUp = null; - NewDisplay newDisplay = options.getNewDisplay(); - int displayId = newDisplay == null ? options.getDisplayId() : Device.DISPLAY_ID_NONE; - if (options.getCleanup()) { - cleanUp = CleanUp.start(displayId, options); + cleanUp = CleanUp.start(options); } int scid = options.getScid(); @@ -131,7 +128,7 @@ public final class Server { if (control) { ControlChannel controlChannel = connection.getControlChannel(); - controller = new Controller(displayId, controlChannel, cleanUp, options.getClipboardAutosync(), options.getPowerOn()); + controller = new Controller(options.getDisplayId(), controlChannel, cleanUp, options.getClipboardAutosync(), options.getPowerOn()); asyncProcessors.add(controller); } @@ -161,11 +158,12 @@ public final class Server { options.getSendFrameMeta()); SurfaceCapture surfaceCapture; if (options.getVideoSource() == VideoSource.DISPLAY) { + NewDisplay newDisplay = options.getNewDisplay(); if (newDisplay != null) { surfaceCapture = new NewDisplayCapture(controller, newDisplay, options.getMaxSize()); } else { - assert displayId != Device.DISPLAY_ID_NONE; - surfaceCapture = new ScreenCapture(controller, displayId, options.getMaxSize(), options.getCrop(), + assert options.getDisplayId() != Device.DISPLAY_ID_NONE; + surfaceCapture = new ScreenCapture(controller, options.getDisplayId(), options.getMaxSize(), options.getCrop(), options.getLockVideoOrientation()); } } else { From bd9d93194b04970655b22f1eb96d5ad2bd4f2c75 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 14 Nov 2024 20:25:12 +0100 Subject: [PATCH 2057/2244] Pass Options instance directly Many constructors take a lot of parameters copied from Options. For simplicity, just pass the Options instance. --- .../java/com/genymobile/scrcpy/Server.java | 16 ++++++---------- .../genymobile/scrcpy/audio/AudioEncoder.java | 9 +++++---- .../genymobile/scrcpy/control/Controller.java | 9 +++++---- .../genymobile/scrcpy/video/CameraCapture.java | 18 +++++++++--------- .../scrcpy/video/NewDisplayCapture.java | 8 +++++--- .../genymobile/scrcpy/video/ScreenCapture.java | 13 ++++++++----- .../scrcpy/video/SurfaceEncoder.java | 14 +++++++------- 7 files changed, 45 insertions(+), 42 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index d0a340da..dae73b64 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -128,7 +128,7 @@ public final class Server { if (control) { ControlChannel controlChannel = connection.getControlChannel(); - controller = new Controller(options.getDisplayId(), controlChannel, cleanUp, options.getClipboardAutosync(), options.getPowerOn()); + controller = new Controller(controlChannel, cleanUp, options); asyncProcessors.add(controller); } @@ -147,8 +147,7 @@ public final class Server { if (audioCodec == AudioCodec.RAW) { audioRecorder = new AudioRawRecorder(audioCapture, audioStreamer); } else { - audioRecorder = new AudioEncoder(audioCapture, audioStreamer, options.getAudioBitRate(), options.getAudioCodecOptions(), - options.getAudioEncoder()); + audioRecorder = new AudioEncoder(audioCapture, audioStreamer, options); } asyncProcessors.add(audioRecorder); } @@ -160,18 +159,15 @@ public final class Server { if (options.getVideoSource() == VideoSource.DISPLAY) { NewDisplay newDisplay = options.getNewDisplay(); if (newDisplay != null) { - surfaceCapture = new NewDisplayCapture(controller, newDisplay, options.getMaxSize()); + surfaceCapture = new NewDisplayCapture(controller, options); } else { assert options.getDisplayId() != Device.DISPLAY_ID_NONE; - surfaceCapture = new ScreenCapture(controller, options.getDisplayId(), options.getMaxSize(), options.getCrop(), - options.getLockVideoOrientation()); + surfaceCapture = new ScreenCapture(controller, options); } } else { - surfaceCapture = new CameraCapture(options.getCameraId(), options.getCameraFacing(), options.getCameraSize(), - options.getMaxSize(), options.getCameraAspectRatio(), options.getCameraFps(), options.getCameraHighSpeed()); + surfaceCapture = new CameraCapture(options); } - SurfaceEncoder surfaceEncoder = new SurfaceEncoder(surfaceCapture, videoStreamer, options.getVideoBitRate(), options.getMaxFps(), - options.getVideoCodecOptions(), options.getVideoEncoder(), options.getDownsizeOnError()); + SurfaceEncoder surfaceEncoder = new SurfaceEncoder(surfaceCapture, videoStreamer, options); asyncProcessors.add(surfaceEncoder); if (controller != null) { diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java index fcc0c52f..267be60a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java @@ -2,6 +2,7 @@ package com.genymobile.scrcpy.audio; import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.AsyncProcessor; +import com.genymobile.scrcpy.Options; import com.genymobile.scrcpy.device.ConfigurationException; import com.genymobile.scrcpy.device.Streamer; import com.genymobile.scrcpy.util.Codec; @@ -67,12 +68,12 @@ public final class AudioEncoder implements AsyncProcessor { private boolean ended; - public AudioEncoder(AudioCapture capture, Streamer streamer, int bitRate, List codecOptions, String encoderName) { + public AudioEncoder(AudioCapture capture, Streamer streamer, Options options) { this.capture = capture; this.streamer = streamer; - this.bitRate = bitRate; - this.codecOptions = codecOptions; - this.encoderName = encoderName; + this.bitRate = options.getAudioBitRate(); + this.codecOptions = options.getAudioCodecOptions(); + this.encoderName = options.getAudioEncoder(); } private static MediaFormat createFormat(String mimeType, int bitRate, List codecOptions) { diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index b4ae07b6..573e8f52 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -3,6 +3,7 @@ package com.genymobile.scrcpy.control; import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.AsyncProcessor; import com.genymobile.scrcpy.CleanUp; +import com.genymobile.scrcpy.Options; import com.genymobile.scrcpy.device.Device; import com.genymobile.scrcpy.device.DeviceApp; import com.genymobile.scrcpy.device.Point; @@ -97,12 +98,12 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { // Used for resetting video encoding on RESET_VIDEO message private SurfaceCapture surfaceCapture; - public Controller(int displayId, ControlChannel controlChannel, CleanUp cleanUp, boolean clipboardAutosync, boolean powerOn) { - this.displayId = displayId; + public Controller(ControlChannel controlChannel, CleanUp cleanUp, Options options) { + this.displayId = options.getDisplayId(); this.controlChannel = controlChannel; this.cleanUp = cleanUp; - this.clipboardAutosync = clipboardAutosync; - this.powerOn = powerOn; + this.clipboardAutosync = options.getClipboardAutosync(); + this.powerOn = options.getPowerOn(); initPointers(); sender = new DeviceMessageSender(controlChannel); diff --git a/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java index 01afad7b..ee4085e9 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java @@ -1,6 +1,7 @@ package com.genymobile.scrcpy.video; import com.genymobile.scrcpy.AndroidVersions; +import com.genymobile.scrcpy.Options; import com.genymobile.scrcpy.device.ConfigurationException; import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.util.HandlerExecutor; @@ -58,15 +59,14 @@ public class CameraCapture extends SurfaceCapture { private final AtomicBoolean disconnected = new AtomicBoolean(); - public CameraCapture(String explicitCameraId, CameraFacing cameraFacing, Size explicitSize, int maxSize, CameraAspectRatio aspectRatio, int fps, - boolean highSpeed) { - this.explicitCameraId = explicitCameraId; - this.cameraFacing = cameraFacing; - this.explicitSize = explicitSize; - this.maxSize = maxSize; - this.aspectRatio = aspectRatio; - this.fps = fps; - this.highSpeed = highSpeed; + public CameraCapture(Options options) { + this.explicitCameraId = options.getCameraId(); + this.cameraFacing = options.getCameraFacing(); + this.explicitSize = options.getCameraSize(); + this.maxSize = options.getMaxSize(); + this.aspectRatio = options.getCameraAspectRatio(); + this.fps = options.getCameraFps(); + this.highSpeed = options.getCameraHighSpeed(); } @Override diff --git a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java index 3dc05ce7..9b1c9933 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java @@ -1,6 +1,7 @@ package com.genymobile.scrcpy.video; import com.genymobile.scrcpy.AndroidVersions; +import com.genymobile.scrcpy.Options; import com.genymobile.scrcpy.control.PositionMapper; import com.genymobile.scrcpy.device.DisplayInfo; import com.genymobile.scrcpy.device.NewDisplay; @@ -43,10 +44,11 @@ public class NewDisplayCapture extends SurfaceCapture { private Size size; private int dpi; - public NewDisplayCapture(VirtualDisplayListener vdListener, NewDisplay newDisplay, int maxSize) { + public NewDisplayCapture(VirtualDisplayListener vdListener, Options options) { this.vdListener = vdListener; - this.newDisplay = newDisplay; - this.maxSize = maxSize; + this.newDisplay = options.getNewDisplay(); + assert newDisplay != null; + this.maxSize = options.getMaxSize(); } @Override diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java index 48e594b7..00d855bd 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java @@ -1,8 +1,10 @@ package com.genymobile.scrcpy.video; import com.genymobile.scrcpy.AndroidVersions; +import com.genymobile.scrcpy.Options; import com.genymobile.scrcpy.control.PositionMapper; import com.genymobile.scrcpy.device.ConfigurationException; +import com.genymobile.scrcpy.device.Device; import com.genymobile.scrcpy.device.DisplayInfo; import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.util.Ln; @@ -48,12 +50,13 @@ public class ScreenCapture extends SurfaceCapture { private IRotationWatcher rotationWatcher; private IDisplayFoldListener displayFoldListener; - public ScreenCapture(VirtualDisplayListener vdListener, int displayId, int maxSize, Rect crop, int lockVideoOrientation) { + public ScreenCapture(VirtualDisplayListener vdListener, Options options) { this.vdListener = vdListener; - this.displayId = displayId; - this.maxSize = maxSize; - this.crop = crop; - this.lockVideoOrientation = lockVideoOrientation; + this.displayId = options.getDisplayId(); + assert displayId != Device.DISPLAY_ID_NONE; + this.maxSize = options.getMaxSize(); + this.crop = options.getCrop(); + this.lockVideoOrientation = options.getLockVideoOrientation(); } @Override diff --git a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java index 8fadfa7b..62581d3d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java @@ -2,6 +2,7 @@ package com.genymobile.scrcpy.video; import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.AsyncProcessor; +import com.genymobile.scrcpy.Options; import com.genymobile.scrcpy.device.ConfigurationException; import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.device.Streamer; @@ -51,15 +52,14 @@ public class SurfaceEncoder implements AsyncProcessor { private final CaptureReset reset = new CaptureReset(); - public SurfaceEncoder(SurfaceCapture capture, Streamer streamer, int videoBitRate, float maxFps, List codecOptions, - String encoderName, boolean downsizeOnError) { + public SurfaceEncoder(SurfaceCapture capture, Streamer streamer, Options options) { this.capture = capture; this.streamer = streamer; - this.videoBitRate = videoBitRate; - this.maxFps = maxFps; - this.codecOptions = codecOptions; - this.encoderName = encoderName; - this.downsizeOnError = downsizeOnError; + this.videoBitRate = options.getVideoBitRate(); + this.maxFps = options.getMaxFps(); + this.codecOptions = options.getVideoCodecOptions(); + this.encoderName = options.getVideoEncoder(); + this.downsizeOnError = options.getDownsizeOnError(); } private void streamCapture() throws IOException, ConfigurationException { From 5694562a74e068df17193f25c9b1dbdd9342e0c0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 18 Nov 2024 18:47:57 +0100 Subject: [PATCH 2058/2244] Remove duplicate log The function prepareRetry() already logs a more detailed message: Retrying with -mXXXX... --- .../main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java | 1 - 1 file changed, 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java index 62581d3d..a00a8236 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java @@ -116,7 +116,6 @@ public class SurfaceEncoder implements AsyncProcessor { if (!prepareRetry(size)) { throw e; } - Ln.i("Retrying..."); alive = true; } finally { reset.setRunningMediaCodec(null); From e411b74a16a12f95c52f0c7a4c9c4f0ab1a3bc8d Mon Sep 17 00:00:00 2001 From: Matthias Stock Date: Mon, 18 Nov 2024 18:10:17 +0100 Subject: [PATCH 2059/2244] Use explicit file protocol for AVIO AVIO expects a `url` to locate a resource. Use the file protocol to handle filenames containing colons. Fixes #5487 PR #5499 Signed-off-by: Romain Vimont --- app/src/recorder.c | 10 ++++++++-- app/src/util/str.c | 20 ++++++++++++++++++++ app/src/util/str.h | 9 +++++++++ app/tests/test_str.c | 11 +++++++++++ 4 files changed, 48 insertions(+), 2 deletions(-) diff --git a/app/src/recorder.c b/app/src/recorder.c index 9e0b3395..15f27157 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -143,8 +143,14 @@ sc_recorder_open_output_file(struct sc_recorder *recorder) { return false; } - int ret = avio_open(&recorder->ctx->pb, recorder->filename, - AVIO_FLAG_WRITE); + char *file_url = sc_str_concat("file:", recorder->filename); + if (!file_url) { + avformat_free_context(recorder->ctx); + return false; + } + + int ret = avio_open(&recorder->ctx->pb, file_url, AVIO_FLAG_WRITE); + free(file_url); if (ret < 0) { LOGE("Failed to open output file: %s", recorder->filename); avformat_free_context(recorder->ctx); diff --git a/app/src/util/str.c b/app/src/util/str.c index 755369d8..304cd302 100644 --- a/app/src/util/str.c +++ b/app/src/util/str.c @@ -64,6 +64,26 @@ sc_str_quote(const char *src) { return quoted; } +char * +sc_str_concat(const char *start, const char *end) { + assert(start); + assert(end); + + size_t start_len = strlen(start); + size_t end_len = strlen(end); + + char *result = malloc(start_len + end_len + 1); + if (!result) { + LOG_OOM(); + return NULL; + } + + memcpy(result, start, start_len); + memcpy(result + start_len, end, end_len + 1); + + return result; +} + bool sc_str_parse_integer(const char *s, long *out) { char *endptr; diff --git a/app/src/util/str.h b/app/src/util/str.h index 20da26f0..d20f1b28 100644 --- a/app/src/util/str.h +++ b/app/src/util/str.h @@ -38,6 +38,15 @@ sc_str_join(char *dst, const char *const tokens[], char sep, size_t n); char * sc_str_quote(const char *src); +/** + * Concat two strings + * + * Return a new allocated string, contanining the concatenation of the two + * input strings. + */ +char * +sc_str_concat(const char *start, const char *end); + /** * Parse `s` as an integer into `out` * diff --git a/app/tests/test_str.c b/app/tests/test_str.c index 5d365ef5..4a906d92 100644 --- a/app/tests/test_str.c +++ b/app/tests/test_str.c @@ -141,6 +141,16 @@ static void test_quote(void) { free(out); } +static void test_concat(void) { + const char *s = "2024:11"; + char *out = sc_str_concat("my-prefix:", s); + + // contains the concat + assert(!strcmp("my-prefix:2024:11", out)); + + free(out); +} + static void test_utf8_truncate(void) { const char *s = "aÉbÔc"; assert(strlen(s) == 7); // É and Ô are 2 bytes-wide @@ -389,6 +399,7 @@ int main(int argc, char *argv[]) { test_join_truncated_before_sep(); test_join_truncated_after_sep(); test_quote(); + test_concat(); test_utf8_truncate(); test_parse_integer(); test_parse_integers(); From 2a04858a2271fdb44a0eeaeeadaedfc966eab48d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 20 Oct 2024 16:51:32 +0200 Subject: [PATCH 2060/2244] Add on-device OpenGL video filter architecture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce several key components to perform OpenGL filters: - OpenGLRunner: a tool for running a filter to be rendered to a Surface from an OpenGL-dedicated thread - OpenGLFilter: a simple OpenGL filter API - AffineOpenGLFilter: a generic OpenGL implementation to apply any 2D affine transform - AffineMatrix: an affine transform matrix, with helpers to build matrices from semantic transformations (rotate, scale, translate…) PR #5455 --- server/build_without_gradle.sh | 1 + .../java/com/genymobile/scrcpy/Server.java | 4 + .../scrcpy/opengl/AffineOpenGLFilter.java | 135 +++++++ .../com/genymobile/scrcpy/opengl/GLUtils.java | 124 ++++++ .../scrcpy/opengl/OpenGLException.java | 13 + .../scrcpy/opengl/OpenGLFilter.java | 21 + .../scrcpy/opengl/OpenGLRunner.java | 246 ++++++++++++ .../genymobile/scrcpy/util/AffineMatrix.java | 368 ++++++++++++++++++ 8 files changed, 912 insertions(+) create mode 100644 server/src/main/java/com/genymobile/scrcpy/opengl/AffineOpenGLFilter.java create mode 100644 server/src/main/java/com/genymobile/scrcpy/opengl/GLUtils.java create mode 100644 server/src/main/java/com/genymobile/scrcpy/opengl/OpenGLException.java create mode 100644 server/src/main/java/com/genymobile/scrcpy/opengl/OpenGLFilter.java create mode 100644 server/src/main/java/com/genymobile/scrcpy/opengl/OpenGLRunner.java create mode 100644 server/src/main/java/com/genymobile/scrcpy/util/AffineMatrix.java diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index e0fc3a95..206aa604 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -60,6 +60,7 @@ SRC=( \ com/genymobile/scrcpy/audio/*.java \ com/genymobile/scrcpy/control/*.java \ com/genymobile/scrcpy/device/*.java \ + com/genymobile/scrcpy/opengl/*.java \ com/genymobile/scrcpy/util/*.java \ com/genymobile/scrcpy/video/*.java \ com/genymobile/scrcpy/wrappers/*.java \ diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index dae73b64..ca53d861 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -14,6 +14,7 @@ import com.genymobile.scrcpy.device.DesktopConnection; import com.genymobile.scrcpy.device.Device; import com.genymobile.scrcpy.device.NewDisplay; import com.genymobile.scrcpy.device.Streamer; +import com.genymobile.scrcpy.opengl.OpenGLRunner; import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.LogUtils; import com.genymobile.scrcpy.video.CameraCapture; @@ -191,6 +192,8 @@ public final class Server { asyncProcessor.stop(); } + OpenGLRunner.quit(); // quit the OpenGL thread, if any + connection.shutdown(); try { @@ -200,6 +203,7 @@ public final class Server { for (AsyncProcessor asyncProcessor : asyncProcessors) { asyncProcessor.join(); } + OpenGLRunner.join(); } catch (InterruptedException e) { // ignore } diff --git a/server/src/main/java/com/genymobile/scrcpy/opengl/AffineOpenGLFilter.java b/server/src/main/java/com/genymobile/scrcpy/opengl/AffineOpenGLFilter.java new file mode 100644 index 00000000..7608a574 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/opengl/AffineOpenGLFilter.java @@ -0,0 +1,135 @@ +package com.genymobile.scrcpy.opengl; + +import com.genymobile.scrcpy.util.AffineMatrix; + +import android.opengl.GLES11Ext; +import android.opengl.GLES20; + +import java.nio.FloatBuffer; + +public class AffineOpenGLFilter implements OpenGLFilter { + + private int program; + private FloatBuffer vertexBuffer; + private FloatBuffer texCoordsBuffer; + private final float[] userMatrix; + + private int vertexPosLoc; + private int texCoordsInLoc; + + private int texLoc; + private int texMatrixLoc; + private int userMatrixLoc; + + public AffineOpenGLFilter(AffineMatrix transform) { + userMatrix = transform.to4x4(); + } + + @Override + public void init() throws OpenGLException { + // @formatter:off + String vertexShaderCode = "#version 100\n" + + "attribute vec4 vertex_pos;\n" + + "attribute vec4 tex_coords_in;\n" + + "varying vec2 tex_coords;\n" + + "uniform mat4 tex_matrix;\n" + + "uniform mat4 user_matrix;\n" + + "void main() {\n" + + " gl_Position = vertex_pos;\n" + + " tex_coords = (tex_matrix * user_matrix * tex_coords_in).xy;\n" + + "}"; + + // @formatter:off + String fragmentShaderCode = "#version 100\n" + + "#extension GL_OES_EGL_image_external : require\n" + + "precision highp float;\n" + + "uniform samplerExternalOES tex;\n" + + "varying vec2 tex_coords;\n" + + "void main() {\n" + + " if (tex_coords.x >= 0.0 && tex_coords.x <= 1.0\n" + + " && tex_coords.y >= 0.0 && tex_coords.y <= 1.0) {\n" + + " gl_FragColor = texture2D(tex, tex_coords);\n" + + " } else {\n" + + " gl_FragColor = vec4(0.0);\n" + + " }\n" + + "}"; + + program = GLUtils.createProgram(vertexShaderCode, fragmentShaderCode); + if (program == 0) { + throw new OpenGLException("Cannot create OpenGL program"); + } + + float[] vertices = { + -1, -1, // Bottom-left + 1, -1, // Bottom-right + -1, 1, // Top-left + 1, 1, // Top-right + }; + + float[] texCoords = { + 0, 0, // Bottom-left + 1, 0, // Bottom-right + 0, 1, // Top-left + 1, 1, // Top-right + }; + + // OpenGL will fill the 3rd and 4th coordinates of the vec4 automatically with 0.0 and 1.0 respectively + vertexBuffer = GLUtils.createFloatBuffer(vertices); + texCoordsBuffer = GLUtils.createFloatBuffer(texCoords); + + vertexPosLoc = GLES20.glGetAttribLocation(program, "vertex_pos"); + assert vertexPosLoc != -1; + + texCoordsInLoc = GLES20.glGetAttribLocation(program, "tex_coords_in"); + assert texCoordsInLoc != -1; + + texLoc = GLES20.glGetUniformLocation(program, "tex"); + assert texLoc != -1; + + texMatrixLoc = GLES20.glGetUniformLocation(program, "tex_matrix"); + assert texMatrixLoc != -1; + + userMatrixLoc = GLES20.glGetUniformLocation(program, "user_matrix"); + assert userMatrixLoc != -1; + } + + @Override + public void draw(int textureId, float[] texMatrix) { + GLES20.glUseProgram(program); + GLUtils.checkGlError(); + + GLES20.glEnableVertexAttribArray(vertexPosLoc); + GLUtils.checkGlError(); + GLES20.glEnableVertexAttribArray(texCoordsInLoc); + GLUtils.checkGlError(); + + GLES20.glVertexAttribPointer(vertexPosLoc, 2, GLES20.GL_FLOAT, false, 0, vertexBuffer); + GLUtils.checkGlError(); + GLES20.glVertexAttribPointer(texCoordsInLoc, 2, GLES20.GL_FLOAT, false, 0, texCoordsBuffer); + GLUtils.checkGlError(); + + GLES20.glActiveTexture(GLES20.GL_TEXTURE0); + GLUtils.checkGlError(); + GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureId); + GLUtils.checkGlError(); + GLES20.glUniform1i(texLoc, 0); + GLUtils.checkGlError(); + + GLES20.glUniformMatrix4fv(texMatrixLoc, 1, false, texMatrix, 0); + GLUtils.checkGlError(); + + GLES20.glUniformMatrix4fv(userMatrixLoc, 1, false, userMatrix, 0); + GLUtils.checkGlError(); + + GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); + GLUtils.checkGlError(); + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); + GLUtils.checkGlError(); + } + + @Override + public void release() { + GLES20.glDeleteProgram(program); + GLUtils.checkGlError(); + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/opengl/GLUtils.java b/server/src/main/java/com/genymobile/scrcpy/opengl/GLUtils.java new file mode 100644 index 00000000..72a3f400 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/opengl/GLUtils.java @@ -0,0 +1,124 @@ +package com.genymobile.scrcpy.opengl; + +import com.genymobile.scrcpy.BuildConfig; +import com.genymobile.scrcpy.util.Ln; + +import android.opengl.GLES20; +import android.opengl.GLU; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; + +public final class GLUtils { + + private static final boolean DEBUG = BuildConfig.DEBUG; + + private GLUtils() { + // not instantiable + } + + public static int createProgram(String vertexSource, String fragmentSource) { + int vertexShader = createShader(GLES20.GL_VERTEX_SHADER, vertexSource); + if (vertexShader == 0) { + return 0; + } + + int fragmentShader = createShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource); + if (fragmentShader == 0) { + GLES20.glDeleteShader(vertexShader); + return 0; + } + + int program = GLES20.glCreateProgram(); + if (program == 0) { + GLES20.glDeleteShader(fragmentShader); + GLES20.glDeleteShader(vertexShader); + return 0; + } + + GLES20.glAttachShader(program, vertexShader); + checkGlError(); + GLES20.glAttachShader(program, fragmentShader); + checkGlError(); + GLES20.glLinkProgram(program); + checkGlError(); + + int[] linkStatus = new int[1]; + GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0); + if (linkStatus[0] == 0) { + Ln.e("Could not link program: " + GLES20.glGetProgramInfoLog(program)); + GLES20.glDeleteProgram(program); + GLES20.glDeleteShader(fragmentShader); + GLES20.glDeleteShader(vertexShader); + return 0; + } + + return program; + } + + public static int createShader(int type, String source) { + int shader = GLES20.glCreateShader(type); + if (shader == 0) { + Ln.e(getGlErrorMessage("Could not create shader")); + return 0; + } + + GLES20.glShaderSource(shader, source); + GLES20.glCompileShader(shader); + + int[] compileStatus = new int[1]; + GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compileStatus, 0); + if (compileStatus[0] == 0) { + Ln.e("Could not compile " + getShaderTypeString(type) + ": " + GLES20.glGetShaderInfoLog(shader)); + GLES20.glDeleteShader(shader); + return 0; + } + + return shader; + } + + private static String getShaderTypeString(int type) { + switch (type) { + case GLES20.GL_VERTEX_SHADER: + return "vertex shader"; + case GLES20.GL_FRAGMENT_SHADER: + return "fragment shader"; + default: + return "shader"; + } + } + + /** + * Throws a runtime exception if {@link GLES20#glGetError()} returns an error (useful for debugging). + */ + public static void checkGlError() { + if (DEBUG) { + int error = GLES20.glGetError(); + if (error != GLES20.GL_NO_ERROR) { + throw new RuntimeException(toErrorString(error)); + } + } + } + + public static String getGlErrorMessage(String userError) { + int glError = GLES20.glGetError(); + if (glError == GLES20.GL_NO_ERROR) { + return userError; + } + + return userError + " (" + toErrorString(glError) + ")"; + } + + private static String toErrorString(int glError) { + String errorString = GLU.gluErrorString(glError); + return "glError 0x" + Integer.toHexString(glError) + " " + errorString; + } + + public static FloatBuffer createFloatBuffer(float[] values) { + FloatBuffer fb = ByteBuffer.allocateDirect(values.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer(); + fb.put(values); + fb.position(0); + return fb; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/opengl/OpenGLException.java b/server/src/main/java/com/genymobile/scrcpy/opengl/OpenGLException.java new file mode 100644 index 00000000..cbc9539b --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/opengl/OpenGLException.java @@ -0,0 +1,13 @@ +package com.genymobile.scrcpy.opengl; + +import java.io.IOException; + +public class OpenGLException extends IOException { + public OpenGLException(String message) { + super(message); + } + + public OpenGLException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/opengl/OpenGLFilter.java b/server/src/main/java/com/genymobile/scrcpy/opengl/OpenGLFilter.java new file mode 100644 index 00000000..6f27777e --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/opengl/OpenGLFilter.java @@ -0,0 +1,21 @@ +package com.genymobile.scrcpy.opengl; + +public interface OpenGLFilter { + + /** + * Initialize the OpenGL filter (typically compile the shaders and create the program). + * + * @throws OpenGLException if an initialization error occurs + */ + void init() throws OpenGLException; + + /** + * Render a frame (call for each frame). + */ + void draw(int textureId, float[] texMatrix); + + /** + * Release resources. + */ + void release(); +} diff --git a/server/src/main/java/com/genymobile/scrcpy/opengl/OpenGLRunner.java b/server/src/main/java/com/genymobile/scrcpy/opengl/OpenGLRunner.java new file mode 100644 index 00000000..a3f9335c --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/opengl/OpenGLRunner.java @@ -0,0 +1,246 @@ +package com.genymobile.scrcpy.opengl; + +import com.genymobile.scrcpy.device.Size; + +import android.graphics.SurfaceTexture; +import android.opengl.EGL14; +import android.opengl.EGLConfig; +import android.opengl.EGLContext; +import android.opengl.EGLDisplay; +import android.opengl.EGLExt; +import android.opengl.EGLSurface; +import android.opengl.GLES11Ext; +import android.opengl.GLES20; +import android.os.Handler; +import android.os.HandlerThread; +import android.view.Surface; + +import java.util.concurrent.Semaphore; + +public final class OpenGLRunner { + + private static HandlerThread handlerThread; + private static Handler handler; + private static boolean quit; + + private EGLDisplay eglDisplay; + private EGLContext eglContext; + private EGLSurface eglSurface; + + private final OpenGLFilter filter; + + private SurfaceTexture surfaceTexture; + private Surface inputSurface; + private int textureId; + + private boolean stopped; + + public OpenGLRunner(OpenGLFilter filter) { + this.filter = filter; + } + + public static synchronized void initOnce() { + if (handlerThread == null) { + if (quit) { + throw new IllegalStateException("Could not init OpenGLRunner after it is quit"); + } + handlerThread = new HandlerThread("OpenGLRunner"); + handlerThread.start(); + handler = new Handler(handlerThread.getLooper()); + } + } + + public static void quit() { + HandlerThread thread; + synchronized (OpenGLRunner.class) { + thread = handlerThread; + quit = true; + } + if (thread != null) { + thread.quitSafely(); + } + } + + public static void join() throws InterruptedException { + HandlerThread thread; + synchronized (OpenGLRunner.class) { + thread = handlerThread; + } + if (thread != null) { + thread.join(); + } + } + + public Surface start(Size inputSize, Size outputSize, Surface outputSurface) throws OpenGLException { + initOnce(); + + // Simulate CompletableFuture, but working for all Android versions + final Semaphore sem = new Semaphore(0); + Throwable[] throwableRef = new Throwable[1]; + + // The whole OpenGL execution must be performed on a Handler, so that SurfaceTexture.setOnFrameAvailableListener() works correctly. + // See + handler.post(() -> { + try { + run(inputSize, outputSize, outputSurface); + } catch (Throwable throwable) { + throwableRef[0] = throwable; + } finally { + sem.release(); + } + }); + + try { + sem.acquire(); + } catch (InterruptedException e) { + // Behave as if this method call was synchronous + Thread.currentThread().interrupt(); + } + + Throwable throwable = throwableRef[0]; + if (throwable != null) { + if (throwable instanceof OpenGLException) { + throw (OpenGLException) throwable; + } + throw new OpenGLException("Asynchronous OpenGL runner init failed", throwable); + } + + // Synchronization is ok: inputSurface is written before sem.release() and read after sem.acquire() + return inputSurface; + } + + private void run(Size inputSize, Size outputSize, Surface outputSurface) throws OpenGLException { + eglDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY); + if (eglDisplay == EGL14.EGL_NO_DISPLAY) { + throw new OpenGLException("Unable to get EGL14 display"); + } + + int[] version = new int[2]; + if (!EGL14.eglInitialize(eglDisplay, version, 0, version, 1)) { + throw new OpenGLException("Unable to initialize EGL14"); + } + + // @formatter:off + int[] attribList = { + EGL14.EGL_RED_SIZE, 8, + EGL14.EGL_GREEN_SIZE, 8, + EGL14.EGL_BLUE_SIZE, 8, + EGL14.EGL_ALPHA_SIZE, 8, + EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT, + EGL14.EGL_NONE + }; + + EGLConfig[] configs = new EGLConfig[1]; + int[] numConfigs = new int[1]; + EGL14.eglChooseConfig(eglDisplay, attribList, 0, configs, 0, configs.length, numConfigs, 0); + if (numConfigs[0] <= 0) { + EGL14.eglTerminate(eglDisplay); + throw new OpenGLException("Unable to find ES2 EGL config"); + } + EGLConfig eglConfig = configs[0]; + + // @formatter:off + int[] contextAttribList = { + EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, + EGL14.EGL_NONE + }; + eglContext = EGL14.eglCreateContext(eglDisplay, eglConfig, EGL14.EGL_NO_CONTEXT, contextAttribList, 0); + if (eglContext == null) { + EGL14.eglTerminate(eglDisplay); + throw new OpenGLException("Failed to create EGL context"); + } + + int[] surfaceAttribList = { + EGL14.EGL_NONE + }; + eglSurface = EGL14.eglCreateWindowSurface(eglDisplay, eglConfig, outputSurface, surfaceAttribList, 0); + if (eglSurface == null) { + EGL14.eglDestroyContext(eglDisplay, eglContext); + EGL14.eglTerminate(eglDisplay); + throw new OpenGLException("Failed to create EGL window surface"); + } + + if (!EGL14.eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext)) { + EGL14.eglDestroySurface(eglDisplay, eglSurface); + EGL14.eglDestroyContext(eglDisplay, eglContext); + EGL14.eglTerminate(eglDisplay); + throw new OpenGLException("Failed to make EGL context current"); + } + + int[] textures = new int[1]; + GLES20.glGenTextures(1, textures, 0); + GLUtils.checkGlError(); + textureId = textures[0]; + + GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR); + GLUtils.checkGlError(); + GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); + GLUtils.checkGlError(); + GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE); + GLUtils.checkGlError(); + GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE); + GLUtils.checkGlError(); + + surfaceTexture = new SurfaceTexture(textureId); + surfaceTexture.setDefaultBufferSize(inputSize.getWidth(), inputSize.getHeight()); + inputSurface = new Surface(surfaceTexture); + + filter.init(); + + surfaceTexture.setOnFrameAvailableListener(surfaceTexture -> { + if (stopped) { + // Make sure to never render after resources have been released + return; + } + + render(outputSize); + }, handler); + } + + private void render(Size outputSize) { + GLES20.glViewport(0, 0, outputSize.getWidth(), outputSize.getHeight()); + GLUtils.checkGlError(); + + surfaceTexture.updateTexImage(); + float[] matrix = new float[16]; + surfaceTexture.getTransformMatrix(matrix); + + filter.draw(textureId, matrix); + + EGLExt.eglPresentationTimeANDROID(eglDisplay, eglSurface, surfaceTexture.getTimestamp()); + EGL14.eglSwapBuffers(eglDisplay, eglSurface); + } + + public void stopAndRelease() { + final Semaphore sem = new Semaphore(0); + + handler.post(() -> { + stopped = true; + surfaceTexture.setOnFrameAvailableListener(null, handler); + + filter.release(); + + int[] textures = {textureId}; + GLES20.glDeleteTextures(1, textures, 0); + GLUtils.checkGlError(); + + EGL14.eglDestroySurface(eglDisplay, eglSurface); + EGL14.eglDestroyContext(eglDisplay, eglContext); + EGL14.eglTerminate(eglDisplay); + eglDisplay = EGL14.EGL_NO_DISPLAY; + eglContext = EGL14.EGL_NO_CONTEXT; + eglSurface = EGL14.EGL_NO_SURFACE; + surfaceTexture.release(); + inputSurface.release(); + + sem.release(); + }); + + try { + sem.acquire(); + } catch (InterruptedException e) { + // Behave as if this method call was synchronous + Thread.currentThread().interrupt(); + } + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/util/AffineMatrix.java b/server/src/main/java/com/genymobile/scrcpy/util/AffineMatrix.java new file mode 100644 index 00000000..0db74af6 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/util/AffineMatrix.java @@ -0,0 +1,368 @@ +package com.genymobile.scrcpy.util; + +import com.genymobile.scrcpy.device.Point; +import com.genymobile.scrcpy.device.Size; + +/** + * Represents a 2D affine transform (a 3x3 matrix): + * + *

+ *     / a c e \
+ *     | b d f |
+ *     \ 0 0 1 /
+ * 
+ *

+ * Or, a 4x4 matrix if we add a z axis: + * + *

+ *     / a c 0 e \
+ *     | b d 0 f |
+ *     | 0 0 1 0 |
+ *     \ 0 0 0 1 /
+ * 
+ */ +public class AffineMatrix { + + private final double a, b, c, d, e, f; + + /** + * The identity matrix. + */ + public static final AffineMatrix IDENTITY = new AffineMatrix(1, 0, 0, 1, 0, 0); + + /** + * Create a new matrix: + * + *
+     *     / a c e \
+     *     | b d f |
+     *     \ 0 0 1 /
+     * 
+ */ + public AffineMatrix(double a, double b, double c, double d, double e, double f) { + this.a = a; + this.b = b; + this.c = c; + this.d = d; + this.e = e; + this.f = f; + } + + @Override + public String toString() { + return "[" + a + ", " + c + ", " + e + "; " + b + ", " + d + ", " + f + "]"; + } + + /** + * Return a matrix which converts from Normalized Device Coordinates to pixels. + * + * @param size the target size + * @return the transform matrix + */ + public static AffineMatrix ndcFromPixels(Size size) { + double w = size.getWidth(); + double h = size.getHeight(); + return new AffineMatrix(1 / w, 0, 0, -1 / h, 0, 1); + } + + /** + * Return a matrix which converts from pixels to Normalized Device Coordinates. + * + * @param size the source size + * @return the transform matrix + */ + public static AffineMatrix ndcToPixels(Size size) { + double w = size.getWidth(); + double h = size.getHeight(); + return new AffineMatrix(w, 0, 0, -h, 0, h); + } + + /** + * Apply the transform to a point ({@code this} should be a matrix converted to pixels coordinates via {@link #ndcToPixels(Size)}). + * + * @param point the source point + * @return the converted point + */ + public Point apply(Point point) { + int x = point.getX(); + int y = point.getY(); + int xx = (int) (a * x + c * y + e); + int yy = (int) (b * x + d * y + f); + return new Point(xx, yy); + } + + /** + * Compute this * rhs. + * + * @param rhs the matrix to multiply + * @return the product + */ + public AffineMatrix multiply(AffineMatrix rhs) { + if (rhs == null) { + // For convenience + return this; + } + + double aa = this.a * rhs.a + this.c * rhs.b; + double bb = this.b * rhs.a + this.d * rhs.b; + double cc = this.a * rhs.c + this.c * rhs.d; + double dd = this.b * rhs.c + this.d * rhs.d; + double ee = this.a * rhs.e + this.c * rhs.f + this.e; + double ff = this.b * rhs.e + this.d * rhs.f + this.f; + return new AffineMatrix(aa, bb, cc, dd, ee, ff); + } + + /** + * Multiply all matrices from left to right, ignoring any {@code null} matrix (for convenience). + * + * @param matrices the matrices + * @return the product + */ + public static AffineMatrix multiplyAll(AffineMatrix... matrices) { + AffineMatrix result = null; + for (AffineMatrix matrix : matrices) { + if (result == null) { + result = matrix; + } else { + result = result.multiply(matrix); + } + } + return result; + } + + /** + * Invert the matrix. + * + * @return the inverse matrix (or {@code null} if not invertible). + */ + public AffineMatrix invert() { + // The 3x3 matrix M can be decomposed into M = M1 * M2: + // M1 M2 + // / 1 0 e \ / a c 0 \ + // | 0 1 f | * | b d 0 | + // \ 0 0 1 / \ 0 0 1 / + // + // The inverse of an invertible 2x2 matrix is given by this formula: + // + // / A B \⁻¹ 1 / D -B \ + // \ C D / = ----- \ -C A / + // AD-BC + // + // Let B=c and C=b (to apply the general formula with the same letters). + // + // M⁻¹ = (M1 * M2)⁻¹ = M2⁻¹ * M1⁻¹ + // + // M2⁻¹ M1⁻¹ + // /----------------\ + // 1 / d -B 0 \ / 1 0 -e \ + // = ----- | -C a 0 | * | 0 1 -f | + // ad-BC \ 0 0 1 / \ 0 0 1 / + // + // With the original letters: + // + // 1 / d -c 0 \ / 1 0 -e \ + // M⁻¹ = ----- | -b a 0 | * | 0 1 -f | + // ad-cb \ 0 0 1 / \ 0 0 1 / + // + // 1 / d -c cf-de \ + // = ----- | -b a be-af | + // ad-cb \ 0 0 1 / + + double det = a * d - c * b; + if (det == 0) { + // Not invertible + return null; + } + + double aa = d / det; + double bb = -b / det; + double cc = -c / det; + double dd = a / det; + double ee = (c * f - d * e) / det; + double ff = (b * e - a * f) / det; + + return new AffineMatrix(aa, bb, cc, dd, ee, ff); + } + + /** + * Return this transform applied from the center (0.5, 0.5). + * + * @return the resulting matrix + */ + public AffineMatrix fromCenter() { + return translate(0.5, 0.5).multiply(this).multiply(translate(-0.5, -0.5)); + } + + /** + * Return this transform with the specified aspect ratio. + * + * @param ar the aspect ratio + * @return the resulting matrix + */ + public AffineMatrix withAspectRatio(double ar) { + return scale(1 / ar, 1).multiply(this).multiply(scale(ar, 1)); + } + + /** + * Return this transform with the specified aspect ratio. + * + * @param size the size describing the aspect ratio + * @return the transform + */ + public AffineMatrix withAspectRatio(Size size) { + double ar = (double) size.getWidth() / size.getHeight(); + return withAspectRatio(ar); + } + + /** + * Return a translation matrix. + * + * @param x the horizontal translation + * @param y the vertical translation + * @return the matrix + */ + public static AffineMatrix translate(double x, double y) { + return new AffineMatrix(1, 0, 0, 1, x, y); + } + + /** + * Return a scaling matrix. + * + * @param x the horizontal scaling + * @param y the vertical scaling + * @return the matrix + */ + public static AffineMatrix scale(double x, double y) { + return new AffineMatrix(x, 0, 0, y, 0, 0); + } + + /** + * Return a scaling matrix. + * + * @param from the source size + * @param to the destination size + * @return the matrix + */ + public static AffineMatrix scale(Size from, Size to) { + double scaleX = (double) to.getWidth() / from.getWidth(); + double scaleY = (double) to.getHeight() / from.getHeight(); + return scale(scaleX, scaleY); + } + + /** + * Return a matrix applying a "reframing" (cropping a rectangle). + *

+ * (x, y) is the bottom-left corner, (w, h) is the size of the rectangle. + * + * @param x horizontal coordinate (increasing to the right) + * @param y vertical coordinate (increasing upwards) + * @param w width + * @param h height + * @return the matrix + */ + public static AffineMatrix reframe(double x, double y, double w, double h) { + if (w == 0 || h == 0) { + throw new IllegalArgumentException("Cannot reframe to an empty area: " + w + "x" + h); + } + return scale(1 / w, 1 / h).multiply(translate(-x, -y)); + } + + /** + * Return an orthogonal rotation matrix. + * + * @param ccwRotation the counter-clockwise rotation + * @return the matrix + */ + public static AffineMatrix rotateOrtho(int ccwRotation) { + switch (ccwRotation) { + case 0: + return IDENTITY; + case 1: + // 90° counter-clockwise + return new AffineMatrix(0, 1, -1, 0, 1, 0); + case 2: + // 180° + return new AffineMatrix(-1, 0, 0, -1, 1, 1); + case 3: + // 90° clockwise + return new AffineMatrix(0, -1, 1, 0, 0, 1); + default: + throw new IllegalArgumentException("Invalid rotation: " + ccwRotation); + } + } + + /** + * Return an horizontal flip matrix. + * + * @return the matrix + */ + public static AffineMatrix hflip() { + return new AffineMatrix(-1, 0, 0, 1, 1, 0); + } + + /** + * Return a vertical flip matrix. + * + * @return the matrix + */ + public static AffineMatrix vflip() { + return new AffineMatrix(1, 0, 0, -1, 0, 1); + } + + /** + * Return a rotation matrix. + * + * @param ccwDegrees the angle, in degrees (counter-clockwise) + * @return the matrix + */ + public static AffineMatrix rotate(double ccwDegrees) { + double radians = Math.toRadians(ccwDegrees); + double cos = Math.cos(radians); + double sin = Math.sin(radians); + return new AffineMatrix(cos, sin, -sin, cos, 0, 0); + } + + /** + * Export this affine transform to a 4x4 column-major order matrix. + * + * @param matrix output 4x4 matrix + */ + public void to4x4(float[] matrix) { + // matrix is a 4x4 matrix in column-major order + + // Column 0 + matrix[0] = (float) a; + matrix[1] = (float) b; + matrix[2] = 0; + matrix[3] = 0; + + // Column 1 + matrix[4] = (float) c; + matrix[5] = (float) d; + matrix[6] = 0; + matrix[7] = 0; + + // Column 2 + matrix[8] = 0; + matrix[9] = 0; + matrix[10] = 1; + matrix[11] = 0; + + // Column 3 + matrix[12] = (float) e; + matrix[13] = (float) f; + matrix[14] = 0; + matrix[15] = 1; + } + + /** + * Export this affine transform to a 4x4 column-major order matrix. + * + * @return 4x4 matrix + */ + public float[] to4x4() { + float[] matrix = new float[16]; + to4x4(matrix); + return matrix; + } +} From 89518f49adb59dfa961f76be3f4414bffdc22cc5 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 10 Nov 2024 09:08:49 +0100 Subject: [PATCH 2061/2244] Revert "Disable broken options on Android 14" This reverts commit d62fa8880e03e8823057a5d4d9659d5f19132806. These options will be reimplemented differently. Refs #4011 Refs #4162 PR #5455 --- app/src/cli.c | 3 +-- app/src/options.h | 2 -- .../java/com/genymobile/scrcpy/Options.java | 6 +----- .../main/java/com/genymobile/scrcpy/Server.java | 17 ----------------- .../com/genymobile/scrcpy/device/Device.java | 2 -- .../com/genymobile/scrcpy/video/ScreenInfo.java | 2 +- 6 files changed, 3 insertions(+), 29 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index ebf0f6f6..e67192bf 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -2856,8 +2856,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], SC_LOCK_VIDEO_ORIENTATION_UNLOCKED) { LOGI("Video orientation is locked for v4l2 sink. " "See --lock-video-orientation."); - opts->lock_video_orientation = - SC_LOCK_VIDEO_ORIENTATION_INITIAL_AUTO; + opts->lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_INITIAL; } // V4L2 could not handle size change. diff --git a/app/src/options.h b/app/src/options.h index 5662719a..9236c3f8 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -134,8 +134,6 @@ enum sc_lock_video_orientation { SC_LOCK_VIDEO_ORIENTATION_UNLOCKED = -1, // lock the current orientation when scrcpy starts SC_LOCK_VIDEO_ORIENTATION_INITIAL = -2, - // like SC_LOCK_VIDEO_ORIENTATION_INITIAL, but set automatically - SC_LOCK_VIDEO_ORIENTATION_INITIAL_AUTO = -3, SC_LOCK_VIDEO_ORIENTATION_0 = 0, SC_LOCK_VIDEO_ORIENTATION_90 = 3, SC_LOCK_VIDEO_ORIENTATION_180 = 2, diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 54888827..c1620432 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -32,7 +32,7 @@ public class Options { private int videoBitRate = 8000000; private int audioBitRate = 128000; private float maxFps; - private int lockVideoOrientation = Device.LOCK_VIDEO_ORIENTATION_UNLOCKED; + private int lockVideoOrientation = -1; private boolean tunnelForward; private Rect crop; private boolean control = true; @@ -259,10 +259,6 @@ public class Options { return sendCodecMeta; } - public void resetLockVideoOrientation() { - this.lockVideoOrientation = Device.LOCK_VIDEO_ORIENTATION_UNLOCKED; - } - @SuppressWarnings("MethodLength") public static Options parse(String... args) { if (args.length < 1) { diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index ca53d861..eb8b533a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -85,23 +85,6 @@ public final class Server { throw new ConfigurationException("New virtual display is not supported"); } - if (Build.VERSION.SDK_INT >= AndroidVersions.API_34_ANDROID_14) { - int lockVideoOrientation = options.getLockVideoOrientation(); - if (lockVideoOrientation != Device.LOCK_VIDEO_ORIENTATION_UNLOCKED) { - if (lockVideoOrientation != Device.LOCK_VIDEO_ORIENTATION_INITIAL_AUTO) { - Ln.e("--lock-video-orientation is broken on Android >= 14: "); - throw new ConfigurationException("--lock-video-orientation is broken on Android >= 14"); - } else { - // If the flag has been set automatically (because v4l2 sink is enabled), do not fail - Ln.w("--lock-video-orientation is ignored on Android >= 14: "); - } - } - if (options.getCrop() != null) { - Ln.e("--crop is broken on Android >= 14: "); - throw new ConfigurationException("Crop is not broken on Android >= 14"); - } - } - CleanUp cleanUp = null; if (options.getCleanup()) { diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Device.java b/server/src/main/java/com/genymobile/scrcpy/device/Device.java index e7a743f8..09c7d2b6 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Device.java @@ -42,8 +42,6 @@ public final class Device { public static final int LOCK_VIDEO_ORIENTATION_UNLOCKED = -1; public static final int LOCK_VIDEO_ORIENTATION_INITIAL = -2; - // like SC_LOCK_VIDEO_ORIENTATION_INITIAL, but set automatically - public static final int LOCK_VIDEO_ORIENTATION_INITIAL_AUTO = -3; private Device() { // not instantiable diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java index 602bd8ab..cc82a654 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java @@ -64,7 +64,7 @@ public final class ScreenInfo { } public static ScreenInfo computeScreenInfo(int rotation, Size deviceSize, Rect crop, int maxSize, int lockedVideoOrientation) { - if (lockedVideoOrientation == Device.LOCK_VIDEO_ORIENTATION_INITIAL || lockedVideoOrientation == Device.LOCK_VIDEO_ORIENTATION_INITIAL_AUTO) { + if (lockedVideoOrientation == Device.LOCK_VIDEO_ORIENTATION_INITIAL) { // The user requested to lock the video orientation to the current orientation lockedVideoOrientation = rotation; } From d6033d28f5a326409218bab15acc432b305cb23f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 10 Nov 2024 09:11:57 +0100 Subject: [PATCH 2062/2244] Split computeVideoSize() into limit() and round8() Expose two methods on Size directly: - limit() to downscale a size; - round8() to round both dimensions to multiples of 8. This will allow removing ScreenInfo completely. PR #5455 --- .../com/genymobile/scrcpy/device/Size.java | 51 +++++++++++++++++++ .../scrcpy/video/NewDisplayCapture.java | 2 +- .../genymobile/scrcpy/video/ScreenInfo.java | 30 +---------- 3 files changed, 53 insertions(+), 30 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Size.java b/server/src/main/java/com/genymobile/scrcpy/device/Size.java index 558deb00..3baa1bdd 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/Size.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Size.java @@ -29,6 +29,57 @@ public final class Size { return new Size(height, width); } + public Size limit(int maxSize) { + assert maxSize >= 0 : "Max size may not be negative"; + assert maxSize % 8 == 0 : "Max size must be a multiple of 8"; + + if (maxSize == 0) { + // No limit + return this; + } + + boolean portrait = height > width; + int major = portrait ? height : width; + if (major <= maxSize) { + return this; + } + + int minor = portrait ? width : height; + + int newMajor = maxSize; + int newMinor = maxSize * minor / major; + + int w = portrait ? newMinor : newMajor; + int h = portrait ? newMajor : newMinor; + return new Size(w, h); + } + + /** + * Round both dimensions of this size to be a multiple of 8 (as required by many encoders). + * + * @return The current size rounded. + */ + public Size round8() { + if ((width & 7) == 0 && (height & 7) == 0) { + // Already a multiple of 8 + return this; + } + + boolean portrait = height > width; + int major = portrait ? height : width; + int minor = portrait ? width : height; + + major &= ~7; // round down to not exceed the initial size + minor = (minor + 4) & ~7; // round to the nearest to minimize aspect ratio distortion + if (minor > major) { + minor = major; + } + + int w = portrait ? minor : major; + int h = portrait ? major : minor; + return new Size(w, h); + } + public Rect toRect() { return new Rect(0, 0, width, height); } diff --git a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java index 9b1c9933..b9cecbe4 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java @@ -71,7 +71,7 @@ public class NewDisplayCapture extends SurfaceCapture { @Override public void prepare() { if (!newDisplay.hasExplicitSize()) { - size = ScreenInfo.computeVideoSize(mainDisplaySize.getWidth(), mainDisplaySize.getHeight(), maxSize); + size = mainDisplaySize.limit(maxSize).round8(); } if (!newDisplay.hasExplicitDpi()) { dpi = scaleDpi(mainDisplaySize, mainDisplayDpi, size); diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java index cc82a654..010ab59a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java @@ -1,6 +1,5 @@ package com.genymobile.scrcpy.video; -import com.genymobile.scrcpy.BuildConfig; import com.genymobile.scrcpy.device.Device; import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.util.Ln; @@ -82,7 +81,7 @@ public final class ScreenInfo { } } - Size videoSize = computeVideoSize(contentRect.width(), contentRect.height(), maxSize); + Size videoSize = new Size(contentRect.width(), contentRect.height()).limit(maxSize).round8(); return new ScreenInfo(contentRect, videoSize, rotation, lockedVideoOrientation); } @@ -90,33 +89,6 @@ public final class ScreenInfo { return rect.width() + ":" + rect.height() + ":" + rect.left + ":" + rect.top; } - public 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 019ce5eea4e8d0655ab481ab59ac498c3be51d0f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 10 Nov 2024 10:04:01 +0100 Subject: [PATCH 2063/2244] Temporarily ignore lock video orientation and crop Get rid of old code implementing --lock-video-orientation and --crop features on the device side. They will be reimplemented differently. Refs #4011 Refs #4162 PR #5455 --- .../scrcpy/control/PositionMapper.java | 32 +---- .../scrcpy/video/NewDisplayCapture.java | 4 +- .../scrcpy/video/ScreenCapture.java | 31 ++--- .../genymobile/scrcpy/video/ScreenInfo.java | 121 ------------------ 4 files changed, 19 insertions(+), 169 deletions(-) delete mode 100644 server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java diff --git a/server/src/main/java/com/genymobile/scrcpy/control/PositionMapper.java b/server/src/main/java/com/genymobile/scrcpy/control/PositionMapper.java index 2ebb5961..7b546652 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/PositionMapper.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/PositionMapper.java @@ -3,46 +3,28 @@ package com.genymobile.scrcpy.control; import com.genymobile.scrcpy.device.Point; import com.genymobile.scrcpy.device.Position; import com.genymobile.scrcpy.device.Size; -import com.genymobile.scrcpy.video.ScreenInfo; - -import android.graphics.Rect; public final class PositionMapper { + private final Size sourceSize; private final Size videoSize; - private final Rect contentRect; - private final int coordsRotation; - public PositionMapper(Size videoSize, Rect contentRect, int videoRotation) { + public PositionMapper(Size sourceSize, Size videoSize) { + this.sourceSize = sourceSize; this.videoSize = videoSize; - this.contentRect = contentRect; - this.coordsRotation = reverseRotation(videoRotation); - } - - public static PositionMapper from(ScreenInfo screenInfo) { - // ignore the locked video orientation, the events will apply in coordinates considered in the physical device orientation - Size videoSize = screenInfo.getUnlockedVideoSize(); - return new PositionMapper(videoSize, screenInfo.getContentRect(), screenInfo.getVideoRotation()); - } - - private static int reverseRotation(int rotation) { - return (4 - rotation) % 4; } public Point map(Position position) { - // reverse the video rotation to apply the events - Position devicePosition = position.rotate(coordsRotation); - - Size clientVideoSize = devicePosition.getScreenSize(); + Size clientVideoSize = position.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; } - Point point = devicePosition.getPoint(); - int convertedX = contentRect.left + point.getX() * contentRect.width() / videoSize.getWidth(); - int convertedY = contentRect.top + point.getY() * contentRect.height() / videoSize.getHeight(); + Point point = position.getPoint(); + int convertedX = point.getX() * sourceSize.getWidth() / videoSize.getWidth(); + int convertedY = point.getY() * sourceSize.getHeight() / videoSize.getHeight(); return new Point(convertedX, convertedY); } } diff --git a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java index b9cecbe4..f657d1a8 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java @@ -9,7 +9,6 @@ import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.wrappers.ServiceManager; -import android.graphics.Rect; import android.hardware.display.DisplayManager; import android.hardware.display.VirtualDisplay; import android.os.Build; @@ -107,8 +106,7 @@ public class NewDisplayCapture extends SurfaceCapture { } if (vdListener != null) { - Rect contentRect = new Rect(0, 0, size.getWidth(), size.getHeight()); - PositionMapper positionMapper = new PositionMapper(size, contentRect, 0); + PositionMapper positionMapper = new PositionMapper(size, size); vdListener.onNewVirtualDisplay(virtualDisplayId, positionMapper); } } diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java index 00d855bd..00ee89d8 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java @@ -28,11 +28,9 @@ public class ScreenCapture extends SurfaceCapture { private final VirtualDisplayListener vdListener; private final int displayId; private int maxSize; - private final Rect crop; - private final int lockVideoOrientation; private DisplayInfo displayInfo; - private ScreenInfo screenInfo; + private Size videoSize; // Source display size (before resizing/crop) for the current session private Size sessionDisplaySize; @@ -55,8 +53,6 @@ public class ScreenCapture extends SurfaceCapture { this.displayId = options.getDisplayId(); assert displayId != Device.DISPLAY_ID_NONE; this.maxSize = options.getMaxSize(); - this.crop = options.getCrop(); - this.lockVideoOrientation = options.getLockVideoOrientation(); } @Override @@ -126,8 +122,9 @@ public class ScreenCapture extends SurfaceCapture { Ln.w("Display doesn't have FLAG_SUPPORTS_PROTECTED_BUFFERS flag, mirroring can be restricted"); } - setSessionDisplaySize(displayInfo.getSize()); - screenInfo = ScreenInfo.computeScreenInfo(displayInfo.getRotation(), displayInfo.getSize(), crop, maxSize, lockVideoOrientation); + Size displaySize = displayInfo.getSize(); + setSessionDisplaySize(displaySize); + videoSize = displaySize.limit(maxSize).round8(); } @Override @@ -144,28 +141,22 @@ public class ScreenCapture extends SurfaceCapture { int virtualDisplayId; PositionMapper positionMapper; try { - Size videoSize = screenInfo.getVideoSize(); virtualDisplay = ServiceManager.getDisplayManager() .createVirtualDisplay("scrcpy", videoSize.getWidth(), videoSize.getHeight(), displayId, surface); virtualDisplayId = virtualDisplay.getDisplay().getDisplayId(); - Rect contentRect = new Rect(0, 0, videoSize.getWidth(), videoSize.getHeight()); // The position are relative to the virtual display, not the original display - positionMapper = new PositionMapper(videoSize, contentRect, 0); + positionMapper = new PositionMapper(videoSize, videoSize); Ln.d("Display: using DisplayManager API"); } catch (Exception displayManagerException) { try { display = createDisplay(); - Rect contentRect = screenInfo.getContentRect(); - - // does not include the locked video orientation - Rect unlockedVideoRect = screenInfo.getUnlockedVideoSize().toRect(); - int videoRotation = screenInfo.getVideoRotation(); + Size deviceSize = displayInfo.getSize(); int layerStack = displayInfo.getLayerStack(); - setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack); + setDisplaySurface(display, surface, deviceSize.toRect(), videoSize.toRect(), layerStack); virtualDisplayId = displayId; - positionMapper = PositionMapper.from(screenInfo); + positionMapper = new PositionMapper(deviceSize, videoSize); Ln.d("Display: using SurfaceControl API"); } catch (Exception surfaceControlException) { Ln.e("Could not create display using DisplayManager", displayManagerException); @@ -206,7 +197,7 @@ public class ScreenCapture extends SurfaceCapture { @Override public Size getSize() { - return screenInfo.getVideoSize(); + return videoSize; } @Override @@ -223,11 +214,11 @@ public class ScreenCapture extends SurfaceCapture { return SurfaceControl.createDisplay("scrcpy", secure); } - private static void setDisplaySurface(IBinder display, Surface surface, int orientation, Rect deviceRect, Rect displayRect, int layerStack) { + private static void setDisplaySurface(IBinder display, Surface surface, Rect deviceRect, Rect displayRect, int layerStack) { SurfaceControl.openTransaction(); try { SurfaceControl.setDisplaySurface(display, surface); - SurfaceControl.setDisplayProjection(display, orientation, deviceRect, displayRect); + SurfaceControl.setDisplayProjection(display, 0, deviceRect, displayRect); SurfaceControl.setDisplayLayerStack(display, layerStack); } finally { SurfaceControl.closeTransaction(); diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java deleted file mode 100644 index 010ab59a..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java +++ /dev/null @@ -1,121 +0,0 @@ -package com.genymobile.scrcpy.video; - -import com.genymobile.scrcpy.device.Device; -import com.genymobile.scrcpy.device.Size; -import com.genymobile.scrcpy.util.Ln; - -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. - *

- * However, it does not include the locked video orientation. - */ - private final Size unlockedVideoSize; - - /** - * Device rotation, related to the natural device orientation (0, 1, 2 or 3) - */ - private final 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.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() { - if (getVideoRotation() % 2 == 0) { - return unlockedVideoSize; - } - - return unlockedVideoSize.rotate(); - } - - public static ScreenInfo computeScreenInfo(int rotation, Size deviceSize, Rect crop, int maxSize, int lockedVideoOrientation) { - if (lockedVideoOrientation == Device.LOCK_VIDEO_ORIENTATION_INITIAL) { - // The user requested to lock the video orientation to the current orientation - lockedVideoOrientation = rotation; - } - - 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 = new Size(contentRect.width(), contentRect.height()).limit(maxSize).round8(); - return new ScreenInfo(contentRect, videoSize, rotation, lockedVideoOrientation); - } - - private static String formatCrop(Rect rect) { - return rect.width() + ":" + rect.height() + ":" + rect.left + ":" + rect.top; - } - - 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 e226950cfab9b9e8f2fe8620dc9970bcf13133ee Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 10 Nov 2024 13:47:14 +0100 Subject: [PATCH 2064/2244] Make PositionMapper use affine transforms This will allow applying transformations performed by video filters. PR #5455 --- .../genymobile/scrcpy/control/PositionMapper.java | 14 ++++++++------ .../genymobile/scrcpy/video/NewDisplayCapture.java | 2 +- .../com/genymobile/scrcpy/video/ScreenCapture.java | 7 +++++-- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/control/PositionMapper.java b/server/src/main/java/com/genymobile/scrcpy/control/PositionMapper.java index 7b546652..cf9b25ab 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/PositionMapper.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/PositionMapper.java @@ -3,15 +3,16 @@ package com.genymobile.scrcpy.control; import com.genymobile.scrcpy.device.Point; import com.genymobile.scrcpy.device.Position; import com.genymobile.scrcpy.device.Size; +import com.genymobile.scrcpy.util.AffineMatrix; public final class PositionMapper { - private final Size sourceSize; private final Size videoSize; + private final AffineMatrix videoToDeviceMatrix; - public PositionMapper(Size sourceSize, Size videoSize) { - this.sourceSize = sourceSize; + public PositionMapper(Size videoSize, AffineMatrix videoToDeviceMatrix) { this.videoSize = videoSize; + this.videoToDeviceMatrix = videoToDeviceMatrix; } public Point map(Position position) { @@ -23,8 +24,9 @@ public final class PositionMapper { } Point point = position.getPoint(); - int convertedX = point.getX() * sourceSize.getWidth() / videoSize.getWidth(); - int convertedY = point.getY() * sourceSize.getHeight() / videoSize.getHeight(); - return new Point(convertedX, convertedY); + if (videoToDeviceMatrix != null) { + point = videoToDeviceMatrix.apply(point); + } + return point; } } diff --git a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java index f657d1a8..cc54876a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java @@ -106,7 +106,7 @@ public class NewDisplayCapture extends SurfaceCapture { } if (vdListener != null) { - PositionMapper positionMapper = new PositionMapper(size, size); + PositionMapper positionMapper = new PositionMapper(size, null); vdListener.onNewVirtualDisplay(virtualDisplayId, positionMapper); } } diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java index 00ee89d8..8873cb6d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java @@ -7,6 +7,7 @@ import com.genymobile.scrcpy.device.ConfigurationException; import com.genymobile.scrcpy.device.Device; import com.genymobile.scrcpy.device.DisplayInfo; import com.genymobile.scrcpy.device.Size; +import com.genymobile.scrcpy.util.AffineMatrix; import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.LogUtils; import com.genymobile.scrcpy.wrappers.DisplayManager; @@ -145,7 +146,7 @@ public class ScreenCapture extends SurfaceCapture { .createVirtualDisplay("scrcpy", videoSize.getWidth(), videoSize.getHeight(), displayId, surface); virtualDisplayId = virtualDisplay.getDisplay().getDisplayId(); // The position are relative to the virtual display, not the original display - positionMapper = new PositionMapper(videoSize, videoSize); + positionMapper = new PositionMapper(videoSize, null); Ln.d("Display: using DisplayManager API"); } catch (Exception displayManagerException) { try { @@ -156,7 +157,9 @@ public class ScreenCapture extends SurfaceCapture { setDisplaySurface(display, surface, deviceSize.toRect(), videoSize.toRect(), layerStack); virtualDisplayId = displayId; - positionMapper = new PositionMapper(deviceSize, videoSize); + + AffineMatrix videoToDeviceMatrix = videoSize.equals(deviceSize) ? null : AffineMatrix.scale(videoSize, deviceSize); + positionMapper = new PositionMapper(videoSize, videoToDeviceMatrix); Ln.d("Display: using SurfaceControl API"); } catch (Exception surfaceControlException) { Ln.e("Could not create display using DisplayManager", displayManagerException); From 904f86152ea7693f481e29cdfd8327ac58eb8c34 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 10 Nov 2024 09:00:32 +0100 Subject: [PATCH 2065/2244] Move mediaCodec.stop() to finally block This will allow stopping MediaCodec only after the cleanup of other components which must be performed beforehand. PR #5455 --- .../com/genymobile/scrcpy/video/SurfaceEncoder.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java index a00a8236..dcb5d648 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java @@ -86,6 +86,7 @@ public class SurfaceEncoder implements AsyncProcessor { format.setInteger(MediaFormat.KEY_HEIGHT, size.getHeight()); Surface surface = null; + boolean mediaCodecStarted = false; try { mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); surface = mediaCodec.createInputSurface(); @@ -93,6 +94,7 @@ public class SurfaceEncoder implements AsyncProcessor { capture.start(surface); mediaCodec.start(); + mediaCodecStarted = true; // Set the MediaCodec instance to "interrupt" (by signaling an EOS) on reset reset.setRunningMediaCodec(mediaCodec); @@ -108,9 +110,6 @@ public class SurfaceEncoder implements AsyncProcessor { // The capture might have been closed internally (for example if the camera is disconnected) alive = !stopped.get() && !capture.isClosed(); } - - // do not call stop() on exception, it would trigger an IllegalStateException - mediaCodec.stop(); } catch (IllegalStateException | IllegalArgumentException e) { Ln.e("Encoding error: " + e.getClass().getName() + ": " + e.getMessage()); if (!prepareRetry(size)) { @@ -119,6 +118,13 @@ public class SurfaceEncoder implements AsyncProcessor { alive = true; } finally { reset.setRunningMediaCodec(null); + if (mediaCodecStarted) { + try { + mediaCodec.stop(); + } catch (IllegalStateException e) { + // ignore (just in case) + } + } mediaCodec.reset(); if (surface != null) { surface.release(); From 23960ca11ab676dc85f6c0f6e6f447efed92bba9 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 10 Nov 2024 11:40:31 +0100 Subject: [PATCH 2066/2244] Ignore signalEndOfStream() error This may be called at any time to interrupt the current encoding, including when MediaCodec is in an expected state. PR #5455 --- .../main/java/com/genymobile/scrcpy/video/CaptureReset.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/video/CaptureReset.java b/server/src/main/java/com/genymobile/scrcpy/video/CaptureReset.java index c11e2e80..79d32d7c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/CaptureReset.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/CaptureReset.java @@ -18,7 +18,11 @@ public class CaptureReset implements SurfaceCapture.CaptureListener { public synchronized void reset() { reset.set(true); if (runningMediaCodec != null) { - runningMediaCodec.signalEndOfInputStream(); + try { + runningMediaCodec.signalEndOfInputStream(); + } catch (IllegalStateException e) { + // ignore + } } } From 9fb0a3dac1770c51758f578bca0b8e349e164e84 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 9 Nov 2024 15:14:52 +0100 Subject: [PATCH 2067/2244] Reimplement crop using transforms Reimplement the --crop feature using affine transforms. Fixes #4162 PR #5455 --- .../scrcpy/control/PositionMapper.java | 12 ++++ .../scrcpy/video/ScreenCapture.java | 56 ++++++++++++--- .../scrcpy/video/SurfaceCapture.java | 7 ++ .../scrcpy/video/SurfaceEncoder.java | 5 ++ .../genymobile/scrcpy/video/VideoFilter.java | 69 +++++++++++++++++++ 5 files changed, 141 insertions(+), 8 deletions(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/video/VideoFilter.java diff --git a/server/src/main/java/com/genymobile/scrcpy/control/PositionMapper.java b/server/src/main/java/com/genymobile/scrcpy/control/PositionMapper.java index cf9b25ab..4d3b8875 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/PositionMapper.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/PositionMapper.java @@ -15,6 +15,18 @@ public final class PositionMapper { this.videoToDeviceMatrix = videoToDeviceMatrix; } + public static PositionMapper create(Size videoSize, AffineMatrix filterTransform, Size targetSize) { + boolean convertToPixels = !videoSize.equals(targetSize) || filterTransform != null; + AffineMatrix transform = filterTransform; + if (convertToPixels) { + AffineMatrix inputTransform = AffineMatrix.ndcFromPixels(videoSize); + AffineMatrix outputTransform = AffineMatrix.ndcToPixels(targetSize); + transform = outputTransform.multiply(transform).multiply(inputTransform); + } + + return new PositionMapper(videoSize, transform); + } + public Point map(Position position) { Size clientVideoSize = position.getScreenSize(); if (!videoSize.equals(clientVideoSize)) { diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java index 8873cb6d..79d4974d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java @@ -7,6 +7,9 @@ import com.genymobile.scrcpy.device.ConfigurationException; import com.genymobile.scrcpy.device.Device; import com.genymobile.scrcpy.device.DisplayInfo; import com.genymobile.scrcpy.device.Size; +import com.genymobile.scrcpy.opengl.AffineOpenGLFilter; +import com.genymobile.scrcpy.opengl.OpenGLFilter; +import com.genymobile.scrcpy.opengl.OpenGLRunner; import com.genymobile.scrcpy.util.AffineMatrix; import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.LogUtils; @@ -24,11 +27,14 @@ import android.view.IDisplayFoldListener; import android.view.IRotationWatcher; import android.view.Surface; +import java.io.IOException; + public class ScreenCapture extends SurfaceCapture { private final VirtualDisplayListener vdListener; private final int displayId; private int maxSize; + private final Rect crop; private DisplayInfo displayInfo; private Size videoSize; @@ -39,6 +45,9 @@ public class ScreenCapture extends SurfaceCapture { private IBinder display; private VirtualDisplay virtualDisplay; + private AffineMatrix transform; + private OpenGLRunner glRunner; + private DisplayManager.DisplayListenerHandle displayListenerHandle; private HandlerThread handlerThread; @@ -54,6 +63,7 @@ public class ScreenCapture extends SurfaceCapture { this.displayId = options.getDisplayId(); assert displayId != Device.DISPLAY_ID_NONE; this.maxSize = options.getMaxSize(); + this.crop = options.getCrop(); } @Override @@ -125,11 +135,20 @@ public class ScreenCapture extends SurfaceCapture { Size displaySize = displayInfo.getSize(); setSessionDisplaySize(displaySize); - videoSize = displaySize.limit(maxSize).round8(); + + VideoFilter filter = new VideoFilter(displaySize); + + if (crop != null) { + boolean transposed = (displayInfo.getRotation() % 2) != 0; + filter.addCrop(crop, transposed); + } + + transform = filter.getInverseTransform(); + videoSize = filter.getOutputSize().limit(maxSize).round8(); } @Override - public void start(Surface surface) { + public void start(Surface surface) throws IOException { if (display != null) { SurfaceControl.destroyDisplay(display); display = null; @@ -139,14 +158,28 @@ public class ScreenCapture extends SurfaceCapture { virtualDisplay = null; } + Size inputSize; + if (transform != null) { + // If there is a filter, it must receive the full display content + inputSize = displayInfo.getSize(); + assert glRunner == null; + OpenGLFilter glFilter = new AffineOpenGLFilter(transform); + glRunner = new OpenGLRunner(glFilter); + surface = glRunner.start(inputSize, videoSize, surface); + } else { + // If there is no filter, the display must be rendered at target video size directly + inputSize = videoSize; + } + int virtualDisplayId; PositionMapper positionMapper; try { virtualDisplay = ServiceManager.getDisplayManager() - .createVirtualDisplay("scrcpy", videoSize.getWidth(), videoSize.getHeight(), displayId, surface); + .createVirtualDisplay("scrcpy", inputSize.getWidth(), inputSize.getHeight(), displayId, surface); virtualDisplayId = virtualDisplay.getDisplay().getDisplayId(); - // The position are relative to the virtual display, not the original display - positionMapper = new PositionMapper(videoSize, null); + + // The positions are relative to the virtual display, not the original display (so use inputSize, not deviceSize!) + positionMapper = PositionMapper.create(videoSize, transform, inputSize); Ln.d("Display: using DisplayManager API"); } catch (Exception displayManagerException) { try { @@ -155,11 +188,10 @@ public class ScreenCapture extends SurfaceCapture { Size deviceSize = displayInfo.getSize(); int layerStack = displayInfo.getLayerStack(); - setDisplaySurface(display, surface, deviceSize.toRect(), videoSize.toRect(), layerStack); + setDisplaySurface(display, surface, deviceSize.toRect(), inputSize.toRect(), layerStack); virtualDisplayId = displayId; - AffineMatrix videoToDeviceMatrix = videoSize.equals(deviceSize) ? null : AffineMatrix.scale(videoSize, deviceSize); - positionMapper = new PositionMapper(videoSize, videoToDeviceMatrix); + positionMapper = PositionMapper.create(videoSize, transform, deviceSize); Ln.d("Display: using SurfaceControl API"); } catch (Exception surfaceControlException) { Ln.e("Could not create display using DisplayManager", displayManagerException); @@ -173,6 +205,14 @@ public class ScreenCapture extends SurfaceCapture { } } + @Override + public void stop() { + if (glRunner != null) { + glRunner.stopAndRelease(); + glRunner = null; + } + } + @Override public void release() { if (Build.VERSION.SDK_INT == AndroidVersions.API_34_ANDROID_14) { diff --git a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java index d0d93f54..39d3bdb8 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java @@ -57,6 +57,13 @@ public abstract class SurfaceCapture { */ public abstract void start(Surface surface) throws IOException; + /** + * Stop the capture. + */ + public void stop() { + // Do nothing by default + } + /** * Return the video size * diff --git a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java index dcb5d648..bc120107 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java @@ -87,11 +87,13 @@ public class SurfaceEncoder implements AsyncProcessor { Surface surface = null; boolean mediaCodecStarted = false; + boolean captureStarted = false; try { mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); surface = mediaCodec.createInputSurface(); capture.start(surface); + captureStarted = true; mediaCodec.start(); mediaCodecStarted = true; @@ -118,6 +120,9 @@ public class SurfaceEncoder implements AsyncProcessor { alive = true; } finally { reset.setRunningMediaCodec(null); + if (captureStarted) { + capture.stop(); + } if (mediaCodecStarted) { try { mediaCodec.stop(); diff --git a/server/src/main/java/com/genymobile/scrcpy/video/VideoFilter.java b/server/src/main/java/com/genymobile/scrcpy/video/VideoFilter.java new file mode 100644 index 00000000..5a52231f --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/video/VideoFilter.java @@ -0,0 +1,69 @@ +package com.genymobile.scrcpy.video; + +import com.genymobile.scrcpy.device.Size; +import com.genymobile.scrcpy.util.AffineMatrix; + +import android.graphics.Rect; + +public class VideoFilter { + + private Size size; + private AffineMatrix transform; + + public VideoFilter(Size inputSize) { + this.size = inputSize; + } + + public Size getOutputSize() { + return size; + } + + public AffineMatrix getTransform() { + return transform; + } + + /** + * Return the inverse transform. + *

+ * The direct affine transform describes how the input image is transformed. + *

+ * It is often useful to retrieve the inverse transform instead: + *

    + *
  • The OpenGL filter expects the matrix to transform the image coordinates, which is the inverse transform;
  • + *
  • The click positions must be transformed back to the device positions, using the inverse transform too.
  • + *
+ * + * @return the inverse transform + */ + public AffineMatrix getInverseTransform() { + if (transform == null) { + return null; + } + return transform.invert(); + } + + private static Rect transposeRect(Rect rect) { + return new Rect(rect.top, rect.left, rect.bottom, rect.right); + } + + public void addCrop(Rect crop, boolean transposed) { + if (transposed) { + crop = transposeRect(crop); + } + + double inputWidth = size.getWidth(); + double inputHeight = size.getHeight(); + + if (crop.left < 0 || crop.top < 0 || crop.right > inputWidth || crop.bottom > inputHeight) { + throw new IllegalArgumentException("Crop " + crop + " exceeds the input area (" + size + ")"); + } + + double x = crop.left / inputWidth; + double y = 1 - (crop.bottom / inputHeight); // OpenGL origin is bottom-left + double w = crop.width() / inputWidth; + double h = crop.height() / inputHeight; + + transform = AffineMatrix.reframe(x, y, w, h).multiply(transform); + size = new Size(crop.width(), crop.height()); + } +} From 06385ce83bcfd57ac82e2f5ca4d9575fe1f1649f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 10 Nov 2024 16:21:59 +0100 Subject: [PATCH 2068/2244] Reimplement lock orientation using transforms Reimplement the --lock-video-orientation feature using affine transforms. Fixes #4011 PR #5455 --- .../genymobile/scrcpy/video/ScreenCapture.java | 11 +++++++++++ .../com/genymobile/scrcpy/video/VideoFilter.java | 16 ++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java index 79d4974d..bc0f825a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java @@ -35,6 +35,7 @@ public class ScreenCapture extends SurfaceCapture { private final int displayId; private int maxSize; private final Rect crop; + private int lockVideoOrientation; private DisplayInfo displayInfo; private Size videoSize; @@ -64,6 +65,7 @@ public class ScreenCapture extends SurfaceCapture { assert displayId != Device.DISPLAY_ID_NONE; this.maxSize = options.getMaxSize(); this.crop = options.getCrop(); + this.lockVideoOrientation = options.getLockVideoOrientation(); } @Override @@ -136,6 +138,11 @@ public class ScreenCapture extends SurfaceCapture { Size displaySize = displayInfo.getSize(); setSessionDisplaySize(displaySize); + if (lockVideoOrientation == Device.LOCK_VIDEO_ORIENTATION_INITIAL) { + // The user requested to lock the video orientation to the current orientation + lockVideoOrientation = displayInfo.getRotation(); + } + VideoFilter filter = new VideoFilter(displaySize); if (crop != null) { @@ -143,6 +150,10 @@ public class ScreenCapture extends SurfaceCapture { filter.addCrop(crop, transposed); } + if (lockVideoOrientation != Device.LOCK_VIDEO_ORIENTATION_UNLOCKED) { + filter.addLockVideoOrientation(lockVideoOrientation, displayInfo.getRotation()); + } + transform = filter.getInverseTransform(); videoSize = filter.getOutputSize().limit(maxSize).round8(); } diff --git a/server/src/main/java/com/genymobile/scrcpy/video/VideoFilter.java b/server/src/main/java/com/genymobile/scrcpy/video/VideoFilter.java index 5a52231f..2d570446 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/VideoFilter.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/VideoFilter.java @@ -66,4 +66,20 @@ public class VideoFilter { transform = AffineMatrix.reframe(x, y, w, h).multiply(transform); size = new Size(crop.width(), crop.height()); } + + public void addRotation(int ccwRotation) { + if (ccwRotation == 0) { + return; + } + + transform = AffineMatrix.rotateOrtho(ccwRotation).multiply(transform); + if (ccwRotation % 2 != 0) { + size = size.rotate(); + } + } + + public void addLockVideoOrientation(int lockVideoOrientation, int displayRotation) { + int ccwRotation = (4 + lockVideoOrientation - displayRotation) % 4; + addRotation(ccwRotation); + } } From d72686c867dcde973f4666117128f2fc38888a1b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 16 Nov 2024 21:47:07 +0100 Subject: [PATCH 2069/2244] Extract display size monitor Detecting display size changes is not straightforward: - from a DisplayListener, "display changed" events are received, but this does not imply that the size has changed (it must be checked); - on Android 14 (see e26bdb07a21493d096ea5c8cfd870fc5a3f015dc), "display changed" events are not received on some versions, so as a fallback, a RotationWatcher and a DisplayFoldListener are registered, but unregistered as soon as a "display changed" event is actually received, which means that the problem is fixed. Extract a "display size monitor" to share the code between screen capture and virtual display capture. PR #5455 --- .../scrcpy/video/DisplaySizeMonitor.java | 189 ++++++++++++++++++ .../scrcpy/video/ScreenCapture.java | 146 +------------- 2 files changed, 193 insertions(+), 142 deletions(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/video/DisplaySizeMonitor.java diff --git a/server/src/main/java/com/genymobile/scrcpy/video/DisplaySizeMonitor.java b/server/src/main/java/com/genymobile/scrcpy/video/DisplaySizeMonitor.java new file mode 100644 index 00000000..df8be323 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/video/DisplaySizeMonitor.java @@ -0,0 +1,189 @@ +package com.genymobile.scrcpy.video; + +import com.genymobile.scrcpy.AndroidVersions; +import com.genymobile.scrcpy.device.Device; +import com.genymobile.scrcpy.device.DisplayInfo; +import com.genymobile.scrcpy.device.Size; +import com.genymobile.scrcpy.util.Ln; +import com.genymobile.scrcpy.wrappers.DisplayManager; +import com.genymobile.scrcpy.wrappers.ServiceManager; + +import android.os.Build; +import android.os.Handler; +import android.os.HandlerThread; +import android.view.IDisplayFoldListener; +import android.view.IRotationWatcher; + +public class DisplaySizeMonitor { + + public interface Listener { + void onDisplaySizeChanged(); + } + + private DisplayManager.DisplayListenerHandle displayListenerHandle; + private HandlerThread handlerThread; + + // On Android 14, DisplayListener may be broken (it never sends events). This is fixed in recent Android 14 upgrades, but we can't really + // detect it directly, so register a RotationWatcher and a DisplayFoldListener as a fallback, until we receive the first event from + // DisplayListener (which proves that it works). + private boolean displayListenerWorks; // only accessed from the display listener thread + private IRotationWatcher rotationWatcher; + private IDisplayFoldListener displayFoldListener; + + private int displayId = Device.DISPLAY_ID_NONE; + + private Size sessionDisplaySize; + + private Listener listener; + + public void start(int displayId, Listener listener) { + // Once started, the listener and the displayId must never change + assert listener != null; + this.listener = listener; + + assert this.displayId == Device.DISPLAY_ID_NONE; + this.displayId = displayId; + + handlerThread = new HandlerThread("DisplayListener"); + handlerThread.start(); + Handler handler = new Handler(handlerThread.getLooper()); + + if (Build.VERSION.SDK_INT == AndroidVersions.API_34_ANDROID_14) { + registerDisplayListenerFallbacks(); + } + + displayListenerHandle = ServiceManager.getDisplayManager().registerDisplayListener(eventDisplayId -> { + if (Ln.isEnabled(Ln.Level.VERBOSE)) { + Ln.v("DisplaySizeMonitor: onDisplayChanged(" + eventDisplayId + ")"); + } + + if (Build.VERSION.SDK_INT == AndroidVersions.API_34_ANDROID_14) { + if (!displayListenerWorks) { + // On the first display listener event, we know it works, we can unregister the fallbacks + displayListenerWorks = true; + unregisterDisplayListenerFallbacks(); + } + } + + if (eventDisplayId == displayId) { + checkDisplaySizeChanged(); + } + }, handler); + } + + /** + * Stop and release the monitor. + *

+ * It must not be used anymore. + * It is ok to call this method even if {@link #start(int, Listener)} was not called. + */ + public void stopAndRelease() { + if (Build.VERSION.SDK_INT == AndroidVersions.API_34_ANDROID_14) { + unregisterDisplayListenerFallbacks(); + } + + // displayListenerHandle may be null if registration failed + if (displayListenerHandle != null) { + ServiceManager.getDisplayManager().unregisterDisplayListener(displayListenerHandle); + displayListenerHandle = null; + } + + if (handlerThread != null) { + handlerThread.quitSafely(); + } + } + + private synchronized Size getSessionDisplaySize() { + return sessionDisplaySize; + } + + public synchronized void setSessionDisplaySize(Size sessionDisplaySize) { + this.sessionDisplaySize = sessionDisplaySize; + } + + private void checkDisplaySizeChanged() { + DisplayInfo di = ServiceManager.getDisplayManager().getDisplayInfo(displayId); + if (di == null) { + Ln.w("DisplayInfo for " + displayId + " cannot be retrieved"); + // We can't compare with the current size, so reset unconditionally + if (Ln.isEnabled(Ln.Level.VERBOSE)) { + Ln.v("DisplaySizeMonitor: requestReset(): " + getSessionDisplaySize() + " -> (unknown)"); + } + setSessionDisplaySize(null); + listener.onDisplaySizeChanged(); + } else { + Size size = di.getSize(); + + // The field is hidden on purpose, to read it with synchronization + @SuppressWarnings("checkstyle:HiddenField") + Size sessionDisplaySize = getSessionDisplaySize(); // synchronized + + // .equals() also works if sessionDisplaySize == null + if (!size.equals(sessionDisplaySize)) { + // Reset only if the size is different + if (Ln.isEnabled(Ln.Level.VERBOSE)) { + Ln.v("DisplaySizeMonitor: requestReset(): " + sessionDisplaySize + " -> " + size); + } + // Set the new size immediately, so that a future onDisplayChanged() event called before the asynchronous prepare() + // considers that the current size is the requested size (to avoid a duplicate requestReset()) + setSessionDisplaySize(size); + listener.onDisplaySizeChanged(); + } else if (Ln.isEnabled(Ln.Level.VERBOSE)) { + Ln.v("DisplaySizeMonitor: Size not changed (" + size + "): do not requestReset()"); + } + } + } + + private void registerDisplayListenerFallbacks() { + rotationWatcher = new IRotationWatcher.Stub() { + @Override + public void onRotationChanged(int rotation) { + if (Ln.isEnabled(Ln.Level.VERBOSE)) { + Ln.v("DisplaySizeMonitor: onRotationChanged(" + rotation + ")"); + } + + checkDisplaySizeChanged(); + } + }; + ServiceManager.getWindowManager().registerRotationWatcher(rotationWatcher, displayId); + + // Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10 (but implied by == API_34_ANDROID 14) + displayFoldListener = new IDisplayFoldListener.Stub() { + + private boolean first = true; + + @Override + public void onDisplayFoldChanged(int displayId, boolean folded) { + if (first) { + // An event is posted on registration to signal the initial state. Ignore it to avoid restarting encoding. + first = false; + return; + } + + if (Ln.isEnabled(Ln.Level.VERBOSE)) { + Ln.v("DisplaySizeMonitor: onDisplayFoldChanged(" + displayId + ", " + folded + ")"); + } + + if (DisplaySizeMonitor.this.displayId != displayId) { + // Ignore events related to other display ids + return; + } + + checkDisplaySizeChanged(); + } + }; + ServiceManager.getWindowManager().registerDisplayFoldListener(displayFoldListener); + } + + private synchronized void unregisterDisplayListenerFallbacks() { + if (rotationWatcher != null) { + ServiceManager.getWindowManager().unregisterRotationWatcher(rotationWatcher); + rotationWatcher = null; + } + if (displayFoldListener != null) { + // Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10 (but implied by == API_34_ANDROID 14) + ServiceManager.getWindowManager().unregisterDisplayFoldListener(displayFoldListener); + displayFoldListener = null; + } + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java index bc0f825a..2a705fa0 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java @@ -13,18 +13,13 @@ import com.genymobile.scrcpy.opengl.OpenGLRunner; import com.genymobile.scrcpy.util.AffineMatrix; import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.LogUtils; -import com.genymobile.scrcpy.wrappers.DisplayManager; import com.genymobile.scrcpy.wrappers.ServiceManager; import com.genymobile.scrcpy.wrappers.SurfaceControl; import android.graphics.Rect; import android.hardware.display.VirtualDisplay; import android.os.Build; -import android.os.Handler; -import android.os.HandlerThread; import android.os.IBinder; -import android.view.IDisplayFoldListener; -import android.view.IRotationWatcher; import android.view.Surface; import java.io.IOException; @@ -40,8 +35,7 @@ public class ScreenCapture extends SurfaceCapture { private DisplayInfo displayInfo; private Size videoSize; - // Source display size (before resizing/crop) for the current session - private Size sessionDisplaySize; + private final DisplaySizeMonitor displaySizeMonitor = new DisplaySizeMonitor(); private IBinder display; private VirtualDisplay virtualDisplay; @@ -49,16 +43,6 @@ public class ScreenCapture extends SurfaceCapture { private AffineMatrix transform; private OpenGLRunner glRunner; - private DisplayManager.DisplayListenerHandle displayListenerHandle; - private HandlerThread handlerThread; - - // On Android 14, the DisplayListener may be broken (it never sends events). This is fixed in recent Android 14 upgrades, but we can't really - // detect it directly, so register a RotationWatcher and a DisplayFoldListener as a fallback, until we receive the first event from - // DisplayListener (which proves that it works). - private boolean displayListenerWorks; // only accessed from the display listener thread - private IRotationWatcher rotationWatcher; - private IDisplayFoldListener displayFoldListener; - public ScreenCapture(VirtualDisplayListener vdListener, Options options) { this.vdListener = vdListener; this.displayId = options.getDisplayId(); @@ -70,57 +54,7 @@ public class ScreenCapture extends SurfaceCapture { @Override public void init() { - if (Build.VERSION.SDK_INT == AndroidVersions.API_34_ANDROID_14) { - registerDisplayListenerFallbacks(); - } - - handlerThread = new HandlerThread("DisplayListener"); - handlerThread.start(); - Handler handler = new Handler(handlerThread.getLooper()); - displayListenerHandle = ServiceManager.getDisplayManager().registerDisplayListener(displayId -> { - if (Ln.isEnabled(Ln.Level.VERBOSE)) { - Ln.v("ScreenCapture: onDisplayChanged(" + displayId + ")"); - } - if (Build.VERSION.SDK_INT == AndroidVersions.API_34_ANDROID_14) { - if (!displayListenerWorks) { - // On the first display listener event, we know it works, we can unregister the fallbacks - displayListenerWorks = true; - unregisterDisplayListenerFallbacks(); - } - } - if (this.displayId == displayId) { - DisplayInfo di = ServiceManager.getDisplayManager().getDisplayInfo(displayId); - if (di == null) { - Ln.w("DisplayInfo for " + displayId + " cannot be retrieved"); - // We can't compare with the current size, so reset unconditionally - if (Ln.isEnabled(Ln.Level.VERBOSE)) { - Ln.v("ScreenCapture: requestReset(): " + getSessionDisplaySize() + " -> (unknown)"); - } - setSessionDisplaySize(null); - invalidate(); - } else { - Size size = di.getSize(); - - // The field is hidden on purpose, to read it with synchronization - @SuppressWarnings("checkstyle:HiddenField") - Size sessionDisplaySize = getSessionDisplaySize(); // synchronized - - // .equals() also works if sessionDisplaySize == null - if (!size.equals(sessionDisplaySize)) { - // Reset only if the size is different - if (Ln.isEnabled(Ln.Level.VERBOSE)) { - Ln.v("ScreenCapture: requestReset(): " + sessionDisplaySize + " -> " + size); - } - // Set the new size immediately, so that a future onDisplayChanged() event called before the asynchronous prepare() - // considers that the current size is the requested size (to avoid a duplicate requestReset()) - setSessionDisplaySize(size); - invalidate(); - } else if (Ln.isEnabled(Ln.Level.VERBOSE)) { - Ln.v("ScreenCapture: Size not changed (" + size + "): do not requestReset()"); - } - } - } - }, handler); + displaySizeMonitor.start(displayId, this::invalidate); } @Override @@ -136,7 +70,7 @@ public class ScreenCapture extends SurfaceCapture { } Size displaySize = displayInfo.getSize(); - setSessionDisplaySize(displaySize); + displaySizeMonitor.setSessionDisplaySize(displaySize); if (lockVideoOrientation == Device.LOCK_VIDEO_ORIENTATION_INITIAL) { // The user requested to lock the video orientation to the current orientation @@ -226,18 +160,7 @@ public class ScreenCapture extends SurfaceCapture { @Override public void release() { - if (Build.VERSION.SDK_INT == AndroidVersions.API_34_ANDROID_14) { - unregisterDisplayListenerFallbacks(); - } - - handlerThread.quitSafely(); - handlerThread = null; - - // displayListenerHandle may be null if registration failed - if (displayListenerHandle != null) { - ServiceManager.getDisplayManager().unregisterDisplayListener(displayListenerHandle); - displayListenerHandle = null; - } + displaySizeMonitor.stopAndRelease(); if (display != null) { SurfaceControl.destroyDisplay(display); @@ -279,67 +202,6 @@ public class ScreenCapture extends SurfaceCapture { } } - private synchronized Size getSessionDisplaySize() { - return sessionDisplaySize; - } - - private synchronized void setSessionDisplaySize(Size sessionDisplaySize) { - this.sessionDisplaySize = sessionDisplaySize; - } - - private void registerDisplayListenerFallbacks() { - rotationWatcher = new IRotationWatcher.Stub() { - @Override - public void onRotationChanged(int rotation) { - if (Ln.isEnabled(Ln.Level.VERBOSE)) { - Ln.v("ScreenCapture: onRotationChanged(" + rotation + ")"); - } - invalidate(); - } - }; - ServiceManager.getWindowManager().registerRotationWatcher(rotationWatcher, displayId); - - // Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10 (but implied by == API_34_ANDROID 14) - displayFoldListener = new IDisplayFoldListener.Stub() { - - private boolean first = true; - - @Override - public void onDisplayFoldChanged(int displayId, boolean folded) { - if (first) { - // An event is posted on registration to signal the initial state. Ignore it to avoid restarting encoding. - first = false; - return; - } - - if (Ln.isEnabled(Ln.Level.VERBOSE)) { - Ln.v("ScreenCapture: onDisplayFoldChanged(" + displayId + ", " + folded + ")"); - } - - if (ScreenCapture.this.displayId != displayId) { - // Ignore events related to other display ids - return; - } - invalidate(); - } - }; - ServiceManager.getWindowManager().registerDisplayFoldListener(displayFoldListener); - } - - private void unregisterDisplayListenerFallbacks() { - synchronized (this) { - if (rotationWatcher != null) { - ServiceManager.getWindowManager().unregisterRotationWatcher(rotationWatcher); - rotationWatcher = null; - } - if (displayFoldListener != null) { - // Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10 (but implied by == API_34_ANDROID 14) - ServiceManager.getWindowManager().unregisterDisplayFoldListener(displayFoldListener); - displayFoldListener = null; - } - } - } - @Override public void requestInvalidate() { invalidate(); From 39d51ff2cc2f3e201ad433d48372b548e5dd11d3 Mon Sep 17 00:00:00 2001 From: Anric Date: Sun, 17 Nov 2024 21:43:32 +0800 Subject: [PATCH 2070/2244] Use DisplayWindowListener for Android 14 On Android 14, DisplayListener may be broken (it never sends events). This is fixed in recent Android 14 upgrades, but we can't really detect it directly. As a workaround, a RotationWatcher and DisplayFoldListener were registered as a fallback, until a first "display changed" event was triggered. To simplify, on Android 14, register a DisplayWindowListener (introduced in Android 11) to listen to configuration changes instead. Refs #5455 comment PR #5455 Co-authored-by: Romain Vimont Signed-off-by: Romain Vimont --- .../android/view/IDisplayWindowListener.aidl | 66 +++++++++ .../scrcpy/video/DisplaySizeMonitor.java | 140 ++++++------------ .../wrappers/DisplayWindowListener.java | 39 +++++ .../scrcpy/wrappers/WindowManager.java | 20 +++ 4 files changed, 170 insertions(+), 95 deletions(-) create mode 100644 server/src/main/aidl/android/view/IDisplayWindowListener.aidl create mode 100644 server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayWindowListener.java diff --git a/server/src/main/aidl/android/view/IDisplayWindowListener.aidl b/server/src/main/aidl/android/view/IDisplayWindowListener.aidl new file mode 100644 index 00000000..2b331175 --- /dev/null +++ b/server/src/main/aidl/android/view/IDisplayWindowListener.aidl @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2019 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.view; + +import android.graphics.Rect; +import android.content.res.Configuration; + +import java.util.List; + +/** + * Interface to listen for changes to display window-containers. + * + * This differs from DisplayManager's DisplayListener in a couple ways: + * - onDisplayAdded is always called after the display is actually added to the WM hierarchy. + * This corresponds to the DisplayContent and not the raw Dislay from DisplayManager. + * - onDisplayConfigurationChanged is called for all configuration changes, not just changes + * to displayinfo (eg. windowing-mode). + * + */ +oneway interface IDisplayWindowListener { + + /** + * Called when a new display is added to the WM hierarchy. The existing display ids are returned + * when this listener is registered with WM via {@link #registerDisplayWindowListener}. + */ + void onDisplayAdded(int displayId); + + /** + * Called when a display's window-container configuration has changed. + */ + void onDisplayConfigurationChanged(int displayId, in Configuration newConfig); + + /** + * Called when a display is removed from the hierarchy. + */ + void onDisplayRemoved(int displayId); + + /** + * Called when fixed rotation is started on a display. + */ + void onFixedRotationStarted(int displayId, int newRotation); + + /** + * Called when the previous fixed rotation on a display is finished. + */ + void onFixedRotationFinished(int displayId); + + /** + * Called when the keep clear ares on a display have changed. + */ + void onKeepClearAreasChanged(int displayId, in List restricted, in List unrestricted); +} diff --git a/server/src/main/java/com/genymobile/scrcpy/video/DisplaySizeMonitor.java b/server/src/main/java/com/genymobile/scrcpy/video/DisplaySizeMonitor.java index df8be323..ff863aa8 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/DisplaySizeMonitor.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/DisplaySizeMonitor.java @@ -6,13 +6,14 @@ import com.genymobile.scrcpy.device.DisplayInfo; import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.wrappers.DisplayManager; +import com.genymobile.scrcpy.wrappers.DisplayWindowListener; import com.genymobile.scrcpy.wrappers.ServiceManager; +import android.content.res.Configuration; import android.os.Build; import android.os.Handler; import android.os.HandlerThread; -import android.view.IDisplayFoldListener; -import android.view.IRotationWatcher; +import android.view.IDisplayWindowListener; public class DisplaySizeMonitor { @@ -20,15 +21,14 @@ public class DisplaySizeMonitor { void onDisplaySizeChanged(); } + // On Android 14, DisplayListener may be broken (it never sends events). This is fixed in recent Android 14 upgrades, but we can't really + // detect it directly, so register a DisplayWindowListener (introduced in Android 11) to listen to configuration changes instead. + private static final boolean USE_DEFAULT_METHOD = Build.VERSION.SDK_INT != AndroidVersions.API_34_ANDROID_14; + private DisplayManager.DisplayListenerHandle displayListenerHandle; private HandlerThread handlerThread; - // On Android 14, DisplayListener may be broken (it never sends events). This is fixed in recent Android 14 upgrades, but we can't really - // detect it directly, so register a RotationWatcher and a DisplayFoldListener as a fallback, until we receive the first event from - // DisplayListener (which proves that it works). - private boolean displayListenerWorks; // only accessed from the display listener thread - private IRotationWatcher rotationWatcher; - private IDisplayFoldListener displayFoldListener; + private IDisplayWindowListener displayWindowListener; private int displayId = Device.DISPLAY_ID_NONE; @@ -44,31 +44,34 @@ public class DisplaySizeMonitor { assert this.displayId == Device.DISPLAY_ID_NONE; this.displayId = displayId; - handlerThread = new HandlerThread("DisplayListener"); - handlerThread.start(); - Handler handler = new Handler(handlerThread.getLooper()); - - if (Build.VERSION.SDK_INT == AndroidVersions.API_34_ANDROID_14) { - registerDisplayListenerFallbacks(); - } - - displayListenerHandle = ServiceManager.getDisplayManager().registerDisplayListener(eventDisplayId -> { - if (Ln.isEnabled(Ln.Level.VERBOSE)) { - Ln.v("DisplaySizeMonitor: onDisplayChanged(" + eventDisplayId + ")"); - } - - if (Build.VERSION.SDK_INT == AndroidVersions.API_34_ANDROID_14) { - if (!displayListenerWorks) { - // On the first display listener event, we know it works, we can unregister the fallbacks - displayListenerWorks = true; - unregisterDisplayListenerFallbacks(); + if (USE_DEFAULT_METHOD) { + handlerThread = new HandlerThread("DisplayListener"); + handlerThread.start(); + Handler handler = new Handler(handlerThread.getLooper()); + displayListenerHandle = ServiceManager.getDisplayManager().registerDisplayListener(eventDisplayId -> { + if (Ln.isEnabled(Ln.Level.VERBOSE)) { + Ln.v("DisplaySizeMonitor: onDisplayChanged(" + eventDisplayId + ")"); } - } - if (eventDisplayId == displayId) { - checkDisplaySizeChanged(); - } - }, handler); + if (eventDisplayId == displayId) { + checkDisplaySizeChanged(); + } + }, handler); + } else { + displayWindowListener = new DisplayWindowListener() { + @Override + public void onDisplayConfigurationChanged(int eventDisplayId, Configuration newConfig) { + if (Ln.isEnabled(Ln.Level.VERBOSE)) { + Ln.v("DisplaySizeMonitor: onDisplayConfigurationChanged(" + eventDisplayId + ")"); + } + + if (eventDisplayId == displayId) { + checkDisplaySizeChanged(); + } + } + }; + ServiceManager.getWindowManager().registerDisplayWindowListener(displayWindowListener); + } } /** @@ -78,18 +81,18 @@ public class DisplaySizeMonitor { * It is ok to call this method even if {@link #start(int, Listener)} was not called. */ public void stopAndRelease() { - if (Build.VERSION.SDK_INT == AndroidVersions.API_34_ANDROID_14) { - unregisterDisplayListenerFallbacks(); - } + if (USE_DEFAULT_METHOD) { + // displayListenerHandle may be null if registration failed + if (displayListenerHandle != null) { + ServiceManager.getDisplayManager().unregisterDisplayListener(displayListenerHandle); + displayListenerHandle = null; + } - // displayListenerHandle may be null if registration failed - if (displayListenerHandle != null) { - ServiceManager.getDisplayManager().unregisterDisplayListener(displayListenerHandle); - displayListenerHandle = null; - } - - if (handlerThread != null) { - handlerThread.quitSafely(); + if (handlerThread != null) { + handlerThread.quitSafely(); + } + } else if (displayWindowListener != null) { + ServiceManager.getWindowManager().unregisterDisplayWindowListener(displayWindowListener); } } @@ -133,57 +136,4 @@ public class DisplaySizeMonitor { } } } - - private void registerDisplayListenerFallbacks() { - rotationWatcher = new IRotationWatcher.Stub() { - @Override - public void onRotationChanged(int rotation) { - if (Ln.isEnabled(Ln.Level.VERBOSE)) { - Ln.v("DisplaySizeMonitor: onRotationChanged(" + rotation + ")"); - } - - checkDisplaySizeChanged(); - } - }; - ServiceManager.getWindowManager().registerRotationWatcher(rotationWatcher, displayId); - - // Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10 (but implied by == API_34_ANDROID 14) - displayFoldListener = new IDisplayFoldListener.Stub() { - - private boolean first = true; - - @Override - public void onDisplayFoldChanged(int displayId, boolean folded) { - if (first) { - // An event is posted on registration to signal the initial state. Ignore it to avoid restarting encoding. - first = false; - return; - } - - if (Ln.isEnabled(Ln.Level.VERBOSE)) { - Ln.v("DisplaySizeMonitor: onDisplayFoldChanged(" + displayId + ", " + folded + ")"); - } - - if (DisplaySizeMonitor.this.displayId != displayId) { - // Ignore events related to other display ids - return; - } - - checkDisplaySizeChanged(); - } - }; - ServiceManager.getWindowManager().registerDisplayFoldListener(displayFoldListener); - } - - private synchronized void unregisterDisplayListenerFallbacks() { - if (rotationWatcher != null) { - ServiceManager.getWindowManager().unregisterRotationWatcher(rotationWatcher); - rotationWatcher = null; - } - if (displayFoldListener != null) { - // Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10 (but implied by == API_34_ANDROID 14) - ServiceManager.getWindowManager().unregisterDisplayFoldListener(displayFoldListener); - displayFoldListener = null; - } - } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayWindowListener.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayWindowListener.java new file mode 100644 index 00000000..f2ecb158 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayWindowListener.java @@ -0,0 +1,39 @@ +package com.genymobile.scrcpy.wrappers; + +import android.content.res.Configuration; +import android.graphics.Rect; +import android.view.IDisplayWindowListener; + +import java.util.List; + +public class DisplayWindowListener extends IDisplayWindowListener.Stub { + @Override + public void onDisplayAdded(int displayId) { + // empty default implementation + } + + @Override + public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) { + // empty default implementation + } + + @Override + public void onDisplayRemoved(int displayId) { + // empty default implementation + } + + @Override + public void onFixedRotationStarted(int displayId, int newRotation) { + // empty default implementation + } + + @Override + public void onFixedRotationFinished(int displayId) { + // empty default implementation + } + + @Override + public void onKeepClearAreasChanged(int displayId, List restricted, List unrestricted) { + // empty default implementation + } +} 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 ee36139a..86dd83f2 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java @@ -6,6 +6,7 @@ import com.genymobile.scrcpy.util.Ln; import android.annotation.TargetApi; import android.os.IInterface; import android.view.IDisplayFoldListener; +import android.view.IDisplayWindowListener; import android.view.IRotationWatcher; import java.lang.reflect.Method; @@ -226,4 +227,23 @@ public final class WindowManager { Ln.e("Could not unregister display fold listener", e); } } + + @TargetApi(AndroidVersions.API_30_ANDROID_11) + public int[] registerDisplayWindowListener(IDisplayWindowListener listener) { + try { + return (int[]) manager.getClass().getMethod("registerDisplayWindowListener", IDisplayWindowListener.class).invoke(manager, listener); + } catch (Exception e) { + Ln.e("Could not register display window listener", e); + } + return null; + } + + @TargetApi(AndroidVersions.API_30_ANDROID_11) + public void unregisterDisplayWindowListener(IDisplayWindowListener listener) { + try { + manager.getClass().getMethod("unregisterDisplayWindowListener", IDisplayWindowListener.class).invoke(manager, listener); + } catch (Exception e) { + Ln.e("Could not unregister display window listener", e); + } + } } From 9b03bfc3ae881f639f9c4bb381eef7365b785437 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 10 Nov 2024 13:47:02 +0100 Subject: [PATCH 2071/2244] Handle virtual display rotation Listen to display size changes and rotate the virtual display accordingly. Note: use `git show -b` to Show this commit ignoring whitespace changes. Fixes #5428 Refs #5370 PR #5455 --- .../scrcpy/video/NewDisplayCapture.java | 73 +++++++++++++++---- 1 file changed, 59 insertions(+), 14 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java index cc54876a..6ce50521 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java @@ -6,10 +6,13 @@ import com.genymobile.scrcpy.control.PositionMapper; import com.genymobile.scrcpy.device.DisplayInfo; import com.genymobile.scrcpy.device.NewDisplay; import com.genymobile.scrcpy.device.Size; +import com.genymobile.scrcpy.opengl.AffineOpenGLFilter; +import com.genymobile.scrcpy.opengl.OpenGLFilter; +import com.genymobile.scrcpy.opengl.OpenGLRunner; +import com.genymobile.scrcpy.util.AffineMatrix; import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.wrappers.ServiceManager; -import android.hardware.display.DisplayManager; import android.hardware.display.VirtualDisplay; import android.os.Build; import android.view.Surface; @@ -19,8 +22,8 @@ import java.io.IOException; public class NewDisplayCapture extends SurfaceCapture { // Internal fields copied from android.hardware.display.DisplayManager - private static final int VIRTUAL_DISPLAY_FLAG_PUBLIC = DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC; - private static final int VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY = DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY; + private static final int VIRTUAL_DISPLAY_FLAG_PUBLIC = android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC; + private static final int VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY = android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY; private static final int VIRTUAL_DISPLAY_FLAG_SUPPORTS_TOUCH = 1 << 6; private static final int VIRTUAL_DISPLAY_FLAG_ROTATES_WITH_CONTENT = 1 << 7; private static final int VIRTUAL_DISPLAY_FLAG_DESTROY_CONTENT_ON_REMOVAL = 1 << 8; @@ -35,12 +38,18 @@ public class NewDisplayCapture extends SurfaceCapture { private final VirtualDisplayListener vdListener; private final NewDisplay newDisplay; + private final DisplaySizeMonitor displaySizeMonitor = new DisplaySizeMonitor(); + + private AffineMatrix displayTransform; + private OpenGLRunner glRunner; + private Size mainDisplaySize; private int mainDisplayDpi; private int maxSize; // only used if newDisplay.getSize() != null private VirtualDisplay virtualDisplay; - private Size size; + private Size size; // the logical size of the display (including rotation) + private Size physicalSize; // the physical size of the display (without rotation) private int dpi; public NewDisplayCapture(VirtualDisplayListener vdListener, Options options) { @@ -69,11 +78,27 @@ public class NewDisplayCapture extends SurfaceCapture { @Override public void prepare() { - if (!newDisplay.hasExplicitSize()) { - size = mainDisplaySize.limit(maxSize).round8(); - } - if (!newDisplay.hasExplicitDpi()) { - dpi = scaleDpi(mainDisplaySize, mainDisplayDpi, size); + if (virtualDisplay == null) { + if (!newDisplay.hasExplicitSize()) { + size = mainDisplaySize.limit(maxSize).round8(); + } + if (!newDisplay.hasExplicitDpi()) { + dpi = scaleDpi(mainDisplaySize, mainDisplayDpi, size); + } + + physicalSize = size; + // Set the current display size to avoid an unnecessary call to invalidate() + displaySizeMonitor.setSessionDisplaySize(size); + } else { + DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(virtualDisplay.getDisplay().getDisplayId()); + size = displayInfo.getSize(); + dpi = displayInfo.getDpi(); + + VideoFilter displayFilter = new VideoFilter(size); + displayFilter.addRotation(displayInfo.getRotation()); + // The display info gives the oriented size, but the virtual display video always remains in the origin orientation + displayTransform = displayFilter.getInverseTransform(); + physicalSize = displayFilter.getOutputSize(); } } @@ -100,28 +125,48 @@ public class NewDisplayCapture extends SurfaceCapture { .createNewVirtualDisplay("scrcpy", size.getWidth(), size.getHeight(), dpi, surface, flags); virtualDisplayId = virtualDisplay.getDisplay().getDisplayId(); Ln.i("New display: " + size.getWidth() + "x" + size.getHeight() + "/" + dpi + " (id=" + virtualDisplayId + ")"); + + displaySizeMonitor.start(virtualDisplayId, this::invalidate); } catch (Exception e) { Ln.e("Could not create display", e); throw new AssertionError("Could not create display"); } - - if (vdListener != null) { - PositionMapper positionMapper = new PositionMapper(size, null); - vdListener.onNewVirtualDisplay(virtualDisplayId, positionMapper); - } } @Override public void start(Surface surface) throws IOException { + if (displayTransform != null) { + assert glRunner == null; + OpenGLFilter glFilter = new AffineOpenGLFilter(displayTransform); + glRunner = new OpenGLRunner(glFilter); + surface = glRunner.start(physicalSize, size, surface); + } + if (virtualDisplay == null) { startNew(surface); } else { virtualDisplay.setSurface(surface); } + + if (vdListener != null) { + // The virtual display rotation must only be applied to video, it is already taken into account when injecting events! + PositionMapper positionMapper = PositionMapper.create(size, null, size); + vdListener.onNewVirtualDisplay(virtualDisplay.getDisplay().getDisplayId(), positionMapper); + } + } + + @Override + public void stop() { + if (glRunner != null) { + glRunner.stopAndRelease(); + glRunner = null; + } } @Override public void release() { + displaySizeMonitor.stopAndRelease(); + if (virtualDisplay != null) { virtualDisplay.release(); virtualDisplay = null; From 45382e3f017ea17925243b5cdcdeb3b1a5a44a37 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 14 Nov 2024 20:19:40 +0100 Subject: [PATCH 2072/2244] Add --capture-orientation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deprecate --lock-video-orientation in favor of a more general option --capture-orientation, which supports all possible orientations (0, 90, 180, 270, flip0, flip90, flip180, flip270), and a "locked" flag via a '@' prefix. All the old "locked video orientations" are supported: - --lock-video-orientation -> --capture-orientation=@ - --lock-video-orientation=0 -> --capture-orientation=@0 - --lock-video-orientation=90 -> --capture-orientation=@90 - --lock-video-orientation=180 -> --capture-orientation=@180 - --lock-video-orientation=270 -> --capture-orientation=@270 In addition, --capture-orientation can rotate/flip the display without locking, so that it follows the physical device rotation. For example: scrcpy --capture-orientation=flip90 always flips and rotates the capture by 90° clockwise. The arguments are consistent with --display-orientation and --record-orientation and --orientation (which provide separate client-side orientation settings). Refs #4011 PR #5455 --- app/data/bash-completion/scrcpy | 11 +- app/data/zsh-completion/_scrcpy | 2 +- app/scrcpy.1 | 24 ++-- app/src/cli.c | 132 +++++++----------- app/src/options.c | 3 +- app/src/options.h | 19 ++- app/src/scrcpy.c | 3 +- app/src/server.c | 14 +- app/src/server.h | 3 +- app/tests/test_cli.c | 2 - doc/video.md | 32 ++++- .../java/com/genymobile/scrcpy/Options.java | 47 +++++-- .../com/genymobile/scrcpy/device/Device.java | 3 - .../genymobile/scrcpy/device/Orientation.java | 47 +++++++ .../scrcpy/video/ScreenCapture.java | 19 ++- .../genymobile/scrcpy/video/VideoFilter.java | 17 ++- 16 files changed, 233 insertions(+), 145 deletions(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/device/Orientation.java diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index d9ad4c8d..c2f32ad0 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -17,6 +17,7 @@ _scrcpy() { --camera-fps= --camera-high-speed --camera-size= + --capture-orientation= --crop= -d --select-usb --disable-screensaver @@ -37,8 +38,6 @@ _scrcpy() { --list-cameras --list-displays --list-encoders - --lock-video-orientation - --lock-video-orientation= -m --max-size= -M --max-fps= @@ -138,6 +137,10 @@ _scrcpy() { COMPREPLY=($(compgen -W 'disabled uhid aoa' -- "$cur")) return ;; + --capture-orientation) + COMPREPLY=($(compgen -W '0 90 180 270 flip0 flip90 flip180 flip270 @0 @90 @180 @270 @flip0 @flip90 @flip180 @flip270' -- "$cur")) + return + ;; --orientation|--display-orientation) COMPREPLY=($(compgen -W '0 90 180 270 flip0 flip90 flip180 flip270' -- "$cur")) return @@ -146,10 +149,6 @@ _scrcpy() { COMPREPLY=($(compgen -W '0 90 180 270' -- "$cur")) return ;; - --lock-video-orientation) - COMPREPLY=($(compgen -W 'unlocked initial 0 90 180 270' -- "$cur")) - return - ;; --pause-on-exit) COMPREPLY=($(compgen -W 'true false if-error' -- "$cur")) return diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 430e8000..59019904 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -24,6 +24,7 @@ arguments=( '--camera-facing=[Select the device camera by its facing direction]:facing:(front back external)' '--camera-fps=[Specify the camera capture frame rate]' '--camera-size=[Specify an explicit camera capture size]' + '--capture-orientation=[Set the capture video orientation]:orientation:(0 90 180 270 flip0 flip90 flip180 flip270 @0 @90 @180 @270 @flip0 @flip90 @flip180 @flip270)' '--crop=[\[width\:height\:x\:y\] Crop the device screen on the server]' {-d,--select-usb}'[Use USB device]' '--disable-screensaver[Disable screensaver while scrcpy is running]' @@ -44,7 +45,6 @@ arguments=( '--list-cameras[List cameras available on the device]' '--list-displays[List displays available on the device]' '--list-encoders[List video and audio encoders available on the device]' - '--lock-video-orientation=[Lock video orientation]:orientation:(unlocked initial 0 90 180 270)' {-m,--max-size=}'[Limit both the width and height of the video to value]' '-M[Use UHID/AOA mouse (same as --mouse=uhid or --mouse=aoa, depending on OTG mode)]' '--max-fps=[Limit the frame rate of screen capture]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 76e36dcb..f0c1e0f1 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -121,6 +121,18 @@ If not specified, Android's default frame rate (30 fps) is used. .BI "\-\-camera\-size " width\fRx\fIheight Specify an explicit camera capture size. +.TP +.BI "\-\-capture\-orientation " value +Possible values are 0, 90, 180, 270, flip0, flip90, flip180 and flip270, possibly prefixed by '@'. + +The number represents the clockwise rotation in degrees; the "flip" keyword applies a horizontal flip before the rotation. + +If a leading '@' is passed (@90) for display capture, then the rotation is locked, and is relative to the natural device orientation. + +If '@' is passed alone, then the rotation is locked to the initial device orientation. + +Default is 0. + .TP .BI "\-\-crop " width\fR:\fIheight\fR:\fIx\fR:\fIy Crop the device screen on the server. @@ -241,16 +253,6 @@ List video and audio encoders available on the device. .B \-\-list\-displays List displays available on the device. -.TP -\fB\-\-lock\-video\-orientation\fR[=\fIvalue\fR] -Lock capture video orientation to \fIvalue\fR. - -Possible values are "unlocked", "initial" (locked to the initial orientation), 0, 90, 180, and 270. The values represent the clockwise rotation from the natural device orientation, in degrees. - -Default is "unlocked". - -Passing the option without argument is equivalent to passing "initial". - .TP .BI "\-m, \-\-max\-size " value Limit both the width and height of the video to \fIvalue\fR. The other dimension is computed so that the device aspect\-ratio is preserved. @@ -548,8 +550,6 @@ Default is "info" for release builds, "debug" for debug builds. .BI "\-\-v4l2-sink " /dev/videoN Output to v4l2loopback device. -It requires to lock the video orientation (see \fB\-\-lock\-video\-orientation\fR). - .TP .BI "\-\-v4l2-buffer " ms Add a buffering delay (in milliseconds) before pushing frames. This increases latency to compensate for jitter. diff --git a/app/src/cli.c b/app/src/cli.c index e67192bf..55ccfc0d 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -107,6 +107,7 @@ enum { OPT_LIST_APPS, OPT_START_APP, OPT_SCREEN_OFF_TIMEOUT, + OPT_CAPTURE_ORIENTATION, }; struct sc_option { @@ -471,18 +472,27 @@ static const struct sc_option options[] = { .text = "List video and audio encoders available on the device.", }, { + .longopt_id = OPT_CAPTURE_ORIENTATION, + .longopt = "capture-orientation", + .argdesc = "value", + .text = "Set the capture video orientation.\n" + "Possible values are 0, 90, 180, 270, flip0, flip90, flip180 " + "and flip270, possibly prefixed by '@'.\n" + "The number represents the clockwise rotation in degrees; the " + "flip\" keyword applies a horizontal flip before the " + "rotation.\n" + "If a leading '@' is passed (@90) for display capture, then " + "the rotation is locked, and is relative to the natural device " + "orientation.\n" + "If '@' is passed alone, then the rotation is locked to the " + "initial device orientation.\n" + "Default is 0.", + }, + { + // deprecated .longopt_id = OPT_LOCK_VIDEO_ORIENTATION, .longopt = "lock-video-orientation", .argdesc = "value", - .optional_arg = true, - .text = "Lock capture video orientation to value.\n" - "Possible values are \"unlocked\", \"initial\" (locked to the " - "initial orientation), 0, 90, 180 and 270. The values " - "represent the clockwise rotation from the natural device " - "orientation, in degrees.\n" - "Default is \"unlocked\".\n" - "Passing the option without argument is equivalent to passing " - "\"initial\".", }, { .shortopt = 'm', @@ -895,8 +905,6 @@ static const struct sc_option options[] = { .longopt = "v4l2-sink", .argdesc = "/dev/videoN", .text = "Output to v4l2loopback device.\n" - "It requires to lock the video orientation (see " - "--lock-video-orientation).\n" "This feature is only available on Linux.", }, { @@ -1582,66 +1590,6 @@ parse_audio_output_buffer(const char *s, sc_tick *tick) { return true; } -static bool -parse_lock_video_orientation(const char *s, - enum sc_lock_video_orientation *lock_mode) { - if (!s || !strcmp(s, "initial")) { - // Without argument, lock the initial orientation - *lock_mode = SC_LOCK_VIDEO_ORIENTATION_INITIAL; - return true; - } - - if (!strcmp(s, "unlocked")) { - *lock_mode = SC_LOCK_VIDEO_ORIENTATION_UNLOCKED; - return true; - } - - if (!strcmp(s, "0")) { - *lock_mode = SC_LOCK_VIDEO_ORIENTATION_0; - return true; - } - - if (!strcmp(s, "90")) { - *lock_mode = SC_LOCK_VIDEO_ORIENTATION_90; - return true; - } - - if (!strcmp(s, "180")) { - *lock_mode = SC_LOCK_VIDEO_ORIENTATION_180; - return true; - } - - if (!strcmp(s, "270")) { - *lock_mode = SC_LOCK_VIDEO_ORIENTATION_270; - return true; - } - - if (!strcmp(s, "1")) { - LOGW("--lock-video-orientation=1 is deprecated, use " - "--lock-video-orientation=270 instead."); - *lock_mode = SC_LOCK_VIDEO_ORIENTATION_270; - return true; - } - - if (!strcmp(s, "2")) { - LOGW("--lock-video-orientation=2 is deprecated, use " - "--lock-video-orientation=180 instead."); - *lock_mode = SC_LOCK_VIDEO_ORIENTATION_180; - return true; - } - - if (!strcmp(s, "3")) { - LOGW("--lock-video-orientation=3 is deprecated, use " - "--lock-video-orientation=90 instead."); - *lock_mode = SC_LOCK_VIDEO_ORIENTATION_90; - return true; - } - - LOGE("Unsupported --lock-video-orientation value: %s (expected initial, " - "unlocked, 0, 90, 180 or 270).", s); - return false; -} - static bool parse_rotation(const char *s, uint8_t *rotation) { long value; @@ -1693,6 +1641,32 @@ parse_orientation(const char *s, enum sc_orientation *orientation) { return false; } +static bool +parse_capture_orientation(const char *s, enum sc_orientation *orientation, + enum sc_orientation_lock *lock) { + if (*s == '\0') { + LOGE("Capture orientation may not be empty (expected 0, 90, 180, 270, " + "flip0, flip90, flip180 or flip270, possibly prefixed by '@')"); + return false; + } + + // Lock the orientation by a leading '@' + if (s[0] == '@') { + // Consume '@' + ++s; + if (*s == '\0') { + // Only '@': lock to the initial orientation (orientation is unused) + *lock = SC_ORIENTATION_LOCKED_INITIAL; + return true; + } + *lock = SC_ORIENTATION_LOCKED_VALUE; + } else { + *lock = SC_ORIENTATION_UNLOCKED; + } + + return parse_orientation(s, orientation); +} + static bool parse_window_position(const char *s, int16_t *position) { // special value for "auto" @@ -2367,8 +2341,13 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], "--mouse=uhid instead."); return false; case OPT_LOCK_VIDEO_ORIENTATION: - if (!parse_lock_video_orientation(optarg, - &opts->lock_video_orientation)) { + LOGE("--lock-video-orientation has been removed, use " + "--capture-orientation instead."); + return false; + case OPT_CAPTURE_ORIENTATION: + if (!parse_capture_orientation(optarg, + &opts->capture_orientation, + &opts->capture_orientation_lock)) { return false; } break; @@ -2852,13 +2831,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; } - if (opts->lock_video_orientation == - SC_LOCK_VIDEO_ORIENTATION_UNLOCKED) { - LOGI("Video orientation is locked for v4l2 sink. " - "See --lock-video-orientation."); - opts->lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_INITIAL; - } - // V4L2 could not handle size change. // Do not log because downsizing on error is the default behavior, // not an explicit request from the user. diff --git a/app/src/options.c b/app/src/options.c index 3cad9d9f..69f8f64d 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -50,7 +50,8 @@ const struct scrcpy_options scrcpy_options_default = { .video_bit_rate = 0, .audio_bit_rate = 0, .max_fps = NULL, - .lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_UNLOCKED, + .capture_orientation = SC_ORIENTATION_0, + .capture_orientation_lock = SC_ORIENTATION_UNLOCKED, .display_orientation = SC_ORIENTATION_0, .record_orientation = SC_ORIENTATION_0, .window_x = SC_WINDOW_POSITION_UNDEFINED, diff --git a/app/src/options.h b/app/src/options.h index 9236c3f8..945fcdf7 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -84,6 +84,12 @@ enum sc_orientation { // v v v SC_ORIENTATION_FLIP_270, // 1 1 1 }; +enum sc_orientation_lock { + SC_ORIENTATION_UNLOCKED, + SC_ORIENTATION_LOCKED_VALUE, // lock to specified orientation + SC_ORIENTATION_LOCKED_INITIAL, // lock to initial device orientation +}; + static inline bool sc_orientation_is_mirror(enum sc_orientation orientation) { assert(!(orientation & ~7)); @@ -130,16 +136,6 @@ sc_orientation_get_name(enum sc_orientation orientation) { } } -enum sc_lock_video_orientation { - SC_LOCK_VIDEO_ORIENTATION_UNLOCKED = -1, - // lock the current orientation when scrcpy starts - SC_LOCK_VIDEO_ORIENTATION_INITIAL = -2, - SC_LOCK_VIDEO_ORIENTATION_0 = 0, - SC_LOCK_VIDEO_ORIENTATION_90 = 3, - SC_LOCK_VIDEO_ORIENTATION_180 = 2, - SC_LOCK_VIDEO_ORIENTATION_270 = 1, -}; - enum sc_keyboard_input_mode { SC_KEYBOARD_INPUT_MODE_AUTO, SC_KEYBOARD_INPUT_MODE_UHID_OR_AOA, // normal vs otg mode @@ -251,7 +247,8 @@ struct scrcpy_options { uint32_t video_bit_rate; uint32_t audio_bit_rate; const char *max_fps; // float to be parsed by the server - enum sc_lock_video_orientation lock_video_orientation; + enum sc_orientation capture_orientation; + enum sc_orientation_lock capture_orientation_lock; enum sc_orientation display_orientation; enum sc_orientation record_orientation; int16_t window_x; // SC_WINDOW_POSITION_UNDEFINED for "auto" diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 2721c0d8..5528910a 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -429,7 +429,8 @@ scrcpy(struct scrcpy_options *options) { .audio_bit_rate = options->audio_bit_rate, .max_fps = options->max_fps, .screen_off_timeout = options->screen_off_timeout, - .lock_video_orientation = options->lock_video_orientation, + .capture_orientation = options->capture_orientation, + .capture_orientation_lock = options->capture_orientation_lock, .control = options->control, .display_id = options->display_id, .new_display = options->new_display, diff --git a/app/src/server.c b/app/src/server.c index 41f0bf27..9c12500e 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -274,9 +274,17 @@ execute_server(struct sc_server *server, VALIDATE_STRING(params->max_fps); ADD_PARAM("max_fps=%s", params->max_fps); } - if (params->lock_video_orientation != SC_LOCK_VIDEO_ORIENTATION_UNLOCKED) { - ADD_PARAM("lock_video_orientation=%" PRIi8, - params->lock_video_orientation); + if (params->capture_orientation_lock != SC_ORIENTATION_UNLOCKED + || params->capture_orientation != SC_ORIENTATION_0) { + if (params->capture_orientation_lock == SC_ORIENTATION_LOCKED_INITIAL) { + ADD_PARAM("capture_orientation=@"); + } else { + const char *orient = + sc_orientation_get_name(params->capture_orientation); + bool locked = + params->capture_orientation_lock != SC_ORIENTATION_UNLOCKED; + ADD_PARAM("capture_orientation=%s%s", locked ? "@" : "", orient); + } } if (server->tunnel.forward) { ADD_PARAM("tunnel_forward=true"); diff --git a/app/src/server.h b/app/src/server.h index 7059be7f..20d998e9 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -46,7 +46,8 @@ struct sc_server_params { uint32_t audio_bit_rate; const char *max_fps; // float to be parsed by the server sc_tick screen_off_timeout; - int8_t lock_video_orientation; + enum sc_orientation capture_orientation; + enum sc_orientation_lock capture_orientation_lock; bool control; uint32_t display_id; const char *new_display; diff --git a/app/tests/test_cli.c b/app/tests/test_cli.c index 14765792..de605cb9 100644 --- a/app/tests/test_cli.c +++ b/app/tests/test_cli.c @@ -51,7 +51,6 @@ static void test_options(void) { "--fullscreen", "--max-fps", "30", "--max-size", "1024", - "--lock-video-orientation=2", // optional arguments require '=' // "--no-control" is not compatible with "--turn-screen-off" // "--no-playback" is not compatible with "--fulscreen" "--port", "1234:1236", @@ -80,7 +79,6 @@ static void test_options(void) { assert(opts->fullscreen); assert(!strcmp(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/doc/video.md b/doc/video.md index 74ec74dd..c00b6602 100644 --- a/doc/video.md +++ b/doc/video.md @@ -103,21 +103,39 @@ The orientation may be applied at 3 different levels: - The [shortcut](shortcuts.md) MOD+r requests the device to switch between portrait and landscape (the current running app may refuse, if it does not support the requested orientation). - - `--lock-video-orientation` changes the mirroring orientation (the orientation + - `--capture-orientation` changes the mirroring orientation (the orientation of the video sent from the device to the computer). This affects the recording. - `--orientation` is applied on the client side, and affects display and recording. For the display, it can be changed dynamically using [shortcuts](shortcuts.md). -To lock the mirroring orientation (on the capture side): +To capture the video with a specific orientation: ```bash -scrcpy --lock-video-orientation # initial (current) orientation -scrcpy --lock-video-orientation=0 # natural orientation -scrcpy --lock-video-orientation=90 # 90° clockwise -scrcpy --lock-video-orientation=180 # 180° -scrcpy --lock-video-orientation=270 # 270° clockwise +scrcpy --capture-orientation=0 +scrcpy --capture-orientation=90 # 90° clockwise +scrcpy --capture-orientation=180 # 180° +scrcpy --capture-orientation=270 # 270° clockwise +scrcpy --capture-orientation=flip0 # hflip +scrcpy --capture-orientation=flip90 # hflip + 90° clockwise +scrcpy --capture-orientation=flip180 # hflip + 180° +scrcpy --capture-orientation=flip270 # hflip + 270° clockwise +``` + +The capture orientation can be locked by using `@`, so that a physical device +rotation does not change the captured video orientation: + +```bash +scrcpy --capture-orientation=@ # locked to the initial orientation +scrcpy --capture-orientation=@0 # locked to 0° +scrcpy --capture-orientation=@90 # locked to 90° clockwise +scrcpy --capture-orientation=@180 # locked to 180° +scrcpy --capture-orientation=@270 # locked to 270° clockwise +scrcpy --capture-orientation=@flip0 # locked to hflip +scrcpy --capture-orientation=@flip90 # locked to hflip + 90° clockwise +scrcpy --capture-orientation=@flip180 # locked to hflip + 180° +scrcpy --capture-orientation=@flip270 # locked to hflip + 270° clockwise ``` To orient the video (on the rendering side): diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index c1620432..e1b3b9af 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -4,6 +4,7 @@ import com.genymobile.scrcpy.audio.AudioCodec; import com.genymobile.scrcpy.audio.AudioSource; import com.genymobile.scrcpy.device.Device; import com.genymobile.scrcpy.device.NewDisplay; +import com.genymobile.scrcpy.device.Orientation; import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.util.CodecOption; import com.genymobile.scrcpy.util.Ln; @@ -13,6 +14,7 @@ import com.genymobile.scrcpy.video.VideoCodec; import com.genymobile.scrcpy.video.VideoSource; import android.graphics.Rect; +import android.util.Pair; import java.util.List; import java.util.Locale; @@ -32,7 +34,6 @@ public class Options { private int videoBitRate = 8000000; private int audioBitRate = 128000; private float maxFps; - private int lockVideoOrientation = -1; private boolean tunnelForward; private Rect crop; private boolean control = true; @@ -59,6 +60,9 @@ public class Options { private NewDisplay newDisplay; + private Orientation.Lock captureOrientationLock = Orientation.Lock.Unlocked; + private Orientation captureOrientation = Orientation.Orient0; + private boolean listEncoders; private boolean listDisplays; private boolean listCameras; @@ -123,10 +127,6 @@ public class Options { return maxFps; } - public int getLockVideoOrientation() { - return lockVideoOrientation; - } - public boolean isTunnelForward() { return tunnelForward; } @@ -219,6 +219,14 @@ public class Options { return newDisplay; } + public Orientation getCaptureOrientation() { + return captureOrientation; + } + + public Orientation.Lock getCaptureOrientationLock() { + return captureOrientationLock; + } + public boolean getList() { return listEncoders || listDisplays || listCameras || listCameraSizes || listApps; } @@ -341,9 +349,6 @@ public class Options { case "max_fps": options.maxFps = parseFloat("max_fps", value); break; - case "lock_video_orientation": - options.lockVideoOrientation = Integer.parseInt(value); - break; case "tunnel_forward": options.tunnelForward = Boolean.parseBoolean(value); break; @@ -448,6 +453,11 @@ public class Options { case "new_display": options.newDisplay = parseNewDisplay(value); break; + case "capture_orientation": + Pair pair = parseCaptureOrientation(value); + options.captureOrientationLock = pair.first; + options.captureOrientation = pair.second; + break; case "send_device_meta": options.sendDeviceMeta = Boolean.parseBoolean(value); break; @@ -571,4 +581,25 @@ public class Options { return new NewDisplay(size, dpi); } + + private static Pair parseCaptureOrientation(String value) { + if (value.isEmpty()) { + throw new IllegalArgumentException("Empty capture orientation string"); + } + + Orientation.Lock lock; + if (value.charAt(0) == '@') { + // Consume '@' + value = value.substring(1); + if (value.isEmpty()) { + // Only '@': lock to the initial orientation (orientation is unused) + return Pair.create(Orientation.Lock.LockedInitial, Orientation.Orient0); + } + lock = Orientation.Lock.LockedValue; + } else { + lock = Orientation.Lock.Unlocked; + } + + return Pair.create(lock, Orientation.getByName(value)); + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Device.java b/server/src/main/java/com/genymobile/scrcpy/device/Device.java index 09c7d2b6..cd713499 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Device.java @@ -40,9 +40,6 @@ public final class Device { public static final int INJECT_MODE_WAIT_FOR_RESULT = InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_RESULT; public static final int INJECT_MODE_WAIT_FOR_FINISH = InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH; - public static final int LOCK_VIDEO_ORIENTATION_UNLOCKED = -1; - public static final int LOCK_VIDEO_ORIENTATION_INITIAL = -2; - private Device() { // not instantiable } diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Orientation.java b/server/src/main/java/com/genymobile/scrcpy/device/Orientation.java new file mode 100644 index 00000000..c269750e --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/device/Orientation.java @@ -0,0 +1,47 @@ +package com.genymobile.scrcpy.device; + +public enum Orientation { + + // @formatter:off + Orient0("0"), + Orient90("90"), + Orient180("180"), + Orient270("270"), + Flip0("flip0"), + Flip90("flip90"), + Flip180("flip180"), + Flip270("flip270"); + + public enum Lock { + Unlocked, LockedInitial, LockedValue, + } + + private final String name; + + Orientation(String name) { + this.name = name; + } + + public static Orientation getByName(String name) { + for (Orientation orientation : values()) { + if (orientation.name.equals(name)) { + return orientation; + } + } + + throw new IllegalArgumentException("Unknown orientation: " + name); + } + + public static Orientation fromRotation(int rotation) { + assert rotation >= 0 && rotation < 4; + return values()[rotation]; + } + + public boolean isFlipped() { + return (ordinal() & 4) != 0; + } + + public int getRotation() { + return ordinal() & 3; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java index 2a705fa0..432d0ae8 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java @@ -6,6 +6,7 @@ import com.genymobile.scrcpy.control.PositionMapper; import com.genymobile.scrcpy.device.ConfigurationException; import com.genymobile.scrcpy.device.Device; import com.genymobile.scrcpy.device.DisplayInfo; +import com.genymobile.scrcpy.device.Orientation; import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.opengl.AffineOpenGLFilter; import com.genymobile.scrcpy.opengl.OpenGLFilter; @@ -30,7 +31,8 @@ public class ScreenCapture extends SurfaceCapture { private final int displayId; private int maxSize; private final Rect crop; - private int lockVideoOrientation; + private Orientation.Lock captureOrientationLock; + private Orientation captureOrientation; private DisplayInfo displayInfo; private Size videoSize; @@ -49,7 +51,10 @@ public class ScreenCapture extends SurfaceCapture { assert displayId != Device.DISPLAY_ID_NONE; this.maxSize = options.getMaxSize(); this.crop = options.getCrop(); - this.lockVideoOrientation = options.getLockVideoOrientation(); + this.captureOrientationLock = options.getCaptureOrientationLock(); + this.captureOrientation = options.getCaptureOrientation(); + assert captureOrientationLock != null; + assert captureOrientation != null; } @Override @@ -72,9 +77,10 @@ public class ScreenCapture extends SurfaceCapture { Size displaySize = displayInfo.getSize(); displaySizeMonitor.setSessionDisplaySize(displaySize); - if (lockVideoOrientation == Device.LOCK_VIDEO_ORIENTATION_INITIAL) { + if (captureOrientationLock == Orientation.Lock.LockedInitial) { // The user requested to lock the video orientation to the current orientation - lockVideoOrientation = displayInfo.getRotation(); + captureOrientationLock = Orientation.Lock.LockedValue; + captureOrientation = Orientation.fromRotation(displayInfo.getRotation()); } VideoFilter filter = new VideoFilter(displaySize); @@ -84,9 +90,8 @@ public class ScreenCapture extends SurfaceCapture { filter.addCrop(crop, transposed); } - if (lockVideoOrientation != Device.LOCK_VIDEO_ORIENTATION_UNLOCKED) { - filter.addLockVideoOrientation(lockVideoOrientation, displayInfo.getRotation()); - } + boolean locked = captureOrientationLock != Orientation.Lock.Unlocked; + filter.addOrientation(displayInfo.getRotation(), locked, captureOrientation); transform = filter.getInverseTransform(); videoSize = filter.getOutputSize().limit(maxSize).round8(); diff --git a/server/src/main/java/com/genymobile/scrcpy/video/VideoFilter.java b/server/src/main/java/com/genymobile/scrcpy/video/VideoFilter.java index 2d570446..8aadaa0d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/VideoFilter.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/VideoFilter.java @@ -1,5 +1,6 @@ package com.genymobile.scrcpy.video; +import com.genymobile.scrcpy.device.Orientation; import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.util.AffineMatrix; @@ -78,8 +79,20 @@ public class VideoFilter { } } - public void addLockVideoOrientation(int lockVideoOrientation, int displayRotation) { - int ccwRotation = (4 + lockVideoOrientation - displayRotation) % 4; + public void addOrientation(Orientation captureOrientation) { + if (captureOrientation.isFlipped()) { + transform = AffineMatrix.hflip().multiply(transform); + } + int ccwRotation = (4 - captureOrientation.getRotation()) % 4; addRotation(ccwRotation); } + + public void addOrientation(int displayRotation, boolean locked, Orientation captureOrientation) { + if (locked) { + // flip/rotate the current display from the natural device orientation (i.e. where display rotation is 0) + int reverseDisplayRotation = (4 - displayRotation) % 4; + addRotation(reverseDisplayRotation); + } + addOrientation(captureOrientation); + } } From 456fa510f25039d6cd016eb9293a82be1f7a2653 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 14 Nov 2024 20:50:41 +0100 Subject: [PATCH 2073/2244] Apply filters to camera capture Apply crop and orientation to camera capture. Fixes #4426 PR #5455 --- .../scrcpy/opengl/OpenGLRunner.java | 18 +++++- .../scrcpy/video/CameraCapture.java | 60 +++++++++++++++++-- 2 files changed, 71 insertions(+), 7 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/opengl/OpenGLRunner.java b/server/src/main/java/com/genymobile/scrcpy/opengl/OpenGLRunner.java index a3f9335c..86bd1859 100644 --- a/server/src/main/java/com/genymobile/scrcpy/opengl/OpenGLRunner.java +++ b/server/src/main/java/com/genymobile/scrcpy/opengl/OpenGLRunner.java @@ -28,6 +28,7 @@ public final class OpenGLRunner { private EGLSurface eglSurface; private final OpenGLFilter filter; + private final float[] overrideTransformMatrix; private SurfaceTexture surfaceTexture; private Surface inputSurface; @@ -35,8 +36,13 @@ public final class OpenGLRunner { private boolean stopped; - public OpenGLRunner(OpenGLFilter filter) { + public OpenGLRunner(OpenGLFilter filter, float[] overrideTransformMatrix) { this.filter = filter; + this.overrideTransformMatrix = overrideTransformMatrix; + } + + public OpenGLRunner(OpenGLFilter filter) { + this(filter, null); } public static synchronized void initOnce() { @@ -202,8 +208,14 @@ public final class OpenGLRunner { GLUtils.checkGlError(); surfaceTexture.updateTexImage(); - float[] matrix = new float[16]; - surfaceTexture.getTransformMatrix(matrix); + + float[] matrix; + if (overrideTransformMatrix != null) { + matrix = overrideTransformMatrix; + } else { + matrix = new float[16]; + surfaceTexture.getTransformMatrix(matrix); + } filter.draw(textureId, matrix); diff --git a/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java index ee4085e9..5a18aeac 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java @@ -3,7 +3,12 @@ package com.genymobile.scrcpy.video; import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.Options; import com.genymobile.scrcpy.device.ConfigurationException; +import com.genymobile.scrcpy.device.Orientation; import com.genymobile.scrcpy.device.Size; +import com.genymobile.scrcpy.opengl.AffineOpenGLFilter; +import com.genymobile.scrcpy.opengl.OpenGLFilter; +import com.genymobile.scrcpy.opengl.OpenGLRunner; +import com.genymobile.scrcpy.util.AffineMatrix; import com.genymobile.scrcpy.util.HandlerExecutor; import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.LogUtils; @@ -41,6 +46,13 @@ import java.util.stream.Stream; public class CameraCapture extends SurfaceCapture { + public static final float[] VFLIP_MATRIX = { + 1, 0, 0, 0, // column 1 + 0, -1, 0, 0, // column 2 + 0, 0, 1, 0, // column 3 + 0, 1, 0, 1, // column 4 + }; + private final String explicitCameraId; private final CameraFacing cameraFacing; private final Size explicitSize; @@ -48,9 +60,15 @@ public class CameraCapture extends SurfaceCapture { private final CameraAspectRatio aspectRatio; private final int fps; private final boolean highSpeed; + private final Rect crop; + private final Orientation captureOrientation; private String cameraId; - private Size size; + private Size captureSize; + private Size videoSize; // after OpenGL transforms + + private AffineMatrix transform; + private OpenGLRunner glRunner; private HandlerThread cameraThread; private Handler cameraHandler; @@ -67,6 +85,9 @@ public class CameraCapture extends SurfaceCapture { this.aspectRatio = options.getCameraAspectRatio(); this.fps = options.getCameraFps(); this.highSpeed = options.getCameraHighSpeed(); + this.crop = options.getCrop(); + this.captureOrientation = options.getCaptureOrientation(); + assert captureOrientation != null; } @Override @@ -92,13 +113,26 @@ public class CameraCapture extends SurfaceCapture { @Override public void prepare() throws IOException { try { - size = selectSize(cameraId, explicitSize, maxSize, aspectRatio, highSpeed); - if (size == null) { + captureSize = selectSize(cameraId, explicitSize, maxSize, aspectRatio, highSpeed); + if (captureSize == null) { throw new IOException("Could not select camera size"); } } catch (CameraAccessException e) { throw new IOException(e); } + + VideoFilter filter = new VideoFilter(captureSize); + + if (crop != null) { + filter.addCrop(crop, false); + } + + if (captureOrientation != Orientation.Orient0) { + filter.addOrientation(captureOrientation); + } + + transform = filter.getInverseTransform(); + videoSize = filter.getOutputSize().limit(maxSize).round8(); } private static String selectCamera(String explicitCameraId, CameraFacing cameraFacing) throws CameraAccessException, ConfigurationException { @@ -214,15 +248,33 @@ public class CameraCapture extends SurfaceCapture { @Override public void start(Surface surface) throws IOException { + if (transform != null) { + assert glRunner == null; + OpenGLFilter glFilter = new AffineOpenGLFilter(transform); + // The transform matrix returned by SurfaceTexture is incorrect for camera capture (it often contains an additional unexpected 90° + // rotation). Use a vertical flip transform matrix instead. + glRunner = new OpenGLRunner(glFilter, VFLIP_MATRIX); + surface = glRunner.start(captureSize, videoSize, surface); + } + try { CameraCaptureSession session = createCaptureSession(cameraDevice, surface); CaptureRequest request = createCaptureRequest(surface); setRepeatingRequest(session, request); } catch (CameraAccessException | InterruptedException e) { + stop(); throw new IOException(e); } } + @Override + public void stop() { + if (glRunner != null) { + glRunner.stopAndRelease(); + glRunner = null; + } + } + @Override public void release() { if (cameraDevice != null) { @@ -235,7 +287,7 @@ public class CameraCapture extends SurfaceCapture { @Override public Size getSize() { - return size; + return videoSize; } @Override From 371ff3122590f35996f904d99d02a58986a8c617 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 14 Nov 2024 23:54:20 +0100 Subject: [PATCH 2074/2244] Apply filters to virtual display capture Apply crop and orientation to virtual display capture. PR #5455 --- .../scrcpy/video/NewDisplayCapture.java | 81 ++++++++++++++----- 1 file changed, 61 insertions(+), 20 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java index 6ce50521..bd4cf033 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java @@ -5,6 +5,7 @@ import com.genymobile.scrcpy.Options; import com.genymobile.scrcpy.control.PositionMapper; import com.genymobile.scrcpy.device.DisplayInfo; import com.genymobile.scrcpy.device.NewDisplay; +import com.genymobile.scrcpy.device.Orientation; import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.opengl.AffineOpenGLFilter; import com.genymobile.scrcpy.opengl.OpenGLFilter; @@ -13,6 +14,7 @@ import com.genymobile.scrcpy.util.AffineMatrix; import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.wrappers.ServiceManager; +import android.graphics.Rect; import android.hardware.display.VirtualDisplay; import android.os.Build; import android.view.Surface; @@ -41,15 +43,21 @@ public class NewDisplayCapture extends SurfaceCapture { private final DisplaySizeMonitor displaySizeMonitor = new DisplaySizeMonitor(); private AffineMatrix displayTransform; + private AffineMatrix eventTransform; private OpenGLRunner glRunner; private Size mainDisplaySize; private int mainDisplayDpi; private int maxSize; // only used if newDisplay.getSize() != null + private final Rect crop; + private final boolean captureOrientationLocked; + private final Orientation captureOrientation; private VirtualDisplay virtualDisplay; - private Size size; // the logical size of the display (including rotation) + private Size videoSize; + private Size displaySize; // the logical size of the display (including rotation) private Size physicalSize; // the physical size of the display (without rotation) + private int dpi; public NewDisplayCapture(VirtualDisplayListener vdListener, Options options) { @@ -57,13 +65,18 @@ public class NewDisplayCapture extends SurfaceCapture { this.newDisplay = options.getNewDisplay(); assert newDisplay != null; this.maxSize = options.getMaxSize(); + this.crop = options.getCrop(); + assert options.getCaptureOrientationLock() != null; + this.captureOrientationLocked = options.getCaptureOrientationLock() != Orientation.Lock.Unlocked; + this.captureOrientation = options.getCaptureOrientation(); + assert captureOrientation != null; } @Override protected void init() { - size = newDisplay.getSize(); + displaySize = newDisplay.getSize(); dpi = newDisplay.getDpi(); - if (size == null || dpi == 0) { + if (displaySize == null || dpi == 0) { DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(0); if (displayInfo != null) { mainDisplaySize = displayInfo.getSize(); @@ -78,28 +91,57 @@ public class NewDisplayCapture extends SurfaceCapture { @Override public void prepare() { + int displayRotation; if (virtualDisplay == null) { if (!newDisplay.hasExplicitSize()) { - size = mainDisplaySize.limit(maxSize).round8(); + displaySize = mainDisplaySize.limit(maxSize).round8(); } if (!newDisplay.hasExplicitDpi()) { - dpi = scaleDpi(mainDisplaySize, mainDisplayDpi, size); + dpi = scaleDpi(mainDisplaySize, mainDisplayDpi, displaySize); } - physicalSize = size; + videoSize = displaySize; + displayRotation = 0; // Set the current display size to avoid an unnecessary call to invalidate() - displaySizeMonitor.setSessionDisplaySize(size); + displaySizeMonitor.setSessionDisplaySize(displaySize); } else { DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(virtualDisplay.getDisplay().getDisplayId()); - size = displayInfo.getSize(); + displaySize = displayInfo.getSize(); dpi = displayInfo.getDpi(); - - VideoFilter displayFilter = new VideoFilter(size); - displayFilter.addRotation(displayInfo.getRotation()); - // The display info gives the oriented size, but the virtual display video always remains in the origin orientation - displayTransform = displayFilter.getInverseTransform(); - physicalSize = displayFilter.getOutputSize(); + displayRotation = displayInfo.getRotation(); } + + VideoFilter filter = new VideoFilter(displaySize); + + if (crop != null) { + boolean transposed = (displayRotation % 2) != 0; + filter.addCrop(crop, transposed); + } + + filter.addOrientation(displayRotation, captureOrientationLocked, captureOrientation); + + eventTransform = filter.getInverseTransform(); + + // DisplayInfo gives the oriented size (so videoSize includes the display rotation) + videoSize = filter.getOutputSize().limit(maxSize).round8(); + + // But the virtual display video always remains in the origin orientation (the video itself is not rotated, so it must rotated manually). + // This additional display rotation must not be included in the input events transform (the expected coordinates are already in the + // physical display size) + if ((displayRotation % 2) == 0) { + physicalSize = displaySize; + } else { + physicalSize = displaySize.rotate(); + } + VideoFilter displayFilter = new VideoFilter(physicalSize); + displayFilter.addRotation(displayRotation); + AffineMatrix displayRotationMatrix = displayFilter.getInverseTransform(); + + // Take care of multiplication order: + // displayTransform = (FILTER_MATRIX * DISPLAY_FILTER_MATRIX)⁻¹ + // = DISPLAY_FILTER_MATRIX⁻¹ * FILTER_MATRIX⁻¹ + // = displayRotationMatrix * eventTransform + displayTransform = AffineMatrix.multiplyAll(displayRotationMatrix, eventTransform); } public void startNew(Surface surface) { @@ -122,9 +164,9 @@ public class NewDisplayCapture extends SurfaceCapture { } } virtualDisplay = ServiceManager.getDisplayManager() - .createNewVirtualDisplay("scrcpy", size.getWidth(), size.getHeight(), dpi, surface, flags); + .createNewVirtualDisplay("scrcpy", displaySize.getWidth(), displaySize.getHeight(), dpi, surface, flags); virtualDisplayId = virtualDisplay.getDisplay().getDisplayId(); - Ln.i("New display: " + size.getWidth() + "x" + size.getHeight() + "/" + dpi + " (id=" + virtualDisplayId + ")"); + Ln.i("New display: " + displaySize.getWidth() + "x" + displaySize.getHeight() + "/" + dpi + " (id=" + virtualDisplayId + ")"); displaySizeMonitor.start(virtualDisplayId, this::invalidate); } catch (Exception e) { @@ -139,7 +181,7 @@ public class NewDisplayCapture extends SurfaceCapture { assert glRunner == null; OpenGLFilter glFilter = new AffineOpenGLFilter(displayTransform); glRunner = new OpenGLRunner(glFilter); - surface = glRunner.start(physicalSize, size, surface); + surface = glRunner.start(physicalSize, videoSize, surface); } if (virtualDisplay == null) { @@ -149,8 +191,7 @@ public class NewDisplayCapture extends SurfaceCapture { } if (vdListener != null) { - // The virtual display rotation must only be applied to video, it is already taken into account when injecting events! - PositionMapper positionMapper = PositionMapper.create(size, null, size); + PositionMapper positionMapper = PositionMapper.create(videoSize, eventTransform, displaySize); vdListener.onNewVirtualDisplay(virtualDisplay.getDisplay().getDisplayId(), positionMapper); } } @@ -175,7 +216,7 @@ public class NewDisplayCapture extends SurfaceCapture { @Override public synchronized Size getSize() { - return size; + return videoSize; } @Override From 4348f12194b5e44e710e61786b452ebe4b9eb850 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 15 Nov 2024 18:41:04 +0100 Subject: [PATCH 2075/2244] Improve mismatching event size warning Include both the event size and the current size in the warning message. PR #5455 --- .../java/com/genymobile/scrcpy/control/Controller.java | 9 +++++++-- .../com/genymobile/scrcpy/control/PositionMapper.java | 4 ++++ .../src/main/java/com/genymobile/scrcpy/device/Size.java | 2 +- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index 573e8f52..cafa11bd 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -8,6 +8,7 @@ import com.genymobile.scrcpy.device.Device; import com.genymobile.scrcpy.device.DeviceApp; import com.genymobile.scrcpy.device.Point; import com.genymobile.scrcpy.device.Position; +import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.LogUtils; import com.genymobile.scrcpy.video.SurfaceCapture; @@ -359,7 +360,9 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { Point point = displayData.positionMapper.map(position); if (point == null) { - Ln.w("Ignore touch event, it was generated for a different device size"); + Size eventSize = position.getScreenSize(); + Size currentSize = displayData.positionMapper.getVideoSize(); + Ln.w("Ignore touch event generated for size " + eventSize + " (current size is " + currentSize + ")"); return false; } @@ -473,7 +476,9 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { Point point = displayData.positionMapper.map(position); if (point == null) { - Ln.w("Ignore scroll event, it was generated for a different device size"); + Size eventSize = position.getScreenSize(); + Size currentSize = displayData.positionMapper.getVideoSize(); + Ln.w("Ignore scroll event generated for size " + eventSize + " (current size is " + currentSize + ")"); return false; } diff --git a/server/src/main/java/com/genymobile/scrcpy/control/PositionMapper.java b/server/src/main/java/com/genymobile/scrcpy/control/PositionMapper.java index 4d3b8875..60109b51 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/PositionMapper.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/PositionMapper.java @@ -27,6 +27,10 @@ public final class PositionMapper { return new PositionMapper(videoSize, transform); } + public Size getVideoSize() { + return videoSize; + } + public Point map(Position position) { Size clientVideoSize = position.getScreenSize(); if (!videoSize.equals(clientVideoSize)) { diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Size.java b/server/src/main/java/com/genymobile/scrcpy/device/Size.java index 3baa1bdd..6500b74e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/Size.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Size.java @@ -103,6 +103,6 @@ public final class Size { @Override public String toString() { - return "Size{" + width + 'x' + height + '}'; + return width + "x" + height; } } From 090488081652593a795ea40697ce703bb5b2a59c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 16 Nov 2024 22:16:17 +0100 Subject: [PATCH 2076/2244] Log event size mismatch as verbose On rotation, it is expected that many successive events are ignored due to size mismatch, when an event was generated from the mirroring window having the old size, but was received on the device with the new size (especially since mouse hover events are forwarded). Do not flood the console with warnings. PR #5455 --- .../genymobile/scrcpy/control/Controller.java | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index cafa11bd..f0e4c037 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -360,9 +360,11 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { Point point = displayData.positionMapper.map(position); if (point == null) { - Size eventSize = position.getScreenSize(); - Size currentSize = displayData.positionMapper.getVideoSize(); - Ln.w("Ignore touch event generated for size " + eventSize + " (current size is " + currentSize + ")"); + if (Ln.isEnabled(Ln.Level.VERBOSE)) { + Size eventSize = position.getScreenSize(); + Size currentSize = displayData.positionMapper.getVideoSize(); + Ln.v("Ignore touch event generated for size " + eventSize + " (current size is " + currentSize + ")"); + } return false; } @@ -476,9 +478,11 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { Point point = displayData.positionMapper.map(position); if (point == null) { - Size eventSize = position.getScreenSize(); - Size currentSize = displayData.positionMapper.getVideoSize(); - Ln.w("Ignore scroll event generated for size " + eventSize + " (current size is " + currentSize + ")"); + if (Ln.isEnabled(Ln.Level.VERBOSE)) { + Size eventSize = position.getScreenSize(); + Size currentSize = displayData.positionMapper.getVideoSize(); + Ln.v("Ignore scroll event generated for size " + eventSize + " (current size is " + currentSize + ")"); + } return false; } From 443f315f609b2bbd7d7c4f8e9ffc2b41bdc381f7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 15 Nov 2024 18:46:08 +0100 Subject: [PATCH 2077/2244] Use natural device orientation for --new-display If no size is provided with --new-display, the main display size is used. But the actual size depended on the current device orientation. To make it deterministic, use the size of the natural device orientation (portrait for phones, landscape for tablets). PR #5455 --- .../java/com/genymobile/scrcpy/video/NewDisplayCapture.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java index bd4cf033..3530cce8 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java @@ -80,6 +80,9 @@ public class NewDisplayCapture extends SurfaceCapture { DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(0); if (displayInfo != null) { mainDisplaySize = displayInfo.getSize(); + if ((displayInfo.getRotation() % 2) != 0) { + mainDisplaySize = mainDisplaySize.rotate(); // Use the natural device orientation (at rotation 0), not the current one + } mainDisplayDpi = displayInfo.getDpi(); } else { Ln.w("Main display not found, fallback to 1920x1080 240dpi"); From d19045628e422d8e969a137580bb9e6496ec51fc Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 15 Nov 2024 18:51:40 +0100 Subject: [PATCH 2078/2244] Remove deprecated options PR #5455 --- app/src/cli.c | 72 +++++++++------------------------------------------ 1 file changed, 12 insertions(+), 60 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index 55ccfc0d..291157c1 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -1590,18 +1590,6 @@ parse_audio_output_buffer(const char *s, sc_tick *tick) { 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_orientation(const char *s, enum sc_orientation *orientation) { if (!strcmp(s, "0")) { @@ -2276,8 +2264,8 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], opts->crop = optarg; break; case OPT_DISPLAY: - LOGW("--display is deprecated, use --display-id instead."); - // fall through + LOGE("--display has been removed, use --display-id instead."); + return false; case OPT_DISPLAY_ID: if (!parse_display_id(optarg, &opts->display_id)) { return false; @@ -2365,8 +2353,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], opts->control = false; break; case OPT_NO_DISPLAY: - LOGW("--no-display is deprecated, use --no-playback instead."); - // fall through + LOGE("--no-display has been removed, use --no-playback " + "instead."); + return false; case 'N': opts->video_playback = false; opts->audio_playback = false; @@ -2452,32 +2441,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], opts->key_inject_mode = SC_KEY_INJECT_MODE_RAW; break; case OPT_ROTATION: - LOGW("--rotation is deprecated, use --display-orientation " - "instead."); - uint8_t rotation; - if (!parse_rotation(optarg, &rotation)) { - return false; - } - assert(rotation <= 3); - switch (rotation) { - case 0: - opts->display_orientation = SC_ORIENTATION_0; - break; - case 1: - // rotation 1 was 90° counterclockwise, but orientation - // is expressed clockwise - opts->display_orientation = SC_ORIENTATION_270; - break; - case 2: - opts->display_orientation = SC_ORIENTATION_180; - break; - case 3: - // rotation 3 was 270° counterclockwise, but orientation - // is expressed clockwise - opts->display_orientation = SC_ORIENTATION_90; - break; - } - break; + LOGE("--rotation has been removed, use --orientation or " + "--capture-orientation instead."); + return false; case OPT_DISPLAY_ORIENTATION: if (!parse_orientation(optarg, &opts->display_orientation)) { return false; @@ -2538,23 +2504,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } break; case OPT_FORWARD_ALL_CLICKS: - LOGW("--forward-all-clicks is deprecated, " + LOGE("--forward-all-clicks has been removed, " "use --mouse-bind=++++ instead."); - opts->mouse_bindings = (struct sc_mouse_bindings) { - .pri = { - .right_click = SC_MOUSE_BINDING_CLICK, - .middle_click = SC_MOUSE_BINDING_CLICK, - .click4 = SC_MOUSE_BINDING_CLICK, - .click5 = SC_MOUSE_BINDING_CLICK, - }, - .sec = { - .right_click = SC_MOUSE_BINDING_CLICK, - .middle_click = SC_MOUSE_BINDING_CLICK, - .click4 = SC_MOUSE_BINDING_CLICK, - .click5 = SC_MOUSE_BINDING_CLICK, - }, - }; - break; + return false; case OPT_LEGACY_PASTE: opts->legacy_paste = true; break; @@ -2562,9 +2514,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], opts->power_off_on_close = true; break; case OPT_DISPLAY_BUFFER: - LOGW("--display-buffer is deprecated, use --video-buffer " + LOGE("--display-buffer has been removed, use --video-buffer " "instead."); - // fall through + return false; case OPT_VIDEO_BUFFER: if (!parse_buffering_time(optarg, &opts->video_buffer)) { return false; From adb674a5c890bf2553149059bfc22ba9df86a04b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 15 Nov 2024 19:17:04 +0100 Subject: [PATCH 2079/2244] Add --angle Add an option to rotate the video content by a custom angle. Fixes #4135 Fixes #4345 Refs #4658 PR #5455 --- app/data/bash-completion/scrcpy | 1 + app/data/zsh-completion/_scrcpy | 1 + app/scrcpy.1 | 4 ++++ app/src/cli.c | 11 +++++++++++ app/src/options.c | 1 + app/src/options.h | 1 + app/src/scrcpy.c | 1 + app/src/server.c | 4 ++++ app/src/server.h | 1 + doc/video.md | 11 +++++++++++ .../src/main/java/com/genymobile/scrcpy/Options.java | 8 ++++++++ .../com/genymobile/scrcpy/video/CameraCapture.java | 4 ++++ .../genymobile/scrcpy/video/NewDisplayCapture.java | 3 +++ .../com/genymobile/scrcpy/video/ScreenCapture.java | 3 +++ .../java/com/genymobile/scrcpy/video/VideoFilter.java | 8 ++++++++ 15 files changed, 62 insertions(+) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index c2f32ad0..cddfc4a6 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -2,6 +2,7 @@ _scrcpy() { local cur prev words cword local opts=" --always-on-top + --angle --audio-bit-rate= --audio-buffer= --audio-codec= diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 59019904..cda49e8e 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -9,6 +9,7 @@ local arguments arguments=( '--always-on-top[Make scrcpy window always on top \(above other windows\)]' + '--angle=[Rotate the video content by a custom angle, in degrees]' '--audio-bit-rate=[Encode the audio at the given bit-rate]' '--audio-buffer=[Configure the audio buffering delay (in milliseconds)]' '--audio-codec=[Select the audio codec]:codec:(opus aac flac raw)' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index f0c1e0f1..543801bc 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -19,6 +19,10 @@ provides display and control of Android devices connected on USB (or over TCP/IP .B \-\-always\-on\-top Make scrcpy window always on top (above other windows). +.TP +.BI "\-\-angle " degrees +Rotate the video content by a custom angle, in degrees (clockwise). + .TP .BI "\-\-audio\-bit\-rate " value Encode the audio at the given bit rate, expressed in bits/s. Unit suffixes are supported: '\fBK\fR' (x1000) and '\fBM\fR' (x1000000). diff --git a/app/src/cli.c b/app/src/cli.c index 291157c1..95dad3d7 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -108,6 +108,7 @@ enum { OPT_START_APP, OPT_SCREEN_OFF_TIMEOUT, OPT_CAPTURE_ORIENTATION, + OPT_ANGLE, }; struct sc_option { @@ -149,6 +150,13 @@ static const struct sc_option options[] = { .longopt = "always-on-top", .text = "Make scrcpy window always on top (above other windows).", }, + { + .longopt_id = OPT_ANGLE, + .longopt = "angle", + .argdesc = "degrees", + .text = "Rotate the video content by a custom angle, in degrees " + "(clockwise).", + }, { .longopt_id = OPT_AUDIO_BIT_RATE, .longopt = "audio-bit-rate", @@ -2689,6 +2697,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; } break; + case OPT_ANGLE: + opts->angle = optarg; + break; default: // getopt prints the error message on stderr return false; diff --git a/app/src/options.c b/app/src/options.c index 69f8f64d..adc7ba0c 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -107,6 +107,7 @@ const struct scrcpy_options scrcpy_options_default = { .audio_dup = false, .new_display = NULL, .start_app = NULL, + .angle = NULL, }; enum sc_orientation diff --git a/app/src/options.h b/app/src/options.h index 945fcdf7..0692276e 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -247,6 +247,7 @@ struct scrcpy_options { uint32_t video_bit_rate; uint32_t audio_bit_rate; const char *max_fps; // float to be parsed by the server + const char *angle; // float to be parsed by the server enum sc_orientation capture_orientation; enum sc_orientation_lock capture_orientation_lock; enum sc_orientation display_orientation; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 5528910a..48befb1d 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -428,6 +428,7 @@ scrcpy(struct scrcpy_options *options) { .video_bit_rate = options->video_bit_rate, .audio_bit_rate = options->audio_bit_rate, .max_fps = options->max_fps, + .angle = options->angle, .screen_off_timeout = options->screen_off_timeout, .capture_orientation = options->capture_orientation, .capture_orientation_lock = options->capture_orientation_lock, diff --git a/app/src/server.c b/app/src/server.c index 9c12500e..9c81a7f6 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -274,6 +274,10 @@ execute_server(struct sc_server *server, VALIDATE_STRING(params->max_fps); ADD_PARAM("max_fps=%s", params->max_fps); } + if (params->angle) { + VALIDATE_STRING(params->angle); + ADD_PARAM("angle=%s", params->angle); + } if (params->capture_orientation_lock != SC_ORIENTATION_UNLOCKED || params->capture_orientation != SC_ORIENTATION_0) { if (params->capture_orientation_lock == SC_ORIENTATION_LOCKED_INITIAL) { diff --git a/app/src/server.h b/app/src/server.h index 20d998e9..9d46b354 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -45,6 +45,7 @@ struct sc_server_params { uint32_t video_bit_rate; uint32_t audio_bit_rate; const char *max_fps; // float to be parsed by the server + const char *angle; // float to be parsed by the server sc_tick screen_off_timeout; enum sc_orientation capture_orientation; enum sc_orientation_lock capture_orientation_lock; diff --git a/doc/video.md b/doc/video.md index c00b6602..9e57e1af 100644 --- a/doc/video.md +++ b/doc/video.md @@ -159,6 +159,17 @@ to the MP4 or MKV target file. Flipping is not supported, so only the 4 first values are allowed when recording. +## Angle + +To rotate the video content by a custom angle (in degrees, clockwise): + +``` +scrcpy --angle=23 +``` + +The center of rotation is the center of the visible area (after cropping). + + ## Crop The device screen may be cropped to mirror only part of the screen. diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index e1b3b9af..6a59fbe7 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -34,6 +34,7 @@ public class Options { private int videoBitRate = 8000000; private int audioBitRate = 128000; private float maxFps; + private float angle; private boolean tunnelForward; private Rect crop; private boolean control = true; @@ -127,6 +128,10 @@ public class Options { return maxFps; } + public float getAngle() { + return angle; + } + public boolean isTunnelForward() { return tunnelForward; } @@ -349,6 +354,9 @@ public class Options { case "max_fps": options.maxFps = parseFloat("max_fps", value); break; + case "angle": + options.angle = parseFloat("angle", value); + break; case "tunnel_forward": options.tunnelForward = Boolean.parseBoolean(value); break; diff --git a/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java index 5a18aeac..0e147cb7 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java @@ -62,6 +62,7 @@ public class CameraCapture extends SurfaceCapture { private final boolean highSpeed; private final Rect crop; private final Orientation captureOrientation; + private final float angle; private String cameraId; private Size captureSize; @@ -88,6 +89,7 @@ public class CameraCapture extends SurfaceCapture { this.crop = options.getCrop(); this.captureOrientation = options.getCaptureOrientation(); assert captureOrientation != null; + this.angle = options.getAngle(); } @Override @@ -131,6 +133,8 @@ public class CameraCapture extends SurfaceCapture { filter.addOrientation(captureOrientation); } + filter.addAngle(angle); + transform = filter.getInverseTransform(); videoSize = filter.getOutputSize().limit(maxSize).round8(); } diff --git a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java index 3530cce8..6a70704e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java @@ -52,6 +52,7 @@ public class NewDisplayCapture extends SurfaceCapture { private final Rect crop; private final boolean captureOrientationLocked; private final Orientation captureOrientation; + private final float angle; private VirtualDisplay virtualDisplay; private Size videoSize; @@ -70,6 +71,7 @@ public class NewDisplayCapture extends SurfaceCapture { this.captureOrientationLocked = options.getCaptureOrientationLock() != Orientation.Lock.Unlocked; this.captureOrientation = options.getCaptureOrientation(); assert captureOrientation != null; + this.angle = options.getAngle(); } @Override @@ -122,6 +124,7 @@ public class NewDisplayCapture extends SurfaceCapture { } filter.addOrientation(displayRotation, captureOrientationLocked, captureOrientation); + filter.addAngle(angle); eventTransform = filter.getInverseTransform(); diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java index 432d0ae8..47425d09 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java @@ -33,6 +33,7 @@ public class ScreenCapture extends SurfaceCapture { private final Rect crop; private Orientation.Lock captureOrientationLock; private Orientation captureOrientation; + private final float angle; private DisplayInfo displayInfo; private Size videoSize; @@ -55,6 +56,7 @@ public class ScreenCapture extends SurfaceCapture { this.captureOrientation = options.getCaptureOrientation(); assert captureOrientationLock != null; assert captureOrientation != null; + this.angle = options.getAngle(); } @Override @@ -92,6 +94,7 @@ public class ScreenCapture extends SurfaceCapture { boolean locked = captureOrientationLock != Orientation.Lock.Unlocked; filter.addOrientation(displayInfo.getRotation(), locked, captureOrientation); + filter.addAngle(angle); transform = filter.getInverseTransform(); videoSize = filter.getOutputSize().limit(maxSize).round8(); diff --git a/server/src/main/java/com/genymobile/scrcpy/video/VideoFilter.java b/server/src/main/java/com/genymobile/scrcpy/video/VideoFilter.java index 8aadaa0d..6bffb51a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/VideoFilter.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/VideoFilter.java @@ -95,4 +95,12 @@ public class VideoFilter { } addOrientation(captureOrientation); } + + public void addAngle(double cwAngle) { + if (cwAngle == 0) { + return; + } + double ccwAngle = -cwAngle; + transform = AffineMatrix.rotate(ccwAngle).withAspectRatio(size).fromCenter().multiply(transform); + } } From f95a5f97b1944b948a45218d349d2acf8e19ac3b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 19 Nov 2024 18:30:19 +0100 Subject: [PATCH 2080/2244] Document filter order Matrix multiplication is not commutative, so the order of filters matters. PR #5455 --- app/scrcpy.1 | 4 +--- app/src/cli.c | 3 +-- doc/video.md | 18 +++++++++++++++--- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 543801bc..bb77d25e 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -141,9 +141,7 @@ Default is 0. .BI "\-\-crop " width\fR:\fIheight\fR:\fIx\fR:\fIy Crop the device screen on the server. -The values are expressed in the device natural orientation (typically, portrait for a phone, landscape for a tablet). Any -.B \-\-max\-size -value is computed on the cropped size. +The values are expressed in the device natural orientation (typically, portrait for a phone, landscape for a tablet). .TP .B \-d, \-\-select\-usb diff --git a/app/src/cli.c b/app/src/cli.c index 95dad3d7..08f1db2b 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -311,8 +311,7 @@ static const struct sc_option options[] = { .argdesc = "width:height:x:y", .text = "Crop the device screen on the server.\n" "The values are expressed in the device natural orientation " - "(typically, portrait for a phone, landscape for a tablet). " - "Any --max-size value is computed on the cropped size.", + "(typically, portrait for a phone, landscape for a tablet).", }, { .shortopt = 'd', diff --git a/doc/video.md b/doc/video.md index 9e57e1af..5f3a42cb 100644 --- a/doc/video.md +++ b/doc/video.md @@ -27,6 +27,9 @@ preserved. That way, a device in 1920×1080 will be mirrored at 1024×576. If encoding fails, scrcpy automatically tries again with a lower definition (unless `--no-downsize-on-error` is enabled). +For camera mirroring, the `--max-size` value is used to select the camera source +size instead (among the available resolutions). + ## Bit rate @@ -138,7 +141,10 @@ scrcpy --capture-orientation=@flip180 # locked to hflip + 180° scrcpy --capture-orientation=@flip270 # locked to hflip + 270° clockwise ``` -To orient the video (on the rendering side): +The capture orientation transform is applied after `--crop`, but before +`--angle`. + +To orient the video (on the client side): ```bash scrcpy --orientation=0 @@ -167,7 +173,9 @@ To rotate the video content by a custom angle (in degrees, clockwise): scrcpy --angle=23 ``` -The center of rotation is the center of the visible area (after cropping). +The center of rotation is the center of the visible area. + +This transformation is applied after `--crop` and `--capture-orientation`. ## Crop @@ -183,7 +191,11 @@ scrcpy --crop=1224:1440:0:0 # 1224x1440 at offset (0,0) The values are expressed in the device natural orientation (portrait for a phone, landscape for a tablet). -If `--max-size` is also specified, resizing is applied after cropping. +Cropping is performed before `--capture-orientation` and `--angle`. + +For screen mirroring, `--max-size` is applied after cropping. For camera and +virtual display mirroring, `--max-size` is applied first (because it selects the +source size rather than resizing it). ## Display From 36d61f9ecd853104ba838d8df18102c31320fd0c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 19 Nov 2024 18:45:05 +0100 Subject: [PATCH 2081/2244] Reference virtual display documentation Reference the documentation about virtual displays from the "Display" section of video.md. --- doc/video.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/video.md b/doc/video.md index 5f3a42cb..63b6078c 100644 --- a/doc/video.md +++ b/doc/video.md @@ -216,6 +216,8 @@ scrcpy --list-displays A secondary display may only be controlled if the device runs at least Android 10 (otherwise it is mirrored as read-only). +It is also possible to create a [virtual display](virtual_display.md). + ## Buffering From 28d64ef319337f2313d74be4d16653774ec8185e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 20 Nov 2024 07:45:15 +0100 Subject: [PATCH 2082/2244] Fix --new-display bash completion The option --new-display accepts an optional argument, but bash must not try to auto-complete it with unrelated content. --- app/data/bash-completion/scrcpy | 1 + 1 file changed, 1 insertion(+) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index cddfc4a6..8fae972f 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -194,6 +194,7 @@ _scrcpy() { |--display-id \ |--max-fps \ |-m|--max-size \ + |--new-display \ |-p|--port \ |--push-target \ |--rotation \ From 145b823b1d029a34f640d5a3f231439e45b0eaf7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 16 Nov 2024 22:45:38 +0100 Subject: [PATCH 2083/2244] Add --no-vd-system-decorations Add an option to disable the following flag for virtual displays: DisplayManager.VIRTUAL_DISPLAY_FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS Some devices render a broken UI when this flag is enabled. Fixes #5494 --- app/data/bash-completion/scrcpy | 1 + app/data/zsh-completion/_scrcpy | 1 + app/scrcpy.1 | 4 ++++ app/src/cli.c | 9 +++++++++ app/src/options.c | 1 + app/src/options.h | 1 + app/src/scrcpy.c | 1 + app/src/server.c | 3 +++ app/src/server.h | 1 + doc/virtual_display.md | 10 ++++++++++ .../src/main/java/com/genymobile/scrcpy/Options.java | 8 ++++++++ .../com/genymobile/scrcpy/video/NewDisplayCapture.java | 8 ++++++-- 12 files changed, 46 insertions(+), 2 deletions(-) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index 8fae972f..6c88927e 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -57,6 +57,7 @@ _scrcpy() { --no-mipmaps --no-mouse-hover --no-power-on + --no-vd-system-decorations --no-video --no-video-playback --orientation= diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index cda49e8e..e0c5e265 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -63,6 +63,7 @@ arguments=( '--no-mipmaps[Disable the generation of mipmaps]' '--no-mouse-hover[Do not forward mouse hover events]' '--no-power-on[Do not power on the device on start]' + '--no-vd-system-decorations[Disable virtual display system decorations flag]' '--no-video[Disable video forwarding]' '--no-video-playback[Disable video playback]' '--orientation=[Set the video orientation]:orientation values:(0 90 180 270 flip0 flip90 flip180 flip270)' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index bb77d25e..711c53c6 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -370,6 +370,10 @@ Do not forward mouse hover (mouse motion without any clicks) events. .B \-\-no\-power\-on Do not power on the device on start. +.TP +.B \-\-no\-vd\-system\-decorations +Disable virtual display system decorations flag. + .TP .B \-\-no\-video Disable video forwarding. diff --git a/app/src/cli.c b/app/src/cli.c index 08f1db2b..177bf934 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -109,6 +109,7 @@ enum { OPT_SCREEN_OFF_TIMEOUT, OPT_CAPTURE_ORIENTATION, OPT_ANGLE, + OPT_NO_VD_SYSTEM_DECORATIONS, }; struct sc_option { @@ -659,6 +660,11 @@ static const struct sc_option options[] = { .longopt = "no-power-on", .text = "Do not power on the device on start.", }, + { + .longopt_id = OPT_NO_VD_SYSTEM_DECORATIONS, + .longopt = "no-vd-system-decorations", + .text = "Disable virtual display system decorations flag.", + }, { .longopt_id = OPT_NO_VIDEO, .longopt = "no-video", @@ -2699,6 +2705,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], case OPT_ANGLE: opts->angle = optarg; break; + case OPT_NO_VD_SYSTEM_DECORATIONS: + opts->vd_system_decorations = optarg; + break; default: // getopt prints the error message on stderr return false; diff --git a/app/src/options.c b/app/src/options.c index adc7ba0c..be3cf8d1 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -108,6 +108,7 @@ const struct scrcpy_options scrcpy_options_default = { .new_display = NULL, .start_app = NULL, .angle = NULL, + .vd_system_decorations = true, }; enum sc_orientation diff --git a/app/src/options.h b/app/src/options.h index 0692276e..eaeba2f2 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -310,6 +310,7 @@ struct scrcpy_options { bool audio_dup; const char *new_display; // [x][/] parsed by the server const char *start_app; + bool vd_system_decorations; }; extern const struct scrcpy_options scrcpy_options_default; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 48befb1d..dc9e237f 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -458,6 +458,7 @@ scrcpy(struct scrcpy_options *options) { .power_on = options->power_on, .kill_adb_on_close = options->kill_adb_on_close, .camera_high_speed = options->camera_high_speed, + .vd_system_decorations = options->vd_system_decorations, .list = options->list, }; diff --git a/app/src/server.c b/app/src/server.c index 9c81a7f6..ce7b1aaf 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -376,6 +376,9 @@ execute_server(struct sc_server *server, VALIDATE_STRING(params->new_display); ADD_PARAM("new_display=%s", params->new_display); } + if (!params->vd_system_decorations) { + ADD_PARAM("vd_system_decorations=false"); + } if (params->list & SC_OPTION_LIST_ENCODERS) { ADD_PARAM("list_encoders=true"); } diff --git a/app/src/server.h b/app/src/server.h index 9d46b354..6d9dbd4d 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -69,6 +69,7 @@ struct sc_server_params { bool power_on; bool kill_adb_on_close; bool camera_high_speed; + bool vd_system_decorations; uint8_t list; }; diff --git a/doc/virtual_display.md b/doc/virtual_display.md index 4ed5961f..97ac01b2 100644 --- a/doc/virtual_display.md +++ b/doc/virtual_display.md @@ -24,3 +24,13 @@ For example: ```bash scrcpy --new-display=1920x1080 --start-app=org.videolan.vlc ``` + +## System decorations + +By default, virtual display system decorations are enabled. But some devices +might display a broken UI; + +Use `--no-vd-system-decorations` to disable it. + +Note that if no app is started, no content will be rendered, so no video frame +will be produced at all. diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 6a59fbe7..43cc790d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -60,6 +60,7 @@ public class Options { private boolean powerOn = true; private NewDisplay newDisplay; + private boolean vdSystemDecorations = true; private Orientation.Lock captureOrientationLock = Orientation.Lock.Unlocked; private Orientation captureOrientation = Orientation.Orient0; @@ -232,6 +233,10 @@ public class Options { return captureOrientationLock; } + public boolean getVDSystemDecorations() { + return vdSystemDecorations; + } + public boolean getList() { return listEncoders || listDisplays || listCameras || listCameraSizes || listApps; } @@ -461,6 +466,9 @@ public class Options { case "new_display": options.newDisplay = parseNewDisplay(value); break; + case "vd_system_decorations": + options.vdSystemDecorations = Boolean.parseBoolean(value); + break; case "capture_orientation": Pair pair = parseCaptureOrientation(value); options.captureOrientationLock = pair.first; diff --git a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java index 6a70704e..dc9c8897 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java @@ -53,6 +53,7 @@ public class NewDisplayCapture extends SurfaceCapture { private final boolean captureOrientationLocked; private final Orientation captureOrientation; private final float angle; + private final boolean vdSystemDecorations; private VirtualDisplay virtualDisplay; private Size videoSize; @@ -72,6 +73,7 @@ public class NewDisplayCapture extends SurfaceCapture { this.captureOrientation = options.getCaptureOrientation(); assert captureOrientation != null; this.angle = options.getAngle(); + this.vdSystemDecorations = options.getVDSystemDecorations(); } @Override @@ -157,8 +159,10 @@ public class NewDisplayCapture extends SurfaceCapture { | VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY | VIRTUAL_DISPLAY_FLAG_SUPPORTS_TOUCH | VIRTUAL_DISPLAY_FLAG_ROTATES_WITH_CONTENT - | VIRTUAL_DISPLAY_FLAG_DESTROY_CONTENT_ON_REMOVAL - | VIRTUAL_DISPLAY_FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS; + | VIRTUAL_DISPLAY_FLAG_DESTROY_CONTENT_ON_REMOVAL; + if (vdSystemDecorations) { + flags |= VIRTUAL_DISPLAY_FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS; + } if (Build.VERSION.SDK_INT >= AndroidVersions.API_33_ANDROID_13) { flags |= VIRTUAL_DISPLAY_FLAG_TRUSTED | VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP From 2ec30bdf8038899b0b0c0fdb4924725561072c66 Mon Sep 17 00:00:00 2001 From: backryun Date: Wed, 2 Oct 2024 04:17:09 +0900 Subject: [PATCH 2084/2244] Upgrade FFmpeg (7.1) for Windows PR #5332 Signed-off-by: Romain Vimont --- app/deps/ffmpeg.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/deps/ffmpeg.sh b/app/deps/ffmpeg.sh index 89431542..93612c1b 100755 --- a/app/deps/ffmpeg.sh +++ b/app/deps/ffmpeg.sh @@ -4,10 +4,10 @@ DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) cd "$DEPS_DIR" . common -VERSION=7.0.2 +VERSION=7.1 FILENAME=ffmpeg-$VERSION.tar.xz PROJECT_DIR=ffmpeg-$VERSION -SHA256SUM=8646515b638a3ad303e23af6a3587734447cb8fc0a0c064ecdb8e95c4fd8b389 +SHA256SUM=40973D44970DBC83EF302B0609F2E74982BE2D85916DD2EE7472D30678A7ABE6 cd "$SOURCES_DIR" From eeb04292a47f7ef7519b2eb9fd5c06a81bb69352 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 20 Nov 2024 07:57:35 +0100 Subject: [PATCH 2085/2244] Upgrade SDL (2.30.9) for Windows --- app/deps/sdl.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/deps/sdl.sh b/app/deps/sdl.sh index c8b62746..55866ccd 100755 --- a/app/deps/sdl.sh +++ b/app/deps/sdl.sh @@ -4,10 +4,10 @@ DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) cd "$DEPS_DIR" . common -VERSION=2.30.7 +VERSION=2.30.9 FILENAME=SDL-$VERSION.tar.gz PROJECT_DIR=SDL-release-$VERSION -SHA256SUM=1578c96f62c9ae36b64e431b2aa0e0b0fd07c275dedbc694afc38e19056688f5 +SHA256SUM=682a055004081e37d81a7d4ce546c3ee3ef2e0e6a675ed2651e430ccd14eb407 cd "$SOURCES_DIR" From f1f27116269ecbe422fd853ec192e3a80e168f76 Mon Sep 17 00:00:00 2001 From: Gutem Date: Tue, 22 Oct 2024 21:43:22 -0300 Subject: [PATCH 2086/2244] Document missing --cask option for macOS Installing android-platform-tools via brew install requires the option --cask. Refs #2004 Refs #2231 PR #5398 Signed-off-by: Romain Vimont --- doc/macos.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/macos.md b/doc/macos.md index 35d90e9d..2c7c6071 100644 --- a/doc/macos.md +++ b/doc/macos.md @@ -13,7 +13,7 @@ brew install scrcpy You need `adb`, accessible from your `PATH`. If you don't have it yet: ```bash -brew install android-platform-tools +brew install --cask android-platform-tools ``` Alternatively, Scrcpy is also available in [MacPorts], which sets up `adb` for you: From 4608a19a1313c3c2805ca05a3106798504ecb642 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 20 Nov 2024 08:14:04 +0100 Subject: [PATCH 2087/2244] Upgrade platform-tools (35.0.2) for Windows Since 35.0.1, the filename has changed on the server from -windows.zip to -win.zip The links are referenced from this file: Refs --- app/deps/adb.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/deps/adb.sh b/app/deps/adb.sh index 58a54659..b07f29b3 100755 --- a/app/deps/adb.sh +++ b/app/deps/adb.sh @@ -4,10 +4,10 @@ DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) cd "$DEPS_DIR" . common -VERSION=35.0.0 -FILENAME=platform-tools_r$VERSION-windows.zip +VERSION=35.0.2 +FILENAME=platform-tools_r$VERSION-win.zip PROJECT_DIR=platform-tools-$VERSION -SHA256SUM=7ab78a8f8b305ae4d0de647d99c43599744de61a0838d3a47bda0cdffefee87e +SHA256SUM=2975a3eac0b19182748d64195375ad056986561d994fffbdc64332a516300bb9 cd "$SOURCES_DIR" From 264110fd70cbdc1350c08618d158d34bf4c55bbd Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 20 Nov 2024 13:01:57 +0100 Subject: [PATCH 2088/2244] Dissociate virtual display size and capture size Allow capturing virtual displays at a lower resolution using -m/--max-size. In the original implementation in #5370, the virtual display size was necessarily the same as the capture size. The --max-size value was only allowed to determine the virtual display size when no explicit size was provided. Since the dpi was scaled down accordingly, it is often better to create a virtual display at the target capture size directly. However, not everything is rendered according to the virtual display DPI. For example, a page in Firefox is rendered too big on small virtual displays. Thus, it makes sense to be able create a virtual display at a given size, and capture it at a lower resolution with --max-size. This is now possible using OpenGL filters. Therefore, change the behavior of --max-size for virtual displays: - --max-size does not impact --new-display without size argument anymore (the virtual display size is the main display size); - it is used to limit the capture size (whether an explicit size is provided or not). This new behavior is consistent with main display capture. Refs #5370 comment Refs #5370 PR #5506 --- app/scrcpy.1 | 3 +-- app/src/cli.c | 10 +--------- doc/video.md | 6 +++--- doc/virtual_display.md | 1 - .../com/genymobile/scrcpy/device/Size.java | 6 +++++- .../scrcpy/video/NewDisplayCapture.java | 20 +++++++++++-------- .../genymobile/scrcpy/video/VideoFilter.java | 13 ++++++++++++ 7 files changed, 35 insertions(+), 24 deletions(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 711c53c6..95d5133d 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -318,14 +318,13 @@ Disable video and audio playback on the computer (equivalent to \fB\-\-no\-video .TP \fB\-\-new\-display\fR[=[\fIwidth\fRx\fIheight\fR][/\fIdpi\fR]] -Create a new display with the specified resolution and density. If not provided, they default to the main display dimensions and DPI, and \fB\-\-max\-size\fR is considered. +Create a new display with the specified resolution and density. If not provided, they default to the main display dimensions and DPI. Examples: \-\-new\-display=1920x1080 \-\-new\-display=1920x1080/420 \-\-new\-display # main display size and density - \-\-new\-display -m1920 # scaled to fit a max size of 1920 \-\-new\-display=/240 # main display size and 240 dpi .TP diff --git a/app/src/cli.c b/app/src/cli.c index 177bf934..3f2d23cb 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -590,12 +590,11 @@ static const struct sc_option options[] = { .optional_arg = true, .text = "Create a new display with the specified resolution and " "density. If not provided, they default to the main display " - "dimensions and DPI, and --max-size is considered.\n" + "dimensions and DPI.\n" "Examples:\n" " --new-display=1920x1080\n" " --new-display=1920x1080/420 # force 420 dpi\n" " --new-display # main display size and density\n" - " --new-display -m1920 # scaled to fit a max size of 1920\n" " --new-display=/240 # main display size and 240 dpi", }, { @@ -2891,13 +2890,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], LOGE("--new-display is incompatible with --no-video"); return false; } - - if (opts->max_size && opts->new_display[0] != '\0' - && opts->new_display[0] != '/') { - // An explicit size is defined (not "" nor "/") - LOGE("Cannot specify both --new-display size and -m/--max-size"); - return false; - } } if (otg) { diff --git a/doc/video.md b/doc/video.md index 63b6078c..db9571f7 100644 --- a/doc/video.md +++ b/doc/video.md @@ -193,9 +193,9 @@ phone, landscape for a tablet). Cropping is performed before `--capture-orientation` and `--angle`. -For screen mirroring, `--max-size` is applied after cropping. For camera and -virtual display mirroring, `--max-size` is applied first (because it selects the -source size rather than resizing it). +For display mirroring, `--max-size` is applied after cropping. For camera, +`--max-size` is applied first (because it selects the source size rather than +resizing the content). ## Display diff --git a/doc/virtual_display.md b/doc/virtual_display.md index 97ac01b2..7523c118 100644 --- a/doc/virtual_display.md +++ b/doc/virtual_display.md @@ -8,7 +8,6 @@ To mirror a new virtual display instead of the device screen: scrcpy --new-display=1920x1080 scrcpy --new-display=1920x1080/420 # force 420 dpi scrcpy --new-display # use the main display size and density -scrcpy --new-display -m1920 # ... scaled to fit a max size of 1920 scrcpy --new-display=/240 # use the main display size and 240 dpi ``` diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Size.java b/server/src/main/java/com/genymobile/scrcpy/device/Size.java index 6500b74e..b448273d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/Size.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Size.java @@ -60,7 +60,7 @@ public final class Size { * @return The current size rounded. */ public Size round8() { - if ((width & 7) == 0 && (height & 7) == 0) { + if (isMultipleOf8()) { // Already a multiple of 8 return this; } @@ -80,6 +80,10 @@ public final class Size { return new Size(w, h); } + public boolean isMultipleOf8() { + return (width & 7) == 0 && (height & 7) == 0; + } + public Rect toRect() { return new Rect(0, 0, width, height); } diff --git a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java index dc9c8897..d92141af 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java @@ -48,7 +48,7 @@ public class NewDisplayCapture extends SurfaceCapture { private Size mainDisplaySize; private int mainDisplayDpi; - private int maxSize; // only used if newDisplay.getSize() != null + private int maxSize; private final Rect crop; private final boolean captureOrientationLocked; private final Orientation captureOrientation; @@ -101,7 +101,7 @@ public class NewDisplayCapture extends SurfaceCapture { int displayRotation; if (virtualDisplay == null) { if (!newDisplay.hasExplicitSize()) { - displaySize = mainDisplaySize.limit(maxSize).round8(); + displaySize = mainDisplaySize; } if (!newDisplay.hasExplicitDpi()) { dpi = scaleDpi(mainDisplaySize, mainDisplayDpi, displaySize); @@ -128,10 +128,19 @@ public class NewDisplayCapture extends SurfaceCapture { filter.addOrientation(displayRotation, captureOrientationLocked, captureOrientation); filter.addAngle(angle); + Size filteredSize = filter.getOutputSize(); + if (!filteredSize.isMultipleOf8() || (maxSize != 0 && filteredSize.getMax() > maxSize)) { + if (maxSize != 0) { + filteredSize = filteredSize.limit(maxSize); + } + filteredSize = filteredSize.round8(); + filter.addResize(filteredSize); + } + eventTransform = filter.getInverseTransform(); // DisplayInfo gives the oriented size (so videoSize includes the display rotation) - videoSize = filter.getOutputSize().limit(maxSize).round8(); + videoSize = filter.getOutputSize(); // But the virtual display video always remains in the origin orientation (the video itself is not rotated, so it must rotated manually). // This additional display rotation must not be included in the input events transform (the expected coordinates are already in the @@ -231,11 +240,6 @@ public class NewDisplayCapture extends SurfaceCapture { @Override public synchronized boolean setMaxSize(int newMaxSize) { - if (newDisplay.hasExplicitSize()) { - // Cannot retry with a different size if the display size was explicitly provided - return false; - } - maxSize = newMaxSize; return true; } diff --git a/server/src/main/java/com/genymobile/scrcpy/video/VideoFilter.java b/server/src/main/java/com/genymobile/scrcpy/video/VideoFilter.java index 6bffb51a..a27915ee 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/VideoFilter.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/VideoFilter.java @@ -103,4 +103,17 @@ public class VideoFilter { double ccwAngle = -cwAngle; transform = AffineMatrix.rotate(ccwAngle).withAspectRatio(size).fromCenter().multiply(transform); } + + public void addResize(Size targetSize) { + if (size.equals(targetSize)) { + return; + } + + if (transform == null) { + // The requested scaling is performed by the viewport (by changing the output size), but the OpenGL filter must still run, even if + // resizing is not performed by the shader. So transform MUST NOT be null. + transform = AffineMatrix.IDENTITY; + } + size = targetSize; + } } From 0e50d1e7dba940bdf3875bfa3855b4227f40a861 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 22 Nov 2024 07:45:04 +0100 Subject: [PATCH 2089/2244] Extract PLATFORM_TOOLS in build_without_gradle.sh Refs #5512 --- server/build_without_gradle.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index 206aa604..6add5a69 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -16,6 +16,7 @@ SCRCPY_VERSION_NAME=2.7 PLATFORM=${ANDROID_PLATFORM:-35} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-35.0.0} +PLATFORM_TOOLS="$ANDROID_HOME/platforms/android-$PLATFORM" BUILD_TOOLS_DIR="$ANDROID_HOME/build-tools/$BUILD_TOOLS" BUILD_DIR="$(realpath ${BUILD_DIR:-build_manual})" @@ -23,7 +24,7 @@ CLASSES_DIR="$BUILD_DIR/classes" GEN_DIR="$BUILD_DIR/gen" SERVER_DIR=$(dirname "$0") SERVER_BINARY=scrcpy-server -ANDROID_JAR="$ANDROID_HOME/platforms/android-$PLATFORM/android.jar" +ANDROID_JAR="$PLATFORM_TOOLS/android.jar" LAMBDA_JAR="$BUILD_TOOLS_DIR/core-lambda-stubs.jar" echo "Platform: android-$PLATFORM" From 24588cb637496c04330bf12f6350a1369b08a718 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 22 Nov 2024 07:48:00 +0100 Subject: [PATCH 2090/2244] Add missing aidl in build_without_gradle.sh Refs 39d51ff2cc2f3e201ad433d48372b548e5dd11d3 Fixes #5512 --- 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 6add5a69..7b293e02 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -25,6 +25,7 @@ GEN_DIR="$BUILD_DIR/gen" SERVER_DIR=$(dirname "$0") SERVER_BINARY=scrcpy-server ANDROID_JAR="$PLATFORM_TOOLS/android.jar" +ANDROID_AIDL="$PLATFORM_TOOLS/framework.aidl" LAMBDA_JAR="$BUILD_TOOLS_DIR/core-lambda-stubs.jar" echo "Platform: android-$PLATFORM" @@ -50,6 +51,8 @@ cd "$SERVER_DIR/src/main/aidl" "$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" -I. \ android/content/IOnPrimaryClipChangedListener.aidl "$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" -I. android/view/IDisplayFoldListener.aidl +"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" -I. -p "$ANDROID_AIDL" \ + android/view/IDisplayWindowListener.aidl # Fake sources to expose hidden Android types to the project FAKE_SRC=( \ From 9f39a5f2d6b89b2f57bf003c81163affc3bf0c60 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 17 Nov 2024 14:42:02 +0100 Subject: [PATCH 2091/2244] Determine debugger command at runtime When server_debugger is enabled, retrieve the device SDK version to execute the correct command. PR #5466 --- app/meson.build | 3 --- app/src/adb/adb.c | 18 ++++++++++++++++++ app/src/adb/adb.h | 6 ++++++ app/src/server.c | 28 ++++++++++++++++++---------- doc/develop.md | 9 --------- meson_options.txt | 1 - 6 files changed, 42 insertions(+), 23 deletions(-) diff --git a/app/meson.build b/app/meson.build index 9d179101..444cf98e 100644 --- a/app/meson.build +++ b/app/meson.build @@ -167,9 +167,6 @@ conf.set('DEFAULT_LOCAL_PORT_RANGE_LAST', '27199') # 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') - # enable V4L2 support (linux only) conf.set('HAVE_V4L2', v4l2_support) diff --git a/app/src/adb/adb.c b/app/src/adb/adb.c index 15c9c85a..b3e90b2f 100644 --- a/app/src/adb/adb.c +++ b/app/src/adb/adb.c @@ -739,3 +739,21 @@ sc_adb_get_device_ip(struct sc_intr *intr, const char *serial, unsigned flags) { return sc_adb_parse_device_ip(buf); } + +uint16_t +sc_adb_get_device_sdk_version(struct sc_intr *intr, const char *serial) { + char *sdk_version = + sc_adb_getprop(intr, serial, "ro.build.version.sdk", SC_ADB_SILENT); + if (!sdk_version) { + return 0; + } + + long value; + bool ok = sc_str_parse_integer(sdk_version, &value); + free(sdk_version); + if (!ok || value < 0 || value > 0xFFFF) { + return 0; + } + + return value; +} diff --git a/app/src/adb/adb.h b/app/src/adb/adb.h index ffd532ea..0292dea1 100644 --- a/app/src/adb/adb.h +++ b/app/src/adb/adb.h @@ -114,4 +114,10 @@ sc_adb_getprop(struct sc_intr *intr, const char *serial, const char *prop, char * sc_adb_get_device_ip(struct sc_intr *intr, const char *serial, unsigned flags); +/** + * Return the device SDK version. + */ +uint16_t +sc_adb_get_device_sdk_version(struct sc_intr *intr, const char *serial); + #endif diff --git a/app/src/server.c b/app/src/server.c index ce7b1aaf..9101aee9 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -201,18 +201,26 @@ execute_server(struct sc_server *server, cmd[count++] = "app_process"; #ifdef SERVER_DEBUGGER + uint16_t sdk_version = sc_adb_get_device_sdk_version(&server->intr, serial); + if (!sdk_version) { + LOGE("Could not determine SDK version"); + return 0; + } + # define SERVER_DEBUGGER_PORT "5005" - cmd[count++] = -# 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; + const char *dbg; + if (sdk_version < 28) { + // Android < 9 + dbg = "-agentlib:jdwp=transport=dt_socket,suspend=y,server=y,address=" + SERVER_DEBUGGER_PORT; + } else { + // Android >= 9 + dbg = "-XjdwpProvider:internal -XjdwpOptions:transport=dt_socket," + "suspend=y,server=y,address=" SERVER_DEBUGGER_PORT; + } + cmd[count++] = dbg; #endif + cmd[count++] = "/"; // unused cmd[count++] = "com.genymobile.scrcpy.Server"; cmd[count++] = SCRCPY_VERSION; diff --git a/doc/develop.md b/doc/develop.md index a094aa32..fb75f471 100644 --- a/doc/develop.md +++ b/doc/develop.md @@ -461,15 +461,6 @@ meson setup 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 setup 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/meson_options.txt b/meson_options.txt index d1030694..76075b3a 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -3,6 +3,5 @@ option('compile_server', type: 'boolean', value: true, description: 'Build the s option('prebuilt_server', type: 'string', description: 'Path of the prebuilt server') option('portable', type: 'boolean', value: false, description: 'Use scrcpy-server from the same directory as the scrcpy executable') 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")') option('v4l2', type: 'boolean', value: true, description: 'Enable V4L2 feature when supported') option('usb', type: 'boolean', value: true, description: 'Enable HID/OTG features when supported') From dc82425769ab0fce545a5c76658fc7ba774468ad Mon Sep 17 00:00:00 2001 From: Enno Boland Date: Sun, 10 Nov 2024 19:17:45 +0100 Subject: [PATCH 2092/2244] Add debugging method for Android >= 11 Fixes #5346 PR #5466 Signed-off-by: Romain Vimont --- app/src/server.c | 21 +++++++++++++++------ doc/develop.md | 21 +++++++++++++++++---- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/app/src/server.c b/app/src/server.c index 9101aee9..584a3c34 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -213,10 +213,15 @@ execute_server(struct sc_server *server, // Android < 9 dbg = "-agentlib:jdwp=transport=dt_socket,suspend=y,server=y,address=" SERVER_DEBUGGER_PORT; - } else { - // Android >= 9 + } else if (sdk_version < 30) { + // Android >= 9 && Android < 11 dbg = "-XjdwpProvider:internal -XjdwpOptions:transport=dt_socket," "suspend=y,server=y,address=" SERVER_DEBUGGER_PORT; + } else { + // Android >= 11 + // Contrary to the other methods, this does not suspend on start. + // + dbg = "-XjdwpProvider:adbconnection"; } cmd[count++] = dbg; #endif @@ -408,10 +413,14 @@ execute_server(struct sc_server *server, cmd[count++] = NULL; #ifdef SERVER_DEBUGGER - LOGI("Server debugger waiting for a client on device port " - SERVER_DEBUGGER_PORT "..."); - // From the computer, run - // adb forward tcp:5005 tcp:5005 + LOGI("Server debugger listening%s...", + sdk_version < 30 ? " on port " SERVER_DEBUGGER_PORT : ""); + // For Android < 11, from the computer: + // - run `adb forward tcp:5005 tcp:5005` + // For Android >= 11: + // - execute `adb jdwp` to get the jdwp port + // - run `adb forward tcp:5005 jdwp:XXXX` (replace XXXX) + // // Then, from Android Studio: Run > Debug > Edit configurations... // On the left, click on '+', "Remote", with: // Host: localhost diff --git a/doc/develop.md b/doc/develop.md index fb75f471..21949ea6 100644 --- a/doc/develop.md +++ b/doc/develop.md @@ -461,17 +461,30 @@ meson setup x -Dserver_debugger=true meson configure x -Dserver_debugger=true ``` -Then recompile. +Then recompile, and run scrcpy. -When you start scrcpy, it will start a debugger on port 5005 on the device. +For Android < 11, it will start a debugger on port 5005 on the device and wait: Redirect that port to the computer: ```bash adb forward tcp:5005 tcp:5005 ``` -In Android Studio, _Run_ > _Debug_ > _Edit configurations..._ On the left, click on -`+`, _Remote_, and fill the form: +For Android >= 11, first find the listening port: + +```bash +adb jdwp +# press Ctrl+C to interrupt +``` + +Then redirect the resulting PID: + +```bash +adb forward tcp:5005 jdwp:XXXX # replace XXXX +``` + +In Android Studio, _Run_ > _Debug_ > _Edit configurations..._ On the left, click +on `+`, _Remote_, and fill the form: - Host: `localhost` - Port: `5005` From 26bf209617d9bf633307eaf9051c03fa0a9f631c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 22 Nov 2024 08:25:15 +0100 Subject: [PATCH 2093/2244] Replace release.mk by release scripts Since commit 2687d202809dfaafe8f40f613aec131ad9501433, the Makefile named release.mk stopped handling dependencies between recipes, because they have to be executed separately (from different Github Actions jobs). Using a Makefile no longer provides any real benefit. Replace it by several individual release scripts for simplicity and readability. Refs #5306 PR #5515 --- .github/workflows/release.yml | 55 +++++++------ release.mk | 141 ---------------------------------- release.sh | 2 - release/.gitignore | 2 + release/build_common | 5 ++ release/build_server.sh | 14 ++++ release/build_windows.sh | 51 ++++++++++++ release/generate_checksums.sh | 11 +++ release/package_client.sh | 32 ++++++++ release/package_server.sh | 10 +++ release/release.sh | 22 ++++++ release/test_client.sh | 12 +++ release/test_server.sh | 9 +++ 13 files changed, 198 insertions(+), 168 deletions(-) delete mode 100644 release.mk delete mode 100755 release.sh create mode 100644 release/.gitignore create mode 100644 release/build_common create mode 100755 release/build_server.sh create mode 100755 release/build_windows.sh create mode 100755 release/generate_checksums.sh create mode 100755 release/package_client.sh create mode 100755 release/package_server.sh create mode 100755 release/release.sh create mode 100755 release/test_client.sh create mode 100755 release/test_server.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e67c1c21..30984ae3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,11 +6,15 @@ on: name: description: 'Version name (default is ref name)' +env: + # $VERSION is used by release scripts + VERSION: ${{ github.event.inputs.name || github.ref_name }} + jobs: build-scrcpy-server: runs-on: ubuntu-latest env: - GRADLE: gradle # use native gradle instead of ./gradlew in release.mk + GRADLE: gradle # use native gradle instead of ./gradlew in scripts steps: - name: Checkout code uses: actions/checkout@v4 @@ -22,16 +26,16 @@ jobs: java-version: '17' - name: Test scrcpy-server - run: make -f release.mk test-server + run: release/test_server.sh - name: Build scrcpy-server - run: make -f release.mk build-server + run: release/build_server.sh - name: Upload scrcpy-server artifact uses: actions/upload-artifact@v4 with: name: scrcpy-server - path: build-server/server/scrcpy-server + path: release/work/build-server/server/scrcpy-server test-client: runs-on: ubuntu-latest @@ -46,13 +50,8 @@ jobs: libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \ libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev - - name: Build - run: | - meson setup d -Db_sanitize=address,undefined - - name: Test - run: | - meson test -Cd + run: release/test_client.sh build-win32: runs-on: ubuntu-latest @@ -71,14 +70,14 @@ jobs: - name: Workaround for old meson version run by Github Actions run: sed -i 's/^pkg-config/pkgconfig/' cross_win32.txt - - name: Build scrcpy win32 - run: make -f release.mk build-win32 + - name: Build win32 + run: release/build_windows.sh 32 - name: Upload build-win32 artifact uses: actions/upload-artifact@v4 with: name: build-win32-intermediate - path: build-win32/dist/ + path: release/work/build-win32/dist/ build-win64: runs-on: ubuntu-latest @@ -97,14 +96,14 @@ jobs: - name: Workaround for old meson version run by Github Actions run: sed -i 's/^pkg-config/pkgconfig/' cross_win64.txt - - name: Build scrcpy win64 - run: make -f release.mk build-win64 + - name: Build win64 + run: release/build_windows.sh 64 - name: Upload build-win64 artifact uses: actions/upload-artifact@v4 with: name: build-win64-intermediate - path: build-win64/dist/ + path: release/work/build-win64/dist/ package: needs: @@ -112,9 +111,6 @@ jobs: - build-win32 - build-win64 runs-on: ubuntu-latest - env: - # $VERSION is used by release.mk - VERSION: ${{ github.event.inputs.name || github.ref_name }} steps: - name: Checkout code uses: actions/checkout@v4 @@ -123,25 +119,34 @@ jobs: uses: actions/download-artifact@v4 with: name: scrcpy-server - path: build-server/server/ + path: release/work/build-server/server/ - name: Download build-win32 uses: actions/download-artifact@v4 with: name: build-win32-intermediate - path: build-win32/dist/ + path: release/work/build-win32/dist/ - name: Download build-win64 uses: actions/download-artifact@v4 with: name: build-win64-intermediate - path: build-win64/dist/ + path: release/work/build-win64/dist/ - - name: Package - run: make -f release.mk package + - name: Package server + run: release/package_server.sh + + - name: Package win32 + run: release/package_client.sh win32 + + - name: Package win64 + run: release/package_client.sh win64 + + - name: Generate checksums + run: release/generate_checksums.sh - name: Upload release artifact uses: actions/upload-artifact@v4 with: name: scrcpy-release-${{ env.VERSION }} - path: release-${{ env.VERSION }} + path: release/output diff --git a/release.mk b/release.mk deleted file mode 100644 index 61145002..00000000 --- a/release.mk +++ /dev/null @@ -1,141 +0,0 @@ -# This makefile provides recipes to build a "portable" version of scrcpy for -# Windows. -# -# Here, "portable" means that the client and server binaries are expected to be -# anywhere, but in the same directory, instead of well-defined separate -# locations (e.g. /usr/bin/scrcpy and /usr/share/scrcpy/scrcpy-server). -# -# In particular, this implies to change the location from where the client push -# the server to the device. - -.PHONY: default clean \ - test test-client test-server \ - build-server \ - prepare-deps-win32 prepare-deps-win64 \ - build-win32 build-win64 \ - zip-win32 zip-win64 \ - package release - -GRADLE ?= ./gradlew - -TEST_BUILD_DIR := build-test -SERVER_BUILD_DIR := build-server -WIN32_BUILD_DIR := build-win32 -WIN64_BUILD_DIR := build-win64 - -VERSION ?= $(shell git describe --tags --exclude='*install-release' --always) - -ZIP := zip -WIN32_TARGET_DIR := scrcpy-win32-$(VERSION) -WIN64_TARGET_DIR := scrcpy-win64-$(VERSION) -WIN32_TARGET := $(WIN32_TARGET_DIR).zip -WIN64_TARGET := $(WIN64_TARGET_DIR).zip - -RELEASE_DIR := release-$(VERSION) - -release: clean test build-server build-win32 build-win64 package - -clean: - $(GRADLE) clean - rm -rf "$(ZIP)" "$(TEST_BUILD_DIR)" "$(SERVER_BUILD_DIR)" \ - "$(WIN32_BUILD_DIR)" "$(WIN64_BUILD_DIR)" - -test-client: - [ -d "$(TEST_BUILD_DIR)" ] || ( mkdir "$(TEST_BUILD_DIR)" && \ - meson setup "$(TEST_BUILD_DIR)" -Db_sanitize=address ) - ninja -C "$(TEST_BUILD_DIR)" - -test-server: - $(GRADLE) -p server check - -test: test-client test-server - -build-server: - $(GRADLE) -p server assembleRelease - mkdir -p "$(SERVER_BUILD_DIR)/server" - cp server/build/outputs/apk/release/server-release-unsigned.apk \ - "$(SERVER_BUILD_DIR)/server/scrcpy-server" - -prepare-deps-win32: - @app/deps/adb.sh win32 - @app/deps/sdl.sh win32 - @app/deps/ffmpeg.sh win32 - @app/deps/libusb.sh win32 - -prepare-deps-win64: - @app/deps/adb.sh win64 - @app/deps/sdl.sh win64 - @app/deps/ffmpeg.sh win64 - @app/deps/libusb.sh win64 - -build-win32: prepare-deps-win32 - rm -rf "$(WIN32_BUILD_DIR)" - mkdir -p "$(WIN32_BUILD_DIR)/local" - meson setup "$(WIN32_BUILD_DIR)" \ - --pkg-config-path="app/deps/work/install/win32/lib/pkgconfig" \ - -Dc_args="-I$(PWD)/app/deps/work/install/win32/include" \ - -Dc_link_args="-L$(PWD)/app/deps/work/install/win32/lib" \ - --cross-file=cross_win32.txt \ - --buildtype=release --strip -Db_lto=true \ - -Dcompile_server=false \ - -Dportable=true - ninja -C "$(WIN32_BUILD_DIR)" - # Group intermediate outputs into a 'dist' directory - mkdir -p "$(WIN32_BUILD_DIR)/dist" - cp "$(WIN32_BUILD_DIR)"/app/scrcpy.exe "$(WIN32_BUILD_DIR)/dist/" - cp app/data/scrcpy-console.bat "$(WIN32_BUILD_DIR)/dist/" - cp app/data/scrcpy-noconsole.vbs "$(WIN32_BUILD_DIR)/dist/" - cp app/data/icon.png "$(WIN32_BUILD_DIR)/dist/" - cp app/data/open_a_terminal_here.bat "$(WIN32_BUILD_DIR)/dist/" - cp app/deps/work/install/win32/bin/*.dll "$(WIN32_BUILD_DIR)/dist/" - cp app/deps/work/install/win32/bin/adb.exe "$(WIN32_BUILD_DIR)/dist/" - -build-win64: prepare-deps-win64 - rm -rf "$(WIN64_BUILD_DIR)" - mkdir -p "$(WIN64_BUILD_DIR)/local" - meson setup "$(WIN64_BUILD_DIR)" \ - --pkg-config-path="app/deps/work/install/win64/lib/pkgconfig" \ - -Dc_args="-I$(PWD)/app/deps/work/install/win64/include" \ - -Dc_link_args="-L$(PWD)/app/deps/work/install/win64/lib" \ - --cross-file=cross_win64.txt \ - --buildtype=release --strip -Db_lto=true \ - -Dcompile_server=false \ - -Dportable=true - ninja -C "$(WIN64_BUILD_DIR)" - # Group intermediate outputs into a 'dist' directory - mkdir -p "$(WIN64_BUILD_DIR)/dist" - cp "$(WIN64_BUILD_DIR)"/app/scrcpy.exe "$(WIN64_BUILD_DIR)/dist/" - cp app/data/scrcpy-console.bat "$(WIN64_BUILD_DIR)/dist/" - cp app/data/scrcpy-noconsole.vbs "$(WIN64_BUILD_DIR)/dist/" - cp app/data/icon.png "$(WIN64_BUILD_DIR)/dist/" - cp app/data/open_a_terminal_here.bat "$(WIN64_BUILD_DIR)/dist/" - cp app/deps/work/install/win64/bin/*.dll "$(WIN64_BUILD_DIR)/dist/" - cp app/deps/work/install/win64/bin/adb.exe "$(WIN64_BUILD_DIR)/dist/" - -zip-win32: - mkdir -p "$(ZIP)/$(WIN32_TARGET_DIR)" - cp -r "$(WIN32_BUILD_DIR)/dist/." "$(ZIP)/$(WIN32_TARGET_DIR)/" - cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(ZIP)/$(WIN32_TARGET_DIR)/" - cd "$(ZIP)"; \ - zip -r "$(WIN32_TARGET)" "$(WIN32_TARGET_DIR)" - rm -rf "$(ZIP)/$(WIN32_TARGET_DIR)" - -zip-win64: - mkdir -p "$(ZIP)/$(WIN64_TARGET_DIR)" - cp -r "$(WIN64_BUILD_DIR)/dist/." "$(ZIP)/$(WIN64_TARGET_DIR)/" - cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(ZIP)/$(WIN64_TARGET_DIR)/" - cd "$(ZIP)"; \ - zip -r "$(WIN64_TARGET)" "$(WIN64_TARGET_DIR)" - rm -rf "$(ZIP)/$(WIN64_TARGET_DIR)" - -package: zip-win32 zip-win64 - mkdir -p "$(RELEASE_DIR)" - cp "$(SERVER_BUILD_DIR)/server/scrcpy-server" \ - "$(RELEASE_DIR)/scrcpy-server-$(VERSION)" - cp "$(ZIP)/$(WIN32_TARGET)" "$(RELEASE_DIR)" - cp "$(ZIP)/$(WIN64_TARGET)" "$(RELEASE_DIR)" - cd "$(RELEASE_DIR)" && \ - sha256sum "scrcpy-server-$(VERSION)" \ - "scrcpy-win32-$(VERSION).zip" \ - "scrcpy-win64-$(VERSION).zip" > SHA256SUMS.txt - @echo "Release generated in $(RELEASE_DIR)/" diff --git a/release.sh b/release.sh deleted file mode 100755 index 51ce2e38..00000000 --- a/release.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -make -f release.mk diff --git a/release/.gitignore b/release/.gitignore new file mode 100644 index 00000000..ed363cdf --- /dev/null +++ b/release/.gitignore @@ -0,0 +1,2 @@ +/work +/output diff --git a/release/build_common b/release/build_common new file mode 100644 index 00000000..199a80b6 --- /dev/null +++ b/release/build_common @@ -0,0 +1,5 @@ +# This file must be sourced from the release scripts directory +WORK_DIR="$PWD/work" +OUTPUT_DIR="$PWD/output" + +VERSION="${VERSION:-$(git describe --tags --always)}" diff --git a/release/build_server.sh b/release/build_server.sh new file mode 100755 index 00000000..f52672de --- /dev/null +++ b/release/build_server.sh @@ -0,0 +1,14 @@ +#!/bin/bash +set -ex +cd "$(dirname ${BASH_SOURCE[0]})" +. build_common +cd .. # root project dir + +GRADLE="${GRADLE:-./gradlew}" +SERVER_BUILD_DIR="$WORK_DIR/build-server" + +rm -rf "$SERVER_BUILD_DIR" +"$GRADLE" -p server assembleRelease +mkdir -p "$SERVER_BUILD_DIR/server" +cp server/build/outputs/apk/release/server-release-unsigned.apk \ + "$SERVER_BUILD_DIR/server/scrcpy-server" diff --git a/release/build_windows.sh b/release/build_windows.sh new file mode 100755 index 00000000..74bd32fc --- /dev/null +++ b/release/build_windows.sh @@ -0,0 +1,51 @@ +#!/bin/bash +set -ex + +case "$1" in + 32) + WINXX=win32 + ;; + 64) + WINXX=win64 + ;; + *) + echo "ERROR: $0 must be called with one argument: 32 or 64" >&2 + exit 1 + ;; +esac + +cd "$(dirname ${BASH_SOURCE[0]})" +. build_common +cd .. # root project dir + +WINXX_BUILD_DIR="$WORK_DIR/build-$WINXX" + +app/deps/adb.sh $WINXX +app/deps/sdl.sh $WINXX +app/deps/ffmpeg.sh $WINXX +app/deps/libusb.sh $WINXX + +DEPS_INSTALL_DIR="$PWD/app/deps/work/install/$WINXX" + +rm -rf "$WINXX_BUILD_DIR" +meson setup "$WINXX_BUILD_DIR" \ + --pkg-config-path="$DEPS_INSTALL_DIR/lib/pkgconfig" \ + -Dc_args="-I$DEPS_INSTALL_DIR/include" \ + -Dc_link_args="-L$DEPS_INSTALL_DIR/lib" \ + --cross-file=cross_$WINXX.txt \ + --buildtype=release \ + --strip \ + -Db_lto=true \ + -Dcompile_server=false \ + -Dportable=true +ninja -C "$WINXX_BUILD_DIR" + +# Group intermediate outputs into a 'dist' directory +mkdir -p "$WINXX_BUILD_DIR/dist" +cp "$WINXX_BUILD_DIR"/app/scrcpy.exe "$WINXX_BUILD_DIR/dist/" +cp app/data/scrcpy-console.bat "$WINXX_BUILD_DIR/dist/" +cp app/data/scrcpy-noconsole.vbs "$WINXX_BUILD_DIR/dist/" +cp app/data/icon.png "$WINXX_BUILD_DIR/dist/" +cp app/data/open_a_terminal_here.bat "$WINXX_BUILD_DIR/dist/" +cp "$DEPS_INSTALL_DIR"/bin/*.dll "$WINXX_BUILD_DIR/dist/" +cp "$DEPS_INSTALL_DIR"/bin/adb.exe "$WINXX_BUILD_DIR/dist/" diff --git a/release/generate_checksums.sh b/release/generate_checksums.sh new file mode 100755 index 00000000..a57f1523 --- /dev/null +++ b/release/generate_checksums.sh @@ -0,0 +1,11 @@ +#!/bin/bash +set -ex +cd "$(dirname ${BASH_SOURCE[0]})" +. build_common + +cd "$OUTPUT_DIR" +sha256sum "scrcpy-server-$VERSION" \ + "scrcpy-win32-$VERSION.zip" \ + "scrcpy-win64-$VERSION.zip" \ + | tee SHA256SUMS.txt +echo "Release checksums generated in $PWD/SHA256SUMS.txt" diff --git a/release/package_client.sh b/release/package_client.sh new file mode 100755 index 00000000..f69b2332 --- /dev/null +++ b/release/package_client.sh @@ -0,0 +1,32 @@ +#!/bin/bash +set -ex +cd "$(dirname ${BASH_SOURCE[0]})" +. build_common +cd .. # root project dir + +if [[ $# != 1 ]] +then + # : for example win64 + echo "Syntax: $0 " >&2 + exit 1 + +fi + +BUILD_DIR="$WORK_DIR/build-$1" +ARCHIVE_DIR="$BUILD_DIR/release-archive" +TARGET="scrcpy-$1-$VERSION" + +rm -rf "$ARCHIVE_DIR/$TARGET" +mkdir -p "$ARCHIVE_DIR/$TARGET" + +cp -r "$BUILD_DIR/dist/." "$ARCHIVE_DIR/$TARGET/" +cp "$WORK_DIR/build-server/server/scrcpy-server" "$ARCHIVE_DIR/$TARGET/" + +mkdir -p "$OUTPUT_DIR" + +cd "$ARCHIVE_DIR" +rm -f "$OUTPUT_DIR/$TARGET.zip" +zip -r "$OUTPUT_DIR/$TARGET.zip" "$TARGET" +rm -rf "$TARGET" +cd - +echo "Generated '$OUTPUT_DIR/$TARGET.zip'" diff --git a/release/package_server.sh b/release/package_server.sh new file mode 100755 index 00000000..a856cebb --- /dev/null +++ b/release/package_server.sh @@ -0,0 +1,10 @@ +#!/bin/bash +set -ex +cd "$(dirname ${BASH_SOURCE[0]})" +OUTPUT_DIR="$PWD/output" +. build_common +cd .. # root project dir + +mkdir -p "$OUTPUT_DIR" +cp "$WORK_DIR/build-server/server/scrcpy-server" "$OUTPUT_DIR/scrcpy-server-$VERSION" +echo "Generated '$OUTPUT_DIR/scrcpy-server-$VERSION'" diff --git a/release/release.sh b/release/release.sh new file mode 100755 index 00000000..0760089f --- /dev/null +++ b/release/release.sh @@ -0,0 +1,22 @@ +#!/bin/bash +# To customize the version name: +# VERSION=myversion ./release.sh +set -e + +cd "$(dirname ${BASH_SOURCE[0]})" +rm -rf output + +./test_server.sh +./test_client.sh + +./build_server.sh +./build_windows.sh 32 +./build_windows.sh 64 + +./package_server.sh +./package_client.sh win32 +./package_client.sh win64 + +./generate_checksums.sh + +echo "Release generated in $PWD/output" diff --git a/release/test_client.sh b/release/test_client.sh new file mode 100755 index 00000000..6059541d --- /dev/null +++ b/release/test_client.sh @@ -0,0 +1,12 @@ +#!/bin/bash +set -ex +cd "$(dirname ${BASH_SOURCE[0]})" +. build_common +cd .. # root project dir + +TEST_BUILD_DIR="$WORK_DIR/build-test" + +rm -rf "$TEST_BUILD_DIR" +meson setup "$TEST_BUILD_DIR" -Dcompile_server=false \ + -Db_sanitize=address,undefined +ninja -C "$TEST_BUILD_DIR" test diff --git a/release/test_server.sh b/release/test_server.sh new file mode 100755 index 00000000..940e8c1a --- /dev/null +++ b/release/test_server.sh @@ -0,0 +1,9 @@ +#!/bin/bash +set -ex +cd "$(dirname ${BASH_SOURCE[0]})" +. build_common +cd .. # root project dir + +GRADLE="${GRADLE:-./gradlew}" + +"$GRADLE" -p server check From 5df218d8f9419f555000fb21758bad46350064fb Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 22 Nov 2024 08:25:33 +0100 Subject: [PATCH 2094/2244] Test scrcpy-server in a separate CI job Use a separate GitHub Action job to build and test the server. PR #5515 --- .github/workflows/release.yml | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 30984ae3..994c55fa 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,7 +11,7 @@ env: VERSION: ${{ github.event.inputs.name || github.ref_name }} jobs: - build-scrcpy-server: + test-scrcpy-server: runs-on: ubuntu-latest env: GRADLE: gradle # use native gradle instead of ./gradlew in scripts @@ -28,6 +28,20 @@ jobs: - name: Test scrcpy-server run: release/test_server.sh + build-scrcpy-server: + runs-on: ubuntu-latest + env: + GRADLE: gradle # use native gradle instead of ./gradlew in scripts + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup JDK + uses: actions/setup-java@v4 + with: + distribution: 'zulu' + java-version: '17' + - name: Build scrcpy-server run: release/build_server.sh From a57180047c58b37ad135c6bd322b02d7122f6dd4 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 22 Nov 2024 21:43:57 +0100 Subject: [PATCH 2095/2244] Split packaging for each target on CI Create separate jobs for packaging win32 and win64 releases. PR #5515 --- .github/workflows/release.yml | 70 +++++++++++++++++++++++++++++++---- 1 file changed, 62 insertions(+), 8 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 994c55fa..f7ac87cb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -119,11 +119,10 @@ jobs: name: build-win64-intermediate path: release/work/build-win64/dist/ - package: + package-win32: needs: - build-scrcpy-server - build-win32 - - build-win64 runs-on: ubuntu-latest steps: - name: Checkout code @@ -141,21 +140,76 @@ jobs: name: build-win32-intermediate path: release/work/build-win32/dist/ + - name: Package win32 + run: release/package_client.sh win32 + + - name: Upload win32 release + uses: actions/upload-artifact@v4 + with: + name: release-win32 + path: release/output/ + + package-win64: + needs: + - build-scrcpy-server + - build-win64 + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Download scrcpy-server + uses: actions/download-artifact@v4 + with: + name: scrcpy-server + path: release/work/build-server/server/ + - name: Download build-win64 uses: actions/download-artifact@v4 with: name: build-win64-intermediate path: release/work/build-win64/dist/ - - name: Package server - run: release/package_server.sh - - - name: Package win32 - run: release/package_client.sh win32 - - name: Package win64 run: release/package_client.sh win64 + - name: Upload win64 release + uses: actions/upload-artifact@v4 + with: + name: release-win64 + path: release/output + + release: + needs: + - build-scrcpy-server + - package-win32 + - package-win64 + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Download scrcpy-server + uses: actions/download-artifact@v4 + with: + name: scrcpy-server + path: release/work/build-server/server/ + + - name: Download release-win32 + uses: actions/download-artifact@v4 + with: + name: release-win32 + path: release/output/ + + - name: Download release-win64 + uses: actions/download-artifact@v4 + with: + name: release-win64 + path: release/output/ + + - name: Package server + run: release/package_server.sh + - name: Generate checksums run: release/generate_checksums.sh From 7fc694328483319105408e0bb767c6eb2341632b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 22 Nov 2024 21:51:58 +0100 Subject: [PATCH 2096/2244] Preserve file permissions in GitHub Actions The upload-artifact action does not preserve file permissions: Even if it is not critical for Windows releases, it will be for other platforms. Wrap everything in a tarball to keep original permissions. PR #5515 --- .github/workflows/release.yml | 36 +++++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f7ac87cb..703bb777 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -87,11 +87,19 @@ jobs: - name: Build win32 run: release/build_windows.sh 32 + # upload-artifact does not preserve permissions + - name: Tar + run: | + cd release/work/build-win32 + mkdir dist-tar + cd dist-tar + tar -C .. -cvf dist.tar.gz dist/ + - name: Upload build-win32 artifact uses: actions/upload-artifact@v4 with: name: build-win32-intermediate - path: release/work/build-win32/dist/ + path: release/work/build-win32/dist-tar/ build-win64: runs-on: ubuntu-latest @@ -113,11 +121,19 @@ jobs: - name: Build win64 run: release/build_windows.sh 64 + # upload-artifact does not preserve permissions + - name: Tar + run: | + cd release/work/build-win64 + mkdir dist-tar + cd dist-tar + tar -C .. -cvf dist.tar.gz dist/ + - name: Upload build-win64 artifact uses: actions/upload-artifact@v4 with: name: build-win64-intermediate - path: release/work/build-win64/dist/ + path: release/work/build-win64/dist-tar/ package-win32: needs: @@ -138,7 +154,13 @@ jobs: uses: actions/download-artifact@v4 with: name: build-win32-intermediate - path: release/work/build-win32/dist/ + path: release/work/build-win32/dist-tar/ + + # upload-artifact does not preserve permissions + - name: Detar + run: | + cd release/work/build-win32 + tar xf dist-tar/dist.tar.gz - name: Package win32 run: release/package_client.sh win32 @@ -168,7 +190,13 @@ jobs: uses: actions/download-artifact@v4 with: name: build-win64-intermediate - path: release/work/build-win64/dist/ + path: release/work/build-win64/dist-tar/ + + # upload-artifact does not preserve permissions + - name: Detar + run: | + cd release/work/build-win64 + tar xf dist-tar/dist.tar.gz - name: Package win64 run: release/package_client.sh win64 From d74f564f563f17a807106b4d2507a6cd4b6cbc3f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 22 Nov 2024 22:08:18 +0100 Subject: [PATCH 2097/2244] Reorder FFmpeg configure args All --disable, then all --enable. PR #5515 --- app/deps/ffmpeg.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/deps/ffmpeg.sh b/app/deps/ffmpeg.sh index 93612c1b..fa170046 100755 --- a/app/deps/ffmpeg.sh +++ b/app/deps/ffmpeg.sh @@ -65,6 +65,7 @@ else --disable-avdevice \ --disable-network \ --disable-everything \ + --disable-vulkan \ --enable-swresample \ --enable-decoder=h264 \ --enable-decoder=hevc \ @@ -83,7 +84,6 @@ else --enable-muxer=opus \ --enable-muxer=flac \ --enable-muxer=wav \ - --disable-vulkan fi make -j From 73b595c806db12e78a5f37de2476309b56a0fac9 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 22 Nov 2024 22:09:06 +0100 Subject: [PATCH 2098/2244] Disable VDPAU and VAAPI for FFmpeg build They are not used, and this prevents Linux builds from working if the dependencies are unavailable. PR #5515 --- app/deps/ffmpeg.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/deps/ffmpeg.sh b/app/deps/ffmpeg.sh index fa170046..c676664e 100755 --- a/app/deps/ffmpeg.sh +++ b/app/deps/ffmpeg.sh @@ -66,6 +66,8 @@ else --disable-network \ --disable-everything \ --disable-vulkan \ + --disable-vaapi \ + --disable-vdpau \ --enable-swresample \ --enable-decoder=h264 \ --enable-decoder=hevc \ From cf0098abf0f4198e199f373f7831673f86be44c0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 21 Nov 2024 22:44:12 +0100 Subject: [PATCH 2099/2244] Store dependencies configure args in bash arrays This will make it easy to conditionally add items. PR #5515 --- app/deps/ffmpeg.sh | 81 ++++++++++++++++++++++++---------------------- app/deps/libusb.sh | 13 +++++--- app/deps/sdl.sh | 11 ++++--- 3 files changed, 57 insertions(+), 48 deletions(-) diff --git a/app/deps/ffmpeg.sh b/app/deps/ffmpeg.sh index c676664e..94fb06d2 100755 --- a/app/deps/ffmpeg.sh +++ b/app/deps/ffmpeg.sh @@ -47,45 +47,48 @@ else mkdir "$HOST" cd "$HOST" - "$SOURCES_DIR/$PROJECT_DIR"/configure \ - --prefix="$INSTALL_DIR/$HOST" \ - --enable-cross-compile \ - --target-os=mingw32 \ - --arch="$ARCH" \ - --cross-prefix="${HOST_TRIPLET}-" \ - --cc="${HOST_TRIPLET}-gcc" \ - --extra-cflags="-O2 -fPIC" \ - --enable-shared \ - --disable-static \ - --disable-programs \ - --disable-doc \ - --disable-swscale \ - --disable-postproc \ - --disable-avfilter \ - --disable-avdevice \ - --disable-network \ - --disable-everything \ - --disable-vulkan \ - --disable-vaapi \ - --disable-vdpau \ - --enable-swresample \ - --enable-decoder=h264 \ - --enable-decoder=hevc \ - --enable-decoder=av1 \ - --enable-decoder=pcm_s16le \ - --enable-decoder=opus \ - --enable-decoder=aac \ - --enable-decoder=flac \ - --enable-decoder=png \ - --enable-protocol=file \ - --enable-demuxer=image2 \ - --enable-parser=png \ - --enable-zlib \ - --enable-muxer=matroska \ - --enable-muxer=mp4 \ - --enable-muxer=opus \ - --enable-muxer=flac \ - --enable-muxer=wav \ + conf=( + --prefix="$INSTALL_DIR/$HOST" + --enable-cross-compile + --target-os=mingw32 + --arch="$ARCH" + --cross-prefix="${HOST_TRIPLET}-" + --cc="${HOST_TRIPLET}-gcc" + --extra-cflags="-O2 -fPIC" + --enable-shared + --disable-static + --disable-programs + --disable-doc + --disable-swscale + --disable-postproc + --disable-avfilter + --disable-avdevice + --disable-network + --disable-everything + --disable-vulkan + --disable-vaapi + --disable-vdpau + --enable-swresample + --enable-decoder=h264 + --enable-decoder=hevc + --enable-decoder=av1 + --enable-decoder=pcm_s16le + --enable-decoder=opus + --enable-decoder=aac + --enable-decoder=flac + --enable-decoder=png + --enable-protocol=file + --enable-demuxer=image2 + --enable-parser=png + --enable-zlib + --enable-muxer=matroska + --enable-muxer=mp4 + --enable-muxer=opus + --enable-muxer=flac + --enable-muxer=wav + ) + + "$SOURCES_DIR/$PROJECT_DIR"/configure "${conf[@]}" fi make -j diff --git a/app/deps/libusb.sh b/app/deps/libusb.sh index 26f0140b..77a904b2 100755 --- a/app/deps/libusb.sh +++ b/app/deps/libusb.sh @@ -33,12 +33,15 @@ else mkdir "$HOST" cd "$HOST" - "$SOURCES_DIR/$PROJECT_DIR"/bootstrap.sh - "$SOURCES_DIR/$PROJECT_DIR"/configure \ - --prefix="$INSTALL_DIR/$HOST" \ - --host="$HOST_TRIPLET" \ - --enable-shared \ + conf=( + --prefix="$INSTALL_DIR/$HOST" + --host="$HOST_TRIPLET" + --enable-shared --disable-static + ) + + "$SOURCES_DIR/$PROJECT_DIR"/bootstrap.sh + "$SOURCES_DIR/$PROJECT_DIR"/configure "${conf[@]}" fi make -j diff --git a/app/deps/sdl.sh b/app/deps/sdl.sh index 55866ccd..1bdd9a4b 100755 --- a/app/deps/sdl.sh +++ b/app/deps/sdl.sh @@ -33,11 +33,14 @@ else mkdir "$HOST" cd "$HOST" - "$SOURCES_DIR/$PROJECT_DIR"/configure \ - --prefix="$INSTALL_DIR/$HOST" \ - --host="$HOST_TRIPLET" \ - --enable-shared \ + conf=( + --prefix="$INSTALL_DIR/$HOST" + --host="$HOST_TRIPLET" + --enable-shared --disable-static + ) + + "$SOURCES_DIR/$PROJECT_DIR"/configure "${conf[@]}" fi make -j From 6a81fc438b6457eaab6efe61c41c194791b9439f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 22 Nov 2024 22:14:05 +0100 Subject: [PATCH 2100/2244] Extract args processing in deps scripts Extract the code that processes arguments into a function. This will make it optional, so the script that only downloads the official ADB binaries will not use arguments. PR #5515 --- app/deps/adb.sh | 1 + app/deps/common | 36 +++++++++++++++++++----------------- app/deps/ffmpeg.sh | 1 + app/deps/libusb.sh | 1 + app/deps/sdl.sh | 1 + 5 files changed, 23 insertions(+), 17 deletions(-) diff --git a/app/deps/adb.sh b/app/deps/adb.sh index b07f29b3..25727535 100755 --- a/app/deps/adb.sh +++ b/app/deps/adb.sh @@ -3,6 +3,7 @@ set -ex DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) cd "$DEPS_DIR" . common +process_args "$@" VERSION=35.0.2 FILENAME=platform-tools_r$VERSION-win.zip diff --git a/app/deps/common b/app/deps/common index c1cc7729..6f8f80dc 100644 --- a/app/deps/common +++ b/app/deps/common @@ -1,25 +1,27 @@ #!/usr/bin/env bash # This file is intended to be sourced by other scripts, not executed -if [[ $# != 1 ]] -then - # : win32 or win64 - echo "Syntax: $0 " >&2 - exit 1 -fi +process_args() { + if [[ $# != 1 ]] + then + # : win32 or win64 + echo "Syntax: $0 " >&2 + exit 1 + fi -HOST="$1" + HOST="$1" -if [[ "$HOST" = win32 ]] -then - HOST_TRIPLET=i686-w64-mingw32 -elif [[ "$HOST" = win64 ]] -then - HOST_TRIPLET=x86_64-w64-mingw32 -else - echo "Unsupported host: $HOST" >&2 - exit 1 -fi + if [[ "$HOST" = win32 ]] + then + HOST_TRIPLET=i686-w64-mingw32 + elif [[ "$HOST" = win64 ]] + then + HOST_TRIPLET=x86_64-w64-mingw32 + else + echo "Unsupported host: $HOST" >&2 + exit 1 + fi +} DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) cd "$DEPS_DIR" diff --git a/app/deps/ffmpeg.sh b/app/deps/ffmpeg.sh index 94fb06d2..20e59375 100755 --- a/app/deps/ffmpeg.sh +++ b/app/deps/ffmpeg.sh @@ -3,6 +3,7 @@ set -ex DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) cd "$DEPS_DIR" . common +process_args "$@" VERSION=7.1 FILENAME=ffmpeg-$VERSION.tar.xz diff --git a/app/deps/libusb.sh b/app/deps/libusb.sh index 77a904b2..ee36d141 100755 --- a/app/deps/libusb.sh +++ b/app/deps/libusb.sh @@ -3,6 +3,7 @@ set -ex DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) cd "$DEPS_DIR" . common +process_args "$@" VERSION=1.0.27 FILENAME=libusb-$VERSION.tar.gz diff --git a/app/deps/sdl.sh b/app/deps/sdl.sh index 1bdd9a4b..d8d0d734 100755 --- a/app/deps/sdl.sh +++ b/app/deps/sdl.sh @@ -3,6 +3,7 @@ set -ex DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) cd "$DEPS_DIR" . common +process_args "$@" VERSION=2.30.9 FILENAME=SDL-$VERSION.tar.gz From 98d2065d6d06d44b3c7af3d199e29cabc6f58b80 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 22 Nov 2024 22:17:41 +0100 Subject: [PATCH 2101/2244] Make the ADB dependency script Windows-specific This will allow adding similar scripts for other platforms. PR #5515 --- app/deps/{adb.sh => adb_windows.sh} | 9 ++++----- release/build_windows.sh | 5 +++-- 2 files changed, 7 insertions(+), 7 deletions(-) rename app/deps/{adb.sh => adb_windows.sh} (79%) diff --git a/app/deps/adb.sh b/app/deps/adb_windows.sh similarity index 79% rename from app/deps/adb.sh rename to app/deps/adb_windows.sh index 25727535..d36706b0 100755 --- a/app/deps/adb.sh +++ b/app/deps/adb_windows.sh @@ -3,11 +3,10 @@ set -ex DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) cd "$DEPS_DIR" . common -process_args "$@" VERSION=35.0.2 FILENAME=platform-tools_r$VERSION-win.zip -PROJECT_DIR=platform-tools-$VERSION +PROJECT_DIR=platform-tools-$VERSION-windows SHA256SUM=2975a3eac0b19182748d64195375ad056986561d994fffbdc64332a516300bb9 cd "$SOURCES_DIR" @@ -28,6 +27,6 @@ else rmdir "$ZIP_PREFIX" fi -mkdir -p "$INSTALL_DIR/$HOST/bin" -cd "$INSTALL_DIR/$HOST/bin" -cp -r "$SOURCES_DIR/$PROJECT_DIR"/. "$INSTALL_DIR/$HOST/bin/" +mkdir -p "$INSTALL_DIR/adb-windows" +cd "$INSTALL_DIR/adb-windows" +cp -r "$SOURCES_DIR/$PROJECT_DIR"/. "$INSTALL_DIR/adb-windows/" diff --git a/release/build_windows.sh b/release/build_windows.sh index 74bd32fc..1b738ea3 100755 --- a/release/build_windows.sh +++ b/release/build_windows.sh @@ -20,12 +20,13 @@ cd .. # root project dir WINXX_BUILD_DIR="$WORK_DIR/build-$WINXX" -app/deps/adb.sh $WINXX +app/deps/adb_windows.sh app/deps/sdl.sh $WINXX app/deps/ffmpeg.sh $WINXX app/deps/libusb.sh $WINXX DEPS_INSTALL_DIR="$PWD/app/deps/work/install/$WINXX" +ADB_INSTALL_DIR="$PWD/app/deps/work/install/adb-windows" rm -rf "$WINXX_BUILD_DIR" meson setup "$WINXX_BUILD_DIR" \ @@ -48,4 +49,4 @@ cp app/data/scrcpy-noconsole.vbs "$WINXX_BUILD_DIR/dist/" cp app/data/icon.png "$WINXX_BUILD_DIR/dist/" cp app/data/open_a_terminal_here.bat "$WINXX_BUILD_DIR/dist/" cp "$DEPS_INSTALL_DIR"/bin/*.dll "$WINXX_BUILD_DIR/dist/" -cp "$DEPS_INSTALL_DIR"/bin/adb.exe "$WINXX_BUILD_DIR/dist/" +cp -r "$ADB_INSTALL_DIR"/. "$WINXX_BUILD_DIR/dist/" From 360936248c0fb59bdc211e298d1aaea49af5ee07 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 21 Nov 2024 23:16:18 +0100 Subject: [PATCH 2102/2244] Add support for build and link types for deps Make dependencies build scripts more flexible, to accept a build type (native or cross) and a link type (static or shared). This lays the groundwork for building binaries for Linux and macOS. PR #5515 --- app/deps/common | 38 +++++++++++++----- app/deps/ffmpeg.sh | 85 ++++++++++++++++++++++++++-------------- app/deps/libusb.sh | 35 ++++++++++++----- app/deps/sdl.sh | 40 ++++++++++++++----- release/build_windows.sh | 8 ++-- 5 files changed, 145 insertions(+), 61 deletions(-) diff --git a/app/deps/common b/app/deps/common index 6f8f80dc..49587e17 100644 --- a/app/deps/common +++ b/app/deps/common @@ -2,25 +2,45 @@ # This file is intended to be sourced by other scripts, not executed process_args() { - if [[ $# != 1 ]] + if [[ $# != 3 ]] then # : win32 or win64 - echo "Syntax: $0 " >&2 + # : native or cross + # : static or shared + echo "Syntax: $0 " >&2 exit 1 fi HOST="$1" + BUILD_TYPE="$2" # native or cross + LINK_TYPE="$3" # static or shared + DIRNAME="$HOST-$BUILD_TYPE-$LINK_TYPE" - if [[ "$HOST" = win32 ]] + if [[ "$BUILD_TYPE" != native && "$BUILD_TYPE" != cross ]] then - HOST_TRIPLET=i686-w64-mingw32 - elif [[ "$HOST" = win64 ]] - then - HOST_TRIPLET=x86_64-w64-mingw32 - else - echo "Unsupported host: $HOST" >&2 + echo "Unsupported build type (expected native or cross): $BUILD_TYPE" >&2 exit 1 fi + + if [[ "$LINK_TYPE" != static && "$LINK_TYPE" != shared ]] + then + echo "Unsupported link type (expected static or shared): $LINK_TYPE" >&2 + exit 1 + fi + + if [[ "$BUILD_TYPE" == cross ]] + then + if [[ "$HOST" = win32 ]] + then + HOST_TRIPLET=i686-w64-mingw32 + elif [[ "$HOST" = win64 ]] + then + HOST_TRIPLET=x86_64-w64-mingw32 + else + echo "Unsupported cross-build to host: $HOST" >&2 + exit 1 + fi + fi } DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) diff --git a/app/deps/ffmpeg.sh b/app/deps/ffmpeg.sh index 20e59375..2484da23 100755 --- a/app/deps/ffmpeg.sh +++ b/app/deps/ffmpeg.sh @@ -23,41 +23,26 @@ fi mkdir -p "$BUILD_DIR/$PROJECT_DIR" cd "$BUILD_DIR/$PROJECT_DIR" -if [[ "$HOST" = win32 ]] +if [[ -d "$DIRNAME" ]] then - ARCH=x86 -elif [[ "$HOST" = win64 ]] -then - ARCH=x86_64 + echo "'$PWD/$DIRNAME' already exists, not reconfigured" + cd "$DIRNAME" else - echo "Unsupported host: $HOST" >&2 - exit 1 -fi + mkdir "$DIRNAME" + cd "$DIRNAME" -# -static-libgcc to avoid missing libgcc_s_dw2-1.dll -# -static to avoid dynamic dependency to zlib -export CFLAGS='-static-libgcc -static' -export CXXFLAGS="$CFLAGS" -export LDFLAGS='-static-libgcc -static' - -if [[ -d "$HOST" ]] -then - echo "'$PWD/$HOST' already exists, not reconfigured" - cd "$HOST" -else - mkdir "$HOST" - cd "$HOST" + if [[ "$HOST" == win* ]] + then + # -static-libgcc to avoid missing libgcc_s_dw2-1.dll + # -static to avoid dynamic dependency to zlib + export CFLAGS='-static-libgcc -static' + export CXXFLAGS="$CFLAGS" + export LDFLAGS='-static-libgcc -static' + fi conf=( - --prefix="$INSTALL_DIR/$HOST" - --enable-cross-compile - --target-os=mingw32 - --arch="$ARCH" - --cross-prefix="${HOST_TRIPLET}-" - --cc="${HOST_TRIPLET}-gcc" + --prefix="$INSTALL_DIR/$DIRNAME" --extra-cflags="-O2 -fPIC" - --enable-shared - --disable-static --disable-programs --disable-doc --disable-swscale @@ -89,6 +74,48 @@ else --enable-muxer=wav ) + if [[ "$LINK_TYPE" == static ]] + then + conf+=( + --enable-static + --disable-shared + ) + else + conf+=( + --disable-static + --enable-shared + ) + fi + + if [[ "$BUILD_TYPE" == cross ]] + then + conf+=( + --enable-cross-compile + --cross-prefix="${HOST_TRIPLET}-" + --cc="${HOST_TRIPLET}-gcc" + ) + + case "$HOST" in + win32) + conf+=( + --target-os=mingw32 + --arch=x86 + ) + ;; + + win64) + conf+=( + --target-os=mingw32 + --arch=x86_64 + ) + ;; + + *) + echo "Unsupported host: $HOST" >&2 + exit 1 + esac + fi + "$SOURCES_DIR/$PROJECT_DIR"/configure "${conf[@]}" fi diff --git a/app/deps/libusb.sh b/app/deps/libusb.sh index ee36d141..340b0f70 100755 --- a/app/deps/libusb.sh +++ b/app/deps/libusb.sh @@ -26,21 +26,38 @@ cd "$BUILD_DIR/$PROJECT_DIR" export CFLAGS='-O2' export CXXFLAGS="$CFLAGS" -if [[ -d "$HOST" ]] +if [[ -d "$DIRNAME" ]] then - echo "'$PWD/$HOST' already exists, not reconfigured" - cd "$HOST" + echo "'$PWD/$DIRNAME' already exists, not reconfigured" + cd "$DIRNAME" else - mkdir "$HOST" - cd "$HOST" + mkdir "$DIRNAME" + cd "$DIRNAME" conf=( - --prefix="$INSTALL_DIR/$HOST" - --host="$HOST_TRIPLET" - --enable-shared - --disable-static + --prefix="$INSTALL_DIR/$DIRNAME" ) + if [[ "$LINK_TYPE" == static ]] + then + conf+=( + --enable-static + --disable-shared + ) + else + conf+=( + --disable-static + --enable-shared + ) + fi + + if [[ "$BUILD_TYPE" == cross ]] + then + conf+=( + --host="$HOST_TRIPLET" + ) + fi + "$SOURCES_DIR/$PROJECT_DIR"/bootstrap.sh "$SOURCES_DIR/$PROJECT_DIR"/configure "${conf[@]}" fi diff --git a/app/deps/sdl.sh b/app/deps/sdl.sh index d8d0d734..71314118 100755 --- a/app/deps/sdl.sh +++ b/app/deps/sdl.sh @@ -26,21 +26,38 @@ cd "$BUILD_DIR/$PROJECT_DIR" export CFLAGS='-O2' export CXXFLAGS="$CFLAGS" -if [[ -d "$HOST" ]] +if [[ -d "$DIRNAME" ]] then - echo "'$PWD/$HOST' already exists, not reconfigured" - cd "$HOST" + echo "'$PWD/$HDIRNAME' already exists, not reconfigured" + cd "$DIRNAME" else - mkdir "$HOST" - cd "$HOST" + mkdir "$DIRNAME" + cd "$DIRNAME" conf=( - --prefix="$INSTALL_DIR/$HOST" - --host="$HOST_TRIPLET" - --enable-shared - --disable-static + --prefix="$INSTALL_DIR/$DIRNAME" ) + if [[ "$LINK_TYPE" == static ]] + then + conf+=( + --enable-static + --disable-shared + ) + else + conf+=( + --disable-static + --enable-shared + ) + fi + + if [[ "$BUILD_TYPE" == cross ]] + then + conf+=( + --host="$HOST_TRIPLET" + ) + fi + "$SOURCES_DIR/$PROJECT_DIR"/configure "${conf[@]}" fi @@ -48,4 +65,7 @@ make -j # There is no "make install-strip" make install # Strip manually -${HOST_TRIPLET}-strip "$INSTALL_DIR/$HOST/bin/SDL2.dll" +if [[ "$LINK_TYPE" == shared && "$HOST" == win* ]] +then + ${HOST_TRIPLET}-strip "$INSTALL_DIR/$DIRNAME/bin/SDL2.dll" +fi diff --git a/release/build_windows.sh b/release/build_windows.sh index 1b738ea3..dbd6cbf4 100755 --- a/release/build_windows.sh +++ b/release/build_windows.sh @@ -21,11 +21,11 @@ cd .. # root project dir WINXX_BUILD_DIR="$WORK_DIR/build-$WINXX" app/deps/adb_windows.sh -app/deps/sdl.sh $WINXX -app/deps/ffmpeg.sh $WINXX -app/deps/libusb.sh $WINXX +app/deps/sdl.sh $WINXX cross shared +app/deps/ffmpeg.sh $WINXX cross shared +app/deps/libusb.sh $WINXX cross shared -DEPS_INSTALL_DIR="$PWD/app/deps/work/install/$WINXX" +DEPS_INSTALL_DIR="$PWD/app/deps/work/install/$WINXX-cross-shared" ADB_INSTALL_DIR="$PWD/app/deps/work/install/adb-windows" rm -rf "$WINXX_BUILD_DIR" From 179c664e2b78f7b1406dc1eed28031591c20934c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 21 Nov 2024 23:28:33 +0100 Subject: [PATCH 2103/2244] Add static build option Use static dependencies if the option is set. PR #5515 --- app/meson.build | 16 +++++++++------- meson_options.txt | 1 + 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/app/meson.build b/app/meson.build index 444cf98e..f089ffb1 100644 --- a/app/meson.build +++ b/app/meson.build @@ -109,20 +109,22 @@ endif cc = meson.get_compiler('c') +static = get_option('static') + dependencies = [ - dependency('libavformat', version: '>= 57.33'), - dependency('libavcodec', version: '>= 57.37'), - dependency('libavutil'), - dependency('libswresample'), - dependency('sdl2', version: '>= 2.0.5'), + dependency('libavformat', version: '>= 57.33', static: static), + dependency('libavcodec', version: '>= 57.37', static: static), + dependency('libavutil', static: static), + dependency('libswresample', static: static), + dependency('sdl2', version: '>= 2.0.5', static: static), ] if v4l2_support - dependencies += dependency('libavdevice') + dependencies += dependency('libavdevice', static: static) endif if usb_support - dependencies += dependency('libusb-1.0') + dependencies += dependency('libusb-1.0', static: static) endif if host_machine.system() == 'windows' diff --git a/meson_options.txt b/meson_options.txt index 76075b3a..fd347734 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -2,6 +2,7 @@ option('compile_app', type: 'boolean', value: true, description: 'Build the clie option('compile_server', type: 'boolean', value: true, description: 'Build the server') option('prebuilt_server', type: 'string', description: 'Path of the prebuilt server') option('portable', type: 'boolean', value: false, description: 'Use scrcpy-server from the same directory as the scrcpy executable') +option('static', type: 'boolean', value: false, description: 'Use static dependencies') option('server_debugger', type: 'boolean', value: false, description: 'Run a server debugger and wait for a client to be attached') option('v4l2', type: 'boolean', value: true, description: 'Enable V4L2 feature when supported') option('usb', type: 'boolean', value: true, description: 'Enable HID/OTG features when supported') From 93da693e8c15fe2a1065c37636ab9fa945cb1254 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 22 Nov 2024 21:17:00 +0100 Subject: [PATCH 2104/2244] Add support for .tar.gz packaging Make package_client.sh accept an archive format. PR #5515 --- .github/workflows/release.yml | 4 ++-- release/package_client.sh | 30 +++++++++++++++++++++++++----- release/release.sh | 4 ++-- 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 703bb777..54722c9f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -163,7 +163,7 @@ jobs: tar xf dist-tar/dist.tar.gz - name: Package win32 - run: release/package_client.sh win32 + run: release/package_client.sh win32 zip - name: Upload win32 release uses: actions/upload-artifact@v4 @@ -199,7 +199,7 @@ jobs: tar xf dist-tar/dist.tar.gz - name: Package win64 - run: release/package_client.sh win64 + run: release/package_client.sh win64 zip - name: Upload win64 release uses: actions/upload-artifact@v4 diff --git a/release/package_client.sh b/release/package_client.sh index f69b2332..c6d430b2 100755 --- a/release/package_client.sh +++ b/release/package_client.sh @@ -4,12 +4,20 @@ cd "$(dirname ${BASH_SOURCE[0]})" . build_common cd .. # root project dir -if [[ $# != 1 ]] +if [[ $# != 2 ]] then # : for example win64 - echo "Syntax: $0 " >&2 + # : zip or tar.gz + echo "Syntax: $0 " >&2 exit 1 +fi +FORMAT=$2 + +if [[ "$2" != zip && "$2" != tar.gz ]] +then + echo "Invalid format (expected zip or tar.gz): $2" >&2 + exit 1 fi BUILD_DIR="$WORK_DIR/build-$1" @@ -25,8 +33,20 @@ cp "$WORK_DIR/build-server/server/scrcpy-server" "$ARCHIVE_DIR/$TARGET/" mkdir -p "$OUTPUT_DIR" cd "$ARCHIVE_DIR" -rm -f "$OUTPUT_DIR/$TARGET.zip" -zip -r "$OUTPUT_DIR/$TARGET.zip" "$TARGET" +rm -f "$OUTPUT_DIR/$TARGET.$FORMAT" + +case "$FORMAT" in + zip) + zip -r "$OUTPUT_DIR/$TARGET.zip" "$TARGET" + ;; + tar.gz) + tar cvf "$OUTPUT_DIR/$TARGET.tar.gz" "$TARGET" + ;; + *) + echo "Invalid format (expected zip or tar.gz): $FORMAT" >&2 + exit 1 +esac + rm -rf "$TARGET" cd - -echo "Generated '$OUTPUT_DIR/$TARGET.zip'" +echo "Generated '$OUTPUT_DIR/$TARGET.$FORMAT'" diff --git a/release/release.sh b/release/release.sh index 0760089f..e07b51c0 100755 --- a/release/release.sh +++ b/release/release.sh @@ -14,8 +14,8 @@ rm -rf output ./build_windows.sh 64 ./package_server.sh -./package_client.sh win32 -./package_client.sh win64 +./package_client.sh win32 zip +./package_client.sh win64 zip ./generate_checksums.sh From cb19686d7950113039f4f478998df8d9ae7ed985 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 22 Nov 2024 21:41:56 +0100 Subject: [PATCH 2105/2244] Add script to release Linux static binary Provide a prebuilt binary for Linux. Fixes #5327 PR #5515 --- .github/workflows/release.yml | 73 +++++++++++++++++++++++++++++++ app/data/scrcpy_static_wrapper.sh | 6 +++ app/deps/adb_linux.sh | 29 ++++++++++++ app/deps/ffmpeg.sh | 9 +++- app/deps/sdl.sh | 8 ++++ release/build_linux.sh | 35 +++++++++++++++ release/generate_checksums.sh | 1 + release/release.sh | 2 + 8 files changed, 162 insertions(+), 1 deletion(-) create mode 100755 app/data/scrcpy_static_wrapper.sh create mode 100755 app/deps/adb_linux.sh create mode 100755 release/build_linux.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 54722c9f..3bc62a3a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -67,6 +67,36 @@ jobs: - name: Test run: release/test_client.sh + build-linux: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt update + sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \ + libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \ + libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev + + - name: Build linux + run: release/build_linux.sh + + # upload-artifact does not preserve permissions + - name: Tar + run: | + cd release/work/build-linux + mkdir dist-tar + cd dist-tar + tar -C .. -cvf dist.tar.gz dist/ + + - name: Upload build-linux artifact + uses: actions/upload-artifact@v4 + with: + name: build-linux-intermediate + path: release/work/build-linux/dist-tar/ + build-win32: runs-on: ubuntu-latest steps: @@ -135,6 +165,42 @@ jobs: name: build-win64-intermediate path: release/work/build-win64/dist-tar/ + package-linux: + needs: + - build-scrcpy-server + - build-linux + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Download scrcpy-server + uses: actions/download-artifact@v4 + with: + name: scrcpy-server + path: release/work/build-server/server/ + + - name: Download build-linux + uses: actions/download-artifact@v4 + with: + name: build-linux-intermediate + path: release/work/build-linux/dist-tar/ + + # upload-artifact does not preserve permissions + - name: Detar + run: | + cd release/work/build-linux + tar xf dist-tar/dist.tar.gz + + - name: Package linux + run: release/package_client.sh linux tar.gz + + - name: Upload linux release + uses: actions/upload-artifact@v4 + with: + name: release-linux + path: release/output/ + package-win32: needs: - build-scrcpy-server @@ -210,6 +276,7 @@ jobs: release: needs: - build-scrcpy-server + - package-linux - package-win32 - package-win64 runs-on: ubuntu-latest @@ -223,6 +290,12 @@ jobs: name: scrcpy-server path: release/work/build-server/server/ + - name: Download release-linux + uses: actions/download-artifact@v4 + with: + name: release-linux + path: release/output/ + - name: Download release-win32 uses: actions/download-artifact@v4 with: diff --git a/app/data/scrcpy_static_wrapper.sh b/app/data/scrcpy_static_wrapper.sh new file mode 100755 index 00000000..ac1e7a95 --- /dev/null +++ b/app/data/scrcpy_static_wrapper.sh @@ -0,0 +1,6 @@ +#!/bin/bash +cd "$(dirname ${BASH_SOURCE[0]})" +export ADB="${ADB:-./adb}" +export SCRCPY_SERVER_PATH="${SCRCPY_SERVER_PATH:-./scrcpy-server}" +export SCRCPY_ICON_PATH="${SCRCPY_ICON_PATH:-./icon.png}" +./scrcpy_bin "$@" diff --git a/app/deps/adb_linux.sh b/app/deps/adb_linux.sh new file mode 100755 index 00000000..17b5641d --- /dev/null +++ b/app/deps/adb_linux.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +set -ex +DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) +cd "$DEPS_DIR" +. common + +VERSION=35.0.2 +FILENAME=platform-tools_r$VERSION-linux.zip +PROJECT_DIR=platform-tools-$VERSION-linux +SHA256SUM=acfdcccb123a8718c46c46c059b2f621140194e5ec1ac9d81715be3d6ab6cd0a + +cd "$SOURCES_DIR" + +if [[ -d "$PROJECT_DIR" ]] +then + echo "$PWD/$PROJECT_DIR" found +else + get_file "https://dl.google.com/android/repository/$FILENAME" "$FILENAME" "$SHA256SUM" + mkdir -p "$PROJECT_DIR" + cd "$PROJECT_DIR" + ZIP_PREFIX=platform-tools + unzip "../$FILENAME" "$ZIP_PREFIX"/adb + mv "$ZIP_PREFIX"/* . + rmdir "$ZIP_PREFIX" +fi + +mkdir -p "$INSTALL_DIR/adb-linux" +cd "$INSTALL_DIR/adb-linux" +cp -r "$SOURCES_DIR/$PROJECT_DIR"/. "$INSTALL_DIR/adb-linux/" diff --git a/app/deps/ffmpeg.sh b/app/deps/ffmpeg.sh index 2484da23..90ffa855 100755 --- a/app/deps/ffmpeg.sh +++ b/app/deps/ffmpeg.sh @@ -48,7 +48,6 @@ else --disable-swscale --disable-postproc --disable-avfilter - --disable-avdevice --disable-network --disable-everything --disable-vulkan @@ -74,6 +73,14 @@ else --enable-muxer=wav ) + if [[ "$HOST" != linux ]] + then + # libavdevice is only used for V4L2 on Linux + conf+=( + --disable-avdevice + ) + fi + if [[ "$LINK_TYPE" == static ]] then conf+=( diff --git a/app/deps/sdl.sh b/app/deps/sdl.sh index 71314118..8698e120 100755 --- a/app/deps/sdl.sh +++ b/app/deps/sdl.sh @@ -38,6 +38,14 @@ else --prefix="$INSTALL_DIR/$DIRNAME" ) + if [[ "$HOST" == linux ]] + then + conf+=( + --enable-video-wayland + --enable-video-x11 + ) + fi + if [[ "$LINK_TYPE" == static ]] then conf+=( diff --git a/release/build_linux.sh b/release/build_linux.sh new file mode 100755 index 00000000..2f2fb62f --- /dev/null +++ b/release/build_linux.sh @@ -0,0 +1,35 @@ +#!/bin/bash +set -ex +cd "$(dirname ${BASH_SOURCE[0]})" +. build_common +cd .. # root project dir + +LINUX_BUILD_DIR="$WORK_DIR/build-linux" + +app/deps/adb_linux.sh +app/deps/sdl.sh linux native static +app/deps/ffmpeg.sh linux native static +app/deps/libusb.sh linux native static + +DEPS_INSTALL_DIR="$PWD/app/deps/work/install/linux-native-static" +ADB_INSTALL_DIR="$PWD/app/deps/work/install/adb-linux" + +rm -rf "$LINUX_BUILD_DIR" +meson setup "$LINUX_BUILD_DIR" \ + --pkg-config-path="$DEPS_INSTALL_DIR/lib/pkgconfig" \ + -Dc_args="-I$DEPS_INSTALL_DIR/include" \ + -Dc_link_args="-L$DEPS_INSTALL_DIR/lib" \ + --buildtype=release \ + --strip \ + -Db_lto=true \ + -Dcompile_server=false \ + -Dportable=true \ + -Dstatic=true +ninja -C "$LINUX_BUILD_DIR" + +# Group intermediate outputs into a 'dist' directory +mkdir -p "$LINUX_BUILD_DIR/dist" +cp "$LINUX_BUILD_DIR"/app/scrcpy "$LINUX_BUILD_DIR/dist/scrcpy_bin" +cp app/data/icon.png "$LINUX_BUILD_DIR/dist/" +cp app/data/scrcpy_static_wrapper.sh "$LINUX_BUILD_DIR/dist/scrcpy" +cp -r "$ADB_INSTALL_DIR"/. "$LINUX_BUILD_DIR/dist/" diff --git a/release/generate_checksums.sh b/release/generate_checksums.sh index a57f1523..d13120de 100755 --- a/release/generate_checksums.sh +++ b/release/generate_checksums.sh @@ -5,6 +5,7 @@ cd "$(dirname ${BASH_SOURCE[0]})" cd "$OUTPUT_DIR" sha256sum "scrcpy-server-$VERSION" \ + "scrcpy-linux-$VERSION.tar.gz" \ "scrcpy-win32-$VERSION.zip" \ "scrcpy-win64-$VERSION.zip" \ | tee SHA256SUMS.txt diff --git a/release/release.sh b/release/release.sh index e07b51c0..8bef11ab 100755 --- a/release/release.sh +++ b/release/release.sh @@ -12,10 +12,12 @@ rm -rf output ./build_server.sh ./build_windows.sh 32 ./build_windows.sh 64 +./build_linux.sh ./package_server.sh ./package_client.sh win32 zip ./package_client.sh win64 zip +./package_client.sh linux tar.gz ./generate_checksums.sh From 28c372e8387c3473613e6653f58c934f81d8fb2f Mon Sep 17 00:00:00 2001 From: Muvaffak Onus Date: Sat, 23 Nov 2024 18:00:45 +0300 Subject: [PATCH 2106/2244] Use generic command for SHA-256 The command sha256sum does not exist on macOS, but `shasum -a256` works both on Linux and macOS. PR #5515 Co-authored-by: Romain Vimont Signed-off-by: Romain Vimont --- app/deps/common | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/deps/common b/app/deps/common index 49587e17..daaa96c0 100644 --- a/app/deps/common +++ b/app/deps/common @@ -59,7 +59,7 @@ checksum() { local file="$1" local sum="$2" echo "$file: verifying checksum..." - echo "$sum $file" | sha256sum -c + echo "$sum $file" | shasum -a256 -c } get_file() { From a7efb180b9befa576513c31e2db0340d0ee33aeb Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 22 Nov 2024 22:23:27 +0100 Subject: [PATCH 2107/2244] Add script to release macOS static binary Provide a prebuilt binary for macOS. Fixes #1733 Fixes #3235 Fixes #4489 Fixes #5327 PR #5515 Co-authored-by: Muvaffak Onus --- .github/workflows/release.yml | 71 +++++++++++++++++++++++++++++++++++ app/deps/adb_macos.sh | 29 ++++++++++++++ app/deps/ffmpeg.sh | 8 ++++ release/build_macos.sh | 35 +++++++++++++++++ release/generate_checksums.sh | 1 + 5 files changed, 144 insertions(+) create mode 100755 app/deps/adb_macos.sh create mode 100755 release/build_macos.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3bc62a3a..40508b7d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -165,6 +165,34 @@ jobs: name: build-win64-intermediate path: release/work/build-win64/dist-tar/ + build-macos: + runs-on: macos-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install dependencies + run: | + brew install meson ninja nasm libiconv zlib automake autoconf \ + libtool + + - name: Build macOS + run: release/build_macos.sh + + # upload-artifact does not preserve permissions + - name: Tar + run: | + cd release/work/build-macos + mkdir dist-tar + cd dist-tar + tar -C .. -cvf dist.tar.gz dist/ + + - name: Upload build-macos artifact + uses: actions/upload-artifact@v4 + with: + name: build-macos-intermediate + path: release/work/build-macos/dist-tar/ + package-linux: needs: - build-scrcpy-server @@ -273,12 +301,49 @@ jobs: name: release-win64 path: release/output + package-macos: + needs: + - build-scrcpy-server + - build-macos + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Download scrcpy-server + uses: actions/download-artifact@v4 + with: + name: scrcpy-server + path: release/work/build-server/server/ + + - name: Download build-macos + uses: actions/download-artifact@v4 + with: + name: build-macos-intermediate + path: release/work/build-macos/dist-tar/ + + # upload-artifact does not preserve permissions + - name: Detar + run: | + cd release/work/build-macos + tar xf dist-tar/dist.tar.gz + + - name: Package macos + run: release/package_client.sh macos tar.gz + + - name: Upload macos release + uses: actions/upload-artifact@v4 + with: + name: release-macos + path: release/output/ + release: needs: - build-scrcpy-server - package-linux - package-win32 - package-win64 + - package-macos runs-on: ubuntu-latest steps: - name: Checkout code @@ -308,6 +373,12 @@ jobs: name: release-win64 path: release/output/ + - name: Download release-macos + uses: actions/download-artifact@v4 + with: + name: release-macos + path: release/output/ + - name: Package server run: release/package_server.sh diff --git a/app/deps/adb_macos.sh b/app/deps/adb_macos.sh new file mode 100755 index 00000000..8a25915e --- /dev/null +++ b/app/deps/adb_macos.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +set -ex +DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) +cd "$DEPS_DIR" +. common + +VERSION=35.0.2 +FILENAME=platform-tools_r$VERSION-darwin.zip +PROJECT_DIR=platform-tools-$VERSION-darwin +SHA256SUM=1820078db90bf21628d257ff052528af1c61bb48f754b3555648f5652fa35d78 + +cd "$SOURCES_DIR" + +if [[ -d "$PROJECT_DIR" ]] +then + echo "$PWD/$PROJECT_DIR" found +else + get_file "https://dl.google.com/android/repository/$FILENAME" "$FILENAME" "$SHA256SUM" + mkdir -p "$PROJECT_DIR" + cd "$PROJECT_DIR" + ZIP_PREFIX=platform-tools + unzip "../$FILENAME" "$ZIP_PREFIX"/adb + mv "$ZIP_PREFIX"/* . + rmdir "$ZIP_PREFIX" +fi + +mkdir -p "$INSTALL_DIR/adb-macos" +cd "$INSTALL_DIR/adb-macos" +cp -r "$SOURCES_DIR/$PROJECT_DIR"/. "$INSTALL_DIR/adb-macos/" diff --git a/app/deps/ffmpeg.sh b/app/deps/ffmpeg.sh index 90ffa855..cc71ab13 100755 --- a/app/deps/ffmpeg.sh +++ b/app/deps/ffmpeg.sh @@ -38,6 +38,14 @@ else export CFLAGS='-static-libgcc -static' export CXXFLAGS="$CFLAGS" export LDFLAGS='-static-libgcc -static' + elif [[ "$HOST" == "macos" ]] + then + export LDFLAGS="$LDFLAGS -L/opt/homebrew/opt/zlib/lib" + export CPPFLAGS="$CPPFLAGS -I/opt/homebrew/opt/zlib/include" + + export LDFLAGS="$LDFLAGS-L/opt/homebrew/opt/libiconv/lib" + export CPPFLAGS="$CPPFLAGS -I/opt/homebrew/opt/libiconv/include" + export PKG_CONFIG_PATH="/opt/homebrew/opt/zlib/lib/pkgconfig" fi conf=( diff --git a/release/build_macos.sh b/release/build_macos.sh new file mode 100755 index 00000000..a42c7e88 --- /dev/null +++ b/release/build_macos.sh @@ -0,0 +1,35 @@ +#!/bin/bash +set -ex +cd "$(dirname ${BASH_SOURCE[0]})" +. build_common +cd .. # root project dir + +MACOS_BUILD_DIR="$WORK_DIR/build-macos" + +app/deps/adb_macos.sh +app/deps/sdl.sh macos native static +app/deps/ffmpeg.sh macos native static +app/deps/libusb.sh macos native static + +DEPS_INSTALL_DIR="$PWD/app/deps/work/install/macos-native-static" +ADB_INSTALL_DIR="$PWD/app/deps/work/install/adb-macos" + +rm -rf "$MACOS_BUILD_DIR" +meson setup "$MACOS_BUILD_DIR" \ + --pkg-config-path="$DEPS_INSTALL_DIR/lib/pkgconfig" \ + -Dc_args="-I$DEPS_INSTALL_DIR/include" \ + -Dc_link_args="-L$DEPS_INSTALL_DIR/lib" \ + --buildtype=release \ + --strip \ + -Db_lto=true \ + -Dcompile_server=false \ + -Dportable=true \ + -Dstatic=true +ninja -C "$MACOS_BUILD_DIR" + +# Group intermediate outputs into a 'dist' directory +mkdir -p "$MACOS_BUILD_DIR/dist" +cp "$MACOS_BUILD_DIR"/app/scrcpy "$MACOS_BUILD_DIR/dist/scrcpy_bin" +cp app/data/icon.png "$MACOS_BUILD_DIR/dist/" +cp app/data/scrcpy_static_wrapper.sh "$MACOS_BUILD_DIR/dist/scrcpy" +cp -r "$ADB_INSTALL_DIR"/. "$MACOS_BUILD_DIR/dist/" diff --git a/release/generate_checksums.sh b/release/generate_checksums.sh index d13120de..b0464bed 100755 --- a/release/generate_checksums.sh +++ b/release/generate_checksums.sh @@ -8,5 +8,6 @@ sha256sum "scrcpy-server-$VERSION" \ "scrcpy-linux-$VERSION.tar.gz" \ "scrcpy-win32-$VERSION.zip" \ "scrcpy-win64-$VERSION.zip" \ + "scrcpy-macos-$VERSION.tar.gz" \ | tee SHA256SUMS.txt echo "Release checksums generated in $PWD/SHA256SUMS.txt" From 6f9520f3e2f7df6061bfc06053d7d92aba9a24b8 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 24 Nov 2024 10:12:25 +0100 Subject: [PATCH 2108/2244] Test build_without_gradle.sh in GitHub Actions Build the server without gradle to make sure that the script works. PR #5515 --- .github/workflows/release.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 40508b7d..13f4accf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -51,6 +51,21 @@ jobs: name: scrcpy-server path: release/work/build-server/server/scrcpy-server + test-build-scrcpy-server-without-gradle: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup JDK + uses: actions/setup-java@v4 + with: + distribution: 'zulu' + java-version: '17' + + - name: Build scrcpy-server without gradle + run: server/build_without_gradle.sh + test-client: runs-on: ubuntu-latest steps: From d40224f299b29d614bbdb2ff0e160b6cccd9af03 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 24 Nov 2024 16:32:56 +0100 Subject: [PATCH 2109/2244] Fix alphabetic order of cli args --- app/scrcpy.1 | 24 ++++++++++----------- app/src/cli.c | 60 +++++++++++++++++++++++++-------------------------- 2 files changed, 42 insertions(+), 42 deletions(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 95d5133d..c513dc9a 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -97,18 +97,6 @@ Select the camera size by its aspect ratio (+/- 10%). Possible values are "sensor" (use the camera sensor aspect ratio), "\fInum\fR:\fIden\fR" (e.g. "4:3") and "\fIvalue\fR" (e.g. "1.6"). -.TP -.B \-\-camera\-high\-speed -Enable high-speed camera capture mode. - -This mode is restricted to specific resolutions and frame rates, listed by \fB\-\-list\-camera\-sizes\fR. - -.TP -.BI "\-\-camera\-id " id -Specify the device camera id to mirror. - -The available camera ids can be listed by \fB\-\-list\-cameras\fR. - .TP .BI "\-\-camera\-facing " facing Select the device camera by its facing direction. @@ -121,6 +109,18 @@ Specify the camera capture frame rate. If not specified, Android's default frame rate (30 fps) is used. +.TP +.B \-\-camera\-high\-speed +Enable high-speed camera capture mode. + +This mode is restricted to specific resolutions and frame rates, listed by \fB\-\-list\-camera\-sizes\fR. + +.TP +.BI "\-\-camera\-id " id +Specify the device camera id to mirror. + +The available camera ids can be listed by \fB\-\-list\-cameras\fR. + .TP .BI "\-\-camera\-size " width\fRx\fIheight Specify an explicit camera capture size. diff --git a/app/src/cli.c b/app/src/cli.c index 3f2d23cb..ee86b34b 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -255,14 +255,6 @@ static const struct sc_option options[] = { "ratio), \":\" (e.g. \"4:3\") or \"\" (e.g. " "\"1.6\")." }, - { - .longopt_id = OPT_CAMERA_ID, - .longopt = "camera-id", - .argdesc = "id", - .text = "Specify the device camera id to mirror.\n" - "The available camera ids can be listed by:\n" - " scrcpy --list-cameras", - }, { .longopt_id = OPT_CAMERA_FACING, .longopt = "camera-facing", @@ -270,6 +262,14 @@ static const struct sc_option options[] = { .text = "Select the device camera by its facing direction.\n" "Possible values are \"front\", \"back\" and \"external\".", }, + { + .longopt_id = OPT_CAMERA_FPS, + .longopt = "camera-fps", + .argdesc = "value", + .text = "Specify the camera capture frame rate.\n" + "If not specified, Android's default frame rate (30 fps) is " + "used.", + }, { .longopt_id = OPT_CAMERA_HIGH_SPEED, .longopt = "camera-high-speed", @@ -277,6 +277,14 @@ static const struct sc_option options[] = { "This mode is restricted to specific resolutions and frame " "rates, listed by --list-camera-sizes.", }, + { + .longopt_id = OPT_CAMERA_ID, + .longopt = "camera-id", + .argdesc = "id", + .text = "Specify the device camera id to mirror.\n" + "The available camera ids can be listed by:\n" + " scrcpy --list-cameras", + }, { .longopt_id = OPT_CAMERA_SIZE, .longopt = "camera-size", @@ -284,12 +292,21 @@ static const struct sc_option options[] = { .text = "Specify an explicit camera capture size.", }, { - .longopt_id = OPT_CAMERA_FPS, - .longopt = "camera-fps", + .longopt_id = OPT_CAPTURE_ORIENTATION, + .longopt = "capture-orientation", .argdesc = "value", - .text = "Specify the camera capture frame rate.\n" - "If not specified, Android's default frame rate (30 fps) is " - "used.", + .text = "Set the capture video orientation.\n" + "Possible values are 0, 90, 180, 270, flip0, flip90, flip180 " + "and flip270, possibly prefixed by '@'.\n" + "The number represents the clockwise rotation in degrees; the " + "flip\" keyword applies a horizontal flip before the " + "rotation.\n" + "If a leading '@' is passed (@90) for display capture, then " + "the rotation is locked, and is relative to the natural device " + "orientation.\n" + "If '@' is passed alone, then the rotation is locked to the " + "initial device orientation.\n" + "Default is 0.", }, { // Not really deprecated (--codec has never been released), but without @@ -479,23 +496,6 @@ static const struct sc_option options[] = { .longopt = "list-encoders", .text = "List video and audio encoders available on the device.", }, - { - .longopt_id = OPT_CAPTURE_ORIENTATION, - .longopt = "capture-orientation", - .argdesc = "value", - .text = "Set the capture video orientation.\n" - "Possible values are 0, 90, 180, 270, flip0, flip90, flip180 " - "and flip270, possibly prefixed by '@'.\n" - "The number represents the clockwise rotation in degrees; the " - "flip\" keyword applies a horizontal flip before the " - "rotation.\n" - "If a leading '@' is passed (@90) for display capture, then " - "the rotation is locked, and is relative to the natural device " - "orientation.\n" - "If '@' is passed alone, then the rotation is locked to the " - "initial device orientation.\n" - "Default is 0.", - }, { // deprecated .longopt_id = OPT_LOCK_VIDEO_ORIENTATION, From 54e1f8e060ca77f9b5dac1a2f57ea7cdf0de83c3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 24 Nov 2024 16:39:56 +0100 Subject: [PATCH 2110/2244] Include scrcpy manpage in Linux and macOS releases --- release/build_linux.sh | 1 + release/build_macos.sh | 1 + 2 files changed, 2 insertions(+) diff --git a/release/build_linux.sh b/release/build_linux.sh index 2f2fb62f..445240ce 100755 --- a/release/build_linux.sh +++ b/release/build_linux.sh @@ -32,4 +32,5 @@ mkdir -p "$LINUX_BUILD_DIR/dist" cp "$LINUX_BUILD_DIR"/app/scrcpy "$LINUX_BUILD_DIR/dist/scrcpy_bin" cp app/data/icon.png "$LINUX_BUILD_DIR/dist/" cp app/data/scrcpy_static_wrapper.sh "$LINUX_BUILD_DIR/dist/scrcpy" +cp app/scrcpy.1 "$LINUX_BUILD_DIR/dist/" cp -r "$ADB_INSTALL_DIR"/. "$LINUX_BUILD_DIR/dist/" diff --git a/release/build_macos.sh b/release/build_macos.sh index a42c7e88..58010704 100755 --- a/release/build_macos.sh +++ b/release/build_macos.sh @@ -32,4 +32,5 @@ mkdir -p "$MACOS_BUILD_DIR/dist" cp "$MACOS_BUILD_DIR"/app/scrcpy "$MACOS_BUILD_DIR/dist/scrcpy_bin" cp app/data/icon.png "$MACOS_BUILD_DIR/dist/" cp app/data/scrcpy_static_wrapper.sh "$MACOS_BUILD_DIR/dist/scrcpy" +cp app/scrcpy.1 "$MACOS_BUILD_DIR/dist/" cp -r "$ADB_INSTALL_DIR"/. "$MACOS_BUILD_DIR/dist/" From 3d478d7d5b19839b10b581b14be0140793fcbeb0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 24 Nov 2024 17:01:37 +0100 Subject: [PATCH 2111/2244] Build FFmpeg with v4l2 support for Linux So that --v4l2-sink works with Linux static builds. --- .github/workflows/release.yml | 6 ++++-- app/deps/ffmpeg.sh | 8 +++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 13f4accf..4f7d0241 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -77,7 +77,8 @@ jobs: sudo apt update sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \ libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \ - libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev + libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \ + libv4l-dev - name: Test run: release/test_client.sh @@ -93,7 +94,8 @@ jobs: sudo apt update sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \ libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \ - libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev + libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \ + libv4l-dev - name: Build linux run: release/build_linux.sh diff --git a/app/deps/ffmpeg.sh b/app/deps/ffmpeg.sh index cc71ab13..386de190 100755 --- a/app/deps/ffmpeg.sh +++ b/app/deps/ffmpeg.sh @@ -81,8 +81,14 @@ else --enable-muxer=wav ) - if [[ "$HOST" != linux ]] + if [[ "$HOST" == linux ]] then + conf+=( + --enable-libv4l2 + --enable-outdev=v4l2 + --enable-encoder=rawvideo + ) + else # libavdevice is only used for V4L2 on Linux conf+=( --disable-avdevice From 5e05f2a25bfdc35840bef81583f02b0ba127b32b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 24 Nov 2024 16:01:16 +0100 Subject: [PATCH 2112/2244] Bump version to 3.0 --- app/scrcpy-windows.rc | 2 +- meson.build | 2 +- server/build.gradle | 4 ++-- server/build_without_gradle.sh | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/scrcpy-windows.rc b/app/scrcpy-windows.rc index 6454c88e..b80e01b9 100644 --- a/app/scrcpy-windows.rc +++ b/app/scrcpy-windows.rc @@ -13,7 +13,7 @@ BEGIN VALUE "LegalCopyright", "Romain Vimont, Genymobile" VALUE "OriginalFilename", "scrcpy.exe" VALUE "ProductName", "scrcpy" - VALUE "ProductVersion", "2.7" + VALUE "ProductVersion", "3.0" END END BLOCK "VarFileInfo" diff --git a/meson.build b/meson.build index f76d5ecf..b3ad3c75 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('scrcpy', 'c', - version: '2.7', + version: '3.0', meson_version: '>= 0.48', default_options: [ 'c_std=c11', diff --git a/server/build.gradle b/server/build.gradle index 2781a2db..72c74a5a 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -7,8 +7,8 @@ android { applicationId "com.genymobile.scrcpy" minSdkVersion 21 targetSdkVersion 35 - versionCode 20700 - versionName "2.7" + versionCode 30000 + versionName "3.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index 7b293e02..d0572615 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -12,7 +12,7 @@ set -e SCRCPY_DEBUG=false -SCRCPY_VERSION_NAME=2.7 +SCRCPY_VERSION_NAME=3.0 PLATFORM=${ANDROID_PLATFORM:-35} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-35.0.0} From 74aecc00b512969fa3b067ed3cf20e12194206d2 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 24 Nov 2024 16:22:40 +0100 Subject: [PATCH 2113/2244] Update links to 3.0 --- README.md | 2 +- doc/build.md | 6 +++--- doc/linux.md | 23 ++++++++++++++++++++--- doc/macos.md | 17 +++++++++++++++++ doc/windows.md | 22 +++++++++++++--------- install_release.sh | 4 ++-- 6 files changed, 56 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 7dada742..253b9254 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ source for the project. Do not download releases from random websites, even if their name contains `scrcpy`.** -# scrcpy (v2.7) +# scrcpy (v3.0) scrcpy diff --git a/doc/build.md b/doc/build.md index 0c70f598..43841268 100644 --- a/doc/build.md +++ b/doc/build.md @@ -233,10 +233,10 @@ install` must be run as root)._ #### Option 2: Use prebuilt server - - [`scrcpy-server-v2.7`][direct-scrcpy-server] - SHA-256: `a23c5659f36c260f105c022d27bcb3eafffa26070e7baa9eda66d01377a1adba` + - [`scrcpy-server-v3.0`][direct-scrcpy-server] + SHA-256: `800044c62a94d5fc16f5ab9c86d45b1050eae3eb436514d1b0d2fe2646b894ea` -[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v2.7/scrcpy-server-v2.7 +[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v3.0/scrcpy-server-v3.0 Download the prebuilt server somewhere, and specify its path during the Meson configuration: diff --git a/doc/linux.md b/doc/linux.md index 6bfe3454..79cbb286 100644 --- a/doc/linux.md +++ b/doc/linux.md @@ -2,6 +2,23 @@ ## Install +### From the official release + +Download a static build of the [latest release]: + + - [`scrcpy-linux-v3.0.tar.gz`][direct-linux] (x86_64) + SHA-256: `06cb74e22f758228c944cea048b78e42b2925c2affe2b5aca901cfd6a649e503` + +[latest release]: https://github.com/Genymobile/scrcpy/releases/latest +[direct-linux]: https://github.com/Genymobile/scrcpy/releases/download/v3.0/scrcpy-linux-v3.0.tar.gz + +and extract it. + +_Static builds of scrcpy for Linux are still experimental._ + + +### From your package manager + Packaging status Scrcpy is packaged in several distributions and package managers: @@ -13,10 +30,10 @@ Scrcpy is packaged in several distributions and package managers: - Snap: `snap install scrcpy` - … (see [repology](https://repology.org/project/scrcpy/versions)) -### Latest version -However, the packaged version is not always the latest release. To install the -latest release from `master`, follow this simplified process. +### From an install script + +To install the latest release from `master`, follow this simplified process. First, you need to install the required packages: diff --git a/doc/macos.md b/doc/macos.md index 2c7c6071..ee3c23be 100644 --- a/doc/macos.md +++ b/doc/macos.md @@ -2,6 +2,23 @@ ## Install +### From the official release + +Download a static build of the [latest release]: + + - [`scrcpy-macos-v3.0.tar.gz`][direct-macos] (arm64) + SHA-256: `5db9821918537eb3aaf0333cdd05baf85babdd851972d5f1b71f86da0530b4bf` + +[latest release]: https://github.com/Genymobile/scrcpy/releases/latest +[direct-macos]: https://github.com/Genymobile/scrcpy/releases/download/v3.0/scrcpy-macos-v3.0.tar.gz + +and extract it. + +_Static builds of scrcpy for macOS are still experimental._ + + +### From a package manager + Scrcpy is available in [Homebrew]: ```bash diff --git a/doc/windows.md b/doc/windows.md index 36e59178..330b4fbd 100644 --- a/doc/windows.md +++ b/doc/windows.md @@ -2,27 +2,32 @@ ## Install +### From the official release + Download the [latest release]: - - [`scrcpy-win64-v2.7.zip`][direct-win64] (64-bit) - SHA-256: `5910bc18d5a16f42d84185ddc7e16a4cee6a6f5f33451559c1a1d6d0099bd5f5` - - [`scrcpy-win32-v2.7.zip`][direct-win32] (32-bit) - SHA-256: `ef4daf89d500f33d78b830625536ecb18481429dd94433e7634c824292059d06` + - [`scrcpy-win64-v3.0.zip`][direct-win64] (64-bit) + SHA-256: `dfbe8a8fef6535197acc506936bfd59d0aa0427e9b44fb2e5c550eae642f72be` + - [`scrcpy-win32-v3.0.zip`][direct-win32] (32-bit) + SHA-256: `7cbf8d7a6ebfdca7b3b161e29a481c11088305f3e0a89d28e8e62f70c7bd0028` [latest release]: https://github.com/Genymobile/scrcpy/releases/latest -[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v2.7/scrcpy-win64-v2.7.zip -[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v2.7/scrcpy-win32-v2.7.zip +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v3.0/scrcpy-win64-v3.0.zip +[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v3.0/scrcpy-win32-v3.0.zip and extract it. -Alternatively, you could install it from packages manager, like [Chocolatey]: + +### From a package manager + +From [Chocolatey]: ```bash choco install scrcpy choco install adb # if you don't have it yet ``` -or [Scoop]: +From [Scoop]: ```bash @@ -30,7 +35,6 @@ scoop install scrcpy scoop install adb # if you don't have it yet ``` -[Winget]: https://github.com/microsoft/winget-cli [Chocolatey]: https://chocolatey.org/ [Scoop]: https://scoop.sh diff --git a/install_release.sh b/install_release.sh index 3cf3490c..46b7dd43 100755 --- a/install_release.sh +++ b/install_release.sh @@ -2,8 +2,8 @@ set -e BUILDDIR=build-auto -PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v2.7/scrcpy-server-v2.7 -PREBUILT_SERVER_SHA256=a23c5659f36c260f105c022d27bcb3eafffa26070e7baa9eda66d01377a1adba +PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v3.0/scrcpy-server-v3.0 +PREBUILT_SERVER_SHA256=800044c62a94d5fc16f5ab9c86d45b1050eae3eb436514d1b0d2fe2646b894ea echo "[scrcpy] Downloading prebuilt server..." wget "$PREBUILT_SERVER_URL" -O scrcpy-server From da8ade88fd0b1c15c5ae07cd276b3cffa2e5382e Mon Sep 17 00:00:00 2001 From: Wouter Schoot Date: Sun, 24 Nov 2024 23:18:54 +0100 Subject: [PATCH 2114/2244] Fix link to virtual display doc in README PR #5525 Signed-off-by: Romain Vimont --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 253b9254..5075e7ed 100644 --- a/README.md +++ b/README.md @@ -141,7 +141,7 @@ documented in the following pages: - [Device](doc/device.md) - [Window](doc/window.md) - [Recording](doc/recording.md) - - [Virtual display](doc/virtual_displays.md) + - [Virtual display](doc/virtual_display.md) - [Tunnels](doc/tunnels.md) - [OTG](doc/otg.md) - [Camera](doc/camera.md) From 7fef05197674aa82b7c81541411115704fc9599f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 25 Nov 2024 20:06:32 +0100 Subject: [PATCH 2115/2244] Add BlueSky link Scrcpy now has a BlueSky account. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 5075e7ed..85023a1d 100644 --- a/README.md +++ b/README.md @@ -181,6 +181,7 @@ to your problem immediately. You can also use: - Reddit: [`r/scrcpy`](https://www.reddit.com/r/scrcpy) + - BlueSky: [`@scrcpy.bsky.social`](https://bsky.app/profile/scrcpy.bsky.social) - Twitter: [`@scrcpy_app`](https://twitter.com/scrcpy_app) From 1d2f16dbb53facdb2ea174578437a2a5afb6aede Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 26 Nov 2024 14:06:36 +0100 Subject: [PATCH 2116/2244] Fix documentation about default mouse mode When video playback is turned off, the default mouse mode has changed from "uhid" to "disabled" in 2c25fd7a8082307da19645a690c31403903fbb1e. Update the documentation accordingly. Refs #5410 Refs #5542 --- doc/control.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/doc/control.md b/doc/control.md index 26805346..86c0efe6 100644 --- a/doc/control.md +++ b/doc/control.md @@ -23,14 +23,20 @@ To control the device without mirroring: scrcpy --no-video --no-audio ``` -By default, mouse mode is switched to UHID if video mirroring is disabled (a -relative mouse mode is required). +By default, the mouse is disabled when video playback is turned off. + +To control the device using a relative mouse, enable UHID mouse mode: + +```bash +scrcpy --no-video --no-audio --mouse=uhid +scrcpy --no-video --no-audio -M # short version +``` To also use a UHID keyboard, set it explicitly: ```bash -scrcpy --no-video --no-audio --keyboard=uhid -scrcpy --no-video --no-audio -K # short version +scrcpy --no-video --no-audio --mouse=uhid --keyboard=uhid +scrcpy --no-video --no-audio -MK # short version ``` To use AOA instead (over USB only): From 3d5294c1e5819535dfa94bd399e53191283105cd Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 24 Nov 2024 21:36:28 +0100 Subject: [PATCH 2117/2244] Set main display power for virtual display Change the display power of the main display when mirroring a virtual display, to make it possible to turn off the screen. Fixes #5522 Refs #5530 --- server/src/main/java/com/genymobile/scrcpy/CleanUp.java | 8 +++++--- .../java/com/genymobile/scrcpy/control/Controller.java | 9 ++++++--- 2 files changed, 11 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 352f7c6b..1c6f1701 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java +++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java @@ -207,13 +207,15 @@ public final class CleanUp { } } - if (displayId != Device.DISPLAY_ID_NONE && Device.isScreenOn(displayId)) { + // Change the power of the main display when mirroring a virtual display + int targetDisplayId = displayId != Device.DISPLAY_ID_NONE ? displayId : 0; + if (Device.isScreenOn(targetDisplayId)) { if (powerOffScreen) { Ln.i("Power off screen"); - Device.powerOffScreen(displayId); + Device.powerOffScreen(targetDisplayId); } else if (restoreDisplayPower) { Ln.i("Restoring display power"); - Device.setDisplayPower(displayId, true); + Device.setDisplayPower(targetDisplayId, true); } } diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index f0e4c037..34c613e6 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -281,7 +281,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { setClipboard(msg.getText(), msg.getPaste(), msg.getSequence()); break; case ControlMessage.TYPE_SET_DISPLAY_POWER: - if (supportsInputEvents && displayId != Device.DISPLAY_ID_NONE) { + if (supportsInputEvents) { setDisplayPower(msg.getOn()); } break; @@ -691,9 +691,12 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { } private void setDisplayPower(boolean on) { - boolean setDisplayPowerOk = Device.setDisplayPower(displayId, on); + // Change the power of the main display when mirroring a virtual display + int targetDisplayId = displayId != Device.DISPLAY_ID_NONE ? displayId : 0; + boolean setDisplayPowerOk = Device.setDisplayPower(targetDisplayId, on); if (setDisplayPowerOk) { - keepDisplayPowerOff = !on; + // Do not keep display power off for virtual displays: MOD+p must wake up the physical device + keepDisplayPowerOff = displayId != Device.DISPLAY_ID_NONE && !on; Ln.i("Device display turned " + (on ? "on" : "off")); if (cleanUp != null) { boolean mustRestoreOnExit = !on; From 3d1f036c04412e17a694e6a0b857b7f9e9217ab3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 26 Nov 2024 15:47:27 +0100 Subject: [PATCH 2118/2244] Rollback to old --turn-screen-off for Android 15 When the screen is turned off with the new display power method introduced in Android 15, video mirroring freezes. Use the Android 14 method for Android 15. Refs 58ba00fa060c9a1f439120f8869ed106e1c935f9 Refs #5418 Fixes #5530 --- .../src/main/java/com/genymobile/scrcpy/device/Device.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Device.java b/server/src/main/java/com/genymobile/scrcpy/device/Device.java index cd713499..3553dc27 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Device.java @@ -40,6 +40,10 @@ public final class Device { public static final int INJECT_MODE_WAIT_FOR_RESULT = InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_RESULT; public static final int INJECT_MODE_WAIT_FOR_FINISH = InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH; + // The new display power method introduced in Android 15 does not work as expected: + // + private static final boolean USE_ANDROID_15_DISPLAY_POWER = false; + private Device() { // not instantiable } @@ -127,7 +131,7 @@ public final class Device { public static boolean setDisplayPower(int displayId, boolean on) { assert displayId != Device.DISPLAY_ID_NONE; - if (Build.VERSION.SDK_INT >= AndroidVersions.API_35_ANDROID_15) { + if (USE_ANDROID_15_DISPLAY_POWER && Build.VERSION.SDK_INT >= AndroidVersions.API_35_ANDROID_15) { return ServiceManager.getDisplayManager().requestDisplayPower(displayId, on); } From 3e689020baa1b3ea1b66cba3260a7a33be458a06 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 27 Nov 2024 07:45:35 +0100 Subject: [PATCH 2119/2244] Fix null return value in DisplayManager.toString() Ensure DisplayListener.toString() returns a non-null value to prevent a NullPointerException on certain devices. Fixes #5537 --- .../java/com/genymobile/scrcpy/wrappers/DisplayManager.java | 3 +++ 1 file changed, 3 insertions(+) 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 b497e97f..d44ac608 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java @@ -192,6 +192,9 @@ public final class DisplayManager { if ("onDisplayChanged".equals(method.getName())) { listener.onDisplayChanged((int) args[0]); } + if ("toString".equals(method.getName())) { + return "DisplayListener"; + } return null; }); try { From 678025b31672c230575fe2dbc4a0d487d5010bb1 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 26 Nov 2024 00:13:02 +0100 Subject: [PATCH 2120/2244] Remove apt update on GitHub Actions Assume the image is up-to-date. --- .github/workflows/release.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4f7d0241..390b99a0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -74,7 +74,6 @@ jobs: - name: Install dependencies run: | - sudo apt update sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \ libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \ libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \ @@ -91,7 +90,6 @@ jobs: - name: Install dependencies run: | - sudo apt update sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \ libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \ libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \ @@ -122,7 +120,6 @@ jobs: - name: Install dependencies run: | - sudo apt update sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \ libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \ libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \ @@ -156,7 +153,6 @@ jobs: - name: Install dependencies run: | - sudo apt update sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \ libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \ libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \ From a18ed1ee7ab10143239e8bf979cfa9bf938a4ea3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 25 Nov 2024 10:21:19 +0100 Subject: [PATCH 2121/2244] Simplify GitHub actions step descriptions Each step is executed within the context of an action, so mentioning the name of the action is unnecessary. --- .github/workflows/release.yml | 38 +++++++++++++++++------------------ 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 390b99a0..8816fbbc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -42,10 +42,10 @@ jobs: distribution: 'zulu' java-version: '17' - - name: Build scrcpy-server + - name: Build run: release/build_server.sh - - name: Upload scrcpy-server artifact + - name: Upload artifact uses: actions/upload-artifact@v4 with: name: scrcpy-server @@ -63,7 +63,7 @@ jobs: distribution: 'zulu' java-version: '17' - - name: Build scrcpy-server without gradle + - name: Build without gradle run: server/build_without_gradle.sh test-client: @@ -95,7 +95,7 @@ jobs: libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \ libv4l-dev - - name: Build linux + - name: Build run: release/build_linux.sh # upload-artifact does not preserve permissions @@ -106,7 +106,7 @@ jobs: cd dist-tar tar -C .. -cvf dist.tar.gz dist/ - - name: Upload build-linux artifact + - name: Upload artifact uses: actions/upload-artifact@v4 with: name: build-linux-intermediate @@ -128,7 +128,7 @@ jobs: - name: Workaround for old meson version run by Github Actions run: sed -i 's/^pkg-config/pkgconfig/' cross_win32.txt - - name: Build win32 + - name: Build run: release/build_windows.sh 32 # upload-artifact does not preserve permissions @@ -139,7 +139,7 @@ jobs: cd dist-tar tar -C .. -cvf dist.tar.gz dist/ - - name: Upload build-win32 artifact + - name: Upload artifact uses: actions/upload-artifact@v4 with: name: build-win32-intermediate @@ -161,7 +161,7 @@ jobs: - name: Workaround for old meson version run by Github Actions run: sed -i 's/^pkg-config/pkgconfig/' cross_win64.txt - - name: Build win64 + - name: Build run: release/build_windows.sh 64 # upload-artifact does not preserve permissions @@ -172,7 +172,7 @@ jobs: cd dist-tar tar -C .. -cvf dist.tar.gz dist/ - - name: Upload build-win64 artifact + - name: Upload artifact uses: actions/upload-artifact@v4 with: name: build-win64-intermediate @@ -189,7 +189,7 @@ jobs: brew install meson ninja nasm libiconv zlib automake autoconf \ libtool - - name: Build macOS + - name: Build run: release/build_macos.sh # upload-artifact does not preserve permissions @@ -200,7 +200,7 @@ jobs: cd dist-tar tar -C .. -cvf dist.tar.gz dist/ - - name: Upload build-macos artifact + - name: Upload artifact uses: actions/upload-artifact@v4 with: name: build-macos-intermediate @@ -233,10 +233,10 @@ jobs: cd release/work/build-linux tar xf dist-tar/dist.tar.gz - - name: Package linux + - name: Package run: release/package_client.sh linux tar.gz - - name: Upload linux release + - name: Upload release uses: actions/upload-artifact@v4 with: name: release-linux @@ -269,10 +269,10 @@ jobs: cd release/work/build-win32 tar xf dist-tar/dist.tar.gz - - name: Package win32 + - name: Package run: release/package_client.sh win32 zip - - name: Upload win32 release + - name: Upload release uses: actions/upload-artifact@v4 with: name: release-win32 @@ -305,10 +305,10 @@ jobs: cd release/work/build-win64 tar xf dist-tar/dist.tar.gz - - name: Package win64 + - name: Package run: release/package_client.sh win64 zip - - name: Upload win64 release + - name: Upload release uses: actions/upload-artifact@v4 with: name: release-win64 @@ -341,10 +341,10 @@ jobs: cd release/work/build-macos tar xf dist-tar/dist.tar.gz - - name: Package macos + - name: Package run: release/package_client.sh macos tar.gz - - name: Upload macos release + - name: Upload release uses: actions/upload-artifact@v4 with: name: release-macos From ee9f7126ffba412d7a7b3119c55b968d5d6e7502 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 25 Nov 2024 09:54:55 +0100 Subject: [PATCH 2122/2244] Use FORMAT variable name in package_client.sh The format is used several times, avoid using "$2" directly. --- release/package_client.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/release/package_client.sh b/release/package_client.sh index c6d430b2..e92cffa9 100755 --- a/release/package_client.sh +++ b/release/package_client.sh @@ -14,9 +14,9 @@ fi FORMAT=$2 -if [[ "$2" != zip && "$2" != tar.gz ]] +if [[ "$FORMAT" != zip && "$FORMAT" != tar.gz ]] then - echo "Invalid format (expected zip or tar.gz): $2" >&2 + echo "Invalid format (expected zip or tar.gz): $FORMAT" >&2 exit 1 fi From acddd811bf9192fce2ca3332b6f7f654b3d3a0e6 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 25 Nov 2024 10:00:11 +0100 Subject: [PATCH 2123/2244] Rename TARGET to TARGET_DIRNAME This avoids confusion with "$1", which is also documented as "". If "$1" (the target) is "linux", then TARGET_DIRNAME is "scrcpy-linux-v3.0". --- release/package_client.sh | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/release/package_client.sh b/release/package_client.sh index e92cffa9..1c0bf801 100755 --- a/release/package_client.sh +++ b/release/package_client.sh @@ -22,31 +22,31 @@ fi BUILD_DIR="$WORK_DIR/build-$1" ARCHIVE_DIR="$BUILD_DIR/release-archive" -TARGET="scrcpy-$1-$VERSION" +TARGET_DIRNAME="scrcpy-$1-$VERSION" -rm -rf "$ARCHIVE_DIR/$TARGET" -mkdir -p "$ARCHIVE_DIR/$TARGET" +rm -rf "$ARCHIVE_DIR/$TARGET_DIRNAME" +mkdir -p "$ARCHIVE_DIR/$TARGET_DIRNAME" -cp -r "$BUILD_DIR/dist/." "$ARCHIVE_DIR/$TARGET/" -cp "$WORK_DIR/build-server/server/scrcpy-server" "$ARCHIVE_DIR/$TARGET/" +cp -r "$BUILD_DIR/dist/." "$ARCHIVE_DIR/$TARGET_DIRNAME/" +cp "$WORK_DIR/build-server/server/scrcpy-server" "$ARCHIVE_DIR/$TARGET_DIRNAME/" mkdir -p "$OUTPUT_DIR" cd "$ARCHIVE_DIR" -rm -f "$OUTPUT_DIR/$TARGET.$FORMAT" +rm -f "$OUTPUT_DIR/$TARGET_DIRNAME.$FORMAT" case "$FORMAT" in zip) - zip -r "$OUTPUT_DIR/$TARGET.zip" "$TARGET" + zip -r "$OUTPUT_DIR/$TARGET_DIRNAME.zip" "$TARGET_DIRNAME" ;; tar.gz) - tar cvf "$OUTPUT_DIR/$TARGET.tar.gz" "$TARGET" + tar cvf "$OUTPUT_DIR/$TARGET_DIRNAME.tar.gz" "$TARGET_DIRNAME" ;; *) echo "Invalid format (expected zip or tar.gz): $FORMAT" >&2 exit 1 esac -rm -rf "$TARGET" +rm -rf "$TARGET_DIRNAME" cd - -echo "Generated '$OUTPUT_DIR/$TARGET.$FORMAT'" +echo "Generated '$OUTPUT_DIR/$TARGET_DIRNAME.$FORMAT'" From 618a978f5b3a37ba08ee7f9832a77fba4e19c667 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 25 Nov 2024 10:28:39 +0100 Subject: [PATCH 2124/2244] Specify architecture for Linux and macOS releases PR #5526 Co-authored-by: Genxster1998 --- .github/workflows/release.yml | 64 +++++++++++++++++------------------ release/build_linux.sh | 9 ++++- release/build_macos.sh | 9 ++++- release/generate_checksums.sh | 4 +-- release/release.sh | 4 +-- 5 files changed, 52 insertions(+), 38 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8816fbbc..1ee8eb35 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -82,7 +82,7 @@ jobs: - name: Test run: release/test_client.sh - build-linux: + build-linux-x86_64: runs-on: ubuntu-latest steps: - name: Checkout code @@ -96,12 +96,12 @@ jobs: libv4l-dev - name: Build - run: release/build_linux.sh + run: release/build_linux.sh x86_64 # upload-artifact does not preserve permissions - name: Tar run: | - cd release/work/build-linux + cd release/work/build-linux-x86_64 mkdir dist-tar cd dist-tar tar -C .. -cvf dist.tar.gz dist/ @@ -109,8 +109,8 @@ jobs: - name: Upload artifact uses: actions/upload-artifact@v4 with: - name: build-linux-intermediate - path: release/work/build-linux/dist-tar/ + name: build-linux-x86_64-intermediate + path: release/work/build-linux-x86_64/dist-tar/ build-win32: runs-on: ubuntu-latest @@ -178,7 +178,7 @@ jobs: name: build-win64-intermediate path: release/work/build-win64/dist-tar/ - build-macos: + build-macos-aarch64: runs-on: macos-latest steps: - name: Checkout code @@ -190,12 +190,12 @@ jobs: libtool - name: Build - run: release/build_macos.sh + run: release/build_macos.sh aarch64 # upload-artifact does not preserve permissions - name: Tar run: | - cd release/work/build-macos + cd release/work/build-macos-aarch64 mkdir dist-tar cd dist-tar tar -C .. -cvf dist.tar.gz dist/ @@ -203,13 +203,13 @@ jobs: - name: Upload artifact uses: actions/upload-artifact@v4 with: - name: build-macos-intermediate - path: release/work/build-macos/dist-tar/ + name: build-macos-aarch64-intermediate + path: release/work/build-macos-aarch64/dist-tar/ - package-linux: + package-linux-x86_64: needs: - build-scrcpy-server - - build-linux + - build-linux-x86_64 runs-on: ubuntu-latest steps: - name: Checkout code @@ -221,25 +221,25 @@ jobs: name: scrcpy-server path: release/work/build-server/server/ - - name: Download build-linux + - name: Download build-linux-x86_64 uses: actions/download-artifact@v4 with: - name: build-linux-intermediate - path: release/work/build-linux/dist-tar/ + name: build-linux-x86_64-intermediate + path: release/work/build-linux-x86_64/dist-tar/ # upload-artifact does not preserve permissions - name: Detar run: | - cd release/work/build-linux + cd release/work/build-linux-x86_64 tar xf dist-tar/dist.tar.gz - name: Package - run: release/package_client.sh linux tar.gz + run: release/package_client.sh linux-x86_64 tar.gz - name: Upload release uses: actions/upload-artifact@v4 with: - name: release-linux + name: release-linux-x86_64 path: release/output/ package-win32: @@ -314,10 +314,10 @@ jobs: name: release-win64 path: release/output - package-macos: + package-macos-aarch64: needs: - build-scrcpy-server - - build-macos + - build-macos-aarch64 runs-on: ubuntu-latest steps: - name: Checkout code @@ -329,34 +329,34 @@ jobs: name: scrcpy-server path: release/work/build-server/server/ - - name: Download build-macos + - name: Download build-macos-aarch64 uses: actions/download-artifact@v4 with: - name: build-macos-intermediate - path: release/work/build-macos/dist-tar/ + name: build-macos-aarch64-intermediate + path: release/work/build-macos-aarch64/dist-tar/ # upload-artifact does not preserve permissions - name: Detar run: | - cd release/work/build-macos + cd release/work/build-macos-aarch64 tar xf dist-tar/dist.tar.gz - name: Package - run: release/package_client.sh macos tar.gz + run: release/package_client.sh macos-aarch64 tar.gz - name: Upload release uses: actions/upload-artifact@v4 with: - name: release-macos + name: release-macos-aarch64 path: release/output/ release: needs: - build-scrcpy-server - - package-linux + - package-linux-x86_64 - package-win32 - package-win64 - - package-macos + - package-macos-aarch64 runs-on: ubuntu-latest steps: - name: Checkout code @@ -368,10 +368,10 @@ jobs: name: scrcpy-server path: release/work/build-server/server/ - - name: Download release-linux + - name: Download release-linux-x86_64 uses: actions/download-artifact@v4 with: - name: release-linux + name: release-linux-x86_64 path: release/output/ - name: Download release-win32 @@ -386,10 +386,10 @@ jobs: name: release-win64 path: release/output/ - - name: Download release-macos + - name: Download release-macos-aarch64 uses: actions/download-artifact@v4 with: - name: release-macos + name: release-macos-aarch64 path: release/output/ - name: Package server diff --git a/release/build_linux.sh b/release/build_linux.sh index 445240ce..39308828 100755 --- a/release/build_linux.sh +++ b/release/build_linux.sh @@ -4,7 +4,14 @@ cd "$(dirname ${BASH_SOURCE[0]})" . build_common cd .. # root project dir -LINUX_BUILD_DIR="$WORK_DIR/build-linux" +if [[ $# != 1 ]] +then + echo "Syntax: $0 " >&2 + exit 1 +fi + +ARCH="$1" +LINUX_BUILD_DIR="$WORK_DIR/build-linux-$ARCH" app/deps/adb_linux.sh app/deps/sdl.sh linux native static diff --git a/release/build_macos.sh b/release/build_macos.sh index 58010704..4794d97d 100755 --- a/release/build_macos.sh +++ b/release/build_macos.sh @@ -4,7 +4,14 @@ cd "$(dirname ${BASH_SOURCE[0]})" . build_common cd .. # root project dir -MACOS_BUILD_DIR="$WORK_DIR/build-macos" +if [[ $# != 1 ]] +then + echo "Syntax: $0 " >&2 + exit 1 +fi + +ARCH="$1" +MACOS_BUILD_DIR="$WORK_DIR/build-macos-$ARCH" app/deps/adb_macos.sh app/deps/sdl.sh macos native static diff --git a/release/generate_checksums.sh b/release/generate_checksums.sh index b0464bed..f4305703 100755 --- a/release/generate_checksums.sh +++ b/release/generate_checksums.sh @@ -5,9 +5,9 @@ cd "$(dirname ${BASH_SOURCE[0]})" cd "$OUTPUT_DIR" sha256sum "scrcpy-server-$VERSION" \ - "scrcpy-linux-$VERSION.tar.gz" \ + "scrcpy-linux-x86_64-$VERSION.tar.gz" \ "scrcpy-win32-$VERSION.zip" \ "scrcpy-win64-$VERSION.zip" \ - "scrcpy-macos-$VERSION.tar.gz" \ + "scrcpy-macos-aarch64-$VERSION.tar.gz" \ | tee SHA256SUMS.txt echo "Release checksums generated in $PWD/SHA256SUMS.txt" diff --git a/release/release.sh b/release/release.sh index 8bef11ab..ddba585b 100755 --- a/release/release.sh +++ b/release/release.sh @@ -12,12 +12,12 @@ rm -rf output ./build_server.sh ./build_windows.sh 32 ./build_windows.sh 64 -./build_linux.sh +./build_linux.sh x86_64 ./package_server.sh ./package_client.sh win32 zip ./package_client.sh win64 zip -./package_client.sh linux tar.gz +./package_client.sh linux-x86_64 tar.gz ./generate_checksums.sh From c1351b250e4824017d876143b39a0d643d7a0f65 Mon Sep 17 00:00:00 2001 From: Genxster1998 Date: Mon, 25 Nov 2024 04:18:46 +0530 Subject: [PATCH 2125/2244] Build macOS x86_64 release Add actions to build a release for macOS x86_64 in addition to the aarch64 version. PR #5526 Signed-off-by: Romain Vimont --- .github/workflows/release.yml | 70 +++++++++++++++++++++++++++++++++++ release/generate_checksums.sh | 1 + 2 files changed, 71 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1ee8eb35..c6187ccb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -206,6 +206,33 @@ jobs: name: build-macos-aarch64-intermediate path: release/work/build-macos-aarch64/dist-tar/ + build-macos-x86_64: + runs-on: macos-13 + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install dependencies + run: brew install meson ninja nasm libiconv zlib automake + # autoconf and libtool are already installed on macos-13 + + - name: Build + run: release/build_macos.sh x86_64 + + # upload-artifact does not preserve permissions + - name: Tar + run: | + cd release/work/build-macos-x86_64 + mkdir dist-tar + cd dist-tar + tar -C .. -cvf dist.tar.gz dist/ + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: build-macos-x86_64-intermediate + path: release/work/build-macos-x86_64/dist-tar/ + package-linux-x86_64: needs: - build-scrcpy-server @@ -350,6 +377,42 @@ jobs: name: release-macos-aarch64 path: release/output/ + package-macos-x86_64: + needs: + - build-scrcpy-server + - build-macos-x86_64 + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Download scrcpy-server + uses: actions/download-artifact@v4 + with: + name: scrcpy-server + path: release/work/build-server/server/ + + - name: Download build-macos + uses: actions/download-artifact@v4 + with: + name: build-macos-x86_64-intermediate + path: release/work/build-macos-x86_64/dist-tar/ + + # upload-artifact does not preserve permissions + - name: Detar + run: | + cd release/work/build-macos-x86_64 + tar xf dist-tar/dist.tar.gz + + - name: Package + run: release/package_client.sh macos-x86_64 tar.gz + + - name: Upload release + uses: actions/upload-artifact@v4 + with: + name: release-macos-x86_64 + path: release/output/ + release: needs: - build-scrcpy-server @@ -357,6 +420,7 @@ jobs: - package-win32 - package-win64 - package-macos-aarch64 + - package-macos-x86_64 runs-on: ubuntu-latest steps: - name: Checkout code @@ -392,6 +456,12 @@ jobs: name: release-macos-aarch64 path: release/output/ + - name: Download release-macos-x86_64 + uses: actions/download-artifact@v4 + with: + name: release-macos-x86_64 + path: release/output/ + - name: Package server run: release/package_server.sh diff --git a/release/generate_checksums.sh b/release/generate_checksums.sh index f4305703..2785c6c3 100755 --- a/release/generate_checksums.sh +++ b/release/generate_checksums.sh @@ -9,5 +9,6 @@ sha256sum "scrcpy-server-$VERSION" \ "scrcpy-win32-$VERSION.zip" \ "scrcpy-win64-$VERSION.zip" \ "scrcpy-macos-aarch64-$VERSION.tar.gz" \ + "scrcpy-macos-x86_64-$VERSION.tar.gz" \ | tee SHA256SUMS.txt echo "Release checksums generated in $PWD/SHA256SUMS.txt" From 017a3672a49b55c7943217089f506a55fea8d834 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 28 Nov 2024 19:44:21 +0100 Subject: [PATCH 2126/2244] Check GitHub runner architecture Make sure that the releases are built for the expected target arch. --- .github/workflows/release.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c6187ccb..a77b7ff1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -85,6 +85,15 @@ jobs: build-linux-x86_64: runs-on: ubuntu-latest steps: + - name: Check architecture + run: | + arch=$(uname -m) + if [[ "$arch" != x86_64 ]] + then + echo "Unexpected architecture: $arch" >&2 + exit 1 + fi + - name: Checkout code uses: actions/checkout@v4 @@ -181,6 +190,15 @@ jobs: build-macos-aarch64: runs-on: macos-latest steps: + - name: Check architecture + run: | + arch=$(uname -m) + if [[ "$arch" != arm64 ]] + then + echo "Unexpected architecture: $arch" >&2 + exit 1 + fi + - name: Checkout code uses: actions/checkout@v4 @@ -209,6 +227,15 @@ jobs: build-macos-x86_64: runs-on: macos-13 steps: + - name: Check architecture + run: | + arch=$(uname -m) + if [[ "$arch" != x86_64 ]] + then + echo "Unexpected architecture: $arch" >&2 + exit 1 + fi + - name: Checkout code uses: actions/checkout@v4 From ff06b6dcc1dde8fb750191e07ba476b2de0c9927 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 26 Nov 2024 19:43:37 +0100 Subject: [PATCH 2127/2244] Split network macro conditions On Windows, interrupting a socket with shutdown() does not wake up accept() or read() calls, the socket must be closed. Introduce a new macro constant SC_SOCKET_CLOSE_ON_INTERRUPT, distinct of _WIN32, because Windows will not be the only platform exhibiting this behavior. Refs #5536 --- app/src/util/net.c | 46 ++++++++++++++++++++-------------------------- app/src/util/net.h | 33 ++++++++++++++++++++++++--------- 2 files changed, 44 insertions(+), 35 deletions(-) diff --git a/app/src/util/net.c b/app/src/util/net.c index d43d1c7a..d68b0af6 100644 --- a/app/src/util/net.c +++ b/app/src/util/net.c @@ -9,8 +9,6 @@ #ifdef _WIN32 # include typedef int socklen_t; - typedef SOCKET sc_raw_socket; -# define SC_RAW_SOCKET_NONE INVALID_SOCKET #else # include # include @@ -23,8 +21,6 @@ typedef struct sockaddr_in SOCKADDR_IN; typedef struct sockaddr SOCKADDR; typedef struct in_addr IN_ADDR; - typedef int sc_raw_socket; -# define SC_RAW_SOCKET_NONE -1 #endif bool @@ -47,17 +43,26 @@ net_cleanup(void) { #endif } +static inline bool +sc_raw_socket_close(sc_raw_socket raw_sock) { +#ifndef _WIN32 + return !close(raw_sock); +#else + return !closesocket(raw_sock); +#endif +} + static inline sc_socket wrap(sc_raw_socket sock) { -#ifdef _WIN32 - if (sock == INVALID_SOCKET) { +#ifdef SC_SOCKET_CLOSE_ON_INTERRUPT + if (sock == SC_RAW_SOCKET_NONE) { return SC_SOCKET_NONE; } - struct sc_socket_windows *socket = malloc(sizeof(*socket)); + struct sc_socket_wrapper *socket = malloc(sizeof(*socket)); if (!socket) { LOG_OOM(); - closesocket(sock); + sc_raw_socket_close(sock); return SC_SOCKET_NONE; } @@ -72,9 +77,9 @@ wrap(sc_raw_socket sock) { static inline sc_raw_socket unwrap(sc_socket socket) { -#ifdef _WIN32 +#ifdef SC_SOCKET_CLOSE_ON_INTERRUPT if (socket == SC_SOCKET_NONE) { - return INVALID_SOCKET; + return SC_RAW_SOCKET_NONE; } return socket->socket; @@ -83,17 +88,6 @@ unwrap(sc_socket socket) { #endif } -#ifndef HAVE_SOCK_CLOEXEC // avoid unused-function warning -static inline bool -sc_raw_socket_close(sc_raw_socket raw_sock) { -#ifndef _WIN32 - return !close(raw_sock); -#else - return !closesocket(raw_sock); -#endif -} -#endif - #ifndef HAVE_SOCK_CLOEXEC // If SOCK_CLOEXEC does not exist, the flag must be set manually once the // socket is created @@ -248,9 +242,9 @@ net_interrupt(sc_socket socket) { sc_raw_socket raw_sock = unwrap(socket); -#ifdef _WIN32 +#ifdef SC_SOCKET_CLOSE_ON_INTERRUPT if (!atomic_flag_test_and_set(&socket->closed)) { - return !closesocket(raw_sock); + return sc_raw_socket_close(raw_sock); } return true; #else @@ -262,15 +256,15 @@ bool net_close(sc_socket socket) { sc_raw_socket raw_sock = unwrap(socket); -#ifdef _WIN32 +#ifdef SC_SOCKET_CLOSE_ON_INTERRUPT bool ret = true; if (!atomic_flag_test_and_set(&socket->closed)) { - ret = !closesocket(raw_sock); + ret = sc_raw_socket_close(raw_sock); } free(socket); return ret; #else - return !close(raw_sock); + return sc_raw_socket_close(raw_sock); #endif } diff --git a/app/src/util/net.h b/app/src/util/net.h index ea54b793..9f23bac9 100644 --- a/app/src/util/net.h +++ b/app/src/util/net.h @@ -7,21 +7,36 @@ #include #ifdef _WIN32 - # include + typedef SOCKET sc_raw_socket; +# define SC_RAW_SOCKET_NONE INVALID_SOCKET +#else // not _WIN32 +# include + typedef int sc_raw_socket; +# define SC_RAW_SOCKET_NONE -1 +#endif + +#ifdef _WIN32 +// On Windows, shutdown() does not interrupt accept() or read() calls, so +// net_interrupt() must call close() instead, and net_close() must behave +// accordingly. +// This causes a small race condition (once the socket is closed, its +// handle becomes invalid and may in theory be reassigned before another +// thread calls accept() or read()), but it is deemed acceptable as a +// workaround. +# define SC_SOCKET_CLOSE_ON_INTERRUPT +#endif + +#ifdef SC_SOCKET_CLOSE_ON_INTERRUPT # include # define SC_SOCKET_NONE NULL - typedef struct sc_socket_windows { - SOCKET socket; + typedef struct sc_socket_wrapper { + sc_raw_socket socket; atomic_flag closed; } *sc_socket; - -#else // not _WIN32 - -# include +#else # define SC_SOCKET_NONE -1 - typedef int sc_socket; - + typedef sc_raw_socket sc_socket; #endif #define IPV4_LOCALHOST 0x7F000001 From d01373c03c135c0882c49c8bf95006d32fef041c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 27 Nov 2024 10:10:18 +0100 Subject: [PATCH 2128/2244] Enable close-on-interrupt for macOS This behavior is also necessary on macOS. Fixes #5536 --- app/src/util/net.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/util/net.h b/app/src/util/net.h index 9f23bac9..94789954 100644 --- a/app/src/util/net.h +++ b/app/src/util/net.h @@ -16,10 +16,10 @@ # define SC_RAW_SOCKET_NONE -1 #endif -#ifdef _WIN32 -// On Windows, shutdown() does not interrupt accept() or read() calls, so -// net_interrupt() must call close() instead, and net_close() must behave -// accordingly. +#if defined(_WIN32) || defined(__APPLE__) +// On Windows and macOS, shutdown() does not interrupt accept() or read() +// calls, so net_interrupt() must call close() instead, and net_close() must +// behave accordingly. // This causes a small race condition (once the socket is closed, its // handle becomes invalid and may in theory be reassigned before another // thread calls accept() or read()), but it is deemed acceptable as a From b2cdaa4bdce0adc256b87c3271c39a1482817dc6 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 28 Nov 2024 21:27:00 +0100 Subject: [PATCH 2129/2244] Factorize position mapper resolution The code was duplicated for touch and scroll events. Extract it to a private function. Refs #5542 --- .../genymobile/scrcpy/control/Controller.java | 52 +++++++++++-------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index 34c613e6..e6901f4b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -21,6 +21,7 @@ import android.content.IOnPrimaryClipChangedListener; import android.content.Intent; import android.os.Build; import android.os.SystemClock; +import android.util.Pair; import android.view.InputDevice; import android.view.KeyCharacterMap; import android.view.KeyEvent; @@ -350,24 +351,36 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { return successCount; } - private boolean injectTouch(int action, long pointerId, Position position, float pressure, int actionButton, int buttons) { - long now = SystemClock.uptimeMillis(); - + private Pair getEventPointAndDisplayId(Position position) { // it hides the field on purpose, to read it with atomic access @SuppressWarnings("checkstyle:HiddenField") DisplayData displayData = this.displayData.get(); - assert displayData != null : "Cannot receive a touch event without a display"; + assert displayData != null : "Cannot receive a positional event without a display"; Point point = displayData.positionMapper.map(position); if (point == null) { if (Ln.isEnabled(Ln.Level.VERBOSE)) { Size eventSize = position.getScreenSize(); Size currentSize = displayData.positionMapper.getVideoSize(); - Ln.v("Ignore touch event generated for size " + eventSize + " (current size is " + currentSize + ")"); + Ln.v("Ignore positional event generated for size " + eventSize + " (current size is " + currentSize + ")"); } + return null; + } + + return Pair.create(point, displayData.virtualDisplayId); + } + + private boolean injectTouch(int action, long pointerId, Position position, float pressure, int actionButton, int buttons) { + long now = SystemClock.uptimeMillis(); + + Pair pair = getEventPointAndDisplayId(position); + if (pair == null) { return false; } + Point point = pair.first; + int targetDisplayId = pair.second; + int pointerIndex = pointersState.getPointerIndex(pointerId); if (pointerIndex == -1) { Ln.w("Too many pointers for touch event"); @@ -421,7 +434,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { // First button pressed: ACTION_DOWN MotionEvent downEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_DOWN, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0); - if (!Device.injectEvent(downEvent, displayData.virtualDisplayId, Device.INJECT_MODE_ASYNC)) { + if (!Device.injectEvent(downEvent, targetDisplayId, Device.INJECT_MODE_ASYNC)) { return false; } } @@ -432,7 +445,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { if (!InputManager.setActionButton(pressEvent, actionButton)) { return false; } - if (!Device.injectEvent(pressEvent, displayData.virtualDisplayId, Device.INJECT_MODE_ASYNC)) { + if (!Device.injectEvent(pressEvent, targetDisplayId, Device.INJECT_MODE_ASYNC)) { return false; } @@ -446,7 +459,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { if (!InputManager.setActionButton(releaseEvent, actionButton)) { return false; } - if (!Device.injectEvent(releaseEvent, displayData.virtualDisplayId, Device.INJECT_MODE_ASYNC)) { + if (!Device.injectEvent(releaseEvent, targetDisplayId, Device.INJECT_MODE_ASYNC)) { return false; } @@ -454,7 +467,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { // Last button released: ACTION_UP MotionEvent upEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_UP, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0); - if (!Device.injectEvent(upEvent, displayData.virtualDisplayId, Device.INJECT_MODE_ASYNC)) { + if (!Device.injectEvent(upEvent, targetDisplayId, Device.INJECT_MODE_ASYNC)) { return false; } } @@ -465,27 +478,20 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { MotionEvent event = MotionEvent.obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0); - return Device.injectEvent(event, displayData.virtualDisplayId, Device.INJECT_MODE_ASYNC); + return Device.injectEvent(event, targetDisplayId, Device.INJECT_MODE_ASYNC); } private boolean injectScroll(Position position, float hScroll, float vScroll, int buttons) { long now = SystemClock.uptimeMillis(); - // it hides the field on purpose, to read it with atomic access - @SuppressWarnings("checkstyle:HiddenField") - DisplayData displayData = this.displayData.get(); - assert displayData != null : "Cannot receive a scroll event without a display"; - - Point point = displayData.positionMapper.map(position); - if (point == null) { - if (Ln.isEnabled(Ln.Level.VERBOSE)) { - Size eventSize = position.getScreenSize(); - Size currentSize = displayData.positionMapper.getVideoSize(); - Ln.v("Ignore scroll event generated for size " + eventSize + " (current size is " + currentSize + ")"); - } + Pair pair = getEventPointAndDisplayId(position); + if (pair == null) { return false; } + Point point = pair.first; + int targetDisplayId = pair.second; + MotionEvent.PointerProperties props = pointerProperties[0]; props.id = 0; @@ -497,7 +503,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { MotionEvent event = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, InputDevice.SOURCE_MOUSE, 0); - return Device.injectEvent(event, displayData.virtualDisplayId, Device.INJECT_MODE_ASYNC); + return Device.injectEvent(event, targetDisplayId, Device.INJECT_MODE_ASYNC); } /** From 3b2b3625e478855392c98367818a360dfba239bf Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 28 Nov 2024 21:10:06 +0100 Subject: [PATCH 2130/2244] Accept positional control events without display The position of touch and scroll must normally be "resolved" with a "position mapper" associated to the display. But to support the injection of such events with scrcpy-server alone without video, handle the case where there is no display. Fixes #5542 --- .../genymobile/scrcpy/control/Controller.java | 29 +++++++++++++------ 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index e6901f4b..a0bdc584 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -355,19 +355,30 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { // it hides the field on purpose, to read it with atomic access @SuppressWarnings("checkstyle:HiddenField") DisplayData displayData = this.displayData.get(); - assert displayData != null : "Cannot receive a positional event without a display"; + // In scrcpy, displayData should never be null (a touch event can only be generated from the client when a video frame is present). + // However, it is possible to send events without video playback when using scrcpy-server alone (except for virtual displays). + assert displayData != null || displayId != Device.DISPLAY_ID_NONE : "Cannot receive a positional event without a display"; - Point point = displayData.positionMapper.map(position); - if (point == null) { - if (Ln.isEnabled(Ln.Level.VERBOSE)) { - Size eventSize = position.getScreenSize(); - Size currentSize = displayData.positionMapper.getVideoSize(); - Ln.v("Ignore positional event generated for size " + eventSize + " (current size is " + currentSize + ")"); + Point point; + int targetDisplayId; + if (displayData != null) { + point = displayData.positionMapper.map(position); + if (point == null) { + if (Ln.isEnabled(Ln.Level.VERBOSE)) { + Size eventSize = position.getScreenSize(); + Size currentSize = displayData.positionMapper.getVideoSize(); + Ln.v("Ignore positional event generated for size " + eventSize + " (current size is " + currentSize + ")"); + } + return null; } - return null; + targetDisplayId = displayData.virtualDisplayId; + } else { + // No display, use the raw coordinates + point = position.getPoint(); + targetDisplayId = displayId; } - return Pair.create(point, displayData.virtualDisplayId); + return Pair.create(point, targetDisplayId); } private boolean injectTouch(int action, long pointerId, Position position, float pressure, int actionButton, int buttons) { From 36574d2ee7b5084a64c54951e7c97f5a46ed0388 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 2 Dec 2024 08:54:22 +0100 Subject: [PATCH 2131/2244] Fix .tar.gz compression The generated .tar.gz releases were in fact non-gzipped tarballs. Fixes #5581 --- release/package_client.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release/package_client.sh b/release/package_client.sh index 1c0bf801..51997e75 100755 --- a/release/package_client.sh +++ b/release/package_client.sh @@ -40,7 +40,7 @@ case "$FORMAT" in zip -r "$OUTPUT_DIR/$TARGET_DIRNAME.zip" "$TARGET_DIRNAME" ;; tar.gz) - tar cvf "$OUTPUT_DIR/$TARGET_DIRNAME.tar.gz" "$TARGET_DIRNAME" + tar cvzf "$OUTPUT_DIR/$TARGET_DIRNAME.tar.gz" "$TARGET_DIRNAME" ;; *) echo "Invalid format (expected zip or tar.gz): $FORMAT" >&2 From 0fd7534bd5a5bb3e3556b964f58a4082d941fa81 Mon Sep 17 00:00:00 2001 From: Genxster1998 Date: Mon, 25 Nov 2024 22:35:03 +0530 Subject: [PATCH 2132/2244] Add method to get executable path on MacOS PR #5560 Signed-off-by: Romain Vimont --- app/src/sys/unix/file.c | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/app/src/sys/unix/file.c b/app/src/sys/unix/file.c index 9c3f7333..6123c788 100644 --- a/app/src/sys/unix/file.c +++ b/app/src/sys/unix/file.c @@ -6,6 +6,9 @@ #include #include #include +#ifdef __APPLE__ +# include // for _NSGetExecutablePath() +#endif #include "util/log.h" @@ -60,11 +63,22 @@ sc_file_get_executable_path(void) { } buf[len] = '\0'; return strdup(buf); +#elif defined(__APPLE__) + char buf[PATH_MAX]; + uint32_t bufsize = PATH_MAX; + if (_NSGetExecutablePath(buf, &bufsize) != 0) { + LOGE("Executable path buffer too small; need %u bytes", bufsize); + return NULL; + } + return realpath(buf, NULL); #else - // in practice, we only need this feature for portable builds, only used on - // Windows, so we don't care implementing it for every platform - // (it's useful to have a working version on Linux for debugging though) - return NULL; + // "_" is often used to store the full path of the command being executed + char *path = getenv("_"); + if (!path) { + LOGE("Could not determine executable path"); + return NULL; + } + return strdup(path); #endif } From 131372d2c4b430b01b137e2a0debb21a33963bba Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 30 Nov 2024 16:10:48 +0100 Subject: [PATCH 2133/2244] Expose sc_get_env() to read environment variable Contrary to getenv(), sc_get_env() returns an allocated string that is guaranteed to be encoded in UTF-8 on all platforms (it uses _wgetenv() internally on Windows and converts the strings). PR #5560 --- app/meson.build | 1 + app/src/icon.c | 22 +++++----------------- app/src/server.c | 22 +++++----------------- app/src/util/env.c | 29 +++++++++++++++++++++++++++++ app/src/util/env.h | 12 ++++++++++++ 5 files changed, 52 insertions(+), 34 deletions(-) create mode 100644 app/src/util/env.c create mode 100644 app/src/util/env.h diff --git a/app/meson.build b/app/meson.build index f089ffb1..be02ebc1 100644 --- a/app/meson.build +++ b/app/meson.build @@ -46,6 +46,7 @@ src = [ 'src/util/acksync.c', 'src/util/audiobuf.c', 'src/util/average.c', + 'src/util/env.c', 'src/util/file.c', 'src/util/intmap.c', 'src/util/intr.c', diff --git a/app/src/icon.c b/app/src/icon.c index a76a85c9..4f3a9a39 100644 --- a/app/src/icon.c +++ b/app/src/icon.c @@ -9,6 +9,7 @@ #include "config.h" #include "compat.h" +#include "util/env.h" #include "util/file.h" #include "util/log.h" #include "util/str.h" @@ -19,35 +20,22 @@ static char * get_icon_path(void) { -#ifdef __WINDOWS__ - const wchar_t *icon_path_env = _wgetenv(L"SCRCPY_ICON_PATH"); -#else - const char *icon_path_env = getenv("SCRCPY_ICON_PATH"); -#endif - if (icon_path_env) { + char *icon_path = sc_get_env("SCRCPY_ICON_PATH"); + if (icon_path) { // if the envvar is set, use it -#ifdef __WINDOWS__ - char *icon_path = sc_str_from_wchars(icon_path_env); -#else - char *icon_path = strdup(icon_path_env); -#endif - if (!icon_path) { - LOG_OOM(); - return NULL; - } LOGD("Using SCRCPY_ICON_PATH: %s", icon_path); return icon_path; } #ifndef PORTABLE LOGD("Using icon: " SCRCPY_DEFAULT_ICON_PATH); - char *icon_path = strdup(SCRCPY_DEFAULT_ICON_PATH); + icon_path = strdup(SCRCPY_DEFAULT_ICON_PATH); if (!icon_path) { LOG_OOM(); return NULL; } #else - char *icon_path = sc_file_get_local_path(SCRCPY_PORTABLE_ICON_FILENAME); + icon_path = sc_file_get_local_path(SCRCPY_PORTABLE_ICON_FILENAME); if (!icon_path) { LOGE("Could not get icon path"); return NULL; diff --git a/app/src/server.c b/app/src/server.c index 584a3c34..fe55baa2 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -9,6 +9,7 @@ #include "adb/adb.h" #include "util/binary.h" +#include "util/env.h" #include "util/file.h" #include "util/log.h" #include "util/net_intr.h" @@ -25,35 +26,22 @@ 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) { + char *server_path = sc_get_env("SCRCPY_SERVER_PATH"); + if (server_path) { // if the envvar is set, use it -#ifdef __WINDOWS__ - char *server_path = sc_str_from_wchars(server_path_env); -#else - char *server_path = strdup(server_path_env); -#endif - if (!server_path) { - LOG_OOM(); - return NULL; - } LOGD("Using SCRCPY_SERVER_PATH: %s", server_path); return server_path; } #ifndef PORTABLE LOGD("Using server: " SC_SERVER_PATH_DEFAULT); - char *server_path = strdup(SC_SERVER_PATH_DEFAULT); + server_path = strdup(SC_SERVER_PATH_DEFAULT); if (!server_path) { LOG_OOM(); return NULL; } #else - char *server_path = sc_file_get_local_path(SC_SERVER_FILENAME); + server_path = sc_file_get_local_path(SC_SERVER_FILENAME); if (!server_path) { LOGE("Could not get local file path, " "using " SC_SERVER_FILENAME " from current directory"); diff --git a/app/src/util/env.c b/app/src/util/env.c new file mode 100644 index 00000000..1128e5ea --- /dev/null +++ b/app/src/util/env.c @@ -0,0 +1,29 @@ +#include "env.h" + +#include +#include +#include "util/str.h" + +char * +sc_get_env(const char *varname) { +#ifdef _WIN32 + wchar_t *w_varname = sc_str_to_wchars(varname); + if (!w_varname) { + return NULL; + } + const wchar_t *value = _wgetenv(w_varname); + free(w_varname); + if (!value) { + return NULL; + } + + return sc_str_from_wchars(value); +#else + const char *value = getenv(varname); + if (!value) { + return NULL; + } + + return strdup(value); +#endif +} diff --git a/app/src/util/env.h b/app/src/util/env.h new file mode 100644 index 00000000..50a31165 --- /dev/null +++ b/app/src/util/env.h @@ -0,0 +1,12 @@ +#ifndef SC_ENV_H +#define SC_ENV_H + +#include "common.h" + +// Return the value of the environment variable (may be NULL). +// +// The returned value must be freed by the caller. +char * +sc_get_env(const char *varname); + +#endif From beee42fb065a4852082e3f949cb4ee20c184f104 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 1 Dec 2024 15:47:40 +0100 Subject: [PATCH 2134/2244] Load ADB value using sc_get_env() Contrary to getenv(), the result of sc_get_env() is encoded in UTF-8 on all platforms. Since it is allocated, it requires an explicit init() and destroy() functions. PR #5560 --- app/src/adb/adb.c | 29 +++++++++++++++++++++++------ app/src/adb/adb.h | 6 ++++++ app/src/server.c | 12 +++++++++++- app/src/usb/scrcpy_otg.c | 11 ++++++++--- 4 files changed, 48 insertions(+), 10 deletions(-) diff --git a/app/src/adb/adb.c b/app/src/adb/adb.c index b3e90b2f..4bb209be 100644 --- a/app/src/adb/adb.c +++ b/app/src/adb/adb.c @@ -7,6 +7,7 @@ #include "adb_device.h" #include "adb_parser.h" +#include "util/env.h" #include "util/file.h" #include "util/log.h" #include "util/process_intr.h" @@ -24,15 +25,31 @@ */ #define SC_ADB_COMMAND(...) { sc_adb_get_executable(), __VA_ARGS__, NULL } -static const char *adb_executable; +static char *adb_executable; + +bool +sc_adb_init(void) { + adb_executable = sc_get_env("ADB"); + if (adb_executable) { + return true; + } + + adb_executable = strdup("adb"); + if (!adb_executable) { + LOG_OOM(); + return false; + } + + return true; +} + +void +sc_adb_destroy(void) { + free(adb_executable); +} const char * sc_adb_get_executable(void) { - if (!adb_executable) { - adb_executable = getenv("ADB"); - if (!adb_executable) - adb_executable = "adb"; - } return adb_executable; } diff --git a/app/src/adb/adb.h b/app/src/adb/adb.h index 0292dea1..43310fb9 100644 --- a/app/src/adb/adb.h +++ b/app/src/adb/adb.h @@ -15,6 +15,12 @@ #define SC_ADB_SILENT (SC_ADB_NO_STDOUT | SC_ADB_NO_STDERR | SC_ADB_NO_LOGERR) +bool +sc_adb_init(void); + +void +sc_adb_destroy(void); + const char * sc_adb_get_executable(void); diff --git a/app/src/server.c b/app/src/server.c index fe55baa2..923b5671 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -485,14 +485,21 @@ sc_server_init(struct sc_server *server, const struct sc_server_params *params, // end of the program server->params = *params; - bool ok = sc_mutex_init(&server->mutex); + bool ok = sc_adb_init(); if (!ok) { return false; } + ok = sc_mutex_init(&server->mutex); + if (!ok) { + sc_adb_destroy(); + return false; + } + ok = sc_cond_init(&server->cond_stopped); if (!ok) { sc_mutex_destroy(&server->mutex); + sc_adb_destroy(); return false; } @@ -500,6 +507,7 @@ sc_server_init(struct sc_server *server, const struct sc_server_params *params, if (!ok) { sc_cond_destroy(&server->cond_stopped); sc_mutex_destroy(&server->mutex); + sc_adb_destroy(); return false; } @@ -1141,4 +1149,6 @@ sc_server_destroy(struct sc_server *server) { sc_intr_destroy(&server->intr); sc_cond_destroy(&server->cond_stopped); sc_mutex_destroy(&server->mutex); + + sc_adb_destroy(); } diff --git a/app/src/usb/scrcpy_otg.c b/app/src/usb/scrcpy_otg.c index 1a7e9544..6ef2fc2a 100644 --- a/app/src/usb/scrcpy_otg.c +++ b/app/src/usb/scrcpy_otg.c @@ -95,9 +95,14 @@ scrcpy_otg(struct scrcpy_options *options) { // On Windows, only one process could open a USB device // LOGI("Killing adb server (if any)..."); - unsigned flags = SC_ADB_NO_STDOUT | SC_ADB_NO_STDERR | SC_ADB_NO_LOGERR; - // uninterruptible (intr == NULL), but in practice it's very quick - sc_adb_kill_server(NULL, flags); + if (sc_adb_init()) { + unsigned flags = SC_ADB_NO_STDOUT | SC_ADB_NO_STDERR | SC_ADB_NO_LOGERR; + // uninterruptible (intr == NULL), but in practice it's very quick + sc_adb_kill_server(NULL, flags); + sc_adb_destroy(); + } else { + LOGW("Could not call adb executable, adb server not killed"); + } #endif static const struct sc_usb_callbacks cbs = { From 6d0ac3626dbc8a5ffa6693a00f370f4f93660cd8 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 1 Dec 2024 15:48:50 +0100 Subject: [PATCH 2135/2244] Use local adb in portable builds For non-Windows portable builds, use the absolute path to the adb executable located in the same directory as scrcpy. On Windows, just use "adb", which is sufficient to use the local one. PR #5560 --- app/src/adb/adb.c | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/src/adb/adb.c b/app/src/adb/adb.c index 4bb209be..ce7cdec1 100644 --- a/app/src/adb/adb.c +++ b/app/src/adb/adb.c @@ -34,11 +34,22 @@ sc_adb_init(void) { return true; } +#if !defined(PORTABLE) || defined(_WIN32) adb_executable = strdup("adb"); if (!adb_executable) { LOG_OOM(); return false; } +#else + // For portable builds, use the absolute path to the adb executable + // in the same directory as scrcpy (except on Windows, where "adb" + // is sufficient) + adb_executable = sc_file_get_local_path("adb"); + if (!adb_executable) { + // Error already logged + return false; + } +#endif return true; } From dc6c279b1e500e4c39fd0adcad425e6eab2f6944 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 1 Dec 2024 16:08:03 +0100 Subject: [PATCH 2136/2244] Log adb executable path Log the ADB executable path (at the DEBUG level) if it is not the default one. PR #5560 --- app/src/adb/adb.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/adb/adb.c b/app/src/adb/adb.c index ce7cdec1..ed3b1ea4 100644 --- a/app/src/adb/adb.c +++ b/app/src/adb/adb.c @@ -31,6 +31,7 @@ bool sc_adb_init(void) { adb_executable = sc_get_env("ADB"); if (adb_executable) { + LOGD("Using adb: %s", adb_executable); return true; } @@ -49,6 +50,8 @@ sc_adb_init(void) { // Error already logged return false; } + + LOGD("Using adb (portable): %s", adb_executable); #endif return true; From aea6a371aa3f5278c8b10cf6bec7bbe215ae1518 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 1 Dec 2024 16:00:20 +0100 Subject: [PATCH 2137/2244] Remove scrcpy wrapper script for static builds All portable builds now use the files located in the same directory as the scrcpy executable by default. PR #5560 --- app/data/scrcpy_static_wrapper.sh | 6 ------ release/build_linux.sh | 3 +-- release/build_macos.sh | 3 +-- 3 files changed, 2 insertions(+), 10 deletions(-) delete mode 100755 app/data/scrcpy_static_wrapper.sh diff --git a/app/data/scrcpy_static_wrapper.sh b/app/data/scrcpy_static_wrapper.sh deleted file mode 100755 index ac1e7a95..00000000 --- a/app/data/scrcpy_static_wrapper.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash -cd "$(dirname ${BASH_SOURCE[0]})" -export ADB="${ADB:-./adb}" -export SCRCPY_SERVER_PATH="${SCRCPY_SERVER_PATH:-./scrcpy-server}" -export SCRCPY_ICON_PATH="${SCRCPY_ICON_PATH:-./icon.png}" -./scrcpy_bin "$@" diff --git a/release/build_linux.sh b/release/build_linux.sh index 39308828..ccf24575 100755 --- a/release/build_linux.sh +++ b/release/build_linux.sh @@ -36,8 +36,7 @@ ninja -C "$LINUX_BUILD_DIR" # Group intermediate outputs into a 'dist' directory mkdir -p "$LINUX_BUILD_DIR/dist" -cp "$LINUX_BUILD_DIR"/app/scrcpy "$LINUX_BUILD_DIR/dist/scrcpy_bin" +cp "$LINUX_BUILD_DIR"/app/scrcpy "$LINUX_BUILD_DIR/dist/" cp app/data/icon.png "$LINUX_BUILD_DIR/dist/" -cp app/data/scrcpy_static_wrapper.sh "$LINUX_BUILD_DIR/dist/scrcpy" cp app/scrcpy.1 "$LINUX_BUILD_DIR/dist/" cp -r "$ADB_INSTALL_DIR"/. "$LINUX_BUILD_DIR/dist/" diff --git a/release/build_macos.sh b/release/build_macos.sh index 4794d97d..2c41d04e 100755 --- a/release/build_macos.sh +++ b/release/build_macos.sh @@ -36,8 +36,7 @@ ninja -C "$MACOS_BUILD_DIR" # Group intermediate outputs into a 'dist' directory mkdir -p "$MACOS_BUILD_DIR/dist" -cp "$MACOS_BUILD_DIR"/app/scrcpy "$MACOS_BUILD_DIR/dist/scrcpy_bin" +cp "$MACOS_BUILD_DIR"/app/scrcpy "$MACOS_BUILD_DIR/dist/" cp app/data/icon.png "$MACOS_BUILD_DIR/dist/" -cp app/data/scrcpy_static_wrapper.sh "$MACOS_BUILD_DIR/dist/scrcpy" cp app/scrcpy.1 "$MACOS_BUILD_DIR/dist/" cp -r "$ADB_INSTALL_DIR"/. "$MACOS_BUILD_DIR/dist/" From 9555d3a537a828731ad89ef5258ba537acf8cc11 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 3 Dec 2024 23:06:33 +0100 Subject: [PATCH 2138/2244] Retry capture on IOException If the capture fails with an IOException, retry with a lower resolution. Fixes #5539 --- .../main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java index bc120107..1402eceb 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java @@ -112,8 +112,8 @@ public class SurfaceEncoder implements AsyncProcessor { // The capture might have been closed internally (for example if the camera is disconnected) alive = !stopped.get() && !capture.isClosed(); } - } catch (IllegalStateException | IllegalArgumentException e) { - Ln.e("Encoding error: " + e.getClass().getName() + ": " + e.getMessage()); + } catch (IllegalStateException | IllegalArgumentException | IOException e) { + Ln.e("Capture/encoding error: " + e.getClass().getName() + ": " + e.getMessage()); if (!prepareRetry(size)) { throw e; } From b26b4fb7458b9e6c361b60198d38b28fbf5e329d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 4 Dec 2024 12:59:48 +0100 Subject: [PATCH 2139/2244] Document launchers in virtual displays Mention how to start a launcher in a virtual display. Refs #5592 --- doc/virtual_display.md | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/doc/virtual_display.md b/doc/virtual_display.md index 7523c118..5036f35b 100644 --- a/doc/virtual_display.md +++ b/doc/virtual_display.md @@ -15,8 +15,10 @@ scrcpy --new-display=/240 # use the main display size and 240 dpi On some devices, a launcher is available in the virtual display. -When no launcher is available, the virtual display is empty. In that case, you -must [start an Android app](device.md#start-android-app). +When no launcher is available (or if is explicitly disabled by +[`--no-vd-system-decorations`](#system-decorations)), the virtual display is +empty. In that case, you must [start an Android +app](device.md#start-android-app). For example: @@ -24,12 +26,27 @@ For example: scrcpy --new-display=1920x1080 --start-app=org.videolan.vlc ``` +The app may itself be a launcher. For example, to run the open source [Fossify +Launcher]: + +```bash +scrcpy --new-display=1920x1080 --no-vd-system-decorations --start-app=org.fossify.home +``` + +[Fossify Launcher]: https://f-droid.org/en/packages/org.fossify.home/ + + ## System decorations -By default, virtual display system decorations are enabled. But some devices -might display a broken UI; +By default, virtual display system decorations are enabled. To disable them, use +`--no-vd-system-decorations`: -Use `--no-vd-system-decorations` to disable it. +``` +scrcpy --new-display --no-vd-system-decorations +``` + +This is useful for some devices which might display a broken UI, or to disable +any default launcher UI available in virtual displays. Note that if no app is started, no content will be rendered, so no video frame will be produced at all. From 0e473eb0051e7210e10fb9acef92dad3b956ca1f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 4 Dec 2024 13:11:43 +0100 Subject: [PATCH 2140/2244] Reset TCP/IP connection with a '+' prefix When running scrcpy with --tcpip=xx.xx.xx.xx, to make sure a new working connection is established, it was first disconnected by a call to: adb disconnect However, this caused all running instances connected to that address to be killed. Running several instances of scrcpy on the same device is now useful with virtual displays, so change the default behavior to NOT disconnect. To force a reconnection, a '+' prefix can be added: scrcpy --tcpip=+192.168.0.x Fixes #5562 --- app/scrcpy.1 | 6 ++++-- app/src/adb/adb.c | 5 +++-- app/src/cli.c | 7 ++++--- app/src/server.c | 23 ++++++++++++++++------- doc/connection.md | 6 ++++++ 5 files changed, 33 insertions(+), 14 deletions(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index c513dc9a..326cb23f 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -518,13 +518,15 @@ Enable "show touches" on start, restore the initial value on exit. It only shows physical touches (not clicks from scrcpy). .TP -.BI "\-\-tcpip\fR[=\fIip\fR[:\fIport\fR]] -Configure and reconnect the device over TCP/IP. +.BI "\-\-tcpip\fR[=[+]\fIip\fR[:\fIport\fR]] +Configure and connect the device over TCP/IP. If a destination address is provided, then scrcpy connects to this address before starting. The device must listen on the given TCP port (default is 5555). If no destination address is provided, then scrcpy attempts to find the IP address and adb port of the current device (typically connected over USB), enables TCP/IP mode if necessary, then connects to this address before starting. +Prefix the address with a '+' to force a reconnection. + .TP .BI "\-\-time\-limit " seconds Set the maximum mirroring time, in seconds. diff --git a/app/src/adb/adb.c b/app/src/adb/adb.c index ed3b1ea4..0cd3c0fd 100644 --- a/app/src/adb/adb.c +++ b/app/src/adb/adb.c @@ -412,7 +412,7 @@ sc_adb_connect(struct sc_intr *intr, const char *ip_port, unsigned flags) { // "adb connect" always returns successfully (with exit code 0), even in // case of failure. As a workaround, check if its output starts with - // "connected". + // "connected" or "already connected". char buf[128]; ssize_t r = sc_pipe_read_all_intr(intr, pid, pout, buf, sizeof(buf) - 1); sc_pipe_close(pout); @@ -429,7 +429,8 @@ sc_adb_connect(struct sc_intr *intr, const char *ip_port, unsigned flags) { assert((size_t) r < sizeof(buf)); buf[r] = '\0'; - ok = !strncmp("connected", buf, sizeof("connected") - 1); + ok = !strncmp("connected", buf, sizeof("connected") - 1) + || !strncmp("already connected", buf, sizeof("already connected") - 1); if (!ok && !(flags & SC_ADB_NO_STDERR)) { // "adb connect" also prints errors to stdout. Since we capture it, // re-print the error to stderr. diff --git a/app/src/cli.c b/app/src/cli.c index ee86b34b..fa46c4e4 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -860,16 +860,17 @@ static const struct sc_option options[] = { { .longopt_id = OPT_TCPIP, .longopt = "tcpip", - .argdesc = "ip[:port]", + .argdesc = "[+]ip[:port]", .optional_arg = true, - .text = "Configure and reconnect the device over TCP/IP.\n" + .text = "Configure and connect the device over TCP/IP.\n" "If a destination address is provided, then scrcpy connects to " "this address before starting. The device must listen on the " "given TCP port (default is 5555).\n" "If no destination address is provided, then scrcpy attempts " "to find the IP address of the current device (typically " "connected over USB), enables TCP/IP mode, then connects to " - "this address before starting.", + "this address before starting.\n" + "Prefix the address with a '+' to force a reconnection.", }, { .longopt_id = OPT_TIME_LIMIT, diff --git a/app/src/server.c b/app/src/server.c index 923b5671..8bdf9501 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -829,11 +829,14 @@ sc_server_switch_to_tcpip(struct sc_server *server, const char *serial) { } static bool -sc_server_connect_to_tcpip(struct sc_server *server, const char *ip_port) { +sc_server_connect_to_tcpip(struct sc_server *server, const char *ip_port, + bool disconnect) { struct sc_intr *intr = &server->intr; - // Error expected if not connected, do not report any error - sc_adb_disconnect(intr, ip_port, SC_ADB_SILENT); + if (disconnect) { + // Error expected if not connected, do not report any error + sc_adb_disconnect(intr, ip_port, SC_ADB_SILENT); + } LOGI("Connecting to %s...", ip_port); @@ -849,7 +852,7 @@ sc_server_connect_to_tcpip(struct sc_server *server, const char *ip_port) { static bool sc_server_configure_tcpip_known_address(struct sc_server *server, - const char *addr) { + const char *addr, bool disconnect) { // Append ":5555" if no port is present bool contains_port = strchr(addr, ':'); char *ip_port = contains_port ? strdup(addr) @@ -860,7 +863,7 @@ sc_server_configure_tcpip_known_address(struct sc_server *server, } server->serial = ip_port; - return sc_server_connect_to_tcpip(server, ip_port); + return sc_server_connect_to_tcpip(server, ip_port, disconnect); } static bool @@ -885,7 +888,7 @@ sc_server_configure_tcpip_unknown_address(struct sc_server *server, } server->serial = ip_port; - return sc_server_connect_to_tcpip(server, ip_port); + return sc_server_connect_to_tcpip(server, ip_port, false); } static void @@ -972,7 +975,13 @@ run_server(void *data) { sc_adb_device_destroy(&device); } } else { - ok = sc_server_configure_tcpip_known_address(server, params->tcpip_dst); + // If the user passed a '+' (--tcpip=+ip), then disconnect first + const char *tcpip_dst = params->tcpip_dst; + bool plus = tcpip_dst[0] == '+'; + if (plus) { + ++tcpip_dst; + } + ok = sc_server_configure_tcpip_known_address(server, tcpip_dst, plus); if (!ok) { goto error_connection_failed; } diff --git a/doc/connection.md b/doc/connection.md index 17efbbdc..2c3d37e1 100644 --- a/doc/connection.md +++ b/doc/connection.md @@ -85,6 +85,12 @@ scrcpy --tcpip=192.168.1.1 # default port is 5555 scrcpy --tcpip=192.168.1.1:5555 ``` +Prefix the address with a '+' to force a reconnection: + +```bash +scrcpy --tcpip=+192.168.1.1 +``` + ### Manual From 5c3626ed47b983625481f852073cef859e7fcaf9 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 4 Dec 2024 18:38:23 +0100 Subject: [PATCH 2141/2244] Handle broken pipe errors specifically Since 9555d3a537a828731ad89ef5258ba537acf8cc11, a capture/encoding error was sometimes logged on exit. --- server/src/main/java/com/genymobile/scrcpy/util/IO.java | 4 ++++ .../main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/server/src/main/java/com/genymobile/scrcpy/util/IO.java b/server/src/main/java/com/genymobile/scrcpy/util/IO.java index b953f290..16ddaedd 100644 --- a/server/src/main/java/com/genymobile/scrcpy/util/IO.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/IO.java @@ -72,4 +72,8 @@ public final class IO { Throwable cause = e.getCause(); return cause instanceof ErrnoException && ((ErrnoException) cause).errno == OsConstants.EPIPE; } + + public static boolean isBrokenPipe(Exception e) { + return e instanceof IOException && isBrokenPipe((IOException) e); + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java index 1402eceb..236a5f48 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java @@ -113,6 +113,10 @@ public class SurfaceEncoder implements AsyncProcessor { alive = !stopped.get() && !capture.isClosed(); } } catch (IllegalStateException | IllegalArgumentException | IOException e) { + if (IO.isBrokenPipe(e)) { + // Do not retry on broken pipe, which is expected on close because the socket is closed by the client + throw e; + } Ln.e("Capture/encoding error: " + e.getClass().getName() + ": " + e.getMessage()); if (!prepareRetry(size)) { throw e; From 5febb1e9fba646d8fe76f891fed001c49ed59b0d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 4 Dec 2024 20:29:21 +0100 Subject: [PATCH 2142/2244] Update links to 3.0.1 --- README.md | 2 +- doc/build.md | 6 +++--- doc/linux.md | 6 +++--- doc/macos.md | 10 +++++++--- doc/windows.md | 12 ++++++------ install_release.sh | 4 ++-- 6 files changed, 22 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 85023a1d..ba4730af 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ source for the project. Do not download releases from random websites, even if their name contains `scrcpy`.** -# scrcpy (v3.0) +# scrcpy (v3.0.1) scrcpy diff --git a/doc/build.md b/doc/build.md index 43841268..2d1b4763 100644 --- a/doc/build.md +++ b/doc/build.md @@ -233,10 +233,10 @@ install` must be run as root)._ #### Option 2: Use prebuilt server - - [`scrcpy-server-v3.0`][direct-scrcpy-server] - SHA-256: `800044c62a94d5fc16f5ab9c86d45b1050eae3eb436514d1b0d2fe2646b894ea` + - [`scrcpy-server-v3.0.1`][direct-scrcpy-server] + SHA-256: `86c4ef31f5acb060a24d5d63d8dd262ef83384e19ae5f9ad78e6408a50743d17` -[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v3.0/scrcpy-server-v3.0 +[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v3.0.1/scrcpy-server-v3.0.1 Download the prebuilt server somewhere, and specify its path during the Meson configuration: diff --git a/doc/linux.md b/doc/linux.md index 79cbb286..4446c8c9 100644 --- a/doc/linux.md +++ b/doc/linux.md @@ -6,11 +6,11 @@ Download a static build of the [latest release]: - - [`scrcpy-linux-v3.0.tar.gz`][direct-linux] (x86_64) - SHA-256: `06cb74e22f758228c944cea048b78e42b2925c2affe2b5aca901cfd6a649e503` + - [`scrcpy-linux-x86_64-v3.0.1.tar.gz`][direct-linux-x86_64] (x86_64) + SHA-256: `6cb7fb16efbe3afd6db19b1ee31ee9f6e104a4735dc1f41c4a478cabbeac3f77` [latest release]: https://github.com/Genymobile/scrcpy/releases/latest -[direct-linux]: https://github.com/Genymobile/scrcpy/releases/download/v3.0/scrcpy-linux-v3.0.tar.gz +[direct-linux-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.0.1/scrcpy-linux-x86_64-v3.0.1.tar.gz and extract it. diff --git a/doc/macos.md b/doc/macos.md index ee3c23be..b1a1e2fc 100644 --- a/doc/macos.md +++ b/doc/macos.md @@ -6,11 +6,15 @@ Download a static build of the [latest release]: - - [`scrcpy-macos-v3.0.tar.gz`][direct-macos] (arm64) - SHA-256: `5db9821918537eb3aaf0333cdd05baf85babdd851972d5f1b71f86da0530b4bf` + - [`scrcpy-macos-aarch64-v3.0.1.tar.gz`][direct-macos-aarch64] (aarch64) + SHA-256: `e1af70898b6881b3e714ee0e15a7664bfab5eda3ea27c101163a09a36e1df753` + + - [`scrcpy-macos-x86_64-v3.0.1.tar.gz`][direct-macos-x86_64] (x86_64) + SHA-256: `d6f9fad290e0142a6dfb0a405a8d1bfbe1698bbb146c1c0c33e38da53762e442` [latest release]: https://github.com/Genymobile/scrcpy/releases/latest -[direct-macos]: https://github.com/Genymobile/scrcpy/releases/download/v3.0/scrcpy-macos-v3.0.tar.gz +[direct-macos-aarch64]: https://github.com/Genymobile/scrcpy/releases/download/v3.0.1/scrcpy-macos-aarch64-v3.0.1.tar.gz +[direct-macos-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.0.1/scrcpy-macos-x86_64-v3.0.1.tar.gz and extract it. diff --git a/doc/windows.md b/doc/windows.md index 330b4fbd..115100e6 100644 --- a/doc/windows.md +++ b/doc/windows.md @@ -6,14 +6,14 @@ Download the [latest release]: - - [`scrcpy-win64-v3.0.zip`][direct-win64] (64-bit) - SHA-256: `dfbe8a8fef6535197acc506936bfd59d0aa0427e9b44fb2e5c550eae642f72be` - - [`scrcpy-win32-v3.0.zip`][direct-win32] (32-bit) - SHA-256: `7cbf8d7a6ebfdca7b3b161e29a481c11088305f3e0a89d28e8e62f70c7bd0028` + - [`scrcpy-win64-v3.0.1.zip`][direct-win64] (64-bit) + SHA-256: `2d2485cead6bb9d80ec337a660a571fc4b3c2e15034ad73c6a2867442206a5f4` + - [`scrcpy-win32-v3.0.1.zip`][direct-win32] (32-bit) + SHA-256: `43296f8bf34dd408c65463d45ca367febe68cec3ae34b78917a8f3ecbf321829` [latest release]: https://github.com/Genymobile/scrcpy/releases/latest -[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v3.0/scrcpy-win64-v3.0.zip -[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v3.0/scrcpy-win32-v3.0.zip +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v3.0.1/scrcpy-win64-v3.0.1.zip +[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v3.0.1/scrcpy-win32-v3.0.1.zip and extract it. diff --git a/install_release.sh b/install_release.sh index 46b7dd43..0bb42035 100755 --- a/install_release.sh +++ b/install_release.sh @@ -2,8 +2,8 @@ set -e BUILDDIR=build-auto -PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v3.0/scrcpy-server-v3.0 -PREBUILT_SERVER_SHA256=800044c62a94d5fc16f5ab9c86d45b1050eae3eb436514d1b0d2fe2646b894ea +PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v3.0.1/scrcpy-server-v3.0.1 +PREBUILT_SERVER_SHA256=86c4ef31f5acb060a24d5d63d8dd262ef83384e19ae5f9ad78e6408a50743d17 echo "[scrcpy] Downloading prebuilt server..." wget "$PREBUILT_SERVER_URL" -O scrcpy-server From 2ed2247e8fe90eefe6070384aa9895a70569e1b6 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 4 Dec 2024 22:35:25 +0100 Subject: [PATCH 2143/2244] Bump version to 3.0.2 The version was not bumped for 3.0.1. --- app/scrcpy-windows.rc | 2 +- meson.build | 2 +- server/build.gradle | 4 ++-- server/build_without_gradle.sh | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/scrcpy-windows.rc b/app/scrcpy-windows.rc index b80e01b9..0f1caf87 100644 --- a/app/scrcpy-windows.rc +++ b/app/scrcpy-windows.rc @@ -13,7 +13,7 @@ BEGIN VALUE "LegalCopyright", "Romain Vimont, Genymobile" VALUE "OriginalFilename", "scrcpy.exe" VALUE "ProductName", "scrcpy" - VALUE "ProductVersion", "3.0" + VALUE "ProductVersion", "3.0.2" END END BLOCK "VarFileInfo" diff --git a/meson.build b/meson.build index b3ad3c75..badc1adb 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('scrcpy', 'c', - version: '3.0', + version: '3.0.2', meson_version: '>= 0.48', default_options: [ 'c_std=c11', diff --git a/server/build.gradle b/server/build.gradle index 72c74a5a..4b7b0254 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -7,8 +7,8 @@ android { applicationId "com.genymobile.scrcpy" minSdkVersion 21 targetSdkVersion 35 - versionCode 30000 - versionName "3.0" + versionCode 30002 + versionName "3.0.2" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index d0572615..f2420f64 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=3.0 +SCRCPY_VERSION_NAME=3.0.2 PLATFORM=${ANDROID_PLATFORM:-35} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-35.0.0} From baa10ed0a36ec775712be85f22d3db3f0a6e19f2 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 4 Dec 2024 22:48:27 +0100 Subject: [PATCH 2144/2244] Update links to 3.0.2 --- README.md | 2 +- doc/build.md | 6 +++--- doc/linux.md | 6 +++--- doc/macos.md | 12 ++++++------ doc/windows.md | 12 ++++++------ install_release.sh | 4 ++-- 6 files changed, 21 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index ba4730af..601085da 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ source for the project. Do not download releases from random websites, even if their name contains `scrcpy`.** -# scrcpy (v3.0.1) +# scrcpy (v3.0.2) scrcpy diff --git a/doc/build.md b/doc/build.md index 2d1b4763..20d1f0f5 100644 --- a/doc/build.md +++ b/doc/build.md @@ -233,10 +233,10 @@ install` must be run as root)._ #### Option 2: Use prebuilt server - - [`scrcpy-server-v3.0.1`][direct-scrcpy-server] - SHA-256: `86c4ef31f5acb060a24d5d63d8dd262ef83384e19ae5f9ad78e6408a50743d17` + - [`scrcpy-server-v3.0.2`][direct-scrcpy-server] + SHA-256: `e19fe024bfa3367809494407ad6ca809a6f6e77dac95e99f85ba75144e0ba35d` -[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v3.0.1/scrcpy-server-v3.0.1 +[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v3.0.2/scrcpy-server-v3.0.2 Download the prebuilt server somewhere, and specify its path during the Meson configuration: diff --git a/doc/linux.md b/doc/linux.md index 4446c8c9..db4d7977 100644 --- a/doc/linux.md +++ b/doc/linux.md @@ -6,11 +6,11 @@ Download a static build of the [latest release]: - - [`scrcpy-linux-x86_64-v3.0.1.tar.gz`][direct-linux-x86_64] (x86_64) - SHA-256: `6cb7fb16efbe3afd6db19b1ee31ee9f6e104a4735dc1f41c4a478cabbeac3f77` + - [`scrcpy-linux-x86_64-v3.0.2.tar.gz`][direct-linux-x86_64] (x86_64) + SHA-256: `20b69dcd379bb7d7208bf1e4858cf04162fc856697be0e6c03863d7b3c1e734a` [latest release]: https://github.com/Genymobile/scrcpy/releases/latest -[direct-linux-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.0.1/scrcpy-linux-x86_64-v3.0.1.tar.gz +[direct-linux-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.0.2/scrcpy-linux-x86_64-v3.0.2.tar.gz and extract it. diff --git a/doc/macos.md b/doc/macos.md index b1a1e2fc..af92f6be 100644 --- a/doc/macos.md +++ b/doc/macos.md @@ -6,15 +6,15 @@ Download a static build of the [latest release]: - - [`scrcpy-macos-aarch64-v3.0.1.tar.gz`][direct-macos-aarch64] (aarch64) - SHA-256: `e1af70898b6881b3e714ee0e15a7664bfab5eda3ea27c101163a09a36e1df753` + - [`scrcpy-macos-aarch64-v3.0.2.tar.gz`][direct-macos-aarch64] (aarch64) + SHA-256: `811ba2f4e856146bdd161e24c3490d78efbec2339ca783fac791d041c0aecfb6` - - [`scrcpy-macos-x86_64-v3.0.1.tar.gz`][direct-macos-x86_64] (x86_64) - SHA-256: `d6f9fad290e0142a6dfb0a405a8d1bfbe1698bbb146c1c0c33e38da53762e442` + - [`scrcpy-macos-x86_64-v3.0.2.tar.gz`][direct-macos-x86_64] (x86_64) + SHA-256: `8effff54dca3a3e46eaaec242771a13a7f81af2e18670b3d0d8ed6b461bb4f79` [latest release]: https://github.com/Genymobile/scrcpy/releases/latest -[direct-macos-aarch64]: https://github.com/Genymobile/scrcpy/releases/download/v3.0.1/scrcpy-macos-aarch64-v3.0.1.tar.gz -[direct-macos-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.0.1/scrcpy-macos-x86_64-v3.0.1.tar.gz +[direct-macos-aarch64]: https://github.com/Genymobile/scrcpy/releases/download/v3.0.2/scrcpy-macos-aarch64-v3.0.2.tar.gz +[direct-macos-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.0.2/scrcpy-macos-x86_64-v3.0.2.tar.gz and extract it. diff --git a/doc/windows.md b/doc/windows.md index 115100e6..e0f0a1b3 100644 --- a/doc/windows.md +++ b/doc/windows.md @@ -6,14 +6,14 @@ Download the [latest release]: - - [`scrcpy-win64-v3.0.1.zip`][direct-win64] (64-bit) - SHA-256: `2d2485cead6bb9d80ec337a660a571fc4b3c2e15034ad73c6a2867442206a5f4` - - [`scrcpy-win32-v3.0.1.zip`][direct-win32] (32-bit) - SHA-256: `43296f8bf34dd408c65463d45ca367febe68cec3ae34b78917a8f3ecbf321829` + - [`scrcpy-win64-v3.0.2.zip`][direct-win64] (64-bit) + SHA-256: `f0de59f5d46127c87cd822d39d6665e016b86db4cd048101b262f6adb6766832` + - [`scrcpy-win32-v3.0.2.zip`][direct-win32] (32-bit) + SHA-256: `8db8d4984d642012c55802de71f507f8ff9f68a8cfed456d7a1982d47e065f64` [latest release]: https://github.com/Genymobile/scrcpy/releases/latest -[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v3.0.1/scrcpy-win64-v3.0.1.zip -[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v3.0.1/scrcpy-win32-v3.0.1.zip +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v3.0.2/scrcpy-win64-v3.0.2.zip +[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v3.0.2/scrcpy-win32-v3.0.2.zip and extract it. diff --git a/install_release.sh b/install_release.sh index 0bb42035..5a6eaa7b 100755 --- a/install_release.sh +++ b/install_release.sh @@ -2,8 +2,8 @@ set -e BUILDDIR=build-auto -PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v3.0.1/scrcpy-server-v3.0.1 -PREBUILT_SERVER_SHA256=86c4ef31f5acb060a24d5d63d8dd262ef83384e19ae5f9ad78e6408a50743d17 +PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v3.0.2/scrcpy-server-v3.0.2 +PREBUILT_SERVER_SHA256=e19fe024bfa3367809494407ad6ca809a6f6e77dac95e99f85ba75144e0ba35d echo "[scrcpy] Downloading prebuilt server..." wget "$PREBUILT_SERVER_URL" -O scrcpy-server From 97fa77c76c5502105a3d128a0be2477a04f4fd1b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 4 Dec 2024 23:35:43 +0100 Subject: [PATCH 2145/2244] Inject main display events to the original display When mirroring a secondary display, touch and scroll events must be sent to the mirroring virtual display id (with coordinates relative to the virtual display size), rather than to the original display (with coordinates relative to the original display size). This behavior, introduced by d19396718ee0c0ba7fb578f595a6553c0458da59, was also applied for the main display for consistency. However, it causes some UI elements to become unclickable. To minimize inconveniences, restore the previous behavior when mirroring the main display: send all events to the original display id (0) with coordinates relative to the original display size. Fixes #5545 Fixes #5605 Fixes #5616 Refs #4598 Refs #5137 Refs #5370 PR #5614 --- .../com/genymobile/scrcpy/video/ScreenCapture.java | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java index 47425d09..5d026a73 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java @@ -129,10 +129,18 @@ public class ScreenCapture extends SurfaceCapture { try { virtualDisplay = ServiceManager.getDisplayManager() .createVirtualDisplay("scrcpy", inputSize.getWidth(), inputSize.getHeight(), displayId, surface); - virtualDisplayId = virtualDisplay.getDisplay().getDisplayId(); - // The positions are relative to the virtual display, not the original display (so use inputSize, not deviceSize!) - positionMapper = PositionMapper.create(videoSize, transform, inputSize); + + if (displayId == 0) { + // Main display: send all events to the original display, relative to the device size + Size deviceSize = displayInfo.getSize(); + positionMapper = PositionMapper.create(videoSize, transform, deviceSize); + virtualDisplayId = 0; + } else { + // The positions are relative to the virtual display, not the original display (so use inputSize, not deviceSize!) + positionMapper = PositionMapper.create(videoSize, transform, inputSize); + virtualDisplayId = virtualDisplay.getDisplay().getDisplayId(); + } Ln.d("Display: using DisplayManager API"); } catch (Exception displayManagerException) { try { From f90dc216d10ac062ab1a06e14e574acc5569d2c3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 4 Dec 2024 23:58:22 +0100 Subject: [PATCH 2146/2244] Refactor virtual display properties initialization Following the changes from the previous commit, the behavior is now identical when mirroring the main display or using the SurfaceControl API. Factorize the code to perform the initialization in a single location. Refs #5605 PR #5614 --- .../scrcpy/video/ScreenCapture.java | 30 ++++++++----------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java index 5d026a73..5f4e1803 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java @@ -124,23 +124,9 @@ public class ScreenCapture extends SurfaceCapture { inputSize = videoSize; } - int virtualDisplayId; - PositionMapper positionMapper; try { virtualDisplay = ServiceManager.getDisplayManager() .createVirtualDisplay("scrcpy", inputSize.getWidth(), inputSize.getHeight(), displayId, surface); - - - if (displayId == 0) { - // Main display: send all events to the original display, relative to the device size - Size deviceSize = displayInfo.getSize(); - positionMapper = PositionMapper.create(videoSize, transform, deviceSize); - virtualDisplayId = 0; - } else { - // The positions are relative to the virtual display, not the original display (so use inputSize, not deviceSize!) - positionMapper = PositionMapper.create(videoSize, transform, inputSize); - virtualDisplayId = virtualDisplay.getDisplay().getDisplayId(); - } Ln.d("Display: using DisplayManager API"); } catch (Exception displayManagerException) { try { @@ -148,11 +134,7 @@ public class ScreenCapture extends SurfaceCapture { Size deviceSize = displayInfo.getSize(); int layerStack = displayInfo.getLayerStack(); - setDisplaySurface(display, surface, deviceSize.toRect(), inputSize.toRect(), layerStack); - virtualDisplayId = displayId; - - positionMapper = PositionMapper.create(videoSize, transform, deviceSize); Ln.d("Display: using SurfaceControl API"); } catch (Exception surfaceControlException) { Ln.e("Could not create display using DisplayManager", displayManagerException); @@ -162,6 +144,18 @@ public class ScreenCapture extends SurfaceCapture { } if (vdListener != null) { + int virtualDisplayId; + PositionMapper positionMapper; + if (virtualDisplay == null || displayId == 0) { + // Surface control or main display: send all events to the original display, relative to the device size + Size deviceSize = displayInfo.getSize(); + positionMapper = PositionMapper.create(videoSize, transform, deviceSize); + virtualDisplayId = displayId; + } else { + // The positions are relative to the virtual display, not the original display (so use inputSize, not deviceSize!) + positionMapper = PositionMapper.create(videoSize, transform, inputSize); + virtualDisplayId = virtualDisplay.getDisplay().getDisplayId(); + } vdListener.onNewVirtualDisplay(virtualDisplayId, positionMapper); } } From 988174805c1942c3a06caa6f715d896764b1fb00 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 5 Dec 2024 20:50:12 +0100 Subject: [PATCH 2147/2244] Fix boolean assignment On --no-vd-system-decoration, the boolean option must be set to false. It was wrongly assigned from optarg (this worked because optarg is NULL at this point, so it was converted to false). PR #5615 --- 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 fa46c4e4..cd0fa1c5 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -2706,7 +2706,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], opts->angle = optarg; break; case OPT_NO_VD_SYSTEM_DECORATIONS: - opts->vd_system_decorations = optarg; + opts->vd_system_decorations = false; break; default: // getopt prints the error message on stderr From 6c6607d404b0e8e886852537c382d3732f3454d7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 5 Dec 2024 21:02:50 +0100 Subject: [PATCH 2148/2244] Add --no-vd-destroy-content Add an option to disable the following flag for virtual displays: DisplayManager.VIRTUAL_DISPLAY_FLAG_DESTROY_CONTENT_ON_REMOVAL With this option, when the virtual display is closed, the running apps are moved to the main display rather than being destroyed. PR #5615 --- app/data/bash-completion/scrcpy | 1 + app/data/zsh-completion/_scrcpy | 1 + app/scrcpy.1 | 6 ++++++ app/src/cli.c | 13 +++++++++++++ app/src/options.c | 1 + app/src/options.h | 1 + app/src/scrcpy.c | 1 + app/src/server.c | 3 +++ app/src/server.h | 1 + doc/virtual_display.md | 11 +++++++++++ .../main/java/com/genymobile/scrcpy/Options.java | 8 ++++++++ .../genymobile/scrcpy/video/NewDisplayCapture.java | 8 ++++++-- 12 files changed, 53 insertions(+), 2 deletions(-) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index 6c88927e..29130892 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -57,6 +57,7 @@ _scrcpy() { --no-mipmaps --no-mouse-hover --no-power-on + --no-vd-destroy-content --no-vd-system-decorations --no-video --no-video-playback diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index e0c5e265..0897b9cc 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -63,6 +63,7 @@ arguments=( '--no-mipmaps[Disable the generation of mipmaps]' '--no-mouse-hover[Do not forward mouse hover events]' '--no-power-on[Do not power on the device on start]' + '--no-vd-destroy-content[Disable virtual display "destroy content on removal" flag]' '--no-vd-system-decorations[Disable virtual display system decorations flag]' '--no-video[Disable video forwarding]' '--no-video-playback[Disable video playback]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 326cb23f..924905e4 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -369,6 +369,12 @@ Do not forward mouse hover (mouse motion without any clicks) events. .B \-\-no\-power\-on Do not power on the device on start. +.TP +.B \-\-no\-vd\-destroy\-content +Disable virtual display "destroy content on removal" flag. + +With this option, when the virtual display is closed, the running apps are moved to the main display rather than being destroyed. + .TP .B \-\-no\-vd\-system\-decorations Disable virtual display system decorations flag. diff --git a/app/src/cli.c b/app/src/cli.c index cd0fa1c5..ed1970d4 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -110,6 +110,7 @@ enum { OPT_CAPTURE_ORIENTATION, OPT_ANGLE, OPT_NO_VD_SYSTEM_DECORATIONS, + OPT_NO_VD_DESTROY_CONTENT, }; struct sc_option { @@ -659,6 +660,15 @@ static const struct sc_option options[] = { .longopt = "no-power-on", .text = "Do not power on the device on start.", }, + { + .longopt_id = OPT_NO_VD_DESTROY_CONTENT, + .longopt = "no-vd-destroy-content", + .text = "Disable virtual display \"destroy content on removal\" " + "flag.\n" + "With this option, when the virtual display is closed, the " + "running apps are moved to the main display rather than being " + "destroyed.", + }, { .longopt_id = OPT_NO_VD_SYSTEM_DECORATIONS, .longopt = "no-vd-system-decorations", @@ -2705,6 +2715,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], case OPT_ANGLE: opts->angle = optarg; break; + case OPT_NO_VD_DESTROY_CONTENT: + opts->vd_destroy_content = false; + break; case OPT_NO_VD_SYSTEM_DECORATIONS: opts->vd_system_decorations = false; break; diff --git a/app/src/options.c b/app/src/options.c index be3cf8d1..df8033e9 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -108,6 +108,7 @@ const struct scrcpy_options scrcpy_options_default = { .new_display = NULL, .start_app = NULL, .angle = NULL, + .vd_destroy_content = true, .vd_system_decorations = true, }; diff --git a/app/src/options.h b/app/src/options.h index eaeba2f2..152881d8 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -310,6 +310,7 @@ struct scrcpy_options { bool audio_dup; const char *new_display; // [x][/] parsed by the server const char *start_app; + bool vd_destroy_content; bool vd_system_decorations; }; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index dc9e237f..f1942e43 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -458,6 +458,7 @@ scrcpy(struct scrcpy_options *options) { .power_on = options->power_on, .kill_adb_on_close = options->kill_adb_on_close, .camera_high_speed = options->camera_high_speed, + .vd_destroy_content = options->vd_destroy_content, .vd_system_decorations = options->vd_system_decorations, .list = options->list, }; diff --git a/app/src/server.c b/app/src/server.c index 8bdf9501..22ddd372 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -377,6 +377,9 @@ execute_server(struct sc_server *server, VALIDATE_STRING(params->new_display); ADD_PARAM("new_display=%s", params->new_display); } + if (!params->vd_destroy_content) { + ADD_PARAM("vd_destroy_content=false"); + } if (!params->vd_system_decorations) { ADD_PARAM("vd_system_decorations=false"); } diff --git a/app/src/server.h b/app/src/server.h index 6d9dbd4d..3c78b9ed 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -69,6 +69,7 @@ struct sc_server_params { bool power_on; bool kill_adb_on_close; bool camera_high_speed; + bool vd_destroy_content; bool vd_system_decorations; uint8_t list; }; diff --git a/doc/virtual_display.md b/doc/virtual_display.md index 5036f35b..5d1673e8 100644 --- a/doc/virtual_display.md +++ b/doc/virtual_display.md @@ -50,3 +50,14 @@ any default launcher UI available in virtual displays. Note that if no app is started, no content will be rendered, so no video frame will be produced at all. + + +## Destroy on close + +By default, when the virtual display is closed, the running apps are destroyed. + +To move them to the main display instead, use: + +``` +scrcpy --new-display --no-vd-destroy-content +``` diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 43cc790d..8a438750 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -60,6 +60,7 @@ public class Options { private boolean powerOn = true; private NewDisplay newDisplay; + private boolean vdDestroyContent = true; private boolean vdSystemDecorations = true; private Orientation.Lock captureOrientationLock = Orientation.Lock.Unlocked; @@ -233,6 +234,10 @@ public class Options { return captureOrientationLock; } + public boolean getVDDestroyContent() { + return vdDestroyContent; + } + public boolean getVDSystemDecorations() { return vdSystemDecorations; } @@ -466,6 +471,9 @@ public class Options { case "new_display": options.newDisplay = parseNewDisplay(value); break; + case "vd_destroy_content": + options.vdDestroyContent = Boolean.parseBoolean(value); + break; case "vd_system_decorations": options.vdSystemDecorations = Boolean.parseBoolean(value); break; diff --git a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java index d92141af..033d6b9a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java @@ -53,6 +53,7 @@ public class NewDisplayCapture extends SurfaceCapture { private final boolean captureOrientationLocked; private final Orientation captureOrientation; private final float angle; + private final boolean vdDestroyContent; private final boolean vdSystemDecorations; private VirtualDisplay virtualDisplay; @@ -73,6 +74,7 @@ public class NewDisplayCapture extends SurfaceCapture { this.captureOrientation = options.getCaptureOrientation(); assert captureOrientation != null; this.angle = options.getAngle(); + this.vdDestroyContent = options.getVDDestroyContent(); this.vdSystemDecorations = options.getVDSystemDecorations(); } @@ -167,8 +169,10 @@ public class NewDisplayCapture extends SurfaceCapture { int flags = VIRTUAL_DISPLAY_FLAG_PUBLIC | VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY | VIRTUAL_DISPLAY_FLAG_SUPPORTS_TOUCH - | VIRTUAL_DISPLAY_FLAG_ROTATES_WITH_CONTENT - | VIRTUAL_DISPLAY_FLAG_DESTROY_CONTENT_ON_REMOVAL; + | VIRTUAL_DISPLAY_FLAG_ROTATES_WITH_CONTENT; + if (vdDestroyContent) { + flags |= VIRTUAL_DISPLAY_FLAG_DESTROY_CONTENT_ON_REMOVAL; + } if (vdSystemDecorations) { flags |= VIRTUAL_DISPLAY_FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS; } From 2780e0bd7b7c1d4bdbbfc07e7a2b978b48abf3c2 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 5 Dec 2024 19:56:53 +0100 Subject: [PATCH 2149/2244] Do not interrupt cleanup configuration Some options, such as --show-touches or --stay-awake, modify Android settings and must be restored upon exit. If scrcpy terminates (e.g. due to an early error) in the middle of the clean up configuration, the device may be left in an inconsistent state (some settings might be changed but not restored). This issue can be reproduced with high probability by forcing scrcpy to fail: scrcpy --show-touches --video-encoder=fail To prevent this problem, ensure that the clean up thread is not interrupted until the clean up process is started. Refs #5601 PR #5613 --- .../java/com/genymobile/scrcpy/CleanUp.java | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java index 1c6f1701..f372855b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java +++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java @@ -24,6 +24,7 @@ public final class CleanUp { private boolean pendingRestoreDisplayPower; private Thread thread; + private boolean interrupted; private CleanUp(Options options) { thread = new Thread(() -> runCleanUp(options), "cleanup"); @@ -34,8 +35,10 @@ public final class CleanUp { return new CleanUp(options); } - public void interrupt() { - thread.interrupt(); + public synchronized void interrupt() { + // Do not use thread.interrupt() because only the wait() call must be interrupted, not Command.exec() + interrupted = true; + notify(); } public void join() throws InterruptedException { @@ -97,15 +100,13 @@ public final class CleanUp { try { run(displayId, restoreStayOn, disableShowTouches, powerOffScreen, restoreScreenOffTimeout); - } catch (InterruptedException e) { - // ignore } catch (IOException e) { Ln.e("Clean up I/O exception", e); } } private void run(int displayId, int restoreStayOn, boolean disableShowTouches, boolean powerOffScreen, int restoreScreenOffTimeout) - throws IOException, InterruptedException { + throws IOException { String[] cmd = { "app_process", "/", @@ -126,8 +127,15 @@ public final class CleanUp { int localPendingChanges; boolean localPendingRestoreDisplayPower; synchronized (this) { - while (pendingChanges == 0) { - wait(); + while (!interrupted && pendingChanges == 0) { + try { + wait(); + } catch (InterruptedException e) { + throw new AssertionError("Clean up thread MUST NOT be interrupted"); + } + } + if (interrupted) { + break; } localPendingChanges = pendingChanges; localPendingRestoreDisplayPower = pendingRestoreDisplayPower; From c59a3c3169973abb4ce236e06990d58ae6567481 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 5 Dec 2024 20:08:21 +0100 Subject: [PATCH 2150/2244] Start cleanup process with setsid or nohup If available, start the cleanup process in a new session to reduce the likelihood of it being terminated along with the scrcpy server process on some devices. The binaries setsid and nohup are often available, but it is not guaranteed. Refs #5601 PR #5613 --- .../java/com/genymobile/scrcpy/CleanUp.java | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java index f372855b..ac265229 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java +++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java @@ -10,6 +10,8 @@ import android.os.BatteryManager; import java.io.File; import java.io.IOException; import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; /** * Handle the cleanup of scrcpy, even if the main process is killed. @@ -107,16 +109,22 @@ public final class CleanUp { private void run(int displayId, int restoreStayOn, boolean disableShowTouches, boolean powerOffScreen, int restoreScreenOffTimeout) throws IOException { - String[] cmd = { - "app_process", - "/", - CleanUp.class.getName(), - String.valueOf(displayId), - String.valueOf(restoreStayOn), - String.valueOf(disableShowTouches), - String.valueOf(powerOffScreen), - String.valueOf(restoreScreenOffTimeout), - }; + + List cmd = new ArrayList<>(); + if (new File("/system/bin/setsid").exists()) { + cmd.add("/system/bin/setsid"); + } else if (new File("/system/bin/nohup").exists()) { + cmd.add("/system/bin/nohup"); + } + + cmd.add("app_process"); + cmd.add("/"); + cmd.add(CleanUp.class.getName()); + cmd.add(String.valueOf(displayId)); + cmd.add(String.valueOf(restoreStayOn)); + cmd.add(String.valueOf(disableShowTouches)); + cmd.add(String.valueOf(powerOffScreen)); + cmd.add(String.valueOf(restoreScreenOffTimeout)); ProcessBuilder builder = new ProcessBuilder(cmd); builder.environment().put("CLASSPATH", Server.SERVER_PATH); From 4bd1c5981db307155452fa7594945e069542ddb3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 7 Dec 2024 14:12:53 +0100 Subject: [PATCH 2151/2244] Split gamepad device added/removed events Use two separate callbacks for gamepad device added and gamepad device removed. It looks cleaner. PR #5623 --- app/src/input_events.h | 16 ----------- app/src/input_manager.c | 19 +++++++------ app/src/trait/gamepad_processor.h | 15 ++++++++-- app/src/uhid/gamepad_uhid.c | 43 +++++++++++++++------------- app/src/usb/gamepad_aoa.c | 47 ++++++++++++++++--------------- app/src/usb/screen_otg.c | 22 +++++++-------- 6 files changed, 80 insertions(+), 82 deletions(-) diff --git a/app/src/input_events.h b/app/src/input_events.h index c8966a35..ad3afa81 100644 --- a/app/src/input_events.h +++ b/app/src/input_events.h @@ -412,18 +412,12 @@ struct sc_touch_event { float pressure; }; -enum sc_gamepad_device_event_type { - SC_GAMEPAD_DEVICE_ADDED, - SC_GAMEPAD_DEVICE_REMOVED, -}; - // As documented in : // The ID value starts at 0 and increments from there. The value -1 is an // invalid ID. #define SC_GAMEPAD_ID_INVALID UINT32_C(-1) struct sc_gamepad_device_event { - enum sc_gamepad_device_event_type type; uint32_t gamepad_id; }; @@ -503,16 +497,6 @@ sc_mouse_buttons_state_from_sdl(uint32_t buttons_state) { return buttons_state; } -static inline enum sc_gamepad_device_event_type -sc_gamepad_device_event_type_from_sdl_type(uint32_t type) { - assert(type == SDL_CONTROLLERDEVICEADDED - || type == SDL_CONTROLLERDEVICEREMOVED); - if (type == SDL_CONTROLLERDEVICEADDED) { - return SC_GAMEPAD_DEVICE_ADDED; - } - return SC_GAMEPAD_DEVICE_REMOVED; -} - static inline enum sc_gamepad_axis sc_gamepad_axis_from_sdl(uint8_t axis) { if (axis <= SDL_CONTROLLER_AXIS_TRIGGERRIGHT) { diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 3955c211..2e4337db 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -908,7 +908,6 @@ sc_input_manager_process_mouse_wheel(struct sc_input_manager *im, static void sc_input_manager_process_gamepad_device(struct sc_input_manager *im, const SDL_ControllerDeviceEvent *event) { - SDL_JoystickID id; if (event->type == SDL_CONTROLLERDEVICEADDED) { SDL_GameController *gc = SDL_GameControllerOpen(event->which); if (!gc) { @@ -923,9 +922,12 @@ sc_input_manager_process_gamepad_device(struct sc_input_manager *im, return; } - id = SDL_JoystickInstanceID(joystick); + struct sc_gamepad_device_event evt = { + .gamepad_id = SDL_JoystickInstanceID(joystick), + }; + im->gp->ops->process_gamepad_added(im->gp, &evt); } else if (event->type == SDL_CONTROLLERDEVICEREMOVED) { - id = event->which; + SDL_JoystickID id = event->which; SDL_GameController *gc = SDL_GameControllerFromInstanceID(id); if (gc) { @@ -933,16 +935,15 @@ sc_input_manager_process_gamepad_device(struct sc_input_manager *im, } else { LOGW("Unknown gamepad device removed"); } + + struct sc_gamepad_device_event evt = { + .gamepad_id = id, + }; + im->gp->ops->process_gamepad_removed(im->gp, &evt); } else { // Nothing to do return; } - - struct sc_gamepad_device_event evt = { - .type = sc_gamepad_device_event_type_from_sdl_type(event->type), - .gamepad_id = id, - }; - im->gp->ops->process_gamepad_device(im->gp, &evt); } static void diff --git a/app/src/trait/gamepad_processor.h b/app/src/trait/gamepad_processor.h index 72479783..19629a9a 100644 --- a/app/src/trait/gamepad_processor.h +++ b/app/src/trait/gamepad_processor.h @@ -20,13 +20,22 @@ struct sc_gamepad_processor { struct sc_gamepad_processor_ops { /** - * Process a gamepad device added or removed + * Process a gamepad device added event * * This function is mandatory. */ void - (*process_gamepad_device)(struct sc_gamepad_processor *gp, - const struct sc_gamepad_device_event *event); + (*process_gamepad_added)(struct sc_gamepad_processor *gp, + const struct sc_gamepad_device_event *event); + + /** + * Process a gamepad device removed event + * + * This function is mandatory. + */ + void + (*process_gamepad_removed)(struct sc_gamepad_processor *gp, + const struct sc_gamepad_device_event *event); /** * Process a gamepad axis event diff --git a/app/src/uhid/gamepad_uhid.c b/app/src/uhid/gamepad_uhid.c index 62b0f653..42db63e7 100644 --- a/app/src/uhid/gamepad_uhid.c +++ b/app/src/uhid/gamepad_uhid.c @@ -52,29 +52,31 @@ sc_gamepad_uhid_send_close(struct sc_gamepad_uhid *gamepad, } static void -sc_gamepad_processor_process_gamepad_device(struct sc_gamepad_processor *gp, +sc_gamepad_processor_process_gamepad_added(struct sc_gamepad_processor *gp, const struct sc_gamepad_device_event *event) { struct sc_gamepad_uhid *gamepad = DOWNCAST(gp); - if (event->type == SC_GAMEPAD_DEVICE_ADDED) { - struct sc_hid_open hid_open; - if (!sc_hid_gamepad_generate_open(&gamepad->hid, &hid_open, - event->gamepad_id)) { - return; - } - - sc_gamepad_uhid_send_open(gamepad, &hid_open); - } else { - assert(event->type == SC_GAMEPAD_DEVICE_REMOVED); - - struct sc_hid_close hid_close; - if (!sc_hid_gamepad_generate_close(&gamepad->hid, &hid_close, - event->gamepad_id)) { - return; - } - - sc_gamepad_uhid_send_close(gamepad, &hid_close); + struct sc_hid_open hid_open; + if (!sc_hid_gamepad_generate_open(&gamepad->hid, &hid_open, + event->gamepad_id)) { + return; } + + sc_gamepad_uhid_send_open(gamepad, &hid_open); +} + +static void +sc_gamepad_processor_process_gamepad_removed(struct sc_gamepad_processor *gp, + const struct sc_gamepad_device_event *event) { + struct sc_gamepad_uhid *gamepad = DOWNCAST(gp); + + struct sc_hid_close hid_close; + if (!sc_hid_gamepad_generate_close(&gamepad->hid, &hid_close, + event->gamepad_id)) { + return; + } + + sc_gamepad_uhid_send_close(gamepad, &hid_close); } static void @@ -114,7 +116,8 @@ sc_gamepad_uhid_init(struct sc_gamepad_uhid *gamepad, gamepad->controller = controller; static const struct sc_gamepad_processor_ops ops = { - .process_gamepad_device = sc_gamepad_processor_process_gamepad_device, + .process_gamepad_added = sc_gamepad_processor_process_gamepad_added, + .process_gamepad_removed = sc_gamepad_processor_process_gamepad_removed, .process_gamepad_axis = sc_gamepad_processor_process_gamepad_axis, .process_gamepad_button = sc_gamepad_processor_process_gamepad_button, }; diff --git a/app/src/usb/gamepad_aoa.c b/app/src/usb/gamepad_aoa.c index 37587532..4372379f 100644 --- a/app/src/usb/gamepad_aoa.c +++ b/app/src/usb/gamepad_aoa.c @@ -7,33 +7,35 @@ #define DOWNCAST(GP) container_of(GP, struct sc_gamepad_aoa, gamepad_processor) static void -sc_gamepad_processor_process_gamepad_device(struct sc_gamepad_processor *gp, +sc_gamepad_processor_process_gamepad_added(struct sc_gamepad_processor *gp, const struct sc_gamepad_device_event *event) { struct sc_gamepad_aoa *gamepad = DOWNCAST(gp); - if (event->type == SC_GAMEPAD_DEVICE_ADDED) { - struct sc_hid_open hid_open; - if (!sc_hid_gamepad_generate_open(&gamepad->hid, &hid_open, - event->gamepad_id)) { - return; - } + struct sc_hid_open hid_open; + if (!sc_hid_gamepad_generate_open(&gamepad->hid, &hid_open, + event->gamepad_id)) { + return; + } - // exit_on_error: false (a gamepad open failure should not exit scrcpy) - if (!sc_aoa_push_open(gamepad->aoa, &hid_open, false)) { - LOGW("Could not push AOA HID open (gamepad)"); - } - } else { - assert(event->type == SC_GAMEPAD_DEVICE_REMOVED); + // exit_on_error: false (a gamepad open failure should not exit scrcpy) + if (!sc_aoa_push_open(gamepad->aoa, &hid_open, false)) { + LOGW("Could not push AOA HID open (gamepad)"); + } +} - struct sc_hid_close hid_close; - if (!sc_hid_gamepad_generate_close(&gamepad->hid, &hid_close, - event->gamepad_id)) { - return; - } +static void +sc_gamepad_processor_process_gamepad_removed(struct sc_gamepad_processor *gp, + const struct sc_gamepad_device_event *event) { + struct sc_gamepad_aoa *gamepad = DOWNCAST(gp); - if (!sc_aoa_push_close(gamepad->aoa, &hid_close)) { - LOGW("Could not push AOA HID close (gamepad)"); - } + struct sc_hid_close hid_close; + if (!sc_hid_gamepad_generate_close(&gamepad->hid, &hid_close, + event->gamepad_id)) { + return; + } + + if (!sc_aoa_push_close(gamepad->aoa, &hid_close)) { + LOGW("Could not push AOA HID close (gamepad)"); } } @@ -76,7 +78,8 @@ sc_gamepad_aoa_init(struct sc_gamepad_aoa *gamepad, struct sc_aoa *aoa) { sc_hid_gamepad_init(&gamepad->hid); static const struct sc_gamepad_processor_ops ops = { - .process_gamepad_device = sc_gamepad_processor_process_gamepad_device, + .process_gamepad_added = sc_gamepad_processor_process_gamepad_added, + .process_gamepad_removed = sc_gamepad_processor_process_gamepad_removed, .process_gamepad_axis = sc_gamepad_processor_process_gamepad_axis, .process_gamepad_button = sc_gamepad_processor_process_gamepad_button, }; diff --git a/app/src/usb/screen_otg.c b/app/src/usb/screen_otg.c index 18377074..368af125 100644 --- a/app/src/usb/screen_otg.c +++ b/app/src/usb/screen_otg.c @@ -175,7 +175,6 @@ sc_screen_otg_process_gamepad_device(struct sc_screen_otg *screen, assert(screen->gamepad); struct sc_gamepad_processor *gp = &screen->gamepad->gamepad_processor; - SDL_JoystickID id; if (event->type == SDL_CONTROLLERDEVICEADDED) { SDL_GameController *gc = SDL_GameControllerOpen(event->which); if (!gc) { @@ -190,9 +189,12 @@ sc_screen_otg_process_gamepad_device(struct sc_screen_otg *screen, return; } - id = SDL_JoystickInstanceID(joystick); + struct sc_gamepad_device_event evt = { + .gamepad_id = SDL_JoystickInstanceID(joystick), + }; + gp->ops->process_gamepad_added(gp, &evt); } else if (event->type == SDL_CONTROLLERDEVICEREMOVED) { - id = event->which; + SDL_JoystickID id = event->which; SDL_GameController *gc = SDL_GameControllerFromInstanceID(id); if (gc) { @@ -200,16 +202,12 @@ sc_screen_otg_process_gamepad_device(struct sc_screen_otg *screen, } else { LOGW("Unknown gamepad device removed"); } - } else { - // Nothing to do - return; - } - struct sc_gamepad_device_event evt = { - .type = sc_gamepad_device_event_type_from_sdl_type(event->type), - .gamepad_id = id, - }; - gp->ops->process_gamepad_device(gp, &evt); + struct sc_gamepad_device_event evt = { + .gamepad_id = id, + }; + gp->ops->process_gamepad_removed(gp, &evt); + } } static void From 9cf4d527215a3f21077b4d28466632be26f72917 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Dec 2024 12:53:07 +0100 Subject: [PATCH 2152/2244] Fix HID gamepad comments PR #5623 --- app/src/hid/hid_gamepad.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/hid/hid_gamepad.c b/app/src/hid/hid_gamepad.c index e2bf0616..99facdd0 100644 --- a/app/src/hid/hid_gamepad.c +++ b/app/src/hid/hid_gamepad.c @@ -65,7 +65,7 @@ static const uint8_t SC_HID_GAMEPAD_REPORT_DESC[] = { 0x75, 0x10, // Report Count (4) 0x95, 0x04, - // Input (Data, Variable, Absolute): 4 bytes (X, Y, Z, Rz) + // Input (Data, Variable, Absolute): 4x2 bytes (X, Y, Z, Rz) 0x81, 0x02, // Usage Page (Simulation Controls) @@ -82,7 +82,7 @@ static const uint8_t SC_HID_GAMEPAD_REPORT_DESC[] = { 0x75, 0x10, // Report Count (2) 0x95, 0x02, - // Input (Data, Variable, Absolute): 2 bytes (L2, R2) + // Input (Data, Variable, Absolute): 2x2 bytes (L2, R2) 0x81, 0x02, // Usage Page (Buttons) @@ -182,7 +182,7 @@ static const uint8_t SC_HID_GAMEPAD_REPORT_DESC[] = { * `------------- SC_GAMEPAD_BUTTON_RIGHT_STICK * * +---------------+ - * byte 14: |0 0 0 . . . . .| hat switch (dpad) position (0-8) + * byte 14: |0 0 0 0 . . . .| hat switch (dpad) position (0-8) * +---------------+ * 9 possible positions and their values: * 8 1 2 From 1786f28e6f9f9c597f4d66de88c206489cb87122 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Dec 2024 12:53:28 +0100 Subject: [PATCH 2153/2244] Fix gamepad HID descriptor Use Z and Rz for L2/R2, which are more widely supported than Brake/Accelerator. The right stick must then be bound to Rx and Ry. Fixes #5362 PR #5623 --- app/src/hid/hid_gamepad.c | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/app/src/hid/hid_gamepad.c b/app/src/hid/hid_gamepad.c index 99facdd0..977bcf68 100644 --- a/app/src/hid/hid_gamepad.c +++ b/app/src/hid/hid_gamepad.c @@ -52,10 +52,10 @@ static const uint8_t SC_HID_GAMEPAD_REPORT_DESC[] = { 0x09, 0x30, // Usage (Y) Left stick y 0x09, 0x31, - // Usage (Z) Right stick x - 0x09, 0x32, - // Usage (Rz) Right stick y - 0x09, 0x35, + // Usage (Rx) Right stick x + 0x09, 0x33, + // Usage (Ry) Right stick y + 0x09, 0x34, // Logical Minimum (0) 0x15, 0x00, // Logical Maximum (65535) @@ -68,12 +68,12 @@ static const uint8_t SC_HID_GAMEPAD_REPORT_DESC[] = { // Input (Data, Variable, Absolute): 4x2 bytes (X, Y, Z, Rz) 0x81, 0x02, - // Usage Page (Simulation Controls) - 0x05, 0x02, - // Usage (Brake) - 0x09, 0xC5, - // Usage (Accelerator) - 0x09, 0xC4, + // Usage Page (Generic Desktop) + 0x05, 0x01, + // Usage (Z) + 0x09, 0x32, + // Usage (Rz) + 0x09, 0x35, // Logical Minimum (0) 0x15, 0x00, // Logical Maximum (32767) From 86a68fac6c631a01e8d0132eee0fc5a831e78417 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 7 Dec 2024 14:40:04 +0100 Subject: [PATCH 2154/2244] Fix gamepad axis initial values By default, initialize axis to 0, which is represented by 0x8000 as a 16-bit unsigned value. PR #5623 --- app/src/hid/hid_gamepad.c | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/app/src/hid/hid_gamepad.c b/app/src/hid/hid_gamepad.c index 977bcf68..892d21f2 100644 --- a/app/src/hid/hid_gamepad.c +++ b/app/src/hid/hid_gamepad.c @@ -191,16 +191,19 @@ static const uint8_t SC_HID_GAMEPAD_REPORT_DESC[] = { * (8 is top-left, 1 is top, 2 is top-right, etc.) */ +// [-32768 to 32767] -> [0 to 65535] +#define AXIS_RESCALE(V) (uint16_t) (((int32_t) V) + 0x8000) + static void sc_hid_gamepad_slot_init(struct sc_hid_gamepad_slot *slot, uint32_t gamepad_id) { assert(gamepad_id != SC_GAMEPAD_ID_INVALID); slot->gamepad_id = gamepad_id; slot->buttons = 0; - slot->axis_left_x = 0; - slot->axis_left_y = 0; - slot->axis_right_x = 0; - slot->axis_right_y = 0; + slot->axis_left_x = AXIS_RESCALE(0); + slot->axis_left_y = AXIS_RESCALE(0); + slot->axis_right_x = AXIS_RESCALE(0); + slot->axis_right_y = AXIS_RESCALE(0); slot->axis_left_trigger = 0; slot->axis_right_trigger = 0; } @@ -423,8 +426,6 @@ sc_hid_gamepad_generate_input_from_axis(struct sc_hid_gamepad *hid, struct sc_hid_gamepad_slot *slot = &hid->slots[slot_idx]; -// [-32768 to 32767] -> [0 to 65535] -#define AXIS_RESCALE(V) (uint16_t) (((int32_t) V) + 0x8000) switch (event->axis) { case SC_GAMEPAD_AXIS_LEFTX: slot->axis_left_x = AXIS_RESCALE(event->value); From 27a5934a1d5365332e4338f76508139dbd61d1ef Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 7 Dec 2024 12:43:49 +0100 Subject: [PATCH 2155/2244] Define UHID vendorId and productId from the client Let the client choose the USB ids, that it transmits in UHID_CREATE requests. PR #5623 --- app/src/control_msg.c | 14 ++++++++++---- app/src/control_msg.h | 2 ++ app/src/uhid/gamepad_uhid.c | 5 +++++ app/src/uhid/keyboard_uhid.c | 2 ++ app/src/uhid/mouse_uhid.c | 2 ++ app/tests/test_control_msg_serialize.c | 6 +++++- .../genymobile/scrcpy/control/ControlMessage.java | 14 +++++++++++++- .../scrcpy/control/ControlMessageReader.java | 4 +++- .../com/genymobile/scrcpy/control/Controller.java | 2 +- .../com/genymobile/scrcpy/control/UhidManager.java | 10 +++++----- .../scrcpy/control/ControlMessageReaderTest.java | 4 ++++ 11 files changed, 52 insertions(+), 13 deletions(-) diff --git a/app/src/control_msg.c b/app/src/control_msg.c index 0defda92..e78f0c57 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -152,8 +152,10 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) { return 2; case SC_CONTROL_MSG_TYPE_UHID_CREATE: sc_write16be(&buf[1], msg->uhid_create.id); + sc_write16be(&buf[3], msg->uhid_create.vendor_id); + sc_write16be(&buf[5], msg->uhid_create.product_id); - size_t index = 3; + size_t index = 7; index += write_string_tiny(&buf[index], msg->uhid_create.name, 127); sc_write16be(&buf[index], msg->uhid_create.report_desc_size); @@ -278,9 +280,13 @@ sc_control_msg_log(const struct sc_control_msg *msg) { // Quote only if name is not null const char *name = msg->uhid_create.name; const char *quote = name ? "\"" : ""; - LOG_CMSG("UHID create [%" PRIu16 "] name=%s%s%s " - "report_desc_size=%" PRIu16, msg->uhid_create.id, - quote, name, quote, msg->uhid_create.report_desc_size); + LOG_CMSG("UHID create [%" PRIu16 "] %04" PRIx16 ":%04" PRIx16 + " name=%s%s%s report_desc_size=%" PRIu16, + msg->uhid_create.id, + msg->uhid_create.vendor_id, + msg->uhid_create.product_id, + quote, name, quote, + msg->uhid_create.report_desc_size); break; } case SC_CONTROL_MSG_TYPE_UHID_INPUT: { diff --git a/app/src/control_msg.h b/app/src/control_msg.h index f0a2e373..74dbcba8 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -94,6 +94,8 @@ struct sc_control_msg { } set_display_power; struct { uint16_t id; + uint16_t vendor_id; + uint16_t product_id; const char *name; // pointer to static data uint16_t report_desc_size; const uint8_t *report_desc; // pointer to static data diff --git a/app/src/uhid/gamepad_uhid.c b/app/src/uhid/gamepad_uhid.c index 42db63e7..5b574409 100644 --- a/app/src/uhid/gamepad_uhid.c +++ b/app/src/uhid/gamepad_uhid.c @@ -7,6 +7,9 @@ /** Downcast gamepad processor to sc_gamepad_uhid */ #define DOWNCAST(GP) container_of(GP, struct sc_gamepad_uhid, gamepad_processor) +#define SC_GAMEPAD_UHID_VENDOR_ID 0 +#define SC_GAMEPAD_UHID_PRODUCT_ID 0 + static void sc_gamepad_uhid_send_input(struct sc_gamepad_uhid *gamepad, const struct sc_hid_input *hid_input, @@ -30,6 +33,8 @@ sc_gamepad_uhid_send_open(struct sc_gamepad_uhid *gamepad, struct sc_control_msg msg; msg.type = SC_CONTROL_MSG_TYPE_UHID_CREATE; msg.uhid_create.id = hid_open->hid_id; + msg.uhid_create.vendor_id = SC_GAMEPAD_UHID_VENDOR_ID; + msg.uhid_create.product_id = SC_GAMEPAD_UHID_PRODUCT_ID; msg.uhid_create.name = hid_open->name; msg.uhid_create.report_desc = hid_open->report_desc; msg.uhid_create.report_desc_size = hid_open->report_desc_size; diff --git a/app/src/uhid/keyboard_uhid.c b/app/src/uhid/keyboard_uhid.c index 496da23d..4d2c978d 100644 --- a/app/src/uhid/keyboard_uhid.c +++ b/app/src/uhid/keyboard_uhid.c @@ -141,6 +141,8 @@ sc_keyboard_uhid_init(struct sc_keyboard_uhid *kb, struct sc_control_msg msg; msg.type = SC_CONTROL_MSG_TYPE_UHID_CREATE; msg.uhid_create.id = SC_HID_ID_KEYBOARD; + msg.uhid_create.vendor_id = 0; + msg.uhid_create.product_id = 0; msg.uhid_create.name = hid_open.name; msg.uhid_create.report_desc = hid_open.report_desc; msg.uhid_create.report_desc_size = hid_open.report_desc_size; diff --git a/app/src/uhid/mouse_uhid.c b/app/src/uhid/mouse_uhid.c index 1dc02777..d6044bdc 100644 --- a/app/src/uhid/mouse_uhid.c +++ b/app/src/uhid/mouse_uhid.c @@ -81,6 +81,8 @@ sc_mouse_uhid_init(struct sc_mouse_uhid *mouse, struct sc_control_msg msg; msg.type = SC_CONTROL_MSG_TYPE_UHID_CREATE; msg.uhid_create.id = SC_HID_ID_MOUSE; + msg.uhid_create.vendor_id = 0; + msg.uhid_create.product_id = 0; msg.uhid_create.name = hid_open.name; msg.uhid_create.report_desc = hid_open.report_desc; msg.uhid_create.report_desc_size = hid_open.report_desc_size; diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c index 9adf2a3d..af97182d 100644 --- a/app/tests/test_control_msg_serialize.c +++ b/app/tests/test_control_msg_serialize.c @@ -329,6 +329,8 @@ static void test_serialize_uhid_create(void) { .type = SC_CONTROL_MSG_TYPE_UHID_CREATE, .uhid_create = { .id = 42, + .vendor_id = 0x1234, + .product_id = 0x5678, .name = "ABC", .report_desc_size = sizeof(report_desc), .report_desc = report_desc, @@ -337,11 +339,13 @@ static void test_serialize_uhid_create(void) { uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; size_t size = sc_control_msg_serialize(&msg, buf); - assert(size == 20); + assert(size == 24); const uint8_t expected[] = { SC_CONTROL_MSG_TYPE_UHID_CREATE, 0, 42, // id + 0x12, 0x34, // vendor id + 0x56, 0x78, // product id 3, // name size 65, 66, 67, // "ABC" 0, 11, // report desc size diff --git a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java index 7455cdf8..0eb96adc 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java @@ -51,6 +51,8 @@ public final class ControlMessage { private int id; private byte[] data; private boolean on; + private int vendorId; + private int productId; private ControlMessage() { } @@ -131,10 +133,12 @@ public final class ControlMessage { return msg; } - public static ControlMessage createUhidCreate(int id, String name, byte[] reportDesc) { + public static ControlMessage createUhidCreate(int id, int vendorId, int productId, String name, byte[] reportDesc) { ControlMessage msg = new ControlMessage(); msg.type = TYPE_UHID_CREATE; msg.id = id; + msg.vendorId = vendorId; + msg.productId = productId; msg.text = name; msg.data = reportDesc; return msg; @@ -237,4 +241,12 @@ public final class ControlMessage { public boolean getOn() { return on; } + + public int getVendorId() { + return vendorId; + } + + public int getProductId() { + return productId; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java index b82615ed..e503ec61 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java @@ -142,9 +142,11 @@ public class ControlMessageReader { private ControlMessage parseUhidCreate() throws IOException { int id = dis.readUnsignedShort(); + int vendorId = dis.readUnsignedShort(); + int productId = dis.readUnsignedShort(); String name = parseString(1); byte[] data = parseByteArray(2); - return ControlMessage.createUhidCreate(id, name, data); + return ControlMessage.createUhidCreate(id, vendorId, productId, name, data); } private ControlMessage parseUhidInput() throws IOException { diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index a0bdc584..5e64a4c5 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -290,7 +290,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { Device.rotateDevice(getActionDisplayId()); break; case ControlMessage.TYPE_UHID_CREATE: - getUhidManager().open(msg.getId(), msg.getText(), msg.getData()); + getUhidManager().open(msg.getId(), msg.getVendorId(), msg.getProductId(), msg.getText(), msg.getData()); break; case ControlMessage.TYPE_UHID_INPUT: getUhidManager().writeInput(msg.getId(), msg.getData()); diff --git a/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java b/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java index 8121adfc..1d7678ec 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java @@ -48,7 +48,7 @@ public final class UhidManager { } } - public void open(int id, String name, byte[] reportDesc) throws IOException { + public void open(int id, int vendorId, int productId, String name, byte[] reportDesc) throws IOException { try { FileDescriptor fd = Os.open("/dev/uhid", OsConstants.O_RDWR, 0); try { @@ -58,7 +58,7 @@ public final class UhidManager { close(old); } - byte[] req = buildUhidCreate2Req(name, reportDesc); + byte[] req = buildUhidCreate2Req(vendorId, productId, name, reportDesc); Os.write(fd, req, 0, req.length); registerUhidListener(id, fd); @@ -148,7 +148,7 @@ public final class UhidManager { } } - private static byte[] buildUhidCreate2Req(String name, byte[] reportDesc) { + private static byte[] buildUhidCreate2Req(int vendorId, int productId, String name, byte[] reportDesc) { /* * struct uhid_event { * uint32_t type; @@ -183,8 +183,8 @@ public final class UhidManager { buf.putShort((short) reportDesc.length); buf.putShort(BUS_VIRTUAL); - buf.putInt(0); // vendor id - buf.putInt(0); // product id + buf.putInt(vendorId); + buf.putInt(productId); buf.putInt(0); // version buf.putInt(0); // country; buf.put(reportDesc); diff --git a/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java index a25507b4..74df064f 100644 --- a/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java @@ -322,6 +322,8 @@ public class ControlMessageReaderTest { DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_UHID_CREATE); dos.writeShort(42); // id + dos.writeShort(0x1234); // vendorId + dos.writeShort(0x5678); // productId dos.writeByte(3); // name size dos.write("ABC".getBytes(StandardCharsets.US_ASCII)); byte[] data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}; @@ -335,6 +337,8 @@ public class ControlMessageReaderTest { ControlMessage event = reader.read(); Assert.assertEquals(ControlMessage.TYPE_UHID_CREATE, event.getType()); Assert.assertEquals(42, event.getId()); + Assert.assertEquals(0x1234, event.getVendorId()); + Assert.assertEquals(0x5678, event.getProductId()); Assert.assertEquals("ABC", event.getText()); Assert.assertArrayEquals(data, event.getData()); From 0a09518a49cb495ba76573597cf38169f6813209 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 7 Dec 2024 18:01:23 +0100 Subject: [PATCH 2156/2244] Use Xbox 360 gamepad USB ids Use the vendorId and productId of an Xbox 360 controller for better support (the HID gamepad protocol used in scrcpy is similar to that of the Xbox 360 controller). Fixes #5362 PR #5623 --- app/src/uhid/gamepad_uhid.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/uhid/gamepad_uhid.c b/app/src/uhid/gamepad_uhid.c index 5b574409..2a063af5 100644 --- a/app/src/uhid/gamepad_uhid.c +++ b/app/src/uhid/gamepad_uhid.c @@ -7,8 +7,9 @@ /** Downcast gamepad processor to sc_gamepad_uhid */ #define DOWNCAST(GP) container_of(GP, struct sc_gamepad_uhid, gamepad_processor) -#define SC_GAMEPAD_UHID_VENDOR_ID 0 -#define SC_GAMEPAD_UHID_PRODUCT_ID 0 +// Xbox 360 +#define SC_GAMEPAD_UHID_VENDOR_ID UINT16_C(0x045e) +#define SC_GAMEPAD_UHID_PRODUCT_ID UINT16_C(0x028e) static void sc_gamepad_uhid_send_input(struct sc_gamepad_uhid *gamepad, From 7418fd06626a2ae405f4bfbe9980a9c4ebd744a7 Mon Sep 17 00:00:00 2001 From: Withoutruless <57673426+Withoutruless@users.noreply.github.com> Date: Sun, 8 Dec 2024 16:49:07 +0100 Subject: [PATCH 2157/2244] Use Xbox 360 gamepad name Some games do not work without a known gamepad name. Fixes #5362 Refs #5623 comment PR #5623 Signed-off-by: Romain Vimont --- app/src/hid/hid_event.h | 1 - app/src/hid/hid_gamepad.c | 6 ------ app/src/hid/hid_keyboard.c | 1 - app/src/hid/hid_mouse.c | 1 - app/src/uhid/gamepad_uhid.c | 3 ++- app/src/uhid/keyboard_uhid.c | 2 +- app/src/uhid/mouse_uhid.c | 2 +- .../java/com/genymobile/scrcpy/control/UhidManager.java | 2 +- 8 files changed, 5 insertions(+), 13 deletions(-) diff --git a/app/src/hid/hid_event.h b/app/src/hid/hid_event.h index 37c3611b..d6818e30 100644 --- a/app/src/hid/hid_event.h +++ b/app/src/hid/hid_event.h @@ -15,7 +15,6 @@ struct sc_hid_input { struct sc_hid_open { uint16_t hid_id; - const char *name; // pointer to static memory const uint8_t *report_desc; // pointer to static memory size_t report_desc_size; }; diff --git a/app/src/hid/hid_gamepad.c b/app/src/hid/hid_gamepad.c index 892d21f2..8f4e4527 100644 --- a/app/src/hid/hid_gamepad.c +++ b/app/src/hid/hid_gamepad.c @@ -246,14 +246,8 @@ sc_hid_gamepad_generate_open(struct sc_hid_gamepad *hid, sc_hid_gamepad_slot_init(&hid->slots[slot_idx], gamepad_id); - SDL_GameController* game_controller = - SDL_GameControllerFromInstanceID(gamepad_id); - assert(game_controller); - const char *name = SDL_GameControllerName(game_controller); - uint16_t hid_id = sc_hid_gamepad_slot_get_id(slot_idx); hid_open->hid_id = hid_id; - hid_open->name = name; hid_open->report_desc = SC_HID_GAMEPAD_REPORT_DESC; hid_open->report_desc_size = sizeof(SC_HID_GAMEPAD_REPORT_DESC); diff --git a/app/src/hid/hid_keyboard.c b/app/src/hid/hid_keyboard.c index 2109224a..961ad790 100644 --- a/app/src/hid/hid_keyboard.c +++ b/app/src/hid/hid_keyboard.c @@ -335,7 +335,6 @@ sc_hid_keyboard_generate_input_from_mods(struct sc_hid_input *hid_input, void sc_hid_keyboard_generate_open(struct sc_hid_open *hid_open) { hid_open->hid_id = SC_HID_ID_KEYBOARD; - hid_open->name = NULL; // No name specified after "scrcpy" hid_open->report_desc = SC_HID_KEYBOARD_REPORT_DESC; hid_open->report_desc_size = sizeof(SC_HID_KEYBOARD_REPORT_DESC); } diff --git a/app/src/hid/hid_mouse.c b/app/src/hid/hid_mouse.c index ac215165..7acc413b 100644 --- a/app/src/hid/hid_mouse.c +++ b/app/src/hid/hid_mouse.c @@ -190,7 +190,6 @@ sc_hid_mouse_generate_input_from_scroll(struct sc_hid_input *hid_input, void sc_hid_mouse_generate_open(struct sc_hid_open *hid_open) { hid_open->hid_id = SC_HID_ID_MOUSE; - hid_open->name = NULL; // No name specified after "scrcpy" hid_open->report_desc = SC_HID_MOUSE_REPORT_DESC; hid_open->report_desc_size = sizeof(SC_HID_MOUSE_REPORT_DESC); } diff --git a/app/src/uhid/gamepad_uhid.c b/app/src/uhid/gamepad_uhid.c index 2a063af5..4da4a21e 100644 --- a/app/src/uhid/gamepad_uhid.c +++ b/app/src/uhid/gamepad_uhid.c @@ -10,6 +10,7 @@ // Xbox 360 #define SC_GAMEPAD_UHID_VENDOR_ID UINT16_C(0x045e) #define SC_GAMEPAD_UHID_PRODUCT_ID UINT16_C(0x028e) +#define SC_GAMEPAD_UHID_NAME "Microsoft X-Box 360 Pad" static void sc_gamepad_uhid_send_input(struct sc_gamepad_uhid *gamepad, @@ -36,7 +37,7 @@ sc_gamepad_uhid_send_open(struct sc_gamepad_uhid *gamepad, msg.uhid_create.id = hid_open->hid_id; msg.uhid_create.vendor_id = SC_GAMEPAD_UHID_VENDOR_ID; msg.uhid_create.product_id = SC_GAMEPAD_UHID_PRODUCT_ID; - msg.uhid_create.name = hid_open->name; + msg.uhid_create.name = SC_GAMEPAD_UHID_NAME; msg.uhid_create.report_desc = hid_open->report_desc; msg.uhid_create.report_desc_size = hid_open->report_desc_size; diff --git a/app/src/uhid/keyboard_uhid.c b/app/src/uhid/keyboard_uhid.c index 4d2c978d..76d70cc5 100644 --- a/app/src/uhid/keyboard_uhid.c +++ b/app/src/uhid/keyboard_uhid.c @@ -143,7 +143,7 @@ sc_keyboard_uhid_init(struct sc_keyboard_uhid *kb, msg.uhid_create.id = SC_HID_ID_KEYBOARD; msg.uhid_create.vendor_id = 0; msg.uhid_create.product_id = 0; - msg.uhid_create.name = hid_open.name; + msg.uhid_create.name = NULL; msg.uhid_create.report_desc = hid_open.report_desc; msg.uhid_create.report_desc_size = hid_open.report_desc_size; if (!sc_controller_push_msg(controller, &msg)) { diff --git a/app/src/uhid/mouse_uhid.c b/app/src/uhid/mouse_uhid.c index d6044bdc..471030e7 100644 --- a/app/src/uhid/mouse_uhid.c +++ b/app/src/uhid/mouse_uhid.c @@ -83,7 +83,7 @@ sc_mouse_uhid_init(struct sc_mouse_uhid *mouse, msg.uhid_create.id = SC_HID_ID_MOUSE; msg.uhid_create.vendor_id = 0; msg.uhid_create.product_id = 0; - msg.uhid_create.name = hid_open.name; + msg.uhid_create.name = NULL; msg.uhid_create.report_desc = hid_open.report_desc; msg.uhid_create.report_desc_size = hid_open.report_desc_size; if (!sc_controller_push_msg(controller, &msg)) { diff --git a/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java b/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java index 1d7678ec..c4867a3f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java @@ -174,7 +174,7 @@ public final class UhidManager { ByteBuffer buf = ByteBuffer.allocate(280 + reportDesc.length).order(ByteOrder.nativeOrder()); buf.putInt(UHID_CREATE2); - String actualName = name.isEmpty() ? "scrcpy" : "scrcpy: " + name; + String actualName = name.isEmpty() ? "scrcpy" : name; byte[] utf8Name = actualName.getBytes(StandardCharsets.UTF_8); int len = StringUtils.getUtf8TruncationIndex(utf8Name, 127); assert len <= 127; From 328bb74f8002693e4be2703450305e82fc015e88 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 8 Dec 2024 17:07:03 +0100 Subject: [PATCH 2158/2244] Log gamepad added/removed Add a log when a gamepad is added or removed. PR #5623 --- app/src/uhid/gamepad_uhid.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/src/uhid/gamepad_uhid.c b/app/src/uhid/gamepad_uhid.c index 4da4a21e..a066cf03 100644 --- a/app/src/uhid/gamepad_uhid.c +++ b/app/src/uhid/gamepad_uhid.c @@ -69,6 +69,12 @@ sc_gamepad_processor_process_gamepad_added(struct sc_gamepad_processor *gp, return; } + SDL_GameController* game_controller = + SDL_GameControllerFromInstanceID(event->gamepad_id); + assert(game_controller); + const char *name = SDL_GameControllerName(game_controller); + LOGI("Gamepad added: [%" PRIu32 "] %s", event->gamepad_id, name); + sc_gamepad_uhid_send_open(gamepad, &hid_open); } @@ -83,6 +89,8 @@ sc_gamepad_processor_process_gamepad_removed(struct sc_gamepad_processor *gp, return; } + LOGI("Gamepad removed: [%" PRIu32 "]", event->gamepad_id); + sc_gamepad_uhid_send_close(gamepad, &hid_close); } From 65256d7cc77216424976eb2ca83befde112c0e26 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 8 Dec 2024 17:13:50 +0100 Subject: [PATCH 2159/2244] Upgrade SDL (2.30.10) --- app/deps/sdl.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/deps/sdl.sh b/app/deps/sdl.sh index 8698e120..c098e367 100755 --- a/app/deps/sdl.sh +++ b/app/deps/sdl.sh @@ -5,10 +5,10 @@ cd "$DEPS_DIR" . common process_args "$@" -VERSION=2.30.9 +VERSION=2.30.10 FILENAME=SDL-$VERSION.tar.gz PROJECT_DIR=SDL-release-$VERSION -SHA256SUM=682a055004081e37d81a7d4ce546c3ee3ef2e0e6a675ed2651e430ccd14eb407 +SHA256SUM=35a8b9c4f3635d85762b904ac60ca4e0806bff89faeb269caafbe80860d67168 cd "$SOURCES_DIR" From 28b5bfb90e76f059571a88931b86eb86f6ca8dd7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 9 Dec 2024 09:29:29 +0100 Subject: [PATCH 2160/2244] Revert "Start cleanup process with setsid or nohup" This reverts commit c59a3c3169973abb4ce236e06990d58ae6567481. The next commit will use Os.setsid() instead. --- .../java/com/genymobile/scrcpy/CleanUp.java | 28 +++++++------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java index ac265229..f372855b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java +++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java @@ -10,8 +10,6 @@ import android.os.BatteryManager; import java.io.File; import java.io.IOException; import java.io.OutputStream; -import java.util.ArrayList; -import java.util.List; /** * Handle the cleanup of scrcpy, even if the main process is killed. @@ -109,22 +107,16 @@ public final class CleanUp { private void run(int displayId, int restoreStayOn, boolean disableShowTouches, boolean powerOffScreen, int restoreScreenOffTimeout) throws IOException { - - List cmd = new ArrayList<>(); - if (new File("/system/bin/setsid").exists()) { - cmd.add("/system/bin/setsid"); - } else if (new File("/system/bin/nohup").exists()) { - cmd.add("/system/bin/nohup"); - } - - cmd.add("app_process"); - cmd.add("/"); - cmd.add(CleanUp.class.getName()); - cmd.add(String.valueOf(displayId)); - cmd.add(String.valueOf(restoreStayOn)); - cmd.add(String.valueOf(disableShowTouches)); - cmd.add(String.valueOf(powerOffScreen)); - cmd.add(String.valueOf(restoreScreenOffTimeout)); + String[] cmd = { + "app_process", + "/", + CleanUp.class.getName(), + String.valueOf(displayId), + String.valueOf(restoreStayOn), + String.valueOf(disableShowTouches), + String.valueOf(powerOffScreen), + String.valueOf(restoreScreenOffTimeout), + }; ProcessBuilder builder = new ProcessBuilder(cmd); builder.environment().put("CLASSPATH", Server.SERVER_PATH); From a9aadc95df6ec51198430a986ac8f56434b25e9d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 9 Dec 2024 09:33:08 +0100 Subject: [PATCH 2161/2244] Start cleanup process with setsid() Reimplement c59a3c3169973abb4ce236e06990d58ae6567481 using Os.setsid(). Refs #5613 comment Suggested-by: Simon Chan <1330321+yume-chan@users.noreply.github.com> --- server/src/main/java/com/genymobile/scrcpy/CleanUp.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java index f372855b..49b23e81 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java +++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java @@ -6,6 +6,8 @@ import com.genymobile.scrcpy.util.Settings; import com.genymobile.scrcpy.util.SettingsException; import android.os.BatteryManager; +import android.system.ErrnoException; +import android.system.Os; import java.io.File; import java.io.IOException; @@ -163,6 +165,12 @@ public final class CleanUp { } public static void main(String... args) { + try { + // Start a new session to avoid being terminated along with the server process on some devices + Os.setsid(); + } catch (ErrnoException e) { + Ln.e("setsid() failed", e); + } unlinkSelf(); int displayId = Integer.parseInt(args[0]); From a507b4f5593d960aacb075082fd7b55cb672ee32 Mon Sep 17 00:00:00 2001 From: Simon Chan <1330321+yume-chan@users.noreply.github.com> Date: Mon, 9 Dec 2024 10:11:17 +0100 Subject: [PATCH 2162/2244] Fix DisplayControl classpath Use the full system server classpath to load DisplayControl, so that turning the screen off on Android 14+ does not crash on certain devices. Refs #4544 comment Fixes #4544 Fixes #5274 Signed-off-by: Romain Vimont --- .../java/com/genymobile/scrcpy/wrappers/DisplayControl.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayControl.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayControl.java index a57f7948..88ca3d3d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayControl.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayControl.java @@ -6,6 +6,7 @@ import com.genymobile.scrcpy.util.Ln; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.os.IBinder; +import android.system.Os; import java.lang.reflect.Method; @@ -21,7 +22,9 @@ public final class DisplayControl { Class classLoaderFactoryClass = Class.forName("com.android.internal.os.ClassLoaderFactory"); Method createClassLoaderMethod = classLoaderFactoryClass.getDeclaredMethod("createClassLoader", String.class, String.class, String.class, ClassLoader.class, int.class, boolean.class, String.class); - ClassLoader classLoader = (ClassLoader) createClassLoaderMethod.invoke(null, "/system/framework/services.jar", null, null, + + String systemServerClasspath = Os.getenv("SYSTEMSERVERCLASSPATH"); + ClassLoader classLoader = (ClassLoader) createClassLoaderMethod.invoke(null, systemServerClasspath, null, null, ClassLoader.getSystemClassLoader(), 0, true, null); displayControlClass = classLoader.loadClass("com.android.server.display.DisplayControl"); From f2018e026c5748db89a31bc2f3535942c081a9ba Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 9 Dec 2024 18:23:56 +0100 Subject: [PATCH 2163/2244] Remove broken macOS flags Due to a typo (a space was missing before the second '-L'), the resulting LDFLAGS value was broken: "-L/opt/homebrew/opt/zlib/lib-L/opt/homebrew/opt/libiconv/lib" This proves that the flag was useless. Remove it. Refs #5517 comment PR #5644 --- app/deps/ffmpeg.sh | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/deps/ffmpeg.sh b/app/deps/ffmpeg.sh index 386de190..acf11584 100755 --- a/app/deps/ffmpeg.sh +++ b/app/deps/ffmpeg.sh @@ -40,11 +40,6 @@ else export LDFLAGS='-static-libgcc -static' elif [[ "$HOST" == "macos" ]] then - export LDFLAGS="$LDFLAGS -L/opt/homebrew/opt/zlib/lib" - export CPPFLAGS="$CPPFLAGS -I/opt/homebrew/opt/zlib/include" - - export LDFLAGS="$LDFLAGS-L/opt/homebrew/opt/libiconv/lib" - export CPPFLAGS="$CPPFLAGS -I/opt/homebrew/opt/libiconv/include" export PKG_CONFIG_PATH="/opt/homebrew/opt/zlib/lib/pkgconfig" fi From aca6d30af5338e27571ed124ff3ef26479b214c5 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 9 Dec 2024 18:30:37 +0100 Subject: [PATCH 2164/2244] Include dav1d in releases Scrcpy supports AV1, but no decoder was provided in binary releases. Include dav1d: - - Fixes #4744 PR #5644 --- app/deps/dav1d.sh | 68 ++++++++++++++++++++++++++++++++++++++++ app/deps/ffmpeg.sh | 5 +++ release/build_linux.sh | 1 + release/build_macos.sh | 1 + release/build_windows.sh | 1 + 5 files changed, 76 insertions(+) create mode 100755 app/deps/dav1d.sh diff --git a/app/deps/dav1d.sh b/app/deps/dav1d.sh new file mode 100755 index 00000000..3069b6fe --- /dev/null +++ b/app/deps/dav1d.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +set -ex +DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) +cd "$DEPS_DIR" +. common +process_args "$@" + +VERSION=1.5.0 +FILENAME=dav1d-$VERSION.tar.gz +PROJECT_DIR=dav1d-$VERSION +SHA256SUM=78b15d9954b513ea92d27f39362535ded2243e1b0924fde39f37a31ebed5f76b + +cd "$SOURCES_DIR" + +if [[ -d "$PROJECT_DIR" ]] +then + echo "$PWD/$PROJECT_DIR" found +else + get_file "https://code.videolan.org/videolan/dav1d/-/archive/$VERSION/$FILENAME" "$FILENAME" "$SHA256SUM" + tar xf "$FILENAME" # First level directory is "$PROJECT_DIR" +fi + +mkdir -p "$BUILD_DIR/$PROJECT_DIR" +cd "$BUILD_DIR/$PROJECT_DIR" + +if [[ -d "$DIRNAME" ]] +then + echo "'$PWD/$DIRNAME' already exists, not reconfigured" + cd "$DIRNAME" +else + mkdir "$DIRNAME" + cd "$DIRNAME" + + conf=( + --prefix="$INSTALL_DIR/$DIRNAME" + --libdir=lib + -Denable_tests=false + -Denable_tools=false + # Always build dav1d statically + --default-library=static + ) + + if [[ "$BUILD_TYPE" == cross ]] + then + case "$HOST" in + win32) + conf+=( + --cross-file="$SOURCES_DIR/$PROJECT_DIR/package/crossfiles/i686-w64-mingw32.meson" + ) + ;; + + win64) + conf+=( + --cross-file="$SOURCES_DIR/$PROJECT_DIR/package/crossfiles/x86_64-w64-mingw32.meson" + ) + ;; + + *) + echo "Unsupported host: $HOST" >&2 + exit 1 + esac + fi + + meson setup . "$SOURCES_DIR/$PROJECT_DIR" "${conf[@]}" +fi + +ninja +ninja install diff --git a/app/deps/ffmpeg.sh b/app/deps/ffmpeg.sh index acf11584..d268ca91 100755 --- a/app/deps/ffmpeg.sh +++ b/app/deps/ffmpeg.sh @@ -43,8 +43,11 @@ else export PKG_CONFIG_PATH="/opt/homebrew/opt/zlib/lib/pkgconfig" fi + export PKG_CONFIG_PATH="$INSTALL_DIR/$DIRNAME/lib/pkgconfig:$PKG_CONFIG_PATH" + conf=( --prefix="$INSTALL_DIR/$DIRNAME" + --pkg-config-flags="--static" --extra-cflags="-O2 -fPIC" --disable-programs --disable-doc @@ -57,9 +60,11 @@ else --disable-vaapi --disable-vdpau --enable-swresample + --enable-libdav1d --enable-decoder=h264 --enable-decoder=hevc --enable-decoder=av1 + --enable-decoder=libdav1d --enable-decoder=pcm_s16le --enable-decoder=opus --enable-decoder=aac diff --git a/release/build_linux.sh b/release/build_linux.sh index ccf24575..6bca6979 100755 --- a/release/build_linux.sh +++ b/release/build_linux.sh @@ -15,6 +15,7 @@ LINUX_BUILD_DIR="$WORK_DIR/build-linux-$ARCH" app/deps/adb_linux.sh app/deps/sdl.sh linux native static +app/deps/dav1d.sh linux native static app/deps/ffmpeg.sh linux native static app/deps/libusb.sh linux native static diff --git a/release/build_macos.sh b/release/build_macos.sh index 2c41d04e..8f4beb9b 100755 --- a/release/build_macos.sh +++ b/release/build_macos.sh @@ -15,6 +15,7 @@ MACOS_BUILD_DIR="$WORK_DIR/build-macos-$ARCH" app/deps/adb_macos.sh app/deps/sdl.sh macos native static +app/deps/dav1d.sh macos native static app/deps/ffmpeg.sh macos native static app/deps/libusb.sh macos native static diff --git a/release/build_windows.sh b/release/build_windows.sh index dbd6cbf4..c83d2e31 100755 --- a/release/build_windows.sh +++ b/release/build_windows.sh @@ -22,6 +22,7 @@ WINXX_BUILD_DIR="$WORK_DIR/build-$WINXX" app/deps/adb_windows.sh app/deps/sdl.sh $WINXX cross shared +app/deps/dav1d.sh $WINXX cross shared app/deps/ffmpeg.sh $WINXX cross shared app/deps/libusb.sh $WINXX cross shared From 754f4fc6fec42774183a0e821be2a8852366b7bd Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 8 Dec 2024 18:15:46 +0100 Subject: [PATCH 2165/2244] Bump version to 3.1 --- app/scrcpy-windows.rc | 2 +- meson.build | 2 +- server/build.gradle | 4 ++-- server/build_without_gradle.sh | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/scrcpy-windows.rc b/app/scrcpy-windows.rc index 0f1caf87..2c441aa1 100644 --- a/app/scrcpy-windows.rc +++ b/app/scrcpy-windows.rc @@ -13,7 +13,7 @@ BEGIN VALUE "LegalCopyright", "Romain Vimont, Genymobile" VALUE "OriginalFilename", "scrcpy.exe" VALUE "ProductName", "scrcpy" - VALUE "ProductVersion", "3.0.2" + VALUE "ProductVersion", "3.1" END END BLOCK "VarFileInfo" diff --git a/meson.build b/meson.build index badc1adb..aa1a3a3b 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('scrcpy', 'c', - version: '3.0.2', + version: '3.1', meson_version: '>= 0.48', default_options: [ 'c_std=c11', diff --git a/server/build.gradle b/server/build.gradle index 4b7b0254..9c0543e9 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -7,8 +7,8 @@ android { applicationId "com.genymobile.scrcpy" minSdkVersion 21 targetSdkVersion 35 - versionCode 30002 - versionName "3.0.2" + versionCode 30100 + versionName "3.1" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index f2420f64..d16592b4 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=3.0.2 +SCRCPY_VERSION_NAME=3.1 PLATFORM=${ANDROID_PLATFORM:-35} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-35.0.0} From 0e2d084751b0513f5db1b7e7afc5460766f4b5c7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 8 Dec 2024 18:36:20 +0100 Subject: [PATCH 2166/2244] Update links to 3.1 --- README.md | 2 +- doc/build.md | 6 +++--- doc/linux.md | 6 +++--- doc/macos.md | 12 ++++++------ doc/windows.md | 12 ++++++------ install_release.sh | 4 ++-- 6 files changed, 21 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 601085da..09fa12b4 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ source for the project. Do not download releases from random websites, even if their name contains `scrcpy`.** -# scrcpy (v3.0.2) +# scrcpy (v3.1) scrcpy diff --git a/doc/build.md b/doc/build.md index 20d1f0f5..2776ed01 100644 --- a/doc/build.md +++ b/doc/build.md @@ -233,10 +233,10 @@ install` must be run as root)._ #### Option 2: Use prebuilt server - - [`scrcpy-server-v3.0.2`][direct-scrcpy-server] - SHA-256: `e19fe024bfa3367809494407ad6ca809a6f6e77dac95e99f85ba75144e0ba35d` + - [`scrcpy-server-v3.1`][direct-scrcpy-server] + SHA-256: `958f0944a62f23b1f33a16e9eb14844c1a04b882ca175a738c16d23cb22b86c0` -[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v3.0.2/scrcpy-server-v3.0.2 +[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v3.1/scrcpy-server-v3.1 Download the prebuilt server somewhere, and specify its path during the Meson configuration: diff --git a/doc/linux.md b/doc/linux.md index db4d7977..9beaed1e 100644 --- a/doc/linux.md +++ b/doc/linux.md @@ -6,11 +6,11 @@ Download a static build of the [latest release]: - - [`scrcpy-linux-x86_64-v3.0.2.tar.gz`][direct-linux-x86_64] (x86_64) - SHA-256: `20b69dcd379bb7d7208bf1e4858cf04162fc856697be0e6c03863d7b3c1e734a` + - [`scrcpy-linux-x86_64-v3.1.tar.gz`][direct-linux-x86_64] (x86_64) + SHA-256: `37dba54092ed9ec6b2f8f95432f61b8ea124aec9f1e9f2b3d22d4b10bb04c59a` [latest release]: https://github.com/Genymobile/scrcpy/releases/latest -[direct-linux-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.0.2/scrcpy-linux-x86_64-v3.0.2.tar.gz +[direct-linux-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.1/scrcpy-linux-x86_64-v3.1.tar.gz and extract it. diff --git a/doc/macos.md b/doc/macos.md index af92f6be..56d9f168 100644 --- a/doc/macos.md +++ b/doc/macos.md @@ -6,15 +6,15 @@ Download a static build of the [latest release]: - - [`scrcpy-macos-aarch64-v3.0.2.tar.gz`][direct-macos-aarch64] (aarch64) - SHA-256: `811ba2f4e856146bdd161e24c3490d78efbec2339ca783fac791d041c0aecfb6` + - [`scrcpy-macos-aarch64-v3.1.tar.gz`][direct-macos-aarch64] (aarch64) + SHA-256: `478618d940421e5f57942f5479d493ecbb38210682937a200f712aee5f235daf` - - [`scrcpy-macos-x86_64-v3.0.2.tar.gz`][direct-macos-x86_64] (x86_64) - SHA-256: `8effff54dca3a3e46eaaec242771a13a7f81af2e18670b3d0d8ed6b461bb4f79` + - [`scrcpy-macos-x86_64-v3.1.tar.gz`][direct-macos-x86_64] (x86_64) + SHA-256: `acde98e29c273710ffa469371dbca4a728a44c41c380381f8a54e5b5301b9e87` [latest release]: https://github.com/Genymobile/scrcpy/releases/latest -[direct-macos-aarch64]: https://github.com/Genymobile/scrcpy/releases/download/v3.0.2/scrcpy-macos-aarch64-v3.0.2.tar.gz -[direct-macos-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.0.2/scrcpy-macos-x86_64-v3.0.2.tar.gz +[direct-macos-aarch64]: https://github.com/Genymobile/scrcpy/releases/download/v3.1/scrcpy-macos-aarch64-v3.1.tar.gz +[direct-macos-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.1/scrcpy-macos-x86_64-v3.1.tar.gz and extract it. diff --git a/doc/windows.md b/doc/windows.md index e0f0a1b3..ec7b904b 100644 --- a/doc/windows.md +++ b/doc/windows.md @@ -6,14 +6,14 @@ Download the [latest release]: - - [`scrcpy-win64-v3.0.2.zip`][direct-win64] (64-bit) - SHA-256: `f0de59f5d46127c87cd822d39d6665e016b86db4cd048101b262f6adb6766832` - - [`scrcpy-win32-v3.0.2.zip`][direct-win32] (32-bit) - SHA-256: `8db8d4984d642012c55802de71f507f8ff9f68a8cfed456d7a1982d47e065f64` + - [`scrcpy-win64-v3.1.zip`][direct-win64] (64-bit) + SHA-256: `0c05ea395d95cfe36bee974eeb435a3db87ea5594ff738370d5dc3068a9538ca` + - [`scrcpy-win32-v3.1.zip`][direct-win32] (32-bit) + SHA-256: `2b4674ef76719680ac5a9b482d1943bdde3fa25821ad2e98f3c40c347d00d560` [latest release]: https://github.com/Genymobile/scrcpy/releases/latest -[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v3.0.2/scrcpy-win64-v3.0.2.zip -[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v3.0.2/scrcpy-win32-v3.0.2.zip +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v3.1/scrcpy-win64-v3.1.zip +[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v3.1/scrcpy-win32-v3.1.zip and extract it. diff --git a/install_release.sh b/install_release.sh index 5a6eaa7b..3774be86 100755 --- a/install_release.sh +++ b/install_release.sh @@ -2,8 +2,8 @@ set -e BUILDDIR=build-auto -PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v3.0.2/scrcpy-server-v3.0.2 -PREBUILT_SERVER_SHA256=e19fe024bfa3367809494407ad6ca809a6f6e77dac95e99f85ba75144e0ba35d +PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v3.1/scrcpy-server-v3.1 +PREBUILT_SERVER_SHA256=958f0944a62f23b1f33a16e9eb14844c1a04b882ca175a738c16d23cb22b86c0 echo "[scrcpy] Downloading prebuilt server..." wget "$PREBUILT_SERVER_URL" -O scrcpy-server From 6469054b15355e55ddfa713fa9cef5b88fa46358 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 12 Dec 2024 18:07:58 +0100 Subject: [PATCH 2167/2244] Revert "Remove apt update on GitHub Actions" This reverts commit 678025b31672c230575fe2dbc4a0d487d5010bb1. This avoids spurious errors on the CI: E: Unable to fetch some archives, maybe run apt-get update or try with --fix-missing? --- .github/workflows/release.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a77b7ff1..a5701b0a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -74,6 +74,7 @@ jobs: - name: Install dependencies run: | + sudo apt update sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \ libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \ libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \ @@ -99,6 +100,7 @@ jobs: - name: Install dependencies run: | + sudo apt update sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \ libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \ libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \ @@ -129,6 +131,7 @@ jobs: - name: Install dependencies run: | + sudo apt update sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \ libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \ libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \ @@ -162,6 +165,7 @@ jobs: - name: Install dependencies run: | + sudo apt update sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \ libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \ libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \ From f751274b1762a183d2848a86458b8a459b50250a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 12 Dec 2024 17:52:17 +0100 Subject: [PATCH 2168/2244] Define both pkg-config and pkgconfig for meson In Meson cross-files, "pkgconfig" was deprecated in favor of "pkg-config" in meson 1.3.0. The new name is used since 85a94dd4b563e961304b2d9082932c5c1cc2e582 to avoid a warning, but then it fails with older versions of meson. To avoid the problem, define both pkg-config and pkgconfig. > For backward compatibility it is still allowed to define both with the > same value, in that case no deprecation warning is printed. --- .github/workflows/release.yml | 6 ------ cross_win32.txt | 2 ++ cross_win64.txt | 2 ++ 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a5701b0a..da021c6e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -137,9 +137,6 @@ jobs: libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \ mingw-w64 mingw-w64-tools libz-mingw-w64-dev - - name: Workaround for old meson version run by Github Actions - run: sed -i 's/^pkg-config/pkgconfig/' cross_win32.txt - - name: Build run: release/build_windows.sh 32 @@ -171,9 +168,6 @@ jobs: libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \ mingw-w64 mingw-w64-tools libz-mingw-w64-dev - - name: Workaround for old meson version run by Github Actions - run: sed -i 's/^pkg-config/pkgconfig/' cross_win64.txt - - name: Build run: release/build_windows.sh 64 diff --git a/cross_win32.txt b/cross_win32.txt index 05f9a86b..ddbc65f3 100644 --- a/cross_win32.txt +++ b/cross_win32.txt @@ -7,6 +7,8 @@ cpp = 'i686-w64-mingw32-g++' ar = 'i686-w64-mingw32-ar' strip = 'i686-w64-mingw32-strip' pkg-config = 'i686-w64-mingw32-pkg-config' +# backward compatibility +pkgconfig = 'i686-w64-mingw32-pkg-config' windres = 'i686-w64-mingw32-windres' [host_machine] diff --git a/cross_win64.txt b/cross_win64.txt index 86364ad6..a6f16e16 100644 --- a/cross_win64.txt +++ b/cross_win64.txt @@ -7,6 +7,8 @@ cpp = 'x86_64-w64-mingw32-g++' ar = 'x86_64-w64-mingw32-ar' strip = 'x86_64-w64-mingw32-strip' pkg-config = 'x86_64-w64-mingw32-pkg-config' +# backward compatibility +pkgconfig = 'x86_64-w64-mingw32-pkg-config' windres = 'x86_64-w64-mingw32-windres' [host_machine] From 17e205e54f8c975c18d3466ce2a9a5663bfbaf96 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 12 Dec 2024 17:59:36 +0100 Subject: [PATCH 2169/2244] Replace meson join_paths() by '/' A new '/' operator was introduced in Meson 0.49 to replace join_paths(): - - Refs #5658 --- app/meson.build | 10 +++++----- meson.build | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/meson.build b/app/meson.build index be02ebc1..85e88940 100644 --- a/app/meson.build +++ b/app/meson.build @@ -192,19 +192,19 @@ datadir = get_option('datadir') # by default 'share' install_man('scrcpy.1') install_data('data/icon.png', rename: 'scrcpy.png', - install_dir: join_paths(datadir, 'icons/hicolor/256x256/apps')) + install_dir: datadir / 'icons/hicolor/256x256/apps') install_data('data/zsh-completion/_scrcpy', - install_dir: join_paths(datadir, 'zsh/site-functions')) + install_dir: datadir / 'zsh/site-functions') install_data('data/bash-completion/scrcpy', - install_dir: join_paths(datadir, 'bash-completion/completions')) + install_dir: datadir / 'bash-completion/completions') # Desktop entry file for application launchers if host_machine.system() == 'linux' # Install a launcher (ex: /usr/local/share/applications/scrcpy.desktop) install_data('data/scrcpy.desktop', - install_dir: join_paths(datadir, 'applications')) + install_dir: datadir / 'applications') install_data('data/scrcpy-console.desktop', - install_dir: join_paths(datadir, 'applications')) + install_dir: datadir / 'applications') endif diff --git a/meson.build b/meson.build index aa1a3a3b..84784814 100644 --- a/meson.build +++ b/meson.build @@ -1,6 +1,6 @@ project('scrcpy', 'c', version: '3.1', - meson_version: '>= 0.48', + meson_version: '>= 0.49', default_options: [ 'c_std=c11', 'warning_level=2', From ec4e826976d977870fc35d33591fd6164ee9d792 Mon Sep 17 00:00:00 2001 From: Colin Kinloch Date: Thu, 12 Dec 2024 12:41:22 +0000 Subject: [PATCH 2170/2244] Set icon and server env paths for meson devenv This allows users to compile and run the project in a dev environment. meson setup x meson compile -C x meson devenv -C x scrcpy This is an alternative to `./run x`. PR #5658 Signed-off-by: Romain Vimont --- app/meson.build | 6 ++++++ server/meson.build | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/app/meson.build b/app/meson.build index 85e88940..f7df69eb 100644 --- a/app/meson.build +++ b/app/meson.build @@ -279,3 +279,9 @@ if get_option('buildtype') == 'debug' test(t[0], exe) endforeach endif + +if meson.version().version_compare('>= 0.58.0') + devenv = environment() + devenv.set('SCRCPY_ICON_PATH', meson.current_source_dir() / 'data/icon.png') + meson.add_devenv(devenv) +endif diff --git a/server/meson.build b/server/meson.build index 42b97981..55828e2d 100644 --- a/server/meson.build +++ b/server/meson.build @@ -23,3 +23,9 @@ else install: true, install_dir: 'share/scrcpy') endif + +if meson.version().version_compare('>= 0.58.0') + devenv = environment() + devenv.set('SCRCPY_SERVER_PATH', meson.current_build_dir() / 'scrcpy-server') + meson.add_devenv(devenv) +endif From 69264703b11614d022c31096d70eec14870393c7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 14 Dec 2024 10:25:13 +0100 Subject: [PATCH 2171/2244] Add missing comments in workarounds The implementation of workarounds uses a lot of reflection code. For better readability, always write the equivalent using direct Java code. --- server/src/main/java/com/genymobile/scrcpy/Workarounds.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java index eec00a04..a5283a96 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java +++ b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java @@ -132,10 +132,13 @@ public final class Workarounds { try { Class configurationControllerClass = Class.forName("android.app.ConfigurationController"); Class activityThreadInternalClass = Class.forName("android.app.ActivityThreadInternal"); + + // configurationController = new ConfigurationController(ACTIVITY_THREAD); Constructor configurationControllerConstructor = configurationControllerClass.getDeclaredConstructor(activityThreadInternalClass); configurationControllerConstructor.setAccessible(true); Object configurationController = configurationControllerConstructor.newInstance(ACTIVITY_THREAD); + // ACTIVITY_THREAD.mConfigurationController = configurationController; Field configurationControllerField = ACTIVITY_THREAD_CLASS.getDeclaredField("mConfigurationController"); configurationControllerField.setAccessible(true); configurationControllerField.set(ACTIVITY_THREAD, configurationController); From dc2fcc46f516588f4575c1fe8cfeca3e57a1653c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 14 Dec 2024 10:15:20 +0100 Subject: [PATCH 2172/2244] Add workaround for Pico 4 Ultra Make ActivityThread.isSystem() return true to avoid a NullPointerException later. Refs #5659 comment Fixes #5659 --- server/src/main/java/com/genymobile/scrcpy/Workarounds.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java index a5283a96..fb4c1389 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java +++ b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java @@ -42,6 +42,11 @@ public final class Workarounds { Field sCurrentActivityThreadField = ACTIVITY_THREAD_CLASS.getDeclaredField("sCurrentActivityThread"); sCurrentActivityThreadField.setAccessible(true); sCurrentActivityThreadField.set(null, ACTIVITY_THREAD); + + // activityThread.mSystemThread = true; + Field mSystemThreadField = ACTIVITY_THREAD_CLASS.getDeclaredField("mSystemThread"); + mSystemThreadField.setAccessible(true); + mSystemThreadField.setBoolean(ACTIVITY_THREAD, true); } catch (Exception e) { throw new AssertionError(e); } From ea6a94d355b92b103da8931d175f2a4a35e8e301 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 17 Dec 2024 12:20:50 +0100 Subject: [PATCH 2173/2244] Fix mouse documentation formatting Make the format consistent with the shortcuts documentation. --- doc/mouse.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/doc/mouse.md b/doc/mouse.md index 3607a92c..0bea4aea 100644 --- a/doc/mouse.md +++ b/doc/mouse.md @@ -83,9 +83,9 @@ process like the _adb daemon_). ## Mouse bindings By default, with SDK mouse: - - right-click triggers BACK (or POWER on) - - middle-click triggers HOME - - the 4th click triggers APP_SWITCH + - right-click triggers `BACK` (or `POWER` on) + - middle-click triggers `HOME` + - the 4th click triggers `APP_SWITCH` - the 5th click expands the notification panel The secondary clicks may be forwarded to the device instead by pressing the @@ -121,9 +121,9 @@ Each character must be one of the following: - `+`: forward the click to the device - `-`: ignore the click - - `b`: trigger shortcut BACK (or turn screen on if off) - - `h`: trigger shortcut HOME - - `s`: trigger shortcut APP_SWITCH + - `b`: trigger shortcut `BACK` (or turn screen on if off) + - `h`: trigger shortcut `HOME` + - `s`: trigger shortcut `APP_SWITCH` - `n`: trigger shortcut "expand notification panel" For example: From 48fc18e3806e6eb77772f9f89671884b40c61714 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 17 Dec 2024 12:21:59 +0100 Subject: [PATCH 2174/2244] Add must-know tips All users should be aware of the main shortcuts and the most important setting to improve performance. --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index 09fa12b4..5eb59ba5 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,16 @@ Note that USB debugging is not required to run scrcpy in [OTG mode](doc/otg.md). - [macOS](doc/macos.md) +## Must-know tips + + - [Reducing resolution](doc/video.md#size) may greatly improve performance + (`scrcpy -m1024`) + - [_Right-click_](doc/mouse.md#mouse-bindings) triggers `BACK` + - [_Middle-click_](doc/mouse.md#mouse-bindings) triggers `HOME` + - Alt+f toggles [fullscreen](doc/window.md#fullscreen) + - There are many other [shortcuts](doc/shortcuts.md) + + ## Usage examples There are a lot of options, [documented](#user-documentation) in separate pages. From 1fd57ede1f7caca8d9dad73b0fe778079fad73f1 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 17 Dec 2024 13:09:24 +0100 Subject: [PATCH 2175/2244] Move "screen off timeout" section in documentation Place the "screen off timeout" section right after "stay awake", as they serve a similar purpose. --- doc/device.md | 50 +++++++++++++++++++++++++------------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/doc/device.md b/doc/device.md index 42208faa..ab1e6ba4 100644 --- a/doc/device.md +++ b/doc/device.md @@ -34,6 +34,31 @@ adb shell settings put global stay_on_while_plugged_in 0 ``` +## Screen off timeout + +The Android screen automatically turns off after some delay. + +To change this delay while scrcpy is running: + +```bash +scrcpy --screen-off-timeout=300 # 300 seconds (5 minutes) +``` + +The initial value is restored on exit. + +It is possible to change this setting manually: + +```bash +# get the current screen_off_timeout value +adb shell settings get system screen_off_timeout +# set a new value (in milliseconds) +adb shell settings put system screen_off_timeout 30000 +``` + +Note that the Android value is in milliseconds, but the scrcpy command line +argument is in seconds. + + ## Turn screen off It is possible to turn the device screen off while mirroring on start with a @@ -71,31 +96,6 @@ adb shell cmd display power-on 0 ``` -## Screen off timeout - -The Android screen automatically turns off after some delay. - -To change this delay while scrcpy is running: - -```bash -scrcpy --screen-off-timeout=300 # 300 seconds (5 minutes) -``` - -The initial value is restored on exit. - -It is possible to change this setting manually: - -```bash -# get the current screen_off_timeout value -adb shell settings get system screen_off_timeout -# set a new value (in milliseconds) -adb shell settings put system screen_off_timeout 30000 -``` - -Note that the Android value is in milliseconds, but the scrcpy command line -argument is in seconds. - - ## Show touches For presentations, it may be useful to show physical touches (on the physical From 5ae01749bf4fad9fb4d8bf7c879dc60f479c1013 Mon Sep 17 00:00:00 2001 From: Markus <65797058+headquarter8302@users.noreply.github.com> Date: Wed, 18 Dec 2024 21:12:58 -0400 Subject: [PATCH 2176/2244] Reintroduce WinGet install note This semantically reverts c27ab46efbcab0b9558a91e691d799ffef496c97. WinGet package has been fixed by: Refs #4027 PR #5686 Signed-off-by: Romain Vimont --- doc/windows.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/doc/windows.md b/doc/windows.md index ec7b904b..89b80727 100644 --- a/doc/windows.md +++ b/doc/windows.md @@ -20,6 +20,12 @@ and extract it. ### From a package manager +From [WinGet] (ADB and other dependencies will be installed alongside scrcpy): + +```bash +winget install --exact Genymobile.scrcpy +``` + From [Chocolatey]: ```bash @@ -29,12 +35,12 @@ choco install adb # if you don't have it yet From [Scoop]: - ```bash scoop install scrcpy scoop install adb # if you don't have it yet ``` +[WinGet]: https://github.com/microsoft/winget-cli [Chocolatey]: https://chocolatey.org/ [Scoop]: https://scoop.sh From fb47b87eebbda65fd28e407cd2a6d33fea476fe3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 20 Dec 2024 20:57:20 +0100 Subject: [PATCH 2177/2244] Fix pipe read return value The function incorrectly returned false, whereas its return type is ssize_t. --- app/src/util/process_intr.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/util/process_intr.c b/app/src/util/process_intr.c index d37bd5a5..641440ab 100644 --- a/app/src/util/process_intr.c +++ b/app/src/util/process_intr.c @@ -5,7 +5,7 @@ sc_pipe_read_intr(struct sc_intr *intr, sc_pid pid, sc_pipe pipe, char *data, size_t len) { if (intr && !sc_intr_set_process(intr, pid)) { // Already interrupted - return false; + return -1; } ssize_t ret = sc_pipe_read(pipe, data, len); @@ -22,7 +22,7 @@ sc_pipe_read_all_intr(struct sc_intr *intr, sc_pid pid, sc_pipe pipe, char *data, size_t len) { if (intr && !sc_intr_set_process(intr, pid)) { // Already interrupted - return false; + return -1; } ssize_t ret = sc_pipe_read_all(pipe, data, len); From 95c4f03c1bfd566b383780977b3473ebad6477ee Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 19 Dec 2024 12:25:20 +0100 Subject: [PATCH 2178/2244] Build static linux binary on Ubuntu 22.04 On Github Actions, ubuntu-latest now points to ubuntu-24.04, which uses a newer version of glibc (2.39). As a result, the binaries fail to work on systems with older versions of glibc, such as Debian Bookworm. To ensure better compatibility, continue building the static Linux binary on Ubuntu 22.04 (with glibc 2.35). Fixes #5689 --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index da021c6e..c90b7fb0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -84,7 +84,7 @@ jobs: run: release/test_client.sh build-linux-x86_64: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - name: Check architecture run: | From 2f44da76f4767aec2f42e6882a30c62615e0f139 Mon Sep 17 00:00:00 2001 From: Simon Chan <1330321+yume-chan@users.noreply.github.com> Date: Sun, 15 Dec 2024 23:50:58 +0800 Subject: [PATCH 2179/2244] Filter out non-backward-compatible cameras PR #5669 Signed-off-by: Romain Vimont --- .../com/genymobile/scrcpy/util/LogUtils.java | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java b/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java index 088be7e7..961b8da0 100644 --- a/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java @@ -120,6 +120,21 @@ public final class LogUtils { } } + private static boolean isCameraBackwardCompatible(CameraCharacteristics characteristics) { + int[] capabilities = characteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES); + if (capabilities == null) { + return false; + } + + for (int capability : capabilities) { + if (capability == CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_BACKWARD_COMPATIBLE) { + return true; + } + } + + return false; + } + public static String buildCameraListMessage(boolean includeSizes) { StringBuilder builder = new StringBuilder("List of cameras:"); CameraManager cameraManager = ServiceManager.getCameraManager(); @@ -129,9 +144,16 @@ public final class LogUtils { builder.append("\n (none)"); } else { for (String id : cameraIds) { - builder.append("\n --camera-id=").append(id); CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(id); + if (!isCameraBackwardCompatible(characteristics)) { + // Ignore depth cameras as suggested by official documentation + // + continue; + } + + builder.append("\n --camera-id=").append(id); + int facing = characteristics.get(CameraCharacteristics.LENS_FACING); builder.append(" (").append(getCameraFacingName(facing)).append(", "); From 538764416099c803fcb041ecffa2c849829b7222 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 22 Dec 2024 21:17:51 +0100 Subject: [PATCH 2180/2244] Ignore low-FPS ranges if not available Do not report an error if the returned FPS ranges array is null. Refs #5669 --- .../src/main/java/com/genymobile/scrcpy/util/LogUtils.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java b/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java index 961b8da0..701ae373 100644 --- a/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java @@ -163,8 +163,10 @@ public final class LogUtils { try { // Capture frame rates for low-FPS mode are the same for every resolution Range[] lowFpsRanges = characteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES); - SortedSet uniqueLowFps = getUniqueSet(lowFpsRanges); - builder.append(", fps=").append(uniqueLowFps); + if (lowFpsRanges != null) { + SortedSet uniqueLowFps = getUniqueSet(lowFpsRanges); + builder.append(", fps=").append(uniqueLowFps); + } } catch (Exception e) { // Some devices may provide invalid ranges, causing an IllegalArgumentException "lower must be less than or equal to upper" Ln.w("Could not get available frame rates for camera " + id, e); From e0423653c892a62342bf665473093a9a020e0ffc Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 23 Dec 2024 10:58:02 +0100 Subject: [PATCH 2181/2244] Remove useless null check The method CameraManager.getCameraIdList() is annotated with @NonNull. This fixes a warning reported by Android Studio. --- server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java b/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java index 701ae373..4f8927ec 100644 --- a/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java @@ -140,7 +140,7 @@ public final class LogUtils { CameraManager cameraManager = ServiceManager.getCameraManager(); try { String[] cameraIds = cameraManager.getCameraIdList(); - if (cameraIds == null || cameraIds.length == 0) { + if (cameraIds.length == 0) { builder.append("\n (none)"); } else { for (String id : cameraIds) { From 69858c6f437b1bfece96bc291c607de842837d36 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 23 Dec 2024 11:01:42 +0100 Subject: [PATCH 2182/2244] Build static linux binary on Ubuntu 20.04 Use the oldest Ubuntu version currently available in GitHub Actions to ensure maximum compatibility with older systems. Refs 95c4f03c1bfd566b383780977b3473ebad6477ee Refs #5689 --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c90b7fb0..b1fedda9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -84,7 +84,7 @@ jobs: run: release/test_client.sh build-linux-x86_64: - runs-on: ubuntu-22.04 + runs-on: ubuntu-20.04 steps: - name: Check architecture run: | From 5b1229a55f8e89facaeb0d3757e37c49d62e88fb Mon Sep 17 00:00:00 2001 From: Simon Chan <1330321+yume-chan@users.noreply.github.com> Date: Mon, 23 Dec 2024 01:12:54 +0800 Subject: [PATCH 2183/2244] Support older macOS versions in CI build Fixes #5649 Fixes #5697 Signed-off-by: Romain Vimont --- .github/workflows/release.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b1fedda9..5875c6bf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -206,6 +206,13 @@ jobs: libtool - name: Build + env: + # the default Xcode (and macOS SDK) version can be found at + # + # + # then the minimal supported deployment target of that macOS SDK can be found at + # + MACOSX_DEPLOYMENT_TARGET: 10.13 run: release/build_macos.sh aarch64 # upload-artifact does not preserve permissions @@ -242,6 +249,13 @@ jobs: # autoconf and libtool are already installed on macos-13 - name: Build + env: + # the default Xcode (and macOS SDK) version can be found at + # + # + # then the minimal supported deployment target of that macOS SDK can be found at + # + MACOSX_DEPLOYMENT_TARGET: 10.13 run: release/build_macos.sh x86_64 # upload-artifact does not preserve permissions From af15c72f9caef4f829d337c35701d8bc00a58989 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 20 Dec 2024 20:58:41 +0100 Subject: [PATCH 2184/2244] Cleanup includes Improved manually with the help of neovim LSP warnings and iwyu: iwyu -Ibuilddir/app/ -Iapp/src/ app/src/XXX.c --- app/src/adb/adb.c | 5 +++-- app/src/adb/adb.h | 2 +- app/src/adb/adb_device.h | 1 - app/src/adb/adb_parser.c | 1 + app/src/adb/adb_parser.h | 4 ++-- app/src/adb/adb_tunnel.c | 4 ++-- app/src/audio_player.h | 4 +--- app/src/audio_regulator.c | 4 ++++ app/src/audio_regulator.h | 2 ++ app/src/cli.c | 2 ++ app/src/decoder.c | 8 +++----- app/src/decoder.h | 6 ++---- app/src/delay_buffer.c | 4 +--- app/src/delay_buffer.h | 1 + app/src/demuxer.c | 7 ++----- app/src/demuxer.h | 4 ---- app/src/device_msg.h | 4 ++-- app/src/display.c | 2 ++ app/src/display.h | 3 ++- app/src/events.c | 2 ++ app/src/file_pusher.c | 2 +- app/src/fps_counter.c | 1 + app/src/fps_counter.h | 2 +- app/src/frame_buffer.c | 2 -- app/src/frame_buffer.h | 1 + app/src/hid/hid_event.h | 1 + app/src/hid/hid_gamepad.c | 2 ++ app/src/hid/hid_gamepad.h | 1 + app/src/hid/hid_keyboard.c | 1 + app/src/hid/hid_keyboard.h | 1 + app/src/hid/hid_mouse.c | 2 ++ app/src/hid/hid_mouse.h | 2 -- app/src/icon.c | 11 ++++++++--- app/src/icon.h | 4 +--- app/src/input_events.h | 1 - app/src/input_manager.c | 6 +++++- app/src/input_manager.h | 6 +++--- app/src/keyboard_sdk.c | 5 +++++ app/src/main.c | 3 --- app/src/mouse_sdk.c | 2 +- app/src/mouse_sdk.h | 1 - app/src/opengl.c | 3 ++- app/src/options.c | 2 ++ app/src/options.h | 1 - app/src/packet_merger.c | 4 ++++ app/src/packet_merger.h | 2 +- app/src/receiver.c | 1 - app/src/recorder.c | 3 +++ app/src/recorder.h | 3 ++- app/src/scrcpy.c | 9 +++++---- app/src/scrcpy.h | 1 - app/src/screen.h | 10 ++++++---- app/src/server.c | 9 ++++----- app/src/server.h | 8 +++----- app/src/shortcut_mod.h | 1 + app/src/sys/unix/file.c | 3 ++- app/src/sys/unix/process.c | 2 ++ app/src/trait/frame_sink.h | 1 - app/src/trait/frame_source.c | 2 ++ app/src/trait/frame_source.h | 4 +++- app/src/trait/gamepad_processor.h | 3 --- app/src/trait/key_processor.h | 1 - app/src/trait/mouse_processor.h | 1 - app/src/trait/packet_sink.h | 1 - app/src/trait/packet_source.c | 2 ++ app/src/trait/packet_source.h | 4 +++- app/src/uhid/gamepad_uhid.c | 5 +++++ app/src/uhid/gamepad_uhid.h | 2 -- app/src/uhid/keyboard_uhid.c | 6 ++++++ app/src/uhid/mouse_uhid.c | 3 +++ app/src/uhid/uhid_output.c | 1 - app/src/uhid/uhid_output.h | 2 +- app/src/usb/aoa_hid.c | 7 +++++-- app/src/usb/aoa_hid.h | 7 ++----- app/src/usb/gamepad_aoa.c | 2 ++ app/src/usb/gamepad_aoa.h | 4 +--- app/src/usb/keyboard_aoa.h | 2 +- app/src/usb/mouse_aoa.c | 1 + app/src/usb/mouse_aoa.h | 2 +- app/src/usb/scrcpy_otg.c | 13 +++++++++++-- app/src/usb/screen_otg.c | 4 ++++ app/src/usb/screen_otg.h | 7 ++++--- app/src/util/acksync.c | 1 - app/src/util/acksync.h | 5 ++++- app/src/util/audiobuf.h | 1 + app/src/util/average.h | 3 --- app/src/util/binary.h | 1 - app/src/util/env.c | 4 +++- app/src/util/intmap.h | 1 + app/src/util/intr.c | 4 ++-- app/src/util/intr.h | 6 +++--- app/src/util/log.c | 5 ++++- app/src/util/net.c | 13 ++++++------- app/src/util/net.h | 3 ++- app/src/util/net_intr.h | 9 +++++++-- app/src/util/process.c | 2 -- app/src/util/process.h | 2 ++ app/src/util/process_intr.h | 4 ++-- app/src/util/str.c | 4 ++-- app/src/util/str.h | 2 ++ app/src/util/strbuf.c | 3 +-- app/src/util/thread.c | 4 +++- app/src/util/tick.c | 1 + app/src/util/timeout.c | 3 ++- app/src/util/timeout.h | 4 ++-- app/src/util/vecdeque.h | 1 + app/src/util/vector.h | 2 +- app/src/v4l2_sink.c | 4 ++++ app/src/v4l2_sink.h | 6 +++--- app/src/version.c | 2 ++ 110 files changed, 225 insertions(+), 151 deletions(-) diff --git a/app/src/adb/adb.c b/app/src/adb/adb.c index 0cd3c0fd..40e9e968 100644 --- a/app/src/adb/adb.c +++ b/app/src/adb/adb.c @@ -4,9 +4,10 @@ #include #include #include +#include -#include "adb_device.h" -#include "adb_parser.h" +#include "adb/adb_device.h" +#include "adb/adb_parser.h" #include "util/env.h" #include "util/file.h" #include "util/log.h" diff --git a/app/src/adb/adb.h b/app/src/adb/adb.h index 43310fb9..e4903902 100644 --- a/app/src/adb/adb.h +++ b/app/src/adb/adb.h @@ -6,7 +6,7 @@ #include #include -#include "adb_device.h" +#include "adb/adb_device.h" #include "util/intr.h" #define SC_ADB_NO_STDOUT (1 << 0) diff --git a/app/src/adb/adb_device.h b/app/src/adb/adb_device.h index 56393bcf..308663ef 100644 --- a/app/src/adb/adb_device.h +++ b/app/src/adb/adb_device.h @@ -4,7 +4,6 @@ #include "common.h" #include -#include #include "util/vector.h" diff --git a/app/src/adb/adb_parser.c b/app/src/adb/adb_parser.c index 66bb1854..90a1b30b 100644 --- a/app/src/adb/adb_parser.c +++ b/app/src/adb/adb_parser.c @@ -3,6 +3,7 @@ #include #include #include +#include #include "util/log.h" #include "util/str.h" diff --git a/app/src/adb/adb_parser.h b/app/src/adb/adb_parser.h index f20349f6..b8738a35 100644 --- a/app/src/adb/adb_parser.h +++ b/app/src/adb/adb_parser.h @@ -3,9 +3,9 @@ #include "common.h" -#include +#include -#include "adb_device.h" +#include "adb/adb_device.h" /** * Parse the available devices from the output of `adb devices` diff --git a/app/src/adb/adb_tunnel.c b/app/src/adb/adb_tunnel.c index fa936e4b..43e80e13 100644 --- a/app/src/adb/adb_tunnel.c +++ b/app/src/adb/adb_tunnel.c @@ -1,11 +1,11 @@ #include "adb_tunnel.h" #include +#include -#include "adb.h" +#include "adb/adb.h" #include "util/log.h" #include "util/net_intr.h" -#include "util/process_intr.h" static bool listen_on_port(struct sc_intr *intr, sc_socket socket, uint16_t port) { diff --git a/app/src/audio_player.h b/app/src/audio_player.h index 9133c24a..5a66d43b 100644 --- a/app/src/audio_player.h +++ b/app/src/audio_player.h @@ -3,9 +3,7 @@ #include "common.h" -#include -#include -#include +#include #include "audio_regulator.h" #include "trait/frame_sink.h" diff --git a/app/src/audio_regulator.c b/app/src/audio_regulator.c index 3e4f78ad..f7e9b81e 100644 --- a/app/src/audio_regulator.c +++ b/app/src/audio_regulator.c @@ -1,5 +1,9 @@ #include "audio_regulator.h" +#include +#include +#include +#include #include #include diff --git a/app/src/audio_regulator.h b/app/src/audio_regulator.h index 1c0eeb9f..03cf6325 100644 --- a/app/src/audio_regulator.h +++ b/app/src/audio_regulator.h @@ -5,6 +5,8 @@ #include #include +#include +#include #include #include #include "util/audiobuf.h" diff --git a/app/src/cli.c b/app/src/cli.c index ed1970d4..756934ea 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -5,6 +5,7 @@ #include #include #include +#include #include #include "options.h" @@ -13,6 +14,7 @@ #include "util/str.h" #include "util/strbuf.h" #include "util/term.h" +#include "util/tick.h" #define STR_IMPL_(x) #x #define STR(x) STR_IMPL_(x) diff --git a/app/src/decoder.c b/app/src/decoder.c index 5d42b8b0..4d0a1daf 100644 --- a/app/src/decoder.c +++ b/app/src/decoder.c @@ -1,11 +1,9 @@ #include "decoder.h" -#include -#include -#include +#include +#include +#include -#include "events.h" -#include "trait/frame_sink.h" #include "util/log.h" /** Downcast packet_sink to decoder */ diff --git a/app/src/decoder.h b/app/src/decoder.h index ba8903f4..1f525fae 100644 --- a/app/src/decoder.h +++ b/app/src/decoder.h @@ -3,13 +3,11 @@ #include "common.h" +#include + #include "trait/frame_source.h" #include "trait/packet_sink.h" -#include -#include -#include - struct sc_decoder { struct sc_packet_sink packet_sink; // packet sink trait struct sc_frame_source frame_source; // frame source trait diff --git a/app/src/delay_buffer.c b/app/src/delay_buffer.c index e89a2092..f75c6f72 100644 --- a/app/src/delay_buffer.c +++ b/app/src/delay_buffer.c @@ -2,9 +2,7 @@ #include #include - -#include -#include +#include #include "util/log.h" diff --git a/app/src/delay_buffer.h b/app/src/delay_buffer.h index 18c1ce94..61cd77e4 100644 --- a/app/src/delay_buffer.h +++ b/app/src/delay_buffer.h @@ -4,6 +4,7 @@ #include "common.h" #include +#include #include "clock.h" #include "trait/frame_source.h" diff --git a/app/src/demuxer.c b/app/src/demuxer.c index 7223b553..885cd6ee 100644 --- a/app/src/demuxer.c +++ b/app/src/demuxer.c @@ -1,14 +1,11 @@ #include "demuxer.h" #include +#include +#include #include -#include -#include -#include "decoder.h" -#include "events.h" #include "packet_merger.h" -#include "recorder.h" #include "util/binary.h" #include "util/log.h" diff --git a/app/src/demuxer.h b/app/src/demuxer.h index 5587d12d..2b7cb703 100644 --- a/app/src/demuxer.h +++ b/app/src/demuxer.h @@ -4,12 +4,8 @@ #include "common.h" #include -#include -#include -#include #include "trait/packet_source.h" -#include "trait/packet_sink.h" #include "util/net.h" #include "util/thread.h" diff --git a/app/src/device_msg.h b/app/src/device_msg.h index 86b2ccb7..d6c701bb 100644 --- a/app/src/device_msg.h +++ b/app/src/device_msg.h @@ -3,9 +3,9 @@ #include "common.h" -#include +#include #include -#include +#include #define DEVICE_MSG_MAX_SIZE (1 << 18) // 256k // type: 1 byte; length: 4 bytes diff --git a/app/src/display.c b/app/src/display.c index 39018834..aee8ef80 100644 --- a/app/src/display.c +++ b/app/src/display.c @@ -1,6 +1,8 @@ #include "display.h" #include +#include +#include #include #include "util/log.h" diff --git a/app/src/display.h b/app/src/display.h index 064bb7bf..4de9b0a9 100644 --- a/app/src/display.h +++ b/app/src/display.h @@ -4,7 +4,8 @@ #include "common.h" #include -#include +#include +#include #include #include "coords.h" diff --git a/app/src/events.c b/app/src/events.c index ce885241..b4322d1b 100644 --- a/app/src/events.c +++ b/app/src/events.c @@ -1,5 +1,7 @@ #include "events.h" +#include + #include "util/log.h" #include "util/thread.h" diff --git a/app/src/file_pusher.c b/app/src/file_pusher.c index 06911052..681fb5d6 100644 --- a/app/src/file_pusher.c +++ b/app/src/file_pusher.c @@ -1,11 +1,11 @@ #include "file_pusher.h" #include +#include #include #include "adb/adb.h" #include "util/log.h" -#include "util/process_intr.h" #define DEFAULT_PUSH_TARGET "/sdcard/Download/" diff --git a/app/src/fps_counter.c b/app/src/fps_counter.c index dd4ae1da..1daa42ba 100644 --- a/app/src/fps_counter.c +++ b/app/src/fps_counter.c @@ -1,6 +1,7 @@ #include "fps_counter.h" #include +#include #include "util/log.h" diff --git a/app/src/fps_counter.h b/app/src/fps_counter.h index e7619271..3eab461c 100644 --- a/app/src/fps_counter.h +++ b/app/src/fps_counter.h @@ -5,9 +5,9 @@ #include #include -#include #include "util/thread.h" +#include "util/tick.h" struct sc_fps_counter { sc_thread thread; diff --git a/app/src/frame_buffer.c b/app/src/frame_buffer.c index 5699b58f..9fd4cf6f 100644 --- a/app/src/frame_buffer.c +++ b/app/src/frame_buffer.c @@ -1,8 +1,6 @@ #include "frame_buffer.h" #include -#include -#include #include "util/log.h" diff --git a/app/src/frame_buffer.h b/app/src/frame_buffer.h index f97261cd..e748adfb 100644 --- a/app/src/frame_buffer.h +++ b/app/src/frame_buffer.h @@ -4,6 +4,7 @@ #include "common.h" #include +#include #include "util/thread.h" diff --git a/app/src/hid/hid_event.h b/app/src/hid/hid_event.h index d6818e30..b0d45ce8 100644 --- a/app/src/hid/hid_event.h +++ b/app/src/hid/hid_event.h @@ -3,6 +3,7 @@ #include "common.h" +#include #include #define SC_HID_MAX_SIZE 15 diff --git a/app/src/hid/hid_gamepad.c b/app/src/hid/hid_gamepad.c index 8f4e4527..842eae9e 100644 --- a/app/src/hid/hid_gamepad.c +++ b/app/src/hid/hid_gamepad.c @@ -2,6 +2,8 @@ #include #include +#include +#include #include "util/binary.h" #include "util/log.h" diff --git a/app/src/hid/hid_gamepad.h b/app/src/hid/hid_gamepad.h index b532a703..8d939ac7 100644 --- a/app/src/hid/hid_gamepad.h +++ b/app/src/hid/hid_gamepad.h @@ -4,6 +4,7 @@ #include "common.h" #include +#include #include "hid/hid_event.h" #include "input_events.h" diff --git a/app/src/hid/hid_keyboard.c b/app/src/hid/hid_keyboard.c index 961ad790..6477396a 100644 --- a/app/src/hid/hid_keyboard.c +++ b/app/src/hid/hid_keyboard.c @@ -1,5 +1,6 @@ #include "hid_keyboard.h" +#include #include #include "util/log.h" diff --git a/app/src/hid/hid_keyboard.h b/app/src/hid/hid_keyboard.h index cde1ac52..5ecfd8cf 100644 --- a/app/src/hid/hid_keyboard.h +++ b/app/src/hid/hid_keyboard.h @@ -4,6 +4,7 @@ #include "common.h" #include +#include #include "hid/hid_event.h" #include "input_events.h" diff --git a/app/src/hid/hid_mouse.c b/app/src/hid/hid_mouse.c index 7acc413b..29cfc594 100644 --- a/app/src/hid/hid_mouse.c +++ b/app/src/hid/hid_mouse.c @@ -1,5 +1,7 @@ #include "hid_mouse.h" +#include + // 1 byte for buttons + padding, 1 byte for X position, 1 byte for Y position, // 1 byte for wheel motion #define SC_HID_MOUSE_INPUT_SIZE 4 diff --git a/app/src/hid/hid_mouse.h b/app/src/hid/hid_mouse.h index a9a54718..06c61dd1 100644 --- a/app/src/hid/hid_mouse.h +++ b/app/src/hid/hid_mouse.h @@ -3,8 +3,6 @@ #include "common.h" -#include - #include "hid/hid_event.h" #include "input_events.h" diff --git a/app/src/icon.c b/app/src/icon.c index 4f3a9a39..797afc75 100644 --- a/app/src/icon.c +++ b/app/src/icon.c @@ -2,17 +2,22 @@ #include #include +#include +#include +#include #include #include +#include #include #include +#include #include "config.h" -#include "compat.h" #include "util/env.h" -#include "util/file.h" +#ifdef PORTABLE +# include "util/file.h" +#endif #include "util/log.h" -#include "util/str.h" #define SCRCPY_PORTABLE_ICON_FILENAME "icon.png" #define SCRCPY_DEFAULT_ICON_PATH \ diff --git a/app/src/icon.h b/app/src/icon.h index 3251e48f..6bcf46d2 100644 --- a/app/src/icon.h +++ b/app/src/icon.h @@ -3,9 +3,7 @@ #include "common.h" -#include -#include -#include +#include SDL_Surface * scrcpy_icon_load(void); diff --git a/app/src/input_events.h b/app/src/input_events.h index ad3afa81..0c022acc 100644 --- a/app/src/input_events.h +++ b/app/src/input_events.h @@ -9,7 +9,6 @@ #include #include "coords.h" -#include "options.h" /* The representation of input events in scrcpy is very close to the SDL API, * for simplicity. diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 2e4337db..635825c9 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -1,8 +1,12 @@ #include "input_manager.h" #include -#include +#include +#include +#include +#include "android/input.h" +#include "android/keycodes.h" #include "input_events.h" #include "screen.h" #include "shortcut_mod.h" diff --git a/app/src/input_manager.h b/app/src/input_manager.h index 8efd0153..af4cbc69 100644 --- a/app/src/input_manager.h +++ b/app/src/input_manager.h @@ -4,12 +4,12 @@ #include "common.h" #include - -#include +#include +#include +#include #include "controller.h" #include "file_pusher.h" -#include "fps_counter.h" #include "options.h" #include "trait/gamepad_processor.h" #include "trait/key_processor.h" diff --git a/app/src/keyboard_sdk.c b/app/src/keyboard_sdk.c index 2d9ca85b..466a1aeb 100644 --- a/app/src/keyboard_sdk.c +++ b/app/src/keyboard_sdk.c @@ -1,8 +1,13 @@ #include "keyboard_sdk.h" #include +#include +#include +#include +#include #include "android/input.h" +#include "android/keycodes.h" #include "control_msg.h" #include "controller.h" #include "input_events.h" diff --git a/app/src/main.c b/app/src/main.c index 8bbd074f..c58e0be7 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -1,9 +1,6 @@ #include "common.h" -#include #include -#include -#include #ifdef HAVE_V4L2 # include #endif diff --git a/app/src/mouse_sdk.c b/app/src/mouse_sdk.c index a7998972..7eceffa7 100644 --- a/app/src/mouse_sdk.c +++ b/app/src/mouse_sdk.c @@ -1,12 +1,12 @@ #include "mouse_sdk.h" #include +#include #include "android/input.h" #include "control_msg.h" #include "controller.h" #include "input_events.h" -#include "util/intmap.h" #include "util/log.h" /** Downcast mouse processor to sc_mouse_sdk */ diff --git a/app/src/mouse_sdk.h b/app/src/mouse_sdk.h index 142b89bb..fe92a2d7 100644 --- a/app/src/mouse_sdk.h +++ b/app/src/mouse_sdk.h @@ -6,7 +6,6 @@ #include #include "controller.h" -#include "screen.h" #include "trait/mouse_processor.h" struct sc_mouse_sdk { diff --git a/app/src/opengl.c b/app/src/opengl.c index 376690af..0cb83ed7 100644 --- a/app/src/opengl.c +++ b/app/src/opengl.c @@ -2,7 +2,8 @@ #include #include -#include "SDL2/SDL.h" +#include +#include void sc_opengl_init(struct sc_opengl *gl) { diff --git a/app/src/options.c b/app/src/options.c index df8033e9..044aa014 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -1,5 +1,7 @@ #include "options.h" +#include + const struct scrcpy_options scrcpy_options_default = { .serial = NULL, .crop = NULL, diff --git a/app/src/options.h b/app/src/options.h index 152881d8..c8425808 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -5,7 +5,6 @@ #include #include -#include #include #include "util/tick.h" diff --git a/app/src/packet_merger.c b/app/src/packet_merger.c index 81b02d2c..dea038b6 100644 --- a/app/src/packet_merger.c +++ b/app/src/packet_merger.c @@ -1,5 +1,9 @@ #include "packet_merger.h" +#include +#include +#include + #include "util/log.h" void diff --git a/app/src/packet_merger.h b/app/src/packet_merger.h index e1824c2c..3f9972ce 100644 --- a/app/src/packet_merger.h +++ b/app/src/packet_merger.h @@ -5,7 +5,7 @@ #include #include -#include +#include /** * Config packets (containing the SPS/PPS) are sent in-band. A new config diff --git a/app/src/receiver.c b/app/src/receiver.c index b89b0c6e..2ccb8a8b 100644 --- a/app/src/receiver.c +++ b/app/src/receiver.c @@ -2,7 +2,6 @@ #include #include -#include #include #include "device_msg.h" diff --git a/app/src/recorder.c b/app/src/recorder.c index 15f27157..c26f8f2d 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -1,6 +1,9 @@ #include "recorder.h" #include +#include +#include +#include #include #include #include diff --git a/app/src/recorder.h b/app/src/recorder.h index d096e79a..70b73836 100644 --- a/app/src/recorder.h +++ b/app/src/recorder.h @@ -4,9 +4,10 @@ #include "common.h" #include +#include +#include #include -#include "coords.h" #include "options.h" #include "trait/packet_sink.h" #include "util/thread.h" diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index f1942e43..641d93f7 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -1,10 +1,11 @@ #include "scrcpy.h" +#include +#include +#include #include +#include #include -#include -#include -#include #include #ifdef _WIN32 @@ -37,9 +38,9 @@ #endif #include "util/acksync.h" #include "util/log.h" -#include "util/net.h" #include "util/rand.h" #include "util/timeout.h" +#include "util/tick.h" #ifdef HAVE_V4L2 # include "v4l2_sink.h" #endif diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index d4d494a3..7f6a0fb2 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -3,7 +3,6 @@ #include "common.h" -#include #include "options.h" enum scrcpy_exit_code { diff --git a/app/src/screen.h b/app/src/screen.h index c716c399..6621b2d2 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -1,11 +1,14 @@ -#ifndef SCREEN_H -#define SCREEN_H +#ifndef SC_SCREEN_H +#define SC_SCREEN_H #include "common.h" #include +#include #include -#include +#include +#include +#include #include "controller.h" #include "coords.h" @@ -14,7 +17,6 @@ #include "frame_buffer.h" #include "input_manager.h" #include "mouse_capture.h" -#include "opengl.h" #include "options.h" #include "trait/key_processor.h" #include "trait/frame_sink.h" diff --git a/app/src/server.c b/app/src/server.c index 22ddd372..cf181abc 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -1,19 +1,18 @@ #include "server.h" #include -#include #include #include -#include -#include +#include +#include +#include #include "adb/adb.h" -#include "util/binary.h" #include "util/env.h" #include "util/file.h" #include "util/log.h" #include "util/net_intr.h" -#include "util/process_intr.h" +#include "util/process.h" #include "util/str.h" #define SC_SERVER_FILENAME "scrcpy-server" diff --git a/app/src/server.h b/app/src/server.h index 3c78b9ed..a03689ff 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -1,19 +1,17 @@ -#ifndef SERVER_H -#define SERVER_H +#ifndef SC_SERVER_H +#define SC_SERVER_H #include "common.h" -#include #include #include #include "adb/adb_tunnel.h" -#include "coords.h" #include "options.h" #include "util/intr.h" -#include "util/log.h" #include "util/net.h" #include "util/thread.h" +#include "util/tick.h" #define SC_DEVICE_NAME_FIELD_LENGTH 64 struct sc_server_info { diff --git a/app/src/shortcut_mod.h b/app/src/shortcut_mod.h index b685e987..f6c13f03 100644 --- a/app/src/shortcut_mod.h +++ b/app/src/shortcut_mod.h @@ -3,6 +3,7 @@ #include "common.h" +#include #include #include #include diff --git a/app/src/sys/unix/file.c b/app/src/sys/unix/file.c index 6123c788..8f7fb074 100644 --- a/app/src/sys/unix/file.c +++ b/app/src/sys/unix/file.c @@ -1,10 +1,11 @@ #include "util/file.h" #include -#include #include +#include #include #include +#include #include #ifdef __APPLE__ # include // for _NSGetExecutablePath() diff --git a/app/src/sys/unix/process.c b/app/src/sys/unix/process.c index 8c4a53c3..36d1ff7d 100644 --- a/app/src/sys/unix/process.c +++ b/app/src/sys/unix/process.c @@ -4,6 +4,8 @@ #include #include #include +#include +#include #include #include #include diff --git a/app/src/trait/frame_sink.h b/app/src/trait/frame_sink.h index 8ef248b6..67be4d46 100644 --- a/app/src/trait/frame_sink.h +++ b/app/src/trait/frame_sink.h @@ -3,7 +3,6 @@ #include "common.h" -#include #include #include diff --git a/app/src/trait/frame_source.c b/app/src/trait/frame_source.c index 416eccd9..56848309 100644 --- a/app/src/trait/frame_source.c +++ b/app/src/trait/frame_source.c @@ -1,5 +1,7 @@ #include "frame_source.h" +#include + void sc_frame_source_init(struct sc_frame_source *source) { source->sink_count = 0; diff --git a/app/src/trait/frame_source.h b/app/src/trait/frame_source.h index 94222af0..cb1ef905 100644 --- a/app/src/trait/frame_source.h +++ b/app/src/trait/frame_source.h @@ -3,7 +3,9 @@ #include "common.h" -#include "frame_sink.h" +#include + +#include "trait/frame_sink.h" #define SC_FRAME_SOURCE_MAX_SINKS 2 diff --git a/app/src/trait/gamepad_processor.h b/app/src/trait/gamepad_processor.h index 19629a9a..5e8dc2a4 100644 --- a/app/src/trait/gamepad_processor.h +++ b/app/src/trait/gamepad_processor.h @@ -3,9 +3,6 @@ #include "common.h" -#include -#include - #include "input_events.h" /** diff --git a/app/src/trait/key_processor.h b/app/src/trait/key_processor.h index 96374413..9e9bb86e 100644 --- a/app/src/trait/key_processor.h +++ b/app/src/trait/key_processor.h @@ -3,7 +3,6 @@ #include "common.h" -#include #include #include "input_events.h" diff --git a/app/src/trait/mouse_processor.h b/app/src/trait/mouse_processor.h index 6e0b596e..d0a96e7c 100644 --- a/app/src/trait/mouse_processor.h +++ b/app/src/trait/mouse_processor.h @@ -3,7 +3,6 @@ #include "common.h" -#include #include #include "input_events.h" diff --git a/app/src/trait/packet_sink.h b/app/src/trait/packet_sink.h index 84cfe814..e12dea12 100644 --- a/app/src/trait/packet_sink.h +++ b/app/src/trait/packet_sink.h @@ -3,7 +3,6 @@ #include "common.h" -#include #include #include diff --git a/app/src/trait/packet_source.c b/app/src/trait/packet_source.c index c0836f1d..0a2c6c4d 100644 --- a/app/src/trait/packet_source.c +++ b/app/src/trait/packet_source.c @@ -1,5 +1,7 @@ #include "packet_source.h" +#include + void sc_packet_source_init(struct sc_packet_source *source) { source->sink_count = 0; diff --git a/app/src/trait/packet_source.h b/app/src/trait/packet_source.h index 16d56e86..8788021a 100644 --- a/app/src/trait/packet_source.h +++ b/app/src/trait/packet_source.h @@ -3,7 +3,9 @@ #include "common.h" -#include "packet_sink.h" +#include + +#include "trait/packet_sink.h" #define SC_PACKET_SOURCE_MAX_SINKS 2 diff --git a/app/src/uhid/gamepad_uhid.c b/app/src/uhid/gamepad_uhid.c index a066cf03..c64feb18 100644 --- a/app/src/uhid/gamepad_uhid.c +++ b/app/src/uhid/gamepad_uhid.c @@ -1,5 +1,10 @@ #include "gamepad_uhid.h" +#include +#include +#include +#include + #include "hid/hid_gamepad.h" #include "input_events.h" #include "util/log.h" diff --git a/app/src/uhid/gamepad_uhid.h b/app/src/uhid/gamepad_uhid.h index 07d03099..ad747604 100644 --- a/app/src/uhid/gamepad_uhid.h +++ b/app/src/uhid/gamepad_uhid.h @@ -3,8 +3,6 @@ #include "common.h" -#include - #include "controller.h" #include "hid/hid_gamepad.h" #include "trait/gamepad_processor.h" diff --git a/app/src/uhid/keyboard_uhid.c b/app/src/uhid/keyboard_uhid.c index 76d70cc5..70082990 100644 --- a/app/src/uhid/keyboard_uhid.c +++ b/app/src/uhid/keyboard_uhid.c @@ -1,6 +1,12 @@ #include "keyboard_uhid.h" +#include +#include +#include +#include + #include "util/log.h" +#include "util/thread.h" /** Downcast key processor to keyboard_uhid */ #define DOWNCAST(KP) container_of(KP, struct sc_keyboard_uhid, key_processor) diff --git a/app/src/uhid/mouse_uhid.c b/app/src/uhid/mouse_uhid.c index 471030e7..7fed8383 100644 --- a/app/src/uhid/mouse_uhid.c +++ b/app/src/uhid/mouse_uhid.c @@ -1,5 +1,8 @@ #include "mouse_uhid.h" +#include +#include + #include "hid/hid_mouse.h" #include "input_events.h" #include "util/log.h" diff --git a/app/src/uhid/uhid_output.c b/app/src/uhid/uhid_output.c index 05e691da..e743a73c 100644 --- a/app/src/uhid/uhid_output.c +++ b/app/src/uhid/uhid_output.c @@ -1,6 +1,5 @@ #include "uhid_output.h" -#include #include #include "uhid/keyboard_uhid.h" diff --git a/app/src/uhid/uhid_output.h b/app/src/uhid/uhid_output.h index cd6a800f..ed028b58 100644 --- a/app/src/uhid/uhid_output.h +++ b/app/src/uhid/uhid_output.h @@ -3,7 +3,7 @@ #include "common.h" -#include +#include #include /** diff --git a/app/src/usb/aoa_hid.c b/app/src/usb/aoa_hid.c index 236a78ed..8cb62bfd 100644 --- a/app/src/usb/aoa_hid.c +++ b/app/src/usb/aoa_hid.c @@ -1,13 +1,16 @@ -#include "util/log.h" +#include "aoa_hid.h" #include #include #include +#include +#include +#include -#include "aoa_hid.h" #include "events.h" #include "util/log.h" #include "util/str.h" +#include "util/tick.h" #include "util/vector.h" // See . diff --git a/app/src/usb/aoa_hid.h b/app/src/usb/aoa_hid.h index 9cc6355e..2755c957 100644 --- a/app/src/usb/aoa_hid.h +++ b/app/src/usb/aoa_hid.h @@ -3,16 +3,13 @@ #include "common.h" -#include #include - -#include +#include #include "hid/hid_event.h" -#include "usb.h" +#include "usb/usb.h" #include "util/acksync.h" #include "util/thread.h" -#include "util/tick.h" #include "util/vecdeque.h" enum sc_aoa_event_type { diff --git a/app/src/usb/gamepad_aoa.c b/app/src/usb/gamepad_aoa.c index 4372379f..d29b1a78 100644 --- a/app/src/usb/gamepad_aoa.c +++ b/app/src/usb/gamepad_aoa.c @@ -1,5 +1,7 @@ #include "gamepad_aoa.h" +#include + #include "input_events.h" #include "util/log.h" diff --git a/app/src/usb/gamepad_aoa.h b/app/src/usb/gamepad_aoa.h index b2dfbe5e..0297a365 100644 --- a/app/src/usb/gamepad_aoa.h +++ b/app/src/usb/gamepad_aoa.h @@ -3,10 +3,8 @@ #include "common.h" -#include - -#include "aoa_hid.h" #include "hid/hid_gamepad.h" +#include "usb/aoa_hid.h" #include "trait/gamepad_processor.h" struct sc_gamepad_aoa { diff --git a/app/src/usb/keyboard_aoa.h b/app/src/usb/keyboard_aoa.h index 565b9177..9e9500a3 100644 --- a/app/src/usb/keyboard_aoa.h +++ b/app/src/usb/keyboard_aoa.h @@ -5,8 +5,8 @@ #include -#include "aoa_hid.h" #include "hid/hid_keyboard.h" +#include "usb/aoa_hid.h" #include "trait/key_processor.h" struct sc_keyboard_aoa { diff --git a/app/src/usb/mouse_aoa.c b/app/src/usb/mouse_aoa.c index cb566cc0..b64e9b12 100644 --- a/app/src/usb/mouse_aoa.c +++ b/app/src/usb/mouse_aoa.c @@ -1,6 +1,7 @@ #include "mouse_aoa.h" #include +#include #include "hid/hid_mouse.h" #include "input_events.h" diff --git a/app/src/usb/mouse_aoa.h b/app/src/usb/mouse_aoa.h index afaed761..506286ba 100644 --- a/app/src/usb/mouse_aoa.h +++ b/app/src/usb/mouse_aoa.h @@ -5,7 +5,7 @@ #include -#include "aoa_hid.h" +#include "usb/aoa_hid.h" #include "trait/mouse_processor.h" struct sc_mouse_aoa { diff --git a/app/src/usb/scrcpy_otg.c b/app/src/usb/scrcpy_otg.c index 6ef2fc2a..1a9cc46e 100644 --- a/app/src/usb/scrcpy_otg.c +++ b/app/src/usb/scrcpy_otg.c @@ -1,10 +1,19 @@ #include "scrcpy_otg.h" +#include +#include +#include #include -#include "adb/adb.h" +#ifdef _WIN32 +# include "adb/adb.h" +#endif #include "events.h" -#include "screen_otg.h" +#include "usb/screen_otg.h" +#include "usb/aoa_hid.h" +#include "usb/gamepad_aoa.h" +#include "usb/keyboard_aoa.h" +#include "usb/mouse_aoa.h" #include "util/log.h" struct scrcpy_otg { diff --git a/app/src/usb/screen_otg.c b/app/src/usb/screen_otg.c index 368af125..02edc3a3 100644 --- a/app/src/usb/screen_otg.c +++ b/app/src/usb/screen_otg.c @@ -1,7 +1,11 @@ #include "screen_otg.h" +#include +#include + #include "icon.h" #include "options.h" +#include "util/acksync.h" #include "util/log.h" static void diff --git a/app/src/usb/screen_otg.h b/app/src/usb/screen_otg.h index 427723ad..08b76ae7 100644 --- a/app/src/usb/screen_otg.h +++ b/app/src/usb/screen_otg.h @@ -4,12 +4,13 @@ #include "common.h" #include +#include #include -#include "keyboard_aoa.h" -#include "mouse_aoa.h" #include "mouse_capture.h" -#include "gamepad_aoa.h" +#include "usb/gamepad_aoa.h" +#include "usb/keyboard_aoa.h" +#include "usb/mouse_aoa.h" struct sc_screen_otg { struct sc_keyboard_aoa *keyboard; diff --git a/app/src/util/acksync.c b/app/src/util/acksync.c index 2899cdcb..76ecee0d 100644 --- a/app/src/util/acksync.c +++ b/app/src/util/acksync.c @@ -1,7 +1,6 @@ #include "acksync.h" #include -#include "util/log.h" bool sc_acksync_init(struct sc_acksync *as) { diff --git a/app/src/util/acksync.h b/app/src/util/acksync.h index 58ab1b35..3d9c9b2f 100644 --- a/app/src/util/acksync.h +++ b/app/src/util/acksync.h @@ -3,7 +3,10 @@ #include "common.h" -#include "thread.h" +#include +#include +#include "util/thread.h" +#include "util/tick.h" #define SC_SEQUENCE_INVALID 0 diff --git a/app/src/util/audiobuf.h b/app/src/util/audiobuf.h index 5e7dd4a0..5cc51932 100644 --- a/app/src/util/audiobuf.h +++ b/app/src/util/audiobuf.h @@ -6,6 +6,7 @@ #include #include #include +#include #include /** diff --git a/app/src/util/average.h b/app/src/util/average.h index 59fae7d1..eded9987 100644 --- a/app/src/util/average.h +++ b/app/src/util/average.h @@ -3,9 +3,6 @@ #include "common.h" -#include -#include - struct sc_average { // Current average value float avg; diff --git a/app/src/util/binary.h b/app/src/util/binary.h index 7de9b505..b6ce3201 100644 --- a/app/src/util/binary.h +++ b/app/src/util/binary.h @@ -4,7 +4,6 @@ #include "common.h" #include -#include #include static inline void diff --git a/app/src/util/env.c b/app/src/util/env.c index 1128e5ea..127f5a1f 100644 --- a/app/src/util/env.c +++ b/app/src/util/env.c @@ -2,7 +2,9 @@ #include #include -#include "util/str.h" +#ifdef _WIN32 +# include "util/str.h" +#endif char * sc_get_env(const char *varname) { diff --git a/app/src/util/intmap.h b/app/src/util/intmap.h index 2898c461..7ab903ca 100644 --- a/app/src/util/intmap.h +++ b/app/src/util/intmap.h @@ -3,6 +3,7 @@ #include "common.h" +#include #include struct sc_intmap_entry { diff --git a/app/src/util/intr.c b/app/src/util/intr.c index 22bd121a..ddf4839f 100644 --- a/app/src/util/intr.c +++ b/app/src/util/intr.c @@ -1,9 +1,9 @@ #include "intr.h" -#include "util/log.h" - #include +#include "util/log.h" + bool sc_intr_init(struct sc_intr *intr) { bool ok = sc_mutex_init(&intr->mutex); diff --git a/app/src/util/intr.h b/app/src/util/intr.h index 1c20f6df..35bd3375 100644 --- a/app/src/util/intr.h +++ b/app/src/util/intr.h @@ -6,9 +6,9 @@ #include #include -#include "net.h" -#include "process.h" -#include "thread.h" +#include "util/net.h" +#include "util/process.h" +#include "util/thread.h" /** * Interruptor to wake up a blocking call from another thread diff --git a/app/src/util/log.c b/app/src/util/log.c index 8a347c84..9114a258 100644 --- a/app/src/util/log.c +++ b/app/src/util/log.c @@ -4,7 +4,10 @@ # include #endif #include -#include +#include +#include +#include +#include static SDL_LogPriority log_level_sc_to_sdl(enum sc_log_level level) { diff --git a/app/src/util/net.c b/app/src/util/net.c index d68b0af6..9562ff6b 100644 --- a/app/src/util/net.c +++ b/app/src/util/net.c @@ -1,28 +1,27 @@ #include "net.h" #include -#include #include -#include "log.h" - #ifdef _WIN32 # include typedef int socklen_t; #else -# include -# include +# include +# include # include # include -# include # include -# include +# include +# include # define SOCKET_ERROR -1 typedef struct sockaddr_in SOCKADDR_IN; typedef struct sockaddr SOCKADDR; typedef struct in_addr IN_ADDR; #endif +#include "util/log.h" + bool net_init(void) { #ifdef _WIN32 diff --git a/app/src/util/net.h b/app/src/util/net.h index 94789954..aa99bbc4 100644 --- a/app/src/util/net.h +++ b/app/src/util/net.h @@ -4,14 +4,15 @@ #include "common.h" #include +#include #include +#include #ifdef _WIN32 # include typedef SOCKET sc_raw_socket; # define SC_RAW_SOCKET_NONE INVALID_SOCKET #else // not _WIN32 -# include typedef int sc_raw_socket; # define SC_RAW_SOCKET_NONE -1 #endif diff --git a/app/src/util/net_intr.h b/app/src/util/net_intr.h index dbef528d..e2bbee88 100644 --- a/app/src/util/net_intr.h +++ b/app/src/util/net_intr.h @@ -3,8 +3,13 @@ #include "common.h" -#include "intr.h" -#include "net.h" +#include +#include +#include +#include + +#include "util/intr.h" +#include "util/net.h" bool net_connect_intr(struct sc_intr *intr, sc_socket socket, uint32_t addr, diff --git a/app/src/util/process.c b/app/src/util/process.c index 9c4dcd9f..29d89a54 100644 --- a/app/src/util/process.c +++ b/app/src/util/process.c @@ -1,8 +1,6 @@ #include "process.h" #include -#include -#include "log.h" enum sc_process_result sc_process_execute(const char *const argv[], sc_pid *pid, unsigned flags) { diff --git a/app/src/util/process.h b/app/src/util/process.h index 4d9d1684..eec51bcc 100644 --- a/app/src/util/process.h +++ b/app/src/util/process.h @@ -4,7 +4,9 @@ #include "common.h" #include +#include #include "util/thread.h" +#include "util/tick.h" #ifdef _WIN32 diff --git a/app/src/util/process_intr.h b/app/src/util/process_intr.h index 530a9046..020eafa1 100644 --- a/app/src/util/process_intr.h +++ b/app/src/util/process_intr.h @@ -3,8 +3,8 @@ #include "common.h" -#include "intr.h" -#include "process.h" +#include "util/intr.h" +#include "util/process.h" ssize_t sc_pipe_read_intr(struct sc_intr *intr, sc_pid pid, sc_pipe pipe, char *data, diff --git a/app/src/util/str.c b/app/src/util/str.c index 304cd302..83d19c4d 100644 --- a/app/src/util/str.c +++ b/app/src/util/str.c @@ -12,8 +12,8 @@ # include #endif -#include "log.h" -#include "strbuf.h" +#include "util/log.h" +#include "util/strbuf.h" size_t sc_strncpy(char *dest, const char *src, size_t n) { diff --git a/app/src/util/str.h b/app/src/util/str.h index d20f1b28..b386b48d 100644 --- a/app/src/util/str.h +++ b/app/src/util/str.h @@ -5,6 +5,8 @@ #include #include +#include +#include /* Stringify a numeric value */ #define SC_STR(s) SC_XSTR(s) diff --git a/app/src/util/strbuf.c b/app/src/util/strbuf.c index 1892b46b..6196d746 100644 --- a/app/src/util/strbuf.c +++ b/app/src/util/strbuf.c @@ -1,11 +1,10 @@ #include "strbuf.h" #include -#include #include #include -#include "log.h" +#include "util/log.h" bool sc_strbuf_init(struct sc_strbuf *buf, size_t init_cap) { diff --git a/app/src/util/thread.c b/app/src/util/thread.c index 9679dfff..2a5253f7 100644 --- a/app/src/util/thread.c +++ b/app/src/util/thread.c @@ -1,10 +1,12 @@ #include "thread.h" #include +#include +#include #include #include -#include "log.h" +#include "util/log.h" sc_thread_id SC_MAIN_THREAD_ID; diff --git a/app/src/util/tick.c b/app/src/util/tick.c index cc0bab5e..edef1070 100644 --- a/app/src/util/tick.c +++ b/app/src/util/tick.c @@ -1,6 +1,7 @@ #include "tick.h" #include +#include #include #ifdef _WIN32 # include diff --git a/app/src/util/timeout.c b/app/src/util/timeout.c index 159a4681..21bc3a53 100644 --- a/app/src/util/timeout.c +++ b/app/src/util/timeout.c @@ -1,8 +1,9 @@ #include "timeout.h" #include +#include -#include "log.h" +#include "util/log.h" bool sc_timeout_init(struct sc_timeout *timeout) { diff --git a/app/src/util/timeout.h b/app/src/util/timeout.h index ae171b86..a45ae2ae 100644 --- a/app/src/util/timeout.h +++ b/app/src/util/timeout.h @@ -5,8 +5,8 @@ #include -#include "thread.h" -#include "tick.h" +#include "util/thread.h" +#include "util/tick.h" struct sc_timeout { sc_thread thread; diff --git a/app/src/util/vecdeque.h b/app/src/util/vecdeque.h index ce559ee9..e31724e2 100644 --- a/app/src/util/vecdeque.h +++ b/app/src/util/vecdeque.h @@ -6,6 +6,7 @@ #include #include #include +#include #include #include diff --git a/app/src/util/vector.h b/app/src/util/vector.h index 97d7c389..5b399d56 100644 --- a/app/src/util/vector.h +++ b/app/src/util/vector.h @@ -5,8 +5,8 @@ #include #include +#include #include -#include // Adapted from vlc_vector: // diff --git a/app/src/v4l2_sink.c b/app/src/v4l2_sink.c index 087e9af4..da9e02ef 100644 --- a/app/src/v4l2_sink.c +++ b/app/src/v4l2_sink.c @@ -1,5 +1,9 @@ #include "v4l2_sink.h" +#include +#include +#include +#include #include #include "util/log.h" diff --git a/app/src/v4l2_sink.h b/app/src/v4l2_sink.h index 365a739d..2b7c5b50 100644 --- a/app/src/v4l2_sink.h +++ b/app/src/v4l2_sink.h @@ -3,13 +3,13 @@ #include "common.h" +#include #include #include -#include "coords.h" -#include "trait/frame_sink.h" #include "frame_buffer.h" -#include "util/tick.h" +#include "trait/frame_sink.h" +#include "util/thread.h" struct sc_v4l2_sink { struct sc_frame_sink frame_sink; // frame sink trait diff --git a/app/src/version.c b/app/src/version.c index 90ea3334..f8610714 100644 --- a/app/src/version.c +++ b/app/src/version.c @@ -1,5 +1,6 @@ #include "version.h" +#include #include #include #include @@ -9,6 +10,7 @@ #ifdef HAVE_USB # include #endif +#include void scrcpy_print_version(void) { From eac711ace68da43b09a4da99f7990143ae93f7c1 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 23 Dec 2024 12:51:27 +0100 Subject: [PATCH 2185/2244] Remove unused rotation and fold listeners IRotationWatcher and IDisplayFoldListener are no longer used since commit 39d51ff2cc2f3e201ad433d48372b548e5dd11d3. --- server/build_without_gradle.sh | 2 - .../android/view/IDisplayFoldListener.aidl | 26 ---------- .../aidl/android/view/IRotationWatcher.aidl | 25 ---------- .../scrcpy/wrappers/WindowManager.java | 48 ------------------- 4 files changed, 101 deletions(-) delete mode 100644 server/src/main/aidl/android/view/IDisplayFoldListener.aidl delete mode 100644 server/src/main/aidl/android/view/IRotationWatcher.aidl diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index d16592b4..e0b69aee 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -47,10 +47,8 @@ EOF echo "Generating java from aidl..." cd "$SERVER_DIR/src/main/aidl" -"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" -I. android/view/IRotationWatcher.aidl "$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" -I. \ android/content/IOnPrimaryClipChangedListener.aidl -"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" -I. android/view/IDisplayFoldListener.aidl "$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" -I. -p "$ANDROID_AIDL" \ android/view/IDisplayWindowListener.aidl diff --git a/server/src/main/aidl/android/view/IDisplayFoldListener.aidl b/server/src/main/aidl/android/view/IDisplayFoldListener.aidl deleted file mode 100644 index 2c91149d..00000000 --- a/server/src/main/aidl/android/view/IDisplayFoldListener.aidl +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright (C) 2019 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.view; - -/** - * {@hide} - */ -oneway interface IDisplayFoldListener -{ - /** Called when the foldedness of a display changes */ - void onDisplayFoldChanged(int displayId, boolean folded); -} diff --git a/server/src/main/aidl/android/view/IRotationWatcher.aidl b/server/src/main/aidl/android/view/IRotationWatcher.aidl deleted file mode 100644 index 2cc5e44a..00000000 --- a/server/src/main/aidl/android/view/IRotationWatcher.aidl +++ /dev/null @@ -1,25 +0,0 @@ -/* //device/java/android/android/hardware/ISensorListener.aidl -** -** Copyright 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.view; - -/** - * {@hide} - */ -interface IRotationWatcher { - oneway void onRotationChanged(int rotation); -} 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 86dd83f2..04f5abd7 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java @@ -5,9 +5,7 @@ import com.genymobile.scrcpy.util.Ln; import android.annotation.TargetApi; import android.os.IInterface; -import android.view.IDisplayFoldListener; import android.view.IDisplayWindowListener; -import android.view.IRotationWatcher; import java.lang.reflect.Method; @@ -182,52 +180,6 @@ public final class WindowManager { } } - 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, displayId); - } catch (NoSuchMethodException e) { - // old version - if (displayId != 0) { - Ln.e("Secondary display rotation not supported on this device"); - return; - } - cls.getMethod("watchRotation", IRotationWatcher.class).invoke(manager, rotationWatcher); - } - } catch (Exception e) { - Ln.e("Could not register rotation watcher", e); - } - } - - public void unregisterRotationWatcher(IRotationWatcher rotationWatcher) { - try { - manager.getClass().getMethod("removeRotationWatcher", IRotationWatcher.class).invoke(manager, rotationWatcher); - } catch (Exception e) { - Ln.e("Could not unregister rotation watcher", e); - } - } - - @TargetApi(AndroidVersions.API_29_ANDROID_10) - public void registerDisplayFoldListener(IDisplayFoldListener foldListener) { - try { - manager.getClass().getMethod("registerDisplayFoldListener", IDisplayFoldListener.class).invoke(manager, foldListener); - } catch (Exception e) { - Ln.e("Could not register display fold listener", e); - } - } - - @TargetApi(AndroidVersions.API_29_ANDROID_10) - public void unregisterDisplayFoldListener(IDisplayFoldListener foldListener) { - try { - manager.getClass().getMethod("unregisterDisplayFoldListener", IDisplayFoldListener.class).invoke(manager, foldListener); - } catch (Exception e) { - Ln.e("Could not unregister display fold listener", e); - } - } - @TargetApi(AndroidVersions.API_30_ANDROID_11) public int[] registerDisplayWindowListener(IDisplayWindowListener listener) { try { From c27d116a662c87ee84963820669ee0d2ce60e6f1 Mon Sep 17 00:00:00 2001 From: Simon Chan <1330321+yume-chan@users.noreply.github.com> Date: Mon, 23 Dec 2024 12:04:18 +0100 Subject: [PATCH 2186/2244] Fix AudioRecord package name for Android 16 Since commit 9f91a5eebb4520b9333576e946b3911d0f946a04 in frameworks/av (AOSP), an AudioRecord can be created only if the declared package name in the AttributionSource is "shell" (for the shell UID): - - Refs Fixes #5698 Signed-off-by: Romain Vimont --- server/src/main/java/com/genymobile/scrcpy/FakeContext.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/FakeContext.java b/server/src/main/java/com/genymobile/scrcpy/FakeContext.java index 2b83e397..22fc6d49 100644 --- a/server/src/main/java/com/genymobile/scrcpy/FakeContext.java +++ b/server/src/main/java/com/genymobile/scrcpy/FakeContext.java @@ -72,7 +72,7 @@ public final class FakeContext extends ContextWrapper { @Override public AttributionSource getAttributionSource() { AttributionSource.Builder builder = new AttributionSource.Builder(Process.SHELL_UID); - builder.setPackageName(PACKAGE_NAME); + builder.setPackageName("shell"); return builder.build(); } From 1c7680f689684fe45b8fa8e1525add5dc3cdfdd2 Mon Sep 17 00:00:00 2001 From: "Jaime J. Denizard" Date: Tue, 31 Dec 2024 16:21:36 -0500 Subject: [PATCH 2187/2244] Fix some grammatical issues in documentation PR #5722 Signed-off-by: Romain Vimont --- doc/connection.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/doc/connection.md b/doc/connection.md index 2c3d37e1..dcf00147 100644 --- a/doc/connection.md +++ b/doc/connection.md @@ -113,16 +113,17 @@ with the device IP address you found)_. 7. Run `scrcpy` as usual. 8. Run `adb disconnect` once you're done. -Since Android 11, a [wireless debugging option][adb-wireless] allows to bypass -having to physically connect your device directly to your computer. +Since Android 11, a [wireless debugging option][adb-wireless] allows you to +bypass having to physically connect your device to your computer. [adb-wireless]: https://developer.android.com/studio/command-line/adb#wireless-android11-command-line ## Autostart -A small tool (by the scrcpy author) allows to run arbitrary commands whenever a -new Android device is connected: [AutoAdb]. It can be used to start scrcpy: +A small tool (by the scrcpy author) allows you to run arbitrary commands +whenever a new Android device is connected: [AutoAdb]. It can be used to start +scrcpy: ```bash autoadb scrcpy -s '{}' From cac8e9c821b9b8314d57e84ccbf685929af08794 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 1 Jan 2025 15:01:18 +0100 Subject: [PATCH 2188/2244] Happy new year 2025! --- LICENSE | 2 +- README.md | 2 +- app/scrcpy.1 | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/LICENSE b/LICENSE index d9326a74..1196b3da 100644 --- a/LICENSE +++ b/LICENSE @@ -188,7 +188,7 @@ identification within third-party archives. Copyright (C) 2018 Genymobile - Copyright (C) 2018-2024 Romain Vimont + Copyright (C) 2018-2025 Romain Vimont Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index 5eb59ba5..b5884350 100644 --- a/README.md +++ b/README.md @@ -210,7 +210,7 @@ work][donate]: ## Licence Copyright (C) 2018 Genymobile - Copyright (C) 2018-2024 Romain Vimont + Copyright (C) 2018-2025 Romain Vimont Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 924905e4..f8b39112 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -829,7 +829,7 @@ Report bugs to . .SH COPYRIGHT Copyright \(co 2018 Genymobile -Copyright \(co 2018\-2024 Romain Vimont +Copyright \(co 2018\-2025 Romain Vimont Licensed under the Apache License, Version 2.0. From 0ba9d3570560cb46b52a0696134442aeb7f634e6 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 15 Jan 2025 10:54:57 +0100 Subject: [PATCH 2189/2244] Mention virtual display destruction The new virtual display does not persist after scrcpy exits. --- doc/virtual_display.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/virtual_display.md b/doc/virtual_display.md index 5d1673e8..09e6f142 100644 --- a/doc/virtual_display.md +++ b/doc/virtual_display.md @@ -11,6 +11,8 @@ scrcpy --new-display # use the main display size and density scrcpy --new-display=/240 # use the main display size and 240 dpi ``` +The new virtual display is destroyed on exit. + ## Start app On some devices, a launcher is available in the virtual display. From 986328ff9ea82a709f2c31ad3cdd4e86ac845c24 Mon Sep 17 00:00:00 2001 From: Sam Listopad II Date: Thu, 30 Jan 2025 14:02:12 -0600 Subject: [PATCH 2190/2244] Allow controls with --no-window Without a window, mouse and keyboard events may not be received, but the control channel is still necessary for other features: * --turn-screen-off * --stay-awake * --show-touches * --power-off-on-close * --start-app Fixes #5803 PR #5804 Signed-off-by: Romain Vimont --- app/scrcpy.1 | 2 +- app/src/cli.c | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 924905e4..75bf6088 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -389,7 +389,7 @@ Disable video playback on the computer. .TP .B \-\-no\-window -Disable scrcpy window. Implies --no-video-playback and --no-control. +Disable scrcpy window. Implies --no-video-playback. .TP .BI "\-\-orientation " value diff --git a/app/src/cli.c b/app/src/cli.c index 756934ea..a2e6ab1a 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -689,8 +689,7 @@ static const struct sc_option options[] = { { .longopt_id = OPT_NO_WINDOW, .longopt = "no-window", - .text = "Disable scrcpy window. Implies --no-video-playback and " - "--no-control.", + .text = "Disable scrcpy window. Implies --no-video-playback.", }, { .longopt_id = OPT_ORIENTATION, @@ -2761,9 +2760,10 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], #endif if (!opts->window) { - // Without window, there cannot be any video playback or control + // Without window, there cannot be any video playback opts->video_playback = false; - opts->control = false; + // Controls are still possible, allowing for options like + // --turn-screen-off } if (!opts->video) { From fd8bef68b794442d9f3bbf47d85632f9a16e504a Mon Sep 17 00:00:00 2001 From: "chengjian.scj" Date: Wed, 25 Dec 2024 15:30:37 +0800 Subject: [PATCH 2191/2244] Add --display-ime-policy option Add an option to select where the IME should be displayed. Possible values are "local", "fallback" and "hide". PR #5703 Signed-off-by: Romain Vimont --- app/data/bash-completion/scrcpy | 5 ++ app/data/zsh-completion/_scrcpy | 1 + app/scrcpy.1 | 13 ++++ app/src/cli.c | 51 +++++++++++++++ app/src/options.c | 1 + app/src/options.h | 8 +++ app/src/scrcpy.c | 1 + app/src/server.c | 19 ++++++ app/src/server.h | 1 + doc/virtual_display.md | 12 ++++ .../java/com/genymobile/scrcpy/CleanUp.java | 29 +++++++-- .../java/com/genymobile/scrcpy/Options.java | 22 +++++++ .../java/com/genymobile/scrcpy/Server.java | 12 +++- .../scrcpy/video/NewDisplayCapture.java | 6 ++ .../scrcpy/wrappers/WindowManager.java | 65 +++++++++++++++++++ 15 files changed, 239 insertions(+), 7 deletions(-) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index 29130892..8d149f97 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -23,6 +23,7 @@ _scrcpy() { -d --select-usb --disable-screensaver --display-id= + --display-ime-policy= --display-orientation= -e --select-tcpip -f --fullscreen @@ -148,6 +149,10 @@ _scrcpy() { COMPREPLY=($(compgen -W '0 90 180 270 flip0 flip90 flip180 flip270' -- "$cur")) return ;; + --display-ime-policy) + COMPREPLY=($(compgen -W 'local fallback hide' -- "$cur")) + return + ;; --record-orientation) COMPREPLY=($(compgen -W '0 90 180 270' -- "$cur")) return diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 0897b9cc..cccfcc6a 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -30,6 +30,7 @@ arguments=( {-d,--select-usb}'[Use USB device]' '--disable-screensaver[Disable screensaver while scrcpy is running]' '--display-id=[Specify the display id to mirror]' + '--display-ime-policy[Set the policy for selecting where the IME should be displayed]' '--display-orientation=[Set the initial display orientation]:orientation values:(0 90 180 270 flip0 flip90 flip180 flip270)' {-e,--select-tcpip}'[Use TCP/IP device]' {-f,--fullscreen}'[Start in fullscreen]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 75bf6088..5eea94f4 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -161,6 +161,19 @@ The available display ids can be listed by \fB\-\-list\-displays\fR. Default is 0. +.TP +.BI "\-\-display\-ime\-policy " value +Set the policy for selecting where the IME should be displayed. + +Possible values are "local", "fallback" and "hide": + + - "local" means that the IME should appear on the local display. + - "fallback" means that the IME should appear on a fallback display (the default display). + - "hide" means that the IME should be hidden. + +By default, the IME policy is left unchanged. + + .TP .BI "\-\-display\-orientation " value Set the initial display orientation. diff --git a/app/src/cli.c b/app/src/cli.c index a2e6ab1a..b83fc9ec 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -113,6 +113,7 @@ enum { OPT_ANGLE, OPT_NO_VD_SYSTEM_DECORATIONS, OPT_NO_VD_DESTROY_CONTENT, + OPT_DISPLAY_IME_POLICY, }; struct sc_option { @@ -366,6 +367,19 @@ static const struct sc_option options[] = { " scrcpy --list-displays\n" "Default is 0.", }, + { + .longopt_id = OPT_DISPLAY_IME_POLICY, + .longopt = "display-ime-policy", + .argdesc = "value", + .text = "Set the policy for selecting where the IME should be " + "displayed.\n" + "Possible values are \"local\", \"fallback\" and \"hide\".\n" + "\"local\" means that the IME should appear on the local " + "display.\n" + "\"fallback\" means that the IME should appear on a fallback " + "display (the default display).\n" + "\"hide\" means that the IME should be hidden.", + }, { .longopt_id = OPT_DISPLAY_ORIENTATION, .longopt = "display-orientation", @@ -1614,6 +1628,25 @@ parse_audio_output_buffer(const char *s, sc_tick *tick) { return true; } +static bool +parse_display_ime_policy(const char *s, enum sc_display_ime_policy *policy) { + if (!strcmp(s, "local")) { + *policy = SC_DISPLAY_IME_POLICY_LOCAL; + return true; + } + if (!strcmp(s, "fallback")) { + *policy = SC_DISPLAY_IME_POLICY_FALLBACK; + return true; + } + if (!strcmp(s, "hide")) { + *policy = SC_DISPLAY_IME_POLICY_HIDE; + return true; + } + LOGE("Unsupported display IME policy: %s (expected local, fallback or " + "hide)", s); + return false; +} + static bool parse_orientation(const char *s, enum sc_orientation *orientation) { if (!strcmp(s, "0")) { @@ -2722,6 +2755,12 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], case OPT_NO_VD_SYSTEM_DECORATIONS: opts->vd_system_decorations = false; break; + case OPT_DISPLAY_IME_POLICY: + if (!parse_display_ime_policy(optarg, + &opts->display_ime_policy)) { + return false; + } + break; default: // getopt prints the error message on stderr return false; @@ -2978,6 +3017,12 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; } + if (opts->display_ime_policy != SC_DISPLAY_IME_POLICY_UNDEFINED) { + LOGE("--display-ime-policy is only available with " + "--video-source=display"); + return false; + } + if (opts->camera_id && opts->camera_facing != SC_CAMERA_FACING_ANY) { LOGE("Cannot specify both --camera-id and --camera-facing"); return false; @@ -3019,6 +3064,12 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; } + if (opts->display_ime_policy != SC_DISPLAY_IME_POLICY_UNDEFINED + && opts->display_id == 0 && !opts->new_display) { + LOGE("--display-ime-policy is only supported on a secondary display"); + return false; + } + if (opts->audio && opts->audio_source == SC_AUDIO_SOURCE_AUTO) { // Select the audio source according to the video source if (opts->video_source == SC_VIDEO_SOURCE_DISPLAY) { diff --git a/app/src/options.c b/app/src/options.c index 044aa014..0fe82d29 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -56,6 +56,7 @@ const struct scrcpy_options scrcpy_options_default = { .capture_orientation_lock = SC_ORIENTATION_UNLOCKED, .display_orientation = SC_ORIENTATION_0, .record_orientation = SC_ORIENTATION_0, + .display_ime_policy = SC_DISPLAY_IME_POLICY_UNDEFINED, .window_x = SC_WINDOW_POSITION_UNDEFINED, .window_y = SC_WINDOW_POSITION_UNDEFINED, .window_width = 0, diff --git a/app/src/options.h b/app/src/options.h index c8425808..ef7542e3 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -89,6 +89,13 @@ enum sc_orientation_lock { SC_ORIENTATION_LOCKED_INITIAL, // lock to initial device orientation }; +enum sc_display_ime_policy { + SC_DISPLAY_IME_POLICY_UNDEFINED, + SC_DISPLAY_IME_POLICY_LOCAL, + SC_DISPLAY_IME_POLICY_FALLBACK, + SC_DISPLAY_IME_POLICY_HIDE, +}; + static inline bool sc_orientation_is_mirror(enum sc_orientation orientation) { assert(!(orientation & ~7)); @@ -251,6 +258,7 @@ struct scrcpy_options { enum sc_orientation_lock capture_orientation_lock; enum sc_orientation display_orientation; enum sc_orientation record_orientation; + enum sc_display_ime_policy display_ime_policy; int16_t window_x; // SC_WINDOW_POSITION_UNDEFINED for "auto" int16_t window_y; // SC_WINDOW_POSITION_UNDEFINED for "auto" uint16_t window_width; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 641d93f7..b3ff9b36 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -436,6 +436,7 @@ scrcpy(struct scrcpy_options *options) { .control = options->control, .display_id = options->display_id, .new_display = options->new_display, + .display_ime_policy = options->display_ime_policy, .video = options->video, .audio = options->audio, .audio_dup = options->audio_dup, diff --git a/app/src/server.c b/app/src/server.c index cf181abc..6979c09b 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -155,6 +155,21 @@ sc_server_get_audio_source_name(enum sc_audio_source audio_source) { } } +static const char * +sc_server_get_display_ime_policy_name(enum sc_display_ime_policy policy) { + switch (policy) { + case SC_DISPLAY_IME_POLICY_LOCAL: + return "local"; + case SC_DISPLAY_IME_POLICY_FALLBACK: + return "fallback"; + case SC_DISPLAY_IME_POLICY_HIDE: + return "hide"; + default: + assert(!"unexpected display IME policy"); + return NULL; + } +} + static bool validate_string(const char *s) { // The parameters values are passed as command line arguments to adb, so @@ -376,6 +391,10 @@ execute_server(struct sc_server *server, VALIDATE_STRING(params->new_display); ADD_PARAM("new_display=%s", params->new_display); } + if (params->display_ime_policy != SC_DISPLAY_IME_POLICY_UNDEFINED) { + ADD_PARAM("display_ime_policy=%s", + sc_server_get_display_ime_policy_name(params->display_ime_policy)); + } if (!params->vd_destroy_content) { ADD_PARAM("vd_destroy_content=false"); } diff --git a/app/src/server.h b/app/src/server.h index a03689ff..5f4592de 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -50,6 +50,7 @@ struct sc_server_params { bool control; uint32_t display_id; const char *new_display; + enum sc_display_ime_policy display_ime_policy; bool video; bool audio; bool audio_dup; diff --git a/doc/virtual_display.md b/doc/virtual_display.md index 5d1673e8..f1645169 100644 --- a/doc/virtual_display.md +++ b/doc/virtual_display.md @@ -61,3 +61,15 @@ To move them to the main display instead, use: ``` scrcpy --new-display --no-vd-destroy-content ``` + + +## Display IME policy + +By default, the virtual display IME appears on the default display. + +To make it appear on the local display, use `--display-ime-policy=local`: + +```bash +scrcpy --display-id=1 --display-ime-policy=local +scrcpy --new-display --display-ime-policy=local +``` diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java index 49b23e81..51db985c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java +++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java @@ -4,6 +4,7 @@ import com.genymobile.scrcpy.device.Device; import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.Settings; import com.genymobile.scrcpy.util.SettingsException; +import com.genymobile.scrcpy.wrappers.ServiceManager; import android.os.BatteryManager; import android.system.ErrnoException; @@ -97,18 +98,31 @@ public final class CleanUp { } } - boolean powerOffScreen = options.getPowerOffScreenOnClose(); int displayId = options.getDisplayId(); + int restoreDisplayImePolicy = -1; + if (displayId > 0) { + int displayImePolicy = options.getDisplayImePolicy(); + if (displayImePolicy != -1) { + int currentDisplayImePolicy = ServiceManager.getWindowManager().getDisplayImePolicy(displayId); + if (currentDisplayImePolicy != displayImePolicy) { + ServiceManager.getWindowManager().setDisplayImePolicy(displayId, displayImePolicy); + restoreDisplayImePolicy = currentDisplayImePolicy; + } + } + } + + boolean powerOffScreen = options.getPowerOffScreenOnClose(); + try { - run(displayId, restoreStayOn, disableShowTouches, powerOffScreen, restoreScreenOffTimeout); + run(displayId, restoreStayOn, disableShowTouches, powerOffScreen, restoreScreenOffTimeout, restoreDisplayImePolicy); } catch (IOException e) { Ln.e("Clean up I/O exception", e); } } - private void run(int displayId, int restoreStayOn, boolean disableShowTouches, boolean powerOffScreen, int restoreScreenOffTimeout) - throws IOException { + private void run(int displayId, int restoreStayOn, boolean disableShowTouches, boolean powerOffScreen, int restoreScreenOffTimeout, + int restoreDisplayImePolicy) throws IOException { String[] cmd = { "app_process", "/", @@ -118,6 +132,7 @@ public final class CleanUp { String.valueOf(disableShowTouches), String.valueOf(powerOffScreen), String.valueOf(restoreScreenOffTimeout), + String.valueOf(restoreDisplayImePolicy), }; ProcessBuilder builder = new ProcessBuilder(cmd); @@ -178,6 +193,7 @@ public final class CleanUp { boolean disableShowTouches = Boolean.parseBoolean(args[2]); boolean powerOffScreen = Boolean.parseBoolean(args[3]); int restoreScreenOffTimeout = Integer.parseInt(args[4]); + int restoreDisplayImePolicy = Integer.parseInt(args[5]); // Dynamic option boolean restoreDisplayPower = false; @@ -223,6 +239,11 @@ public final class CleanUp { } } + if (restoreDisplayImePolicy != -1) { + Ln.i("Restoring \"display IME policy\""); + ServiceManager.getWindowManager().setDisplayImePolicy(displayId, restoreDisplayImePolicy); + } + // Change the power of the main display when mirroring a virtual display int targetDisplayId = displayId != Device.DISPLAY_ID_NONE ? displayId : 0; if (Device.isScreenOn(targetDisplayId)) { diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 8a438750..66bb68e8 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 @@ import com.genymobile.scrcpy.video.CameraAspectRatio; import com.genymobile.scrcpy.video.CameraFacing; import com.genymobile.scrcpy.video.VideoCodec; import com.genymobile.scrcpy.video.VideoSource; +import com.genymobile.scrcpy.wrappers.WindowManager; import android.graphics.Rect; import android.util.Pair; @@ -48,6 +49,7 @@ public class Options { private boolean showTouches; private boolean stayAwake; private int screenOffTimeout = -1; + private int displayImePolicy = -1; private List videoCodecOptions; private List audioCodecOptions; @@ -186,6 +188,10 @@ public class Options { return screenOffTimeout; } + public int getDisplayImePolicy() { + return displayImePolicy; + } + public List getVideoCodecOptions() { return videoCodecOptions; } @@ -482,6 +488,9 @@ public class Options { options.captureOrientationLock = pair.first; options.captureOrientation = pair.second; break; + case "display_ime_policy": + options.displayImePolicy = parseDisplayImePolicy(value); + break; case "send_device_meta": options.sendDeviceMeta = Boolean.parseBoolean(value); break; @@ -626,4 +635,17 @@ public class Options { return Pair.create(lock, Orientation.getByName(value)); } + + private static int parseDisplayImePolicy(String value) { + switch (value) { + case "local": + return WindowManager.DISPLAY_IME_POLICY_LOCAL; + case "fallback": + return WindowManager.DISPLAY_IME_POLICY_FALLBACK_DISPLAY; + case "hide": + return WindowManager.DISPLAY_IME_POLICY_HIDE; + default: + throw new IllegalArgumentException("Invalid display IME policy: " + value); + } + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index eb8b533a..09cfd6cf 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -80,9 +80,15 @@ public final class Server { throw new ConfigurationException("Camera mirroring is not supported"); } - if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10 && options.getNewDisplay() != null) { - Ln.e("New virtual display is not supported before Android 10"); - throw new ConfigurationException("New virtual display is not supported"); + if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) { + if (options.getNewDisplay() != null) { + Ln.e("New virtual display is not supported before Android 10"); + throw new ConfigurationException("New virtual display is not supported"); + } + if (options.getDisplayImePolicy() != -1) { + Ln.e("Display IME policy is not supported before Android 10"); + throw new ConfigurationException("Display IME policy is not supported"); + } } CleanUp cleanUp = null; diff --git a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java index 033d6b9a..792b3a8a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java @@ -49,6 +49,7 @@ public class NewDisplayCapture extends SurfaceCapture { private Size mainDisplaySize; private int mainDisplayDpi; private int maxSize; + private int displayImePolicy; private final Rect crop; private final boolean captureOrientationLocked; private final Orientation captureOrientation; @@ -68,6 +69,7 @@ public class NewDisplayCapture extends SurfaceCapture { this.newDisplay = options.getNewDisplay(); assert newDisplay != null; this.maxSize = options.getMaxSize(); + this.displayImePolicy = options.getDisplayImePolicy(); this.crop = options.getCrop(); assert options.getCaptureOrientationLock() != null; this.captureOrientationLocked = options.getCaptureOrientationLock() != Orientation.Lock.Unlocked; @@ -191,6 +193,10 @@ public class NewDisplayCapture extends SurfaceCapture { virtualDisplayId = virtualDisplay.getDisplay().getDisplayId(); Ln.i("New display: " + displaySize.getWidth() + "x" + displaySize.getHeight() + "/" + dpi + " (id=" + virtualDisplayId + ")"); + if (displayImePolicy != -1) { + ServiceManager.getWindowManager().setDisplayImePolicy(virtualDisplayId, displayImePolicy); + } + displaySizeMonitor.start(virtualDisplayId, this::invalidate); } catch (Exception e) { Ln.e("Could not create display", e); diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java index 04f5abd7..08bab1a9 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java @@ -4,12 +4,19 @@ import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.util.Ln; import android.annotation.TargetApi; +import android.os.Build; import android.os.IInterface; import android.view.IDisplayWindowListener; import java.lang.reflect.Method; public final class WindowManager { + + // + public static final int DISPLAY_IME_POLICY_LOCAL = 0; + public static final int DISPLAY_IME_POLICY_FALLBACK_DISPLAY = 1; + public static final int DISPLAY_IME_POLICY_HIDE = 2; + private final IInterface manager; private Method getRotationMethod; @@ -22,6 +29,9 @@ public final class WindowManager { private Method thawDisplayRotationMethod; private int thawDisplayRotationMethodVersion; + private Method getDisplayImePolicyMethod; + private Method setDisplayImePolicyMethod; + static WindowManager create() { IInterface manager = ServiceManager.getService("window", "android.view.IWindowManager"); return new WindowManager(manager); @@ -198,4 +208,59 @@ public final class WindowManager { Ln.e("Could not unregister display window listener", e); } } + + @TargetApi(AndroidVersions.API_29_ANDROID_10) + private Method getGetDisplayImePolicyMethod() throws NoSuchMethodException { + if (getDisplayImePolicyMethod == null) { + if (Build.VERSION.SDK_INT >= AndroidVersions.API_31_ANDROID_12) { + getDisplayImePolicyMethod = manager.getClass().getMethod("getDisplayImePolicy", int.class); + } else { + getDisplayImePolicyMethod = manager.getClass().getMethod("shouldShowIme", int.class); + } + } + return getDisplayImePolicyMethod; + } + + @TargetApi(AndroidVersions.API_29_ANDROID_10) + public int getDisplayImePolicy(int displayId) { + try { + Method method = getGetDisplayImePolicyMethod(); + if (Build.VERSION.SDK_INT >= AndroidVersions.API_31_ANDROID_12) { + return (int) method.invoke(manager, displayId); + } + boolean shouldShowIme = (boolean) method.invoke(manager, displayId); + return shouldShowIme ? DISPLAY_IME_POLICY_LOCAL : DISPLAY_IME_POLICY_FALLBACK_DISPLAY; + } catch (ReflectiveOperationException e) { + Ln.e("Could not invoke method", e); + return -1; + } + } + + @TargetApi(AndroidVersions.API_29_ANDROID_10) + private Method getSetDisplayImePolicyMethod() throws NoSuchMethodException { + if (setDisplayImePolicyMethod == null) { + if (Build.VERSION.SDK_INT >= AndroidVersions.API_31_ANDROID_12) { + setDisplayImePolicyMethod = manager.getClass().getMethod("setDisplayImePolicy", int.class, int.class); + } else { + setDisplayImePolicyMethod = manager.getClass().getMethod("setShouldShowIme", int.class, boolean.class); + } + } + return setDisplayImePolicyMethod; + } + + @TargetApi(AndroidVersions.API_29_ANDROID_10) + public void setDisplayImePolicy(int displayId, int displayImePolicy) { + try { + Method method = getSetDisplayImePolicyMethod(); + if (Build.VERSION.SDK_INT >= AndroidVersions.API_31_ANDROID_12) { + method.invoke(manager, displayId, displayImePolicy); + } else if (displayImePolicy != DISPLAY_IME_POLICY_HIDE) { + method.invoke(manager, displayId, displayImePolicy == DISPLAY_IME_POLICY_LOCAL); + } else { + Ln.w("DISPLAY_IME_POLICY_HIDE is not supported before Android 12"); + } + } catch (ReflectiveOperationException e) { + Ln.e("Could not invoke method", e); + } + } } From d892a9aac58bd35b778ff8a0a933d11ec8092df1 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 22 Feb 2025 12:22:45 +0100 Subject: [PATCH 2192/2244] Disable checkstyle line length warning Checkstyle reports a warning because the line containing a long URL is more than 150 characters. But we can't split the URL, so disable the warning. --- .../main/java/com/genymobile/scrcpy/wrappers/WindowManager.java | 1 + 1 file changed, 1 insertion(+) 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 08bab1a9..7ba5cc06 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java @@ -12,6 +12,7 @@ import java.lang.reflect.Method; public final class WindowManager { + @SuppressWarnings("checkstyle:LineLength") // public static final int DISPLAY_IME_POLICY_LOCAL = 0; public static final int DISPLAY_IME_POLICY_FALLBACK_DISPLAY = 1; From c63d9e1803c658b29b067d5ea68baa38df7e1359 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 7 Mar 2025 18:40:28 +0100 Subject: [PATCH 2193/2244] Work around broken display listener on Android 15 A recent Android 15 upgrade broke the display listener (again). Use the alternative method for Android >= 14. Fixes #5908 --- .../java/com/genymobile/scrcpy/video/DisplaySizeMonitor.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/video/DisplaySizeMonitor.java b/server/src/main/java/com/genymobile/scrcpy/video/DisplaySizeMonitor.java index ff863aa8..3d7cccfe 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/DisplaySizeMonitor.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/DisplaySizeMonitor.java @@ -23,7 +23,9 @@ public class DisplaySizeMonitor { // On Android 14, DisplayListener may be broken (it never sends events). This is fixed in recent Android 14 upgrades, but we can't really // detect it directly, so register a DisplayWindowListener (introduced in Android 11) to listen to configuration changes instead. - private static final boolean USE_DEFAULT_METHOD = Build.VERSION.SDK_INT != AndroidVersions.API_34_ANDROID_14; + // It has been broken again after an Android 15 upgrade: + // So use the default method only before Android 14. + private static final boolean USE_DEFAULT_METHOD = Build.VERSION.SDK_INT < AndroidVersions.API_34_ANDROID_14; private DisplayManager.DisplayListenerHandle displayListenerHandle; private HandlerThread handlerThread; From 7044122fc59cad404722215f375a711667bc4fa0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 9 Mar 2025 21:10:21 +0100 Subject: [PATCH 2194/2244] Simplify wording in README --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index b5884350..404359f2 100644 --- a/README.md +++ b/README.md @@ -8,10 +8,10 @@ their name contains `scrcpy`.** _pronounced "**scr**een **c**o**py**"_ -This application mirrors Android devices (video and audio) connected via -USB or [over TCP/IP](doc/connection.md#tcpip-wireless), and allows to control the -device with the keyboard and the mouse of the computer. It does not require any -_root_ access. It works on _Linux_, _Windows_ and _macOS_. +This application mirrors Android devices (video and audio) connected via USB or +[TCP/IP](doc/connection.md#tcpip-wireless) and allows control using the +computer's keyboard and mouse. It does not require _root_ access. It works on +_Linux_, _Windows_, and _macOS_. ![screenshot](assets/screenshot-debian-600.jpg) From 7998811fa55a4f1610dd07680a387251ead2dd1f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 9 Mar 2025 21:15:58 +0100 Subject: [PATCH 2195/2244] Mention that no Android app is required --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 404359f2..16b8bca1 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,8 @@ _pronounced "**scr**een **c**o**py**"_ This application mirrors Android devices (video and audio) connected via USB or [TCP/IP](doc/connection.md#tcpip-wireless) and allows control using the -computer's keyboard and mouse. It does not require _root_ access. It works on -_Linux_, _Windows_, and _macOS_. +computer's keyboard and mouse. It does not require _root_ access or an app +installed on the device. It works on _Linux_, _Windows_, and _macOS_. ![screenshot](assets/screenshot-debian-600.jpg) From 457c7fe5cfa5165c02d4263cd7d2a41598990d0e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 22 Feb 2025 11:09:16 +0100 Subject: [PATCH 2196/2244] Disable audio regulator underflow logs Only enable them if SC_AUDIO_REGULATOR_DEBUG is set, as they may spam the output. PR #5870 --- app/src/audio_regulator.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/audio_regulator.c b/app/src/audio_regulator.c index f7e9b81e..f11ed0e7 100644 --- a/app/src/audio_regulator.c +++ b/app/src/audio_regulator.c @@ -76,8 +76,10 @@ sc_audio_regulator_pull(struct sc_audio_regulator *ar, uint8_t *out, // Wait until the buffer is filled up to at least target_buffering // before playing if (buffered_samples < ar->target_buffering) { - LOGV("[Audio] Inserting initial buffering silence: %" PRIu32 +#ifdef SC_AUDIO_REGULATOR_DEBUG + LOGD("[Audio] Inserting initial buffering silence: %" PRIu32 " samples", out_samples); +#endif // Delay playback starting to reach the target buffering. Fill the // whole buffer with silence (len is small compared to the // arbitrary margin value). @@ -98,8 +100,10 @@ sc_audio_regulator_pull(struct sc_audio_regulator *ar, uint8_t *out, // dropped to keep the latency minimal. However, this would cause very // audible glitches, so let the clock compensation restore the target // latency. +#ifdef SC_AUDIO_REGULATOR_DEBUG LOGD("[Audio] Buffer underflow, inserting silence: %" PRIu32 " samples", silence); +#endif memset(out + TO_BYTES(read), 0, TO_BYTES(silence)); bool received = atomic_load_explicit(&ar->received, From 1d253381198495ae351e2fba377a0b325f4b8bfb Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 22 Feb 2025 11:20:35 +0100 Subject: [PATCH 2197/2244] Report underflow samples in verbose mode Report the number of silence samples inserted due to underflow every second, along with the other metrics. PR #5870 --- app/src/audio_regulator.c | 6 +++++- app/src/audio_regulator.h | 3 +++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/app/src/audio_regulator.c b/app/src/audio_regulator.c index f11ed0e7..66900b51 100644 --- a/app/src/audio_regulator.c +++ b/app/src/audio_regulator.c @@ -213,6 +213,7 @@ sc_audio_regulator_push(struct sc_audio_regulator *ar, const AVFrame *frame) { if (played) { underflow = atomic_exchange_explicit(&ar->underflow, 0, memory_order_relaxed); + ar->underflow_report += underflow; max_buffered_samples = ar->target_buffering * 11 / 10 + 60 * ar->sample_rate / 1000 /* 60 ms */; @@ -315,7 +316,9 @@ sc_audio_regulator_push(struct sc_audio_regulator *ar, const AVFrame *frame) { int abs_max_diff = distance / 50; diff = CLAMP(diff, -abs_max_diff, abs_max_diff); LOGV("[Audio] Buffering: target=%" PRIu32 " avg=%f cur=%" PRIu32 - " compensation=%d", ar->target_buffering, avg, can_read, diff); + " compensation=%d (underflow=%" PRIu32 ")", + ar->target_buffering, avg, can_read, diff, ar->underflow_report); + ar->underflow_report = 0; int ret = swr_set_compensation(swr_ctx, diff, distance); if (ret < 0) { @@ -398,6 +401,7 @@ sc_audio_regulator_init(struct sc_audio_regulator *ar, size_t sample_size, atomic_init(&ar->played, false); atomic_init(&ar->received, false); atomic_init(&ar->underflow, 0); + ar->underflow_report = 0; ar->compensation_active = false; return true; diff --git a/app/src/audio_regulator.h b/app/src/audio_regulator.h index 03cf6325..79238fbe 100644 --- a/app/src/audio_regulator.h +++ b/app/src/audio_regulator.h @@ -46,6 +46,9 @@ struct sc_audio_regulator { // Number of silence samples inserted since the last received packet atomic_uint_least32_t underflow; + // Number of silence samples inserted since the last log + uint32_t underflow_report; + // Non-zero compensation applied (only used by the receiver thread) bool compensation_active; From 245981281e99662aa77e53259a0864ead39ff438 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 2 Mar 2025 17:09:43 +0100 Subject: [PATCH 2198/2244] Fix PTS produced by the default opus/flac encoders The default OPUS and FLAC encoders on Android rewrite the input PTS so that they exactly match the number of samples. As a consequence: - audio clock drift is not compensated - implicit silences (without packets) are ignored To work around this behavior, generate new PTS based on the current time (after encoding) and the packet duration. PR #5870 --- .../genymobile/scrcpy/audio/AudioEncoder.java | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java index 267be60a..33177228 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java @@ -55,6 +55,9 @@ public final class AudioEncoder implements AsyncProcessor { private final List codecOptions; private final String encoderName; + private boolean recreatePts; + private long previousPts; + // Capacity of 64 is in practice "infinite" (it is limited by the number of available MediaCodec buffers, typically 4). // So many pending tasks would lead to an unacceptable delay anyway. private final BlockingQueue inputTasks = new ArrayBlockingQueue<>(64); @@ -118,6 +121,9 @@ public final class AudioEncoder implements AsyncProcessor { OutputTask task = outputTasks.take(); ByteBuffer buffer = mediaCodec.getOutputBuffer(task.index); try { + if (recreatePts) { + fixTimestamp(task.bufferInfo); + } streamer.writePacket(buffer, task.bufferInfo); } finally { mediaCodec.releaseOutputBuffer(task.index, false); @@ -125,6 +131,25 @@ public final class AudioEncoder implements AsyncProcessor { } } + private void fixTimestamp(MediaCodec.BufferInfo bufferInfo) { + assert recreatePts; + + if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) { + // Config packet, nothing to fix + return; + } + + long pts = bufferInfo.presentationTimeUs; + if (previousPts != 0) { + long now = System.nanoTime() / 1000; + // This specific encoder produces PTS matching the exact number of samples + long duration = pts - previousPts; + bufferInfo.presentationTimeUs = now - duration; + } + + previousPts = pts; + } + @Override public void start(TerminationListener listener) { thread = new Thread(() -> { @@ -194,6 +219,12 @@ public final class AudioEncoder implements AsyncProcessor { Codec codec = streamer.getCodec(); mediaCodec = createMediaCodec(codec, encoderName); + // The default OPUS and FLAC encoders overwrite the input PTS with a value that matches the number of samples. This is not the behavior + // we want: it ignores any audio clock drift and hard silences (packets not produced on silence). To work around this behavior, + // regenerate PTS based on the current time and the packet duration. + String codecName = mediaCodec.getCanonicalName(); + recreatePts = "c2.android.opus.encoder".equals(codecName) || "c2.android.flac.encoder".equals(codecName); + mediaCodecThread = new HandlerThread("media-codec"); mediaCodecThread.start(); From 3a0703f428fe024c0bb226c0e311966be22ef57c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 16 Feb 2025 17:38:27 +0100 Subject: [PATCH 2199/2244] Handle audio stream discontinuities The audio regulator assumed a continuous audio stream. But some audio sources (like the "voice call" audio source) do not produce any packets on silence, breaking this assumption. Use PTS to detect such discontinuities. PR #5870 --- app/src/audio_regulator.c | 33 ++++++++++++++++++++++++++++++++- app/src/audio_regulator.h | 3 +++ app/src/util/audiobuf.c | 35 +++++++++++++++++++++++++++++++++++ app/src/util/audiobuf.h | 3 +++ app/tests/test_audiobuf.c | 8 ++++++++ 5 files changed, 81 insertions(+), 1 deletion(-) diff --git a/app/src/audio_regulator.c b/app/src/audio_regulator.c index 66900b51..16fdd08b 100644 --- a/app/src/audio_regulator.c +++ b/app/src/audio_regulator.c @@ -141,6 +141,36 @@ bool sc_audio_regulator_push(struct sc_audio_regulator *ar, const AVFrame *frame) { SwrContext *swr_ctx = ar->swr_ctx; + uint32_t input_samples = frame->nb_samples; + + assert(frame->pts >= 0); + int64_t pts = frame->pts; + if (ar->next_expected_pts && pts - ar->next_expected_pts > 100000) { + LOGV("[Audio] Discontinuity detected: %" PRIi64 "µs", + pts - ar->next_expected_pts); + // More than 100ms: consider it as a discontinuity + // (typically because silence packets were not captured) + uint32_t can_read = sc_audiobuf_can_read(&ar->buf); + if (input_samples + can_read < ar->target_buffering) { + // Adjust buffering to the target value directly + uint32_t silence = ar->target_buffering - can_read - input_samples; + sc_audiobuf_write_silence(&ar->buf, silence); + } + + // Reset state + ar->avg_buffering.avg = ar->target_buffering; + int ret = swr_set_compensation(swr_ctx, 0, 0); + (void) ret; + assert(!ret); // disabling compensation should never fail + ar->compensation_active = false; + ar->samples_since_resync = 0; + atomic_store_explicit(&ar->underflow, 0, memory_order_relaxed); + } + + int64_t packet_duration = input_samples * INT64_C(1000000) + / ar->sample_rate; + ar->next_expected_pts = pts + packet_duration; + int64_t swr_delay = swr_get_delay(swr_ctx, ar->sample_rate); // No need to av_rescale_rnd(), input and output sample rates are the same. // Add more space (256) for clock compensation. @@ -260,7 +290,7 @@ sc_audio_regulator_push(struct sc_audio_regulator *ar, const AVFrame *frame) { } // Number of samples added (or removed, if negative) for compensation - int32_t instant_compensation = (int32_t) written - frame->nb_samples; + int32_t instant_compensation = (int32_t) written - input_samples; // Inserting silence instantly increases buffering int32_t inserted_silence = (int32_t) underflow; // Dropping input samples instantly decreases buffering @@ -403,6 +433,7 @@ sc_audio_regulator_init(struct sc_audio_regulator *ar, size_t sample_size, atomic_init(&ar->underflow, 0); ar->underflow_report = 0; ar->compensation_active = false; + ar->next_expected_pts = 0; return true; diff --git a/app/src/audio_regulator.h b/app/src/audio_regulator.h index 79238fbe..4e18fe08 100644 --- a/app/src/audio_regulator.h +++ b/app/src/audio_regulator.h @@ -57,6 +57,9 @@ struct sc_audio_regulator { // Set to true the first time samples are pulled by the player atomic_bool played; + + // PTS of the next expected packet (useful to detect discontinuities) + int64_t next_expected_pts; }; bool diff --git a/app/src/util/audiobuf.c b/app/src/util/audiobuf.c index 3cc5cad1..eeb27514 100644 --- a/app/src/util/audiobuf.c +++ b/app/src/util/audiobuf.c @@ -116,3 +116,38 @@ sc_audiobuf_write(struct sc_audiobuf *buf, const void *from_, return samples_count; } + +uint32_t +sc_audiobuf_write_silence(struct sc_audiobuf *buf, uint32_t samples_count) { + // Only the writer thread can write head, so memory_order_relaxed is + // sufficient + uint32_t head = atomic_load_explicit(&buf->head, memory_order_relaxed); + + // The tail cursor is updated after the data is consumed by the reader + uint32_t tail = atomic_load_explicit(&buf->tail, memory_order_acquire); + + uint32_t can_write = (buf->alloc_size + tail - head - 1) % buf->alloc_size; + if (!can_write) { + return 0; + } + if (samples_count > can_write) { + samples_count = can_write; + } + + uint32_t right_count = buf->alloc_size - head; + if (right_count > samples_count) { + right_count = samples_count; + } + memset(buf->data + (head * buf->sample_size), 0, + right_count * buf->sample_size); + + if (samples_count > right_count) { + uint32_t left_count = samples_count - right_count; + memset(buf->data, 0, left_count * buf->sample_size); + } + + uint32_t new_head = (head + samples_count) % buf->alloc_size; + atomic_store_explicit(&buf->head, new_head, memory_order_release); + + return samples_count; +} diff --git a/app/src/util/audiobuf.h b/app/src/util/audiobuf.h index 5cc51932..b55a5a59 100644 --- a/app/src/util/audiobuf.h +++ b/app/src/util/audiobuf.h @@ -50,6 +50,9 @@ uint32_t sc_audiobuf_write(struct sc_audiobuf *buf, const void *from, uint32_t samples_count); +uint32_t +sc_audiobuf_write_silence(struct sc_audiobuf *buf, uint32_t samples); + static inline uint32_t sc_audiobuf_capacity(struct sc_audiobuf *buf) { assert(buf->alloc_size); diff --git a/app/tests/test_audiobuf.c b/app/tests/test_audiobuf.c index 94d0f07a..539ee238 100644 --- a/app/tests/test_audiobuf.c +++ b/app/tests/test_audiobuf.c @@ -113,6 +113,14 @@ static void test_audiobuf_partial_read_write(void) { uint32_t expected2[] = {4, 5, 6, 1, 2, 3, 4, 1, 2, 3}; assert(!memcmp(data, expected2, 12)); + w = sc_audiobuf_write_silence(&buf, 4); + assert(w == 4); + + r = sc_audiobuf_read(&buf, data, 4); + assert(r == 4); + uint32_t expected3[] = {0, 0, 0, 0}; + assert(!memcmp(data, expected3, 4)); + sc_audiobuf_destroy(&buf); } From 609719bde025559067e9d11672178531f8431619 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 22 Feb 2025 11:40:24 +0100 Subject: [PATCH 2200/2244] Refactor audio sources Store the target audio source integer (one of the constants from android.media.MediaRecorder.AudioSource) in the AudioSource enum (or -1 if not relevant). This will simplify adding new audio sources. PR #5870 --- .../scrcpy/audio/AudioDirectCapture.java | 14 +------------- .../com/genymobile/scrcpy/audio/AudioSource.java | 16 ++++++++++++---- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioDirectCapture.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioDirectCapture.java index 5c859738..bf870bee 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioDirectCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioDirectCapture.java @@ -12,7 +12,6 @@ import android.content.ComponentName; import android.content.Intent; import android.media.AudioRecord; import android.media.MediaCodec; -import android.media.MediaRecorder; import android.os.Build; import android.os.SystemClock; @@ -32,18 +31,7 @@ public class AudioDirectCapture implements AudioCapture { private AudioRecordReader reader; public AudioDirectCapture(AudioSource audioSource) { - this.audioSource = getAudioSourceValue(audioSource); - } - - private static int getAudioSourceValue(AudioSource audioSource) { - switch (audioSource) { - case OUTPUT: - return MediaRecorder.AudioSource.REMOTE_SUBMIX; - case MIC: - return MediaRecorder.AudioSource.MIC; - default: - throw new IllegalArgumentException("Unsupported audio source: " + audioSource); - } + this.audioSource = audioSource.getDirectAudioSource(); } @TargetApi(AndroidVersions.API_23_ANDROID_6_0) diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioSource.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioSource.java index 6082f20e..353f2281 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioSource.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioSource.java @@ -1,20 +1,28 @@ package com.genymobile.scrcpy.audio; +import android.media.MediaRecorder; + public enum AudioSource { - OUTPUT("output"), - MIC("mic"), - PLAYBACK("playback"); + OUTPUT("output", MediaRecorder.AudioSource.REMOTE_SUBMIX), + MIC("mic", MediaRecorder.AudioSource.MIC), + PLAYBACK("playback", -1); private final String name; + private final int directAudioSource; - AudioSource(String name) { + AudioSource(String name, int directAudioSource) { this.name = name; + this.directAudioSource = directAudioSource; } public boolean isDirect() { return this != PLAYBACK; } + public int getDirectAudioSource() { + return directAudioSource; + } + public static AudioSource findByName(String name) { for (AudioSource audioSource : AudioSource.values()) { if (name.equals(audioSource.name)) { From bef2d8473b3426b1dd2ea9ed0cae3a243a999218 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 22 Feb 2025 12:18:05 +0100 Subject: [PATCH 2201/2244] Add more audio sources Expose more audio sources from MediaRecorder.AudioSource. Refs Fixes #5412 Fixes #5670 PR #5870 --- app/data/bash-completion/scrcpy | 2 +- app/data/zsh-completion/_scrcpy | 2 +- app/scrcpy.1 | 18 +++-- app/src/cli.c | 76 +++++++++++++++++-- app/src/options.h | 8 ++ app/src/server.c | 16 ++++ doc/audio.md | 14 ++++ .../genymobile/scrcpy/audio/AudioSource.java | 12 ++- 8 files changed, 131 insertions(+), 17 deletions(-) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index 8d149f97..9918918c 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -122,7 +122,7 @@ _scrcpy() { return ;; --audio-source) - COMPREPLY=($(compgen -W 'output mic playback' -- "$cur")) + COMPREPLY=($(compgen -W 'output playback mic mic-unprocessed mic-camcorder mic-voice-recognition mic-voice-communication voice-call voice-call-uplink voice-call-downlink voice-performance' -- "$cur")) return ;; --camera-facing) diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index cccfcc6a..450fc8f5 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -16,7 +16,7 @@ arguments=( '--audio-codec-options=[Set a list of comma-separated key\:type=value options for the device audio encoder]' '--audio-dup=[Duplicate audio]' '--audio-encoder=[Use a specific MediaCodec audio encoder]' - '--audio-source=[Select the audio source]:source:(output mic playback)' + '--audio-source=[Select the audio source]:source:(output playback mic mic-unprocessed mic-camcorder mic-voice-recognition mic-voice-communication voice-call voice-call-uplink voice-call-downlink voice-performance)' '--audio-output-buffer=[Configure the size of the SDL audio output buffer (in milliseconds)]' {-b,--video-bit-rate=}'[Encode the video at the given bit-rate]' '--camera-ar=[Select the camera size by its aspect ratio]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 5eea94f4..ffb66ab9 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -67,13 +67,19 @@ The available encoders can be listed by \fB\-\-list\-encoders\fR. .TP .BI "\-\-audio\-source " source -Select the audio source (output, mic or playback). +Select the audio source. Possible values are: -The "output" source forwards the whole audio output, and disables playback on the device. - -The "playback" source captures the audio playback (Android apps can opt-out, so the whole output is not necessarily captured). - -The "mic" source captures the microphone. + - "output": forwards the whole audio output, and disables playback on the device. + - "playback": captures the audio playback (Android apps can opt-out, so the whole output is not necessarily captured). + - "mic": captures the microphone. + - "mic-unprocessed": captures the microphone unprocessed (raw) sound. + - "mic-camcorder": captures the microphone tuned for video recording, with the same orientation as the camera if available. + - "mic-voice-recognition": captures the microphone tuned for voice recognition. + - "mic-voice-communication": captures the microphone tuned for voice communications (it will for instance take advantage of echo cancellation or automatic gain control if available). + - "voice-call": captures voice call. + - "voice-call-uplink": captures voice call uplink only. + - "voice-call-downlink": captures voice call downlink only. + - "voice-performance": captures audio meant to be processed for live performance (karaoke), includes both the microphone and the device playback. Default is output. diff --git a/app/src/cli.c b/app/src/cli.c index b83fc9ec..b2e3e30a 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -217,13 +217,31 @@ static const struct sc_option options[] = { .longopt_id = OPT_AUDIO_SOURCE, .longopt = "audio-source", .argdesc = "source", - .text = "Select the audio source (output, mic or playback).\n" - "The \"output\" source forwards the whole audio output, and " - "disables playback on the device.\n" - "The \"playback\" source captures the audio playback (Android " - "apps can opt-out, so the whole output is not necessarily " + .text = "Select the audio source. Possible values are:\n" + " - \"output\": forwards the whole audio output, and disables " + "playback on the device.\n" + " - \"playback\": captures the audio playback (Android apps " + "can opt-out, so the whole output is not necessarily " "captured).\n" - "The \"mic\" source captures the microphone.\n" + " - \"mic\": captures the microphone.\n" + " - \"mic-unprocessed\": captures the microphone unprocessed " + "(raw) sound.\n" + " - \"mic-camcorder\": captures the microphone tuned for video " + "recording, with the same orientation as the camera if " + "available.\n" + " - \"mic-voice-recognition\": captures the microphone tuned " + "for voice recognition.\n" + " - \"mic-voice-communication\": captures the microphone tuned " + "for voice communications (it will for instance take advantage " + "of echo cancellation or automatic gain control if " + "available).\n" + " - \"voice-call\": captures voice call.\n" + " - \"voice-call-uplink\": captures voice call uplink only.\n" + " - \"voice-call-downlink\": captures voice call downlink " + "only.\n" + " - \"voice-performance\": captures audio meant to be " + "processed for live performance (karaoke), includes both the " + "microphone and the device playback.\n" "Default is output.", }, { @@ -2036,8 +2054,50 @@ parse_audio_source(const char *optarg, enum sc_audio_source *source) { return true; } - LOGE("Unsupported audio source: %s (expected output, mic or playback)", - optarg); + if (!strcmp(optarg, "mic-unprocessed")) { + *source = SC_AUDIO_SOURCE_MIC_UNPROCESSED; + return true; + } + + if (!strcmp(optarg, "mic-camcorder")) { + *source = SC_AUDIO_SOURCE_MIC_CAMCORDER; + return true; + } + + if (!strcmp(optarg, "mic-voice-recognition")) { + *source = SC_AUDIO_SOURCE_MIC_VOICE_RECOGNITION; + return true; + } + + if (!strcmp(optarg, "mic-voice-communication")) { + *source = SC_AUDIO_SOURCE_MIC_VOICE_COMMUNICATION; + return true; + } + + if (!strcmp(optarg, "voice-call")) { + *source = SC_AUDIO_SOURCE_VOICE_CALL; + return true; + } + + if (!strcmp(optarg, "voice-call-uplink")) { + *source = SC_AUDIO_SOURCE_VOICE_CALL_UPLINK; + return true; + } + + if (!strcmp(optarg, "voice-call-downlink")) { + *source = SC_AUDIO_SOURCE_VOICE_CALL_DOWNLINK; + return true; + } + + if (!strcmp(optarg, "voice-performance")) { + *source = SC_AUDIO_SOURCE_VOICE_PERFORMANCE; + return true; + } + + LOGE("Unsupported audio source: %s (expected output, mic, playback, " + "mic-unprocessed, mic-camcorder, mic-voice-recognition, " + "mic-voice-communication, voice-call, voice-call-uplink, " + "voice-call-downlink, voice-performance)", optarg); return false; } diff --git a/app/src/options.h b/app/src/options.h index ef7542e3..03b42913 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -59,6 +59,14 @@ enum sc_audio_source { SC_AUDIO_SOURCE_OUTPUT, SC_AUDIO_SOURCE_MIC, SC_AUDIO_SOURCE_PLAYBACK, + SC_AUDIO_SOURCE_MIC_UNPROCESSED, + SC_AUDIO_SOURCE_MIC_CAMCORDER, + SC_AUDIO_SOURCE_MIC_VOICE_RECOGNITION, + SC_AUDIO_SOURCE_MIC_VOICE_COMMUNICATION, + SC_AUDIO_SOURCE_VOICE_CALL, + SC_AUDIO_SOURCE_VOICE_CALL_UPLINK, + SC_AUDIO_SOURCE_VOICE_CALL_DOWNLINK, + SC_AUDIO_SOURCE_VOICE_PERFORMANCE, }; enum sc_camera_facing { diff --git a/app/src/server.c b/app/src/server.c index 6979c09b..153219c3 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -149,6 +149,22 @@ sc_server_get_audio_source_name(enum sc_audio_source audio_source) { return "mic"; case SC_AUDIO_SOURCE_PLAYBACK: return "playback"; + case SC_AUDIO_SOURCE_MIC_UNPROCESSED: + return "mic-unprocessed"; + case SC_AUDIO_SOURCE_MIC_CAMCORDER: + return "mic-camcorder"; + case SC_AUDIO_SOURCE_MIC_VOICE_RECOGNITION: + return "mic-voice-recognition"; + case SC_AUDIO_SOURCE_MIC_VOICE_COMMUNICATION: + return "mic-voice-communication"; + case SC_AUDIO_SOURCE_VOICE_CALL: + return "voice-call"; + case SC_AUDIO_SOURCE_VOICE_CALL_UPLINK: + return "voice-call-uplink"; + case SC_AUDIO_SOURCE_VOICE_CALL_DOWNLINK: + return "voice-call-downlink"; + case SC_AUDIO_SOURCE_VOICE_PERFORMANCE: + return "voice-performance"; default: assert(!"unexpected audio source"); return NULL; diff --git a/doc/audio.md b/doc/audio.md index 85f76ac5..142626f5 100644 --- a/doc/audio.md +++ b/doc/audio.md @@ -66,6 +66,20 @@ the computer: scrcpy --audio-source=mic --no-video --no-playback --record=file.opus ``` +Many sources are available: + + - `output` (default): forwards the whole audio output, and disables playback on the device (mapped to [`REMOTE_SUBMIX`](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#REMOTE_SUBMIX)). + - `playback`: captures the audio playback (Android apps can opt-out, so the whole output is not necessarily captured). + - `mic`: captures the microphone (mapped to [`MIC`](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#MIC)). + - `mic-unprocessed`: captures the microphone unprocessed (raw) sound (mapped to [`UNPROCESSED`](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#UNPROCESSED)). + - `mic-camcorder`: captures the microphone tuned for video recording, with the same orientation as the camera if available (mapped to [`CAMCORDER`](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#CAMCORDER)). + - `mic-voice-recognition`: captures the microphone tuned for voice recognition (mapped to [`VOICE_RECOGNITION`](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#VOICE_RECOGNITION)). + - `mic-voice-communication`: captures the microphone tuned for voice communications (it will for instance take advantage of echo cancellation or automatic gain control if available) (mapped to [`VOICE_COMMUNICATION`](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#VOICE_COMMUNICATION)). + - `voice-call`: captures voice call (mapped to [`VOICE_CALL`](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#VOICE_CALL)). + - `voice-call-uplink`: captures voice call uplink only (mapped to [`VOICE_UPLINK`](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#VOICE_UPLINK)). + - `voice-call-downlink`: captures voice call downlink only (mapped to [`VOICE_DOWNLINK`](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#VOICE_DOWNLINK)). + - `voice-performance`: captures audio meant to be processed for live performance (karaoke), includes both the microphone and the device playback (mapped to [`VOICE_PERFORMANCE`](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#VOICE_PERFORMANCE)). + ### Duplication An alternative device audio capture method is also available (only for Android diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioSource.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioSource.java index 353f2281..d16b5e38 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioSource.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioSource.java @@ -1,11 +1,21 @@ package com.genymobile.scrcpy.audio; +import android.annotation.SuppressLint; import android.media.MediaRecorder; +@SuppressLint("InlinedApi") public enum AudioSource { OUTPUT("output", MediaRecorder.AudioSource.REMOTE_SUBMIX), MIC("mic", MediaRecorder.AudioSource.MIC), - PLAYBACK("playback", -1); + PLAYBACK("playback", -1), + MIC_UNPROCESSED("mic-unprocessed", MediaRecorder.AudioSource.UNPROCESSED), + MIC_CAMCORDER("mic-camcorder", MediaRecorder.AudioSource.CAMCORDER), + MIC_VOICE_RECOGNITION("mic-voice-recognition", MediaRecorder.AudioSource.VOICE_RECOGNITION), + MIC_VOICE_COMMUNICATION("mic-voice-communication", MediaRecorder.AudioSource.VOICE_COMMUNICATION), + VOICE_CALL("voice-call", MediaRecorder.AudioSource.VOICE_CALL), + VOICE_CALL_UPLINK("voice-call-uplink", MediaRecorder.AudioSource.VOICE_UPLINK), + VOICE_CALL_DOWNLINK("voice-call-downlink", MediaRecorder.AudioSource.VOICE_DOWNLINK), + VOICE_PERFORMANCE("voice-performance", MediaRecorder.AudioSource.VOICE_PERFORMANCE); private final String name; private final int directAudioSource; From dd1bfae4e00e5756e5f4f43649ce9d6a824cf028 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 29 Mar 2025 15:02:38 +0100 Subject: [PATCH 2202/2244] Upgrade libusb (1.0.28) --- app/deps/libusb.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/deps/libusb.sh b/app/deps/libusb.sh index 340b0f70..4be03eb1 100755 --- a/app/deps/libusb.sh +++ b/app/deps/libusb.sh @@ -5,10 +5,10 @@ cd "$DEPS_DIR" . common process_args "$@" -VERSION=1.0.27 +VERSION=1.0.28 FILENAME=libusb-$VERSION.tar.gz PROJECT_DIR=libusb-$VERSION -SHA256SUM=e8f18a7a36ecbb11fb820bd71540350d8f61bcd9db0d2e8c18a6fb80b214a3de +SHA256SUM=378b3709a405065f8f9fb9f35e82d666defde4d342c2a1b181a9ac134d23c6fe cd "$SOURCES_DIR" From b7add421544039c4fd5ba97243578f0fbcc908a7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 29 Mar 2025 15:03:42 +0100 Subject: [PATCH 2203/2244] Upgrade SDL (2.32.2) Also apply this additional patch to fix the build: --- ...that-the-correct-struct-is-used-for-.patch | 33 +++++++++++++++++++ app/deps/sdl.sh | 5 +-- 2 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 app/deps/patches/SDL-pipewire-Ensure-that-the-correct-struct-is-used-for-.patch diff --git a/app/deps/patches/SDL-pipewire-Ensure-that-the-correct-struct-is-used-for-.patch b/app/deps/patches/SDL-pipewire-Ensure-that-the-correct-struct-is-used-for-.patch new file mode 100644 index 00000000..cbb516ec --- /dev/null +++ b/app/deps/patches/SDL-pipewire-Ensure-that-the-correct-struct-is-used-for-.patch @@ -0,0 +1,33 @@ +From 6be87ceb33a9aad3bf5204bb13b3a5e8b498fd26 Mon Sep 17 00:00:00 2001 +From: Neal Gompa +Date: Mon, 10 Feb 2025 05:00:56 -0500 +Subject: [PATCH] pipewire: Ensure that the correct struct is used for + enumeration APIs + +PipeWire now requires the correct struct type is used, otherwise +it will fail to compile. + +Reference: https://gitlab.freedesktop.org/pipewire/pipewire/-/commit/188d920733f0791413d3386e5536ee7377f71b2f + +Fixes: https://github.com/libsdl-org/SDL/issues/12224 +(cherry picked from commit d35bef64e913dd7d5dd3153a4b61f10ef837dad6) +--- + src/audio/pipewire/SDL_pipewire.c | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/src/audio/pipewire/SDL_pipewire.c b/src/audio/pipewire/SDL_pipewire.c +index 889e05decb..5d1bfc28de 100644 +--- a/src/audio/pipewire/SDL_pipewire.c ++++ b/src/audio/pipewire/SDL_pipewire.c +@@ -590,7 +590,7 @@ static void node_event_info(void *object, const struct pw_node_info *info) + + /* Need to parse the parameters to get the sample rate */ + for (i = 0; i < info->n_params; ++i) { +- pw_node_enum_params(node->proxy, 0, info->params[i].id, 0, 0, NULL); ++ pw_node_enum_params((struct pw_node*)node->proxy, 0, info->params[i].id, 0, 0, NULL); + } + + hotplug_core_sync(node); +-- +2.49.0 + diff --git a/app/deps/sdl.sh b/app/deps/sdl.sh index c098e367..c3edee58 100755 --- a/app/deps/sdl.sh +++ b/app/deps/sdl.sh @@ -5,10 +5,10 @@ cd "$DEPS_DIR" . common process_args "$@" -VERSION=2.30.10 +VERSION=2.32.2 FILENAME=SDL-$VERSION.tar.gz PROJECT_DIR=SDL-release-$VERSION -SHA256SUM=35a8b9c4f3635d85762b904ac60ca4e0806bff89faeb269caafbe80860d67168 +SHA256SUM=f2c7297ae7b3d3910a8b131e1e2a558fdd6d1a4443d5e345374d45cadfcb05a4 cd "$SOURCES_DIR" @@ -18,6 +18,7 @@ then else get_file "https://github.com/libsdl-org/SDL/archive/refs/tags/release-$VERSION.tar.gz" "$FILENAME" "$SHA256SUM" tar xf "$FILENAME" # First level directory is "$PROJECT_DIR" + patch -d "$PROJECT_DIR" -p1 < "$PATCHES_DIR"/SDL-pipewire-Ensure-that-the-correct-struct-is-used-for-.patch fi mkdir -p "$BUILD_DIR/$PROJECT_DIR" From 5d12d9071dad6d9d80197546d820dc90fbd31998 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 29 Mar 2025 15:06:00 +0100 Subject: [PATCH 2204/2244] Upgrade FFmpeg (7.1.1) --- app/deps/ffmpeg.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/deps/ffmpeg.sh b/app/deps/ffmpeg.sh index d268ca91..fb8b9a25 100755 --- a/app/deps/ffmpeg.sh +++ b/app/deps/ffmpeg.sh @@ -5,10 +5,10 @@ cd "$DEPS_DIR" . common process_args "$@" -VERSION=7.1 +VERSION=7.1.1 FILENAME=ffmpeg-$VERSION.tar.xz PROJECT_DIR=ffmpeg-$VERSION -SHA256SUM=40973D44970DBC83EF302B0609F2E74982BE2D85916DD2EE7472D30678A7ABE6 +SHA256SUM=733984395e0dbbe5c046abda2dc49a5544e7e0e1e2366bba849222ae9e3a03b1 cd "$SOURCES_DIR" From 89b624770c7cc133cd14070a1ba766df3befa85f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 29 Mar 2025 15:45:28 +0100 Subject: [PATCH 2205/2244] Bump version to 3.2 --- app/scrcpy-windows.rc | 2 +- meson.build | 2 +- server/build.gradle | 4 ++-- server/build_without_gradle.sh | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/scrcpy-windows.rc b/app/scrcpy-windows.rc index 2c441aa1..19475e0b 100644 --- a/app/scrcpy-windows.rc +++ b/app/scrcpy-windows.rc @@ -13,7 +13,7 @@ BEGIN VALUE "LegalCopyright", "Romain Vimont, Genymobile" VALUE "OriginalFilename", "scrcpy.exe" VALUE "ProductName", "scrcpy" - VALUE "ProductVersion", "3.1" + VALUE "ProductVersion", "3.2" END END BLOCK "VarFileInfo" diff --git a/meson.build b/meson.build index 84784814..b64a6c90 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('scrcpy', 'c', - version: '3.1', + version: '3.2', meson_version: '>= 0.49', default_options: [ 'c_std=c11', diff --git a/server/build.gradle b/server/build.gradle index 9c0543e9..02508001 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -7,8 +7,8 @@ android { applicationId "com.genymobile.scrcpy" minSdkVersion 21 targetSdkVersion 35 - versionCode 30100 - versionName "3.1" + versionCode 30200 + versionName "3.2" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index e0b69aee..8bb8632b 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=3.1 +SCRCPY_VERSION_NAME=3.2 PLATFORM=${ANDROID_PLATFORM:-35} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-35.0.0} From e0f37f834bb2e9371c0ca893757b60eb36e759af Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 29 Mar 2025 16:15:14 +0100 Subject: [PATCH 2206/2244] Update links to 3.2 --- README.md | 2 +- doc/build.md | 6 +++--- doc/linux.md | 6 +++--- doc/macos.md | 12 ++++++------ doc/windows.md | 12 ++++++------ install_release.sh | 4 ++-- 6 files changed, 21 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 16b8bca1..a3b0d834 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ source for the project. Do not download releases from random websites, even if their name contains `scrcpy`.** -# scrcpy (v3.1) +# scrcpy (v3.2) scrcpy diff --git a/doc/build.md b/doc/build.md index 2776ed01..afe8b21b 100644 --- a/doc/build.md +++ b/doc/build.md @@ -233,10 +233,10 @@ install` must be run as root)._ #### Option 2: Use prebuilt server - - [`scrcpy-server-v3.1`][direct-scrcpy-server] - SHA-256: `958f0944a62f23b1f33a16e9eb14844c1a04b882ca175a738c16d23cb22b86c0` + - [`scrcpy-server-v3.2`][direct-scrcpy-server] + SHA-256: `b920e0ea01936bf2482f4ba2fa985c22c13c621999e3d33b45baa5acfc1ea3d0` -[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v3.1/scrcpy-server-v3.1 +[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-server-v3.2 Download the prebuilt server somewhere, and specify its path during the Meson configuration: diff --git a/doc/linux.md b/doc/linux.md index 9beaed1e..52345d1a 100644 --- a/doc/linux.md +++ b/doc/linux.md @@ -6,11 +6,11 @@ Download a static build of the [latest release]: - - [`scrcpy-linux-x86_64-v3.1.tar.gz`][direct-linux-x86_64] (x86_64) - SHA-256: `37dba54092ed9ec6b2f8f95432f61b8ea124aec9f1e9f2b3d22d4b10bb04c59a` + - [`scrcpy-linux-x86_64-v3.2.tar.gz`][direct-linux-x86_64] (x86_64) + SHA-256: `df6cf000447428fcde322022848d655ff0211d98688d0f17cbbf21be9c1272be` [latest release]: https://github.com/Genymobile/scrcpy/releases/latest -[direct-linux-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.1/scrcpy-linux-x86_64-v3.1.tar.gz +[direct-linux-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-linux-x86_64-v3.2.tar.gz and extract it. diff --git a/doc/macos.md b/doc/macos.md index 56d9f168..b0335d18 100644 --- a/doc/macos.md +++ b/doc/macos.md @@ -6,15 +6,15 @@ Download a static build of the [latest release]: - - [`scrcpy-macos-aarch64-v3.1.tar.gz`][direct-macos-aarch64] (aarch64) - SHA-256: `478618d940421e5f57942f5479d493ecbb38210682937a200f712aee5f235daf` + - [`scrcpy-macos-aarch64-v3.2.tar.gz`][direct-macos-aarch64] (aarch64) + SHA-256: `f6d1f3c5f74d4d46f5080baa5b56b69f5edbf698d47e0cf4e2a1fd5058f9507b` - - [`scrcpy-macos-x86_64-v3.1.tar.gz`][direct-macos-x86_64] (x86_64) - SHA-256: `acde98e29c273710ffa469371dbca4a728a44c41c380381f8a54e5b5301b9e87` + - [`scrcpy-macos-x86_64-v3.2.tar.gz`][direct-macos-x86_64] (x86_64) + SHA-256: `e337d5cf0ba4e1281699c338ce5f104aee96eb7b2893dc851399b6643eb4044e` [latest release]: https://github.com/Genymobile/scrcpy/releases/latest -[direct-macos-aarch64]: https://github.com/Genymobile/scrcpy/releases/download/v3.1/scrcpy-macos-aarch64-v3.1.tar.gz -[direct-macos-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.1/scrcpy-macos-x86_64-v3.1.tar.gz +[direct-macos-aarch64]: https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-macos-aarch64-v3.2.tar.gz +[direct-macos-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-macos-x86_64-v3.2.tar.gz and extract it. diff --git a/doc/windows.md b/doc/windows.md index 89b80727..fb3e3887 100644 --- a/doc/windows.md +++ b/doc/windows.md @@ -6,14 +6,14 @@ Download the [latest release]: - - [`scrcpy-win64-v3.1.zip`][direct-win64] (64-bit) - SHA-256: `0c05ea395d95cfe36bee974eeb435a3db87ea5594ff738370d5dc3068a9538ca` - - [`scrcpy-win32-v3.1.zip`][direct-win32] (32-bit) - SHA-256: `2b4674ef76719680ac5a9b482d1943bdde3fa25821ad2e98f3c40c347d00d560` + - [`scrcpy-win64-v3.2.zip`][direct-win64] (64-bit) + SHA-256: `eaa27133e0520979873ba57ad651560a4cc2618373bd05450b23a84d32beafd0` + - [`scrcpy-win32-v3.2.zip`][direct-win32] (32-bit) + SHA-256: `4a3407d7f0c2c8a03e22a12cf0b5e1e585a5056fe23c8e5cf3252207c6fa8357` [latest release]: https://github.com/Genymobile/scrcpy/releases/latest -[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v3.1/scrcpy-win64-v3.1.zip -[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v3.1/scrcpy-win32-v3.1.zip +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-win64-v3.2.zip +[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-win32-v3.2.zip and extract it. diff --git a/install_release.sh b/install_release.sh index 3774be86..2d2d2c2f 100755 --- a/install_release.sh +++ b/install_release.sh @@ -2,8 +2,8 @@ set -e BUILDDIR=build-auto -PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v3.1/scrcpy-server-v3.1 -PREBUILT_SERVER_SHA256=958f0944a62f23b1f33a16e9eb14844c1a04b882ca175a738c16d23cb22b86c0 +PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-server-v3.2 +PREBUILT_SERVER_SHA256=b920e0ea01936bf2482f4ba2fa985c22c13c621999e3d33b45baa5acfc1ea3d0 echo "[scrcpy] Downloading prebuilt server..." wget "$PREBUILT_SERVER_URL" -O scrcpy-server From db9dc6ae836193dcd7883d001fdddbf54cbe9859 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 1 Apr 2025 11:04:34 +0200 Subject: [PATCH 2207/2244] Make the snap version as obsolete The version of scrcpy packaged in snap is currently 1.25. Refs --- doc/linux.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/linux.md b/doc/linux.md index 52345d1a..979ef568 100644 --- a/doc/linux.md +++ b/doc/linux.md @@ -27,7 +27,7 @@ Scrcpy is packaged in several distributions and package managers: - Arch Linux: `pacman -S scrcpy` - Fedora: `dnf copr enable zeno/scrcpy && dnf install scrcpy` - Gentoo: `emerge scrcpy` - - Snap: `snap install scrcpy` + - Snap: ~~`snap install scrcpy`~~ _(obsolete version)_ - … (see [repology](https://repology.org/project/scrcpy/versions)) From 882003f314ad5077a41bbc936831aeb36dd8b078 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 3 Apr 2025 08:04:11 +0200 Subject: [PATCH 2208/2244] Fix segfault on SDL event without window Since #5804, controls have been enabled even with --no-window. As a result, the Android clipboard is synchronized with the computer, causing SDL to trigger an SDL_CLIPBOARDUPDATE event. This event is ignored by scrcpy, but it was still transmitted to the sc_screen instance, even if it had not been initialized. Fix the issue by calling sc_screen_handle_event() only when a screen instance exists. Refs #5804 Fixes #5970 --- 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 b3ff9b36..4d08e667 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -165,7 +165,7 @@ sdl_configure(bool video_playback, bool disable_screensaver) { } static enum scrcpy_exit_code -event_loop(struct scrcpy *s) { +event_loop(struct scrcpy *s, bool has_screen) { SDL_Event event; while (SDL_WaitEvent(&event)) { switch (event.type) { @@ -197,7 +197,7 @@ event_loop(struct scrcpy *s) { break; } default: - if (!sc_screen_handle_event(&s->screen, &event)) { + if (has_screen && !sc_screen_handle_event(&s->screen, &event)) { return SCRCPY_EXIT_FAILURE; } break; @@ -933,7 +933,7 @@ aoa_complete: } } - ret = event_loop(s); + ret = event_loop(s, options->window); terminate_event_loop(); LOGD("quit..."); From 5900e9e39c7496bf8dfc1f246c6bbf0a1e072a69 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 7 Apr 2025 10:30:56 +0200 Subject: [PATCH 2209/2244] Remove irrelevant link in FAQ --- FAQ.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/FAQ.md b/FAQ.md index 5f089cd7..24722c74 100644 --- a/FAQ.md +++ b/FAQ.md @@ -166,14 +166,13 @@ Rebooting the device is necessary once this option is set. ### Special characters do not work -The default text injection method is [limited to ASCII characters][text-input]. -A trick allows to also inject some [accented characters][accented-characters], +The default text injection method is limited to ASCII characters. A trick allows +to also inject some [accented characters][accented-characters], but that's all. See [#37]. To avoid the problem, [change the keyboard mode to simulate a physical keyboard][hid]. -[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 [hid]: doc/keyboard.md#physical-keyboard-simulation From d2447b5c1982b8c91fbce8f515aeedac3d2ecb33 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 14 Apr 2025 18:05:08 +0200 Subject: [PATCH 2210/2244] Fix --screen-off-timeout bash completion Only the option must be auto-completed, not its value. --- app/data/bash-completion/scrcpy | 1 + 1 file changed, 1 insertion(+) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index 9918918c..a49da8ca 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -205,6 +205,7 @@ _scrcpy() { |-p|--port \ |--push-target \ |--rotation \ + |--screen-off-timeout \ |--tunnel-host \ |--tunnel-port \ |--v4l2-buffer \ From 1a0d300786827974b5a593959c1c21f89469777e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 14 Apr 2025 18:07:37 +0200 Subject: [PATCH 2211/2244] Add missing --screen-off-timeout doc in manpage Refs eff5b4b219be6043a3baf51149b1d6752569a173 --- app/scrcpy.1 | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index d481ddd1..d72fda13 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -510,6 +510,10 @@ The device serial number. Mandatory only if several devices are connected to adb .B \-S, \-\-turn\-screen\-off Turn the device screen off immediately. +.TP +.B "\-\-screen\-off\-timeout " seconds +Set the screen off timeout while scrcpy is running (restore the initial value on exit). + .TP .BI "\-\-shortcut\-mod " key\fR[+...]][,...] Specify the modifiers to use for scrcpy shortcuts. Possible keys are "lctrl", "rctrl", "lalt", "ralt", "lsuper" and "rsuper". From c5ed2cfc28ee7c7b59b11eb4db1258ac1c633bff Mon Sep 17 00:00:00 2001 From: Nicholas Wilson Date: Fri, 18 Apr 2025 09:54:59 -0500 Subject: [PATCH 2212/2244] Replace "licence" with "license" in README Although "licence" is correct in British English, the rest of the statement uses "license," so change it for consistency. PR #6017 Signed-off-by: Romain Vimont --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a3b0d834..c1fd9f7f 100644 --- a/README.md +++ b/README.md @@ -207,7 +207,7 @@ work][donate]: [donate]: https://blog.rom1v.com/about/#support-my-open-source-work -## Licence +## License Copyright (C) 2018 Genymobile Copyright (C) 2018-2025 Romain Vimont From 6875e9aa88833525b60322597304b01f6ba91987 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 24 Apr 2025 16:05:13 +0200 Subject: [PATCH 2213/2244] Revert "Fix AudioRecord package name for Android 16" This reverts commit c27d116a662c87ee84963820669ee0d2ce60e6f1. This commit breaks audio on Android 16 beta 4. Refs #5960 comment Fixes #6021 --- server/src/main/java/com/genymobile/scrcpy/FakeContext.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/FakeContext.java b/server/src/main/java/com/genymobile/scrcpy/FakeContext.java index 22fc6d49..2b83e397 100644 --- a/server/src/main/java/com/genymobile/scrcpy/FakeContext.java +++ b/server/src/main/java/com/genymobile/scrcpy/FakeContext.java @@ -72,7 +72,7 @@ public final class FakeContext extends ContextWrapper { @Override public AttributionSource getAttributionSource() { AttributionSource.Builder builder = new AttributionSource.Builder(Process.SHELL_UID); - builder.setPackageName("shell"); + builder.setPackageName(PACKAGE_NAME); return builder.build(); } From 48f38c4bb6d91e378a657082e0da7eb846d25acc Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 24 Apr 2025 16:12:28 +0200 Subject: [PATCH 2214/2244] Fix default locked capture orientation The default landscape locked orientation was reversed. Fixes #6010 --- .../java/com/genymobile/scrcpy/device/Orientation.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Orientation.java b/server/src/main/java/com/genymobile/scrcpy/device/Orientation.java index c269750e..81168aae 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/Orientation.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Orientation.java @@ -32,9 +32,11 @@ public enum Orientation { throw new IllegalArgumentException("Unknown orientation: " + name); } - public static Orientation fromRotation(int rotation) { - assert rotation >= 0 && rotation < 4; - return values()[rotation]; + public static Orientation fromRotation(int ccwRotation) { + assert ccwRotation >= 0 && ccwRotation < 4; + // Display rotation is expressed counter-clockwise, orientation is expressed clockwise + int cwRotation = (4 - ccwRotation) % 4; + return values()[cwRotation]; } public boolean isFlipped() { From 91a4a74641903bedff189548cc5c33289752b4b4 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 25 Apr 2025 10:23:08 +0200 Subject: [PATCH 2215/2244] Move regex pattern initialization If text == null, then the Pattern is not used. --- .../java/com/genymobile/scrcpy/wrappers/DisplayManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 d44ac608..130f86c6 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java @@ -95,12 +95,12 @@ public final class DisplayManager { } private static int parseDisplayFlags(String text) { - Pattern regex = Pattern.compile("FLAG_[A-Z_]+"); if (text == null) { return 0; } int flags = 0; + Pattern regex = Pattern.compile("FLAG_[A-Z_]+"); Matcher m = regex.matcher(text); while (m.find()) { String flagString = m.group(); From cc309a2b34da13bbc15fbb64a6bba33ff8c79ce1 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 2 May 2025 11:37:47 +0200 Subject: [PATCH 2216/2244] Build static linux binary on Ubuntu 22.04 Ubuntu 20.04 is no longer available on GitHub Actions. Refs Refs #6050 This reverts commit 69858c6f437b1bfece96bc291c607de842837d36. --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5875c6bf..49402a6e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -84,7 +84,7 @@ jobs: run: release/test_client.sh build-linux-x86_64: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - name: Check architecture run: | From 8cd63cb63eeb4873b304a44fb66d55db03f2dd36 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 9 May 2025 18:22:40 +0200 Subject: [PATCH 2217/2244] Report specific error for INJECT_EVENT permission Some devices require a specific option to be enabled in Developer Options to avoid a permission issue when injecting input events. When this error occurs, hide the stack trace and print a human-readable message explaining how to fix the issue. PR #6080 --- README.md | 2 +- .../scrcpy/wrappers/InputManager.java | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a3b0d834..36f978f9 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ Make sure you [enabled USB debugging][enable-adb] on your device(s). On some devices (especially Xiaomi), you might get the following error: ``` -java.lang.SecurityException: Injecting input events requires the caller (or the source of the instrumentation, if any) to have the INJECT_EVENTS permission. +Injecting input events requires the caller (or the source of the instrumentation, if any) to have the INJECT_EVENTS permission. ``` In that case, you need to enable [an additional option][control] `USB debugging 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 5c5ba56c..f9f8e3ac 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java @@ -6,6 +6,7 @@ import android.annotation.SuppressLint; import android.view.InputEvent; import android.view.MotionEvent; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @SuppressLint("PrivateApi,DiscouragedPrivateApi") @@ -17,6 +18,7 @@ public final class InputManager { private final Object manager; private Method injectInputEventMethod; + private long lastPermissionLogDate; private static Method setDisplayIdMethod; private static Method setActionButtonMethod; @@ -57,6 +59,23 @@ public final class InputManager { Method method = getInjectInputEventMethod(); return (boolean) method.invoke(manager, inputEvent, mode); } catch (ReflectiveOperationException e) { + if (e instanceof InvocationTargetException) { + Throwable cause = e.getCause(); + if (cause instanceof SecurityException) { + String message = e.getCause().getMessage(); + if (message != null && message.contains("INJECT_EVENTS permission")) { + // Do not flood the console, limit to one permission error log every 3 seconds + long now = System.currentTimeMillis(); + if (lastPermissionLogDate <= now - 3000) { + Ln.e(message); + Ln.e("Make sure you have enabled \"USB debugging (Security Settings)\" and then rebooted your device."); + lastPermissionLogDate = now; + } + // Do not print the stack trace + return false; + } + } + } Ln.e("Could not invoke method", e); return false; } From 38f779d9d37833e617a577b5a9f3bc91d0358174 Mon Sep 17 00:00:00 2001 From: hltdev8642 <39349712+hltdev8642@users.noreply.github.com> Date: Fri, 9 May 2025 09:42:01 -0400 Subject: [PATCH 2218/2244] Escape parentheses in zsh completion script PR #6079 Co-authored-by: Romain Vimont Signed-off-by: Romain Vimont --- app/data/zsh-completion/_scrcpy | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 450fc8f5..8c2498f1 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -11,7 +11,7 @@ arguments=( '--always-on-top[Make scrcpy window always on top \(above other windows\)]' '--angle=[Rotate the video content by a custom angle, in degrees]' '--audio-bit-rate=[Encode the audio at the given bit-rate]' - '--audio-buffer=[Configure the audio buffering delay (in milliseconds)]' + '--audio-buffer=[Configure the audio buffering delay \(in milliseconds\)]' '--audio-codec=[Select the audio codec]:codec:(opus aac flac raw)' '--audio-codec-options=[Set a list of comma-separated key\:type=value options for the device audio encoder]' '--audio-dup=[Duplicate audio]' @@ -35,10 +35,10 @@ arguments=( {-e,--select-tcpip}'[Use TCP/IP device]' {-f,--fullscreen}'[Start in fullscreen]' '--force-adb-forward[Do not attempt to use \"adb reverse\" to connect to the device]' - '-G[Use UHID/AOA gamepad (same as --gamepad=uhid or --gamepad=aoa, depending on OTG mode)]' + '-G[Use UHID/AOA gamepad \(same as --gamepad=uhid or --gamepad=aoa, depending on OTG mode\)]' '--gamepad=[Set the gamepad input mode]:mode:(disabled uhid aoa)' {-h,--help}'[Print the help]' - '-K[Use UHID/AOA keyboard (same as --keyboard=uhid or --keyboard=aoa, depending on OTG mode)]' + '-K[Use UHID/AOA keyboard \(same as --keyboard=uhid or --keyboard=aoa, depending on OTG mode\)]' '--keyboard=[Set the keyboard input mode]:mode:(disabled sdk uhid aoa)' '--kill-adb-on-close[Kill adb when scrcpy terminates]' '--legacy-paste[Inject computer clipboard text as a sequence of key events on Ctrl+v]' @@ -48,7 +48,7 @@ arguments=( '--list-displays[List displays available on the device]' '--list-encoders[List video and audio encoders available on the device]' {-m,--max-size=}'[Limit both the width and height of the video to value]' - '-M[Use UHID/AOA mouse (same as --mouse=uhid or --mouse=aoa, depending on OTG mode)]' + '-M[Use UHID/AOA mouse \(same as --mouse=uhid or --mouse=aoa, depending on OTG mode\)]' '--max-fps=[Limit the frame rate of screen capture]' '--mouse=[Set the mouse input mode]:mode:(disabled sdk uhid aoa)' '--mouse-bind=[Configure bindings of secondary clicks]' From 70bfa2cf394955accb7446a99f9b6c6f5dfbaa2c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 15 May 2025 19:51:36 +0200 Subject: [PATCH 2219/2244] Remove useless flag in zsh completion script The -N flag is only useful after a pattern section (-p) to switch back to listing command names. Refs --- app/data/zsh-completion/_scrcpy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 8c2498f1..04ffb8f1 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -1,4 +1,4 @@ -#compdef -N scrcpy -N scrcpy.exe +#compdef scrcpy scrcpy.exe # # name: scrcpy # auth: hltdev [hltdev8642@gmail.com] From 52f5d08d1fab9600e78b21c71fa4a6c106d3783f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 3 Jun 2025 21:13:29 +0200 Subject: [PATCH 2220/2244] Avoid calling wait(0) Calling wait(0) results in waiting without a timeout, which is unintended. Refs #6009 comment --- .../main/java/com/genymobile/scrcpy/control/Controller.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index 5e64a4c5..24d827fd 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -699,7 +699,9 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { if (timeout < 0) { return null; } - displayDataAvailable.wait(timeout); + if (timeout > 0) { + displayDataAvailable.wait(timeout); + } data = displayData.get(); } From d2cc930975a8c7a2a073a9bffd9c2576d58cff7b Mon Sep 17 00:00:00 2001 From: Colin Kinloch Date: Thu, 22 May 2025 18:50:41 +0100 Subject: [PATCH 2221/2244] Add app name SDL hint This allows pulseaudio to label the audio stream "scrcpy" rather than "SDL Application". PR #6107 Signed-off-by: Romain Vimont --- app/src/compat.h | 8 ++++++++ app/src/scrcpy.c | 11 +++++++++++ 2 files changed, 19 insertions(+) diff --git a/app/src/compat.h b/app/src/compat.h index 1995d384..296d1a9f 100644 --- a/app/src/compat.h +++ b/app/src/compat.h @@ -75,6 +75,14 @@ # define SCRCPY_SDL_HAS_THREAD_PRIORITY_TIME_CRITICAL #endif +#if SDL_VERSION_ATLEAST(2, 0, 18) +# define SCRCPY_SDL_HAS_HINT_APP_NAME +#endif + +#if SDL_VERSION_ATLEAST(2, 0, 14) +# define SCRCPY_SDL_HAS_HINT_AUDIO_DEVICE_APP_NAME +#endif + #ifndef HAVE_STRDUP char *strdup(const char *s); #endif diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 4d08e667..a4c8c340 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -107,6 +107,17 @@ sdl_set_hints(const char *render_driver) { LOGW("Could not set render driver"); } + // App name used in various contexts (such as PulseAudio) +#if defined(SCRCPY_SDL_HAS_HINT_APP_NAME) + if (!SDL_SetHint(SDL_HINT_APP_NAME, "scrcpy")) { + LOGW("Could not set app name"); + } +#elif defined(SCRCPY_SDL_HAS_HINT_AUDIO_DEVICE_APP_NAME) + if (!SDL_SetHint(SDL_HINT_AUDIO_DEVICE_APP_NAME, "scrcpy")) { + LOGW("Could not set audio device app name"); + } +#endif + // Linear filtering if (!SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1")) { LOGW("Could not enable linear filtering"); From 41ed40f5f9ee557c5ccbca8b30a51c74af92da92 Mon Sep 17 00:00:00 2001 From: Simon Chan <1330321+yume-chan@users.noreply.github.com> Date: Fri, 25 Apr 2025 12:31:22 +0800 Subject: [PATCH 2222/2244] Simplify InputManager wrapper Use the public InputManager API. PR #6009 Signed-off-by: Romain Vimont --- .../scrcpy/wrappers/InputManager.java | 31 ++++++------------- 1 file changed, 9 insertions(+), 22 deletions(-) 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 f9f8e3ac..24c5f80c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java @@ -1,5 +1,6 @@ package com.genymobile.scrcpy.wrappers; +import com.genymobile.scrcpy.FakeContext; import com.genymobile.scrcpy.util.Ln; import android.annotation.SuppressLint; @@ -16,40 +17,26 @@ public final class InputManager { public static final int INJECT_INPUT_EVENT_MODE_WAIT_FOR_RESULT = 1; public static final int INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH = 2; - private final Object manager; - private Method injectInputEventMethod; + private final android.hardware.input.InputManager manager; private long lastPermissionLogDate; + private static Method injectInputEventMethod; private static Method setDisplayIdMethod; private static Method setActionButtonMethod; static InputManager create() { - try { - Class inputManagerClass = getInputManagerClass(); - Method getInstanceMethod = inputManagerClass.getDeclaredMethod("getInstance"); - Object im = getInstanceMethod.invoke(null); - return new InputManager(im); - } catch (ReflectiveOperationException e) { - throw new AssertionError(e); - } + android.hardware.input.InputManager manager = (android.hardware.input.InputManager) FakeContext.get() + .getSystemService(FakeContext.INPUT_SERVICE); + return new InputManager(manager); } - private static Class getInputManagerClass() { - try { - // Parts of the InputManager class have been moved to a new InputManagerGlobal class in Android 14 preview - return Class.forName("android.hardware.input.InputManagerGlobal"); - } catch (ClassNotFoundException e) { - return android.hardware.input.InputManager.class; - } - } - - private InputManager(Object manager) { + private InputManager(android.hardware.input.InputManager manager) { this.manager = manager; } - private Method getInjectInputEventMethod() throws NoSuchMethodException { + private static Method getInjectInputEventMethod() throws NoSuchMethodException { if (injectInputEventMethod == null) { - injectInputEventMethod = manager.getClass().getMethod("injectInputEvent", InputEvent.class, int.class); + injectInputEventMethod = android.hardware.input.InputManager.class.getMethod("injectInputEvent", InputEvent.class, int.class); } return injectInputEventMethod; } From ee414231ed136a59113787dc83a739623518c728 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 3 May 2025 13:43:10 +0200 Subject: [PATCH 2223/2244] Cache getDisplayInfo method Do not use reflection to retrieve the method for every call. PR #6009 --- .../genymobile/scrcpy/wrappers/DisplayManager.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) 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 130f86c6..3f8ed2bd 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java @@ -46,6 +46,7 @@ public final class DisplayManager { } private final Object manager; // instance of hidden class android.hardware.display.DisplayManagerGlobal + private Method getDisplayInfoMethod; private Method createVirtualDisplayMethod; private Method requestDisplayPowerMethod; @@ -114,9 +115,17 @@ public final class DisplayManager { return flags; } + private Method getGetDisplayInfoMethod() throws NoSuchMethodException { + if (getDisplayInfoMethod == null) { + getDisplayInfoMethod = manager.getClass().getMethod("getDisplayInfo", int.class); + } + return getDisplayInfoMethod; + } + public DisplayInfo getDisplayInfo(int displayId) { try { - Object displayInfo = manager.getClass().getMethod("getDisplayInfo", int.class).invoke(manager, displayId); + Method method = getGetDisplayInfoMethod(); + Object displayInfo = method.invoke(manager, displayId); if (displayInfo == null) { // fallback when displayInfo is null return getDisplayInfoFromDumpsysDisplay(displayId); From 7a3fe830d4d85d02ec21a23b88d8b48ce798a13e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 2 May 2025 23:03:15 +0200 Subject: [PATCH 2224/2244] Synchronize access to DisplayManager The DisplayManager and its method getDisplayInfo() may be used from both the Controller thread and the video (main) thread. PR #6009 --- .../java/com/genymobile/scrcpy/wrappers/DisplayManager.java | 3 ++- .../java/com/genymobile/scrcpy/wrappers/ServiceManager.java | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) 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 3f8ed2bd..2f86bbd2 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java @@ -115,7 +115,8 @@ public final class DisplayManager { return flags; } - private Method getGetDisplayInfoMethod() throws NoSuchMethodException { + // getDisplayInfo() may be used from both the Controller thread and the video (main) thread + private synchronized Method getGetDisplayInfoMethod() throws NoSuchMethodException { if (getDisplayInfoMethod == null) { getDisplayInfoMethod = manager.getClass().getMethod("getDisplayInfo", int.class); } 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 a8a56dab..b1123b55 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java @@ -54,7 +54,8 @@ public final class ServiceManager { return windowManager; } - public static DisplayManager getDisplayManager() { + // The DisplayManager may be used from both the Controller thread and the video (main) thread + public static synchronized DisplayManager getDisplayManager() { if (displayManager == null) { displayManager = DisplayManager.create(); } From ca4f50c5ef12eee2c0efb562e157a8b2d75cb001 Mon Sep 17 00:00:00 2001 From: Simon Chan <1330321+yume-chan@users.noreply.github.com> Date: Wed, 16 Apr 2025 14:26:24 +0800 Subject: [PATCH 2225/2244] Associate UHID devices to virtual displays This allows the mouse pointer to appear on the correct display (only for devices running Android 15+). Fixes #5547 PR #6009 Signed-off-by: Romain Vimont --- .../genymobile/scrcpy/control/Controller.java | 29 ++++++++- .../scrcpy/control/UhidManager.java | 65 ++++++++++++++++--- .../genymobile/scrcpy/device/DisplayInfo.java | 9 ++- .../scrcpy/wrappers/DisplayManager.java | 5 +- .../scrcpy/wrappers/InputManager.java | 40 ++++++++++++ 5 files changed, 134 insertions(+), 14 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index 24d827fd..a905b6c9 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -6,6 +6,7 @@ import com.genymobile.scrcpy.CleanUp; import com.genymobile.scrcpy.Options; import com.genymobile.scrcpy.device.Device; import com.genymobile.scrcpy.device.DeviceApp; +import com.genymobile.scrcpy.device.DisplayInfo; import com.genymobile.scrcpy.device.Point; import com.genymobile.scrcpy.device.Position; import com.genymobile.scrcpy.device.Size; @@ -156,8 +157,34 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { private UhidManager getUhidManager() { if (uhidManager == null) { - uhidManager = new UhidManager(sender); + int uhidDisplayId = displayId; + if (Build.VERSION.SDK_INT >= AndroidVersions.API_35_ANDROID_15) { + if (displayId == Device.DISPLAY_ID_NONE) { + // Mirroring a new virtual display id (using --new-display-id feature) on Android >= 15, where the UHID mouse pointer can be + // associated to the virtual display + try { + // Wait for at most 1 second until a virtual display id is known + DisplayData data = waitDisplayData(1000); + if (data != null) { + uhidDisplayId = data.virtualDisplayId; + } + } catch (InterruptedException e) { + // do nothing + } + } + } + + String displayUniqueId = null; + if (uhidDisplayId > 0) { + // Ignore Device.DISPLAY_ID_NONE and 0 (main display) + DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(uhidDisplayId); + if (displayInfo != null) { + displayUniqueId = displayInfo.getUniqueId(); + } + } + uhidManager = new UhidManager(sender, displayUniqueId); } + return uhidManager; } diff --git a/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java b/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java index c4867a3f..20532c0b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java @@ -3,6 +3,7 @@ package com.genymobile.scrcpy.control; import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.StringUtils; +import com.genymobile.scrcpy.wrappers.ServiceManager; import android.os.Build; import android.os.HandlerThread; @@ -31,14 +32,20 @@ public final class UhidManager { private static final int SIZE_OF_UHID_EVENT = 4380; // sizeof(struct uhid_event) + // Must be unique across the system + private static final String INPUT_PORT = "scrcpy:" + Os.getpid(); + + private final String displayUniqueId; + private final ArrayMap fds = new ArrayMap<>(); private final ByteBuffer buffer = ByteBuffer.allocate(SIZE_OF_UHID_EVENT).order(ByteOrder.nativeOrder()); private final DeviceMessageSender sender; private final MessageQueue queue; - public UhidManager(DeviceMessageSender sender) { + public UhidManager(DeviceMessageSender sender, String displayUniqueId) { this.sender = sender; + this.displayUniqueId = displayUniqueId; if (Build.VERSION.SDK_INT >= AndroidVersions.API_23_ANDROID_6_0) { HandlerThread thread = new HandlerThread("UHidManager"); thread.start(); @@ -52,15 +59,22 @@ public final class UhidManager { try { FileDescriptor fd = Os.open("/dev/uhid", OsConstants.O_RDWR, 0); try { + // First UHID device added + boolean firstDevice = fds.isEmpty(); + FileDescriptor old = fds.put(id, fd); if (old != null) { Ln.w("Duplicate UHID id: " + id); close(old); } - byte[] req = buildUhidCreate2Req(vendorId, productId, name, reportDesc); + String phys = mustUseInputPort() ? INPUT_PORT : null; + byte[] req = buildUhidCreate2Req(vendorId, productId, name, reportDesc, phys); Os.write(fd, req, 0, req.length); + if (firstDevice) { + addUniqueIdAssociation(); + } registerUhidListener(id, fd); } catch (Exception e) { close(fd); @@ -148,7 +162,7 @@ public final class UhidManager { } } - private static byte[] buildUhidCreate2Req(int vendorId, int productId, String name, byte[] reportDesc) { + private static byte[] buildUhidCreate2Req(int vendorId, int productId, String name, byte[] reportDesc, String phys) { /* * struct uhid_event { * uint32_t type; @@ -170,17 +184,23 @@ public final class UhidManager { * } __attribute__((__packed__)); */ - byte[] empty = new byte[256]; ByteBuffer buf = ByteBuffer.allocate(280 + reportDesc.length).order(ByteOrder.nativeOrder()); buf.putInt(UHID_CREATE2); String actualName = name.isEmpty() ? "scrcpy" : name; - byte[] utf8Name = actualName.getBytes(StandardCharsets.UTF_8); - int len = StringUtils.getUtf8TruncationIndex(utf8Name, 127); - assert len <= 127; - buf.put(utf8Name, 0, len); - buf.put(empty, 0, 256 - len); + byte[] nameBytes = actualName.getBytes(StandardCharsets.UTF_8); + int nameLen = StringUtils.getUtf8TruncationIndex(nameBytes, 127); + assert nameLen <= 127; + buf.put(nameBytes, 0, nameLen); + if (phys != null) { + buf.position(4 + 128); + byte[] physBytes = phys.getBytes(StandardCharsets.US_ASCII); + assert physBytes.length <= 63; + buf.put(physBytes); + } + + buf.position(4 + 256); buf.putShort((short) reportDesc.length); buf.putShort(BUS_VIRTUAL); buf.putInt(vendorId); @@ -219,15 +239,26 @@ public final class UhidManager { if (fd != null) { unregisterUhidListener(fd); close(fd); + + if (fds.isEmpty()) { + // Last UHID device removed + removeUniqueIdAssociation(); + } } else { Ln.w("Closing unknown UHID device: " + id); } } public void closeAll() { + if (fds.isEmpty()) { + return; + } + for (FileDescriptor fd : fds.values()) { close(fd); } + + removeUniqueIdAssociation(); } private static void close(FileDescriptor fd) { @@ -237,4 +268,20 @@ public final class UhidManager { Ln.e("Failed to close uhid: " + e.getMessage()); } } + + private boolean mustUseInputPort() { + return Build.VERSION.SDK_INT >= AndroidVersions.API_35_ANDROID_15 && displayUniqueId != null; + } + + private void addUniqueIdAssociation() { + if (mustUseInputPort()) { + ServiceManager.getInputManager().addUniqueIdAssociationByPort(INPUT_PORT, displayUniqueId); + } + } + + private void removeUniqueIdAssociation() { + if (mustUseInputPort()) { + ServiceManager.getInputManager().removeUniqueIdAssociationByPort(INPUT_PORT); + } + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/device/DisplayInfo.java b/server/src/main/java/com/genymobile/scrcpy/device/DisplayInfo.java index cdd4bab9..8d26b7ce 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/DisplayInfo.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/DisplayInfo.java @@ -7,16 +7,18 @@ public final class DisplayInfo { private final int layerStack; private final int flags; private final int dpi; + private final String uniqueId; public static final int FLAG_SUPPORTS_PROTECTED_BUFFERS = 0x00000001; - public DisplayInfo(int displayId, Size size, int rotation, int layerStack, int flags, int dpi) { + public DisplayInfo(int displayId, Size size, int rotation, int layerStack, int flags, int dpi, String uniqueId) { this.displayId = displayId; this.size = size; this.rotation = rotation; this.layerStack = layerStack; this.flags = flags; this.dpi = dpi; + this.uniqueId = uniqueId; } public int getDisplayId() { @@ -42,5 +44,8 @@ public final class DisplayInfo { public int getDpi() { return dpi; } -} + public String getUniqueId() { + return uniqueId; + } +} 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 2f86bbd2..a12470a4 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java @@ -82,7 +82,7 @@ public final class DisplayManager { int density = Integer.parseInt(m.group(5)); int layerStack = Integer.parseInt(m.group(6)); - return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags, density); + return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags, density, null); } private static DisplayInfo getDisplayInfoFromDumpsysDisplay(int displayId) { @@ -139,7 +139,8 @@ public final class DisplayManager { int layerStack = cls.getDeclaredField("layerStack").getInt(displayInfo); int flags = cls.getDeclaredField("flags").getInt(displayInfo); int dpi = cls.getDeclaredField("logicalDensityDpi").getInt(displayInfo); - return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags, dpi); + String uniqueId = (String) cls.getDeclaredField("uniqueId").get(displayInfo); + return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags, dpi, uniqueId); } catch (ReflectiveOperationException 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 24c5f80c..f55648d5 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java @@ -1,9 +1,11 @@ package com.genymobile.scrcpy.wrappers; +import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.FakeContext; import com.genymobile.scrcpy.util.Ln; import android.annotation.SuppressLint; +import android.annotation.TargetApi; import android.view.InputEvent; import android.view.MotionEvent; @@ -23,6 +25,8 @@ public final class InputManager { private static Method injectInputEventMethod; private static Method setDisplayIdMethod; private static Method setActionButtonMethod; + private static Method addUniqueIdAssociationByPortMethod; + private static Method removeUniqueIdAssociationByPortMethod; static InputManager create() { android.hardware.input.InputManager manager = (android.hardware.input.InputManager) FakeContext.get() @@ -103,4 +107,40 @@ public final class InputManager { return false; } } + + private static Method getAddUniqueIdAssociationByPortMethod() throws NoSuchMethodException { + if (addUniqueIdAssociationByPortMethod == null) { + addUniqueIdAssociationByPortMethod = android.hardware.input.InputManager.class.getMethod( + "addUniqueIdAssociationByPort", String.class, String.class); + } + return addUniqueIdAssociationByPortMethod; + } + + @TargetApi(AndroidVersions.API_35_ANDROID_15) + public void addUniqueIdAssociationByPort(String inputPort, String uniqueId) { + try { + Method method = getAddUniqueIdAssociationByPortMethod(); + method.invoke(manager, inputPort, uniqueId); + } catch (ReflectiveOperationException e) { + Ln.e("Cannot add unique id association by port", e); + } + } + + private static Method getRemoveUniqueIdAssociationByPortMethod() throws NoSuchMethodException { + if (removeUniqueIdAssociationByPortMethod == null) { + removeUniqueIdAssociationByPortMethod = android.hardware.input.InputManager.class.getMethod( + "removeUniqueIdAssociationByPort", String.class); + } + return removeUniqueIdAssociationByPortMethod; + } + + @TargetApi(AndroidVersions.API_35_ANDROID_15) + public void removeUniqueIdAssociationByPort(String inputPort) { + try { + Method method = getRemoveUniqueIdAssociationByPortMethod(); + method.invoke(manager, inputPort); + } catch (ReflectiveOperationException e) { + Ln.e("Cannot remove unique id association by port", e); + } + } } From 283326b2f6fa3fdaeecc181f69a3a4bcd429c06a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 5 Jun 2025 20:33:15 +0200 Subject: [PATCH 2226/2244] Run a main looper Instead of blocking the main thread until completion, run a looper. This will allow the main thread to process any event posted to the main looper. Refs #6009 comment PR #6129 --- .../java/com/genymobile/scrcpy/Server.java | 31 ++++++++++++------- .../com/genymobile/scrcpy/Workarounds.java | 15 --------- 2 files changed, 19 insertions(+), 27 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 09cfd6cf..c1d8c1f2 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -25,9 +25,11 @@ import com.genymobile.scrcpy.video.SurfaceEncoder; import com.genymobile.scrcpy.video.VideoSource; import android.os.Build; +import android.os.Looper; import java.io.File; import java.io.IOException; +import java.lang.reflect.Field; import java.util.ArrayList; import java.util.List; @@ -55,17 +57,7 @@ public final class Server { this.fatalError = true; } if (running == 0 || this.fatalError) { - notify(); - } - } - - synchronized void await() { - try { - while (running > 0 && !fatalError) { - wait(); - } - } catch (InterruptedException e) { - // ignore + Looper.getMainLooper().quitSafely(); } } } @@ -104,6 +96,7 @@ public final class Server { boolean audio = options.getAudio(); boolean sendDummyByte = options.getSendDummyByte(); + prepareMainLooper(); Workarounds.apply(); List asyncProcessors = new ArrayList<>(); @@ -172,7 +165,7 @@ public final class Server { }); } - completion.await(); + Looper.loop(); // interrupted by the Completion implementation } finally { if (cleanUp != null) { cleanUp.interrupt(); @@ -201,6 +194,20 @@ public final class Server { } } + private static void prepareMainLooper() { + // Like Looper.prepareMainLooper(), but with quitAllowed set to true + Looper.prepare(); + synchronized (Looper.class) { + try { + Field field = Looper.class.getDeclaredField("sMainLooper"); + field.setAccessible(true); + field.set(null, Looper.myLooper()); + } catch (ReflectiveOperationException e) { + throw new AssertionError(e); + } + } + } + public static void main(String... args) { int status = 0; try { diff --git a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java index fb4c1389..b89f19ae 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java +++ b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java @@ -29,8 +29,6 @@ public final class Workarounds { private static final Object ACTIVITY_THREAD; static { - prepareMainLooper(); - try { // ActivityThread activityThread = new ActivityThread(); ACTIVITY_THREAD_CLASS = Class.forName("android.app.ActivityThread"); @@ -77,19 +75,6 @@ public final class Workarounds { fillAppContext(); } - @SuppressWarnings("deprecation") - private static void prepareMainLooper() { - // Some devices internally create a Handler when creating an input Surface, causing an exception: - // "Can't create handler inside thread that has not called Looper.prepare()" - // - // - // Use Looper.prepareMainLooper() instead of Looper.prepare() to avoid a NullPointerException: - // "Attempt to read from field 'android.os.MessageQueue android.os.Looper.mQueue' - // on a null object reference" - // - Looper.prepareMainLooper(); - } - private static void fillAppInfo() { try { // ActivityThread.AppBindData appBindData = new ActivityThread.AppBindData(); From 8a02e3c2f58cffc3fdd8c08b26aae04bbf9d5a97 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 17 Apr 2025 18:09:55 +0200 Subject: [PATCH 2227/2244] Simplify ClipboardManager wrapper Use the public ClipboardManager API, with the FakeContext as context. This requires a running main looper, otherwise clipboard changes are not processed. Refs #6009 PR #6129 Suggested by: Simon Chan <1330321+yume-chan@users.noreply.github.com> --- .../com/genymobile/scrcpy/FakeContext.java | 25 ++ .../genymobile/scrcpy/control/Controller.java | 22 +- .../scrcpy/wrappers/ClipboardManager.java | 255 +----------------- 3 files changed, 48 insertions(+), 254 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/FakeContext.java b/server/src/main/java/com/genymobile/scrcpy/FakeContext.java index 2b83e397..b43e9e1b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/FakeContext.java +++ b/server/src/main/java/com/genymobile/scrcpy/FakeContext.java @@ -2,8 +2,10 @@ package com.genymobile.scrcpy; import com.genymobile.scrcpy.wrappers.ServiceManager; +import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.content.AttributionSource; +import android.content.ClipboardManager; import android.content.ContentResolver; import android.content.Context; import android.content.ContextWrapper; @@ -11,6 +13,8 @@ import android.content.IContentProvider; import android.os.Binder; import android.os.Process; +import java.lang.reflect.Field; + public final class FakeContext extends ContextWrapper { public static final String PACKAGE_NAME = "com.android.shell"; @@ -91,4 +95,25 @@ public final class FakeContext extends ContextWrapper { public ContentResolver getContentResolver() { return contentResolver; } + + @SuppressLint("SoonBlockedPrivateApi") + @Override + public Object getSystemService(String name) { + Object service = super.getSystemService(name); + if (service == null) { + return null; + } + + if (Context.CLIPBOARD_SERVICE.equals(name)) { + try { + Field field = ClipboardManager.class.getDeclaredField("mContext"); + field.setAccessible(true); + field.set(service, this); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + + return service; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index a905b6c9..bfbee7dc 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -18,7 +18,6 @@ import com.genymobile.scrcpy.wrappers.ClipboardManager; import com.genymobile.scrcpy.wrappers.InputManager; import com.genymobile.scrcpy.wrappers.ServiceManager; -import android.content.IOnPrimaryClipChangedListener; import android.content.Intent; import android.os.Build; import android.os.SystemClock; @@ -119,18 +118,15 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { // If control and autosync are enabled, synchronize Android clipboard to the computer automatically 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; - } - String text = Device.getClipboardText(); - if (text != null) { - DeviceMessage msg = DeviceMessage.createClipboard(text); - sender.send(msg); - } + clipboardManager.addPrimaryClipChangedListener(() -> { + if (isSettingClipboard.get()) { + // This is a notification for the change we are currently applying, ignore it + return; + } + String text = Device.getClipboardText(); + if (text != null) { + DeviceMessage msg = DeviceMessage.createClipboard(text); + sender.send(msg); } }); } else { 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 791df0f8..fae8a056 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java @@ -1,270 +1,43 @@ package com.genymobile.scrcpy.wrappers; -import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.FakeContext; -import com.genymobile.scrcpy.util.Ln; import android.content.ClipData; -import android.content.IOnPrimaryClipChangedListener; -import android.os.Build; -import android.os.IInterface; - -import java.lang.reflect.Method; public final class ClipboardManager { - private final IInterface manager; - private Method getPrimaryClipMethod; - private Method setPrimaryClipMethod; - private Method addPrimaryClipChangedListener; - private int getMethodVersion; - private int setMethodVersion; - private int addListenerMethodVersion; + private final android.content.ClipboardManager manager; static ClipboardManager create() { - IInterface clipboard = ServiceManager.getService("clipboard", "android.content.IClipboard"); - if (clipboard == null) { + android.content.ClipboardManager manager = (android.content.ClipboardManager) FakeContext.get() + .getSystemService(FakeContext.CLIPBOARD_SERVICE); + if (manager == null) { // Some devices have no clipboard manager // // return null; } - return new ClipboardManager(clipboard); + return new ClipboardManager(manager); } - private ClipboardManager(IInterface manager) { + private ClipboardManager(android.content.ClipboardManager manager) { this.manager = manager; } - private Method getGetPrimaryClipMethod() throws NoSuchMethodException { - if (getPrimaryClipMethod == null) { - if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) { - getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class); - return getPrimaryClipMethod; - } - - try { - getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class); - getMethodVersion = 0; - return getPrimaryClipMethod; - } catch (NoSuchMethodException e) { - // fall-through - } - - try { - getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class); - getMethodVersion = 1; - return getPrimaryClipMethod; - } catch (NoSuchMethodException e) { - // fall-through - } - - try { - getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class, int.class); - getMethodVersion = 2; - return getPrimaryClipMethod; - } catch (NoSuchMethodException e) { - // fall-through - } - - try { - getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class, String.class); - getMethodVersion = 3; - return getPrimaryClipMethod; - } catch (NoSuchMethodException e) { - // fall-through - } - - try { - getPrimaryClipMethod = manager.getClass() - .getMethod("getPrimaryClip", String.class, String.class, int.class, int.class, boolean.class); - getMethodVersion = 4; - return getPrimaryClipMethod; - } catch (NoSuchMethodException e) { - // fall-through - } - - try { - getPrimaryClipMethod = manager.getClass() - .getMethod("getPrimaryClip", String.class, String.class, String.class, String.class, int.class, int.class, boolean.class); - getMethodVersion = 5; - return getPrimaryClipMethod; - } catch (NoSuchMethodException e) { - // fall-through - } - - getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class, int.class, String.class); - getMethodVersion = 6; - } - return getPrimaryClipMethod; - } - - private Method getSetPrimaryClipMethod() throws NoSuchMethodException { - if (setPrimaryClipMethod == null) { - if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) { - setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class); - return setPrimaryClipMethod; - } - - try { - setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, int.class); - setMethodVersion = 0; - return setPrimaryClipMethod; - } catch (NoSuchMethodException e1) { - // fall-through - } - - try { - setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class); - setMethodVersion = 1; - return setPrimaryClipMethod; - } catch (NoSuchMethodException e2) { - // fall-through - } - - try { - setPrimaryClipMethod = manager.getClass() - .getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class, int.class); - setMethodVersion = 2; - return setPrimaryClipMethod; - } catch (NoSuchMethodException e3) { - // fall-through - } - - setPrimaryClipMethod = manager.getClass() - .getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class, int.class, boolean.class); - setMethodVersion = 3; - } - return setPrimaryClipMethod; - } - - private static ClipData getPrimaryClip(Method method, int methodVersion, IInterface manager) throws ReflectiveOperationException { - if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) { - return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME); - } - - switch (methodVersion) { - case 0: - return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID); - case 1: - return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID); - case 2: - return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0); - case 3: - return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID, null); - case 4: - // The last boolean parameter is "userOperate" - return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0, true); - case 5: - return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, null, null, FakeContext.ROOT_UID, 0, true); - default: - return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0, null); - } - } - - private static void setPrimaryClip(Method method, int methodVersion, IInterface manager, ClipData clipData) throws ReflectiveOperationException { - if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) { - method.invoke(manager, clipData, FakeContext.PACKAGE_NAME); - return; - } - - switch (methodVersion) { - case 0: - method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID); - break; - case 1: - method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID); - break; - case 2: - method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0); - break; - default: - // The last boolean parameter is "userOperate" - method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0, true); - } - } - public CharSequence getText() { - try { - Method method = getGetPrimaryClipMethod(); - ClipData clipData = getPrimaryClip(method, getMethodVersion, manager); - if (clipData == null || clipData.getItemCount() == 0) { - return null; - } - return clipData.getItemAt(0).getText(); - } catch (ReflectiveOperationException e) { - Ln.e("Could not invoke method", e); + ClipData clipData = manager.getPrimaryClip(); + if (clipData == null || clipData.getItemCount() == 0) { return null; } + return clipData.getItemAt(0).getText(); } public boolean setText(CharSequence text) { - try { - Method method = getSetPrimaryClipMethod(); - ClipData clipData = ClipData.newPlainText(null, text); - setPrimaryClip(method, setMethodVersion, manager, clipData); - return true; - } catch (ReflectiveOperationException e) { - Ln.e("Could not invoke method", e); - return false; - } + ClipData clipData = ClipData.newPlainText(null, text); + manager.setPrimaryClip(clipData); + return true; } - private static void addPrimaryClipChangedListener(Method method, int methodVersion, IInterface manager, IOnPrimaryClipChangedListener listener) - throws ReflectiveOperationException { - if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) { - method.invoke(manager, listener, FakeContext.PACKAGE_NAME); - return; - } - - switch (methodVersion) { - case 0: - method.invoke(manager, listener, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID); - break; - case 1: - method.invoke(manager, listener, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID); - break; - default: - method.invoke(manager, listener, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0); - break; - } - } - - private Method getAddPrimaryClipChangedListener() throws NoSuchMethodException { - if (addPrimaryClipChangedListener == null) { - if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) { - addPrimaryClipChangedListener = manager.getClass() - .getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class); - } else { - try { - addPrimaryClipChangedListener = manager.getClass() - .getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, int.class); - addListenerMethodVersion = 0; - } catch (NoSuchMethodException e1) { - try { - addPrimaryClipChangedListener = manager.getClass() - .getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, String.class, - int.class); - addListenerMethodVersion = 1; - } catch (NoSuchMethodException e2) { - addPrimaryClipChangedListener = manager.getClass() - .getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, String.class, - int.class, int.class); - addListenerMethodVersion = 2; - } - } - } - } - return addPrimaryClipChangedListener; - } - - public boolean addPrimaryClipChangedListener(IOnPrimaryClipChangedListener listener) { - try { - Method method = getAddPrimaryClipChangedListener(); - addPrimaryClipChangedListener(method, addListenerMethodVersion, manager, listener); - return true; - } catch (ReflectiveOperationException e) { - Ln.e("Could not invoke method", e); - return false; - } + public void addPrimaryClipChangedListener(android.content.ClipboardManager.OnPrimaryClipChangedListener listener) { + manager.addPrimaryClipChangedListener(listener); } } From ac16be54c8d4afed7c69b7132719b37282baea49 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 11 Jun 2025 19:36:22 +0200 Subject: [PATCH 2228/2244] Upgrade platform-tools (36.0.0) --- app/deps/adb_linux.sh | 4 ++-- app/deps/adb_macos.sh | 4 ++-- app/deps/adb_windows.sh | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/deps/adb_linux.sh b/app/deps/adb_linux.sh index 17b5641d..a3e339ec 100755 --- a/app/deps/adb_linux.sh +++ b/app/deps/adb_linux.sh @@ -4,10 +4,10 @@ DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) cd "$DEPS_DIR" . common -VERSION=35.0.2 +VERSION=36.0.0 FILENAME=platform-tools_r$VERSION-linux.zip PROJECT_DIR=platform-tools-$VERSION-linux -SHA256SUM=acfdcccb123a8718c46c46c059b2f621140194e5ec1ac9d81715be3d6ab6cd0a +SHA256SUM=0ead642c943ffe79701fccca8f5f1c69c4ce4f43df2eefee553f6ccb27cbfbe8 cd "$SOURCES_DIR" diff --git a/app/deps/adb_macos.sh b/app/deps/adb_macos.sh index 8a25915e..36f5df89 100755 --- a/app/deps/adb_macos.sh +++ b/app/deps/adb_macos.sh @@ -4,10 +4,10 @@ DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) cd "$DEPS_DIR" . common -VERSION=35.0.2 +VERSION=36.0.0 FILENAME=platform-tools_r$VERSION-darwin.zip PROJECT_DIR=platform-tools-$VERSION-darwin -SHA256SUM=1820078db90bf21628d257ff052528af1c61bb48f754b3555648f5652fa35d78 +SHA256SUM=b241878e6ec20650b041bf715ea05f7d5dc73bd24529464bd9cf68946e3132bd cd "$SOURCES_DIR" diff --git a/app/deps/adb_windows.sh b/app/deps/adb_windows.sh index d36706b0..de37162c 100755 --- a/app/deps/adb_windows.sh +++ b/app/deps/adb_windows.sh @@ -4,10 +4,10 @@ DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) cd "$DEPS_DIR" . common -VERSION=35.0.2 +VERSION=36.0.0 FILENAME=platform-tools_r$VERSION-win.zip PROJECT_DIR=platform-tools-$VERSION-windows -SHA256SUM=2975a3eac0b19182748d64195375ad056986561d994fffbdc64332a516300bb9 +SHA256SUM=24bd8bebbbb58b9870db202b5c6775c4a49992632021c60750d9d8ec8179d5f0 cd "$SOURCES_DIR" From 1a9ffb38146b7f70021387ccb0206c873fb07d99 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 11 Jun 2025 19:38:29 +0200 Subject: [PATCH 2229/2244] Upgrade SDL (2.32.8) --- ...that-the-correct-struct-is-used-for-.patch | 33 ------------------- app/deps/sdl.sh | 5 ++- 2 files changed, 2 insertions(+), 36 deletions(-) delete mode 100644 app/deps/patches/SDL-pipewire-Ensure-that-the-correct-struct-is-used-for-.patch diff --git a/app/deps/patches/SDL-pipewire-Ensure-that-the-correct-struct-is-used-for-.patch b/app/deps/patches/SDL-pipewire-Ensure-that-the-correct-struct-is-used-for-.patch deleted file mode 100644 index cbb516ec..00000000 --- a/app/deps/patches/SDL-pipewire-Ensure-that-the-correct-struct-is-used-for-.patch +++ /dev/null @@ -1,33 +0,0 @@ -From 6be87ceb33a9aad3bf5204bb13b3a5e8b498fd26 Mon Sep 17 00:00:00 2001 -From: Neal Gompa -Date: Mon, 10 Feb 2025 05:00:56 -0500 -Subject: [PATCH] pipewire: Ensure that the correct struct is used for - enumeration APIs - -PipeWire now requires the correct struct type is used, otherwise -it will fail to compile. - -Reference: https://gitlab.freedesktop.org/pipewire/pipewire/-/commit/188d920733f0791413d3386e5536ee7377f71b2f - -Fixes: https://github.com/libsdl-org/SDL/issues/12224 -(cherry picked from commit d35bef64e913dd7d5dd3153a4b61f10ef837dad6) ---- - src/audio/pipewire/SDL_pipewire.c | 2 +- - 1 file changed, 1 insertion(+), 1 deletion(-) - -diff --git a/src/audio/pipewire/SDL_pipewire.c b/src/audio/pipewire/SDL_pipewire.c -index 889e05decb..5d1bfc28de 100644 ---- a/src/audio/pipewire/SDL_pipewire.c -+++ b/src/audio/pipewire/SDL_pipewire.c -@@ -590,7 +590,7 @@ static void node_event_info(void *object, const struct pw_node_info *info) - - /* Need to parse the parameters to get the sample rate */ - for (i = 0; i < info->n_params; ++i) { -- pw_node_enum_params(node->proxy, 0, info->params[i].id, 0, 0, NULL); -+ pw_node_enum_params((struct pw_node*)node->proxy, 0, info->params[i].id, 0, 0, NULL); - } - - hotplug_core_sync(node); --- -2.49.0 - diff --git a/app/deps/sdl.sh b/app/deps/sdl.sh index c3edee58..54fee12b 100755 --- a/app/deps/sdl.sh +++ b/app/deps/sdl.sh @@ -5,10 +5,10 @@ cd "$DEPS_DIR" . common process_args "$@" -VERSION=2.32.2 +VERSION=2.32.8 FILENAME=SDL-$VERSION.tar.gz PROJECT_DIR=SDL-release-$VERSION -SHA256SUM=f2c7297ae7b3d3910a8b131e1e2a558fdd6d1a4443d5e345374d45cadfcb05a4 +SHA256SUM=dd35e05644ae527848d02433bec24dd0ea65db59faecf1a0e5d1880c533dac2c cd "$SOURCES_DIR" @@ -18,7 +18,6 @@ then else get_file "https://github.com/libsdl-org/SDL/archive/refs/tags/release-$VERSION.tar.gz" "$FILENAME" "$SHA256SUM" tar xf "$FILENAME" # First level directory is "$PROJECT_DIR" - patch -d "$PROJECT_DIR" -p1 < "$PATCHES_DIR"/SDL-pipewire-Ensure-that-the-correct-struct-is-used-for-.patch fi mkdir -p "$BUILD_DIR/$PROJECT_DIR" From 454beaa7571d3d86339a53a5a3202bd47e1d2353 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 11 Jun 2025 19:39:02 +0200 Subject: [PATCH 2230/2244] Upgrade libusb (1.0.29) --- app/deps/libusb.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/deps/libusb.sh b/app/deps/libusb.sh index 4be03eb1..887a2a77 100755 --- a/app/deps/libusb.sh +++ b/app/deps/libusb.sh @@ -5,10 +5,10 @@ cd "$DEPS_DIR" . common process_args "$@" -VERSION=1.0.28 +VERSION=1.0.29 FILENAME=libusb-$VERSION.tar.gz PROJECT_DIR=libusb-$VERSION -SHA256SUM=378b3709a405065f8f9fb9f35e82d666defde4d342c2a1b181a9ac134d23c6fe +SHA256SUM=7c2dd39c0b2589236e48c93247c986ae272e27570942b4163cb00a060fcf1b74 cd "$SOURCES_DIR" From dc169e425e9cc94e8e871dbeac24bdebd277bf39 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 11 Jun 2025 19:39:48 +0200 Subject: [PATCH 2231/2244] Bump version to 3.3 --- app/scrcpy-windows.rc | 2 +- meson.build | 2 +- server/build.gradle | 4 ++-- server/build_without_gradle.sh | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/scrcpy-windows.rc b/app/scrcpy-windows.rc index 19475e0b..45f1960c 100644 --- a/app/scrcpy-windows.rc +++ b/app/scrcpy-windows.rc @@ -13,7 +13,7 @@ BEGIN VALUE "LegalCopyright", "Romain Vimont, Genymobile" VALUE "OriginalFilename", "scrcpy.exe" VALUE "ProductName", "scrcpy" - VALUE "ProductVersion", "3.2" + VALUE "ProductVersion", "3.3" END END BLOCK "VarFileInfo" diff --git a/meson.build b/meson.build index b64a6c90..1e9a5729 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('scrcpy', 'c', - version: '3.2', + version: '3.3', meson_version: '>= 0.49', default_options: [ 'c_std=c11', diff --git a/server/build.gradle b/server/build.gradle index 02508001..059a6f30 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -7,8 +7,8 @@ android { applicationId "com.genymobile.scrcpy" minSdkVersion 21 targetSdkVersion 35 - versionCode 30200 - versionName "3.2" + versionCode 30300 + versionName "3.3" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index 8bb8632b..5b35e3ec 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=3.2 +SCRCPY_VERSION_NAME=3.3 PLATFORM=${ANDROID_PLATFORM:-35} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-35.0.0} From 696402c68c5f91fa77c3ed03cd835dc4412a253e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 11 Jun 2025 22:15:30 +0200 Subject: [PATCH 2232/2244] Update links to 3.3 --- README.md | 2 +- doc/build.md | 6 +++--- doc/linux.md | 6 +++--- doc/macos.md | 12 ++++++------ doc/windows.md | 12 ++++++------ install_release.sh | 4 ++-- 6 files changed, 21 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 81399f52..dc00ac22 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ source for the project. Do not download releases from random websites, even if their name contains `scrcpy`.** -# scrcpy (v3.2) +# scrcpy (v3.3) scrcpy diff --git a/doc/build.md b/doc/build.md index afe8b21b..c915e367 100644 --- a/doc/build.md +++ b/doc/build.md @@ -233,10 +233,10 @@ install` must be run as root)._ #### Option 2: Use prebuilt server - - [`scrcpy-server-v3.2`][direct-scrcpy-server] - SHA-256: `b920e0ea01936bf2482f4ba2fa985c22c13c621999e3d33b45baa5acfc1ea3d0` + - [`scrcpy-server-v3.3`][direct-scrcpy-server] + SHA-256: `351cb2edc7e4c2c75f09a7933fdabcf137be52e2602df154f24ec02db46e9e51` -[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-server-v3.2 +[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v3.3/scrcpy-server-v3.3 Download the prebuilt server somewhere, and specify its path during the Meson configuration: diff --git a/doc/linux.md b/doc/linux.md index 979ef568..5cfd6e4e 100644 --- a/doc/linux.md +++ b/doc/linux.md @@ -6,11 +6,11 @@ Download a static build of the [latest release]: - - [`scrcpy-linux-x86_64-v3.2.tar.gz`][direct-linux-x86_64] (x86_64) - SHA-256: `df6cf000447428fcde322022848d655ff0211d98688d0f17cbbf21be9c1272be` + - [`scrcpy-linux-x86_64-v3.3.tar.gz`][direct-linux-x86_64] (x86_64) + SHA-256: `a0abf37003c3c47a53c1b2a12420296a2b0ee323cf3610fd6fbf9d9bab9d99f3` [latest release]: https://github.com/Genymobile/scrcpy/releases/latest -[direct-linux-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-linux-x86_64-v3.2.tar.gz +[direct-linux-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3/scrcpy-linux-x86_64-v3.3.tar.gz and extract it. diff --git a/doc/macos.md b/doc/macos.md index b0335d18..73a982f6 100644 --- a/doc/macos.md +++ b/doc/macos.md @@ -6,15 +6,15 @@ Download a static build of the [latest release]: - - [`scrcpy-macos-aarch64-v3.2.tar.gz`][direct-macos-aarch64] (aarch64) - SHA-256: `f6d1f3c5f74d4d46f5080baa5b56b69f5edbf698d47e0cf4e2a1fd5058f9507b` + - [`scrcpy-macos-aarch64-v3.3.tar.gz`][direct-macos-aarch64] (aarch64) + SHA-256: `7a4cdaeb8ba74593edda278c000ddedc8d70a51263a80b16a6345475d42ac21e` - - [`scrcpy-macos-x86_64-v3.2.tar.gz`][direct-macos-x86_64] (x86_64) - SHA-256: `e337d5cf0ba4e1281699c338ce5f104aee96eb7b2893dc851399b6643eb4044e` + - [`scrcpy-macos-x86_64-v3.3.tar.gz`][direct-macos-x86_64] (x86_64) + SHA-256: `bb3c13aac166b92539371883a8781aa861a7cd18e3e6077e570ab7a1f562f774` [latest release]: https://github.com/Genymobile/scrcpy/releases/latest -[direct-macos-aarch64]: https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-macos-aarch64-v3.2.tar.gz -[direct-macos-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-macos-x86_64-v3.2.tar.gz +[direct-macos-aarch64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3/scrcpy-macos-aarch64-v3.3.tar.gz +[direct-macos-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3/scrcpy-macos-x86_64-v3.3.tar.gz and extract it. diff --git a/doc/windows.md b/doc/windows.md index fb3e3887..7935461d 100644 --- a/doc/windows.md +++ b/doc/windows.md @@ -6,14 +6,14 @@ Download the [latest release]: - - [`scrcpy-win64-v3.2.zip`][direct-win64] (64-bit) - SHA-256: `eaa27133e0520979873ba57ad651560a4cc2618373bd05450b23a84d32beafd0` - - [`scrcpy-win32-v3.2.zip`][direct-win32] (32-bit) - SHA-256: `4a3407d7f0c2c8a03e22a12cf0b5e1e585a5056fe23c8e5cf3252207c6fa8357` + - [`scrcpy-win64-v3.3.zip`][direct-win64] (64-bit) + SHA-256: `a120cb4be7cde2891af38e83d2008173a0b6b6b5e344b2dfe668d0f892999933` + - [`scrcpy-win32-v3.3.zip`][direct-win32] (32-bit) + SHA-256: `e409ab83f8c57bd6ac741d652635cab7699fcf3d384e233833872f117b993ca6` [latest release]: https://github.com/Genymobile/scrcpy/releases/latest -[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-win64-v3.2.zip -[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-win32-v3.2.zip +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3/scrcpy-win64-v3.3.zip +[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v3.3/scrcpy-win32-v3.3.zip and extract it. diff --git a/install_release.sh b/install_release.sh index 2d2d2c2f..aabe9873 100755 --- a/install_release.sh +++ b/install_release.sh @@ -2,8 +2,8 @@ set -e BUILDDIR=build-auto -PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-server-v3.2 -PREBUILT_SERVER_SHA256=b920e0ea01936bf2482f4ba2fa985c22c13c621999e3d33b45baa5acfc1ea3d0 +PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v3.3/scrcpy-server-v3.3 +PREBUILT_SERVER_SHA256=351cb2edc7e4c2c75f09a7933fdabcf137be52e2602df154f24ec02db46e9e51 echo "[scrcpy] Downloading prebuilt server..." wget "$PREBUILT_SERVER_URL" -O scrcpy-server From 4e1cf13a5092bfe8651c8f55eda3861b7d01b64a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 12 Jun 2025 09:03:39 +0200 Subject: [PATCH 2233/2244] Run a main looper in the cleanup process Since a main looper is explicitly run in the main process, the initialization of workarounds no longer calls Looper.prepareMainLooper(), leading to a crash: java.lang.RuntimeException: Can't create handler inside thread Thread[main,5,main] that has not called Looper.prepare() As a result, --power-off-on-close was broken. Refs 283326b2f6fa3fdaeecc181f69a3a4bcd429c06a Fixes #6146 --- server/src/main/java/com/genymobile/scrcpy/CleanUp.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java index 51db985c..77018afa 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java +++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java @@ -7,6 +7,7 @@ import com.genymobile.scrcpy.util.SettingsException; import com.genymobile.scrcpy.wrappers.ServiceManager; import android.os.BatteryManager; +import android.os.Looper; import android.system.ErrnoException; import android.system.Os; @@ -179,6 +180,11 @@ public final class CleanUp { } } + @SuppressWarnings("deprecation") + private static void prepareMainLooper() { + Looper.prepareMainLooper(); + } + public static void main(String... args) { try { // Start a new session to avoid being terminated along with the server process on some devices @@ -188,6 +194,9 @@ public final class CleanUp { } unlinkSelf(); + // Needed for workarounds + prepareMainLooper(); + int displayId = Integer.parseInt(args[0]); int restoreStayOn = Integer.parseInt(args[1]); boolean disableShowTouches = Boolean.parseBoolean(args[2]); From 38256d8ff9d019f8d4fd84719eeafd0214c836e8 Mon Sep 17 00:00:00 2001 From: berk ziya Date: Thu, 12 Jun 2025 16:27:40 +0300 Subject: [PATCH 2234/2244] Fix deprecated brew command `brew cask` is an outdated command, replaced by `brew install --cask`. Refs #5398 PR #6149 Signed-off-by: Romain Vimont --- app/src/adb/adb.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/adb/adb.c b/app/src/adb/adb.c index 40e9e968..9e9cfd6b 100644 --- a/app/src/adb/adb.c +++ b/app/src/adb/adb.c @@ -110,7 +110,7 @@ show_adb_installation_msg(void) { } pkg_managers[] = { {"apt", "apt install adb"}, {"apt-get", "apt-get install adb"}, - {"brew", "brew cask install android-platform-tools"}, + {"brew", "brew install --cask android-platform-tools"}, {"dnf", "dnf install android-tools"}, {"emerge", "emerge dev-util/android-tools"}, {"pacman", "pacman -S android-tools"}, From 772f42134a327eea60955463d0ee8bb712168dd0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 13 Jun 2025 09:36:34 +0200 Subject: [PATCH 2235/2244] Use Context.CLIPBOARD_SERVICE directly The constant is defined in Context, not FakeContext. --- .../java/com/genymobile/scrcpy/wrappers/ClipboardManager.java | 4 ++-- 1 file changed, 2 insertions(+), 2 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 fae8a056..54936122 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java @@ -3,13 +3,13 @@ package com.genymobile.scrcpy.wrappers; import com.genymobile.scrcpy.FakeContext; import android.content.ClipData; +import android.content.Context; public final class ClipboardManager { private final android.content.ClipboardManager manager; static ClipboardManager create() { - android.content.ClipboardManager manager = (android.content.ClipboardManager) FakeContext.get() - .getSystemService(FakeContext.CLIPBOARD_SERVICE); + android.content.ClipboardManager manager = (android.content.ClipboardManager) FakeContext.get().getSystemService(Context.CLIPBOARD_SERVICE); if (manager == null) { // Some devices have no clipboard manager // From cd3a5d50b650da6dcafbdbddd606ef5031f1833a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 13 Jun 2025 09:24:36 +0200 Subject: [PATCH 2236/2244] Create ClipboardManager from the main thread The ClipboardManager is instantiated by the first call to ServiceManager.getClipboardManager(). Now that scrcpy uses android.content.ClipboardManager directly, it must ensure that it is created on the main thread (or at least on a thread with a Looper), to avoid the following error: > Can't create handler inside thread that has not called > Looper.prepare() Refs 8a02e3c2f58cffc3fdd8c08b26aae04bbf9d5a97 Fixes #6151 --- .../main/java/com/genymobile/scrcpy/control/Controller.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index bfbee7dc..b4a8e3ca 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -114,9 +114,10 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { Ln.w("Input events are not supported for secondary displays before Android 10"); } + // Make sure the clipboard manager is always created from the main thread (even if clipboardAutosync is disabled) + ClipboardManager clipboardManager = ServiceManager.getClipboardManager(); if (clipboardAutosync) { // If control and autosync are enabled, synchronize Android clipboard to the computer automatically - ClipboardManager clipboardManager = ServiceManager.getClipboardManager(); if (clipboardManager != null) { clipboardManager.addPrimaryClipChangedListener(() -> { if (isSettingClipboard.get()) { From d74cfd5711b2ae2a12e38c0e7111e1af0f9af72c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 13 Jun 2025 09:40:20 +0200 Subject: [PATCH 2237/2244] Silence DiscouragedPrivateApi lint warning --- server/src/main/java/com/genymobile/scrcpy/Server.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index c1d8c1f2..46f3294f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -24,6 +24,7 @@ import com.genymobile.scrcpy.video.SurfaceCapture; import com.genymobile.scrcpy.video.SurfaceEncoder; import com.genymobile.scrcpy.video.VideoSource; +import android.annotation.SuppressLint; import android.os.Build; import android.os.Looper; @@ -199,6 +200,7 @@ public final class Server { Looper.prepare(); synchronized (Looper.class) { try { + @SuppressLint("DiscouragedPrivateApi") Field field = Looper.class.getDeclaredField("sMainLooper"); field.setAccessible(true); field.set(null, Looper.myLooper()); From 98d30288f78b0dd40ae1aa1b285c45f5769f49fc Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 17 Jun 2025 21:02:41 +0200 Subject: [PATCH 2238/2244] Prepare the main looper earlier The looper must be initialized before listing apps, to avoid the following error: > Can't create handler inside thread that has not called > Looper.prepare() Refs 283326b2f6fa3fdaeecc181f69a3a4bcd429c06a Fixes #6165 --- 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 46f3294f..a08c948c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -97,7 +97,6 @@ public final class Server { boolean audio = options.getAudio(); boolean sendDummyByte = options.getSendDummyByte(); - prepareMainLooper(); Workarounds.apply(); List asyncProcessors = new ArrayList<>(); @@ -230,6 +229,8 @@ public final class Server { Ln.e("Exception on thread " + t, e); }); + prepareMainLooper(); + Options options = Options.parse(args); Ln.disableSystemStreams(); From 9787fe5d261df8255e49b65f37e2d89bf1a129fa Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 19 Jun 2025 20:50:26 +0200 Subject: [PATCH 2239/2244] Preserve original scroll values in mouse event Clamp scroll values to [-1, 1] only for the SDK mouse. HID mouse implementations perform their own clamping to [-127, 127] (in hid_mouse.c). PR #6172 --- app/src/input_manager.c | 8 ++++---- app/src/mouse_sdk.c | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 635825c9..f7a787d1 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -897,11 +897,11 @@ sc_input_manager_process_mouse_wheel(struct sc_input_manager *im, struct sc_mouse_scroll_event evt = { .position = sc_input_manager_get_position(im, mouse_x, mouse_y), #if SDL_VERSION_ATLEAST(2, 0, 18) - .hscroll = CLAMP(event->preciseX, -1.0f, 1.0f), - .vscroll = CLAMP(event->preciseY, -1.0f, 1.0f), + .hscroll = event->preciseX, + .vscroll = event->preciseY, #else - .hscroll = CLAMP(event->x, -1, 1), - .vscroll = CLAMP(event->y, -1, 1), + .hscroll = event->x, + .vscroll = event->y, #endif .buttons_state = im->mouse_buttons_state, }; diff --git a/app/src/mouse_sdk.c b/app/src/mouse_sdk.c index 7eceffa7..1b05d02b 100644 --- a/app/src/mouse_sdk.c +++ b/app/src/mouse_sdk.c @@ -113,8 +113,8 @@ sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp, .type = SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT, .inject_scroll_event = { .position = event->position, - .hscroll = event->hscroll, - .vscroll = event->vscroll, + .hscroll = CLAMP(event->hscroll, -1, 1), + .vscroll = CLAMP(event->vscroll, -1, 1), .buttons = convert_mouse_buttons(event->buttons_state), }, }; From 7c8bdccbdc24b616c8d4ada861c424b3686912ea Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 20 Jun 2025 09:06:10 +0200 Subject: [PATCH 2240/2244] Extend value range for SDK mouse scrolling SDL precise scrolling can sometimes produce values greater than 1 or less than -1. On the wire, the value is encoded as a 16-bit fixed-point number. Previously, the range was interpreted as [-1, 1], using 1 bit for the integral part (the sign) and 15 bits for the fractional part. To support larger values, interpret the range as [-16, 16] instead, using 5 bits for the integral part and 11 bits for the fractional part (which is more than enough). PR #6172 --- app/src/control_msg.c | 12 ++++++++---- app/src/mouse_sdk.c | 4 ++-- app/tests/test_control_msg_serialize.c | 8 ++++---- .../scrcpy/control/ControlMessageReader.java | 5 +++-- .../scrcpy/control/ControlMessageReaderTest.java | 4 ++-- 5 files changed, 19 insertions(+), 14 deletions(-) diff --git a/app/src/control_msg.c b/app/src/control_msg.c index e78f0c57..e46c6165 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -127,10 +127,14 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) { return 32; case SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT: write_position(&buf[1], &msg->inject_scroll_event.position); - int16_t hscroll = - sc_float_to_i16fp(msg->inject_scroll_event.hscroll); - int16_t vscroll = - sc_float_to_i16fp(msg->inject_scroll_event.vscroll); + // Accept values in the range [-16, 16]. + // Normalize to [-1, 1] in order to use sc_float_to_i16fp(). + float hscroll_norm = msg->inject_scroll_event.hscroll / 16; + hscroll_norm = CLAMP(hscroll_norm, -1, 1); + float vscroll_norm = msg->inject_scroll_event.vscroll / 16; + vscroll_norm = CLAMP(vscroll_norm, -1, 1); + int16_t hscroll = sc_float_to_i16fp(hscroll_norm); + int16_t vscroll = sc_float_to_i16fp(vscroll_norm); sc_write16be(&buf[13], (uint16_t) hscroll); sc_write16be(&buf[15], (uint16_t) vscroll); sc_write32be(&buf[17], msg->inject_scroll_event.buttons); diff --git a/app/src/mouse_sdk.c b/app/src/mouse_sdk.c index 1b05d02b..7eceffa7 100644 --- a/app/src/mouse_sdk.c +++ b/app/src/mouse_sdk.c @@ -113,8 +113,8 @@ sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp, .type = SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT, .inject_scroll_event = { .position = event->position, - .hscroll = CLAMP(event->hscroll, -1, 1), - .vscroll = CLAMP(event->vscroll, -1, 1), + .hscroll = event->hscroll, + .vscroll = event->vscroll, .buttons = convert_mouse_buttons(event->buttons_state), }, }; diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c index af97182d..0d19919e 100644 --- a/app/tests/test_control_msg_serialize.c +++ b/app/tests/test_control_msg_serialize.c @@ -127,8 +127,8 @@ static void test_serialize_inject_scroll_event(void) { .height = 1920, }, }, - .hscroll = 1, - .vscroll = -1, + .hscroll = 16, + .vscroll = -16, .buttons = 1, }, }; @@ -141,8 +141,8 @@ static void test_serialize_inject_scroll_event(void) { SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT, 0x00, 0x00, 0x01, 0x04, 0x00, 0x00, 0x04, 0x02, // 260 1026 0x04, 0x38, 0x07, 0x80, // 1080 1920 - 0x7F, 0xFF, // 1 (float encoded as i16) - 0x80, 0x00, // -1 (float encoded as i16) + 0x7F, 0xFF, // 16 (float encoded as i16 in the range [-16, 16]) + 0x80, 0x00, // -16 (float encoded as i16 in the range [-16, 16]) 0x00, 0x00, 0x00, 0x01, // 1 }; assert(!memcmp(buf, expected, sizeof(expected))); diff --git a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java index e503ec61..830a7ec7 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java @@ -112,8 +112,9 @@ public class ControlMessageReader { private ControlMessage parseInjectScrollEvent() throws IOException { Position position = parsePosition(); - float hScroll = Binary.i16FixedPointToFloat(dis.readShort()); - float vScroll = Binary.i16FixedPointToFloat(dis.readShort()); + // Binary.i16FixedPointToFloat() decodes values assuming the full range is [-1, 1], but the actual range is [-16, 16]. + float hScroll = Binary.i16FixedPointToFloat(dis.readShort()) * 16; + float vScroll = Binary.i16FixedPointToFloat(dis.readShort()) * 16; int buttons = dis.readInt(); return ControlMessage.createInjectScrollEvent(position, hScroll, vScroll, buttons); } diff --git a/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java index 74df064f..0cc0a6b5 100644 --- a/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java @@ -125,7 +125,7 @@ public class ControlMessageReaderTest { dos.writeShort(1080); dos.writeShort(1920); dos.writeShort(0); // 0.0f encoded as i16 - dos.writeShort(0x8000); // -1.0f encoded as i16 + dos.writeShort(0x8000); // -16.0f encoded as i16 (the range is [-16, 16]) dos.writeInt(1); byte[] packet = bos.toByteArray(); @@ -139,7 +139,7 @@ public class ControlMessageReaderTest { Assert.assertEquals(1080, event.getPosition().getScreenSize().getWidth()); Assert.assertEquals(1920, event.getPosition().getScreenSize().getHeight()); Assert.assertEquals(0f, event.getHScroll(), 0f); - Assert.assertEquals(-1f, event.getVScroll(), 0f); + Assert.assertEquals(-16f, event.getVScroll(), 0f); Assert.assertEquals(1, event.getButtons()); Assert.assertEquals(-1, bis.read()); // EOS From fc75319bb291121116419c784a5fa507fd820eca Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 18 Jun 2025 18:26:13 +0200 Subject: [PATCH 2241/2244] Fix HID mouse support with SDL precise scrolling Over HID, only integral scroll values can be sent. When SDL precise scrolling is active, scroll events may include fractional values (e.g., 0.05), which are truncated to 0 in the HID event. To fix the problem, use the integral scroll value reported by SDL, which internally accumulates fractional deltas. Fixes #6156 PR #6172 --- app/src/hid/hid_mouse.c | 12 ++++++++---- app/src/hid/hid_mouse.h | 2 +- app/src/input_events.h | 2 ++ app/src/input_manager.c | 2 ++ app/src/uhid/mouse_uhid.c | 4 +++- app/src/usb/mouse_aoa.c | 4 +++- app/src/usb/screen_otg.c | 7 +++++++ 7 files changed, 26 insertions(+), 7 deletions(-) diff --git a/app/src/hid/hid_mouse.c b/app/src/hid/hid_mouse.c index 29cfc594..e1fff45b 100644 --- a/app/src/hid/hid_mouse.c +++ b/app/src/hid/hid_mouse.c @@ -175,19 +175,23 @@ sc_hid_mouse_generate_input_from_click(struct sc_hid_input *hid_input, data[3] = 0; // wheel coordinates only used for scrolling } -void +bool sc_hid_mouse_generate_input_from_scroll(struct sc_hid_input *hid_input, const struct sc_mouse_scroll_event *event) { + if (!event->vscroll_int) { + // Need a full integral value for HID + return false; + } + sc_hid_mouse_input_init(hid_input); uint8_t *data = hid_input->data; data[0] = 0; // buttons state irrelevant (and unknown) data[1] = 0; // no x motion data[2] = 0; // no y motion - // In practice, vscroll is always -1, 0 or 1, but in theory other values - // are possible - data[3] = CLAMP(event->vscroll, -127, 127); + data[3] = CLAMP(event->vscroll_int, -127, 127); // Horizontal scrolling ignored + return true; } void sc_hid_mouse_generate_open(struct sc_hid_open *hid_open) { diff --git a/app/src/hid/hid_mouse.h b/app/src/hid/hid_mouse.h index 06c61dd1..4ae4bfd4 100644 --- a/app/src/hid/hid_mouse.h +++ b/app/src/hid/hid_mouse.h @@ -22,7 +22,7 @@ void sc_hid_mouse_generate_input_from_click(struct sc_hid_input *hid_input, const struct sc_mouse_click_event *event); -void +bool sc_hid_mouse_generate_input_from_scroll(struct sc_hid_input *hid_input, const struct sc_mouse_scroll_event *event); diff --git a/app/src/input_events.h b/app/src/input_events.h index 0c022acc..1e34b50e 100644 --- a/app/src/input_events.h +++ b/app/src/input_events.h @@ -393,6 +393,8 @@ struct sc_mouse_scroll_event { struct sc_position position; float hscroll; float vscroll; + int32_t hscroll_int; + int32_t vscroll_int; uint8_t buttons_state; // bitwise-OR of sc_mouse_button values }; diff --git a/app/src/input_manager.c b/app/src/input_manager.c index f7a787d1..3e4dd0f3 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -903,6 +903,8 @@ sc_input_manager_process_mouse_wheel(struct sc_input_manager *im, .hscroll = event->x, .vscroll = event->y, #endif + .hscroll_int = event->x, + .vscroll_int = event->y, .buttons_state = im->mouse_buttons_state, }; diff --git a/app/src/uhid/mouse_uhid.c b/app/src/uhid/mouse_uhid.c index 7fed8383..869e48a4 100644 --- a/app/src/uhid/mouse_uhid.c +++ b/app/src/uhid/mouse_uhid.c @@ -55,7 +55,9 @@ sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp, struct sc_mouse_uhid *mouse = DOWNCAST(mp); struct sc_hid_input hid_input; - sc_hid_mouse_generate_input_from_scroll(&hid_input, event); + if (!sc_hid_mouse_generate_input_from_scroll(&hid_input, event)) { + return; + } sc_mouse_uhid_send_input(mouse, &hid_input, "mouse scroll"); } diff --git a/app/src/usb/mouse_aoa.c b/app/src/usb/mouse_aoa.c index b64e9b12..fd5fa5e0 100644 --- a/app/src/usb/mouse_aoa.c +++ b/app/src/usb/mouse_aoa.c @@ -42,7 +42,9 @@ sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp, struct sc_mouse_aoa *mouse = DOWNCAST(mp); struct sc_hid_input hid_input; - sc_hid_mouse_generate_input_from_scroll(&hid_input, event); + if (!sc_hid_mouse_generate_input_from_scroll(&hid_input, event)) { + return; + } if (!sc_aoa_push_input(mouse->aoa, &hid_input)) { LOGW("Could not push AOA HID input (mouse scroll)"); diff --git a/app/src/usb/screen_otg.c b/app/src/usb/screen_otg.c index 02edc3a3..5c580df9 100644 --- a/app/src/usb/screen_otg.c +++ b/app/src/usb/screen_otg.c @@ -164,8 +164,15 @@ sc_screen_otg_process_mouse_wheel(struct sc_screen_otg *screen, struct sc_mouse_scroll_event evt = { // .position not used for HID events +#if SDL_VERSION_ATLEAST(2, 0, 18) + .hscroll = event->preciseX, + .vscroll = event->preciseY, +#else .hscroll = event->x, .vscroll = event->y, +#endif + .hscroll_int = event->x, + .vscroll_int = event->y, .buttons_state = sc_mouse_buttons_state_from_sdl(sdl_buttons_state), }; From 4841fdd1eff58f313a62c539f31453eac3e21b62 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 18 Jun 2025 18:29:04 +0200 Subject: [PATCH 2242/2244] Add horizontal scrolling support for HID mouse PR #6172 --- app/src/hid/hid_mouse.c | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/app/src/hid/hid_mouse.c b/app/src/hid/hid_mouse.c index e1fff45b..33f0807e 100644 --- a/app/src/hid/hid_mouse.c +++ b/app/src/hid/hid_mouse.c @@ -3,8 +3,8 @@ #include // 1 byte for buttons + padding, 1 byte for X position, 1 byte for Y position, -// 1 byte for wheel motion -#define SC_HID_MOUSE_INPUT_SIZE 4 +// 1 byte for wheel motion, 1 byte for hozizontal scrolling +#define SC_HID_MOUSE_INPUT_SIZE 5 /** * Mouse descriptor from the specification: @@ -75,6 +75,21 @@ static const uint8_t SC_HID_MOUSE_REPORT_DESC[] = { // Input (Data, Variable, Relative): 3 position bytes (X, Y, Wheel) 0x81, 0x06, + // Usage Page (Consumer Page) + 0x05, 0x0C, + // Usage(AC Pan) + 0x0A, 0x38, 0x02, + // Logical Minimum (-127) + 0x15, 0x81, + // Logical Maximum (127) + 0x25, 0x7F, + // Report Size (8) + 0x75, 0x08, + // Report Count (1) + 0x95, 0x01, + // Input (Data, Variable, Relative): 1 byte (AC Pan) + 0x81, 0x06, + // End Collection 0xC0, @@ -160,7 +175,8 @@ sc_hid_mouse_generate_input_from_motion(struct sc_hid_input *hid_input, data[0] = sc_hid_buttons_from_buttons_state(event->buttons_state); data[1] = CLAMP(event->xrel, -127, 127); data[2] = CLAMP(event->yrel, -127, 127); - data[3] = 0; // wheel coordinates only used for scrolling + data[3] = 0; // no vertical scrolling + data[4] = 0; // no horizontal scrolling } void @@ -172,13 +188,14 @@ sc_hid_mouse_generate_input_from_click(struct sc_hid_input *hid_input, data[0] = sc_hid_buttons_from_buttons_state(event->buttons_state); data[1] = 0; // no x motion data[2] = 0; // no y motion - data[3] = 0; // wheel coordinates only used for scrolling + data[3] = 0; // no vertical scrolling + data[4] = 0; // no horizontal scrolling } bool sc_hid_mouse_generate_input_from_scroll(struct sc_hid_input *hid_input, const struct sc_mouse_scroll_event *event) { - if (!event->vscroll_int) { + if (!event->vscroll_int && !event->hscroll_int) { // Need a full integral value for HID return false; } @@ -190,7 +207,7 @@ sc_hid_mouse_generate_input_from_scroll(struct sc_hid_input *hid_input, data[1] = 0; // no x motion data[2] = 0; // no y motion data[3] = CLAMP(event->vscroll_int, -127, 127); - // Horizontal scrolling ignored + data[4] = CLAMP(event->hscroll_int, -127, 127); return true; } From 5b18ce0d2e91fd9875b3fe3b10a2c5dcb4399cd1 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 20 Jun 2025 19:42:40 +0200 Subject: [PATCH 2243/2244] Bump version to 3.3.1 --- app/scrcpy-windows.rc | 2 +- meson.build | 2 +- server/build.gradle | 4 ++-- server/build_without_gradle.sh | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/scrcpy-windows.rc b/app/scrcpy-windows.rc index 45f1960c..9c5374ae 100644 --- a/app/scrcpy-windows.rc +++ b/app/scrcpy-windows.rc @@ -13,7 +13,7 @@ BEGIN VALUE "LegalCopyright", "Romain Vimont, Genymobile" VALUE "OriginalFilename", "scrcpy.exe" VALUE "ProductName", "scrcpy" - VALUE "ProductVersion", "3.3" + VALUE "ProductVersion", "3.3.1" END END BLOCK "VarFileInfo" diff --git a/meson.build b/meson.build index 1e9a5729..d991d672 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('scrcpy', 'c', - version: '3.3', + version: '3.3.1', meson_version: '>= 0.49', default_options: [ 'c_std=c11', diff --git a/server/build.gradle b/server/build.gradle index 059a6f30..31092b12 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -7,8 +7,8 @@ android { applicationId "com.genymobile.scrcpy" minSdkVersion 21 targetSdkVersion 35 - versionCode 30300 - versionName "3.3" + versionCode 30301 + versionName "3.3.1" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index 5b35e3ec..193a9902 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=3.3 +SCRCPY_VERSION_NAME=3.3.1 PLATFORM=${ANDROID_PLATFORM:-35} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-35.0.0} From f01231dff8294fe2c99045a4f9a14b233a71bb86 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 20 Jun 2025 20:14:42 +0200 Subject: [PATCH 2244/2244] Update links to 3.3.1 --- README.md | 2 +- doc/build.md | 6 +++--- doc/linux.md | 6 +++--- doc/macos.md | 12 ++++++------ doc/windows.md | 12 ++++++------ install_release.sh | 4 ++-- 6 files changed, 21 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index dc00ac22..d886d23c 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ source for the project. Do not download releases from random websites, even if their name contains `scrcpy`.** -# scrcpy (v3.3) +# scrcpy (v3.3.1) scrcpy diff --git a/doc/build.md b/doc/build.md index c915e367..7f76b4fd 100644 --- a/doc/build.md +++ b/doc/build.md @@ -233,10 +233,10 @@ install` must be run as root)._ #### Option 2: Use prebuilt server - - [`scrcpy-server-v3.3`][direct-scrcpy-server] - SHA-256: `351cb2edc7e4c2c75f09a7933fdabcf137be52e2602df154f24ec02db46e9e51` + - [`scrcpy-server-v3.3.1`][direct-scrcpy-server] + SHA-256: `a0f70b20aa4998fbf658c94118cd6c8dab6abbb0647a3bdab344d70bc1ebcbb8` -[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v3.3/scrcpy-server-v3.3 +[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.1/scrcpy-server-v3.3.1 Download the prebuilt server somewhere, and specify its path during the Meson configuration: diff --git a/doc/linux.md b/doc/linux.md index 5cfd6e4e..be433df4 100644 --- a/doc/linux.md +++ b/doc/linux.md @@ -6,11 +6,11 @@ Download a static build of the [latest release]: - - [`scrcpy-linux-x86_64-v3.3.tar.gz`][direct-linux-x86_64] (x86_64) - SHA-256: `a0abf37003c3c47a53c1b2a12420296a2b0ee323cf3610fd6fbf9d9bab9d99f3` + - [`scrcpy-linux-x86_64-v3.3.1.tar.gz`][direct-linux-x86_64] (x86_64) + SHA-256: `bbfe54c6b178adafeaffbbfbbc1548a74486553170c63e63bdd41863ad123422` [latest release]: https://github.com/Genymobile/scrcpy/releases/latest -[direct-linux-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3/scrcpy-linux-x86_64-v3.3.tar.gz +[direct-linux-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.1/scrcpy-linux-x86_64-v3.3.1.tar.gz and extract it. diff --git a/doc/macos.md b/doc/macos.md index 73a982f6..f6b01c30 100644 --- a/doc/macos.md +++ b/doc/macos.md @@ -6,15 +6,15 @@ Download a static build of the [latest release]: - - [`scrcpy-macos-aarch64-v3.3.tar.gz`][direct-macos-aarch64] (aarch64) - SHA-256: `7a4cdaeb8ba74593edda278c000ddedc8d70a51263a80b16a6345475d42ac21e` + - [`scrcpy-macos-aarch64-v3.3.1.tar.gz`][direct-macos-aarch64] (aarch64) + SHA-256: `907b925900ebd8499c1e47acc9689a95bd3a6f9930eb1d7bdfbca8375ae4f139` - - [`scrcpy-macos-x86_64-v3.3.tar.gz`][direct-macos-x86_64] (x86_64) - SHA-256: `bb3c13aac166b92539371883a8781aa861a7cd18e3e6077e570ab7a1f562f774` + - [`scrcpy-macos-x86_64-v3.3.1.tar.gz`][direct-macos-x86_64] (x86_64) + SHA-256: `69772491dad718eea82fc65c8e89febff7d41c4ce6faff02f4789a588d10fd7d` [latest release]: https://github.com/Genymobile/scrcpy/releases/latest -[direct-macos-aarch64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3/scrcpy-macos-aarch64-v3.3.tar.gz -[direct-macos-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3/scrcpy-macos-x86_64-v3.3.tar.gz +[direct-macos-aarch64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.1/scrcpy-macos-aarch64-v3.3.1.tar.gz +[direct-macos-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.1/scrcpy-macos-x86_64-v3.3.1.tar.gz and extract it. diff --git a/doc/windows.md b/doc/windows.md index 7935461d..8fa1921f 100644 --- a/doc/windows.md +++ b/doc/windows.md @@ -6,14 +6,14 @@ Download the [latest release]: - - [`scrcpy-win64-v3.3.zip`][direct-win64] (64-bit) - SHA-256: `a120cb4be7cde2891af38e83d2008173a0b6b6b5e344b2dfe668d0f892999933` - - [`scrcpy-win32-v3.3.zip`][direct-win32] (32-bit) - SHA-256: `e409ab83f8c57bd6ac741d652635cab7699fcf3d384e233833872f117b993ca6` + - [`scrcpy-win64-v3.3.1.zip`][direct-win64] (64-bit) + SHA-256: `4fcad494772a3ae5de9a133149f8856d2fc429b41795f7cf7c754e0c1bb6fbc0` + - [`scrcpy-win32-v3.3.1.zip`][direct-win32] (32-bit) + SHA-256: `ccdf1b4f5d19dfe760446a107e55b0a010a00e097d46533a161499c9333a20a6` [latest release]: https://github.com/Genymobile/scrcpy/releases/latest -[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3/scrcpy-win64-v3.3.zip -[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v3.3/scrcpy-win32-v3.3.zip +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.1/scrcpy-win64-v3.3.1.zip +[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.1/scrcpy-win32-v3.3.1.zip and extract it. diff --git a/install_release.sh b/install_release.sh index aabe9873..d960932b 100755 --- a/install_release.sh +++ b/install_release.sh @@ -2,8 +2,8 @@ set -e BUILDDIR=build-auto -PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v3.3/scrcpy-server-v3.3 -PREBUILT_SERVER_SHA256=351cb2edc7e4c2c75f09a7933fdabcf137be52e2602df154f24ec02db46e9e51 +PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v3.3.1/scrcpy-server-v3.3.1 +PREBUILT_SERVER_SHA256=a0f70b20aa4998fbf658c94118cd6c8dab6abbb0647a3bdab344d70bc1ebcbb8 echo "[scrcpy] Downloading prebuilt server..." wget "$PREBUILT_SERVER_URL" -O scrcpy-server